Files
toir-automatization/server/src/auth/auth.service.ts
2026-04-06 12:50:46 +03:00

94 lines
2.7 KiB
TypeScript

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,
};
}
}