simplify changes

This commit is contained in:
Olivier Chafik 2025-06-18 11:59:19 +01:00
commit 68424ef7e6
No known key found for this signature in database
3 changed files with 1 additions and 298 deletions

View file

@ -354,18 +354,6 @@ describe("OAuth Authorization", () => {
expect(authorizationUrl.searchParams.get("resource")).toBe("https://api.example.com/mcp-server");
});
it("excludes resource parameter when not provided", async () => {
const { authorizationUrl } = await startAuthorization(
"https://auth.example.com",
{
clientInformation: validClientInfo,
redirectUrl: "http://localhost:3000/callback",
}
);
expect(authorizationUrl.searchParams.has("resource")).toBe(false);
});
it("includes scope parameter when provided", async () => {
const { authorizationUrl } = await startAuthorization(
"https://auth.example.com",
@ -535,24 +523,6 @@ describe("OAuth Authorization", () => {
expect(body.get("resource")).toBe("https://api.example.com/mcp-server");
});
it("excludes resource parameter from token exchange when not provided", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => validTokens,
});
await exchangeAuthorization("https://auth.example.com", {
clientInformation: validClientInfo,
authorizationCode: "code123",
codeVerifier: "verifier123",
redirectUri: "http://localhost:3000/callback",
});
const body = mockFetch.mock.calls[0][1].body as URLSearchParams;
expect(body.has("resource")).toBe(false);
});
it("validates token response schema", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
@ -659,22 +629,6 @@ describe("OAuth Authorization", () => {
expect(body.get("resource")).toBe("https://api.example.com/mcp-server");
});
it("excludes resource parameter from refresh token request when not provided", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => validTokensWithNewRefreshToken,
});
await refreshAuthorization("https://auth.example.com", {
clientInformation: validClientInfo,
refreshToken: "refresh123",
});
const body = mockFetch.mock.calls[0][1].body as URLSearchParams;
expect(body.has("resource")).toBe(false);
});
it("exchanges refresh token for new tokens and keep existing refresh token if none is returned", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
@ -1136,47 +1090,5 @@ describe("OAuth Authorization", () => {
// Verify that the two resources are different (critical for security)
expect(authUrl1.searchParams.get("resource")).not.toBe(authUrl2.searchParams.get("resource"));
});
it("preserves query parameters in resource URI", async () => {
// Mock successful metadata discovery
mockFetch.mockImplementation((url) => {
const urlString = url.toString();
if (urlString.includes("/.well-known/oauth-authorization-server")) {
return Promise.resolve({
ok: true,
status: 200,
json: async () => ({
issuer: "https://auth.example.com",
authorization_endpoint: "https://auth.example.com/authorize",
token_endpoint: "https://auth.example.com/token",
response_types_supported: ["code"],
code_challenge_methods_supported: ["S256"],
}),
});
}
return Promise.resolve({ ok: false, status: 404 });
});
// Mock provider methods
(mockProvider.clientInformation as jest.Mock).mockResolvedValue({
client_id: "test-client",
client_secret: "test-secret",
});
(mockProvider.tokens as jest.Mock).mockResolvedValue(undefined);
(mockProvider.saveCodeVerifier as jest.Mock).mockResolvedValue(undefined);
(mockProvider.redirectToAuthorization as jest.Mock).mockResolvedValue(undefined);
// Call auth with resource containing query parameters
const result = await auth(mockProvider, {
serverUrl: "https://api.example.com/mcp-server?param=value&another=test",
});
expect(result).toBe("REDIRECT");
// Verify query parameters are preserved (only fragment is removed)
const redirectCall = (mockProvider.redirectToAuthorization as jest.Mock).mock.calls[0];
const authUrl: URL = redirectCall[0];
expect(authUrl.searchParams.get("resource")).toBe("https://api.example.com/mcp-server?param=value&another=test");
});
});
});

View file

