Codebase MCP
import { mcpAuthRouter, AuthRouterOptions } from './router.js';
import { OAuthServerProvider, AuthorizationParams } from './provider.js';
import { OAuthRegisteredClientsStore } from './clients.js';
import { OAuthClientInformationFull, OAuthTokenRevocationRequest, OAuthTokens } from '../../shared/auth.js';
import express, { Response } from 'express';
import supertest from 'supertest';
import { AuthInfo } from './types.js';
import { InvalidTokenError } from './errors.js';
describe('MCP Auth Router', () => {
// Setup mock provider with full capabilities
const mockClientStore: OAuthRegisteredClientsStore = {
async getClient(clientId: string): Promise<OAuthClientInformationFull | undefined> {
if (clientId === 'valid-client') {
return {
client_id: 'valid-client',
client_secret: 'valid-secret',
redirect_uris: ['https://example.com/callback']
};
}
return undefined;
},
async registerClient(client: OAuthClientInformationFull): Promise<OAuthClientInformationFull> {
return client;
}
};
const mockProvider: OAuthServerProvider = {
clientsStore: mockClientStore,
async authorize(client: OAuthClientInformationFull, params: AuthorizationParams, res: Response): Promise<void> {
const redirectUrl = new URL(params.redirectUri);
redirectUrl.searchParams.set('code', 'mock_auth_code');
if (params.state) {
redirectUrl.searchParams.set('state', params.state);
}
res.redirect(302, redirectUrl.toString());
},
async challengeForAuthorizationCode(): Promise<string> {
return 'mock_challenge';
},
async exchangeAuthorizationCode(): Promise<OAuthTokens> {
return {
access_token: 'mock_access_token',
token_type: 'bearer',
expires_in: 3600,
refresh_token: 'mock_refresh_token'
};
},
async exchangeRefreshToken(): Promise<OAuthTokens> {
return {
access_token: 'new_mock_access_token',
token_type: 'bearer',
expires_in: 3600,
refresh_token: 'new_mock_refresh_token'
};
},
async verifyAccessToken(token: string): Promise<AuthInfo> {
if (token === 'valid_token') {
return {
token,
clientId: 'valid-client',
scopes: ['read', 'write'],
expiresAt: Date.now() / 1000 + 3600
};
}
throw new InvalidTokenError('Token is invalid or expired');
},
async revokeToken(_client: OAuthClientInformationFull, _request: OAuthTokenRevocationRequest): Promise<void> {
// Success - do nothing in mock
}
};
// Provider without registration and revocation
const mockProviderMinimal: OAuthServerProvider = {
clientsStore: {
async getClient(clientId: string): Promise<OAuthClientInformationFull | undefined> {
if (clientId === 'valid-client') {
return {
client_id: 'valid-client',
client_secret: 'valid-secret',
redirect_uris: ['https://example.com/callback']
};
}
return undefined;
}
},
async authorize(client: OAuthClientInformationFull, params: AuthorizationParams, res: Response): Promise<void> {
const redirectUrl = new URL(params.redirectUri);
redirectUrl.searchParams.set('code', 'mock_auth_code');
if (params.state) {
redirectUrl.searchParams.set('state', params.state);
}
res.redirect(302, redirectUrl.toString());
},
async challengeForAuthorizationCode(): Promise<string> {
return 'mock_challenge';
},
async exchangeAuthorizationCode(): Promise<OAuthTokens> {
return {
access_token: 'mock_access_token',
token_type: 'bearer',
expires_in: 3600,
refresh_token: 'mock_refresh_token'
};
},
async exchangeRefreshToken(): Promise<OAuthTokens> {
return {
access_token: 'new_mock_access_token',
token_type: 'bearer',
expires_in: 3600,
refresh_token: 'new_mock_refresh_token'
};
},
async verifyAccessToken(token: string): Promise<AuthInfo> {
if (token === 'valid_token') {
return {
token,
clientId: 'valid-client',
scopes: ['read'],
expiresAt: Date.now() / 1000 + 3600
};
}
throw new InvalidTokenError('Token is invalid or expired');
}
};
describe('Router creation', () => {
it('throws error for non-HTTPS issuer URL', () => {
const options: AuthRouterOptions = {
provider: mockProvider,
issuerUrl: new URL('http://auth.example.com')
};
expect(() => mcpAuthRouter(options)).toThrow('Issuer URL must be HTTPS');
});
it('allows localhost HTTP for development', () => {
const options: AuthRouterOptions = {
provider: mockProvider,
issuerUrl: new URL('http://localhost:3000')
};
expect(() => mcpAuthRouter(options)).not.toThrow();
});
it('throws error for issuer URL with fragment', () => {
const options: AuthRouterOptions = {
provider: mockProvider,
issuerUrl: new URL('https://auth.example.com#fragment')
};
expect(() => mcpAuthRouter(options)).toThrow('Issuer URL must not have a fragment');
});
it('throws error for issuer URL with query string', () => {
const options: AuthRouterOptions = {
provider: mockProvider,
issuerUrl: new URL('https://auth.example.com?param=value')
};
expect(() => mcpAuthRouter(options)).toThrow('Issuer URL must not have a query string');
});
it('successfully creates router with valid options', () => {
const options: AuthRouterOptions = {
provider: mockProvider,
issuerUrl: new URL('https://auth.example.com')
};
expect(() => mcpAuthRouter(options)).not.toThrow();
});
});
describe('Metadata endpoint', () => {
let app: express.Express;
beforeEach(() => {
// Setup full-featured router
app = express();
const options: AuthRouterOptions = {
provider: mockProvider,
issuerUrl: new URL('https://auth.example.com'),
serviceDocumentationUrl: new URL('https://docs.example.com')
};
app.use(mcpAuthRouter(options));
});
it('returns complete metadata for full-featured router', async () => {
const response = await supertest(app)
.get('/.well-known/oauth-authorization-server');
expect(response.status).toBe(200);
// Verify essential fields
expect(response.body.issuer).toBe('https://auth.example.com/');
expect(response.body.authorization_endpoint).toBe('https://auth.example.com/authorize');
expect(response.body.token_endpoint).toBe('https://auth.example.com/token');
expect(response.body.registration_endpoint).toBe('https://auth.example.com/register');
expect(response.body.revocation_endpoint).toBe('https://auth.example.com/revoke');
// Verify supported features
expect(response.body.response_types_supported).toEqual(['code']);
expect(response.body.grant_types_supported).toEqual(['authorization_code', 'refresh_token']);
expect(response.body.code_challenge_methods_supported).toEqual(['S256']);
expect(response.body.token_endpoint_auth_methods_supported).toEqual(['client_secret_post']);
expect(response.body.revocation_endpoint_auth_methods_supported).toEqual(['client_secret_post']);
// Verify optional fields
expect(response.body.service_documentation).toBe('https://docs.example.com/');
});
it('returns minimal metadata for minimal router', async () => {
// Setup minimal router
const minimalApp = express();
const options: AuthRouterOptions = {
provider: mockProviderMinimal,
issuerUrl: new URL('https://auth.example.com')
};
minimalApp.use(mcpAuthRouter(options));
const response = await supertest(minimalApp)
.get('/.well-known/oauth-authorization-server');
expect(response.status).toBe(200);
// Verify essential endpoints
expect(response.body.issuer).toBe('https://auth.example.com/');
expect(response.body.authorization_endpoint).toBe('https://auth.example.com/authorize');
expect(response.body.token_endpoint).toBe('https://auth.example.com/token');
// Verify missing optional endpoints
expect(response.body.registration_endpoint).toBeUndefined();
expect(response.body.revocation_endpoint).toBeUndefined();
expect(response.body.revocation_endpoint_auth_methods_supported).toBeUndefined();
expect(response.body.service_documentation).toBeUndefined();
});
});
describe('Endpoint routing', () => {
let app: express.Express;
beforeEach(() => {
// Setup full-featured router
app = express();
const options: AuthRouterOptions = {
provider: mockProvider,
issuerUrl: new URL('https://auth.example.com')
};
app.use(mcpAuthRouter(options));
});
it('routes to authorization endpoint', async () => {
const response = await supertest(app)
.get('/authorize')
.query({
client_id: 'valid-client',
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.has('code')).toBe(true);
});
it('routes to token endpoint', async () => {
// Setup verifyChallenge mock for token handler
jest.mock('pkce-challenge', () => ({
verifyChallenge: jest.fn().mockResolvedValue(true)
}));
const response = await supertest(app)
.post('/token')
.type('form')
.send({
client_id: 'valid-client',
client_secret: 'valid-secret',
grant_type: 'authorization_code',
code: 'valid_code',
code_verifier: 'valid_verifier'
});
// The request will fail in testing due to mocking limitations,
// but we can verify the route was matched
expect(response.status).not.toBe(404);
});
it('routes to registration endpoint', async () => {
const response = await supertest(app)
.post('/register')
.send({
redirect_uris: ['https://example.com/callback']
});
// The request will fail in testing due to mocking limitations,
// but we can verify the route was matched
expect(response.status).not.toBe(404);
});
it('routes to revocation endpoint', async () => {
const response = await supertest(app)
.post('/revoke')
.type('form')
.send({
client_id: 'valid-client',
client_secret: 'valid-secret',
token: 'token_to_revoke'
});
// The request will fail in testing due to mocking limitations,
// but we can verify the route was matched
expect(response.status).not.toBe(404);
});
it('excludes endpoints for unsupported features', async () => {
// Setup minimal router
const minimalApp = express();
const options: AuthRouterOptions = {
provider: mockProviderMinimal,
issuerUrl: new URL('https://auth.example.com')
};
minimalApp.use(mcpAuthRouter(options));
// Registration should not be available
const regResponse = await supertest(minimalApp)
.post('/register')
.send({
redirect_uris: ['https://example.com/callback']
});
expect(regResponse.status).toBe(404);
// Revocation should not be available
const revokeResponse = await supertest(minimalApp)
.post('/revoke')
.send({
client_id: 'valid-client',
client_secret: 'valid-secret',
token: 'token_to_revoke'
});
expect(revokeResponse.status).toBe(404);
});
});
});