import { BadRequestException, Injectable, Inject } from '@nestjs/common'; import type { ClientRegistrationDto, IOAuthStore, OAuthClient, } from '../stores/oauth-store.interface'; import { randomBytes } from 'crypto'; import type { OAuthModuleOptions } from '../providers/oauth-provider.interface'; @Injectable() export class ClientService { constructor( @Inject('IOAuthStore') private readonly store: IOAuthStore, @Inject('OAUTH_MODULE_OPTIONS') private readonly options: OAuthModuleOptions, ) {} /** * Register a client application. * Always creates a new client record. client_name is not treated as unique. * * Note: Left open for future enhancements (e.g., software statements, * URL-based Client ID Metadata Documents) via preRegistrationChecks(). */ async registerClient( registrationDto: ClientRegistrationDto, ): Promise { // Validate required fields if ( !registrationDto.redirect_uris || !Array.isArray(registrationDto.redirect_uris) ) { throw new BadRequestException( 'redirect_uris is required and must be an array', ); } // Validate token_endpoint_auth_method if provided const supportedAuthMethods = [ 'client_secret_basic', 'client_secret_post', 'none', ]; if ( registrationDto.token_endpoint_auth_method && !supportedAuthMethods.includes(registrationDto.token_endpoint_auth_method) ) { throw new BadRequestException( `Unsupported token_endpoint_auth_method. Supported methods: ${supportedAuthMethods.join(', ')}`, ); } // Default values for new clients const defaultClientValues = { grant_types: ['authorization_code', 'refresh_token'], response_types: ['code'], token_endpoint_auth_method: registrationDto.token_endpoint_auth_method || 'none', }; // Future-proofing: hook for software statements / metadata URL validations await this.preRegistrationChecks(registrationDto); const now = new Date(); // Create new client - merge defaults with registration data const client_id = this.store.generateClientId( registrationDto as OAuthClient, ); // Only generate client_secret for methods that require it const authMethod = registrationDto.token_endpoint_auth_method || 'none'; const client_secret = authMethod !== 'none' ? randomBytes(32).toString('hex') : undefined; const newClient: OAuthClient = { ...defaultClientValues, ...registrationDto, client_id, client_secret, created_at: now, updated_at: now, }; const client = await this.store.storeClient(newClient); const filteredClient = Object.fromEntries( Object.entries(client).filter(([, value]) => value !== null), ) as OAuthClient; return filteredClient; } /** * Hook for future registration policies (e.g., software statements per RFC 7591/7592, * or URL-based Client Registration using Client ID Metadata Documents). * Currently a no-op to keep behavior: always create a new client. */ protected async preRegistrationChecks( _dto: ClientRegistrationDto, ): Promise { // Intentionally left blank. Implement validations/attestations in the future. } async getClient(clientId: string): Promise { const client = await this.store.getClient(clientId); if (!client) { return null; } // Remove null fields from the client object const filteredClient = Object.fromEntries( Object.entries(client).filter(([, value]) => value !== null), ) as OAuthClient; return filteredClient; } async validateRedirectUri( clientId: string, redirectUri: string, ): Promise { const client = await this.getClient(clientId); return client ? client.redirect_uris.includes(redirectUri) : false; } }