git init
This commit is contained in:
93
server/src/auth/auth.service.ts
Normal file
93
server/src/auth/auth.service.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import {
|
||||
createRemoteJWKSet,
|
||||
jwtVerify,
|
||||
} from 'jose';
|
||||
|
||||
export interface AuthenticatedUser {
|
||||
sub: string;
|
||||
username?: string;
|
||||
email?: string;
|
||||
name?: string;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
type RemoteJwks = ReturnType<typeof createRemoteJWKSet>;
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
private jwksPromise: Promise<RemoteJwks> | null = null;
|
||||
|
||||
async verifyAccessToken(token: string): Promise<AuthenticatedUser> {
|
||||
const issuer = process.env.KEYCLOAK_ISSUER_URL;
|
||||
const audience = process.env.KEYCLOAK_AUDIENCE;
|
||||
|
||||
if (!issuer || !audience) {
|
||||
throw new UnauthorizedException('Keycloak issuer or audience is not configured');
|
||||
}
|
||||
|
||||
try {
|
||||
const jwks = await this.getJwks();
|
||||
const result = await jwtVerify(token, jwks, {
|
||||
issuer,
|
||||
audience,
|
||||
});
|
||||
|
||||
return this.mapPayloadToUser(result);
|
||||
} catch (error) {
|
||||
throw new UnauthorizedException('Token validation failed');
|
||||
}
|
||||
}
|
||||
|
||||
private async getJwks(): Promise<RemoteJwks> {
|
||||
if (!this.jwksPromise) {
|
||||
this.jwksPromise = this.resolveJwks().catch((error) => {
|
||||
this.jwksPromise = null;
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
return this.jwksPromise;
|
||||
}
|
||||
|
||||
private async resolveJwks(): Promise<RemoteJwks> {
|
||||
const issuer = process.env.KEYCLOAK_ISSUER_URL;
|
||||
if (!issuer) {
|
||||
throw new UnauthorizedException('KEYCLOAK_ISSUER_URL is not configured');
|
||||
}
|
||||
|
||||
const explicitJwksUrl = process.env.KEYCLOAK_JWKS_URL;
|
||||
if (explicitJwksUrl) {
|
||||
return createRemoteJWKSet(new URL(explicitJwksUrl));
|
||||
}
|
||||
|
||||
try {
|
||||
const discoveryUrl = new URL('.well-known/openid-configuration', `${issuer.replace(/\/$/, '')}/`);
|
||||
const response = await fetch(discoveryUrl);
|
||||
if (response.ok) {
|
||||
const discovery = (await response.json()) as { jwks_uri?: string };
|
||||
if (discovery.jwks_uri) {
|
||||
return createRemoteJWKSet(new URL(discovery.jwks_uri));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Fall through to the Keycloak certs endpoint.
|
||||
}
|
||||
|
||||
return createRemoteJWKSet(new URL(`${issuer.replace(/\/$/, '')}/protocol/openid-connect/certs`));
|
||||
}
|
||||
|
||||
private mapPayloadToUser(result: Awaited<ReturnType<typeof jwtVerify>>): AuthenticatedUser {
|
||||
const payload = result.payload;
|
||||
const realmAccess = payload.realm_access as { roles?: string[] } | undefined;
|
||||
const roles = Array.isArray(realmAccess?.roles) ? realmAccess.roles : [];
|
||||
|
||||
return {
|
||||
sub: String(payload.sub ?? ''),
|
||||
username: typeof payload.preferred_username === 'string' ? payload.preferred_username : undefined,
|
||||
email: typeof payload.email === 'string' ? payload.email : undefined,
|
||||
name: typeof payload.name === 'string' ? payload.name : undefined,
|
||||
roles,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user