@flink-app/oauth-plugin 0.12.1-alpha.33

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 (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +783 -0
  3. package/SECURITY.md +433 -0
  4. package/dist/OAuthInternalContext.d.ts +45 -0
  5. package/dist/OAuthInternalContext.js +2 -0
  6. package/dist/OAuthPlugin.d.ts +70 -0
  7. package/dist/OAuthPlugin.js +220 -0
  8. package/dist/OAuthPluginContext.d.ts +49 -0
  9. package/dist/OAuthPluginContext.js +2 -0
  10. package/dist/OAuthPluginOptions.d.ts +111 -0
  11. package/dist/OAuthPluginOptions.js +2 -0
  12. package/dist/index.d.ts +48 -0
  13. package/dist/index.js +66 -0
  14. package/dist/providers/GitHubProvider.d.ts +32 -0
  15. package/dist/providers/GitHubProvider.js +82 -0
  16. package/dist/providers/GoogleProvider.d.ts +32 -0
  17. package/dist/providers/GoogleProvider.js +83 -0
  18. package/dist/providers/OAuthProvider.d.ts +69 -0
  19. package/dist/providers/OAuthProvider.js +2 -0
  20. package/dist/providers/OAuthProviderBase.d.ts +32 -0
  21. package/dist/providers/OAuthProviderBase.js +86 -0
  22. package/dist/providers/ProviderRegistry.d.ts +14 -0
  23. package/dist/providers/ProviderRegistry.js +24 -0
  24. package/dist/repos/OAuthConnectionRepo.d.ts +30 -0
  25. package/dist/repos/OAuthConnectionRepo.js +38 -0
  26. package/dist/repos/OAuthSessionRepo.d.ts +22 -0
  27. package/dist/repos/OAuthSessionRepo.js +28 -0
  28. package/dist/schemas/OAuthConnection.d.ts +12 -0
  29. package/dist/schemas/OAuthConnection.js +2 -0
  30. package/dist/schemas/OAuthSession.d.ts +9 -0
  31. package/dist/schemas/OAuthSession.js +2 -0
  32. package/dist/utils/encryption-utils.d.ts +34 -0
  33. package/dist/utils/encryption-utils.js +134 -0
  34. package/dist/utils/error-utils.d.ts +68 -0
  35. package/dist/utils/error-utils.js +120 -0
  36. package/dist/utils/state-utils.d.ts +36 -0
  37. package/dist/utils/state-utils.js +72 -0
  38. package/examples/api-client-auth.ts +550 -0
  39. package/examples/basic-auth.ts +288 -0
  40. package/examples/multi-provider.ts +409 -0
  41. package/examples/token-storage.ts +490 -0
  42. package/package.json +38 -0
  43. package/spec/OAuthHandlers.spec.ts +146 -0
  44. package/spec/OAuthPluginSpec.ts +31 -0
  45. package/spec/ProvidersSpec.ts +178 -0
  46. package/spec/README.md +365 -0
  47. package/spec/helpers/mockJwtAuthPlugin.ts +104 -0
  48. package/spec/helpers/mockOAuthProviders.ts +189 -0
  49. package/spec/helpers/reporter.ts +41 -0
  50. package/spec/helpers/testDatabase.ts +107 -0
  51. package/spec/helpers/testHelpers.ts +192 -0
  52. package/spec/integration-critical.spec.ts +857 -0
  53. package/spec/integration.spec.ts +301 -0
  54. package/spec/repositories.spec.ts +181 -0
  55. package/spec/support/jasmine.json +7 -0
  56. package/spec/utils/security.spec.ts +243 -0
  57. package/src/OAuthInternalContext.ts +46 -0
  58. package/src/OAuthPlugin.ts +251 -0
  59. package/src/OAuthPluginContext.ts +53 -0
  60. package/src/OAuthPluginOptions.ts +122 -0
  61. package/src/handlers/CallbackOAuth.ts +238 -0
  62. package/src/handlers/InitiateOAuth.ts +99 -0
  63. package/src/index.ts +62 -0
  64. package/src/providers/GitHubProvider.ts +90 -0
  65. package/src/providers/GoogleProvider.ts +91 -0
  66. package/src/providers/OAuthProvider.ts +77 -0
  67. package/src/providers/OAuthProviderBase.ts +98 -0
  68. package/src/providers/ProviderRegistry.ts +27 -0
  69. package/src/repos/OAuthConnectionRepo.ts +41 -0
  70. package/src/repos/OAuthSessionRepo.ts +30 -0
  71. package/src/repos/TTL_INDEX_NOTE.md +28 -0
  72. package/src/schemas/CallbackRequest.ts +64 -0
  73. package/src/schemas/InitiateRequest.ts +10 -0
  74. package/src/schemas/OAuthConnection.ts +12 -0
  75. package/src/schemas/OAuthSession.ts +9 -0
  76. package/src/utils/encryption-utils.ts +148 -0
  77. package/src/utils/error-utils.ts +139 -0
  78. package/src/utils/state-utils.ts +70 -0
  79. package/src/utils/token-response-utils.ts +49 -0
  80. package/src/utils/validation-utils.ts +120 -0
  81. package/tsconfig.dist.json +4 -0
  82. package/tsconfig.json +24 -0
@@ -0,0 +1,550 @@
1
+ /**
2
+ * API Client Integration with response_type=json
3
+ *
4
+ * This example demonstrates:
5
+ * - Using OAuth plugin with mobile apps and SPAs
6
+ * - response_type=json for JSON responses instead of redirects
7
+ * - Handling OAuth flow in API clients
8
+ * - Storing and using JWT tokens in mobile/SPA context
9
+ */
10
+
11
+ import { FlinkApp } from "@flink-app/flink";
12
+ import { jwtAuthPlugin } from "@flink-app/jwt-auth-plugin";
13
+ import { oauthPlugin } from "@flink-app/oauth-plugin";
14
+ import { FlinkContext, FlinkRepo } from "@flink-app/flink";
15
+
16
+ interface User {
17
+ _id?: string;
18
+ email: string;
19
+ name?: string;
20
+ avatarUrl?: string;
21
+ oauthProviders: Array<{
22
+ provider: "github" | "google";
23
+ providerId: string;
24
+ }>;
25
+ roles: string[];
26
+ createdAt: Date;
27
+ updatedAt: Date;
28
+ }
29
+
30
+ class UserRepo extends FlinkRepo<AppContext, User> {
31
+ async findByEmail(email: string) {
32
+ return this.getOne({ email });
33
+ }
34
+ }
35
+
36
+ interface AppContext extends FlinkContext {
37
+ repos: {
38
+ userRepo: UserRepo;
39
+ };
40
+ plugins: {
41
+ jwtAuth: any;
42
+ oauth: any;
43
+ };
44
+ }
45
+
46
+ async function start() {
47
+ const app = new FlinkApp<AppContext>({
48
+ name: "OAuth API Client Example",
49
+ port: 3336,
50
+
51
+ db: {
52
+ uri: process.env.MONGODB_URI || "mongodb://localhost:27017/oauth-api-client-example",
53
+ },
54
+
55
+ auth: jwtAuthPlugin({
56
+ secret: process.env.JWT_SECRET || "your-super-secret-jwt-key",
57
+ getUser: async (tokenData) => {
58
+ const user = await app.ctx.repos.userRepo.getById(tokenData.userId);
59
+ if (!user) throw new Error("User not found");
60
+ return {
61
+ id: user._id,
62
+ email: user.email,
63
+ roles: user.roles,
64
+ };
65
+ },
66
+ rolePermissions: {
67
+ user: ["read:own", "write:own"],
68
+ },
69
+ expiresIn: "30d", // Longer expiration for mobile apps
70
+ }),
71
+
72
+ plugins: [
73
+ oauthPlugin({
74
+ providers: {
75
+ github: {
76
+ clientId: process.env.GITHUB_CLIENT_ID!,
77
+ clientSecret: process.env.GITHUB_CLIENT_SECRET!,
78
+ callbackUrl: process.env.GITHUB_CALLBACK_URL || "http://localhost:3336/oauth/github/callback",
79
+ },
80
+ google: {
81
+ clientId: process.env.GOOGLE_CLIENT_ID!,
82
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
83
+ callbackUrl: process.env.GOOGLE_CALLBACK_URL || "http://localhost:3336/oauth/google/callback",
84
+ },
85
+ },
86
+
87
+ storeTokens: false,
88
+
89
+ onAuthSuccess: async ({ profile, provider }, ctx) => {
90
+ console.log(`\nAPI Client OAuth Success: ${provider} - ${profile.email}`);
91
+
92
+ let user = await ctx.repos.userRepo.findByEmail(profile.email);
93
+
94
+ if (!user) {
95
+ user = await ctx.repos.userRepo.create({
96
+ email: profile.email,
97
+ name: profile.name,
98
+ avatarUrl: profile.avatarUrl,
99
+ oauthProviders: [{ provider, providerId: profile.id }],
100
+ roles: ["user"],
101
+ createdAt: new Date(),
102
+ updatedAt: new Date(),
103
+ });
104
+ }
105
+
106
+ const token = await ctx.plugins.jwtAuth.createToken({ userId: user._id, email: user.email }, user.roles);
107
+
108
+ console.log(`JWT token generated for API client`);
109
+ console.log(`Token will be returned as JSON (not redirect)`);
110
+
111
+ return {
112
+ user: {
113
+ _id: user._id,
114
+ email: user.email,
115
+ name: user.name,
116
+ avatarUrl: user.avatarUrl,
117
+ roles: user.roles,
118
+ },
119
+ token,
120
+ // redirectUrl is ignored when response_type=json
121
+ redirectUrl: "/dashboard",
122
+ };
123
+ },
124
+
125
+ onAuthError: async ({ error, provider }) => {
126
+ console.error(`OAuth error for ${provider}:`, error);
127
+ return {
128
+ redirectUrl: `/login?error=${error.code}`,
129
+ };
130
+ },
131
+ }),
132
+ ],
133
+
134
+ // IMPORTANT: Configure CORS for API clients
135
+ cors: {
136
+ origin: [
137
+ "http://localhost:3000", // React/Vue dev server
138
+ "http://localhost:19006", // Expo dev server
139
+ "capacitor://localhost", // Capacitor iOS/Android
140
+ "ionic://localhost", // Ionic
141
+ /\.myapp\.com$/, // Production domains
142
+ ],
143
+ credentials: true,
144
+ allowedHeaders: ["Authorization", "Content-Type"],
145
+ },
146
+ });
147
+
148
+ await app.start();
149
+
150
+ console.log("\n===========================================");
151
+ console.log("OAuth API Client Integration Example");
152
+ console.log("===========================================");
153
+ console.log(`Server: http://localhost:3336`);
154
+ console.log(`\nKey Feature: response_type=json`);
155
+ console.log(` Add ?response_type=json to callback URL for JSON response`);
156
+ console.log(` Perfect for mobile apps, SPAs, and API clients`);
157
+ console.log(`\nExample URLs:`);
158
+ console.log(` Initiate: http://localhost:3336/oauth/github/initiate`);
159
+ console.log(` Callback: http://localhost:3336/oauth/github/callback?code=xxx&state=yyy&response_type=json`);
160
+ console.log("===========================================\n");
161
+ }
162
+
163
+ start().catch((error) => {
164
+ console.error("Failed to start application:", error);
165
+ process.exit(1);
166
+ });
167
+
168
+ /**
169
+ * CLIENT INTEGRATION EXAMPLES
170
+ */
171
+
172
+ /**
173
+ * Example 1: React SPA with Popup Window
174
+ */
175
+
176
+ const ReactSPAExample = `
177
+ import React, { useState } from 'react';
178
+
179
+ function OAuthLogin() {
180
+ const [loading, setLoading] = useState(false);
181
+
182
+ const handleGitHubLogin = async () => {
183
+ setLoading(true);
184
+
185
+ // Open OAuth flow in popup window
186
+ const width = 600;
187
+ const height = 700;
188
+ const left = window.screen.width / 2 - width / 2;
189
+ const top = window.screen.height / 2 - height / 2;
190
+
191
+ const popup = window.open(
192
+ 'http://localhost:3336/oauth/github/initiate',
193
+ 'oauth_popup',
194
+ \`width=\${width},height=\${height},left=\${left},top=\${top}\`
195
+ );
196
+
197
+ // Listen for message from popup
198
+ window.addEventListener('message', async (event) => {
199
+ if (event.origin !== 'http://localhost:3336') return;
200
+
201
+ const { code, state } = event.data;
202
+
203
+ // Exchange code for token using response_type=json
204
+ const response = await fetch(
205
+ \`http://localhost:3336/oauth/github/callback?code=\${code}&state=\${state}&response_type=json\`,
206
+ {
207
+ credentials: 'include'
208
+ }
209
+ );
210
+
211
+ const data = await response.json();
212
+
213
+ if (data.token) {
214
+ // Store JWT token
215
+ localStorage.setItem('jwt_token', data.token);
216
+
217
+ // Store user data
218
+ localStorage.setItem('user', JSON.stringify(data.user));
219
+
220
+ // Close popup
221
+ popup?.close();
222
+
223
+ // Redirect to dashboard
224
+ window.location.href = '/dashboard';
225
+ }
226
+
227
+ setLoading(false);
228
+ });
229
+ };
230
+
231
+ return (
232
+ <div>
233
+ <button onClick={handleGitHubLogin} disabled={loading}>
234
+ {loading ? 'Logging in...' : 'Login with GitHub'}
235
+ </button>
236
+ </div>
237
+ );
238
+ }
239
+ `;
240
+
241
+ /**
242
+ * Example 2: React Native with expo-auth-session
243
+ */
244
+
245
+ const ReactNativeExample = `
246
+ import React from 'react';
247
+ import { Button, View, Text } from 'react-native';
248
+ import * as WebBrowser from 'expo-web-browser';
249
+ import * as AuthSession from 'expo-auth-session';
250
+ import AsyncStorage from '@react-native-async-storage/async-storage';
251
+
252
+ // Tell expo-auth-session to finish the session
253
+ WebBrowser.maybeCompleteAuthSession();
254
+
255
+ function OAuthLogin() {
256
+ const [user, setUser] = React.useState(null);
257
+
258
+ // Configure OAuth discovery
259
+ const discovery = {
260
+ authorizationEndpoint: 'http://localhost:3336/oauth/github/initiate',
261
+ };
262
+
263
+ // Create auth request
264
+ const [request, response, promptAsync] = AuthSession.useAuthRequest(
265
+ {
266
+ clientId: 'not-used-for-our-flow',
267
+ redirectUri: AuthSession.makeRedirectUri({
268
+ native: 'myapp://oauth/callback',
269
+ }),
270
+ },
271
+ discovery
272
+ );
273
+
274
+ React.useEffect(() => {
275
+ if (response?.type === 'success') {
276
+ const { code, state } = response.params;
277
+
278
+ // Exchange code for token with response_type=json
279
+ fetch(
280
+ \`http://localhost:3336/oauth/github/callback?code=\${code}&state=\${state}&response_type=json\`
281
+ )
282
+ .then(res => res.json())
283
+ .then(async (data) => {
284
+ // Store JWT token securely
285
+ await AsyncStorage.setItem('jwt_token', data.token);
286
+
287
+ // Store user data
288
+ await AsyncStorage.setItem('user', JSON.stringify(data.user));
289
+
290
+ setUser(data.user);
291
+ })
292
+ .catch(console.error);
293
+ }
294
+ }, [response]);
295
+
296
+ const handleLogin = () => {
297
+ promptAsync();
298
+ };
299
+
300
+ if (user) {
301
+ return (
302
+ <View>
303
+ <Text>Welcome, {user.name}!</Text>
304
+ <Text>Email: {user.email}</Text>
305
+ </View>
306
+ );
307
+ }
308
+
309
+ return (
310
+ <View>
311
+ <Button
312
+ title="Login with GitHub"
313
+ onPress={handleLogin}
314
+ disabled={!request}
315
+ />
316
+ </View>
317
+ );
318
+ }
319
+ `;
320
+
321
+ /**
322
+ * Example 3: Mobile App with In-App Browser
323
+ */
324
+
325
+ const MobileInAppBrowserExample = `
326
+ import { InAppBrowser } from 'react-native-inappbrowser-reborn';
327
+ import AsyncStorage from '@react-native-async-storage/async-storage';
328
+
329
+ async function loginWithGitHub() {
330
+ try {
331
+ // Check if InAppBrowser is available
332
+ if (await InAppBrowser.isAvailable()) {
333
+ // Open OAuth URL in in-app browser
334
+ const result = await InAppBrowser.openAuth(
335
+ 'http://localhost:3336/oauth/github/initiate',
336
+ 'myapp://oauth/callback',
337
+ {
338
+ // iOS options
339
+ ephemeralWebSession: false,
340
+ // Android options
341
+ showTitle: false,
342
+ enableUrlBarHiding: true,
343
+ enableDefaultShare: false,
344
+ }
345
+ );
346
+
347
+ if (result.type === 'success') {
348
+ const url = new URL(result.url);
349
+ const code = url.searchParams.get('code');
350
+ const state = url.searchParams.get('state');
351
+
352
+ // Exchange code for token with response_type=json
353
+ const response = await fetch(
354
+ \`http://localhost:3336/oauth/github/callback?code=\${code}&state=\${state}&response_type=json\`
355
+ );
356
+
357
+ const data = await response.json();
358
+
359
+ // Store JWT token securely
360
+ await AsyncStorage.setItem('jwt_token', data.token);
361
+ await AsyncStorage.setItem('user', JSON.stringify(data.user));
362
+
363
+ return data;
364
+ }
365
+ }
366
+ } catch (error) {
367
+ console.error('OAuth error:', error);
368
+ }
369
+ }
370
+
371
+ // Usage in component
372
+ const LoginScreen = () => {
373
+ const handleLogin = async () => {
374
+ const result = await loginWithGitHub();
375
+ if (result) {
376
+ // Navigate to home screen
377
+ navigation.navigate('Home');
378
+ }
379
+ };
380
+
381
+ return <Button title="Login with GitHub" onPress={handleLogin} />;
382
+ };
383
+ `;
384
+
385
+ /**
386
+ * Example 4: Vue.js SPA
387
+ */
388
+
389
+ const VueJSExample = `
390
+ <template>
391
+ <div>
392
+ <button @click="loginWithGitHub" :disabled="loading">
393
+ {{ loading ? 'Logging in...' : 'Login with GitHub' }}
394
+ </button>
395
+ </div>
396
+ </template>
397
+
398
+ <script>
399
+ export default {
400
+ data() {
401
+ return {
402
+ loading: false,
403
+ };
404
+ },
405
+ methods: {
406
+ async loginWithGitHub() {
407
+ this.loading = true;
408
+
409
+ // Open OAuth in popup
410
+ const popup = window.open(
411
+ 'http://localhost:3336/oauth/github/initiate',
412
+ 'oauth_popup',
413
+ 'width=600,height=700'
414
+ );
415
+
416
+ // Poll for popup to close or message
417
+ const checkPopup = setInterval(async () => {
418
+ try {
419
+ if (popup.closed) {
420
+ clearInterval(checkPopup);
421
+ this.loading = false;
422
+ return;
423
+ }
424
+
425
+ // Check if popup has navigated to callback URL
426
+ const popupUrl = popup.location.href;
427
+ if (popupUrl.includes('/oauth/github/callback')) {
428
+ const url = new URL(popupUrl);
429
+ const code = url.searchParams.get('code');
430
+ const state = url.searchParams.get('state');
431
+
432
+ if (code && state) {
433
+ // Exchange for token with response_type=json
434
+ const response = await fetch(
435
+ \`http://localhost:3336/oauth/github/callback?code=\${code}&state=\${state}&response_type=json\`,
436
+ { credentials: 'include' }
437
+ );
438
+
439
+ const data = await response.json();
440
+
441
+ // Store JWT token
442
+ localStorage.setItem('jwt_token', data.token);
443
+ localStorage.setItem('user', JSON.stringify(data.user));
444
+
445
+ // Close popup
446
+ popup.close();
447
+ clearInterval(checkPopup);
448
+
449
+ // Redirect
450
+ this.$router.push('/dashboard');
451
+ }
452
+ }
453
+ } catch (e) {
454
+ // Cross-origin error - popup hasn't navigated yet
455
+ }
456
+ }, 500);
457
+ },
458
+ },
459
+ };
460
+ </script>
461
+ `;
462
+
463
+ /**
464
+ * Example 5: Using JWT Token for API Requests
465
+ */
466
+
467
+ const UsingJWTTokenExample = `
468
+ // After OAuth login, use JWT token for authenticated requests
469
+
470
+ // Get stored token
471
+ const token = localStorage.getItem('jwt_token');
472
+ // or for React Native
473
+ const token = await AsyncStorage.getItem('jwt_token');
474
+
475
+ // Make authenticated request
476
+ const response = await fetch('http://localhost:3336/api/profile', {
477
+ headers: {
478
+ 'Authorization': \`Bearer \${token}\`,
479
+ 'Content-Type': 'application/json'
480
+ }
481
+ });
482
+
483
+ const userData = await response.json();
484
+
485
+ // Token refresh logic (if needed)
486
+ if (response.status === 401) {
487
+ // Token expired - redirect to login
488
+ localStorage.removeItem('jwt_token');
489
+ window.location.href = '/login';
490
+ }
491
+ `;
492
+
493
+ /**
494
+ * IMPORTANT NOTES
495
+ *
496
+ * Response Type Formats:
497
+ *
498
+ * 1. response_type=json (for API clients):
499
+ * GET /oauth/github/callback?code=xxx&state=yyy&response_type=json
500
+ *
501
+ * Response:
502
+ * {
503
+ * "user": { "_id": "...", "email": "...", ... },
504
+ * "token": "eyJhbGc..."
505
+ * }
506
+ *
507
+ * 2. Default (redirect with token in query):
508
+ * GET /oauth/github/callback?code=xxx&state=yyy
509
+ *
510
+ * Redirects to:
511
+ * /dashboard?token=eyJhbGc...
512
+ *
513
+ * 3. URL fragment (if configured):
514
+ * Redirects to:
515
+ * /dashboard#token=eyJhbGc...
516
+ *
517
+ * Best Practices for API Clients:
518
+ *
519
+ * 1. Always use response_type=json for mobile apps and SPAs
520
+ * 2. Store JWT tokens in secure storage:
521
+ * - Mobile: AsyncStorage, Keychain, SecureStore
522
+ * - Web: localStorage (or sessionStorage for extra security)
523
+ * 3. Use HTTPS in production
524
+ * 4. Implement token refresh mechanism
525
+ * 5. Clear tokens on logout
526
+ * 6. Validate tokens on app startup
527
+ * 7. Handle token expiration gracefully
528
+ *
529
+ * Security Considerations:
530
+ *
531
+ * 1. NEVER store tokens in URL for long-term
532
+ * 2. Use HTTPS for all OAuth callbacks in production
533
+ * 3. Implement CORS properly for API clients
534
+ * 4. Validate redirect URIs on server
535
+ * 5. Use state parameter for CSRF protection
536
+ * 6. Implement rate limiting on OAuth endpoints
537
+ * 7. Log all OAuth attempts for security monitoring
538
+ */
539
+
540
+ console.log("\n=== Client Integration Code Examples ===\n");
541
+ console.log("1. React SPA with Popup:");
542
+ console.log(ReactSPAExample);
543
+ console.log("\n2. React Native with expo-auth-session:");
544
+ console.log(ReactNativeExample);
545
+ console.log("\n3. Mobile In-App Browser:");
546
+ console.log(MobileInAppBrowserExample);
547
+ console.log("\n4. Vue.js SPA:");
548
+ console.log(VueJSExample);
549
+ console.log("\n5. Using JWT Token:");
550
+ console.log(UsingJWTTokenExample);