Implement MCP 2026-07-28 (#2286)

Co-authored-by: Matt <77928207+mattzcarey@users.noreply.github.com>
Co-authored-by: Konstantin Konstantinov <KKonstantinov@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Felix Weinberger 2026-06-26 14:01:10 +01:00 • committed by GitHub
commit 1823aae891
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 69131 additions and 9001 deletions

View file

@ -397,4 +397,78 @@ describe('Authorization Handler', () => {
expect(location.searchParams.has('code')).toBe(true);
});
});
describe('RFC 9207 iss parameter', () => {
const ISSUER = 'https://auth.example.com/';
let issApp: express.Express;
beforeEach(() => {
issApp = express();
issApp.use('/authorize', authorizationHandler({ provider: mockProvider, issuerUrl: new URL(ISSUER) }));
});
it("appends iss to the provider's success redirect and supplies issuer to provider.authorize()", async () => {
// mockProvider.authorize() does NOT set `iss` itself — the handler must add it.
const spy = vi.spyOn(mockProvider, 'authorize');
const response = await supertest(issApp).get('/authorize').query({
client_id: 'valid-client',
redirect_uri: 'https://example.com/callback',
response_type: 'code',
code_challenge: 'challenge123',
code_challenge_method: 'S256'
});
expect(response.status).toBe(302);
const location = new URL(response.header.location!);
expect(location.searchParams.get('code')).toBe('mock_auth_code');
expect(location.searchParams.get('iss')).toBe(ISSUER);
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ issuer: ISSUER }), expect.anything());
spy.mockRestore();
});
it('leaves redirects to non-callback targets untouched', async () => {
// A provider that hops to an upstream authorize endpoint (proxy pattern) — the
// handler must not append `iss` to that hop.
const upstream = 'https://upstream.example.com/authorize?client_id=x';
const proxyLike: OAuthServerProvider = {
...mockProvider,
async authorize(_client, _params, res) {
res.redirect(upstream);
}
};
const proxyApp = express();
proxyApp.use('/authorize', authorizationHandler({ provider: proxyLike, issuerUrl: new URL(ISSUER) }));
const response = await supertest(proxyApp).get('/authorize').query({
client_id: 'valid-client',
redirect_uri: 'https://example.com/callback',
response_type: 'code',
code_challenge: 'challenge123',
code_challenge_method: 'S256'
});
expect(response.status).toBe(302);
expect(response.header.location).toBe(upstream);
});
it('appends iss to error redirects', async () => {
const response = await supertest(issApp).get('/authorize').query({
client_id: 'valid-client',
redirect_uri: 'https://example.com/callback',
response_type: 'token' // invalid → error redirect
});
expect(response.status).toBe(302);
const location = new URL(response.header.location!);
expect(location.searchParams.get('error')).toBe('invalid_request');
expect(location.searchParams.get('iss')).toBe(ISSUER);
});
it('omits iss when issuerUrl is not configured', async () => {
const response = await supertest(app).get('/authorize').query({
client_id: 'valid-client',
redirect_uri: 'https://example.com/callback',
response_type: 'token'
});
expect(response.status).toBe(302);
const location = new URL(response.header.location!);
expect(location.searchParams.has('iss')).toBe(false);
});
});
});

View file

@ -106,6 +106,14 @@ describe('Proxy OAuth Server Provider', () => {
expect(mockResponse.redirect).toHaveBeenCalledWith(expectedUrl.toString());
});
it('reports authorizationResponseIssParameterSupported = false (upstream issues the callback)', () => {
// The proxy cannot guarantee its own `iss` on the callback, so the metadata flag
// derived from the provider must be false — otherwise RFC 9207 clients reject any
// callback that arrives *without* `iss`. (A present upstream `iss` still mismatches
// the proxy issuer regardless of this flag.)
expect(provider.authorizationResponseIssParameterSupported).toBe(false);
});
});
describe('token exchange', () => {

View file

@ -218,6 +218,22 @@ describe('MCP Auth Router', () => {
// Verify optional fields
expect(response.body.service_documentation).toBe('https://docs.example.com/');
// RFC 9207: the bundled authorize handler emits `iss`, so the metadata advertises it.
expect(response.body.authorization_response_iss_parameter_supported).toBe(true);
});
it('derives authorization_response_iss_parameter_supported from the provider', async () => {
const optOutApp = express();
optOutApp.use(
mcpAuthRouter({
provider: { ...mockProvider, authorizationResponseIssParameterSupported: false },
issuerUrl: new URL('https://auth.example.com')
})
);
const response = await supertest(optOutApp).get('/.well-known/oauth-authorization-server');
expect(response.status).toBe(200);
expect(response.body.authorization_response_iss_parameter_supported).toBe(false);
});
it('returns minimal metadata for minimal router', async () => {