Created
May 30, 2023 15:47
-
-
Save herveGuigoz/a0094759bc8f9ade9e7af23e48672ef8 to your computer and use it in GitHub Desktop.
Revisions
-
herveGuigoz created this gist
May 30, 2023 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,38 @@ import { CanActivate, ExecutionContext, HttpException, HttpStatus, Injectable, UnauthorizedException, } from '@nestjs/common'; import { AuthenticationService } from './authentication.service'; import { Request } from 'express'; @Injectable() export class AuthenticationGuard implements CanActivate { constructor(private readonly authenticationService: AuthenticationService) {} async canActivate(context: ExecutionContext): Promise<boolean> { const request: Request = context.switchToHttp().getRequest(); const token = this.extractTokenFromHeader(request); if (!token) { throw new UnauthorizedException(); } try { // Store the user on the request object if we want to retrieve it from the controllers request['user'] = await this.authenticationService.authenticate(token); return true; } catch (e) { throw new HttpException(e.message, HttpStatus.UNAUTHORIZED); } } private extractTokenFromHeader(request: Request): string | undefined { const [type, token] = request.headers.authorization?.split(' ') ?? []; return type === 'Bearer' ? token : undefined; } } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,19 @@ import { Module } from '@nestjs/common'; import { AuthenticationGuard } from './authentication.guard'; import { AuthenticationService } from './authentication.service'; import { AUTHENTICATION_STRATEGY_TOKEN } from './authentication.strategy'; import { KeycloakAuthenticationStrategy } from './keycloak.strategy'; @Module({ providers: [ AuthenticationGuard, AuthenticationService, { provide: AUTHENTICATION_STRATEGY_TOKEN, useClass: KeycloakAuthenticationStrategy, }, ], exports: [AuthenticationService], }) export class AuthenticationModule {} This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,35 @@ import { Inject, Injectable, Logger } from '@nestjs/common'; import { AUTHENTICATION_STRATEGY_TOKEN, AuthenticationStrategy, } from './authentication.strategy'; export class AuthenticationError extends Error {} @Injectable() export class AuthenticationService { private logger = new Logger(AuthenticationService.name); constructor( @Inject(AUTHENTICATION_STRATEGY_TOKEN) private readonly strategy: AuthenticationStrategy, ) {} async authenticate(accessToken: string): Promise<string> { try { const userInfos = await this.strategy.authenticate(accessToken); const user = { id: userInfos.sub, username: userInfos.preferred_username, }; // TODO: create user if it doesn't exist return user.id; } catch (e) { this.logger.error(e.message, e.stackTrace); throw new AuthenticationError(e.message); } } } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,15 @@ export const AUTHENTICATION_STRATEGY_TOKEN = 'AuthenticationStrategy'; export interface KeycloakUserInfoResponse { sub: string; email_verified: boolean; name: string; preferred_username: string; given_name: string; family_name: string; email: string; } export interface AuthenticationStrategy { authenticate(accessToken: string): Promise<KeycloakUserInfoResponse>; } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,71 @@ import { Injectable, UnauthorizedException } from '@nestjs/common'; import * as jwt from 'jsonwebtoken'; import { AuthenticationStrategy, KeycloakUserInfoResponse, } from './authentication.strategy'; interface KeycloakCertsResponse { keys: KeycloakKey[]; } interface KeycloakKey { kid: string; x5c: string; } export class InvalidToken extends Error { constructor(keyId: string) { super(`Invalid public key ID ${keyId}`); } } @Injectable() export class KeycloakAuthenticationStrategy implements AuthenticationStrategy { private readonly baseURL: string; private readonly realm: string; constructor() { this.baseURL = process.env.KEYCLOAK_BASE_URL; this.realm = process.env.KEYCLOAK_REALM; } async authenticate(accessToken: string): Promise<KeycloakUserInfoResponse> { try { const token = jwt.decode(accessToken, { complete: true }); const keyId = token.header.kid; const publicKey = await this.getPublicKey(keyId); return jwt.verify(accessToken, publicKey, { algorithms: ['RS256'], }); } catch (_) { throw new UnauthorizedException(); } } /* * Fetches the public key from Keycloak to sign the token */ private async getPublicKey(keyId: string): Promise<string> { const response = await fetch( `${this.baseURL}/realms/${this.realm}/protocol/openid-connect/certs`, { method: 'GET' }, ); const { keys }: KeycloakCertsResponse = await response.json(); const key = keys.find((k) => k.kid === keyId); if (!key) { // Token is probably so old, Keycloak doesn't even advertise the corresponding public key anymore throw new InvalidToken(keyId); } const publicKey = `-----BEGIN CERTIFICATE-----\r\n${key.x5c}\r\n-----END CERTIFICATE-----`; return publicKey; } }