UNPKG

@foal/jwt

Version:

Authentication with JWT for FoalTS

181 lines (180 loc) 7.06 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.JWT = JWT; // 3p const core_1 = require("@foal/core"); const jsonwebtoken_1 = require("jsonwebtoken"); // FoalTS const constants_1 = require("./constants"); const check_and_convert_user_id_type_1 = require("./check-and-convert-user-id-type"); const get_jwt_from_request_1 = require("./get-jwt-from-request"); const core_2 = require("../core"); const invalid_token_error_1 = require("./invalid-token.error"); const utils_1 = require("./utils"); class InvalidTokenResponse extends core_1.HttpResponseUnauthorized { constructor(description) { super({ code: 'invalid_token', description }); this.setHeader('WWW-Authenticate', `error="invalid_token", error_description="${description}"`); } } class InvalidRequestResponse extends core_1.HttpResponseBadRequest { constructor(description) { super({ code: 'invalid_request', description }); } } /** * Sub-function used by JWTRequired and JWTOptional to avoid code duplication. * * @export * @param {boolean} required * @param {JWTOptions} options * @param {VerifyOptions} verifyOptions * @returns {HookDecorator} */ function JWT(required, options, verifyOptions) { async function hook(ctx, services) { let token; try { token = (0, get_jwt_from_request_1.getJwtFromRequest)(ctx.request, options.cookie ? 'token-in-cookie' : 'token-in-header', required); } catch (error) { if (error instanceof get_jwt_from_request_1.RequestValidationError) { return new InvalidRequestResponse(error.message); } // TODO: test this. throw error; } if (!token) { return; } if (options.blackList && await options.blackList(token)) { return new InvalidTokenResponse('jwt revoked'); } const parts = token.split('.'); if (parts.length !== 3) { return new InvalidTokenResponse('jwt malformed'); } let decoded; try { decoded = (0, jsonwebtoken_1.decode)(token, { complete: true }); } catch (error) { return new InvalidTokenResponse(error.message); } if (!decoded) { return new InvalidTokenResponse('invalid token'); } let secretOrPublicKey; if (options.secretOrPublicKey) { try { secretOrPublicKey = await options.secretOrPublicKey(decoded.header, decoded.payload); } catch (error) { if ((0, invalid_token_error_1.isInvalidTokenError)(error)) { return new InvalidTokenResponse(error.message); } throw error; } } else { secretOrPublicKey = (0, core_2.getSecretOrPublicKey)(); } let payload; try { payload = await new Promise((resolve, reject) => { (0, jsonwebtoken_1.verify)(token, secretOrPublicKey, verifyOptions, (err, value) => { if (err) { reject(err); } else { resolve(value); } }); }); } catch (error) { return new InvalidTokenResponse(error.message); } /* Verify CSRF token */ if ((0, utils_1.shouldVerifyCsrfToken)(ctx.request, options)) { const expectedCsrftoken = (0, utils_1.getCsrfTokenFromCookie)(ctx.request); if (!expectedCsrftoken) { return new core_1.HttpResponseForbidden('CSRF token missing or incorrect.'); } try { const csrfPayload = await new Promise((resolve, reject) => { (0, jsonwebtoken_1.verify)(expectedCsrftoken, secretOrPublicKey, (err, value) => { if (err) { reject(err); } else { resolve(value); } }); }); if (csrfPayload.sub !== payload.sub) { return new core_1.HttpResponseForbidden('CSRF token missing or incorrect.'); } } catch { return new core_1.HttpResponseForbidden('CSRF token missing or incorrect.'); } const actualCsrfToken = (0, utils_1.getCsrfTokenFromRequest)(ctx.request); if (actualCsrfToken !== expectedCsrftoken) { return new core_1.HttpResponseForbidden('CSRF token missing or incorrect.'); } } /* Set ctx.user */ if (!options.user) { ctx.user = payload; return; } if (typeof payload.sub !== 'string') { return new InvalidTokenResponse('The token must include a subject which is the id of the user.'); } const userId = (0, check_and_convert_user_id_type_1.checkAndConvertUserIdType)(payload.sub, options.userIdType); const logger = services.get(core_1.Logger); logger.addLogContext({ userId }); const user = await options.user(userId, services); if (!user) { return new InvalidTokenResponse('The token subject does not match any user.'); } ctx.user = user; } const openapi = [ required ? (0, core_1.ApiResponse)(401, { description: 'JWT is missing or invalid.' }) : (0, core_1.ApiResponse)(401, { description: 'JWT is invalid.' }) ]; if (options.cookie) { const securityScheme = { in: 'cookie', name: core_1.Config.get('settings.jwt.cookie.name', 'string', constants_1.JWT_DEFAULT_COOKIE_NAME), type: 'apiKey', }; openapi.push((0, core_1.ApiDefineSecurityScheme)('cookieAuth', securityScheme)); if (required) { openapi.push((0, core_1.ApiSecurityRequirement)({ cookieAuth: [] })); } if (core_1.Config.get('settings.jwt.csrf.enabled', 'boolean', false)) { openapi.push((0, core_1.ApiParameter)({ description: 'CSRF token', in: 'cookie', name: core_1.Config.get('settings.jwt.csrf.cookie.name', 'string', constants_1.JWT_DEFAULT_CSRF_COOKIE_NAME), })); openapi.push((0, core_1.ApiResponse)(403, { description: 'CSRF token is missing or incorrect.' })); } } else { const securityScheme = { bearerFormat: 'JWT', scheme: 'bearer', type: 'http', }; openapi.push((0, core_1.ApiDefineSecurityScheme)('bearerAuth', securityScheme)); if (required) { openapi.push((0, core_1.ApiSecurityRequirement)({ bearerAuth: [] })); } } return (0, core_1.Hook)(hook, openapi, { openapi: options.openapi }); }