import { Injectable, CanActivate, OnApplicationBootstrap, ExecutionContext, Inject, ForbiddenException, } 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'; import { ApiKeyService } from 'src/modules/api-key/api-key.service'; @Injectable() export class AuthenticationGuard implements CanActivate, OnApplicationBootstrap { logger: DadosferaLogger; pems: Map; constructor( @Inject(DadosferaLogger) dadosferaLogger: DadosferaLogger, private reflector: Reflector, private authClient: AuthClientService, private apiKeyService: ApiKeyService ) { 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); }); } async canActivate(ctx: ExecutionContext): Promise { const authFunctions = this.reflector.getAllAndMerge< AuthenticationFunction[] >(AUTH_FUNCTION_KEY, [ctx.getClass(), ctx.getHandler()]); const mustBeAuthenticated = authFunctions.length > 0; if (!mustBeAuthenticated) { // no need to be authenticated return true; } const request = ctx.switchToHttp().getRequest(); const apiKey = request.get('X-api-key'); if (apiKey) { const { api_key } = await this.apiKeyService.get(apiKey); request.user = { user_id: api_key.user_id, username: api_key.username, permissions: api_key.permissions, customer_id: api_key.customer_id, customer_name: api_key.customer_name, customer_tier: api_key.customer_tier, customer_modules: api_key.customer_modules, access_token: apiKey, }; return true; } const accessToken = this.validateToken(request, mustBeAuthenticated); 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; } // Bloquear outros customer de usar o maestor dedicado const DEDICATED_PROXY = process.env.DEDICATED_PROXY || ''; if (DEDICATED_PROXY !== '' && DEDICATED_PROXY !== accessTokenPayload.customer_id) { throw new ErrorBuilder(ErrorCodes.AUTH.FORBIDDEN); } // Bloquear o customer de acesso o maestro publico const hasNetworkPolicyModule = accessTokenPayload.customer_modules.includes('network-policy'); if (hasNetworkPolicyModule && DEDICATED_PROXY === '') { throw new ForbiddenException(ErrorCodes.AUTH.FORBIDDEN); } 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, customer_modules: accessTokenPayload.customer_modules, 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; } }