simplify changes
This commit is contained in:
parent
4fcbb6870b
commit
68424ef7e6
3 changed files with 1 additions and 298 deletions
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue