mcp-oauth-provider 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +668 -0
- package/dist/__tests__/config.test.js +56 -0
- package/dist/__tests__/config.test.js.map +1 -0
- package/dist/__tests__/integration.test.js +341 -0
- package/dist/__tests__/integration.test.js.map +1 -0
- package/dist/__tests__/oauth-flow.test.js +201 -0
- package/dist/__tests__/oauth-flow.test.js.map +1 -0
- package/dist/__tests__/server.test.js +271 -0
- package/dist/__tests__/server.test.js.map +1 -0
- package/dist/__tests__/storage.test.js +256 -0
- package/dist/__tests__/storage.test.js.map +1 -0
- package/dist/client/config.js +30 -0
- package/dist/client/config.js.map +1 -0
- package/dist/client/factory.js +16 -0
- package/dist/client/factory.js.map +1 -0
- package/dist/client/index.js +237 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/oauth-flow.js +73 -0
- package/dist/client/oauth-flow.js.map +1 -0
- package/dist/client/storage.js +237 -0
- package/dist/client/storage.js.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/server/callback.js +164 -0
- package/dist/server/callback.js.map +1 -0
- package/dist/server/index.js +8 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/templates.js +245 -0
- package/dist/server/templates.js.map +1 -0
- package/package.json +66 -0
- package/src/__tests__/config.test.ts +78 -0
- package/src/__tests__/integration.test.ts +398 -0
- package/src/__tests__/oauth-flow.test.ts +276 -0
- package/src/__tests__/server.test.ts +391 -0
- package/src/__tests__/storage.test.ts +329 -0
- package/src/client/config.ts +134 -0
- package/src/client/factory.ts +19 -0
- package/src/client/index.ts +361 -0
- package/src/client/oauth-flow.ts +115 -0
- package/src/client/storage.ts +335 -0
- package/src/index.ts +31 -0
- package/src/server/callback.ts +257 -0
- package/src/server/index.ts +21 -0
- package/src/server/templates.ts +271 -0
@@ -0,0 +1,78 @@
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
2
|
+
import {
|
3
|
+
DEFAULT_CLIENT_METADATA,
|
4
|
+
generateSessionId,
|
5
|
+
generateState,
|
6
|
+
} from '../client/config.js';
|
7
|
+
|
8
|
+
describe('config utilities', () => {
|
9
|
+
describe('generateSessionId', () => {
|
10
|
+
test('should generate a valid UUID v4', () => {
|
11
|
+
const sessionId = generateSessionId();
|
12
|
+
const uuidRegex =
|
13
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
14
|
+
|
15
|
+
expect(sessionId).toMatch(uuidRegex);
|
16
|
+
});
|
17
|
+
|
18
|
+
test('should generate unique session IDs', () => {
|
19
|
+
const id1 = generateSessionId();
|
20
|
+
const id2 = generateSessionId();
|
21
|
+
|
22
|
+
expect(id1).not.toBe(id2);
|
23
|
+
});
|
24
|
+
});
|
25
|
+
|
26
|
+
describe('generateState', () => {
|
27
|
+
test('should generate a random state string', () => {
|
28
|
+
const state = generateState();
|
29
|
+
|
30
|
+
expect(state).toBeString();
|
31
|
+
expect(state.length).toBeGreaterThan(0);
|
32
|
+
});
|
33
|
+
|
34
|
+
test('should generate unique state values', () => {
|
35
|
+
const state1 = generateState();
|
36
|
+
const state2 = generateState();
|
37
|
+
|
38
|
+
expect(state1).not.toBe(state2);
|
39
|
+
});
|
40
|
+
|
41
|
+
test('should generate URL-safe strings', () => {
|
42
|
+
const state = generateState();
|
43
|
+
|
44
|
+
// Should only contain alphanumeric and URL-safe characters
|
45
|
+
expect(state).toMatch(/^[A-Za-z0-9_-]+$/);
|
46
|
+
});
|
47
|
+
});
|
48
|
+
|
49
|
+
describe('DEFAULT_CLIENT_METADATA', () => {
|
50
|
+
test('should have required OAuth client metadata fields', () => {
|
51
|
+
expect(DEFAULT_CLIENT_METADATA).toHaveProperty('client_name');
|
52
|
+
expect(DEFAULT_CLIENT_METADATA).toHaveProperty('grant_types');
|
53
|
+
expect(DEFAULT_CLIENT_METADATA).toHaveProperty('response_types');
|
54
|
+
expect(DEFAULT_CLIENT_METADATA).toHaveProperty('scope');
|
55
|
+
expect(DEFAULT_CLIENT_METADATA).toHaveProperty(
|
56
|
+
'token_endpoint_auth_method'
|
57
|
+
);
|
58
|
+
});
|
59
|
+
|
60
|
+
test('should include authorization_code grant type', () => {
|
61
|
+
expect(DEFAULT_CLIENT_METADATA.grant_types).toContain(
|
62
|
+
'authorization_code'
|
63
|
+
);
|
64
|
+
});
|
65
|
+
|
66
|
+
test('should include refresh_token grant type', () => {
|
67
|
+
expect(DEFAULT_CLIENT_METADATA.grant_types).toContain('refresh_token');
|
68
|
+
});
|
69
|
+
|
70
|
+
test('should use code response type', () => {
|
71
|
+
expect(DEFAULT_CLIENT_METADATA.response_types).toContain('code');
|
72
|
+
});
|
73
|
+
|
74
|
+
test('should request openid scope by default', () => {
|
75
|
+
expect(DEFAULT_CLIENT_METADATA.scope).toContain('openid');
|
76
|
+
});
|
77
|
+
});
|
78
|
+
});
|
@@ -0,0 +1,398 @@
|
|
1
|
+
import { beforeEach, describe, expect, test } from 'bun:test';
|
2
|
+
import type { OAuthConfig, OAuthTokens } from '../client/config.js';
|
3
|
+
import { MCPOAuthClientProvider } from '../client/index.js';
|
4
|
+
import { MemoryStorage } from '../client/storage.js';
|
5
|
+
|
6
|
+
describe('MCPOAuthClientProvider Integration', () => {
|
7
|
+
let config: OAuthConfig;
|
8
|
+
let storage: MemoryStorage;
|
9
|
+
|
10
|
+
beforeEach(() => {
|
11
|
+
storage = new MemoryStorage();
|
12
|
+
config = {
|
13
|
+
redirectUri: 'http://localhost:8080/callback',
|
14
|
+
scope: 'openid profile email',
|
15
|
+
storage,
|
16
|
+
};
|
17
|
+
});
|
18
|
+
|
19
|
+
describe('initialization', () => {
|
20
|
+
test('should create provider with minimal config', () => {
|
21
|
+
const provider = new MCPOAuthClientProvider(config);
|
22
|
+
|
23
|
+
expect(provider).toBeDefined();
|
24
|
+
expect(provider.redirectUrl).toBe(config.redirectUri);
|
25
|
+
});
|
26
|
+
|
27
|
+
test('should generate session ID if not provided', () => {
|
28
|
+
const provider1 = new MCPOAuthClientProvider(config);
|
29
|
+
const provider2 = new MCPOAuthClientProvider(config);
|
30
|
+
|
31
|
+
// Each provider should have its own session ID
|
32
|
+
// We can't directly access sessionId, but we can verify they work independently
|
33
|
+
expect(provider1).not.toBe(provider2);
|
34
|
+
});
|
35
|
+
|
36
|
+
test('should use provided session ID', () => {
|
37
|
+
const sessionId = 'test-session-123';
|
38
|
+
const provider = new MCPOAuthClientProvider({
|
39
|
+
...config,
|
40
|
+
sessionId,
|
41
|
+
});
|
42
|
+
|
43
|
+
expect(provider).toBeDefined();
|
44
|
+
});
|
45
|
+
|
46
|
+
test('should use provided client credentials', async () => {
|
47
|
+
const provider = new MCPOAuthClientProvider({
|
48
|
+
...config,
|
49
|
+
clientId: 'test-client-id',
|
50
|
+
clientSecret: 'test-secret',
|
51
|
+
});
|
52
|
+
|
53
|
+
const clientInfo = await provider.clientInformation();
|
54
|
+
|
55
|
+
expect(clientInfo).toEqual({
|
56
|
+
client_id: 'test-client-id',
|
57
|
+
client_secret: 'test-secret',
|
58
|
+
});
|
59
|
+
});
|
60
|
+
});
|
61
|
+
|
62
|
+
describe('OAuth state management', () => {
|
63
|
+
test('should generate unique state values', async () => {
|
64
|
+
const provider = new MCPOAuthClientProvider(config);
|
65
|
+
const state1 = await provider.state();
|
66
|
+
const state2 = await provider.state();
|
67
|
+
|
68
|
+
expect(state1).not.toBe(state2);
|
69
|
+
expect(state1).toBeString();
|
70
|
+
expect(state2).toBeString();
|
71
|
+
});
|
72
|
+
});
|
73
|
+
|
74
|
+
describe('client metadata', () => {
|
75
|
+
test('should provide default client metadata', () => {
|
76
|
+
const provider = new MCPOAuthClientProvider(config);
|
77
|
+
const metadata = provider.clientMetadata;
|
78
|
+
|
79
|
+
expect(metadata).toHaveProperty('client_name');
|
80
|
+
expect(metadata).toHaveProperty('grant_types');
|
81
|
+
expect(metadata).toHaveProperty('response_types');
|
82
|
+
expect(metadata.redirect_uris).toContain(config.redirectUri);
|
83
|
+
});
|
84
|
+
|
85
|
+
test('should merge custom metadata with defaults', () => {
|
86
|
+
const customMetadata = {
|
87
|
+
client_name: 'Custom OAuth Client',
|
88
|
+
};
|
89
|
+
const provider = new MCPOAuthClientProvider({
|
90
|
+
...config,
|
91
|
+
clientMetadata: customMetadata,
|
92
|
+
});
|
93
|
+
const metadata = provider.clientMetadata;
|
94
|
+
|
95
|
+
expect(metadata.client_name).toBe('Custom OAuth Client');
|
96
|
+
expect(metadata.grant_types).toBeDefined(); // Should still have defaults
|
97
|
+
});
|
98
|
+
});
|
99
|
+
|
100
|
+
describe('authorization server metadata', () => {
|
101
|
+
test('should accept authorizationServerMetadata through constructor', () => {
|
102
|
+
const metadata = {
|
103
|
+
issuer: 'https://auth.example.com',
|
104
|
+
authorization_endpoint: 'https://auth.example.com/authorize',
|
105
|
+
token_endpoint: 'https://auth.example.com/token',
|
106
|
+
};
|
107
|
+
const provider = new MCPOAuthClientProvider({
|
108
|
+
...config,
|
109
|
+
authorizationServerMetadata: metadata,
|
110
|
+
});
|
111
|
+
|
112
|
+
expect(provider.authorizationServerMetadata).toBeDefined();
|
113
|
+
expect(provider.authorizationServerMetadata?.issuer).toBe(
|
114
|
+
'https://auth.example.com'
|
115
|
+
);
|
116
|
+
expect(provider.authorizationServerMetadata?.token_endpoint).toBe(
|
117
|
+
'https://auth.example.com/token'
|
118
|
+
);
|
119
|
+
});
|
120
|
+
|
121
|
+
test('should be undefined when not provided', () => {
|
122
|
+
const provider = new MCPOAuthClientProvider(config);
|
123
|
+
|
124
|
+
expect(provider.authorizationServerMetadata).toBeUndefined();
|
125
|
+
});
|
126
|
+
});
|
127
|
+
|
128
|
+
describe('token management', () => {
|
129
|
+
test('should return undefined when no tokens exist', async () => {
|
130
|
+
const provider = new MCPOAuthClientProvider(config);
|
131
|
+
const tokens = await provider.tokens();
|
132
|
+
|
133
|
+
expect(tokens).toBeUndefined();
|
134
|
+
});
|
135
|
+
|
136
|
+
test('should save and retrieve tokens', async () => {
|
137
|
+
const provider = new MCPOAuthClientProvider(config);
|
138
|
+
const mockTokens = {
|
139
|
+
access_token: 'test-access-token',
|
140
|
+
token_type: 'Bearer' as const,
|
141
|
+
expires_in: 3600,
|
142
|
+
refresh_token: 'test-refresh-token',
|
143
|
+
};
|
144
|
+
|
145
|
+
await provider.saveTokens(mockTokens);
|
146
|
+
const retrieved = await provider.tokens();
|
147
|
+
|
148
|
+
expect(retrieved).toEqual(mockTokens);
|
149
|
+
});
|
150
|
+
|
151
|
+
test('should clear tokens', async () => {
|
152
|
+
const provider = new MCPOAuthClientProvider(config);
|
153
|
+
const mockTokens = {
|
154
|
+
access_token: 'test-access-token',
|
155
|
+
token_type: 'Bearer' as const,
|
156
|
+
};
|
157
|
+
|
158
|
+
await provider.saveTokens(mockTokens);
|
159
|
+
await provider.invalidateCredentials('tokens');
|
160
|
+
const tokens = await provider.tokens();
|
161
|
+
|
162
|
+
expect(tokens).toBeUndefined();
|
163
|
+
});
|
164
|
+
|
165
|
+
test('should initialize tokens from config and allow updates', async () => {
|
166
|
+
const initialTokens = {
|
167
|
+
access_token: 'initial-token',
|
168
|
+
token_type: 'Bearer' as const,
|
169
|
+
refresh_token: 'initial-refresh',
|
170
|
+
};
|
171
|
+
const provider = new MCPOAuthClientProvider({
|
172
|
+
...config,
|
173
|
+
tokens: initialTokens,
|
174
|
+
});
|
175
|
+
|
176
|
+
// Should return initial tokens from config on first access
|
177
|
+
const tokens1 = await provider.tokens();
|
178
|
+
|
179
|
+
expect(tokens1).toEqual(initialTokens);
|
180
|
+
|
181
|
+
// Should be able to update tokens
|
182
|
+
const newTokens = {
|
183
|
+
access_token: 'new-token',
|
184
|
+
token_type: 'Bearer' as const,
|
185
|
+
refresh_token: 'new-refresh',
|
186
|
+
};
|
187
|
+
|
188
|
+
await provider.saveTokens(newTokens);
|
189
|
+
|
190
|
+
// Should return updated tokens (storage takes precedence after initialization)
|
191
|
+
const tokens2 = await provider.tokens();
|
192
|
+
|
193
|
+
expect(tokens2).toEqual(newTokens);
|
194
|
+
expect(tokens2?.access_token).not.toBe(initialTokens.access_token);
|
195
|
+
});
|
196
|
+
});
|
197
|
+
|
198
|
+
describe('client information management', () => {
|
199
|
+
test('should return undefined when no client info exists and no static credentials', async () => {
|
200
|
+
const provider = new MCPOAuthClientProvider(config);
|
201
|
+
const clientInfo = await provider.clientInformation();
|
202
|
+
|
203
|
+
expect(clientInfo).toBeUndefined();
|
204
|
+
});
|
205
|
+
|
206
|
+
test('should save and retrieve client information from dynamic registration', async () => {
|
207
|
+
const provider = new MCPOAuthClientProvider(config);
|
208
|
+
const clientInfo = {
|
209
|
+
client_id: 'dynamic-client-id',
|
210
|
+
client_secret: 'dynamic-secret',
|
211
|
+
redirect_uris: [config.redirectUri],
|
212
|
+
client_id_issued_at: Date.now(),
|
213
|
+
};
|
214
|
+
|
215
|
+
await provider.saveClientInformation(clientInfo);
|
216
|
+
const retrieved = await provider.clientInformation();
|
217
|
+
|
218
|
+
expect(retrieved).toMatchObject({
|
219
|
+
client_id: clientInfo.client_id,
|
220
|
+
client_secret: clientInfo.client_secret,
|
221
|
+
});
|
222
|
+
});
|
223
|
+
|
224
|
+
test('should prefer static credentials over stored credentials', async () => {
|
225
|
+
const staticCreds = {
|
226
|
+
clientId: 'static-id',
|
227
|
+
clientSecret: 'static-secret',
|
228
|
+
};
|
229
|
+
const provider = new MCPOAuthClientProvider({
|
230
|
+
...config,
|
231
|
+
...staticCreds,
|
232
|
+
});
|
233
|
+
|
234
|
+
// Save dynamic credentials
|
235
|
+
await provider.saveClientInformation({
|
236
|
+
client_id: 'dynamic-id',
|
237
|
+
client_secret: 'dynamic-secret',
|
238
|
+
redirect_uris: [config.redirectUri],
|
239
|
+
});
|
240
|
+
|
241
|
+
// Should return static credentials
|
242
|
+
const clientInfo = await provider.clientInformation();
|
243
|
+
|
244
|
+
expect(clientInfo?.client_id).toBe(staticCreds.clientId);
|
245
|
+
});
|
246
|
+
});
|
247
|
+
|
248
|
+
describe('storage isolation', () => {
|
249
|
+
test('should isolate tokens between different sessions', async () => {
|
250
|
+
const provider1 = new MCPOAuthClientProvider({
|
251
|
+
...config,
|
252
|
+
sessionId: 'session-1',
|
253
|
+
});
|
254
|
+
const provider2 = new MCPOAuthClientProvider({
|
255
|
+
...config,
|
256
|
+
sessionId: 'session-2',
|
257
|
+
});
|
258
|
+
const tokens1 = {
|
259
|
+
access_token: 'token-1',
|
260
|
+
token_type: 'Bearer' as const,
|
261
|
+
};
|
262
|
+
const tokens2 = {
|
263
|
+
access_token: 'token-2',
|
264
|
+
token_type: 'Bearer' as const,
|
265
|
+
};
|
266
|
+
|
267
|
+
await provider1.saveTokens(tokens1);
|
268
|
+
await provider2.saveTokens(tokens2);
|
269
|
+
const retrieved1 = await provider1.tokens();
|
270
|
+
const retrieved2 = await provider2.tokens();
|
271
|
+
|
272
|
+
expect(retrieved1?.access_token).toBe('token-1');
|
273
|
+
expect(retrieved2?.access_token).toBe('token-2');
|
274
|
+
});
|
275
|
+
});
|
276
|
+
|
277
|
+
describe('automatic token refresh', () => {
|
278
|
+
test('should automatically refresh expired tokens when metadata is available', async () => {
|
279
|
+
const provider = new MCPOAuthClientProvider({
|
280
|
+
...config,
|
281
|
+
clientId: 'test-client',
|
282
|
+
clientSecret: 'test-secret',
|
283
|
+
});
|
284
|
+
|
285
|
+
// Set authorization server metadata with token endpoint
|
286
|
+
provider.authorizationServerMetadata = {
|
287
|
+
issuer: 'https://auth.example.com',
|
288
|
+
authorization_endpoint: 'https://auth.example.com/authorize',
|
289
|
+
token_endpoint: 'https://auth.example.com/token',
|
290
|
+
response_types_supported: ['code'],
|
291
|
+
grant_types_supported: ['authorization_code', 'refresh_token'],
|
292
|
+
code_challenge_methods_supported: ['S256'],
|
293
|
+
};
|
294
|
+
|
295
|
+
// Save expired tokens with refresh token
|
296
|
+
const expiredTokens: OAuthTokens = {
|
297
|
+
access_token: 'expired-token',
|
298
|
+
token_type: 'Bearer',
|
299
|
+
expires_in: 100, // Less than 5 minute buffer, will trigger refresh
|
300
|
+
refresh_token: 'refresh-token-123',
|
301
|
+
};
|
302
|
+
|
303
|
+
await provider.saveTokens(expiredTokens);
|
304
|
+
|
305
|
+
// Mock the refresh (we can't easily test the actual HTTP call in unit test)
|
306
|
+
// The tokens() method should detect expired tokens and attempt refresh
|
307
|
+
const tokens = await provider.tokens();
|
308
|
+
|
309
|
+
// Should return the expired tokens (refresh will fail due to no network mock)
|
310
|
+
// but the attempt to refresh should have been made
|
311
|
+
expect(tokens).toBeDefined();
|
312
|
+
expect(tokens?.access_token).toBe('expired-token');
|
313
|
+
});
|
314
|
+
|
315
|
+
test('should return tokens immediately if not expired', async () => {
|
316
|
+
const provider = new MCPOAuthClientProvider({
|
317
|
+
...config,
|
318
|
+
clientId: 'test-client',
|
319
|
+
clientSecret: 'test-secret',
|
320
|
+
});
|
321
|
+
|
322
|
+
// Save valid tokens that won't expire soon
|
323
|
+
const validTokens: OAuthTokens = {
|
324
|
+
access_token: 'valid-token',
|
325
|
+
token_type: 'Bearer',
|
326
|
+
expires_in: 3600, // 1 hour, well beyond 5 minute buffer
|
327
|
+
refresh_token: 'refresh-token-123',
|
328
|
+
};
|
329
|
+
|
330
|
+
await provider.saveTokens(validTokens);
|
331
|
+
|
332
|
+
// Should return tokens without attempting refresh
|
333
|
+
const tokens = await provider.tokens();
|
334
|
+
|
335
|
+
expect(tokens).toBeDefined();
|
336
|
+
expect(tokens?.access_token).toBe('valid-token');
|
337
|
+
});
|
338
|
+
|
339
|
+
test('should not attempt refresh if no refresh token available', async () => {
|
340
|
+
const provider = new MCPOAuthClientProvider({
|
341
|
+
...config,
|
342
|
+
clientId: 'test-client',
|
343
|
+
clientSecret: 'test-secret',
|
344
|
+
});
|
345
|
+
|
346
|
+
provider.authorizationServerMetadata = {
|
347
|
+
issuer: 'https://auth.example.com',
|
348
|
+
authorization_endpoint: 'https://auth.example.com/authorize',
|
349
|
+
token_endpoint: 'https://auth.example.com/token',
|
350
|
+
response_types_supported: ['code'],
|
351
|
+
grant_types_supported: ['authorization_code', 'refresh_token'],
|
352
|
+
code_challenge_methods_supported: ['S256'],
|
353
|
+
};
|
354
|
+
|
355
|
+
// Save expired tokens WITHOUT refresh token
|
356
|
+
const expiredTokens: OAuthTokens = {
|
357
|
+
access_token: 'expired-token',
|
358
|
+
token_type: 'Bearer',
|
359
|
+
expires_in: 100, // Less than 5 minute buffer
|
360
|
+
// No refresh_token
|
361
|
+
};
|
362
|
+
|
363
|
+
await provider.saveTokens(expiredTokens);
|
364
|
+
|
365
|
+
// Should return expired tokens without attempting refresh
|
366
|
+
const tokens = await provider.tokens();
|
367
|
+
|
368
|
+
expect(tokens).toBeDefined();
|
369
|
+
expect(tokens?.access_token).toBe('expired-token');
|
370
|
+
});
|
371
|
+
|
372
|
+
test('should not attempt refresh if no metadata available', async () => {
|
373
|
+
const provider = new MCPOAuthClientProvider({
|
374
|
+
...config,
|
375
|
+
clientId: 'test-client',
|
376
|
+
clientSecret: 'test-secret',
|
377
|
+
});
|
378
|
+
|
379
|
+
// No authorizationServerMetadata set
|
380
|
+
|
381
|
+
// Save expired tokens with refresh token
|
382
|
+
const expiredTokens: OAuthTokens = {
|
383
|
+
access_token: 'expired-token',
|
384
|
+
token_type: 'Bearer',
|
385
|
+
expires_in: 100, // Less than 5 minute buffer
|
386
|
+
refresh_token: 'refresh-token-123',
|
387
|
+
};
|
388
|
+
|
389
|
+
await provider.saveTokens(expiredTokens);
|
390
|
+
|
391
|
+
// Should return expired tokens without attempting refresh (no metadata)
|
392
|
+
const tokens = await provider.tokens();
|
393
|
+
|
394
|
+
expect(tokens).toBeDefined();
|
395
|
+
expect(tokens?.access_token).toBe('expired-token');
|
396
|
+
});
|
397
|
+
});
|
398
|
+
});
|