Skip to content

Instantly share code, notes, and snippets.

@herveGuigoz
Created May 30, 2023 15:47
Show Gist options
  • Save herveGuigoz/a0094759bc8f9ade9e7af23e48672ef8 to your computer and use it in GitHub Desktop.
Save herveGuigoz/a0094759bc8f9ade9e7af23e48672ef8 to your computer and use it in GitHub Desktop.
Nest Keykloak
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;
}
}
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 {}
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);
}
}
}
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>;
}
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