@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.
- package/LICENSE +21 -0
- package/README.md +783 -0
- package/SECURITY.md +433 -0
- package/dist/OAuthInternalContext.d.ts +45 -0
- package/dist/OAuthInternalContext.js +2 -0
- package/dist/OAuthPlugin.d.ts +70 -0
- package/dist/OAuthPlugin.js +220 -0
- package/dist/OAuthPluginContext.d.ts +49 -0
- package/dist/OAuthPluginContext.js +2 -0
- package/dist/OAuthPluginOptions.d.ts +111 -0
- package/dist/OAuthPluginOptions.js +2 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +66 -0
- package/dist/providers/GitHubProvider.d.ts +32 -0
- package/dist/providers/GitHubProvider.js +82 -0
- package/dist/providers/GoogleProvider.d.ts +32 -0
- package/dist/providers/GoogleProvider.js +83 -0
- package/dist/providers/OAuthProvider.d.ts +69 -0
- package/dist/providers/OAuthProvider.js +2 -0
- package/dist/providers/OAuthProviderBase.d.ts +32 -0
- package/dist/providers/OAuthProviderBase.js +86 -0
- package/dist/providers/ProviderRegistry.d.ts +14 -0
- package/dist/providers/ProviderRegistry.js +24 -0
- package/dist/repos/OAuthConnectionRepo.d.ts +30 -0
- package/dist/repos/OAuthConnectionRepo.js +38 -0
- package/dist/repos/OAuthSessionRepo.d.ts +22 -0
- package/dist/repos/OAuthSessionRepo.js +28 -0
- package/dist/schemas/OAuthConnection.d.ts +12 -0
- package/dist/schemas/OAuthConnection.js +2 -0
- package/dist/schemas/OAuthSession.d.ts +9 -0
- package/dist/schemas/OAuthSession.js +2 -0
- package/dist/utils/encryption-utils.d.ts +34 -0
- package/dist/utils/encryption-utils.js +134 -0
- package/dist/utils/error-utils.d.ts +68 -0
- package/dist/utils/error-utils.js +120 -0
- package/dist/utils/state-utils.d.ts +36 -0
- package/dist/utils/state-utils.js +72 -0
- package/examples/api-client-auth.ts +550 -0
- package/examples/basic-auth.ts +288 -0
- package/examples/multi-provider.ts +409 -0
- package/examples/token-storage.ts +490 -0
- package/package.json +38 -0
- package/spec/OAuthHandlers.spec.ts +146 -0
- package/spec/OAuthPluginSpec.ts +31 -0
- package/spec/ProvidersSpec.ts +178 -0
- package/spec/README.md +365 -0
- package/spec/helpers/mockJwtAuthPlugin.ts +104 -0
- package/spec/helpers/mockOAuthProviders.ts +189 -0
- package/spec/helpers/reporter.ts +41 -0
- package/spec/helpers/testDatabase.ts +107 -0
- package/spec/helpers/testHelpers.ts +192 -0
- package/spec/integration-critical.spec.ts +857 -0
- package/spec/integration.spec.ts +301 -0
- package/spec/repositories.spec.ts +181 -0
- package/spec/support/jasmine.json +7 -0
- package/spec/utils/security.spec.ts +243 -0
- package/src/OAuthInternalContext.ts +46 -0
- package/src/OAuthPlugin.ts +251 -0
- package/src/OAuthPluginContext.ts +53 -0
- package/src/OAuthPluginOptions.ts +122 -0
- package/src/handlers/CallbackOAuth.ts +238 -0
- package/src/handlers/InitiateOAuth.ts +99 -0
- package/src/index.ts +62 -0
- package/src/providers/GitHubProvider.ts +90 -0
- package/src/providers/GoogleProvider.ts +91 -0
- package/src/providers/OAuthProvider.ts +77 -0
- package/src/providers/OAuthProviderBase.ts +98 -0
- package/src/providers/ProviderRegistry.ts +27 -0
- package/src/repos/OAuthConnectionRepo.ts +41 -0
- package/src/repos/OAuthSessionRepo.ts +30 -0
- package/src/repos/TTL_INDEX_NOTE.md +28 -0
- package/src/schemas/CallbackRequest.ts +64 -0
- package/src/schemas/InitiateRequest.ts +10 -0
- package/src/schemas/OAuthConnection.ts +12 -0
- package/src/schemas/OAuthSession.ts +9 -0
- package/src/utils/encryption-utils.ts +148 -0
- package/src/utils/error-utils.ts +139 -0
- package/src/utils/state-utils.ts +70 -0
- package/src/utils/token-response-utils.ts +49 -0
- package/src/utils/validation-utils.ts +120 -0
- package/tsconfig.dist.json +4 -0
- 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
|