Skip to content

Commit 4795c6a

Browse files
committed
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 modelcontextprotocol#773. Closes modelcontextprotocol#772
1 parent 7ba58da commit 4795c6a

2 files changed

Lines changed: 79 additions & 3 deletions

File tree

packages/client/src/client/auth.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,19 @@ export interface OAuthClientProvider {
351351
* re-discovery in case the authorization server has changed.
352352
*/
353353
discoveryState?(): OAuthDiscoveryState | undefined | Promise<OAuthDiscoveryState | undefined>;
354+
355+
/**
356+
* If implemented, provides an initial access token for OAuth 2.0 Dynamic
357+
* Client Registration (RFC 7591 Section 3). When the authorization server
358+
* requires pre-authorisation for client registration, this token is included
359+
* as a Bearer token in the registration request.
360+
*
361+
* The token is per-authorisation-server, so implementations should return the
362+
* appropriate token for the server being registered with.
363+
*
364+
* When not implemented or returning `undefined`, open registration is assumed.
365+
*/
366+
dcrRegistrationAccessToken?(): string | undefined | Promise<string | undefined>;
354367
}
355368

356369
/**
@@ -730,10 +743,13 @@ async function authInternal(
730743
throw new Error('OAuth client information must be saveable for dynamic registration');
731744
}
732745

746+
const initialAccessToken = await provider.dcrRegistrationAccessToken?.();
747+
733748
const fullInformation = await registerClient(authorizationServerUrl, {
734749
metadata,
735750
clientMetadata: provider.clientMetadata,
736751
scope: resolvedScope,
752+
initialAccessToken,
737753
fetchFn
738754
});
739755

@@ -1684,11 +1700,13 @@ export async function registerClient(
16841700
metadata,
16851701
clientMetadata,
16861702
scope,
1703+
initialAccessToken,
16871704
fetchFn
16881705
}: {
16891706
metadata?: AuthorizationServerMetadata;
16901707
clientMetadata: OAuthClientMetadata;
16911708
scope?: string;
1709+
initialAccessToken?: string;
16921710
fetchFn?: FetchLike;
16931711
}
16941712
): Promise<OAuthClientInformationFull> {
@@ -1704,11 +1722,17 @@ export async function registerClient(
17041722
registrationUrl = new URL('/register', authorizationServerUrl);
17051723
}
17061724

1725+
const headers: Record<string, string> = {
1726+
'Content-Type': 'application/json'
1727+
};
1728+
1729+
if (initialAccessToken) {
1730+
headers['Authorization'] = `Bearer ${initialAccessToken}`;
1731+
}
1732+
17071733
const response = await (fetchFn ?? fetch)(registrationUrl, {
17081734
method: 'POST',
1709-
headers: {
1710-
'Content-Type': 'application/json'
1711-
},
1735+
headers,
17121736
body: JSON.stringify({
17131737
...clientMetadata,
17141738
...(scope === undefined ? {} : { scope })

packages/client/test/client/auth.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2132,6 +2132,58 @@ describe('OAuth Authorization', () => {
21322132
})
21332133
).rejects.toThrow('Dynamic client registration failed');
21342134
});
2135+
2136+
it('includes Authorization header when initialAccessToken is provided', async () => {
2137+
mockFetch.mockResolvedValueOnce({
2138+
ok: true,
2139+
status: 200,
2140+
json: async () => validClientInfo
2141+
});
2142+
2143+
await registerClient('https://auth.example.com', {
2144+
clientMetadata: validClientMetadata,
2145+
initialAccessToken: 'my-initial-token'
2146+
});
2147+
2148+
expect(mockFetch).toHaveBeenCalledWith(
2149+
expect.objectContaining({
2150+
href: 'https://auth.example.com/register'
2151+
}),
2152+
expect.objectContaining({
2153+
method: 'POST',
2154+
headers: {
2155+
'Content-Type': 'application/json',
2156+
Authorization: 'Bearer my-initial-token'
2157+
},
2158+
body: JSON.stringify(validClientMetadata)
2159+
})
2160+
);
2161+
});
2162+
2163+
it('does not include Authorization header when initialAccessToken is not provided', async () => {
2164+
mockFetch.mockResolvedValueOnce({
2165+
ok: true,
2166+
status: 200,
2167+
json: async () => validClientInfo
2168+
});
2169+
2170+
await registerClient('https://auth.example.com', {
2171+
clientMetadata: validClientMetadata
2172+
});
2173+
2174+
expect(mockFetch).toHaveBeenCalledWith(
2175+
expect.objectContaining({
2176+
href: 'https://auth.example.com/register'
2177+
}),
2178+
expect.objectContaining({
2179+
method: 'POST',
2180+
headers: {
2181+
'Content-Type': 'application/json'
2182+
},
2183+
body: JSON.stringify(validClientMetadata)
2184+
})
2185+
);
2186+
});
21352187
});
21362188

21372189
describe('auth function', () => {

0 commit comments

Comments
 (0)