Files
maestro/src/authentication/authentication.guard.ts
T
2023-02-17 09:55:10 -03:00

137 lines
3.9 KiB
TypeScript

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<string, string>;
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;
}
}