Skip to content

Commit 2b9a581

Browse files
committed
Add dynamic client registration
1 parent 62d67ad commit 2b9a581

File tree

1 file changed

+84
-3
lines changed

1 file changed

+84
-3
lines changed

src/client/auth.ts

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
4576
export type OAuthMetadata = z.infer<typeof OAuthMetadataSchema>;
4677
export 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

Comments
 (0)