豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content

Commit c9b58d1

Browse files
antogynpcarleton
andauthored
feat: use scopes_supported from resource metadata by default (fixes #580) (#757)
Co-authored-by: Paul Carleton <paulc@anthropic.com>
1 parent 351e124 commit c9b58d1

File tree

2 files changed

+63
-3
lines changed

2 files changed

+63
-3
lines changed

src/client/auth.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,13 @@ async function authInternal(
501501

502502
const resource: URL | undefined = await selectResourceURL(serverUrl, provider, resourceMetadata);
503503

504+
// Apply scope selection strategy (SEP-835):
505+
// 1. WWW-Authenticate scope (passed via `scope` param)
506+
// 2. PRM scopes_supported
507+
// 3. Client metadata scope (user-configured fallback)
508+
// The resolved scope is used consistently for both DCR and the authorization request.
509+
const resolvedScope = scope || resourceMetadata?.scopes_supported?.join(' ') || provider.clientMetadata.scope;
510+
504511
// Handle client registration if needed
505512
let clientInformation = await Promise.resolve(provider.clientInformation());
506513
if (!clientInformation) {
@@ -534,6 +541,7 @@ async function authInternal(
534541
const fullInformation = await registerClient(authorizationServerUrl, {
535542
metadata,
536543
clientMetadata: provider.clientMetadata,
544+
scope: resolvedScope,
537545
fetchFn
538546
});
539547

@@ -594,7 +602,7 @@ async function authInternal(
594602
clientInformation,
595603
state,
596604
redirectUrl: provider.redirectUrl,
597-
scope: scope || resourceMetadata?.scopes_supported?.join(' ') || provider.clientMetadata.scope,
605+
scope: resolvedScope,
598606
resource
599607
});
600608

@@ -1416,16 +1424,22 @@ export async function fetchToken(
14161424

14171425
/**
14181426
* Performs OAuth 2.0 Dynamic Client Registration according to RFC 7591.
1427+
*
1428+
* If `scope` is provided, it overrides `clientMetadata.scope` in the registration
1429+
* request body. This allows callers to apply the Scope Selection Strategy (SEP-835)
1430+
* consistently across both DCR and the subsequent authorization request.
14191431
*/
14201432
export async function registerClient(
14211433
authorizationServerUrl: string | URL,
14221434
{
14231435
metadata,
14241436
clientMetadata,
1437+
scope,
14251438
fetchFn
14261439
}: {
14271440
metadata?: AuthorizationServerMetadata;
14281441
clientMetadata: OAuthClientMetadata;
1442+
scope?: string;
14291443
fetchFn?: FetchLike;
14301444
}
14311445
): Promise<OAuthClientInformationFull> {
@@ -1446,7 +1460,10 @@ export async function registerClient(
14461460
headers: {
14471461
'Content-Type': 'application/json'
14481462
},
1449-
body: JSON.stringify(clientMetadata)
1463+
body: JSON.stringify({
1464+
...clientMetadata,
1465+
...(scope !== undefined ? { scope } : {})
1466+
})
14501467
});
14511468

14521469
if (!response.ok) {

test/client/auth.test.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
} from '../../src/client/auth.js';
1818
import { createPrivateKeyJwtAuth } from '../../src/client/auth-extensions.js';
1919
import { InvalidClientMetadataError, ServerError } from '../../src/server/auth/errors.js';
20-
import { AuthorizationServerMetadata, OAuthTokens } from '../../src/shared/auth.js';
20+
import { AuthorizationServerMetadata, OAuthClientMetadata, OAuthTokens } from '../../src/shared/auth.js';
2121
import { expect, vi, type Mock } from 'vitest';
2222

2323
// Mock pkce-challenge
@@ -1873,6 +1873,43 @@ describe('OAuth Authorization', () => {
18731873
);
18741874
});
18751875

1876+
it('includes scope in registration body when provided, overriding clientMetadata.scope', async () => {
1877+
const clientMetadataWithScope: OAuthClientMetadata = {
1878+
...validClientMetadata,
1879+
scope: 'should-be-overridden'
1880+
};
1881+
1882+
const expectedClientInfo = {
1883+
...validClientInfo,
1884+
scope: 'openid profile'
1885+
};
1886+
1887+
mockFetch.mockResolvedValueOnce({
1888+
ok: true,
1889+
status: 200,
1890+
json: async () => expectedClientInfo
1891+
});
1892+
1893+
const clientInfo = await registerClient('https://auth.example.com', {
1894+
clientMetadata: clientMetadataWithScope,
1895+
scope: 'openid profile'
1896+
});
1897+
1898+
expect(clientInfo).toEqual(expectedClientInfo);
1899+
expect(mockFetch).toHaveBeenCalledWith(
1900+
expect.objectContaining({
1901+
href: 'https://auth.example.com/register'
1902+
}),
1903+
expect.objectContaining({
1904+
method: 'POST',
1905+
headers: {
1906+
'Content-Type': 'application/json'
1907+
},
1908+
body: JSON.stringify({ ...validClientMetadata, scope: 'openid profile' })
1909+
})
1910+
);
1911+
});
1912+
18761913
it('validates client information response schema', async () => {
18771914
mockFetch.mockResolvedValueOnce({
18781915
ok: true,
@@ -2746,6 +2783,12 @@ describe('OAuth Authorization', () => {
27462783
const redirectCall = (mockProvider.redirectToAuthorization as Mock).mock.calls[0];
27472784
const authUrl: URL = redirectCall[0];
27482785
expect(authUrl.searchParams.get('scope')).toBe('mcp:read mcp:write mcp:admin');
2786+
2787+
// Verify the same scope was also used in the DCR request body
2788+
const registerCall = mockFetch.mock.calls.find(call => call[0].toString().includes('/register'));
2789+
expect(registerCall).toBeDefined();
2790+
const registerBody = JSON.parse(registerCall![1].body);
2791+
expect(registerBody.scope).toBe('mcp:read mcp:write mcp:admin');
27492792
});
27502793

27512794
it('prefers explicit scope parameter over scopes_supported from PRM', async () => {

0 commit comments

Comments
 (0)