Skip to content

Commit cee3ec3

Browse files
test: port v1 server/auth handler/provider/middleware tests
1 parent 7cac712 commit cee3ec3

9 files changed

Lines changed: 2382 additions & 0 deletions

File tree

packages/server-auth-legacy/test/handlers/authorize.test.ts

Lines changed: 400 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { metadataHandler } from '../../src/handlers/metadata.js';
2+
import { OAuthMetadata } from '@modelcontextprotocol/core';
3+
import express from 'express';
4+
import supertest from 'supertest';
5+
6+
describe('Metadata Handler', () => {
7+
const exampleMetadata: OAuthMetadata = {
8+
issuer: 'https://auth.example.com',
9+
authorization_endpoint: 'https://auth.example.com/authorize',
10+
token_endpoint: 'https://auth.example.com/token',
11+
registration_endpoint: 'https://auth.example.com/register',
12+
revocation_endpoint: 'https://auth.example.com/revoke',
13+
scopes_supported: ['profile', 'email'],
14+
response_types_supported: ['code'],
15+
grant_types_supported: ['authorization_code', 'refresh_token'],
16+
token_endpoint_auth_methods_supported: ['client_secret_basic'],
17+
code_challenge_methods_supported: ['S256']
18+
};
19+
20+
let app: express.Express;
21+
22+
beforeEach(() => {
23+
// Setup express app with metadata handler
24+
app = express();
25+
app.use('/.well-known/oauth-authorization-server', metadataHandler(exampleMetadata));
26+
});
27+
28+
it('requires GET method', async () => {
29+
const response = await supertest(app).post('/.well-known/oauth-authorization-server').send({});
30+
31+
expect(response.status).toBe(405);
32+
expect(response.headers.allow).toBe('GET, OPTIONS');
33+
expect(response.body).toEqual({
34+
error: 'method_not_allowed',
35+
error_description: 'The method POST is not allowed for this endpoint'
36+
});
37+
});
38+
39+
it('returns the metadata object', async () => {
40+
const response = await supertest(app).get('/.well-known/oauth-authorization-server');
41+
42+
expect(response.status).toBe(200);
43+
expect(response.body).toEqual(exampleMetadata);
44+
});
45+
46+
it('includes CORS headers in response', async () => {
47+
const response = await supertest(app).get('/.well-known/oauth-authorization-server').set('Origin', 'https://example.com');
48+
49+
expect(response.header['access-control-allow-origin']).toBe('*');
50+
});
51+
52+
it('supports OPTIONS preflight requests', async () => {
53+
const response = await supertest(app)
54+
.options('/.well-known/oauth-authorization-server')
55+
.set('Origin', 'https://example.com')
56+
.set('Access-Control-Request-Method', 'GET');
57+
58+
expect(response.status).toBe(204);
59+
expect(response.header['access-control-allow-origin']).toBe('*');
60+
});
61+
62+
it('works with minimal metadata', async () => {
63+
// Setup a new express app with minimal metadata
64+
const minimalApp = express();
65+
const minimalMetadata: OAuthMetadata = {
66+
issuer: 'https://auth.example.com',
67+
authorization_endpoint: 'https://auth.example.com/authorize',
68+
token_endpoint: 'https://auth.example.com/token',
69+
response_types_supported: ['code']
70+
};
71+
minimalApp.use('/.well-known/oauth-authorization-server', metadataHandler(minimalMetadata));
72+
73+
const response = await supertest(minimalApp).get('/.well-known/oauth-authorization-server');
74+
75+
expect(response.status).toBe(200);
76+
expect(response.body).toEqual(minimalMetadata);
77+
});
78+
});
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
import { clientRegistrationHandler, ClientRegistrationHandlerOptions } from '../../src/handlers/register.js';
2+
import { OAuthRegisteredClientsStore } from '../../src/clients.js';
3+
import { OAuthClientInformationFull, OAuthClientMetadata } from '@modelcontextprotocol/core';
4+
import express from 'express';
5+
import supertest from 'supertest';
6+
import { MockInstance } from 'vitest';
7+
8+
describe('Client Registration Handler', () => {
9+
// Mock client store with registration support
10+
const mockClientStoreWithRegistration: OAuthRegisteredClientsStore = {
11+
async getClient(_clientId: string): Promise<OAuthClientInformationFull | undefined> {
12+
return undefined;
13+
},
14+
15+
async registerClient(client: OAuthClientInformationFull): Promise<OAuthClientInformationFull> {
16+
// Return the client info as-is in the mock
17+
return client;
18+
}
19+
};
20+
21+
// Mock client store without registration support
22+
const mockClientStoreWithoutRegistration: OAuthRegisteredClientsStore = {
23+
async getClient(_clientId: string): Promise<OAuthClientInformationFull | undefined> {
24+
return undefined;
25+
}
26+
// No registerClient method
27+
};
28+
29+
describe('Handler creation', () => {
30+
it('throws error if client store does not support registration', () => {
31+
const options: ClientRegistrationHandlerOptions = {
32+
clientsStore: mockClientStoreWithoutRegistration
33+
};
34+
35+
expect(() => clientRegistrationHandler(options)).toThrow('does not support registering clients');
36+
});
37+
38+
it('creates handler if client store supports registration', () => {
39+
const options: ClientRegistrationHandlerOptions = {
40+
clientsStore: mockClientStoreWithRegistration
41+
};
42+
43+
expect(() => clientRegistrationHandler(options)).not.toThrow();
44+
});
45+
});
46+
47+
describe('Request handling', () => {
48+
let app: express.Express;
49+
let spyRegisterClient: MockInstance;
50+
51+
beforeEach(() => {
52+
// Setup express app with registration handler
53+
app = express();
54+
const options: ClientRegistrationHandlerOptions = {
55+
clientsStore: mockClientStoreWithRegistration,
56+
clientSecretExpirySeconds: 86400 // 1 day for testing
57+
};
58+
59+
app.use('/register', clientRegistrationHandler(options));
60+
61+
// Spy on the registerClient method
62+
spyRegisterClient = vi.spyOn(mockClientStoreWithRegistration, 'registerClient');
63+
});
64+
65+
afterEach(() => {
66+
spyRegisterClient.mockRestore();
67+
});
68+
69+
it('requires POST method', async () => {
70+
const response = await supertest(app)
71+
.get('/register')
72+
.send({
73+
redirect_uris: ['https://example.com/callback']
74+
});
75+
76+
expect(response.status).toBe(405);
77+
expect(response.headers.allow).toBe('POST');
78+
expect(response.body).toEqual({
79+
error: 'method_not_allowed',
80+
error_description: 'The method GET is not allowed for this endpoint'
81+
});
82+
expect(spyRegisterClient).not.toHaveBeenCalled();
83+
});
84+
85+
it('validates required client metadata', async () => {
86+
const response = await supertest(app).post('/register').send({
87+
// Missing redirect_uris (required)
88+
client_name: 'Test Client'
89+
});
90+
91+
expect(response.status).toBe(400);
92+
expect(response.body.error).toBe('invalid_client_metadata');
93+
expect(spyRegisterClient).not.toHaveBeenCalled();
94+
});
95+
96+
it('validates redirect URIs format', async () => {
97+
const response = await supertest(app)
98+
.post('/register')
99+
.send({
100+
redirect_uris: ['invalid-url'] // Invalid URL format
101+
});
102+
103+
expect(response.status).toBe(400);
104+
expect(response.body.error).toBe('invalid_client_metadata');
105+
expect(response.body.error_description).toContain('redirect_uris');
106+
expect(spyRegisterClient).not.toHaveBeenCalled();
107+
});
108+
109+
it('successfully registers client with minimal metadata', async () => {
110+
const clientMetadata: OAuthClientMetadata = {
111+
redirect_uris: ['https://example.com/callback']
112+
};
113+
114+
const response = await supertest(app).post('/register').send(clientMetadata);
115+
116+
expect(response.status).toBe(201);
117+
118+
// Verify the generated client information
119+
expect(response.body.client_id).toBeDefined();
120+
expect(response.body.client_secret).toBeDefined();
121+
expect(response.body.client_id_issued_at).toBeDefined();
122+
expect(response.body.client_secret_expires_at).toBeDefined();
123+
expect(response.body.redirect_uris).toEqual(['https://example.com/callback']);
124+
125+
// Verify client was registered
126+
expect(spyRegisterClient).toHaveBeenCalledTimes(1);
127+
});
128+
129+
it('sets client_secret to undefined for token_endpoint_auth_method=none', async () => {
130+
const clientMetadata: OAuthClientMetadata = {
131+
redirect_uris: ['https://example.com/callback'],
132+
token_endpoint_auth_method: 'none'
133+
};
134+
135+
const response = await supertest(app).post('/register').send(clientMetadata);
136+
137+
expect(response.status).toBe(201);
138+
expect(response.body.client_secret).toBeUndefined();
139+
expect(response.body.client_secret_expires_at).toBeUndefined();
140+
});
141+
142+
it('sets client_secret_expires_at for public clients only', async () => {
143+
// Test for public client (token_endpoint_auth_method not 'none')
144+
const publicClientMetadata: OAuthClientMetadata = {
145+
redirect_uris: ['https://example.com/callback'],
146+
token_endpoint_auth_method: 'client_secret_basic'
147+
};
148+
149+
const publicResponse = await supertest(app).post('/register').send(publicClientMetadata);
150+
151+
expect(publicResponse.status).toBe(201);
152+
expect(publicResponse.body.client_secret).toBeDefined();
153+
expect(publicResponse.body.client_secret_expires_at).toBeDefined();
154+
155+
// Test for non-public client (token_endpoint_auth_method is 'none')
156+
const nonPublicClientMetadata: OAuthClientMetadata = {
157+
redirect_uris: ['https://example.com/callback'],
158+
token_endpoint_auth_method: 'none'
159+
};
160+
161+
const nonPublicResponse = await supertest(app).post('/register').send(nonPublicClientMetadata);
162+
163+
expect(nonPublicResponse.status).toBe(201);
164+
expect(nonPublicResponse.body.client_secret).toBeUndefined();
165+
expect(nonPublicResponse.body.client_secret_expires_at).toBeUndefined();
166+
});
167+
168+
it('sets expiry based on clientSecretExpirySeconds', async () => {
169+
// Create handler with custom expiry time
170+
const customApp = express();
171+
const options: ClientRegistrationHandlerOptions = {
172+
clientsStore: mockClientStoreWithRegistration,
173+
clientSecretExpirySeconds: 3600 // 1 hour
174+
};
175+
176+
customApp.use('/register', clientRegistrationHandler(options));
177+
178+
const response = await supertest(customApp)
179+
.post('/register')
180+
.send({
181+
redirect_uris: ['https://example.com/callback']
182+
});
183+
184+
expect(response.status).toBe(201);
185+
186+
// Verify the expiration time (~1 hour from now)
187+
const issuedAt = response.body.client_id_issued_at;
188+
const expiresAt = response.body.client_secret_expires_at;
189+
expect(expiresAt - issuedAt).toBe(3600);
190+
});
191+
192+
it('sets no expiry when clientSecretExpirySeconds=0', async () => {
193+
// Create handler with no expiry
194+
const customApp = express();
195+
const options: ClientRegistrationHandlerOptions = {
196+
clientsStore: mockClientStoreWithRegistration,
197+
clientSecretExpirySeconds: 0 // No expiry
198+
};
199+
200+
customApp.use('/register', clientRegistrationHandler(options));
201+
202+
const response = await supertest(customApp)
203+
.post('/register')
204+
.send({
205+
redirect_uris: ['https://example.com/callback']
206+
});
207+
208+
expect(response.status).toBe(201);
209+
expect(response.body.client_secret_expires_at).toBe(0);
210+
});
211+
212+
it('sets no client_id when clientIdGeneration=false', async () => {
213+
// Create handler with no expiry
214+
const customApp = express();
215+
const options: ClientRegistrationHandlerOptions = {
216+
clientsStore: mockClientStoreWithRegistration,
217+
clientIdGeneration: false
218+
};
219+
220+
customApp.use('/register', clientRegistrationHandler(options));
221+
222+
const response = await supertest(customApp)
223+
.post('/register')
224+
.send({
225+
redirect_uris: ['https://example.com/callback']
226+
});
227+
228+
expect(response.status).toBe(201);
229+
expect(response.body.client_id).toBeUndefined();
230+
expect(response.body.client_id_issued_at).toBeUndefined();
231+
});
232+
233+
it('handles client with all metadata fields', async () => {
234+
const fullClientMetadata: OAuthClientMetadata = {
235+
redirect_uris: ['https://example.com/callback'],
236+
token_endpoint_auth_method: 'client_secret_basic',
237+
grant_types: ['authorization_code', 'refresh_token'],
238+
response_types: ['code'],
239+
client_name: 'Test Client',
240+
client_uri: 'https://example.com',
241+
logo_uri: 'https://example.com/logo.png',
242+
scope: 'profile email',
243+
contacts: ['dev@example.com'],
244+
tos_uri: 'https://example.com/tos',
245+
policy_uri: 'https://example.com/privacy',
246+
jwks_uri: 'https://example.com/jwks',
247+
software_id: 'test-software',
248+
software_version: '1.0.0'
249+
};
250+
251+
const response = await supertest(app).post('/register').send(fullClientMetadata);
252+
253+
expect(response.status).toBe(201);
254+
255+
// Verify all metadata was preserved
256+
Object.entries(fullClientMetadata).forEach(([key, value]) => {
257+
expect(response.body[key]).toEqual(value);
258+
});
259+
});
260+
261+
it('includes CORS headers in response', async () => {
262+
const response = await supertest(app)
263+
.post('/register')
264+
.set('Origin', 'https://example.com')
265+
.send({
266+
redirect_uris: ['https://example.com/callback']
267+
});
268+
269+
expect(response.header['access-control-allow-origin']).toBe('*');
270+
});
271+
});
272+
});

0 commit comments

Comments
 (0)