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.
Files changed (44) hide show
  1. package/README.md +668 -0
  2. package/dist/__tests__/config.test.js +56 -0
  3. package/dist/__tests__/config.test.js.map +1 -0
  4. package/dist/__tests__/integration.test.js +341 -0
  5. package/dist/__tests__/integration.test.js.map +1 -0
  6. package/dist/__tests__/oauth-flow.test.js +201 -0
  7. package/dist/__tests__/oauth-flow.test.js.map +1 -0
  8. package/dist/__tests__/server.test.js +271 -0
  9. package/dist/__tests__/server.test.js.map +1 -0
  10. package/dist/__tests__/storage.test.js +256 -0
  11. package/dist/__tests__/storage.test.js.map +1 -0
  12. package/dist/client/config.js +30 -0
  13. package/dist/client/config.js.map +1 -0
  14. package/dist/client/factory.js +16 -0
  15. package/dist/client/factory.js.map +1 -0
  16. package/dist/client/index.js +237 -0
  17. package/dist/client/index.js.map +1 -0
  18. package/dist/client/oauth-flow.js +73 -0
  19. package/dist/client/oauth-flow.js.map +1 -0
  20. package/dist/client/storage.js +237 -0
  21. package/dist/client/storage.js.map +1 -0
  22. package/dist/index.js +12 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/server/callback.js +164 -0
  25. package/dist/server/callback.js.map +1 -0
  26. package/dist/server/index.js +8 -0
  27. package/dist/server/index.js.map +1 -0
  28. package/dist/server/templates.js +245 -0
  29. package/dist/server/templates.js.map +1 -0
  30. package/package.json +66 -0
  31. package/src/__tests__/config.test.ts +78 -0
  32. package/src/__tests__/integration.test.ts +398 -0
  33. package/src/__tests__/oauth-flow.test.ts +276 -0
  34. package/src/__tests__/server.test.ts +391 -0
  35. package/src/__tests__/storage.test.ts +329 -0
  36. package/src/client/config.ts +134 -0
  37. package/src/client/factory.ts +19 -0
  38. package/src/client/index.ts +361 -0
  39. package/src/client/oauth-flow.ts +115 -0
  40. package/src/client/storage.ts +335 -0
  41. package/src/index.ts +31 -0
  42. package/src/server/callback.ts +257 -0
  43. package/src/server/index.ts +21 -0
  44. package/src/server/templates.ts +271 -0
