Codebase MCP

import { authorizationHandler, AuthorizationHandlerOptions } from './authorize.js'; import { OAuthServerProvider, AuthorizationParams } from '../provider.js'; import { OAuthRegisteredClientsStore } from '../clients.js'; import { OAuthClientInformationFull, 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('Authorization Handler', () => { // Mock client data const validClient: OAuthClientInformationFull = { client_id: 'valid-client', client_secret: 'valid-secret', redirect_uris: ['https://example.com/callback'], scope: 'profile email' }; const multiRedirectClient: OAuthClientInformationFull = { client_id: 'multi-redirect-client', client_secret: 'valid-secret', redirect_uris: [ 'https://example.com/callback1', 'https://example.com/callback2' ], scope: 'profile email' }; // Mock client store const mockClientStore: OAuthRegisteredClientsStore = { async getClient(clientId: string): Promise<OAuthClientInformationFull | undefined> { if (clientId === 'valid-client') { return validClient; } else if (clientId === 'multi-redirect-client') { return multiRedirectClient; } return undefined; } }; // Mock provider const mockProvider: OAuthServerProvider = { clientsStore: mockClientStore, async authorize(client: OAuthClientInformationFull, params: AuthorizationParams, res: Response): Promise<void> { // Mock implementation - redirects to redirectUri with code and state 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(): Promise<void> { // Do nothing in mock } }; // Setup express app with handler let app: express.Express; let options: AuthorizationHandlerOptions; beforeEach(() => { app = express(); options = { provider: mockProvider }; const handler = authorizationHandler(options); app.use('/authorize', handler); }); describe('HTTP method validation', () => { it('rejects non-GET/POST methods', async () => { const response = await supertest(app) .put('/authorize') .query({ client_id: 'valid-client' }); expect(response.status).toBe(405); // Method not allowed response from handler }); }); describe('Client validation', () => { it('requires client_id parameter', async () => { const response = await supertest(app) .get('/authorize'); expect(response.status).toBe(400); expect(response.text).toContain('client_id'); }); it('validates that client exists', async () => { const response = await supertest(app) .get('/authorize') .query({ client_id: 'nonexistent-client' }); expect(response.status).toBe(400); }); }); describe('Redirect URI validation', () => { it('uses the only redirect_uri if client has just one and none provided', 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.origin + location.pathname).toBe('https://example.com/callback'); }); it('requires redirect_uri if client has multiple', async () => { const response = await supertest(app) .get('/authorize') .query({ client_id: 'multi-redirect-client', response_type: 'code', code_challenge: 'challenge123', code_challenge_method: 'S256' }); expect(response.status).toBe(400); }); it('validates redirect_uri against client registered URIs', async () => { const response = await supertest(app) .get('/authorize') .query({ client_id: 'valid-client', redirect_uri: 'https://malicious.com/callback', response_type: 'code', code_challenge: 'challenge123', code_challenge_method: 'S256' }); expect(response.status).toBe(400); }); it('accepts valid redirect_uri that client registered with', async () => { const response = await supertest(app) .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.origin + location.pathname).toBe('https://example.com/callback'); }); }); describe('Authorization request validation', () => { it('requires response_type=code', async () => { const response = await supertest(app) .get('/authorize') .query({ client_id: 'valid-client', redirect_uri: 'https://example.com/callback', response_type: 'token', // invalid - we only support code flow code_challenge: 'challenge123', code_challenge_method: 'S256' }); expect(response.status).toBe(302); const location = new URL(response.header.location); expect(location.searchParams.get('error')).toBe('invalid_request'); }); it('requires code_challenge parameter', async () => { const response = await supertest(app) .get('/authorize') .query({ client_id: 'valid-client', redirect_uri: 'https://example.com/callback', response_type: 'code', code_challenge_method: 'S256' // Missing code_challenge }); expect(response.status).toBe(302); const location = new URL(response.header.location); expect(location.searchParams.get('error')).toBe('invalid_request'); }); it('requires code_challenge_method=S256', async () => { const response = await supertest(app) .get('/authorize') .query({ client_id: 'valid-client', redirect_uri: 'https://example.com/callback', response_type: 'code', code_challenge: 'challenge123', code_challenge_method: 'plain' // Only S256 is supported }); expect(response.status).toBe(302); const location = new URL(response.header.location); expect(location.searchParams.get('error')).toBe('invalid_request'); }); }); describe('Scope validation', () => { it('validates requested scopes against client registered scopes', async () => { const response = await supertest(app) .get('/authorize') .query({ client_id: 'valid-client', redirect_uri: 'https://example.com/callback', response_type: 'code', code_challenge: 'challenge123', code_challenge_method: 'S256', scope: 'profile email admin' // 'admin' not in client scopes }); expect(response.status).toBe(302); const location = new URL(response.header.location); expect(location.searchParams.get('error')).toBe('invalid_scope'); }); it('accepts valid scopes subset', async () => { const response = await supertest(app) .get('/authorize') .query({ client_id: 'valid-client', redirect_uri: 'https://example.com/callback', response_type: 'code', code_challenge: 'challenge123', code_challenge_method: 'S256', scope: 'profile' // subset of client scopes }); expect(response.status).toBe(302); const location = new URL(response.header.location); expect(location.searchParams.has('code')).toBe(true); }); }); describe('Successful authorization', () => { it('handles successful authorization with all parameters', async () => { const response = await supertest(app) .get('/authorize') .query({ client_id: 'valid-client', redirect_uri: 'https://example.com/callback', response_type: 'code', code_challenge: 'challenge123', code_challenge_method: 'S256', scope: 'profile email', state: 'xyz789' }); expect(response.status).toBe(302); const location = new URL(response.header.location); expect(location.origin + location.pathname).toBe('https://example.com/callback'); expect(location.searchParams.get('code')).toBe('mock_auth_code'); expect(location.searchParams.get('state')).toBe('xyz789'); }); it('preserves state parameter in response', async () => { const response = await supertest(app) .get('/authorize') .query({ client_id: 'valid-client', redirect_uri: 'https://example.com/callback', response_type: 'code', code_challenge: 'challenge123', code_challenge_method: 'S256', state: 'state-value-123' }); expect(response.status).toBe(302); const location = new URL(response.header.location); expect(location.searchParams.get('state')).toBe('state-value-123'); }); it('handles POST requests the same as GET', async () => { const response = await supertest(app) .post('/authorize') .type('form') .send({ 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); }); }); });