Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-www-auth-substring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@modelcontextprotocol/client': patch
---

Fix `WWW-Authenticate` parsing matching a field name inside another auth-param. The OAuth client extracted `resource_metadata`, `scope`, `error`, and `error_description` with a regex that searched for the field name anywhere in the header, so a different parameter whose name ended with the requested field (for example `error_scope` when reading `scope`) could shadow the real value, and a parameter like `x_resource_metadata` could supply a decoy resource metadata URL. The parameter name is now anchored to the header start or a separator.
5 changes: 4 additions & 1 deletion packages/client/src/client/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1467,7 +1467,10 @@ function extractFieldFromWwwAuth(response: Response, fieldName: string): string
return null;
}

const pattern = new RegExp(String.raw`${fieldName}=(?:"([^"]+)"|([^\s,]+))`);
// Require the parameter name to start at the header start or after a separator (whitespace
// or comma) so it is not matched as a substring of another auth-param name (e.g. searching
// "scope" must not match the value of "error_scope").
const pattern = new RegExp(String.raw`(?:^|[\s,])${fieldName}=(?:"([^"]+)"|([^\s,]+))`);
const match = wwwAuthHeader.match(pattern);

if (match) {
Expand Down
20 changes: 20 additions & 0 deletions packages/client/test/client/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,26 @@ describe('OAuth Authorization', () => {
errorDescription: 'needs admin'
});
});

it('does not match a field name inside a longer auth-param name', async () => {
const mockResponse = {
headers: {
get: vi.fn(name => (name === 'WWW-Authenticate' ? `Bearer error_scope="should-not-be-read", scope="read write"` : null))
}
} as unknown as Response;

expect(extractWWWAuthenticateParams(mockResponse)).toEqual({ scope: 'read write' });
});

it('does not match resource_metadata inside a longer auth-param name', async () => {
const mockResponse = {
headers: {
get: vi.fn(name => (name === 'WWW-Authenticate' ? `Bearer x_resource_metadata="https://decoy.example.com"` : null))
}
} as unknown as Response;

expect(extractWWWAuthenticateParams(mockResponse)).toEqual({});
});
});

describe('computeScopeUnion', () => {
Expand Down
Loading