Files
maestro/src/authentication/authentication.guard.ts
T

163 lines
4.5 KiB
TypeScript

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<string, string>;
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<Permission[]>(
PERMISSIONS_KEY,
[ctx.getHandler(), ctx.getClass()],
);
const customAuthenticationFunction =
this.reflector.getAllAndOverride<AuthenticationFunction>(
CUSTOM_AUTHENTICATION_FUNCTION_KEY,
[ctx.getHandler(), ctx.getClass()],
);
const mustBeAuthenticated = this.reflector.getAllAndOverride<boolean>(
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),
);
}
}