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