Created
May 30, 2023 15:47
-
-
Save herveGuigoz/a0094759bc8f9ade9e7af23e48672ef8 to your computer and use it in GitHub Desktop.
Nest Keykloak
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 characters
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 characters
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 characters
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 characters
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 characters
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; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment