@flink-app/oidc-plugin 1.0.0
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/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +846 -0
- package/dist/OidcInternalContext.d.ts +15 -0
- package/dist/OidcInternalContext.d.ts.map +1 -0
- package/dist/OidcInternalContext.js +2 -0
- package/dist/OidcPlugin.d.ts +77 -0
- package/dist/OidcPlugin.d.ts.map +1 -0
- package/dist/OidcPlugin.js +274 -0
- package/dist/OidcPluginContext.d.ts +73 -0
- package/dist/OidcPluginContext.d.ts.map +1 -0
- package/dist/OidcPluginContext.js +2 -0
- package/dist/OidcPluginOptions.d.ts +267 -0
- package/dist/OidcPluginOptions.d.ts.map +1 -0
- package/dist/OidcPluginOptions.js +2 -0
- package/dist/OidcProviderConfig.d.ts +77 -0
- package/dist/OidcProviderConfig.d.ts.map +1 -0
- package/dist/OidcProviderConfig.js +2 -0
- package/dist/handlers/CallbackOidc.d.ts +38 -0
- package/dist/handlers/CallbackOidc.d.ts.map +1 -0
- package/dist/handlers/CallbackOidc.js +219 -0
- package/dist/handlers/InitiateOidc.d.ts +35 -0
- package/dist/handlers/InitiateOidc.d.ts.map +1 -0
- package/dist/handlers/InitiateOidc.js +91 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/providers/OidcProvider.d.ts +90 -0
- package/dist/providers/OidcProvider.d.ts.map +1 -0
- package/dist/providers/OidcProvider.js +208 -0
- package/dist/providers/ProviderRegistry.d.ts +55 -0
- package/dist/providers/ProviderRegistry.d.ts.map +1 -0
- package/dist/providers/ProviderRegistry.js +94 -0
- package/dist/repos/OidcConnectionRepo.d.ts +75 -0
- package/dist/repos/OidcConnectionRepo.d.ts.map +1 -0
- package/dist/repos/OidcConnectionRepo.js +122 -0
- package/dist/repos/OidcSessionRepo.d.ts +57 -0
- package/dist/repos/OidcSessionRepo.d.ts.map +1 -0
- package/dist/repos/OidcSessionRepo.js +91 -0
- package/dist/schemas/CallbackRequest.d.ts +37 -0
- package/dist/schemas/CallbackRequest.d.ts.map +1 -0
- package/dist/schemas/CallbackRequest.js +2 -0
- package/dist/schemas/InitiateRequest.d.ts +17 -0
- package/dist/schemas/InitiateRequest.d.ts.map +1 -0
- package/dist/schemas/InitiateRequest.js +2 -0
- package/dist/schemas/OidcConnection.d.ts +69 -0
- package/dist/schemas/OidcConnection.d.ts.map +1 -0
- package/dist/schemas/OidcConnection.js +2 -0
- package/dist/schemas/OidcProfile.d.ts +69 -0
- package/dist/schemas/OidcProfile.d.ts.map +1 -0
- package/dist/schemas/OidcProfile.js +2 -0
- package/dist/schemas/OidcSession.d.ts +46 -0
- package/dist/schemas/OidcSession.d.ts.map +1 -0
- package/dist/schemas/OidcSession.js +2 -0
- package/dist/schemas/OidcTokenSet.d.ts +42 -0
- package/dist/schemas/OidcTokenSet.d.ts.map +1 -0
- package/dist/schemas/OidcTokenSet.js +2 -0
- package/dist/utils/claims-mapper.d.ts +46 -0
- package/dist/utils/claims-mapper.d.ts.map +1 -0
- package/dist/utils/claims-mapper.js +104 -0
- package/dist/utils/encryption-utils.d.ts +32 -0
- package/dist/utils/encryption-utils.d.ts.map +1 -0
- package/dist/utils/encryption-utils.js +82 -0
- package/dist/utils/error-utils.d.ts +65 -0
- package/dist/utils/error-utils.d.ts.map +1 -0
- package/dist/utils/error-utils.js +150 -0
- package/dist/utils/response-utils.d.ts +18 -0
- package/dist/utils/response-utils.d.ts.map +1 -0
- package/dist/utils/response-utils.js +42 -0
- package/dist/utils/state-utils.d.ts +36 -0
- package/dist/utils/state-utils.d.ts.map +1 -0
- package/dist/utils/state-utils.js +66 -0
- package/examples/basic-oidc.ts +151 -0
- package/examples/multi-provider.ts +146 -0
- package/package.json +44 -0
- package/spec/handlers/InitiateOidc.spec.ts +62 -0
- package/spec/helpers/reporter.ts +34 -0
- package/spec/helpers/test-helpers.ts +108 -0
- package/spec/plugin/OidcPlugin.spec.ts +126 -0
- package/spec/providers/ProviderRegistry.spec.ts +197 -0
- package/spec/repos/OidcConnectionRepo.spec.ts +257 -0
- package/spec/repos/OidcSessionRepo.spec.ts +196 -0
- package/spec/support/jasmine.json +7 -0
- package/spec/utils/claims-mapper.spec.ts +257 -0
- package/spec/utils/encryption-utils.spec.ts +126 -0
- package/spec/utils/error-utils.spec.ts +107 -0
- package/spec/utils/state-utils.spec.ts +102 -0
- package/src/OidcInternalContext.ts +15 -0
- package/src/OidcPlugin.ts +290 -0
- package/src/OidcPluginContext.ts +76 -0
- package/src/OidcPluginOptions.ts +286 -0
- package/src/OidcProviderConfig.ts +87 -0
- package/src/handlers/CallbackOidc.ts +257 -0
- package/src/handlers/InitiateOidc.ts +110 -0
- package/src/index.ts +38 -0
- package/src/providers/OidcProvider.ts +237 -0
- package/src/providers/ProviderRegistry.ts +107 -0
- package/src/repos/OidcConnectionRepo.ts +132 -0
- package/src/repos/OidcSessionRepo.ts +99 -0
- package/src/schemas/CallbackRequest.ts +41 -0
- package/src/schemas/InitiateRequest.ts +17 -0
- package/src/schemas/OidcConnection.ts +80 -0
- package/src/schemas/OidcProfile.ts +79 -0
- package/src/schemas/OidcSession.ts +52 -0
- package/src/schemas/OidcTokenSet.ts +47 -0
- package/src/utils/claims-mapper.ts +114 -0
- package/src/utils/encryption-utils.ts +92 -0
- package/src/utils/error-utils.ts +167 -0
- package/src/utils/response-utils.ts +41 -0
- package/src/utils/state-utils.ts +66 -0
- package/tsconfig.dist.json +9 -0
- package/tsconfig.json +20 -0
package/README.md
ADDED
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
# OIDC Authentication Plugin
|
|
2
|
+
|
|
3
|
+
A flexible OpenID Connect (OIDC) authentication plugin for Flink that supports generic Identity Providers (IdPs) with MongoDB session storage, JWT token generation, and configurable token handling.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- OpenID Connect Authorization Code flow with any OIDC-compliant IdP
|
|
8
|
+
- Automatic JWT token generation via JWT Auth Plugin integration
|
|
9
|
+
- MongoDB session storage with automatic TTL cleanup
|
|
10
|
+
- Support for multiple OIDC providers per application
|
|
11
|
+
- OIDC Discovery support (automatic endpoint configuration)
|
|
12
|
+
- Manual endpoint configuration for custom IdPs
|
|
13
|
+
- PKCE (Proof Key for Code Exchange) for enhanced security
|
|
14
|
+
- CSRF protection with cryptographically secure state parameters
|
|
15
|
+
- Nonce validation for ID token replay protection
|
|
16
|
+
- Encrypted token storage (AES-256-GCM)
|
|
17
|
+
- JIT (Just-In-Time) user provisioning
|
|
18
|
+
- Built-in HTTP endpoints for OIDC flow
|
|
19
|
+
- TypeScript support with full type safety
|
|
20
|
+
- Configurable response formats (JSON, URL fragment)
|
|
21
|
+
- Dynamic provider loading from database
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @flink-app/oidc-plugin @flink-app/jwt-auth-plugin
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Prerequisites
|
|
30
|
+
|
|
31
|
+
### 1. JWT Auth Plugin Dependency
|
|
32
|
+
|
|
33
|
+
This plugin requires `@flink-app/jwt-auth-plugin` to be installed and configured. The OIDC plugin uses the JWT Auth Plugin to generate authentication tokens after successful OIDC authentication.
|
|
34
|
+
|
|
35
|
+
### 2. OIDC Provider Credentials
|
|
36
|
+
|
|
37
|
+
You need OIDC application credentials from your Identity Provider:
|
|
38
|
+
|
|
39
|
+
#### Generic OIDC Provider Setup
|
|
40
|
+
|
|
41
|
+
1. Register your application with your IdP
|
|
42
|
+
2. Configure the redirect URI: `https://yourdomain.com/oidc/{provider}/callback`
|
|
43
|
+
3. Obtain Client ID and Client Secret
|
|
44
|
+
4. Note the Issuer URL (e.g., `https://idp.example.com`)
|
|
45
|
+
5. Get the Discovery URL (usually `{issuer}/.well-known/openid-configuration`)
|
|
46
|
+
|
|
47
|
+
#### Common OIDC Providers
|
|
48
|
+
|
|
49
|
+
- **Azure AD / Entra ID**: `https://login.microsoftonline.com/{tenant}/v2.0`
|
|
50
|
+
- **Okta**: `https://{domain}.okta.com`
|
|
51
|
+
- **Auth0**: `https://{domain}.auth0.com`
|
|
52
|
+
- **Keycloak**: `https://{domain}/realms/{realm}`
|
|
53
|
+
- **Google**: `https://accounts.google.com` (use oauth-plugin for Google)
|
|
54
|
+
|
|
55
|
+
### 3. MongoDB Connection
|
|
56
|
+
|
|
57
|
+
The plugin requires MongoDB to store OIDC sessions during the authentication flow.
|
|
58
|
+
|
|
59
|
+
## Quick Start
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import { FlinkApp } from "@flink-app/flink";
|
|
63
|
+
import { jwtAuthPlugin } from "@flink-app/jwt-auth-plugin";
|
|
64
|
+
import { oidcPlugin } from "@flink-app/oidc-plugin";
|
|
65
|
+
import { Context } from "./Context";
|
|
66
|
+
|
|
67
|
+
const app = new FlinkApp<Context>({
|
|
68
|
+
name: "My App",
|
|
69
|
+
|
|
70
|
+
// JWT Auth Plugin MUST be configured first
|
|
71
|
+
auth: jwtAuthPlugin({
|
|
72
|
+
secret: process.env.JWT_SECRET!,
|
|
73
|
+
getUser: async (tokenData) => {
|
|
74
|
+
return await app.ctx.repos.userRepo.getById(tokenData.userId);
|
|
75
|
+
},
|
|
76
|
+
rolePermissions: {
|
|
77
|
+
user: ["read:own", "write:own"],
|
|
78
|
+
admin: ["read:all", "write:all"],
|
|
79
|
+
},
|
|
80
|
+
}),
|
|
81
|
+
|
|
82
|
+
db: {
|
|
83
|
+
uri: process.env.MONGODB_URI!,
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
plugins: [
|
|
87
|
+
oidcPlugin({
|
|
88
|
+
providers: {
|
|
89
|
+
// Provider name used in URLs: /oidc/acme/initiate
|
|
90
|
+
acme: {
|
|
91
|
+
issuer: process.env.OIDC_ISSUER!,
|
|
92
|
+
clientId: process.env.OIDC_CLIENT_ID!,
|
|
93
|
+
clientSecret: process.env.OIDC_CLIENT_SECRET!,
|
|
94
|
+
callbackUrl: "https://myapp.com/oidc/acme/callback",
|
|
95
|
+
|
|
96
|
+
// Option 1: Use OIDC Discovery (recommended)
|
|
97
|
+
discoveryUrl: `${process.env.OIDC_ISSUER}/.well-known/openid-configuration`,
|
|
98
|
+
|
|
99
|
+
// Option 2: Manual endpoint configuration (if discovery not available)
|
|
100
|
+
// authorizationEndpoint: "https://idp.acme.com/authorize",
|
|
101
|
+
// tokenEndpoint: "https://idp.acme.com/token",
|
|
102
|
+
// userinfoEndpoint: "https://idp.acme.com/userinfo",
|
|
103
|
+
// jwksUri: "https://idp.acme.com/.well-known/jwks.json",
|
|
104
|
+
|
|
105
|
+
scope: ["openid", "email", "profile"],
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
// Callback after successful OIDC authentication (JIT provisioning)
|
|
110
|
+
onAuthSuccess: async ({ profile, claims, provider }, ctx) => {
|
|
111
|
+
// Find user by OIDC subject + issuer (unique IdP identifier)
|
|
112
|
+
let user = await ctx.repos.userRepo.getOne({
|
|
113
|
+
"oidcConnections.subject": claims.sub,
|
|
114
|
+
"oidcConnections.issuer": claims.iss,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (!user) {
|
|
118
|
+
// JIT provisioning - create new user
|
|
119
|
+
user = await ctx.repos.userRepo.create({
|
|
120
|
+
email: claims.email,
|
|
121
|
+
name: claims.name,
|
|
122
|
+
emailVerified: claims.email_verified || false,
|
|
123
|
+
oidcConnections: [
|
|
124
|
+
{
|
|
125
|
+
issuer: claims.iss,
|
|
126
|
+
subject: claims.sub,
|
|
127
|
+
provider: "acme",
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
createdAt: new Date(),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Generate JWT token for YOUR application
|
|
135
|
+
const token = await ctx.plugins.jwtAuth.createToken({ userId: user._id, email: user.email }, ["user"]);
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
user,
|
|
139
|
+
token,
|
|
140
|
+
redirectUrl: "/dashboard",
|
|
141
|
+
};
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
// Optional: Handle OIDC errors
|
|
145
|
+
onAuthError: async ({ error, provider }) => {
|
|
146
|
+
console.error(`OIDC error for ${provider}:`, error);
|
|
147
|
+
return {
|
|
148
|
+
redirectUrl: `/login?error=${error.code}`,
|
|
149
|
+
};
|
|
150
|
+
},
|
|
151
|
+
}),
|
|
152
|
+
],
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await app.start();
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Configuration
|
|
159
|
+
|
|
160
|
+
### OidcPluginOptions
|
|
161
|
+
|
|
162
|
+
| Option | Type | Required | Default | Description |
|
|
163
|
+
| --------------------------- | ---------- | -------- | ------------------ | ------------------------------------------------------ |
|
|
164
|
+
| `providers` | `object` | Yes | - | OIDC provider configurations (at least one required) |
|
|
165
|
+
| `storeTokens` | `boolean` | No | `false` | Store encrypted OIDC tokens for future API access |
|
|
166
|
+
| `onAuthSuccess` | `Function` | Yes | - | Callback after successful authentication (JIT) |
|
|
167
|
+
| `onAuthError` | `Function` | No | - | Callback on OIDC errors |
|
|
168
|
+
| `providerLoader` | `Function` | No | - | Dynamic provider loading from database |
|
|
169
|
+
| `sessionTTL` | `number` | No | `600` | Session TTL in seconds (default: 10 minutes) |
|
|
170
|
+
| `sessionsCollectionName` | `string` | No | `"oidc_sessions"` | MongoDB collection for sessions |
|
|
171
|
+
| `connectionsCollectionName` | `string` | No | `"oidc_connections"` | MongoDB collection for connections |
|
|
172
|
+
| `encryptionKey` | `string` | No | (derived) | Encryption key for tokens (32+ chars recommended) |
|
|
173
|
+
| `registerRoutes` | `boolean` | No | `true` | Auto-register OIDC routes |
|
|
174
|
+
|
|
175
|
+
### Provider Configuration
|
|
176
|
+
|
|
177
|
+
#### OIDC Discovery (Recommended)
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
{
|
|
181
|
+
issuer: "https://idp.acme.com",
|
|
182
|
+
clientId: "your-client-id",
|
|
183
|
+
clientSecret: "your-client-secret",
|
|
184
|
+
callbackUrl: "https://myapp.com/oidc/acme/callback",
|
|
185
|
+
discoveryUrl: "https://idp.acme.com/.well-known/openid-configuration",
|
|
186
|
+
scope: ["openid", "email", "profile"]
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
#### Manual Configuration
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
{
|
|
194
|
+
issuer: "https://idp.acme.com",
|
|
195
|
+
clientId: "your-client-id",
|
|
196
|
+
clientSecret: "your-client-secret",
|
|
197
|
+
callbackUrl: "https://myapp.com/oidc/acme/callback",
|
|
198
|
+
authorizationEndpoint: "https://idp.acme.com/authorize",
|
|
199
|
+
tokenEndpoint: "https://idp.acme.com/token",
|
|
200
|
+
userinfoEndpoint: "https://idp.acme.com/userinfo",
|
|
201
|
+
jwksUri: "https://idp.acme.com/.well-known/jwks.json",
|
|
202
|
+
scope: ["openid", "email", "profile"]
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Callback Functions
|
|
207
|
+
|
|
208
|
+
#### onAuthSuccess
|
|
209
|
+
|
|
210
|
+
Called when OIDC authentication succeeds. Must generate and return a JWT token.
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
onAuthSuccess: async (params: {
|
|
214
|
+
profile: OidcProfile; // Normalized user profile
|
|
215
|
+
claims: Record<string, any>; // Raw OIDC claims
|
|
216
|
+
provider: string; // Provider name
|
|
217
|
+
tokens?: OidcTokenSet; // Only if storeTokens: true
|
|
218
|
+
}, ctx: Context) => Promise<{
|
|
219
|
+
user: any;
|
|
220
|
+
token: string; // JWT token from ctx.plugins.jwtAuth.createToken()
|
|
221
|
+
redirectUrl?: string;
|
|
222
|
+
}>
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Important:** The `redirectUrl` should NOT include the token. The plugin automatically appends `#token=...` to the URL.
|
|
226
|
+
|
|
227
|
+
#### onAuthError
|
|
228
|
+
|
|
229
|
+
Called when OIDC authentication fails.
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
onAuthError: async (params: {
|
|
233
|
+
error: OidcError;
|
|
234
|
+
provider: string;
|
|
235
|
+
}) => Promise<{
|
|
236
|
+
redirectUrl?: string;
|
|
237
|
+
}>
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## OIDC Flow
|
|
241
|
+
|
|
242
|
+
### Complete Authentication Flow
|
|
243
|
+
|
|
244
|
+
```
|
|
245
|
+
1. User visits: GET /login
|
|
246
|
+
→ Shows "Login via Portal" button
|
|
247
|
+
|
|
248
|
+
2. User clicks button
|
|
249
|
+
→ Redirects to: GET /oidc/acme/initiate
|
|
250
|
+
|
|
251
|
+
3. Plugin generates state, code_verifier, nonce, stores session
|
|
252
|
+
→ Redirects to IdP: https://idp.acme.com/authorize?
|
|
253
|
+
client_id=...&
|
|
254
|
+
redirect_uri=https://myapp.com/oidc/acme/callback&
|
|
255
|
+
scope=openid+email+profile&
|
|
256
|
+
state=...&
|
|
257
|
+
code_challenge=...&
|
|
258
|
+
code_challenge_method=S256&
|
|
259
|
+
nonce=...&
|
|
260
|
+
response_type=code
|
|
261
|
+
|
|
262
|
+
4. User logs in at IdP portal
|
|
263
|
+
→ IdP validates credentials
|
|
264
|
+
|
|
265
|
+
5. IdP redirects back: GET /oidc/acme/callback?code=...&state=...
|
|
266
|
+
|
|
267
|
+
6. Plugin validates state (CSRF protection)
|
|
268
|
+
→ Exchanges code for tokens using code_verifier (PKCE)
|
|
269
|
+
→ Validates ID token signature (JWT)
|
|
270
|
+
→ Validates nonce in ID token (replay protection)
|
|
271
|
+
→ Extracts claims from ID token
|
|
272
|
+
→ Optionally calls UserInfo endpoint
|
|
273
|
+
|
|
274
|
+
7. Plugin calls onAuthSuccess with profile
|
|
275
|
+
→ App checks if user exists by subject+issuer
|
|
276
|
+
→ If not, JIT create user (provisions account)
|
|
277
|
+
→ App generates JWT token via ctx.plugins.jwtAuth.createToken()
|
|
278
|
+
|
|
279
|
+
8. Plugin returns JWT to client
|
|
280
|
+
→ Redirects to: https://myapp.com/dashboard#token=eyJ...
|
|
281
|
+
|
|
282
|
+
9. Frontend extracts token from URL fragment
|
|
283
|
+
→ Stores JWT in localStorage/sessionStorage
|
|
284
|
+
→ Uses JWT for subsequent API calls
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Initiate OIDC Flow
|
|
288
|
+
|
|
289
|
+
```
|
|
290
|
+
GET /oidc/:provider/initiate?redirectUri={optional_redirect}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
**Example:**
|
|
294
|
+
|
|
295
|
+
```
|
|
296
|
+
GET /oidc/acme/initiate
|
|
297
|
+
GET /oidc/acme/initiate?redirectUri=https://myapp.com/welcome
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**Response:**
|
|
301
|
+
|
|
302
|
+
- 302 redirect to IdP authorization URL
|
|
303
|
+
|
|
304
|
+
### OIDC Callback
|
|
305
|
+
|
|
306
|
+
```
|
|
307
|
+
GET /oidc/:provider/callback?code={auth_code}&state={state}&response_type={json}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
**Query Parameters:**
|
|
311
|
+
|
|
312
|
+
- `code` - Authorization code from IdP
|
|
313
|
+
- `state` - CSRF protection token
|
|
314
|
+
- `response_type` - Optional: `json` for JSON response, omit for redirect
|
|
315
|
+
|
|
316
|
+
**Response Formats:**
|
|
317
|
+
|
|
318
|
+
1. **URL Fragment Redirect** (default):
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
https://myapp.com/dashboard#token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
2. **JSON Response** (when `response_type=json`):
|
|
325
|
+
|
|
326
|
+
```json
|
|
327
|
+
{
|
|
328
|
+
"user": {
|
|
329
|
+
"_id": "...",
|
|
330
|
+
"email": "user@example.com",
|
|
331
|
+
"name": "John Doe"
|
|
332
|
+
},
|
|
333
|
+
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Extracting JWT Token in Frontend
|
|
338
|
+
|
|
339
|
+
**IMPORTANT:** The plugin returns the JWT token as a **URL fragment** (`#token=...`), NOT as a query parameter (`?token=...`).
|
|
340
|
+
|
|
341
|
+
```javascript
|
|
342
|
+
// ✅ CORRECT - Read from URL fragment (hash)
|
|
343
|
+
const hash = window.location.hash.slice(1); // Remove leading #
|
|
344
|
+
const params = new URLSearchParams(hash);
|
|
345
|
+
const token = params.get("token");
|
|
346
|
+
|
|
347
|
+
// Store JWT token
|
|
348
|
+
localStorage.setItem("jwt_token", token);
|
|
349
|
+
|
|
350
|
+
// Clean URL (remove fragment)
|
|
351
|
+
window.history.replaceState({}, document.title, "/dashboard");
|
|
352
|
+
|
|
353
|
+
// ❌ WRONG - Reading from query parameters won't work
|
|
354
|
+
const params = new URLSearchParams(window.location.search);
|
|
355
|
+
const token = params.get("token"); // This returns null!
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## Context API
|
|
359
|
+
|
|
360
|
+
The plugin exposes methods via `ctx.plugins.oidc`:
|
|
361
|
+
|
|
362
|
+
### getConnection
|
|
363
|
+
|
|
364
|
+
Get stored OIDC connection for a user and provider.
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
const connection = await ctx.plugins.oidc.getConnection(userId, "acme");
|
|
368
|
+
|
|
369
|
+
// Returns OidcConnection or null
|
|
370
|
+
interface OidcConnection {
|
|
371
|
+
_id: string;
|
|
372
|
+
userId: string;
|
|
373
|
+
provider: string;
|
|
374
|
+
subject: string; // OIDC sub claim
|
|
375
|
+
issuer: string; // OIDC iss claim
|
|
376
|
+
email?: string;
|
|
377
|
+
accessToken?: string; // Decrypted (if storeTokens enabled)
|
|
378
|
+
idToken?: string; // Decrypted
|
|
379
|
+
refreshToken?: string; // Decrypted
|
|
380
|
+
scope?: string;
|
|
381
|
+
expiresAt?: Date;
|
|
382
|
+
createdAt: Date;
|
|
383
|
+
updatedAt: Date;
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### getConnections
|
|
388
|
+
|
|
389
|
+
Get all OIDC connections for a user.
|
|
390
|
+
|
|
391
|
+
```typescript
|
|
392
|
+
const connections = await ctx.plugins.oidc.getConnections(userId);
|
|
393
|
+
// Returns OidcConnection[]
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### deleteConnection
|
|
397
|
+
|
|
398
|
+
Delete/unlink an OIDC connection.
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
await ctx.plugins.oidc.deleteConnection(userId, "acme");
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## Token Storage
|
|
405
|
+
|
|
406
|
+
### Auth-Only Mode (Default)
|
|
407
|
+
|
|
408
|
+
By default, `storeTokens: false`, meaning OIDC tokens are NOT stored. OIDC is used only for authentication.
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
oidcPlugin({
|
|
412
|
+
providers: { acme: {...} },
|
|
413
|
+
storeTokens: false, // OIDC tokens discarded after auth
|
|
414
|
+
onAuthSuccess: async ({ profile, claims }, ctx) => {
|
|
415
|
+
// Create user and generate JWT token
|
|
416
|
+
// OIDC tokens are NOT available here
|
|
417
|
+
}
|
|
418
|
+
})
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
**Use when:**
|
|
422
|
+
|
|
423
|
+
- You only need OIDC for user authentication
|
|
424
|
+
- You don't need to call IdP APIs on behalf of users
|
|
425
|
+
- You want to minimize stored credentials
|
|
426
|
+
|
|
427
|
+
### Token Storage Mode
|
|
428
|
+
|
|
429
|
+
Set `storeTokens: true` to store encrypted OIDC tokens for future API access.
|
|
430
|
+
|
|
431
|
+
```typescript
|
|
432
|
+
oidcPlugin({
|
|
433
|
+
providers: { acme: {...} },
|
|
434
|
+
storeTokens: true, // Store encrypted OIDC tokens
|
|
435
|
+
onAuthSuccess: async ({ profile, claims, tokens }, ctx) => {
|
|
436
|
+
// tokens.accessToken, tokens.idToken, tokens.refreshToken are available
|
|
437
|
+
// Tokens are automatically encrypted and stored
|
|
438
|
+
}
|
|
439
|
+
})
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
**Use when:**
|
|
443
|
+
|
|
444
|
+
- You need to call IdP APIs on behalf of users
|
|
445
|
+
- You want to access user's resources at the IdP
|
|
446
|
+
- You need long-term API access via refresh tokens
|
|
447
|
+
|
|
448
|
+
**Note:** OIDC tokens are encrypted using AES-256-GCM before storage.
|
|
449
|
+
|
|
450
|
+
## JIT Provisioning Patterns
|
|
451
|
+
|
|
452
|
+
### Basic JIT
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
onAuthSuccess: async ({ profile, claims }, ctx) => {
|
|
456
|
+
let user = await ctx.repos.userRepo.getOne({
|
|
457
|
+
"oidcConnections.subject": claims.sub,
|
|
458
|
+
"oidcConnections.issuer": claims.iss,
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
if (!user) {
|
|
462
|
+
user = await ctx.repos.userRepo.create({
|
|
463
|
+
email: claims.email,
|
|
464
|
+
name: claims.name,
|
|
465
|
+
oidcConnections: [
|
|
466
|
+
{
|
|
467
|
+
issuer: claims.iss,
|
|
468
|
+
subject: claims.sub,
|
|
469
|
+
provider: "acme",
|
|
470
|
+
},
|
|
471
|
+
],
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const token = await ctx.plugins.jwtAuth.createToken({ userId: user._id }, ["user"]);
|
|
476
|
+
|
|
477
|
+
return { user, token, redirectUrl: "/dashboard" };
|
|
478
|
+
};
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### JIT with Email Matching
|
|
482
|
+
|
|
483
|
+
```typescript
|
|
484
|
+
onAuthSuccess: async ({ profile, claims }, ctx) => {
|
|
485
|
+
// First try to find by OIDC connection
|
|
486
|
+
let user = await ctx.repos.userRepo.getOne({
|
|
487
|
+
"oidcConnections.subject": claims.sub,
|
|
488
|
+
"oidcConnections.issuer": claims.iss,
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// If not found, try to find by email
|
|
492
|
+
if (!user) {
|
|
493
|
+
user = await ctx.repos.userRepo.getOne({ email: claims.email });
|
|
494
|
+
|
|
495
|
+
if (user) {
|
|
496
|
+
// Link OIDC connection to existing user
|
|
497
|
+
user.oidcConnections = user.oidcConnections || [];
|
|
498
|
+
user.oidcConnections.push({
|
|
499
|
+
issuer: claims.iss,
|
|
500
|
+
subject: claims.sub,
|
|
501
|
+
provider: "acme",
|
|
502
|
+
});
|
|
503
|
+
await ctx.repos.userRepo.updateOne(user._id, {
|
|
504
|
+
oidcConnections: user.oidcConnections,
|
|
505
|
+
});
|
|
506
|
+
} else {
|
|
507
|
+
// Create new user
|
|
508
|
+
user = await ctx.repos.userRepo.create({
|
|
509
|
+
email: claims.email,
|
|
510
|
+
name: claims.name,
|
|
511
|
+
oidcConnections: [
|
|
512
|
+
{
|
|
513
|
+
issuer: claims.iss,
|
|
514
|
+
subject: claims.sub,
|
|
515
|
+
provider: "acme",
|
|
516
|
+
},
|
|
517
|
+
],
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const token = await ctx.plugins.jwtAuth.createToken({ userId: user._id }, ["user"]);
|
|
523
|
+
|
|
524
|
+
return { user, token, redirectUrl: "/dashboard" };
|
|
525
|
+
};
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### JIT with Role Mapping
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
onAuthSuccess: async ({ profile, claims }, ctx) => {
|
|
532
|
+
const groups = claims.groups || []; // Custom claim from IdP
|
|
533
|
+
const roles = mapGroupsToRoles(groups); // Your mapping logic
|
|
534
|
+
|
|
535
|
+
let user = await ctx.repos.userRepo.getOne({
|
|
536
|
+
"oidcConnections.subject": claims.sub,
|
|
537
|
+
"oidcConnections.issuer": claims.iss,
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
if (!user) {
|
|
541
|
+
user = await ctx.repos.userRepo.create({
|
|
542
|
+
email: claims.email,
|
|
543
|
+
name: claims.name,
|
|
544
|
+
roles,
|
|
545
|
+
oidcConnections: [
|
|
546
|
+
{
|
|
547
|
+
issuer: claims.iss,
|
|
548
|
+
subject: claims.sub,
|
|
549
|
+
provider: "acme",
|
|
550
|
+
},
|
|
551
|
+
],
|
|
552
|
+
});
|
|
553
|
+
} else {
|
|
554
|
+
// Update roles on each login
|
|
555
|
+
await ctx.repos.userRepo.updateOne(user._id, { roles });
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const token = await ctx.plugins.jwtAuth.createToken({ userId: user._id }, roles);
|
|
559
|
+
|
|
560
|
+
return { user, token, redirectUrl: "/dashboard" };
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
function mapGroupsToRoles(groups: string[]): string[] {
|
|
564
|
+
const roleMap: Record<string, string> = {
|
|
565
|
+
"admins": "admin",
|
|
566
|
+
"developers": "developer",
|
|
567
|
+
"users": "user",
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
return groups.map((group) => roleMap[group] || "user").filter(Boolean);
|
|
571
|
+
}
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
## Multi-Tenant / Dynamic Providers
|
|
575
|
+
|
|
576
|
+
Load OIDC provider configurations from database at runtime:
|
|
577
|
+
|
|
578
|
+
```typescript
|
|
579
|
+
oidcPlugin({
|
|
580
|
+
providers: {}, // Empty static config
|
|
581
|
+
|
|
582
|
+
// Dynamic loader
|
|
583
|
+
providerLoader: async (providerName) => {
|
|
584
|
+
const config = await ctx.repos.oidcProviderRepo.getByName(providerName);
|
|
585
|
+
|
|
586
|
+
if (!config || !config.enabled) {
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return {
|
|
591
|
+
issuer: config.issuer,
|
|
592
|
+
clientId: config.clientId,
|
|
593
|
+
clientSecret: decryptSecret(config.clientSecret),
|
|
594
|
+
callbackUrl: config.callbackUrl,
|
|
595
|
+
discoveryUrl: config.discoveryUrl,
|
|
596
|
+
scope: config.scope || ["openid", "email", "profile"]
|
|
597
|
+
};
|
|
598
|
+
},
|
|
599
|
+
|
|
600
|
+
onAuthSuccess: async ({ profile, claims, provider }, ctx) => {
|
|
601
|
+
// ... JIT provisioning
|
|
602
|
+
}
|
|
603
|
+
})
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
### Database Schema for Dynamic Providers
|
|
607
|
+
|
|
608
|
+
```typescript
|
|
609
|
+
interface OidcProviderConfigDB {
|
|
610
|
+
_id: string;
|
|
611
|
+
name: string; // Provider name (used in URLs)
|
|
612
|
+
organizationId?: string; // For multi-tenant
|
|
613
|
+
enabled: boolean;
|
|
614
|
+
issuer: string;
|
|
615
|
+
clientId: string;
|
|
616
|
+
clientSecret: string; // Store encrypted!
|
|
617
|
+
callbackUrl: string;
|
|
618
|
+
discoveryUrl?: string;
|
|
619
|
+
authorizationEndpoint?: string;
|
|
620
|
+
tokenEndpoint?: string;
|
|
621
|
+
userinfoEndpoint?: string;
|
|
622
|
+
jwksUri?: string;
|
|
623
|
+
scope: string[];
|
|
624
|
+
createdAt: Date;
|
|
625
|
+
updatedAt: Date;
|
|
626
|
+
}
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
## Security
|
|
630
|
+
|
|
631
|
+
### CSRF Protection
|
|
632
|
+
|
|
633
|
+
The plugin uses cryptographically secure state parameters to prevent CSRF attacks:
|
|
634
|
+
|
|
635
|
+
1. Generate 32-byte random state using `crypto.randomBytes()`
|
|
636
|
+
2. Store state in MongoDB session with 10-minute expiration
|
|
637
|
+
3. Validate state on callback using constant-time comparison
|
|
638
|
+
4. Clear session after successful validation
|
|
639
|
+
|
|
640
|
+
### PKCE (Proof Key for Code Exchange)
|
|
641
|
+
|
|
642
|
+
PKCE prevents authorization code interception attacks:
|
|
643
|
+
|
|
644
|
+
1. Generate `code_verifier` (random 43-128 char string)
|
|
645
|
+
2. Calculate `code_challenge` = BASE64URL(SHA256(code_verifier))
|
|
646
|
+
3. Send `code_challenge` in authorization request
|
|
647
|
+
4. Send `code_verifier` in token exchange
|
|
648
|
+
5. IdP validates: SHA256(code_verifier) === code_challenge
|
|
649
|
+
|
|
650
|
+
Even if attacker steals authorization code, they can't exchange it without the `code_verifier`.
|
|
651
|
+
|
|
652
|
+
### ID Token Validation
|
|
653
|
+
|
|
654
|
+
The plugin validates ID tokens automatically:
|
|
655
|
+
|
|
656
|
+
- JWT signature verification using IdP's public keys (JWKS)
|
|
657
|
+
- Issuer (`iss`) claim validation
|
|
658
|
+
- Audience (`aud`) claim validation (must match client ID)
|
|
659
|
+
- Expiration (`exp`) claim validation
|
|
660
|
+
- Nonce validation for replay protection
|
|
661
|
+
|
|
662
|
+
### Token Encryption
|
|
663
|
+
|
|
664
|
+
When `storeTokens: true`, OIDC tokens are encrypted before storage:
|
|
665
|
+
|
|
666
|
+
- **Algorithm:** AES-256-GCM
|
|
667
|
+
- **Encryption key:** Derived from client secret or custom key
|
|
668
|
+
- **Storage:** Encrypted tokens in MongoDB
|
|
669
|
+
- **Decryption:** Automatic when retrieved via context methods
|
|
670
|
+
|
|
671
|
+
### HTTPS Requirement
|
|
672
|
+
|
|
673
|
+
**IMPORTANT:** OIDC callback URLs MUST use HTTPS in production. IdPs reject HTTP callback URLs for security reasons.
|
|
674
|
+
|
|
675
|
+
### Secrets Management
|
|
676
|
+
|
|
677
|
+
Never commit secrets to version control:
|
|
678
|
+
|
|
679
|
+
```bash
|
|
680
|
+
# .env
|
|
681
|
+
OIDC_ISSUER=https://idp.acme.com
|
|
682
|
+
OIDC_CLIENT_ID=your_client_id
|
|
683
|
+
OIDC_CLIENT_SECRET=your_client_secret
|
|
684
|
+
OIDC_ENCRYPTION_KEY=your_encryption_key_32_chars_min
|
|
685
|
+
JWT_SECRET=your_jwt_secret
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
## Client Integration Examples
|
|
689
|
+
|
|
690
|
+
### React Web App
|
|
691
|
+
|
|
692
|
+
```typescript
|
|
693
|
+
import React from "react";
|
|
694
|
+
|
|
695
|
+
function LoginPage() {
|
|
696
|
+
const handleOidcLogin = () => {
|
|
697
|
+
// Redirect to OIDC initiation
|
|
698
|
+
window.location.href = "/oidc/acme/initiate?redirectUri=https://myapp.com/dashboard";
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
React.useEffect(() => {
|
|
702
|
+
// Extract token from URL fragment
|
|
703
|
+
const hash = window.location.hash.slice(1);
|
|
704
|
+
const params = new URLSearchParams(hash);
|
|
705
|
+
const token = params.get("token");
|
|
706
|
+
|
|
707
|
+
if (token) {
|
|
708
|
+
// Store JWT token
|
|
709
|
+
localStorage.setItem("jwt_token", token);
|
|
710
|
+
|
|
711
|
+
// Clean URL
|
|
712
|
+
window.history.replaceState({}, document.title, "/dashboard");
|
|
713
|
+
|
|
714
|
+
// Redirect to dashboard
|
|
715
|
+
window.location.href = "/dashboard";
|
|
716
|
+
}
|
|
717
|
+
}, []);
|
|
718
|
+
|
|
719
|
+
return (
|
|
720
|
+
<div>
|
|
721
|
+
<h1>Login</h1>
|
|
722
|
+
<button onClick={handleOidcLogin}>Login via Portal</button>
|
|
723
|
+
</div>
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
### React Native App
|
|
729
|
+
|
|
730
|
+
```typescript
|
|
731
|
+
import { openAuthSessionAsync } from "expo-auth-session";
|
|
732
|
+
|
|
733
|
+
async function loginWithOidc() {
|
|
734
|
+
const result = await openAuthSessionAsync("https://api.myapp.com/oidc/acme/initiate", "myapp://oidc/callback");
|
|
735
|
+
|
|
736
|
+
if (result.type === "success") {
|
|
737
|
+
const url = result.url;
|
|
738
|
+
|
|
739
|
+
// Extract token from URL fragment
|
|
740
|
+
const urlObj = new URL(url);
|
|
741
|
+
const token = new URLSearchParams(urlObj.hash.slice(1)).get("token");
|
|
742
|
+
|
|
743
|
+
if (token) {
|
|
744
|
+
await AsyncStorage.setItem("jwt_token", token);
|
|
745
|
+
// Navigate to home screen
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
## Troubleshooting
|
|
752
|
+
|
|
753
|
+
### OIDC Discovery Failed
|
|
754
|
+
|
|
755
|
+
**Issue:** `discovery_failed` error
|
|
756
|
+
|
|
757
|
+
**Solution:**
|
|
758
|
+
|
|
759
|
+
- Verify discovery URL is correct
|
|
760
|
+
- Check IdP is accessible from your server
|
|
761
|
+
- Try manual endpoint configuration instead
|
|
762
|
+
|
|
763
|
+
### Invalid State Parameter
|
|
764
|
+
|
|
765
|
+
**Issue:** `invalid_state` error
|
|
766
|
+
|
|
767
|
+
**Solution:**
|
|
768
|
+
|
|
769
|
+
- Ensure cookies are enabled (sessions use MongoDB, but CSRF validation may use cookies)
|
|
770
|
+
- Check session TTL hasn't expired (default: 10 minutes)
|
|
771
|
+
- Verify clock synchronization between servers
|
|
772
|
+
|
|
773
|
+
### Token Exchange Failed
|
|
774
|
+
|
|
775
|
+
**Issue:** `token_exchange_failed` error
|
|
776
|
+
|
|
777
|
+
**Solution:**
|
|
778
|
+
|
|
779
|
+
- Verify client ID and secret are correct
|
|
780
|
+
- Check callback URL matches exactly (including trailing slashes)
|
|
781
|
+
- Ensure code hasn't expired (typically 10 minutes)
|
|
782
|
+
- Check PKCE is supported by IdP
|
|
783
|
+
|
|
784
|
+
### ID Token Validation Failed
|
|
785
|
+
|
|
786
|
+
**Issue:** `id_token_validation_failed` error
|
|
787
|
+
|
|
788
|
+
**Solution:**
|
|
789
|
+
|
|
790
|
+
- Verify issuer URL matches ID token `iss` claim
|
|
791
|
+
- Check client ID matches ID token `aud` claim
|
|
792
|
+
- Ensure ID token hasn't expired
|
|
793
|
+
- Verify nonce matches
|
|
794
|
+
|
|
795
|
+
### JWT Token Not Generated
|
|
796
|
+
|
|
797
|
+
**Issue:** `jwt_generation_failed` error
|
|
798
|
+
|
|
799
|
+
**Solution:**
|
|
800
|
+
|
|
801
|
+
- Ensure JWT Auth Plugin is configured
|
|
802
|
+
- Verify `ctx.plugins.jwtAuth` is available in `onAuthSuccess`
|
|
803
|
+
- Check JWT secret is set in environment variables
|
|
804
|
+
|
|
805
|
+
## TypeScript Types
|
|
806
|
+
|
|
807
|
+
```typescript
|
|
808
|
+
import {
|
|
809
|
+
OidcPluginOptions,
|
|
810
|
+
OidcProfile,
|
|
811
|
+
OidcTokenSet,
|
|
812
|
+
OidcConnection,
|
|
813
|
+
OidcError,
|
|
814
|
+
OidcPluginContext,
|
|
815
|
+
} from "@flink-app/oidc-plugin";
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
## Production Checklist
|
|
819
|
+
|
|
820
|
+
- [ ] Configure HTTPS for all OIDC callback URLs
|
|
821
|
+
- [ ] Set OIDC credentials in secure environment variables
|
|
822
|
+
- [ ] Configure JWT Auth Plugin with secure secret
|
|
823
|
+
- [ ] Set appropriate JWT token expiration
|
|
824
|
+
- [ ] Implement rate limiting on OIDC endpoints
|
|
825
|
+
- [ ] Set up monitoring and error alerting
|
|
826
|
+
- [ ] Test OIDC flow for all providers
|
|
827
|
+
- [ ] Implement proper error handling in callbacks
|
|
828
|
+
- [ ] Configure CORS for OIDC endpoints
|
|
829
|
+
- [ ] Set up session cleanup and monitoring
|
|
830
|
+
- [ ] Document OIDC provider setup for team
|
|
831
|
+
- [ ] Test JIT provisioning logic
|
|
832
|
+
- [ ] Validate role mapping (if using)
|
|
833
|
+
|
|
834
|
+
## Examples
|
|
835
|
+
|
|
836
|
+
See the `examples/` directory for complete working examples:
|
|
837
|
+
|
|
838
|
+
- `basic-oidc.ts` - Basic OIDC authentication with JIT provisioning
|
|
839
|
+
- `multi-provider.ts` - Multiple OIDC provider support
|
|
840
|
+
- `token-storage.ts` - Storing OIDC tokens for API access
|
|
841
|
+
- `dynamic-providers.ts` - Loading providers from database
|
|
842
|
+
- `role-mapping.ts` - Mapping IdP groups to app roles
|
|
843
|
+
|
|
844
|
+
## License
|
|
845
|
+
|
|
846
|
+
MIT
|