import { Injectable, UnauthorizedException } from '@nestjs/common'; import { createRemoteJWKSet, jwtVerify, type JWTPayload } from 'jose'; export interface AuthenticatedUser { sub: string; username?: string; email?: string; name?: string; realmRoles: string[]; payload: JWTPayload; } @Injectable() export class AuthService { private readonly KEYCLOAK_ISSUER_URL = process.env.KEYCLOAK_ISSUER_URL ?? 'https://sso.greact.ru/realms/toir'; private readonly KEYCLOAK_AUDIENCE = process.env.KEYCLOAK_AUDIENCE ?? 'toir-backend'; private readonly KEYCLOAK_JWKS_URL = process.env.KEYCLOAK_JWKS_URL; private jwksPromise: Promise> | null = null; async verifyBearerToken(token: string): Promise { try { const JWKS = await this.resolveJwks(); const { payload } = await jwtVerify(token, JWKS, { issuer: this.KEYCLOAK_ISSUER_URL, audience: this.KEYCLOAK_AUDIENCE, }); const realmAccess = payload.realm_access as | { roles?: unknown } | undefined; const realmRoles = Array.isArray(realmAccess?.roles) ? realmAccess.roles.filter( (role): role is string => typeof role === 'string', ) : []; 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, realmRoles, payload, }; } catch (error) { throw new UnauthorizedException( error instanceof Error ? error.message : 'Invalid access token', ); } } private async resolveJwks(): Promise> { if (!this.jwksPromise) { this.jwksPromise = (async () => { if (this.KEYCLOAK_JWKS_URL) { return createRemoteJWKSet(new URL(this.KEYCLOAK_JWKS_URL)); } const discoveryUrl = new URL( `${this.KEYCLOAK_ISSUER_URL}/.well-known/openid-configuration`, ); try { const response = await fetch(discoveryUrl); if (response.ok) { const discovery = (await response.json()) as { jwks_uri?: unknown; }; if (typeof discovery.jwks_uri === 'string') { return createRemoteJWKSet(new URL(discovery.jwks_uri)); } } } catch { // Fall through to the Keycloak certs endpoint. } return createRemoteJWKSet( new URL( `${this.KEYCLOAK_ISSUER_URL}/protocol/openid-connect/certs`, ), ); })(); } return this.jwksPromise; } }