97 lines
2.8 KiB
TypeScript
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;
|
|
}
|
|
}
|