@ -1,200 +0,0 @@
import { describe, it, expect, beforeEach } from '@jest/globals';
import { DemoInMemoryAuthProvider, DemoInMemoryClientsStore } from './demoInMemoryOAuthProvider.js';
import { OAuthClientInformationFull } from '../../shared/auth.js';
import { Response } from 'express';
describe('DemoInMemoryOAuthProvider', () => {
let provider: DemoInMemoryAuthProvider;
let clientsStore: DemoInMemoryClientsStore;
let mockClient: OAuthClientInformationFull;
let mockResponse: Partial<Response>;
beforeEach(() => {
provider = new DemoInMemoryAuthProvider();
clientsStore = provider.clientsStore as DemoInMemoryClientsStore;
mockClient = {
client_id: 'test-client',
client_name: 'Test Client',
client_uri: 'https://example.com',
redirect_uris: ['https://example.com/callback'],
grant_types: ['authorization_code'],
response_types: ['code'],
scope: 'mcp:tools',
token_endpoint_auth_method: 'none',
};
mockResponse = {
redirect: jest.fn(),
};
});
describe('Basic authorization flow', () => {
it('should handle authorization successfully', async () => {
await clientsStore.registerClient(mockClient);
await provider.authorize(mockClient, {
codeChallenge: 'test-challenge',
redirectUri: mockClient.redirect_uris[0],
resource: new URL('https://api.example.com/v1'),
scopes: ['mcp:tools']
}, mockResponse as Response);
expect(mockResponse.redirect).toHaveBeenCalled();
const redirectCall = (mockResponse.redirect as jest.Mock).mock.calls[0][0];
expect(redirectCall).toContain('code=');
});
it('should handle authorization without resource', async () => {
await clientsStore.registerClient(mockClient);
await provider.authorize(mockClient, {
codeChallenge: 'test-challenge',
redirectUri: mockClient.redirect_uris[0],
scopes: ['mcp:tools']
}, mockResponse as Response);
expect(mockResponse.redirect).toHaveBeenCalled();
});
it('should preserve state parameter', async () => {
await clientsStore.registerClient(mockClient);
await provider.authorize(mockClient, {
codeChallenge: 'test-challenge',
redirectUri: mockClient.redirect_uris[0],
state: 'test-state',
scopes: ['mcp:tools']
}, mockResponse as Response);
const redirectCall = (mockResponse.redirect as jest.Mock).mock.calls[0][0];
expect(redirectCall).toContain('state=test-state');
});
});
describe('Token exchange', () => {
let authorizationCode: string;
beforeEach(async () => {
await clientsStore.registerClient(mockClient);
await provider.authorize(mockClient, {
codeChallenge: 'test-challenge',
redirectUri: mockClient.redirect_uris[0],
scopes: ['mcp:tools']
}, mockResponse as Response);
const redirectCall = (mockResponse.redirect as jest.Mock).mock.calls[0][0];
const url = new URL(redirectCall);
authorizationCode = url.searchParams.get('code')!;
});
it('should exchange authorization code for tokens', async () => {
const tokens = await provider.exchangeAuthorizationCode(
mockClient,
authorizationCode
);
expect(tokens).toHaveProperty('access_token');
expect(tokens.token_type).toBe('bearer');
expect(tokens.expires_in).toBe(3600);
});
it('should reject invalid authorization code', async () => {
await expect(provider.exchangeAuthorizationCode(
mockClient,
'invalid-code'
)).rejects.toThrow('Invalid authorization code');
});
it('should reject code from different client', async () => {
const otherClient: OAuthClientInformationFull = {
...mockClient,
client_id: 'other-client'
};
await clientsStore.registerClient(otherClient);
await expect(provider.exchangeAuthorizationCode(
otherClient,
authorizationCode
)).rejects.toThrow('Authorization code was not issued to this client');
});
it('should store resource in token when provided during authorization', async () => {
mockResponse.redirect = jest.fn();
await provider.authorize(mockClient, {
codeChallenge: 'test-challenge',
redirectUri: mockClient.redirect_uris[0],
resource: new URL('https://api.example.com/v1'),
scopes: ['mcp:tools']
}, mockResponse as Response);
const redirectCall = (mockResponse.redirect as jest.Mock).mock.calls[0][0];
const url = new URL(redirectCall);
const codeWithResource = url.searchParams.get('code')!;
const tokens = await provider.exchangeAuthorizationCode(
mockClient,
codeWithResource
);
const tokenDetails = provider.getTokenDetails(tokens.access_token);
expect(tokenDetails?.resource).toEqual(new URL('https://api.example.com/v1'));
});
});
describe('Token verification', () => {
it('should verify valid access token', async () => {
await clientsStore.registerClient(mockClient);
await provider.authorize(mockClient, {
codeChallenge: 'test-challenge',
redirectUri: mockClient.redirect_uris[0],
scopes: ['mcp:tools']
}, mockResponse as Response);
const redirectCall = (mockResponse.redirect as jest.Mock).mock.calls[0][0];
const url = new URL(redirectCall);
const code = url.searchParams.get('code')!;
const tokens = await provider.exchangeAuthorizationCode(mockClient, code);
const tokenInfo = await provider.verifyAccessToken(tokens.access_token);
expect(tokenInfo.clientId).toBe(mockClient.client_id);
expect(tokenInfo.scopes).toEqual(['mcp:tools']);
});
it('should reject invalid token', async () => {
await expect(provider.verifyAccessToken('invalid-token'))
.rejects.toThrow('Invalid or expired token');
});
});
describe('Refresh token', () => {
it('should throw error for refresh token (not implemented)', async () => {
await clientsStore.registerClient(mockClient);
await expect(provider.exchangeRefreshToken(
mockClient,
'refresh-token'
)).rejects.toThrow('Not implemented for example demo');
});
});
describe('Server URL validation', () => {
it('should accept mcpServerUrl configuration', () => {
const serverUrl = new URL('https://api.example.com/mcp');
const providerWithUrl = new DemoInMemoryAuthProvider({ mcpServerUrl: serverUrl });
expect(providerWithUrl).toBeDefined();
});
it('should handle server URL with fragment', () => {
const serverUrl = new URL('https://api.example.com/mcp#fragment');
const providerWithUrl = new DemoInMemoryAuthProvider({ mcpServerUrl: serverUrl });
expect(providerWithUrl).toBeDefined();
});
});
});

View file

@ -150,13 +150,6 @@ export class DemoInMemoryAuthProvider implements OAuthServerProvider {
resource: tokenData.resource,
};
}
/**
* Get token details including resource information (for demo introspection endpoint)
*/
getTokenDetails(token: string): AuthInfo | undefined {
return this.tokens.get(token);
}
}
@ -190,14 +183,12 @@ export const setupAuthServer = (authServerUrl: URL, mcpServerUrl: URL): OAuthMet
}
const tokenInfo = await provider.verifyAccessToken(token);
// For demo purposes, we'll add a method to get token details
const tokenDetails = provider.getTokenDetails(token);
res.json({
active: true,
client_id: tokenInfo.clientId,
scope: tokenInfo.scopes.join(' '),
exp: tokenInfo.expiresAt,
...(tokenDetails?.resource && { aud: tokenDetails.resource })
aud: tokenInfo.resource,
});
return
} catch (error) {