@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
package/README.md ADDED
@@ -0,0 +1,783 @@
1
+ # OAuth Plugin
2
+
3
+ A flexible OAuth 2.0 authentication plugin for Flink that supports multiple providers (GitHub, Google) with MongoDB session storage, JWT token generation, and configurable token handling.
4
+
5
+ ## Features
6
+
7
+ - OAuth 2.0 Authorization Code flow for GitHub and Google
8
+ - Automatic JWT token generation via JWT Auth Plugin integration
9
+ - MongoDB session storage with automatic TTL cleanup
10
+ - Support for linking multiple OAuth providers to a single user account
11
+ - Flexible token storage (store OAuth tokens for API access or auth-only mode)
12
+ - CSRF protection with cryptographically secure state parameters
13
+ - Encrypted OAuth token storage (AES-256-GCM)
14
+ - Built-in HTTP endpoints for OAuth flow
15
+ - TypeScript support with full type safety
16
+ - Configurable response formats (JSON, URL fragment, query parameter)
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install @flink-app/oauth-plugin @flink-app/jwt-auth-plugin
22
+ ```
23
+
24
+ ## Prerequisites
25
+
26
+ ### 1. JWT Auth Plugin Dependency
27
+
28
+ This plugin requires `@flink-app/jwt-auth-plugin` to be installed and configured. The OAuth plugin uses the JWT Auth Plugin to generate authentication tokens after successful OAuth authentication.
29
+
30
+ ### 2. OAuth Provider Credentials
31
+
32
+ You need OAuth application credentials from your desired providers:
33
+
34
+ #### GitHub OAuth App
35
+
36
+ 1. Go to [GitHub Developer Settings](https://github.com/settings/developers)
37
+ 2. Create a new OAuth App
38
+ 3. Set Authorization callback URL to `https://yourdomain.com/oauth/github/callback`
39
+ 4. Save Client ID and Client Secret
40
+
41
+ #### Google OAuth App
42
+
43
+ 1. Go to [Google Cloud Console](https://console.cloud.google.com/)
44
+ 2. Create a new project or select existing
45
+ 3. Enable Google+ API
46
+ 4. Go to Credentials > Create Credentials > OAuth 2.0 Client ID
47
+ 5. Set Authorized redirect URI to `https://yourdomain.com/oauth/google/callback`
48
+ 6. Save Client ID and Client Secret
49
+
50
+ ### 3. MongoDB Connection
51
+
52
+ The plugin requires MongoDB to store OAuth sessions.
53
+
54
+ ## Quick Start
55
+
56
+ ```typescript
57
+ import { FlinkApp } from "@flink-app/flink";
58
+ import { jwtAuthPlugin } from "@flink-app/jwt-auth-plugin";
59
+ import { oauthPlugin } from "@flink-app/oauth-plugin";
60
+ import { Context } from "./Context";
61
+
62
+ const app = new FlinkApp<Context>({
63
+ name: "My App",
64
+
65
+ // JWT Auth Plugin MUST be configured first
66
+ auth: jwtAuthPlugin({
67
+ secret: process.env.JWT_SECRET!,
68
+ getUser: async (tokenData) => {
69
+ return await app.ctx.repos.userRepo.getById(tokenData.userId);
70
+ },
71
+ rolePermissions: {
72
+ user: ["read:own", "write:own"],
73
+ admin: ["read:all", "write:all"],
74
+ },
75
+ }),
76
+
77
+ db: {
78
+ uri: process.env.MONGODB_URI!,
79
+ },
80
+
81
+ plugins: [
82
+ oauthPlugin({
83
+ providers: {
84
+ github: {
85
+ clientId: process.env.GITHUB_CLIENT_ID!,
86
+ clientSecret: process.env.GITHUB_CLIENT_SECRET!,
87
+ callbackUrl: "https://myapp.com/oauth/github/callback",
88
+ },
89
+ google: {
90
+ clientId: process.env.GOOGLE_CLIENT_ID!,
91
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
92
+ callbackUrl: "https://myapp.com/oauth/google/callback",
93
+ },
94
+ },
95
+
96
+ // Callback after successful OAuth authentication
97
+ onAuthSuccess: async ({ profile, provider }, ctx) => {
98
+ // Find or create user
99
+ let user = await ctx.repos.userRepo.getOne({ email: profile.email });
100
+
101
+ if (!user) {
102
+ user = await ctx.repos.userRepo.create({
103
+ email: profile.email,
104
+ name: profile.name,
105
+ avatarUrl: profile.avatarUrl,
106
+ oauthProviders: [{ provider, providerId: profile.id }],
107
+ });
108
+ } else {
109
+ // Link provider to existing user
110
+ await ctx.repos.userRepo.updateOne(user._id, {
111
+ oauthProviders: [...user.oauthProviders, { provider, providerId: profile.id }],
112
+ });
113
+ }
114
+
115
+ // Generate JWT token using JWT Auth Plugin
116
+ const token = await ctx.plugins.jwtAuth.createToken({ userId: user._id, email: user.email }, ["user"]);
117
+
118
+ return {
119
+ user,
120
+ token,
121
+ redirectUrl: "https://myapp.com/dashboard",
122
+ };
123
+ },
124
+
125
+ // Optional: Handle OAuth errors
126
+ onAuthError: async ({ error, provider }) => {
127
+ console.error(`OAuth error for ${provider}:`, error);
128
+ return {
129
+ redirectUrl: `https://myapp.com/login?error=${error.code}`,
130
+ };
131
+ },
132
+ }),
133
+ ],
134
+ });
135
+
136
+ await app.start();
137
+ ```
138
+
139
+ ## Configuration
140
+
141
+ ### OAuthPluginOptions
142
+
143
+ | Option | Type | Required | Default | Description |
144
+ | --------------------------- | ---------- | -------- | --------------------- | ---------------------------------------------- |
145
+ | `providers` | `object` | Yes | - | OAuth provider configurations (GitHub, Google) |
146
+ | `storeTokens` | `boolean` | No | `false` | Store OAuth tokens for future API access |
147
+ | `onAuthSuccess` | `Function` | Yes | - | Callback after successful authentication |
148
+ | `onAuthError` | `Function` | No | - | Callback on OAuth errors |
149
+ | `sessionTTL` | `number` | No | `600` | Session TTL in seconds (default: 10 minutes) |
150
+ | `sessionsCollectionName` | `string` | No | `"oauth_sessions"` | MongoDB collection for sessions |
151
+ | `connectionsCollectionName` | `string` | No | `"oauth_connections"` | MongoDB collection for connections |
152
+
153
+ ### Provider Configuration
154
+
155
+ #### GitHub Provider
156
+
157
+ ```typescript
158
+ {
159
+ github: {
160
+ clientId: string;
161
+ clientSecret: string;
162
+ callbackUrl: string;
163
+ scope?: string[]; // Default: ["user:email"]
164
+ }
165
+ }
166
+ ```
167
+
168
+ #### Google Provider
169
+
170
+ ```typescript
171
+ {
172
+ google: {
173
+ clientId: string;
174
+ clientSecret: string;
175
+ callbackUrl: string;
176
+ scope?: string[]; // Default: ["openid", "email", "profile"]
177
+ }
178
+ }
179
+ ```
180
+
181
+ ### Callback Functions
182
+
183
+ #### onAuthSuccess
184
+
185
+ Called when OAuth authentication succeeds. Must generate and return a JWT token.
186
+
187
+ ```typescript
188
+ onAuthSuccess: async (
189
+ params: {
190
+ profile: OAuthProfile;
191
+ provider: "github" | "google";
192
+ tokens?: OAuthTokens; // Only if storeTokens: true
193
+ },
194
+ ctx: Context
195
+ ) =>
196
+ Promise<{
197
+ user: any;
198
+ token: string; // JWT token from ctx.plugins.jwtAuth.createToken()
199
+ redirectUrl?: string;
200
+ }>;
201
+ ```
202
+
203
+ **OAuth Profile Structure:**
204
+
205
+ ```typescript
206
+ interface OAuthProfile {
207
+ id: string; // Provider user ID
208
+ email: string; // User email
209
+ name?: string; // Full name
210
+ avatarUrl?: string; // Profile picture URL
211
+ raw: any; // Raw provider response
212
+ }
213
+ ```
214
+
215
+ **OAuth Tokens Structure (if storeTokens: true):**
216
+
217
+ ```typescript
218
+ interface OAuthTokens {
219
+ accessToken: string;
220
+ refreshToken?: string;
221
+ expiresIn?: number;
222
+ scope?: string;
223
+ }
224
+ ```
225
+
226
+ #### onAuthError
227
+
228
+ Called when OAuth authentication fails.
229
+
230
+ ```typescript
231
+ onAuthError: async (params: { error: OAuthError; provider: "github" | "google" }) =>
232
+ Promise<{
233
+ redirectUrl?: string;
234
+ }>;
235
+
236
+ interface OAuthError {
237
+ code: string; // Error code (e.g., "access_denied")
238
+ message: string; // User-friendly error message
239
+ details?: any; // Additional error details
240
+ }
241
+ ```
242
+
243
+ **Common Error Codes:**
244
+
245
+ - `invalid_state` - State parameter mismatch or expired
246
+ - `access_denied` - User denied OAuth authorization
247
+ - `invalid_grant` - Authorization code expired or invalid
248
+ - `network_error` - Provider API unreachable
249
+ - `jwt_generation_failed` - Failed to generate JWT token
250
+
251
+ ## OAuth Flow
252
+
253
+ ### Complete Authentication Flow
254
+
255
+ 1. User clicks "Login with GitHub" or "Login with Google"
256
+ 2. Client redirects to `/oauth/:provider/initiate`
257
+ 3. Plugin generates secure state parameter and stores session
258
+ 4. Plugin redirects user to OAuth provider (GitHub/Google)
259
+ 5. User authorizes app on OAuth provider
260
+ 6. OAuth provider redirects to `/oauth/:provider/callback` with authorization code
261
+ 7. Plugin validates state parameter (CSRF protection)
262
+ 8. Plugin exchanges authorization code for OAuth access token
263
+ 9. Plugin fetches user profile from provider
264
+ 10. Plugin calls `onAuthSuccess` callback with profile and context
265
+ 11. App creates/links user account
266
+ 12. **App generates JWT token via `ctx.plugins.jwtAuth.createToken()`**
267
+ 13. **Plugin returns JWT token to client**
268
+ 14. Client stores JWT token and uses it for authenticated requests
269
+
270
+ ### Initiate OAuth Flow
271
+
272
+ ```
273
+ GET /oauth/:provider/initiate?redirect_uri={optional_redirect}
274
+ ```
275
+
276
+ **Example:**
277
+
278
+ ```
279
+ GET /oauth/github/initiate
280
+ GET /oauth/google/initiate?redirect_uri=https://myapp.com/welcome
281
+ ```
282
+
283
+ **Response:**
284
+
285
+ - 302 redirect to OAuth provider authorization URL
286
+
287
+ ### OAuth Callback
288
+
289
+ ```
290
+ GET /oauth/:provider/callback?code={auth_code}&state={state}&response_type={json|fragment|query}
291
+ ```
292
+
293
+ **Query Parameters:**
294
+
295
+ - `code` - Authorization code from provider
296
+ - `state` - CSRF protection token
297
+ - `response_type` - Optional response format (default: redirect with token in query)
298
+
299
+ **Response Formats:**
300
+
301
+ 1. **JSON Response** (when `response_type=json`):
302
+
303
+ ```json
304
+ {
305
+ "user": {
306
+ "_id": "...",
307
+ "email": "user@example.com",
308
+ "name": "John Doe"
309
+ },
310
+ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
311
+ }
312
+ ```
313
+
314
+ 2. **URL Fragment** (when redirect URL supports fragments):
315
+
316
+ ```
317
+ https://myapp.com/dashboard#token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
318
+ ```
319
+
320
+ 3. **Query Parameter** (default):
321
+
322
+ ```
323
+ https://myapp.com/dashboard?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
324
+ ```
325
+
326
+ ## Context API
327
+
328
+ The plugin exposes methods via `ctx.plugins.oauth`:
329
+
330
+ ### getConnection
331
+
332
+ Get stored OAuth connection for a user and provider.
333
+
334
+ ```typescript
335
+ const connection = await ctx.plugins.oauth.getConnection(userId, "github");
336
+
337
+ // Returns OAuthConnection or null
338
+ interface OAuthConnection {
339
+ _id: string;
340
+ userId: string;
341
+ provider: "github" | "google";
342
+ providerId: string;
343
+ accessToken: string; // Encrypted
344
+ refreshToken?: string; // Encrypted
345
+ scope: string;
346
+ expiresAt?: Date;
347
+ createdAt: Date;
348
+ updatedAt: Date;
349
+ }
350
+ ```
351
+
352
+ ### getConnections
353
+
354
+ Get all OAuth connections for a user.
355
+
356
+ ```typescript
357
+ const connections = await ctx.plugins.oauth.getConnections(userId);
358
+ // Returns OAuthConnection[]
359
+ ```
360
+
361
+ ### deleteConnection
362
+
363
+ Delete/unlink an OAuth connection.
364
+
365
+ ```typescript
366
+ await ctx.plugins.oauth.deleteConnection(userId, "github");
367
+ ```
368
+
369
+ ## Token Storage
370
+
371
+ ### Auth-Only Mode (Default)
372
+
373
+ By default, `storeTokens: false`, meaning OAuth tokens are NOT stored. OAuth is used only for authentication.
374
+
375
+ ```typescript
376
+ oauthPlugin({
377
+ providers: { github: {...}, google: {...} },
378
+ storeTokens: false, // OAuth tokens discarded after auth
379
+ onAuthSuccess: async ({ profile }, ctx) => {
380
+ // Create user and generate JWT token
381
+ // OAuth tokens are NOT available here
382
+ }
383
+ })
384
+ ```
385
+
386
+ **Use when:**
387
+
388
+ - You only need OAuth for user authentication
389
+ - You don't need to call provider APIs on behalf of users
390
+ - You want to minimize stored credentials
391
+
392
+ ### Token Storage Mode
393
+
394
+ Set `storeTokens: true` to store encrypted OAuth tokens for future API access.
395
+
396
+ ```typescript
397
+ oauthPlugin({
398
+ providers: { github: {...}, google: {...} },
399
+ storeTokens: true, // Store encrypted OAuth tokens
400
+ onAuthSuccess: async ({ profile, tokens }, ctx) => {
401
+ // tokens.accessToken and tokens.refreshToken are available
402
+ // Tokens are automatically encrypted and stored
403
+ }
404
+ })
405
+ ```
406
+
407
+ **Use when:**
408
+
409
+ - You need to call GitHub/Google APIs on behalf of users
410
+ - You want to access user's GitHub repos or Google Drive
411
+ - You need long-term API access
412
+
413
+ **Note:** OAuth tokens are encrypted using AES-256-GCM before storage.
414
+
415
+ ## JWT vs OAuth Tokens
416
+
417
+ It's important to understand the difference between OAuth tokens and JWT tokens:
418
+
419
+ ### OAuth Tokens
420
+
421
+ - **Purpose:** Access provider APIs (GitHub, Google) on behalf of the user
422
+ - **Issued by:** OAuth provider (GitHub, Google)
423
+ - **Used for:** Calling GitHub API, Google API, etc.
424
+ - **Storage:** Optional (only if `storeTokens: true`)
425
+ - **Lifetime:** Varies by provider (hours to months)
426
+
427
+ ### JWT Tokens
428
+
429
+ - **Purpose:** Authenticate requests to YOUR app
430
+ - **Issued by:** Your app (via JWT Auth Plugin)
431
+ - **Used for:** Accessing protected endpoints in your app
432
+ - **Storage:** Client-side (localStorage, sessionStorage)
433
+ - **Lifetime:** Configured in JWT Auth Plugin
434
+
435
+ **Example Flow:**
436
+
437
+ ```
438
+ User OAuth Login (GitHub)
439
+ -> OAuth access token (to call GitHub API)
440
+ -> Your app generates JWT token
441
+ -> Client uses JWT token for app authentication
442
+ ```
443
+
444
+ ## Security
445
+
446
+ ### CSRF Protection
447
+
448
+ The plugin uses cryptographically secure state parameters to prevent CSRF attacks:
449
+
450
+ 1. Generate 32-byte random state using `crypto.randomBytes()`
451
+ 2. Store state in MongoDB session with 10-minute expiration
452
+ 3. Validate state on callback using constant-time comparison
453
+ 4. Clear session after successful validation
454
+
455
+ ### Token Encryption
456
+
457
+ When `storeTokens: true`, OAuth tokens are encrypted before storage:
458
+
459
+ - **Algorithm:** AES-256-GCM
460
+ - **Encryption key:** Derived from client secret
461
+ - **Storage:** Encrypted tokens in MongoDB
462
+ - **Decryption:** Automatic when retrieved via context methods
463
+
464
+ ### HTTPS Requirement
465
+
466
+ **IMPORTANT:** OAuth callback URLs MUST use HTTPS in production. OAuth providers reject HTTP callback URLs for security reasons.
467
+
468
+ ### Secrets Management
469
+
470
+ Never commit secrets to version control:
471
+
472
+ ```bash
473
+ # .env
474
+ GITHUB_CLIENT_ID=your_github_client_id
475
+ GITHUB_CLIENT_SECRET=your_github_client_secret
476
+ GOOGLE_CLIENT_ID=your_google_client_id
477
+ GOOGLE_CLIENT_SECRET=your_google_client_secret
478
+ JWT_SECRET=your_jwt_secret
479
+ ```
480
+
481
+ ### JWT Token Security
482
+
483
+ - Store JWT tokens in secure storage (httpOnly cookies or secure localStorage)
484
+ - Never expose JWT tokens in URLs for long-term storage
485
+ - Use short token expiration times
486
+ - Implement token refresh mechanism
487
+ - Validate tokens on every request
488
+
489
+ ## API Client Integration
490
+
491
+ For API clients (mobile apps, SPAs), use `response_type=json`:
492
+
493
+ ```typescript
494
+ // Mobile app OAuth flow
495
+ const initiateUrl = "https://api.myapp.com/oauth/github/initiate";
496
+
497
+ // Open browser for OAuth
498
+ openBrowser(initiateUrl);
499
+
500
+ // After OAuth, catch callback
501
+ const callbackUrl = "https://api.myapp.com/oauth/github/callback?code=xxx&state=yyy&response_type=json";
502
+
503
+ // Fetch JSON response
504
+ const response = await fetch(callbackUrl);
505
+ const { user, token } = await response.json();
506
+
507
+ // Store JWT token in secure storage
508
+ await secureStorage.setItem("jwt_token", token);
509
+
510
+ // Use JWT token for authenticated requests
511
+ const headers = {
512
+ Authorization: `Bearer ${token}`,
513
+ };
514
+ ```
515
+
516
+ ## Client Integration Examples
517
+
518
+ ### React Web App
519
+
520
+ ```typescript
521
+ import React from "react";
522
+
523
+ function LoginPage() {
524
+ const handleGitHubLogin = () => {
525
+ // Redirect to OAuth initiation
526
+ window.location.href = "/oauth/github/initiate?redirect_uri=https://myapp.com/dashboard";
527
+ };
528
+
529
+ const handleGoogleLogin = () => {
530
+ window.location.href = "/oauth/google/initiate?redirect_uri=https://myapp.com/dashboard";
531
+ };
532
+
533
+ React.useEffect(() => {
534
+ // Check for token in URL after OAuth redirect
535
+ const urlParams = new URLSearchParams(window.location.search);
536
+ const token = urlParams.get("token");
537
+
538
+ if (token) {
539
+ // Store JWT token
540
+ localStorage.setItem("jwt_token", token);
541
+
542
+ // Clean URL
543
+ window.history.replaceState({}, document.title, "/dashboard");
544
+
545
+ // Redirect to dashboard
546
+ window.location.href = "/dashboard";
547
+ }
548
+ }, []);
549
+
550
+ return (
551
+ <div>
552
+ <h1>Login</h1>
553
+ <button onClick={handleGitHubLogin}>Login with GitHub</button>
554
+ <button onClick={handleGoogleLogin}>Login with Google</button>
555
+ </div>
556
+ );
557
+ }
558
+ ```
559
+
560
+ ### React Native App
561
+
562
+ ```typescript
563
+ import { openAuthSessionAsync } from "expo-auth-session";
564
+
565
+ async function loginWithGitHub() {
566
+ const result = await openAuthSessionAsync("https://api.myapp.com/oauth/github/initiate", "myapp://oauth/callback");
567
+
568
+ if (result.type === "success") {
569
+ const url = result.url;
570
+ const token = new URL(url).searchParams.get("token");
571
+
572
+ if (token) {
573
+ await AsyncStorage.setItem("jwt_token", token);
574
+ // Navigate to home screen
575
+ }
576
+ }
577
+ }
578
+ ```
579
+
580
+ ## Multiple Provider Linking
581
+
582
+ Allow users to link multiple OAuth providers to their account:
583
+
584
+ ```typescript
585
+ onAuthSuccess: async ({ profile, provider }, ctx) => {
586
+ // Find user by email (regardless of provider)
587
+ let user = await ctx.repos.userRepo.getOne({ email: profile.email });
588
+
589
+ if (user) {
590
+ // User exists - link new provider
591
+ const existingProviders = user.oauthProviders || [];
592
+ const isAlreadyLinked = existingProviders.some((p) => p.provider === provider && p.providerId === profile.id);
593
+
594
+ if (!isAlreadyLinked) {
595
+ await ctx.repos.userRepo.updateOne(user._id, {
596
+ oauthProviders: [...existingProviders, { provider, providerId: profile.id }],
597
+ });
598
+ }
599
+ } else {
600
+ // New user - create account
601
+ user = await ctx.repos.userRepo.create({
602
+ email: profile.email,
603
+ name: profile.name,
604
+ avatarUrl: profile.avatarUrl,
605
+ oauthProviders: [{ provider, providerId: profile.id }],
606
+ });
607
+ }
608
+
609
+ // Generate JWT token
610
+ const token = await ctx.plugins.jwtAuth.createToken({ userId: user._id }, ["user"]);
611
+
612
+ return { user, token, redirectUrl: "/dashboard" };
613
+ };
614
+ ```
615
+
616
+ ## Migration from BankID Plugin
617
+
618
+ If you're migrating from the BankID plugin, the OAuth plugin follows similar patterns:
619
+
620
+ ### Similarities
621
+
622
+ - Callback-based architecture with `onAuthSuccess`
623
+ - JWT token generation via JWT Auth Plugin
624
+ - MongoDB session storage with TTL
625
+ - Context-based dependency injection
626
+
627
+ ### Improvements
628
+
629
+ 1. **Provider abstraction** - Easy to add new OAuth providers
630
+ 2. **Flexible token storage** - Choose whether to store OAuth tokens
631
+ 3. **Better error handling** - Dedicated `onAuthError` callback
632
+ 4. **Multiple provider support** - Built-in linking of GitHub + Google
633
+ 5. **Cleaner separation** - Plugin handles OAuth, app handles user logic
634
+
635
+ ### Migration Example
636
+
637
+ **BankID Plugin:**
638
+
639
+ ```typescript
640
+ bankIdPlugin({
641
+ onAuthSuccess: async (userData, ip, payload) => {
642
+ const user = await findOrCreateUser(userData);
643
+ const token = await ctx.auth.createToken({ userId: user._id }, ["user"]);
644
+ return { user, token };
645
+ },
646
+ });
647
+ ```
648
+
649
+ **OAuth Plugin:**
650
+
651
+ ```typescript
652
+ oauthPlugin({
653
+ onAuthSuccess: async ({ profile, provider }, ctx) => {
654
+ const user = await findOrCreateUser(profile, provider);
655
+ const token = await ctx.plugins.jwtAuth.createToken({ userId: user._id }, ["user"]);
656
+ return { user, token, redirectUrl: "/dashboard" };
657
+ },
658
+ });
659
+ ```
660
+
661
+ ## Troubleshooting
662
+
663
+ ### OAuth Error: Invalid Redirect URI
664
+
665
+ **Issue:** `redirect_uri_mismatch` error from OAuth provider
666
+
667
+ **Solution:**
668
+
669
+ - Verify callback URL in provider settings matches exactly
670
+ - Ensure callback URL uses HTTPS in production
671
+ - Check for trailing slashes (they matter!)
672
+
673
+ ### State Parameter Mismatch
674
+
675
+ **Issue:** `invalid_state` error
676
+
677
+ **Solution:**
678
+
679
+ - Ensure cookies are enabled (sessions use MongoDB, but state validation may use cookies)
680
+ - Check session TTL hasn't expired (default: 10 minutes)
681
+ - Verify clock synchronization between servers
682
+
683
+ ### JWT Token Not Generated
684
+
685
+ **Issue:** `jwt_generation_failed` error
686
+
687
+ **Solution:**
688
+
689
+ - Ensure JWT Auth Plugin is configured
690
+ - Verify `ctx.plugins.jwtAuth` is available in `onAuthSuccess`
691
+ - Check JWT secret is set in environment variables
692
+
693
+ ### User Denied Access
694
+
695
+ **Issue:** User cancels OAuth authorization
696
+
697
+ **Solution:**
698
+
699
+ ```typescript
700
+ onAuthError: async ({ error, provider }) => {
701
+ if (error.code === "access_denied") {
702
+ return {
703
+ redirectUrl: "/login?message=You must authorize the app to continue",
704
+ };
705
+ }
706
+ return { redirectUrl: "/login?error=oauth_failed" };
707
+ };
708
+ ```
709
+
710
+ ### Tokens Not Stored
711
+
712
+ **Issue:** `getConnection()` returns null
713
+
714
+ **Solution:**
715
+
716
+ - Set `storeTokens: true` in plugin configuration
717
+ - Verify `onAuthSuccess` completes successfully
718
+ - Check MongoDB connection is active
719
+
720
+ ## TypeScript Types
721
+
722
+ ```typescript
723
+ import { OAuthPluginOptions, OAuthProfile, OAuthTokens, OAuthConnection, OAuthError, OAuthPluginContext } from "@flink-app/oauth-plugin";
724
+
725
+ // OAuth profile from provider
726
+ interface OAuthProfile {
727
+ id: string;
728
+ email: string;
729
+ name?: string;
730
+ avatarUrl?: string;
731
+ raw: any;
732
+ }
733
+
734
+ // OAuth tokens (if storeTokens: true)
735
+ interface OAuthTokens {
736
+ accessToken: string;
737
+ refreshToken?: string;
738
+ expiresIn?: number;
739
+ scope?: string;
740
+ }
741
+
742
+ // Stored connection
743
+ interface OAuthConnection {
744
+ _id: string;
745
+ userId: string;
746
+ provider: "github" | "google";
747
+ providerId: string;
748
+ accessToken: string;
749
+ refreshToken?: string;
750
+ scope: string;
751
+ expiresAt?: Date;
752
+ createdAt: Date;
753
+ updatedAt: Date;
754
+ }
755
+ ```
756
+
757
+ ## Production Checklist
758
+
759
+ - [ ] Configure HTTPS for all OAuth callback URLs
760
+ - [ ] Set OAuth credentials in secure environment variables
761
+ - [ ] Configure JWT Auth Plugin with secure secret
762
+ - [ ] Set appropriate JWT token expiration
763
+ - [ ] Implement rate limiting on OAuth endpoints
764
+ - [ ] Set up monitoring and error alerting
765
+ - [ ] Test OAuth flow for all providers
766
+ - [ ] Implement proper error handling in callbacks
767
+ - [ ] Configure CORS for OAuth endpoints
768
+ - [ ] Set up session cleanup and monitoring
769
+ - [ ] Document OAuth provider setup for team
770
+ - [ ] Test token refresh mechanism (if using stored tokens)
771
+
772
+ ## Examples
773
+
774
+ See the `examples/` directory for complete working examples:
775
+
776
+ - `basic-auth.ts` - Basic OAuth authentication with JWT
777
+ - `multi-provider.ts` - Multiple provider linking
778
+ - `token-storage.ts` - Storing OAuth tokens for API access
779
+ - `api-client-auth.ts` - API client integration with `response_type=json`
780
+
781
+ ## License
782
+
783
+ MIT