From 4795c6a92a032ab7631d183063d2bf0214c76e0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusufhan=20Sa=C3=A7ak?= Date: Fri, 10 Apr 2026 12:13:49 +0300 Subject: [PATCH] feat(client): add initial access token support for Dynamic Client Registration Add optional `dcrRegistrationAccessToken()` method to `OAuthClientProvider` interface, enabling OAuth 2.0 Dynamic Client Registration with initial access tokens per RFC 7591 Section 3. When the authorization server requires pre-authorisation for client registration, providers can implement this method to supply a Bearer token that is included in the DCR request. When not implemented, open registration continues as before (fully backward compatible). The token resolution is kept in the provider (not the SDK) as it is per-authorisation-server, following maintainer guidance from #773. Closes #772 --- packages/client/src/client/auth.ts | 30 ++++++++++++-- packages/client/test/client/auth.test.ts | 52 ++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/packages/client/src/client/auth.ts b/packages/client/src/client/auth.ts index 93a03ece6..cf5b6fbd7 100644 --- a/packages/client/src/client/auth.ts +++ b/packages/client/src/client/auth.ts @@ -351,6 +351,19 @@ export interface OAuthClientProvider { * re-discovery in case the authorization server has changed. */ discoveryState?(): OAuthDiscoveryState | undefined | Promise; + + /** + * If implemented, provides an initial access token for OAuth 2.0 Dynamic + * Client Registration (RFC 7591 Section 3). When the authorization server + * requires pre-authorisation for client registration, this token is included + * as a Bearer token in the registration request. + * + * The token is per-authorisation-server, so implementations should return the + * appropriate token for the server being registered with. + * + * When not implemented or returning `undefined`, open registration is assumed. + */ + dcrRegistrationAccessToken?(): string | undefined | Promise; } /** @@ -730,10 +743,13 @@ async function authInternal( throw new Error('OAuth client information must be saveable for dynamic registration'); } + const initialAccessToken = await provider.dcrRegistrationAccessToken?.(); + const fullInformation = await registerClient(authorizationServerUrl, { metadata, clientMetadata: provider.clientMetadata, scope: resolvedScope, + initialAccessToken, fetchFn }); @@ -1684,11 +1700,13 @@ export async function registerClient( metadata, clientMetadata, scope, + initialAccessToken, fetchFn }: { metadata?: AuthorizationServerMetadata; clientMetadata: OAuthClientMetadata; scope?: string; + initialAccessToken?: string; fetchFn?: FetchLike; } ): Promise { @@ -1704,11 +1722,17 @@ export async function registerClient( registrationUrl = new URL('/register', authorizationServerUrl); } + const headers: Record = { + 'Content-Type': 'application/json' + }; + + if (initialAccessToken) { + headers['Authorization'] = `Bearer ${initialAccessToken}`; + } + const response = await (fetchFn ?? fetch)(registrationUrl, { method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, + headers, body: JSON.stringify({ ...clientMetadata, ...(scope === undefined ? {} : { scope }) diff --git a/packages/client/test/client/auth.test.ts b/packages/client/test/client/auth.test.ts index 53263ad8c..b8553f595 100644 --- a/packages/client/test/client/auth.test.ts +++ b/packages/client/test/client/auth.test.ts @@ -2132,6 +2132,58 @@ describe('OAuth Authorization', () => { }) ).rejects.toThrow('Dynamic client registration failed'); }); + + it('includes Authorization header when initialAccessToken is provided', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => validClientInfo + }); + + await registerClient('https://auth.example.com', { + clientMetadata: validClientMetadata, + initialAccessToken: 'my-initial-token' + }); + + expect(mockFetch).toHaveBeenCalledWith( + expect.objectContaining({ + href: 'https://auth.example.com/register' + }), + expect.objectContaining({ + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer my-initial-token' + }, + body: JSON.stringify(validClientMetadata) + }) + ); + }); + + it('does not include Authorization header when initialAccessToken is not provided', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => validClientInfo + }); + + await registerClient('https://auth.example.com', { + clientMetadata: validClientMetadata + }); + + expect(mockFetch).toHaveBeenCalledWith( + expect.objectContaining({ + href: 'https://auth.example.com/register' + }), + expect.objectContaining({ + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(validClientMetadata) + }) + ); + }); }); describe('auth function', () => {