UNPKG

@graphql-yoga/plugin-jwt

Version:
207 lines (206 loc) • 8.63 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.useJWT = useJWT; const tslib_1 = require("tslib"); const graphql_1 = require("graphql"); const jsonwebtoken_1 = tslib_1.__importDefault(require("jsonwebtoken")); const promise_helpers_1 = require("@whatwg-node/promise-helpers"); const config_js_1 = require("./config.js"); const utils_js_1 = require("./utils.js"); function useJWT(options) { let logger; const normalizedOptions = (0, config_js_1.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 (0, promise_helpers_1.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 (0, promise_helpers_1.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 graphql_1.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 (0, promise_helpers_1.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 (0, utils_js_1.unauthorizedError)(`Unauthenticated`); } return; } // Decode the token first, in order to get the key id to use. let decodedToken; try { decodedToken = jsonwebtoken_1.default.decode(lookupResult.token, { complete: true }); } catch (e) { logger.warn(`Failed to decode JWT authentication token: `, e); throw (0, utils_js_1.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 (0, utils_js_1.badRequestError)(`Invalid authentication token provided`); } // Fetch the signing key based on the key id. return (0, promise_helpers_1.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 (0, promise_helpers_1.handleMaybePromise)(() => verify(logger, lookupResult.token, signingKey, normalizedOptions.tokenVerification), verified => { if (!verified) { logger.debug(`Token failed to verify, JWT plugin failed to authenticate.`); throw (0, utils_js_1.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 (0, promise_helpers_1.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_1.default.verify(token, signingKey, options, (err, result) => { if (err) { logger.warn(`Failed to verify authentication token: `, err); reject((0, utils_js_1.unauthorizedError)('Unauthenticated')); } else { resolve(result); } }); }); }