lbx-jwt
Version:
Provides JWT authentication for loopback applications. Includes storing roles inside tokens and handling refreshing. Built-in reuse detection.
103 lines (92 loc) • 4.69 kB
text/typescript
import { AuthenticationBindings, AuthenticationMetadata, AuthenticationStrategy, TokenService } from '@loopback/authentication';
import { inject } from '@loopback/core';
import { HttpErrors, Request } from '@loopback/rest';
import { TwoFactorService } from './two-factor.service';
import { LbxJwtBindings } from '../keys';
import { BaseUser, BaseUserProfile } from '../models';
import { BaseUserRepository } from '../repositories';
/**
* The jwt authentication strategy.
*/
export class JwtAuthenticationStrategy implements AuthenticationStrategy {
// eslint-disable-next-line jsdoc/require-jsdoc
readonly name: string = 'jwt';
constructor(
(LbxJwtBindings.ACCESS_TOKEN_SERVICE)
private readonly accessTokenService: TokenService,
(AuthenticationBindings.METADATA)
private readonly metadataArray: AuthenticationMetadata[],
(LbxJwtBindings.BASE_USER_REPOSITORY)
private readonly baseUserRepository: BaseUserRepository<string>,
(LbxJwtBindings.FORCE_TWO_FACTOR)
private readonly forceTwoFactor: boolean,
(LbxJwtBindings.FORCE_TWO_FACTOR_ALLOWED_ROUTES)
private readonly forceTwoFactorAllowedRoutes: string[],
(LbxJwtBindings.TWO_FACTOR_SERVICE)
private readonly twoFactorService: TwoFactorService<string>
) {}
// eslint-disable-next-line jsdoc/require-jsdoc
async authenticate(request: Request): Promise<BaseUserProfile<string> | undefined> {
const token: string = this.extractTokenFromRequest(request);
const userProfile: BaseUserProfile<string> = await this.accessTokenService.verifyToken(token) as BaseUserProfile<string>;
const user: BaseUser<string> = await this.baseUserRepository.findById(userProfile.id);
// eslint-disable-next-line typescript/strict-boolean-expressions
if (user.requiresPasswordChange) {
throw new HttpErrors.BadRequest('This account needs to change his password before it can access this route.');
}
await this.validate2FA(user, request);
return userProfile;
}
/**
* Checks if the request requires 2fa and validates accordingly.
* @param user - The currently logged in user.
* @param request - The request, is used to extract the two factor code from the custom header.
*/
protected async validate2FA(user: BaseUser<string>, request: Request): Promise<void> {
if (
this.forceTwoFactor && user.twoFactorEnabled != true
&& !this.forceTwoFactorAllowedRoutes.find(r => request.url === r || new URL(request.url).pathname === r)
) {
throw new HttpErrors.BadRequest('This account needs to setup two factor authentication before it can access this route.');
}
const metadata: AuthenticationMetadata | undefined = this.metadataArray.find(m => m.strategy === this.name);
// eslint-disable-next-line typescript/strict-boolean-expressions
if (!metadata?.options?.['require2fa']) {
return;
}
// eslint-disable-next-line typescript/strict-boolean-expressions
if (!this.forceTwoFactor && !user.twoFactorEnabled) {
return;
}
const code: string = this.twoFactorService.extractCodeFromRequest(request);
await this.twoFactorService.validateCode(user.id, code);
}
/**
* Extracts the token from the given request.
* @param request - The request to get the token from.
* @returns The found token. An error otherwise.
* @throws An Http-Unauthorized-Error when no token could be found.
*/
protected extractTokenFromRequest(request: Request): string {
if (!request.headers.authorization) {
throw new HttpErrors.Unauthorized('Authorization header not found.');
}
// for example : Bearer xxx.yyy.zzz
const authHeaderValue: string = request.headers.authorization;
if (!authHeaderValue.startsWith('Bearer')) {
throw new HttpErrors.Unauthorized(
'Authorization header is not of type \'Bearer\'.'
);
}
//split the string into 2 parts : 'Bearer ' and the `xxx.yyy.zzz`
const parts: string[] = authHeaderValue.split(' ');
if (parts.length !== 2) {
throw new HttpErrors.Unauthorized(
// eslint-disable-next-line stylistic/max-len
'Authorization header value has too many parts. It must follow the pattern: \'Bearer xx.yy.zz\' where xx.yy.zz is a valid JWT token.'
);
}
const token: string = parts[1];
return token;
}
}