@foal/jwt
Version:
Authentication with JWT for FoalTS
181 lines (180 loc) • 7.06 kB
JavaScript
;
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 });
}