import { DependencyRegistryIndex, getDefaultQualifier, Inject, Injectable } from '@travetto/di'; import { toConcrete, TimeUtil } from '@travetto/runtime'; import type { Principal } from './types/principal.ts'; import type { Authenticator } from './types/authenticator.ts'; import type { Authorizer } from './types/authorizer.ts'; import { AuthenticationError } from './types/error.ts'; import type { AuthContext } from './context.ts'; import type { AuthConfig } from './config.ts'; @Injectable() export class AuthService { @Inject() authContext: AuthContext; @Inject() config: AuthConfig; #authenticators = new Map>(); @Inject() authorizer?: Authorizer; async postConstruct(): Promise { // Find all authenticators const AuthenticatorTarget = toConcrete(); for (const source of DependencyRegistryIndex.getCandidates(AuthenticatorTarget)) { const qualifier = source.qualifier || getDefaultQualifier(source.class); const instance = DependencyRegistryIndex.getInstance(AuthenticatorTarget, qualifier); this.#authenticators.set(qualifier, instance); } } /** * Get authenticators by keys */ async getAuthenticators(keys: symbol[]): Promise[]> { return await Promise.all(keys.map(key => this.#authenticators.get(key)!)); } /** * Authenticate. Supports multi-step login. * @param ctx The authenticator context * @param authenticators List of valid authentication sources */ async authenticate(payload: T, context: C, authenticators: symbol[]): Promise { let lastError: Error | undefined; /** * Attempt to authenticate, checking with multiple authentication sources */ for (const authenticator of await this.getAuthenticators(authenticators)) { try { const principal = await authenticator.authenticate(payload, context); if (authenticator.getState) { this.authContext.authenticatorState = await authenticator.getState(context); } if (!principal) { // Multi-step login process return; } return this.authContext.principal = (await this.authorizer?.authorize(principal)) ?? principal; } catch (error) { if (!(error instanceof Error)) { throw error; } lastError = error; } } if (lastError) { console.warn('Failed to authenticate', { error: lastError, sources: authenticators.map(symbol => symbol.toString()) }); } // Take the last error and return throw new AuthenticationError('Unable to authenticate', { cause: lastError }); } /** * Manage expiry state, renewing if allowed */ manageExpiry(principal?: Principal): void { if (!principal) { return; } if (this.config.maxAgeMs) { principal.expiresAt ??= TimeUtil.fromNow(this.config.maxAgeMs); } principal.issuedAt ??= new Date(); if (principal.expiresAt && this.config.maxAgeMs && this.config.rollingRenew) { // Session behavior const end = principal.expiresAt.getTime(); const midPoint = end - this.config.maxAgeMs / 2; if (Date.now() > midPoint) { // If we are past the half way mark, renew the token principal.issuedAt = new Date(); principal.expiresAt = TimeUtil.fromNow(this.config.maxAgeMs); // This will trigger a re-send } } } /** * Enforce expiry, invalidating the principal if expired */ enforceExpiry(principal?: Principal): Principal | undefined { if (principal && principal.expiresAt && principal.expiresAt.getTime() < Date.now()) { return undefined; } return principal; } }