import { Injectable, CanActivate, OnApplicationBootstrap, ExecutionContext, Inject, } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import assert from 'assert'; import jwt from 'jsonwebtoken'; import { DadosferaLogger } from '@dadosfera/dadosfera-logs'; import { AuthClientService } from '../modules/auth/auth.service'; import { AuthenticationFunction, AUTH_FUNCTION_KEY, } from '../decorators/authentication.decorator'; import { RequestUser } from '../decorators/user.decorator'; import ErrorBuilder from '../utils/ErrorBuilder'; import ErrorCodes from '../utils/errorCodes'; @Injectable() export class AuthenticationGuard implements CanActivate, OnApplicationBootstrap { logger: DadosferaLogger; pems: Map; constructor( @Inject(DadosferaLogger) dadosferaLogger: DadosferaLogger, private reflector: Reflector, private authClient: AuthClientService, ) { this.pems = new Map(); this.logger = dadosferaLogger.logger; } async onApplicationBootstrap() { return this.loadDucJWKS(); } private async loadDucJWKS() { const { keys } = await this.authClient.getPublicKeys(); keys.forEach((key) => { this.pems.set(key.kid, key.pem); }); } canActivate(ctx: ExecutionContext): boolean { const authFunctions = this.reflector.getAllAndMerge< AuthenticationFunction[] >(AUTH_FUNCTION_KEY, [ctx.getClass(), ctx.getHandler()]); const mustBeAuthenticated = authFunctions.length > 0; const request = ctx.switchToHttp().getRequest(); const accessToken = this.validateToken(request, mustBeAuthenticated); if (!mustBeAuthenticated) { // no need to be authenticated return true; } if (!accessToken) { // couldn't load valid token throw new ErrorBuilder(ErrorCodes.AUTH.UNAUTHORIZED); } // every authentication function must return true to authenticate if (!authFunctions.every((func) => func(request, accessToken))) { throw new ErrorBuilder(ErrorCodes.AUTH.FORBIDDEN); } return true; } private validateToken( request, mustBeAuthenticated: boolean, ): RequestUser | false { const accessToken = request.get('Authorization'); let accessTokenPayload: RequestUser; // 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.error('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, access_token: accessToken, }; // 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; } }