UNPKG

@graphql-yoga/plugin-jwt

Version:
203 lines (202 loc) • 8.23 kB
import { GraphQLError } from 'graphql'; import jsonwebtoken from 'jsonwebtoken'; import { handleMaybePromise } from '@whatwg-node/promise-helpers'; import { normalizeConfig } from './config.js'; import { badRequestError, unauthorizedError } from './utils.js'; export function useJWT(options) { let logger; const normalizedOptions = normalizeConfig(options); const payloadByContext = new WeakMap(); const payloadByRequest = new WeakMap(); const validatedRequestAndContextSet = new WeakSet(); const lookupToken = (payload) => { const iterator = normalizedOptions.tokenLookupLocations[Symbol.iterator](); function iterate() { const { done, value } = iterator.next(); if (done) { return null; } if (!value) { return iterate(); } return handleMaybePromise(() => value(payload), result => { if (result?.token) { return result; } return iterate(); }); } return iterate(); }; const getSigningKey = (kid) => { const iterator = normalizedOptions.signingKeyProviders[Symbol.iterator](); function iterate() { const { done, value } = iterator.next(); if (done) { return null; } if (!value) { return iterate(); } return handleMaybePromise(() => value(kid), key => { if (key) { return key; } return iterate(); }, e => { logger.error(`Failed to fetch signing key from signing provided:`, e); return iterate(); }); } return iterate(); }; function handleError(e) { // User-facing errors should be handled based on the configuration. // These errors are handled based on the value of "reject.invalidToken" config. if (e instanceof GraphQLError) { if (normalizedOptions.reject.invalidToken) { throw e; } return; } // Server/internal errors should be thrown, so they can be handled by the error handler and be masked. throw e; } const lookupAndValidate = (payload) => { // Mark the context and request as validated, so we don't process them again. if (payload.serverContext) { if (validatedRequestAndContextSet.has(payload.serverContext)) { return; } validatedRequestAndContextSet.add(payload.serverContext); } if (payload.request) { if (validatedRequestAndContextSet.has(payload.request)) { return; } validatedRequestAndContextSet.add(payload.request); } // Try to find token in request, and reject the request if needed. return handleMaybePromise(() => lookupToken(payload), lookupResult => { if (!lookupResult) { // If token is missing, we can reject the request based on the configuration. if (normalizedOptions.reject.missingToken) { logger.debug(`Token is missing in incoming HTTP request, JWT plugin failed to locate.`); throw unauthorizedError(`Unauthenticated`); } return; } // Decode the token first, in order to get the key id to use. let decodedToken; try { decodedToken = jsonwebtoken.decode(lookupResult.token, { complete: true }); } catch (e) { logger.warn(`Failed to decode JWT authentication token: `, e); throw badRequestError(`Invalid authentication token provided`); } if (!decodedToken) { logger.warn(`Failed to extract payload from incoming token, please make sure the token is a valid JWT.`); throw badRequestError(`Invalid authentication token provided`); } // Fetch the signing key based on the key id. return handleMaybePromise(() => getSigningKey(decodedToken?.header.kid), signingKey => { if (!signingKey) { logger.warn(`Signing key is not available for the key id: ${decodedToken?.header.kid}. Please make sure signing key providers are configured correctly.`); throw Error(`Authentication is not available at the moment.`); } // Verify the token with the signing key. return handleMaybePromise(() => verify(logger, lookupResult.token, signingKey, normalizedOptions.tokenVerification), verified => { if (!verified) { logger.debug(`Token failed to verify, JWT plugin failed to authenticate.`); throw unauthorizedError(`Unauthenticated`); } if (verified) { // Link the verified payload with the request (see `onContextBuilding` for the reading part) const pluginPayload = { payload: verified, token: { value: lookupResult.token, prefix: lookupResult.prefix, }, }; if (payload.request) { payloadByRequest.set(payload.request, pluginPayload); } else { payloadByContext.set(payload.serverContext, pluginPayload); } } }, handleError); }, handleError); }, handleError); }; function ensureContext({ request, url, serverContext, extendContext, }) { if (normalizedOptions.extendContextFieldName === null) { return; } if (serverContext[normalizedOptions.extendContextFieldName]) { return; } // Ensure the request has been validated before extending the context. return handleMaybePromise(() => lookupAndValidate({ request, url, serverContext, }), () => { // Then check the result const result = request ? payloadByRequest.get(request) : payloadByContext.get(serverContext); if (result && normalizedOptions.extendContextFieldName) { extendContext({ [normalizedOptions.extendContextFieldName]: { payload: result.payload, token: result.token, }, }); } }); } let fetchAPI; return { onYogaInit({ yoga }) { logger = yoga.logger; fetchAPI = yoga.fetchAPI; }, onRequestParse({ request, url, serverContext }) { return ensureContext({ request, url, serverContext, extendContext: newContext => { Object.assign(serverContext, newContext); }, }); }, onContextBuilding({ context, extendContext }) { const request = context.request; return ensureContext({ request, get url() { return request && new fetchAPI.URL(request.url); }, serverContext: context, extendContext, }); }, }; } function verify(logger, token, signingKey, options) { return new Promise((resolve, reject) => { jsonwebtoken.verify(token, signingKey, options, (err, result) => { if (err) { logger.warn(`Failed to verify authentication token: `, err); reject(unauthorizedError('Unauthenticated')); } else { resolve(result); } }); }); }