(llm-first): context budget, validation, and eval harness, orchestration general-prompt
This commit is contained in:
@@ -1,129 +1,96 @@
|
||||
import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
||||
import { RuntimeEnvironment } from '../config/env.validation';
|
||||
import {
|
||||
AuthenticatedUser,
|
||||
KeycloakJwtPayload,
|
||||
} from './interfaces/authenticated-user.interface';
|
||||
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 logger = new Logger(AuthService.name);
|
||||
private readonly issuerUrl: string;
|
||||
private readonly audience: string;
|
||||
private readonly explicitJwksUrl?: string;
|
||||
private readonly KEYCLOAK_ISSUER_URL =
|
||||
process.env.KEYCLOAK_ISSUER_URL ?? 'https://sso.greact.ru/realms/toir';
|
||||
|
||||
private jwksResolverPromise: Promise<ReturnType<typeof createRemoteJWKSet>> | null =
|
||||
private readonly KEYCLOAK_AUDIENCE =
|
||||
process.env.KEYCLOAK_AUDIENCE ?? 'toir-backend';
|
||||
|
||||
private readonly KEYCLOAK_JWKS_URL = process.env.KEYCLOAK_JWKS_URL;
|
||||
|
||||
private jwksPromise: Promise<ReturnType<typeof createRemoteJWKSet>> | null =
|
||||
null;
|
||||
private jwksResolver: ReturnType<typeof createRemoteJWKSet> | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly configService: ConfigService<RuntimeEnvironment, true>,
|
||||
) {
|
||||
this.issuerUrl = this.configService.getOrThrow('KEYCLOAK_ISSUER_URL');
|
||||
this.audience = this.configService.getOrThrow('KEYCLOAK_AUDIENCE');
|
||||
this.explicitJwksUrl = this.configService.get('KEYCLOAK_JWKS_URL');
|
||||
}
|
||||
|
||||
async verifyAccessToken(token: string): Promise<AuthenticatedUser> {
|
||||
async verifyBearerToken(token: string): Promise<AuthenticatedUser> {
|
||||
try {
|
||||
const jwksResolver = await this.getJwksResolver();
|
||||
|
||||
const { payload } = await jwtVerify(token, jwksResolver, {
|
||||
issuer: this.issuerUrl,
|
||||
audience: this.audience,
|
||||
const JWKS = await this.resolveJwks();
|
||||
const { payload } = await jwtVerify(token, JWKS, {
|
||||
issuer: this.KEYCLOAK_ISSUER_URL,
|
||||
audience: this.KEYCLOAK_AUDIENCE,
|
||||
});
|
||||
|
||||
return this.mapPayloadToUser(payload as KeycloakJwtPayload);
|
||||
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) {
|
||||
this.logger.warn(
|
||||
`JWT verification failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
throw new UnauthorizedException(
|
||||
error instanceof Error ? error.message : 'Invalid access token',
|
||||
);
|
||||
throw new UnauthorizedException('Invalid or expired access token');
|
||||
}
|
||||
}
|
||||
|
||||
private mapPayloadToUser(payload: KeycloakJwtPayload): AuthenticatedUser {
|
||||
if (!payload.sub) {
|
||||
throw new UnauthorizedException('Token subject is missing');
|
||||
}
|
||||
|
||||
const roles = Array.isArray(payload.realm_access?.roles)
|
||||
? payload.realm_access.roles.filter(
|
||||
(role): role is string => typeof role === 'string',
|
||||
)
|
||||
: [];
|
||||
|
||||
return {
|
||||
sub: payload.sub,
|
||||
username: payload.preferred_username,
|
||||
name: payload.name,
|
||||
email: payload.email,
|
||||
roles,
|
||||
claims: payload,
|
||||
};
|
||||
}
|
||||
|
||||
private async getJwksResolver() {
|
||||
if (this.jwksResolver) {
|
||||
return this.jwksResolver;
|
||||
}
|
||||
|
||||
if (!this.jwksResolverPromise) {
|
||||
this.jwksResolverPromise = this.createJwksResolver()
|
||||
.then((resolver) => {
|
||||
this.jwksResolver = resolver;
|
||||
return resolver;
|
||||
})
|
||||
.finally(() => {
|
||||
this.jwksResolverPromise = null;
|
||||
});
|
||||
}
|
||||
|
||||
return this.jwksResolverPromise;
|
||||
}
|
||||
|
||||
private async createJwksResolver() {
|
||||
const jwksUrl = await this.resolveJwksUrl();
|
||||
this.logger.log(`Using JWKS URL: ${jwksUrl}`);
|
||||
return createRemoteJWKSet(new URL(jwksUrl));
|
||||
}
|
||||
|
||||
private async resolveJwksUrl(): Promise<string> {
|
||||
if (this.explicitJwksUrl) {
|
||||
return this.explicitJwksUrl;
|
||||
}
|
||||
|
||||
const issuer = this.issuerUrl.replace(/\/+$/, '');
|
||||
const discoveryUrl = `${issuer}/.well-known/openid-configuration`;
|
||||
|
||||
try {
|
||||
const discoveryResponse = await fetch(discoveryUrl, {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (discoveryResponse.ok) {
|
||||
const discoveryDocument = (await discoveryResponse.json()) as {
|
||||
jwks_uri?: string;
|
||||
};
|
||||
|
||||
if (
|
||||
typeof discoveryDocument.jwks_uri === 'string' &&
|
||||
discoveryDocument.jwks_uri.trim().length > 0
|
||||
) {
|
||||
return discoveryDocument.jwks_uri;
|
||||
private async resolveJwks(): Promise<ReturnType<typeof createRemoteJWKSet>> {
|
||||
if (!this.jwksPromise) {
|
||||
this.jwksPromise = (async () => {
|
||||
if (this.KEYCLOAK_JWKS_URL) {
|
||||
return createRemoteJWKSet(new URL(this.KEYCLOAK_JWKS_URL));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`OIDC discovery failed at ${discoveryUrl}: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
);
|
||||
|
||||
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 `${issuer}/protocol/openid-connect/certs`;
|
||||
return this.jwksPromise;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user