@@ -0,0 +1,329 @@
1
+ import { beforeEach, describe, expect, test } from 'bun:test';
2
+ import type { OAuthTokens } from '../client/config.js';
3
+ import {
4
+ FileStorage,
5
+ MemoryStorage,
6
+ OAuthStorage,
7
+ createStorageAdapter,
8
+ } from '../client/storage.js';
9
+
10
+ describe('MemoryStorage', () => {
11
+ let storage: MemoryStorage;
12
+
13
+ beforeEach(() => {
14
+ storage = new MemoryStorage();
15
+ });
16
+
17
+ test('should store and retrieve values', async () => {
18
+ await storage.set('test-key', 'test-value');
19
+ const value = await storage.get('test-key');
20
+
21
+ expect(value).toBe('test-value');
22
+ });
23
+
24
+ test('should return undefined for missing keys', async () => {
25
+ const value = await storage.get('non-existent');
26
+
27
+ expect(value).toBeUndefined();
28
+ });
29
+
30
+ test('should delete values', async () => {
31
+ await storage.set('test-key', 'test-value');
32
+ await storage.delete('test-key');
33
+ const value = await storage.get('test-key');
34
+
35
+ expect(value).toBeUndefined();
36
+ });
37
+
38
+ test('should clear all data', async () => {
39
+ await storage.set('key1', 'value1');
40
+ await storage.set('key2', 'value2');
41
+ storage.clear();
42
+ const value1 = await storage.get('key1');
43
+ const value2 = await storage.get('key2');
44
+
45
+ expect(value1).toBeUndefined();
46
+ expect(value2).toBeUndefined();
47
+ });
48
+
49
+ test('should handle multiple values', async () => {
50
+ await storage.set('key1', 'value1');
51
+ await storage.set('key2', 'value2');
52
+ await storage.set('key3', 'value3');
53
+ const value1 = await storage.get('key1');
54
+ const value2 = await storage.get('key2');
55
+ const value3 = await storage.get('key3');
56
+
57
+ expect(value1).toBe('value1');
58
+ expect(value2).toBe('value2');
59
+ expect(value3).toBe('value3');
60
+ });
61
+ });
62
+
63
+ describe('FileStorage', () => {
64
+ let storage: FileStorage;
65
+ const testPath = './test-oauth-data';
66
+
67
+ beforeEach(async () => {
68
+ storage = new FileStorage(testPath);
69
+ await storage.clear(); // Clean up before each test
70
+ });
71
+
72
+ test('should store and retrieve values', async () => {
73
+ await storage.set('test-key', 'test-value');
74
+ const value = await storage.get('test-key');
75
+
76
+ expect(value).toBe('test-value');
77
+ });
78
+
79
+ test('should return undefined for missing keys', async () => {
80
+ const value = await storage.get('non-existent');
81
+
82
+ expect(value).toBeUndefined();
83
+ });
84
+
85
+ test('should delete values', async () => {
86
+ await storage.set('test-key', 'test-value');
87
+ await storage.delete('test-key');
88
+ const value = await storage.get('test-key');
89
+
90
+ expect(value).toBeUndefined();
91
+ });
92
+
93
+ test('should sanitize keys for filenames', async () => {
94
+ const unsafeKey = 'test/key:with@special#chars';
95
+
96
+ await storage.set(unsafeKey, 'test-value');
97
+ const value = await storage.get(unsafeKey);
98
+
99
+ expect(value).toBe('test-value');
100
+ });
101
+
102
+ test('should handle multiple values', async () => {
103
+ await storage.set('key1', 'value1');
104
+ await storage.set('key2', 'value2');
105
+ const value1 = await storage.get('key1');
106
+ const value2 = await storage.get('key2');
107
+
108
+ expect(value1).toBe('value1');
109
+ expect(value2).toBe('value2');
110
+ });
111
+
112
+ test('should clear all data', async () => {
113
+ await storage.set('key1', 'value1');
114
+ await storage.set('key2', 'value2');
115
+ await storage.clear();
116
+ const value1 = await storage.get('key1');
117
+ const value2 = await storage.get('key2');
118
+
119
+ expect(value1).toBeUndefined();
120
+ expect(value2).toBeUndefined();
121
+ });
122
+ });
123
+
124
+ describe('createStorageAdapter', () => {
125
+ test('should create MemoryStorage by default', () => {
126
+ const storage = createStorageAdapter();
127
+
128
+ expect(storage).toBeInstanceOf(MemoryStorage);
129
+ });
130
+
131
+ test('should create MemoryStorage when type is "memory"', () => {
132
+ const storage = createStorageAdapter('memory');
133
+
134
+ expect(storage).toBeInstanceOf(MemoryStorage);
135
+ });
136
+
137
+ test('should create FileStorage when type is "file"', () => {
138
+ const storage = createStorageAdapter('file');
139
+
140
+ expect(storage).toBeInstanceOf(FileStorage);
141
+ });
142
+
143
+ test('should use custom path for FileStorage', async () => {
144
+ const customPath = './custom-oauth-path';
145
+ const storage = createStorageAdapter('file', { path: customPath });
146
+
147
+ expect(storage).toBeInstanceOf(FileStorage);
148
+
149
+ // Clean up
150
+ if (storage instanceof FileStorage) {
151
+ await storage.clear();
152
+ }
153
+ });
154
+ });
155
+
156
+ describe('OAuthStorage', () => {
157
+ let storage: MemoryStorage;
158
+ let oauthStorage: OAuthStorage;
159
+ const sessionId = 'test-session-id';
160
+
161
+ beforeEach(() => {
162
+ storage = new MemoryStorage();
163
+ oauthStorage = new OAuthStorage(storage, sessionId);
164
+ });
165
+
166
+ describe('token management', () => {
167
+ test('should save and retrieve tokens', async () => {
168
+ const tokens: OAuthTokens = {
169
+ access_token: 'test-access-token',
170
+ token_type: 'Bearer',
171
+ expires_in: 3600,
172
+ refresh_token: 'test-refresh-token',
173
+ };
174
+
175
+ await oauthStorage.saveTokens(tokens);
176
+ const retrieved = await oauthStorage.getTokens();
177
+
178
+ expect(retrieved).toBeDefined();
179
+ expect(retrieved?.access_token).toBe(tokens.access_token);
180
+ expect(retrieved?.token_type).toBe(tokens.token_type);
181
+ expect(retrieved?.refresh_token).toBe(tokens.refresh_token);
182
+ // expires_in should be close to original value (within 5 seconds due to time passing)
183
+ expect(retrieved?.expires_in).toBeGreaterThanOrEqual(3595);
184
+ expect(retrieved?.expires_in).toBeLessThanOrEqual(3600);
185
+ });
186
+
187
+ test('should return undefined when no tokens exist', async () => {
188
+ const tokens = await oauthStorage.getTokens();
189
+
190
+ expect(tokens).toBeUndefined();
191
+ });
192
+
193
+ test('should clear tokens', async () => {
194
+ const tokens: OAuthTokens = {
195
+ access_token: 'test-access-token',
196
+ token_type: 'Bearer',
197
+ };
198
+
199
+ await oauthStorage.saveTokens(tokens);
200
+ await oauthStorage.clearTokens();
201
+ const retrieved = await oauthStorage.getTokens();
202
+
203
+ expect(retrieved).toBeUndefined();
204
+ });
205
+
206
+ test('should handle tokens without optional fields', async () => {
207
+ const tokens: OAuthTokens = {
208
+ access_token: 'test-access-token',
209
+ token_type: 'Bearer',
210
+ };
211
+
212
+ await oauthStorage.saveTokens(tokens);
213
+ const retrieved = await oauthStorage.getTokens();
214
+
215
+ expect(retrieved).toEqual(tokens);
216
+ });
217
+ });
218
+
219
+ describe('client info management', () => {
220
+ test('should save and retrieve client info', async () => {
221
+ const clientInfo = {
222
+ client_id: 'test-client-id',
223
+ client_secret: 'test-client-secret',
224
+ redirect_uris: ['http://localhost:8080/callback'],
225
+ };
226
+
227
+ await oauthStorage.saveClientInfo(clientInfo);
228
+ const retrieved = await oauthStorage.getClientInfo();
229
+
230
+ expect(retrieved).toEqual(clientInfo);
231
+ });
232
+
233
+ test('should return undefined when no client info exists', async () => {
234
+ const clientInfo = await oauthStorage.getClientInfo();
235
+
236
+ expect(clientInfo).toBeUndefined();
237
+ });
238
+
239
+ test('should handle client info without secret', async () => {
240
+ const clientInfo = {
241
+ client_id: 'test-client-id',
242
+ redirect_uris: ['http://localhost:8080/callback'],
243
+ };
244
+
245
+ await oauthStorage.saveClientInfo(clientInfo);
246
+ const retrieved = await oauthStorage.getClientInfo();
247
+
248
+ expect(retrieved).toEqual(clientInfo);
249
+ });
250
+ });
251
+
252
+ describe('code verifier management', () => {
253
+ test('should save and retrieve code verifier', async () => {
254
+ const verifier = 'test-code-verifier-12345';
255
+
256
+ await oauthStorage.saveCodeVerifier(verifier);
257
+ const retrieved = await oauthStorage.getCodeVerifier();
258
+
259
+ expect(retrieved).toBe(verifier);
260
+ });
261
+
262
+ test('should return undefined when no code verifier exists', async () => {
263
+ const verifier = await oauthStorage.getCodeVerifier();
264
+
265
+ expect(verifier).toBeUndefined();
266
+ });
267
+
268
+ test('should clear code verifier', async () => {
269
+ const verifier = 'test-code-verifier-12345';
270
+
271
+ await oauthStorage.saveCodeVerifier(verifier);
272
+ await oauthStorage.clearCodeVerifier();
273
+ const retrieved = await oauthStorage.getCodeVerifier();
274
+
275
+ expect(retrieved).toBeUndefined();
276
+ });
277
+ });
278
+
279
+ describe('session isolation', () => {
280
+ test('should isolate tokens between sessions', async () => {
281
+ const session1 = new OAuthStorage(storage, 'session-1');
282
+ const session2 = new OAuthStorage(storage, 'session-2');
283
+ const tokens1: OAuthTokens = {
284
+ access_token: 'token-1',
285
+ token_type: 'Bearer',
286
+ };
287
+ const tokens2: OAuthTokens = {
288
+ access_token: 'token-2',
289
+ token_type: 'Bearer',
290
+ };
291
+
292
+ await session1.saveTokens(tokens1);
293
+ await session2.saveTokens(tokens2);
294
+ const retrieved1 = await session1.getTokens();
295
+ const retrieved2 = await session2.getTokens();
296
+
297
+ expect(retrieved1).toEqual(tokens1);
298
+ expect(retrieved2).toEqual(tokens2);
299
+ });
300
+
301
+ test('should correctly calculate expires_in over time', async () => {
302
+ const tokens: OAuthTokens = {
303
+ access_token: 'test-access-token',
304
+ token_type: 'Bearer',
305
+ expires_in: 3600, // 1 hour
306
+ refresh_token: 'test-refresh-token',
307
+ };
308
+
309
+ // Save tokens
310
+ await oauthStorage.saveTokens(tokens);
311
+
312
+ // Retrieve immediately - should be close to 3600
313
+ const retrieved1 = await oauthStorage.getTokens();
314
+
315
+ expect(retrieved1?.expires_in).toBeGreaterThanOrEqual(3595);
316
+ expect(retrieved1?.expires_in).toBeLessThanOrEqual(3600);
317
+
318
+ // Wait 2 seconds
319
+ await new Promise(resolve => setTimeout(resolve, 2000));
320
+
321
+ // Retrieve again - should be approximately 2 seconds less
322
+ const retrieved2 = await oauthStorage.getTokens();
323
+
324
+ expect(retrieved2?.expires_in).toBeGreaterThanOrEqual(3593);
325
+ expect(retrieved2?.expires_in).toBeLessThanOrEqual(3598);
326
+ expect(retrieved2?.expires_in).toBeLessThan(retrieved1!.expires_in!);
327
+ });
328
+ });
329
+ });
@@ -0,0 +1,134 @@
1
+ import type {
2
+ OAuthClientInformation,
3
+ OAuthClientInformationFull,
4
+ OAuthClientMetadata,
5
+ OAuthTokens,
6
+ } from '@modelcontextprotocol/sdk/shared/auth.js';
7
+
8
+ export type {
9
+ OAuthClientInformation,
10
+ OAuthClientInformationFull,
11
+ OAuthClientMetadata,
12
+ OAuthTokens,
13
+ };
14
+
15
+ /**
16
+ * Storage adapter interface for persisting OAuth data
17
+ */
18
+ export interface StorageAdapter {
19
+ /**
20
+ * Get a value by key
21
+ */
22
+ get(key: string): Promise<string | undefined> | string | undefined;
23
+
24
+ /**
25
+ * Set a value by key
26
+ */
27
+ set(key: string, value: string): Promise<void> | void;
28
+
29
+ /**
30
+ * Delete a value by key
31
+ */
32
+ delete(key: string): Promise<void> | void;
33
+ }
34
+
35
+ /**
36
+ * Configuration for OAuth client provider
37
+ */
38
+ export interface OAuthConfig {
39
+ /**
40
+ * OAuth client ID (can be provided or dynamically registered)
41
+ */
42
+ clientId?: string;
43
+
44
+ /**
45
+ * OAuth client secret (optional for public clients)
46
+ */
47
+ clientSecret?: string;
48
+
49
+ /**
50
+ * Redirect URI for OAuth callbacks
51
+ */
52
+ redirectUri: string;
53
+
54
+ /**
55
+ * OAuth scope to request
56
+ */
57
+ scope?: string;
58
+
59
+ /**
60
+ * Session identifier for this OAuth client instance
61
+ */
62
+ sessionId?: string;
63
+
64
+ /**
65
+ * Storage adapter for persisting OAuth data
66
+ */
67
+ storage?: StorageAdapter;
68
+
69
+ /**
70
+ * OAuth client metadata for registration
71
+ */
72
+ clientMetadata?: Partial<OAuthClientMetadata>;
73
+
74
+ /**
75
+ * OAuth tokens (can be provided statically or loaded from storage)
76
+ */
77
+ tokens?: OAuthTokens;
78
+
79
+ /**
80
+ * Token refresh configuration
81
+ */
82
+ tokenRefresh?: {
83
+ /**
84
+ * Maximum number of retry attempts for token refresh
85
+ */
86
+ maxRetries?: number;
87
+
88
+ /**
89
+ * Delay between retry attempts in milliseconds
90
+ */
91
+ retryDelay?: number;
92
+ };
93
+
94
+ /**
95
+ * Authorization server metadata (can be provided or obtained during auth flow)
96
+ */
97
+ authorizationServerMetadata?: {
98
+ issuer: string;
99
+ authorization_endpoint: string;
100
+ token_endpoint: string;
101
+ [key: string]: unknown;
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Default OAuth client metadata
107
+ */
108
+ export const DEFAULT_CLIENT_METADATA: OAuthClientMetadata = {
109
+ redirect_uris: [],
110
+ grant_types: ['authorization_code', 'refresh_token'],
111
+ response_types: ['code'],
112
+ token_endpoint_auth_method: 'client_secret_post',
113
+ scope: 'openid profile email',
114
+ client_name: 'MCP OAuth Client',
115
+ client_uri: 'https://github.com/modelcontextprotocol/typescript-sdk',
116
+ };
117
+
118
+ /**
119
+ * Generate a random session ID
120
+ */
121
+ export function generateSessionId(): string {
122
+ return crypto.randomUUID();
123
+ }
124
+
125
+ /**
126
+ * Generate a random state parameter for CSRF protection
127
+ */
128
+ export function generateState(): string {
129
+ const array = new Uint8Array(32);
130
+
131
+ crypto.getRandomValues(array);
132
+
133
+ return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
134
+ }
@@ -0,0 +1,19 @@
1
+ import type { OAuthConfig } from './config.js';
2
+ import { MCPOAuthClientProvider } from './index.js';
3
+
4
+ /**
5
+ * Factory function to create an OAuth client provider
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const provider = createOAuthProvider({
10
+ * redirectUri: 'http://localhost:8080/callback',
11
+ * scope: 'openid profile email',
12
+ * });
13
+ * ```
14
+ */
15
+ export function createOAuthProvider(
16
+ config: OAuthConfig
17
+ ): MCPOAuthClientProvider {
18
+ return new MCPOAuthClientProvider(config);
19
+ }