TomasTokaMrazek

Auth Service

Oct 20th, 2025
218
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
TypeScript 11.89 KB | Software | 0 0
  1. import { Inject, Injectable, Logger } from "@nestjs/common";
  2. import { ConfigService } from "@nestjs/config";
  3. import type { FastifySessionObject } from "@fastify/session";
  4.  
  5. import { randomUUID } from "node:crypto";
  6. import { addMinutes, compareDesc, getUnixTime } from "date-fns";
  7.  
  8. import type { RedisClientType } from "redis";
  9.  
  10. import type { ClientAuth, ClientMetadata, ServerMetadata, ServerMetadataHelpers, TokenEndpointResponse } from "openid-client";
  11. import * as oidc from "openid-client";
  12. import type { JSONWebKeySet, JWK, JWSHeaderParameters, JWTPayload, JWTVerifyResult } from "jose";
  13. import * as jose from "jose";
  14.  
  15. import { DefaultAzureCredential } from "@azure/identity";
  16. import type { GetKeyOptions, JsonWebKey, KeyProperties, KeyVaultKey, SignResult } from "@azure/keyvault-keys";
  17. import { CryptographyClient, KeyClient, KnownSignatureAlgorithms } from "@azure/keyvault-keys";
  18.  
  19. import { REDIS_CLIENT } from "../app.symbols.js";
  20. import { assertExists } from "../app.utils.ts";
  21. import type { Provider } from "../app.config.ts";
  22.  
  23. interface IDTokenPayload extends JWTPayload {
  24.     sub: string;
  25.     email?: string;
  26. }
  27.  
  28. interface LogoutTokenPayload extends JWTPayload {
  29.     sid?: string;
  30.     events?: {
  31.         "http://schemas.openid.net/event/backchannel-logout"?: Record<string, unknown>;
  32.     };
  33. }
  34.  
  35. @Injectable()
  36. export class AuthService {
  37.     private readonly logger: Logger = new Logger(AuthService.name);
  38.     private readonly configurations: Map<string, oidc.Configuration> = new Map<string, oidc.Configuration>();
  39.  
  40.     constructor(
  41.         private readonly configService: ConfigService,
  42.         @Inject(REDIS_CLIENT) private readonly redisClient: RedisClientType,
  43.     ) {}
  44.  
  45.     async onModuleInit(): Promise<void> {
  46.         const providers: Provider[] = assertExists(this.configService.get<Provider[]>(`providers`));
  47.         await Promise.all(
  48.             providers.map(async (provider: Provider): Promise<void> => {
  49.                 const issuer: URL = new URL(provider.issuer);
  50.  
  51.                 const clientMetadata: ClientMetadata = provider.metadata as ClientMetadata;
  52.                 const clientId: string = clientMetadata.client_id;
  53.  
  54.                 const configuration: oidc.Configuration = await oidc.discovery(issuer, clientId, clientMetadata);
  55.                 this.configurations.set(provider.name, configuration);
  56.             }),
  57.         );
  58.     }
  59.  
  60.     async loginRedirect(provider: Provider, session: FastifySessionObject): Promise<URL> {
  61.         const configuration: oidc.Configuration = assertExists(this.configurations.get(provider.name));
  62.  
  63.         const codeVerifier: string = oidc.randomPKCECodeVerifier();
  64.         const codeChallenge: string = await oidc.calculatePKCECodeChallenge(codeVerifier);
  65.         const state: string = oidc.randomState();
  66.  
  67.         session.codeVerifier = codeVerifier;
  68.         session.state = state;
  69.  
  70.         const parameters: Record<string, string> = {
  71.             redirect_uri: provider.loginCallbackUrl,
  72.             code_challenge_method: "S256",
  73.             code_challenge: codeChallenge,
  74.             state: state,
  75.             scope: provider.scope,
  76.             resource: provider.resource,
  77.         };
  78.  
  79.         return oidc.buildAuthorizationUrl(configuration, parameters);
  80.     }
  81.  
  82.     async loginCallback(provider: Provider, url: URL, session: FastifySessionObject): Promise<void> {
  83.         const configuration: oidc.Configuration = assertExists(this.configurations.get(provider.name));
  84.         const clientMetadata: Readonly<ClientMetadata> = configuration.clientMetadata();
  85.         const serverMetadata: Readonly<ServerMetadata> & ServerMetadataHelpers = configuration.serverMetadata();
  86.         const clientId: string = assertExists(clientMetadata.client_id);
  87.         const jwksUri: string = assertExists(serverMetadata.jwks_uri);
  88.         const jwksUrl: URL = new URL(jwksUri);
  89.  
  90.         const clientAssertion: string = await this.privateKeyJwt(provider);
  91.         const clientAuth: ClientAuth = (server: ServerMetadata, client: ClientMetadata, body: URLSearchParams): void => {
  92.             body.set("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
  93.             body.set("client_assertion", clientAssertion);
  94.         };
  95.         const requestConfiguration: oidc.Configuration = new oidc.Configuration(serverMetadata, clientId, clientMetadata, clientAuth);
  96.  
  97.         const codeVerifier: string = assertExists(session.codeVerifier, "Session does not have 'codeVerifier' property.");
  98.         const state: string = assertExists(session.state, "Session does not have 'state' property.");
  99.         const parameters: Record<string, string> = {
  100.             pkceCodeVerifier: codeVerifier,
  101.             expectedState: state,
  102.         };
  103.  
  104.         const tokens: TokenEndpointResponse = await oidc.authorizationCodeGrant(requestConfiguration, url, parameters);
  105.         const idToken: string = assertExists(tokens.id_token);
  106.  
  107.         const jwtVerifyResult: JWTVerifyResult<IDTokenPayload> = await jose.jwtVerify<IDTokenPayload>(idToken, jose.createRemoteJWKSet(jwksUrl));
  108.         const email: string = assertExists(jwtVerifyResult.payload.email);
  109.  
  110.         await this.redisClient.multi().hSet(session.sessionId, "access_token", assertExists(tokens.access_token)).hSet(session.sessionId, "refresh_token", assertExists(tokens.refresh_token)).hSet(session.sessionId, "id_token", assertExists(tokens.id_token)).exec();
  111.  
  112.         delete session.codeVerifier;
  113.         delete session.state;
  114.  
  115.         session.email = email;
  116.     }
  117.  
  118.     async logoutRedirect(provider: Provider, session: FastifySessionObject): Promise<URL> {
  119.         const configuration: oidc.Configuration = assertExists(this.configurations.get(provider.name));
  120.  
  121.         const idToken: string = assertExists(await this.redisClient.hGet(session.sessionId, "id_token"));
  122.  
  123.         const parameters: Record<string, string> = {
  124.             next: provider.logoutCallbackUrl,
  125.             post_logout_redirect_uri: provider.logoutCallbackUrl,
  126.             id_token_hint: idToken,
  127.         };
  128.  
  129.         return oidc.buildEndSessionUrl(configuration, parameters);
  130.     }
  131.  
  132.     async logoutCallback(provider: Provider, session: FastifySessionObject): Promise<void> {
  133.         await this.redisClient.del(session.sessionId);
  134.     }
  135.  
  136.     async backchannelLogout(provider: Provider, logoutToken: string): Promise<void> {
  137.         const configuration: oidc.Configuration = assertExists(this.configurations.get(provider.name));
  138.         const serverMetadata: Readonly<ServerMetadata> & ServerMetadataHelpers = configuration.serverMetadata();
  139.         const jwksUri: string = assertExists(serverMetadata.jwks_uri);
  140.         const jwksUrl: URL = new URL(jwksUri);
  141.  
  142.         const jwtVerifyResult: JWTVerifyResult<LogoutTokenPayload> = await jose.jwtVerify<LogoutTokenPayload>(logoutToken, jose.createRemoteJWKSet(jwksUrl));
  143.  
  144.         this.logger.log(`sub: ${jwtVerifyResult.payload.sub}`);
  145.         this.logger.log(`sid: ${jwtVerifyResult.payload.sid}`);
  146.  
  147.         assertExists(jwtVerifyResult.payload.sub);
  148.     }
  149.  
  150.     async session(session: FastifySessionObject): Promise<boolean> {
  151.         return Boolean(await this.redisClient.exists((session.sessionId)));
  152.     }
  153.  
  154.     async jwkSet(): Promise<JSONWebKeySet> {
  155.         const managedIdentity: string = assertExists(this.configService.get<string>(`azure.managedIdentity`), "Configuration 'azure.managedIdentity' property is undefined.");
  156.         const keyVaultName: string = assertExists(this.configService.get<string>(`azure.keyVaultName`), "Configuration 'azure.keyVaultName' property is undefined.");
  157.         const keyVaultUrl = `https://${keyVaultName}.vault.azure.net`;
  158.  
  159.         const azureCredential: DefaultAzureCredential = new DefaultAzureCredential({
  160.             managedIdentityClientId: managedIdentity,
  161.         });
  162.         const keyClient = new KeyClient(keyVaultUrl, azureCredential);
  163.  
  164.         const ecKeyName: string = assertExists(this.configService.get<string>(`azure.ecKeyName`), "Configuration 'azure.ecKeyName' property is undefined.");
  165.  
  166.         const ecKeyVersions: KeyProperties[] = [];
  167.         for await (const ecKeyProperty of keyClient.listPropertiesOfKeyVersions(ecKeyName)) {
  168.             ecKeyVersions.push(ecKeyProperty);
  169.         }
  170.         ecKeyVersions.sort((a: KeyProperties, b: KeyProperties): number => {
  171.             return compareDesc(assertExists(a.createdOn), assertExists(b.createdOn));
  172.         });
  173.  
  174.         const jwkSet: JSONWebKeySet = {
  175.             keys: [],
  176.         };
  177.         for (const ecKeyVersion of ecKeyVersions.slice(0, 2)) {
  178.             const ecKeyOptions: GetKeyOptions = {
  179.                 version: assertExists(ecKeyVersion.version),
  180.             };
  181.             const ecKey: KeyVaultKey = assertExists(await keyClient.getKey(ecKeyName, ecKeyOptions), "Configuration 'azure.ecKeyName' property is undefined.");
  182.             const ecJwk: JsonWebKey = assertExists(ecKey.key);
  183.  
  184.             const jwk: JWK = {
  185.                 kid: assertExists(ecKeyVersion.version),
  186.                 kty: assertExists(ecJwk.kty),
  187.                 crv: assertExists(ecJwk.crv),
  188.                 x: jose.base64url.encode(assertExists(ecJwk.x)),
  189.                 y: jose.base64url.encode(assertExists(ecJwk.y)),
  190.                 alg: KnownSignatureAlgorithms.ES256,
  191.             };
  192.  
  193.             jwkSet.keys.push(jwk);
  194.         }
  195.  
  196.         return jwkSet;
  197.     }
  198.  
  199.     async privateKeyJwt(provider: Provider): Promise<string> {
  200.         const configuration: oidc.Configuration = assertExists(this.configurations.get(provider.name));
  201.         const clientMetadata: Readonly<ClientMetadata> = configuration.clientMetadata();
  202.         const serverMetadata: Readonly<ServerMetadata> & ServerMetadataHelpers = configuration.serverMetadata();
  203.         const clientId: string = assertExists(clientMetadata.client_id);
  204.         const tokenEndpoint: string = assertExists(serverMetadata.token_endpoint);
  205.  
  206.         const managedIdentity: string = assertExists(this.configService.get<string>(`azure.managedIdentity`), "Configuration 'azure.managedIdentity' property is undefined.");
  207.         const keyVaultName: string = assertExists(this.configService.get<string>(`azure.keyVaultName`), "Configuration 'azure.keyVaultName' property is undefined.");
  208.         const keyVaultUrl = `https://${keyVaultName}.vault.azure.net`;
  209.  
  210.         const azureCredential: DefaultAzureCredential = new DefaultAzureCredential({
  211.             managedIdentityClientId: managedIdentity,
  212.         });
  213.         const keyClient = new KeyClient(keyVaultUrl, azureCredential);
  214.  
  215.         const ecKeyName: string = assertExists(this.configService.get<string>(`azure.ecKeyName`), "Configuration 'azure.ecKeyName' property is undefined.");
  216.         const ecKey: KeyVaultKey = assertExists(await keyClient.getKey(ecKeyName), "Configuration 'azure.ecKeyName' property is undefined.");
  217.         const ecKeyVersion: string = assertExists(ecKey.properties.version);
  218.  
  219.         const header: JWSHeaderParameters = {
  220.             alg: KnownSignatureAlgorithms.ES256,
  221.             kid: ecKeyVersion,
  222.         };
  223.         const encodedHeader: string = jose.base64url.encode(JSON.stringify(header));
  224.  
  225.         const now: Date = new Date();
  226.         const payload: JWTPayload = {
  227.             jti: randomUUID(),
  228.             sub: clientId,
  229.             iss: clientId,
  230.             aud: tokenEndpoint,
  231.             iat: getUnixTime(now),
  232.             exp: getUnixTime(addMinutes(now, 5)),
  233.         };
  234.         const encodedPayload: string = jose.base64url.encode(JSON.stringify(payload));
  235.  
  236.         const cryptoClient = new CryptographyClient(ecKey, azureCredential);
  237.         const signatureResult: SignResult = await cryptoClient.signData(KnownSignatureAlgorithms.ES256, Buffer.from(`${encodedHeader}.${encodedPayload}`));
  238.         const signature: Uint8Array<ArrayBufferLike> = signatureResult.result;
  239.         const encodedSignature: string = Buffer.from(signature).toString("base64url");
  240.  
  241.         return `${encodedHeader}.${encodedPayload}.${encodedSignature}`;
  242.     }
  243. }
  244.  
Add Comment
Please, Sign In to add comment