Files
KIS-TOiR/server/src/auth/auth.service.ts

97 lines
2.8 KiB
TypeScript

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<ReturnType<typeof createRemoteJWKSet>> | null =
null;
async verifyBearerToken(token: string): Promise<AuthenticatedUser> {
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<ReturnType<typeof createRemoteJWKSet>> {
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;
}
}