@@ -42,9 +42,43 @@ export const OAuthTokensSchema = z
4242 } )
4343 . strip ( ) ;
4444
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+
4576export type OAuthMetadata = z . infer < typeof OAuthMetadataSchema > ;
4677export type OAuthTokens = z . infer < typeof OAuthTokensSchema > ;
4778
79+ export type ClientMetadata = z . infer < typeof ClientMetadataSchema > ;
80+ export type ClientInformation = z . infer < typeof ClientInformationSchema > ;
81+
4882/**
4983 * Looks up RFC 8414 OAuth 2.0 Authorization Server Metadata.
5084 *
@@ -77,7 +111,7 @@ export async function startAuthorization(
77111 {
78112 metadata,
79113 redirectUrl,
80- } : { metadata : OAuthMetadata ; redirectUrl : string | URL } ,
114+ } : { metadata ? : OAuthMetadata ; redirectUrl : string | URL } ,
81115) : Promise < { authorizationUrl : URL ; codeVerifier : string } > {
82116 const responseType = "code" ;
83117 const codeChallengeMethod = "S256" ;
@@ -130,7 +164,7 @@ export async function exchangeAuthorization(
130164 authorizationCode,
131165 codeVerifier,
132166 } : {
133- metadata : OAuthMetadata ;
167+ metadata ? : OAuthMetadata ;
134168 authorizationCode : string ;
135169 codeVerifier : string ;
136170 } ,
@@ -182,7 +216,7 @@ export async function refreshAuthorization(
182216 metadata,
183217 refreshToken,
184218 } : {
185- metadata : OAuthMetadata ;
219+ metadata ? : OAuthMetadata ;
186220 refreshToken : string ;
187221 } ,
188222) : Promise < OAuthTokens > {
@@ -221,3 +255,50 @@ export async function refreshAuthorization(
221255
222256 return OAuthTokensSchema . parse ( await response . json ( ) ) ;
223257}
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