@@ -42,9 +42,43 @@ export const OAuthTokensSchema = z
42
42
} )
43
43
. strip ( ) ;
44
44
45
+ /**
46
+ * Client metadata schema according to RFC 7591 OAuth 2.0 Dynamic Client Registration
47
+ */
48
+ export const ClientMetadataSchema = z . object ( {
49
+ redirect_uris : z . array ( z . string ( ) ) ,
50
+ token_endpoint_auth_method : z . string ( ) . optional ( ) ,
51
+ grant_types : z . array ( z . string ( ) ) . optional ( ) ,
52
+ response_types : z . array ( z . string ( ) ) . optional ( ) ,
53
+ client_name : z . string ( ) . optional ( ) ,
54
+ client_uri : z . string ( ) . optional ( ) ,
55
+ logo_uri : z . string ( ) . optional ( ) ,
56
+ scope : z . string ( ) . optional ( ) ,
57
+ contacts : z . array ( z . string ( ) ) . optional ( ) ,
58
+ tos_uri : z . string ( ) . optional ( ) ,
59
+ policy_uri : z . string ( ) . optional ( ) ,
60
+ jwks_uri : z . string ( ) . optional ( ) ,
61
+ jwks : z . any ( ) . optional ( ) ,
62
+ software_id : z . string ( ) . optional ( ) ,
63
+ software_version : z . string ( ) . optional ( ) ,
64
+ } ) . passthrough ( ) ;
65
+
66
+ /**
67
+ * Client information response schema according to RFC 7591
68
+ */
69
+ export const ClientInformationSchema = z . object ( {
70
+ client_id : z . string ( ) ,
71
+ client_secret : z . string ( ) . optional ( ) ,
72
+ client_id_issued_at : z . number ( ) . optional ( ) ,
73
+ client_secret_expires_at : z . number ( ) . optional ( ) ,
74
+ } ) . merge ( ClientMetadataSchema ) ;
75
+
45
76
export type OAuthMetadata = z . infer < typeof OAuthMetadataSchema > ;
46
77
export type OAuthTokens = z . infer < typeof OAuthTokensSchema > ;
47
78
79
+ export type ClientMetadata = z . infer < typeof ClientMetadataSchema > ;
80
+ export type ClientInformation = z . infer < typeof ClientInformationSchema > ;
81
+
48
82
/**
49
83
* Looks up RFC 8414 OAuth 2.0 Authorization Server Metadata.
50
84
*
@@ -77,7 +111,7 @@ export async function startAuthorization(
77
111
{
78
112
metadata,
79
113
redirectUrl,
80
- } : { metadata : OAuthMetadata ; redirectUrl : string | URL } ,
114
+ } : { metadata ? : OAuthMetadata ; redirectUrl : string | URL } ,
81
115
) : Promise < { authorizationUrl : URL ; codeVerifier : string } > {
82
116
const responseType = "code" ;
83
117
const codeChallengeMethod = "S256" ;
@@ -130,7 +164,7 @@ export async function exchangeAuthorization(
130
164
authorizationCode,
131
165
codeVerifier,
132
166
} : {
133
- metadata : OAuthMetadata ;
167
+ metadata ? : OAuthMetadata ;
134
168
authorizationCode : string ;
135
169
codeVerifier : string ;
136
170
} ,
@@ -182,7 +216,7 @@ export async function refreshAuthorization(
182
216
metadata,
183
217
refreshToken,
184
218
} : {
185
- metadata : OAuthMetadata ;
219
+ metadata ? : OAuthMetadata ;
186
220
refreshToken : string ;
187
221
} ,
188
222
) : Promise < OAuthTokens > {
@@ -221,3 +255,50 @@ export async function refreshAuthorization(
221
255
222
256
return OAuthTokensSchema . parse ( await response . json ( ) ) ;
223
257
}
258
+
259
+ /**
260
+ * Performs OAuth 2.0 Dynamic Client Registration according to RFC 7591.
261
+ *
262
+ * @param serverUrl - The base URL of the authorization server
263
+ * @param options - Registration options
264
+ * @param options.metadata - OAuth server metadata containing the registration endpoint
265
+ * @param options.clientMetadata - Client metadata for registration
266
+ * @returns The registered client information
267
+ * @throws Error if the server doesn't support dynamic registration or if registration fails
268
+ */
269
+ export async function registerClient (
270
+ serverUrl : string | URL ,
271
+ {
272
+ metadata,
273
+ clientMetadata,
274
+ } : {
275
+ metadata ?: OAuthMetadata ;
276
+ clientMetadata : ClientMetadata ;
277
+ } ,
278
+ ) : Promise < ClientInformation > {
279
+ let registrationUrl : URL ;
280
+
281
+ if ( metadata ) {
282
+ if ( ! metadata . registration_endpoint ) {
283
+ throw new Error ( "Incompatible auth server: does not support dynamic client registration" ) ;
284
+ }
285
+
286
+ registrationUrl = new URL ( metadata . registration_endpoint ) ;
287
+ } else {
288
+ registrationUrl = new URL ( "/register" , serverUrl ) ;
289
+ }
290
+
291
+ const response = await fetch ( registrationUrl , {
292
+ method : "POST" ,
293
+ headers : {
294
+ "Content-Type" : "application/json" ,
295
+ } ,
296
+ body : JSON . stringify ( clientMetadata ) ,
297
+ } ) ;
298
+
299
+ if ( ! response . ok ) {
300
+ throw new Error ( `Dynamic client registration failed: HTTP ${ response . status } ` ) ;
301
+ }
302
+
303
+ return ClientInformationSchema . parse ( await response . json ( ) ) ;
304
+ }
0 commit comments