import { Injectable, CanActivate, OnApplicationBootstrap, ExecutionContext, Logger, } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import assert from 'assert'; import jwt from 'jsonwebtoken'; import { AuthClientService } from '../clients/auth/client.service'; import { Permission } from './permissions.enum'; import { AuthenticationFunction, PERMISSIONS_KEY, CUSTOM_AUTHENTICATION_FUNCTION_KEY, MUST_BE_AUTHENTICATED_KEY, } from './authentication.decorator'; import ErrorBuilder from '../utils/ErrorBuilder'; import ErrorCodes from '../utils/errorCodes'; @Injectable() export class AuthenticationGuard implements CanActivate, OnApplicationBootstrap { private readonly logger = new Logger(AuthenticationGuard.name); pems: Map; constructor( private reflector: Reflector, private authClient: AuthClientService, ) { this.pems = new Map(); } async onApplicationBootstrap() { return this.loadDucJWKS(); } async loadDucJWKS() { const { keys } = await this.authClient.getPublicKeys(); keys.forEach((key) => { this.pems.set(key.kid, key.pem); }); } canActivate(ctx: ExecutionContext): boolean { const requiredPermissions = this.reflector.getAllAndOverride( PERMISSIONS_KEY, [ctx.getHandler(), ctx.getClass()], ); const customAuthenticationFunction = this.reflector.getAllAndOverride( CUSTOM_AUTHENTICATION_FUNCTION_KEY, [ctx.getHandler(), ctx.getClass()], ); const mustBeAuthenticated = this.reflector.getAllAndOverride( MUST_BE_AUTHENTICATED_KEY, [ctx.getHandler(), ctx.getClass()], ); const request = ctx.switchToHttp().getRequest(); const accessToken = this.verifyToken(request, mustBeAuthenticated); if (!accessToken) { // couldn't load valid token if (!mustBeAuthenticated) { // no need to be authenticated return true; } throw new ErrorBuilder(ErrorCodes.AUTH.UNAUTHORIZED); } if ( typeof customAuthenticationFunction === 'function' && !customAuthenticationFunction(request, request.user) ) { // custom authentication function forbidden this request throw new ErrorBuilder(ErrorCodes.AUTH.FORBIDDEN); } if ( Array.isArray(requiredPermissions) && requiredPermissions.length > 0 && !this.matchPermissions(requiredPermissions, accessToken.permissions) ) { // couldn't match permissions throw new ErrorBuilder(ErrorCodes.AUTH.FORBIDDEN); } return true; } private verifyToken(request, mustBeAuthenticated: boolean) { const accessToken = request.get('Authorization'); let accessTokenPayload; // If the user isn't authenticated, an error will occurr anywhere here. // Fancy error avoidance isn't performed by purpose, such as avoiding to access null values. try { const accessTokenDecoded: any = jwt.decode(accessToken, { complete: true, }); /*assert( accessTokenDecoded.payload.iss === 'duc', 'token should be issued by DUC', );*/ assert( accessTokenDecoded.payload.token_use === 'access', 'should be an access token', ); const { kid } = accessTokenDecoded.header; const pemValue: string = this.pems.get(kid); jwt.verify(accessToken, pemValue); accessTokenPayload = accessTokenDecoded.payload; } catch (err) { // log errors if authentication is required if (mustBeAuthenticated) { this.logger.log('Unable to verify duc access token\n' + err.stack); } return false; } request.accessTokenPayload = accessTokenPayload; request.user = { user_id: accessTokenPayload.user_id, username: accessTokenPayload.username, permissions: accessTokenPayload.permissions, customer_id: accessTokenPayload.customer_id, customer_name: accessTokenPayload.customer_name, customer_tier: accessTokenPayload.customer_tier, }; // TODO: for backwards compatibility. remove in the future request.body.info = { user_id: accessTokenPayload.user_id, customer_id: accessTokenPayload.customer_id, customer: accessTokenPayload.customer_name, customer_tier: accessTokenPayload.customer_tier, }; return accessTokenPayload; } private matchPermissions( requiredPermissions: Permission[], userPermissions: number[], ) { return requiredPermissions.some((permission) => userPermissions.includes(permission.seqid), ); } }