1+ import {
2+ OAuthClientInformation ,
3+ OAuthClientMetadata ,
4+ OAuthClientMetadataSchema ,
5+ ClientRegistrationError
6+ } from "../shared/auth.js" ;
7+
8+ /**
9+ * OAuth 2.0 Client Registration Provider interface.
10+ * Implementations provide storage and lifecycle management for OAuth clients.
11+ */
12+ export interface OAuthClientRegistrationProvider {
13+ /**
14+ * Store a new client registration.
15+ * @param metadata The client metadata to register
16+ * @returns The client information including the assigned client_id
17+ */
18+ registerClient ( metadata : OAuthClientMetadata ) : Promise < OAuthClientInformation > ;
19+
20+ /**
21+ * Retrieve client information by client_id.
22+ * @param clientId The client_id to look up
23+ * @returns The client information or null if not found
24+ */
25+ getClient ( clientId : string ) : Promise < OAuthClientInformation | null > ;
26+
27+ /**
28+ * Update an existing client registration.
29+ * @param clientId The client_id to update
30+ * @param metadata The updated client metadata
31+ * @returns The updated client information
32+ */
33+ updateClient ( clientId : string , metadata : OAuthClientMetadata ) : Promise < OAuthClientInformation > ;
34+
35+ /**
36+ * Delete a client registration.
37+ * @param clientId The client_id to delete
38+ * @returns true if the client was deleted, false if not found
39+ */
40+ deleteClient ( clientId : string ) : Promise < boolean > ;
41+ }
42+
43+ /**
44+ * In-memory implementation of OAuthClientRegistrationProvider.
45+ * Useful for development and testing.
46+ */
47+ export class InMemoryClientRegistrationProvider implements OAuthClientRegistrationProvider {
48+ private clients : Map < string , {
49+ metadata : OAuthClientMetadata ;
50+ info : OAuthClientInformation ;
51+ } > = new Map ( ) ;
52+
53+ private generateClientId ( ) : string {
54+ return `client_${ Math . random ( ) . toString ( 36 ) . substring ( 2 , 15 ) } ` ;
55+ }
56+
57+ async registerClient ( metadata : OAuthClientMetadata ) : Promise < OAuthClientInformation > {
58+ // Generate a client_id and optional client_secret
59+ const clientId = this . generateClientId ( ) ;
60+ const clientSecret = metadata . token_endpoint_auth_method === "none"
61+ ? undefined
62+ : `secret_${ Math . random ( ) . toString ( 36 ) . substring ( 2 , 15 ) } ` ;
63+
64+ const now = Math . floor ( Date . now ( ) / 1000 ) ;
65+
66+ const clientInfo : OAuthClientInformation = {
67+ client_id : clientId ,
68+ client_id_issued_at : now ,
69+ ...clientSecret && {
70+ client_secret,
71+ client_secret_expires_at : 0 // Never expires
72+ }
73+ } ;
74+
75+ this . clients . set ( clientId , {
76+ metadata,
77+ info : clientInfo
78+ } ) ;
79+
80+ return clientInfo ;
81+ }
82+
83+ async getClient ( clientId : string ) : Promise < OAuthClientInformation | null > {
84+ const client = this . clients . get ( clientId ) ;
85+ return client ? client . info : null ;
86+ }
87+
88+ async updateClient ( clientId : string , metadata : OAuthClientMetadata ) : Promise < OAuthClientInformation > {
89+ const client = this . clients . get ( clientId ) ;
90+ if ( ! client ) {
91+ throw new Error ( `Client ${ clientId } not found` ) ;
92+ }
93+
94+ // Update metadata but keep the same client_id and secret
95+ this . clients . set ( clientId , {
96+ metadata,
97+ info : client . info
98+ } ) ;
99+
100+ return client . info ;
101+ }
102+
103+ async deleteClient ( clientId : string ) : Promise < boolean > {
104+ return this . clients . delete ( clientId ) ;
105+ }
106+ }
107+
108+ /**
109+ * RFC 7591 Client Registration options.
110+ */
111+ export interface ClientRegistrationOptions {
112+ /**
113+ * The provider that handles client registration storage.
114+ */
115+ provider : OAuthClientRegistrationProvider ;
116+
117+ /**
118+ * Optional function to validate client metadata beyond the basic schema validation.
119+ * Return true to accept, or throw an error with details about the rejection.
120+ */
121+ validateMetadata ?: ( metadata : OAuthClientMetadata ) => Promise < boolean > ;
122+
123+ /**
124+ * Initial access token validator function.
125+ * If provided, requests to the registration endpoint must include a valid token.
126+ * @param token The bearer token from the Authorization header
127+ * @returns true if the token is valid, false otherwise
128+ */
129+ validateInitialAccessToken ?: ( token : string ) => Promise < boolean > ;
130+ }
131+
132+
133+ /**
134+ * Client registration configuration information for the .well-known/oauth-authorization-server endpoint.
135+ */
136+ export interface RegistrationEndpointConfig {
137+ registration_endpoint : string ;
138+ registration_endpoint_auth_methods_supported ?: string [ ] ;
139+ require_initial_access_token ?: boolean ;
140+ }
141+
142+ /**
143+ * RFC 7591 handler for registering OAuth clients.
144+ * This implements the server-side logic for the Dynamic Client Registration Protocol.
145+ */
146+ export class ClientRegistrationHandler {
147+ private options : ClientRegistrationOptions ;
148+
149+ constructor ( options : ClientRegistrationOptions ) {
150+ this . options = options ;
151+ }
152+
153+ /**
154+ * Handle a client registration request.
155+ *
156+ * @param metadata The client metadata from the request
157+ * @param authHeader Optional Authorization header value (for initial access token)
158+ * @returns Client information or error response
159+ */
160+ async handleRegistration (
161+ metadata : unknown ,
162+ authHeader ?: string
163+ ) : Promise < OAuthClientInformation | ClientRegistrationError > {
164+ // Validate initial access token if required
165+ if ( this . options . validateInitialAccessToken ) {
166+ if ( ! authHeader ) {
167+ return {
168+ error : "invalid_client" ,
169+ error_description : "Initial access token required"
170+ } ;
171+ }
172+
173+ const match = / ^ B e a r e r \s + ( .+ ) $ / . exec ( authHeader ) ;
174+ if ( ! match ) {
175+ return {
176+ error : "invalid_client" ,
177+ error_description : "Invalid authorization header format, expected 'Bearer TOKEN'"
178+ } ;
179+ }
180+
181+ const token = match [ 1 ] ;
182+ const isValid = await this . options . validateInitialAccessToken ( token ) ;
183+ if ( ! isValid ) {
184+ return {
185+ error : "invalid_token" ,
186+ error_description : "Invalid initial access token"
187+ } ;
188+ }
189+ }
190+
191+ // Validate metadata against the schema
192+ const result = OAuthClientMetadataSchema . safeParse ( metadata ) ;
193+
194+ if ( ! result . success ) {
195+ return {
196+ error : "invalid_client_metadata" ,
197+ error_description : `Invalid client metadata: ${ result . error . message } `
198+ } ;
199+ }
200+
201+ const validatedMetadata = result . data ;
202+
203+ // Additional custom validation if provided
204+ if ( this . options . validateMetadata ) {
205+ try {
206+ await this . options . validateMetadata ( validatedMetadata ) ;
207+ } catch ( error ) {
208+ return {
209+ error : "invalid_client_metadata" ,
210+ error_description : error instanceof Error
211+ ? error . message
212+ : "Client metadata validation failed"
213+ } ;
214+ }
215+ }
216+
217+ try {
218+ // Register the client
219+ const clientInfo = await this . options . provider . registerClient ( validatedMetadata ) ;
220+ return clientInfo ;
221+ } catch ( error ) {
222+ return {
223+ error : "server_error" ,
224+ error_description : error instanceof Error
225+ ? error . message
226+ : "Failed to register client"
227+ } ;
228+ }
229+ }
230+
231+ /**
232+ * Handle a client update request.
233+ *
234+ * @param clientId The client_id to update
235+ * @param metadata The updated client metadata
236+ * @param authHeader Authorization header value for client authentication
237+ * @returns Updated client information or error response
238+ */
239+ async handleUpdate (
240+ clientId : string ,
241+ metadata : unknown ,
242+ _authHeader : string
243+ ) : Promise < OAuthClientInformation | ClientRegistrationError > {
244+ // TODO: Implement client authentication validation
245+
246+ // Validate metadata
247+ const result = OAuthClientMetadataSchema . safeParse ( metadata ) ;
248+
249+ if ( ! result . success ) {
250+ return {
251+ error : "invalid_client_metadata" ,
252+ error_description : `Invalid client metadata: ${ result . error . message } `
253+ } ;
254+ }
255+
256+ const validatedMetadata = result . data ;
257+
258+ // Additional custom validation if provided
259+ if ( this . options . validateMetadata ) {
260+ try {
261+ await this . options . validateMetadata ( validatedMetadata ) ;
262+ } catch ( error ) {
263+ return {
264+ error : "invalid_client_metadata" ,
265+ error_description : error instanceof Error
266+ ? error . message
267+ : "Client metadata validation failed"
268+ } ;
269+ }
270+ }
271+
272+ try {
273+ // Verify client exists first
274+ const existingClient = await this . options . provider . getClient ( clientId ) ;
275+ if ( ! existingClient ) {
276+ return {
277+ error : "invalid_client" ,
278+ error_description : "Client not found"
279+ } ;
280+ }
281+
282+ // Update the client
283+ const clientInfo = await this . options . provider . updateClient ( clientId , validatedMetadata ) ;
284+ return clientInfo ;
285+ } catch ( error ) {
286+ return {
287+ error : "server_error" ,
288+ error_description : error instanceof Error
289+ ? error . message
290+ : "Failed to update client"
291+ } ;
292+ }
293+ }
294+
295+ /**
296+ * Handle a client deletion request.
297+ *
298+ * @param clientId The client_id to delete
299+ * @param authHeader Authorization header value for client authentication
300+ * @returns Success message or error response
301+ */
302+ async handleDelete (
303+ clientId : string ,
304+ _authHeader : string
305+ ) : Promise < { success : true } | ClientRegistrationError > {
306+ // TODO: Implement client authentication validation
307+
308+ try {
309+ const success = await this . options . provider . deleteClient ( clientId ) ;
310+
311+ if ( ! success ) {
312+ return {
313+ error : "invalid_client" ,
314+ error_description : "Client not found"
315+ } ;
316+ }
317+
318+ return { success : true } ;
319+ } catch ( error ) {
320+ return {
321+ error : "server_error" ,
322+ error_description : error instanceof Error
323+ ? error . message
324+ : "Failed to delete client"
325+ } ;
326+ }
327+ }
328+ }
0 commit comments