@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.
Files changed (112) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +846 -0
  4. package/dist/OidcInternalContext.d.ts +15 -0
  5. package/dist/OidcInternalContext.d.ts.map +1 -0
  6. package/dist/OidcInternalContext.js +2 -0
  7. package/dist/OidcPlugin.d.ts +77 -0
  8. package/dist/OidcPlugin.d.ts.map +1 -0
  9. package/dist/OidcPlugin.js +274 -0
  10. package/dist/OidcPluginContext.d.ts +73 -0
  11. package/dist/OidcPluginContext.d.ts.map +1 -0
  12. package/dist/OidcPluginContext.js +2 -0
  13. package/dist/OidcPluginOptions.d.ts +267 -0
  14. package/dist/OidcPluginOptions.d.ts.map +1 -0
  15. package/dist/OidcPluginOptions.js +2 -0
  16. package/dist/OidcProviderConfig.d.ts +77 -0
  17. package/dist/OidcProviderConfig.d.ts.map +1 -0
  18. package/dist/OidcProviderConfig.js +2 -0
  19. package/dist/handlers/CallbackOidc.d.ts +38 -0
  20. package/dist/handlers/CallbackOidc.d.ts.map +1 -0
  21. package/dist/handlers/CallbackOidc.js +219 -0
  22. package/dist/handlers/InitiateOidc.d.ts +35 -0
  23. package/dist/handlers/InitiateOidc.d.ts.map +1 -0
  24. package/dist/handlers/InitiateOidc.js +91 -0
  25. package/dist/index.d.ts +27 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +40 -0
  28. package/dist/providers/OidcProvider.d.ts +90 -0
  29. package/dist/providers/OidcProvider.d.ts.map +1 -0
  30. package/dist/providers/OidcProvider.js +208 -0
  31. package/dist/providers/ProviderRegistry.d.ts +55 -0
  32. package/dist/providers/ProviderRegistry.d.ts.map +1 -0
  33. package/dist/providers/ProviderRegistry.js +94 -0
  34. package/dist/repos/OidcConnectionRepo.d.ts +75 -0
  35. package/dist/repos/OidcConnectionRepo.d.ts.map +1 -0
  36. package/dist/repos/OidcConnectionRepo.js +122 -0
  37. package/dist/repos/OidcSessionRepo.d.ts +57 -0
  38. package/dist/repos/OidcSessionRepo.d.ts.map +1 -0
  39. package/dist/repos/OidcSessionRepo.js +91 -0
  40. package/dist/schemas/CallbackRequest.d.ts +37 -0
  41. package/dist/schemas/CallbackRequest.d.ts.map +1 -0
  42. package/dist/schemas/CallbackRequest.js +2 -0
  43. package/dist/schemas/InitiateRequest.d.ts +17 -0
  44. package/dist/schemas/InitiateRequest.d.ts.map +1 -0
  45. package/dist/schemas/InitiateRequest.js +2 -0
  46. package/dist/schemas/OidcConnection.d.ts +69 -0
  47. package/dist/schemas/OidcConnection.d.ts.map +1 -0
  48. package/dist/schemas/OidcConnection.js +2 -0
  49. package/dist/schemas/OidcProfile.d.ts +69 -0
  50. package/dist/schemas/OidcProfile.d.ts.map +1 -0
  51. package/dist/schemas/OidcProfile.js +2 -0
  52. package/dist/schemas/OidcSession.d.ts +46 -0
  53. package/dist/schemas/OidcSession.d.ts.map +1 -0
  54. package/dist/schemas/OidcSession.js +2 -0
  55. package/dist/schemas/OidcTokenSet.d.ts +42 -0
  56. package/dist/schemas/OidcTokenSet.d.ts.map +1 -0
  57. package/dist/schemas/OidcTokenSet.js +2 -0
  58. package/dist/utils/claims-mapper.d.ts +46 -0
  59. package/dist/utils/claims-mapper.d.ts.map +1 -0
  60. package/dist/utils/claims-mapper.js +104 -0
  61. package/dist/utils/encryption-utils.d.ts +32 -0
  62. package/dist/utils/encryption-utils.d.ts.map +1 -0
  63. package/dist/utils/encryption-utils.js +82 -0
  64. package/dist/utils/error-utils.d.ts +65 -0
  65. package/dist/utils/error-utils.d.ts.map +1 -0
  66. package/dist/utils/error-utils.js +150 -0
  67. package/dist/utils/response-utils.d.ts +18 -0
  68. package/dist/utils/response-utils.d.ts.map +1 -0
  69. package/dist/utils/response-utils.js +42 -0
  70. package/dist/utils/state-utils.d.ts +36 -0
  71. package/dist/utils/state-utils.d.ts.map +1 -0
  72. package/dist/utils/state-utils.js +66 -0
  73. package/examples/basic-oidc.ts +151 -0
  74. package/examples/multi-provider.ts +146 -0
  75. package/package.json +44 -0
  76. package/spec/handlers/InitiateOidc.spec.ts +62 -0
  77. package/spec/helpers/reporter.ts +34 -0
  78. package/spec/helpers/test-helpers.ts +108 -0
  79. package/spec/plugin/OidcPlugin.spec.ts +126 -0
  80. package/spec/providers/ProviderRegistry.spec.ts +197 -0
  81. package/spec/repos/OidcConnectionRepo.spec.ts +257 -0
  82. package/spec/repos/OidcSessionRepo.spec.ts +196 -0
  83. package/spec/support/jasmine.json +7 -0
  84. package/spec/utils/claims-mapper.spec.ts +257 -0
  85. package/spec/utils/encryption-utils.spec.ts +126 -0
  86. package/spec/utils/error-utils.spec.ts +107 -0
  87. package/spec/utils/state-utils.spec.ts +102 -0
  88. package/src/OidcInternalContext.ts +15 -0
  89. package/src/OidcPlugin.ts +290 -0
  90. package/src/OidcPluginContext.ts +76 -0
  91. package/src/OidcPluginOptions.ts +286 -0
  92. package/src/OidcProviderConfig.ts +87 -0
  93. package/src/handlers/CallbackOidc.ts +257 -0
  94. package/src/handlers/InitiateOidc.ts +110 -0
  95. package/src/index.ts +38 -0
  96. package/src/providers/OidcProvider.ts +237 -0
  97. package/src/providers/ProviderRegistry.ts +107 -0
  98. package/src/repos/OidcConnectionRepo.ts +132 -0
  99. package/src/repos/OidcSessionRepo.ts +99 -0
  100. package/src/schemas/CallbackRequest.ts +41 -0
  101. package/src/schemas/InitiateRequest.ts +17 -0
  102. package/src/schemas/OidcConnection.ts +80 -0
  103. package/src/schemas/OidcProfile.ts +79 -0
  104. package/src/schemas/OidcSession.ts +52 -0
  105. package/src/schemas/OidcTokenSet.ts +47 -0
  106. package/src/utils/claims-mapper.ts +114 -0
  107. package/src/utils/encryption-utils.ts +92 -0
  108. package/src/utils/error-utils.ts +167 -0
  109. package/src/utils/response-utils.ts +41 -0
  110. package/src/utils/state-utils.ts +66 -0
  111. package/tsconfig.dist.json +9 -0
  112. package/tsconfig.json +20 -0
@@ -0,0 +1,290 @@
1
+ import { FlinkApp, FlinkPlugin, log } from "@flink-app/flink";
2
+ import { Db } from "mongodb";
3
+ import { OidcPluginOptions } from "./OidcPluginOptions";
4
+ import { OidcPluginContext } from "./OidcPluginContext";
5
+ import { OidcInternalContext } from "./OidcInternalContext";
6
+ import OidcSessionRepo from "./repos/OidcSessionRepo";
7
+ import OidcConnectionRepo from "./repos/OidcConnectionRepo";
8
+ import { encryptToken, decryptToken, validateEncryptionSecret } from "./utils/encryption-utils";
9
+ import OidcConnection from "./schemas/OidcConnection";
10
+ import { ProviderRegistry } from "./providers/ProviderRegistry";
11
+ import * as InitiateOidc from "./handlers/InitiateOidc";
12
+ import * as CallbackOidc from "./handlers/CallbackOidc";
13
+
14
+ /**
15
+ * OIDC Plugin Factory Function
16
+ *
17
+ * Creates a Flink plugin for OIDC authentication with generic IdP support.
18
+ * Integrates with JWT Auth Plugin for token generation.
19
+ *
20
+ * @param options - OIDC plugin configuration options
21
+ * @returns FlinkPlugin instance
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * import { jwtAuthPlugin } from '@flink-app/jwt-auth-plugin';
26
+ * import { oidcPlugin } from '@flink-app/oidc-plugin';
27
+ *
28
+ * const app = new FlinkApp({
29
+ * auth: jwtAuthPlugin({
30
+ * secret: process.env.JWT_SECRET!,
31
+ * getUser: async (tokenData) => {
32
+ * return ctx.repos.userRepo.getById(tokenData.userId);
33
+ * },
34
+ * rolePermissions: {
35
+ * user: ['read:own'],
36
+ * admin: ['read:all', 'write:all']
37
+ * }
38
+ * }),
39
+ *
40
+ * plugins: [
41
+ * oidcPlugin({
42
+ * providers: {
43
+ * acme: {
44
+ * issuer: 'https://idp.acme.com',
45
+ * clientId: process.env.OIDC_CLIENT_ID!,
46
+ * clientSecret: process.env.OIDC_CLIENT_SECRET!,
47
+ * callbackUrl: 'https://myapp.com/oidc/acme/callback',
48
+ * discoveryUrl: 'https://idp.acme.com/.well-known/openid-configuration'
49
+ * }
50
+ * },
51
+ * onAuthSuccess: async ({ profile, claims, provider }, ctx) => {
52
+ * // Find or create user (JIT provisioning)
53
+ * let user = await ctx.repos.userRepo.getOne({
54
+ * 'oidcConnections.subject': claims.sub,
55
+ * 'oidcConnections.issuer': claims.iss
56
+ * });
57
+ *
58
+ * if (!user) {
59
+ * user = await ctx.repos.userRepo.create({
60
+ * email: claims.email,
61
+ * name: claims.name,
62
+ * oidcConnections: [{
63
+ * issuer: claims.iss,
64
+ * subject: claims.sub,
65
+ * provider
66
+ * }]
67
+ * });
68
+ * }
69
+ *
70
+ * // Generate JWT token
71
+ * const token = await ctx.plugins.jwtAuth.createToken(
72
+ * { userId: user._id, email: user.email },
73
+ * ['user']
74
+ * );
75
+ *
76
+ * return {
77
+ * user,
78
+ * token,
79
+ * redirectUrl: '/dashboard'
80
+ * };
81
+ * }
82
+ * })
83
+ * ]
84
+ * });
85
+ * ```
86
+ */
87
+ export function oidcPlugin(options: OidcPluginOptions): FlinkPlugin {
88
+ // Validation
89
+ if (!options.providers || Object.keys(options.providers).length === 0) {
90
+ throw new Error("OIDC Plugin: At least one provider must be configured");
91
+ }
92
+
93
+ // Validate provider configurations
94
+ const configuredProviders = Object.keys(options.providers);
95
+ for (const providerName of configuredProviders) {
96
+ const providerConfig = options.providers[providerName];
97
+ if (!providerConfig) continue;
98
+
99
+ if (!providerConfig.issuer) {
100
+ throw new Error(`OIDC Plugin: ${providerName} issuer is required`);
101
+ }
102
+ if (!providerConfig.clientId) {
103
+ throw new Error(`OIDC Plugin: ${providerName} clientId is required`);
104
+ }
105
+ if (!providerConfig.clientSecret) {
106
+ throw new Error(`OIDC Plugin: ${providerName} clientSecret is required`);
107
+ }
108
+ if (!providerConfig.callbackUrl) {
109
+ throw new Error(`OIDC Plugin: ${providerName} callbackUrl is required`);
110
+ }
111
+
112
+ // Validate that either discoveryUrl or manual endpoints are provided
113
+ if (!providerConfig.discoveryUrl) {
114
+ if (!providerConfig.authorizationEndpoint || !providerConfig.tokenEndpoint || !providerConfig.jwksUri) {
115
+ throw new Error(
116
+ `OIDC Plugin: ${providerName} must have either discoveryUrl or manual endpoints ` +
117
+ `(authorizationEndpoint, tokenEndpoint, jwksUri)`
118
+ );
119
+ }
120
+ }
121
+ }
122
+
123
+ if (!options.onAuthSuccess) {
124
+ throw new Error("OIDC Plugin: onAuthSuccess callback is required");
125
+ }
126
+
127
+ // Determine encryption key
128
+ let encryptionKey = options.encryptionKey;
129
+ if (!encryptionKey) {
130
+ // Derive from first configured provider's client secret
131
+ const firstProvider = configuredProviders[0];
132
+ const firstProviderConfig = options.providers[firstProvider];
133
+ if (firstProviderConfig) {
134
+ encryptionKey = firstProviderConfig.clientSecret;
135
+ log.warn(
136
+ "OIDC Plugin: No encryption key provided, deriving from client secret. " + "For better security, provide a dedicated encryptionKey in options."
137
+ );
138
+ }
139
+ }
140
+
141
+ if (!encryptionKey || encryptionKey.length < 32) {
142
+ throw new Error("OIDC Plugin: Encryption key must be at least 32 characters");
143
+ }
144
+
145
+ // Validate encryption key
146
+ validateEncryptionSecret(encryptionKey);
147
+
148
+ let flinkApp: FlinkApp<OidcInternalContext>;
149
+ let sessionRepo: OidcSessionRepo;
150
+ let connectionRepo: OidcConnectionRepo;
151
+ let providerRegistry: ProviderRegistry;
152
+
153
+ /**
154
+ * Plugin initialization
155
+ */
156
+ async function init(app: FlinkApp<any>, db?: Db) {
157
+ log.info("Initializing OIDC Plugin...");
158
+
159
+ flinkApp = app as FlinkApp<OidcInternalContext>;
160
+
161
+ try {
162
+ if (!db) {
163
+ throw new Error("OIDC Plugin: Database connection is required");
164
+ }
165
+
166
+ // Initialize repositories
167
+ const sessionsCollectionName = options.sessionsCollectionName || "oidc_sessions";
168
+ const connectionsCollectionName = options.connectionsCollectionName || "oidc_connections";
169
+
170
+ sessionRepo = new OidcSessionRepo(sessionsCollectionName, db);
171
+ connectionRepo = new OidcConnectionRepo(connectionsCollectionName, db);
172
+
173
+ flinkApp.addRepo("oidcSessionRepo", sessionRepo as any);
174
+ flinkApp.addRepo("oidcConnectionRepo", connectionRepo as any);
175
+
176
+ // Create TTL index for session expiration
177
+ const sessionTTL = options.sessionTTL || 600; // Default 10 minutes
178
+ await db.collection(sessionsCollectionName).createIndex({ createdAt: 1 }, { expireAfterSeconds: sessionTTL });
179
+
180
+ log.info(`OIDC Plugin: Created TTL index on ${sessionsCollectionName} with ${sessionTTL}s expiration`);
181
+
182
+ // Initialize provider registry
183
+ providerRegistry = new ProviderRegistry(options.providers, options.providerLoader);
184
+
185
+ // Store provider registry in app context for handlers to access
186
+ (flinkApp.ctx as any).oidcProviderRegistry = providerRegistry;
187
+
188
+ // Register OIDC handlers
189
+ // Only register handlers if registerRoutes is enabled (default: true)
190
+ if (options.registerRoutes !== false) {
191
+ flinkApp.addHandler(InitiateOidc);
192
+ flinkApp.addHandler(CallbackOidc);
193
+ }
194
+
195
+ log.info(`OIDC Plugin initialized with providers: ${configuredProviders.join(", ")}`);
196
+ } catch (error) {
197
+ log.error("Failed to initialize OIDC Plugin:", error);
198
+ throw error;
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Get OIDC connection for a user
204
+ */
205
+ async function getConnection(userId: string, provider: string): Promise<OidcConnection | null> {
206
+ if (!connectionRepo) {
207
+ throw new Error("OIDC Plugin: Plugin not initialized");
208
+ }
209
+
210
+ const connection = await connectionRepo.findByUserAndProvider(userId, provider);
211
+
212
+ if (!connection) {
213
+ return null;
214
+ }
215
+
216
+ // Decrypt tokens before returning
217
+ if (connection.accessToken && encryptionKey) {
218
+ connection.accessToken = decryptToken(connection.accessToken, encryptionKey);
219
+ }
220
+
221
+ if (connection.idToken && encryptionKey) {
222
+ connection.idToken = decryptToken(connection.idToken, encryptionKey);
223
+ }
224
+
225
+ if (connection.refreshToken && encryptionKey) {
226
+ connection.refreshToken = decryptToken(connection.refreshToken, encryptionKey);
227
+ }
228
+
229
+ return connection;
230
+ }
231
+
232
+ /**
233
+ * Get all OIDC connections for a user
234
+ */
235
+ async function getConnections(userId: string): Promise<OidcConnection[]> {
236
+ if (!connectionRepo) {
237
+ throw new Error("OIDC Plugin: Plugin not initialized");
238
+ }
239
+
240
+ const connections = await connectionRepo.findByUserId(userId);
241
+
242
+ // Decrypt tokens in each connection
243
+ return connections.map((connection) => {
244
+ if (connection.accessToken && encryptionKey) {
245
+ connection.accessToken = decryptToken(connection.accessToken, encryptionKey);
246
+ }
247
+
248
+ if (connection.idToken && encryptionKey) {
249
+ connection.idToken = decryptToken(connection.idToken, encryptionKey);
250
+ }
251
+
252
+ if (connection.refreshToken && encryptionKey) {
253
+ connection.refreshToken = decryptToken(connection.refreshToken, encryptionKey);
254
+ }
255
+
256
+ return connection;
257
+ });
258
+ }
259
+
260
+ /**
261
+ * Delete OIDC connection for a user
262
+ */
263
+ async function deleteConnection(userId: string, provider: string): Promise<void> {
264
+ if (!connectionRepo) {
265
+ throw new Error("OIDC Plugin: Plugin not initialized");
266
+ }
267
+
268
+ await connectionRepo.deleteByUserAndProvider(userId, provider);
269
+ log.info(`OIDC Plugin: Deleted ${provider} connection for user ${userId}`);
270
+ }
271
+
272
+ /**
273
+ * Plugin context exposed via ctx.plugins.oidc
274
+ */
275
+ const pluginCtx: OidcPluginContext["oidc"] = {
276
+ getConnection,
277
+ getConnections,
278
+ deleteConnection,
279
+ options: Object.freeze({ ...options }),
280
+ };
281
+
282
+ return {
283
+ id: "oidc",
284
+ db: {
285
+ useHostDb: true,
286
+ },
287
+ ctx: pluginCtx,
288
+ init,
289
+ };
290
+ }
@@ -0,0 +1,76 @@
1
+ import OidcConnection from "./schemas/OidcConnection";
2
+ import { OidcPluginOptions } from "./OidcPluginOptions";
3
+
4
+ /**
5
+ * OIDC Plugin context API exposed via ctx.plugins.oidc
6
+ *
7
+ * Provides methods to manage OIDC connections for users.
8
+ */
9
+ export interface OidcPluginContext {
10
+ oidc: {
11
+ /**
12
+ * Get OIDC connection for a user and provider
13
+ *
14
+ * Returns the stored OIDC connection with decrypted tokens (if storeTokens enabled).
15
+ *
16
+ * @param userId - Application user ID
17
+ * @param provider - Provider name (e.g., "acme")
18
+ * @returns OIDC connection or null if not found
19
+ *
20
+ * Example:
21
+ * ```typescript
22
+ * const connection = await ctx.plugins.oidc.getConnection(user._id, 'acme');
23
+ * if (connection) {
24
+ * console.log('User connected to:', connection.issuer);
25
+ * console.log('Subject:', connection.subject);
26
+ * if (connection.accessToken) {
27
+ * // Use access token to call IdP APIs
28
+ * }
29
+ * }
30
+ * ```
31
+ */
32
+ getConnection: (userId: string, provider: string) => Promise<OidcConnection | null>;
33
+
34
+ /**
35
+ * Get all OIDC connections for a user
36
+ *
37
+ * Returns all IdP connections for the user with decrypted tokens.
38
+ *
39
+ * @param userId - Application user ID
40
+ * @returns Array of OIDC connections
41
+ *
42
+ * Example:
43
+ * ```typescript
44
+ * const connections = await ctx.plugins.oidc.getConnections(user._id);
45
+ * console.log('User has', connections.length, 'IdP connections');
46
+ * connections.forEach(conn => {
47
+ * console.log('- Provider:', conn.provider, 'Email:', conn.email);
48
+ * });
49
+ * ```
50
+ */
51
+ getConnections: (userId: string) => Promise<OidcConnection[]>;
52
+
53
+ /**
54
+ * Delete/unlink OIDC connection for a user
55
+ *
56
+ * Removes the connection between the user and the IdP.
57
+ * The user will need to re-authenticate with the IdP.
58
+ *
59
+ * @param userId - Application user ID
60
+ * @param provider - Provider name (e.g., "acme")
61
+ *
62
+ * Example:
63
+ * ```typescript
64
+ * // User wants to disconnect their Acme account
65
+ * await ctx.plugins.oidc.deleteConnection(user._id, 'acme');
66
+ * ```
67
+ */
68
+ deleteConnection: (userId: string, provider: string) => Promise<void>;
69
+
70
+ /**
71
+ * Plugin configuration options (read-only)
72
+ * Useful for checking plugin settings at runtime
73
+ */
74
+ options: Readonly<OidcPluginOptions>;
75
+ };
76
+ }
@@ -0,0 +1,286 @@
1
+ import OidcProfile from "./schemas/OidcProfile";
2
+ import OidcTokenSet from "./schemas/OidcTokenSet";
3
+ import { OidcProviderConfig } from "./OidcProviderConfig";
4
+
5
+ /**
6
+ * OIDC error information passed to onAuthError callback
7
+ */
8
+ export interface OidcError {
9
+ /**
10
+ * Error code (e.g., "invalid_state", "access_denied", "token_exchange_failed")
11
+ */
12
+ code: string;
13
+
14
+ /**
15
+ * Human-readable error message
16
+ */
17
+ message: string;
18
+
19
+ /**
20
+ * Additional error details for debugging
21
+ */
22
+ details?: any;
23
+ }
24
+
25
+ /**
26
+ * Response from onAuthSuccess callback
27
+ * Must include JWT token generated by the application via jwt-auth-plugin
28
+ */
29
+ export interface AuthSuccessCallbackResponse {
30
+ /**
31
+ * User object from your application
32
+ * Should include _id field for connection storage
33
+ */
34
+ user: any;
35
+
36
+ /**
37
+ * JWT token for your application (not the OIDC tokens!)
38
+ * Generated using ctx.plugins.jwtAuth.createToken()
39
+ */
40
+ token: string;
41
+
42
+ /**
43
+ * Optional redirect URL after authentication
44
+ * Plugin will append #token=... to this URL
45
+ */
46
+ redirectUrl?: string;
47
+ }
48
+
49
+ /**
50
+ * Response from onAuthError callback
51
+ */
52
+ export interface AuthErrorCallbackResponse {
53
+ /**
54
+ * Optional redirect URL for error page
55
+ * e.g., "/login?error=authentication_failed"
56
+ */
57
+ redirectUrl?: string;
58
+ }
59
+
60
+ /**
61
+ * Configuration options for OIDC Plugin with JWT integration
62
+ *
63
+ * The plugin depends on @flink-app/jwt-auth-plugin being installed and configured.
64
+ * The onAuthSuccess callback receives the Flink context as a second parameter,
65
+ * allowing the application to generate JWT tokens using ctx.plugins.jwtAuth.createToken().
66
+ */
67
+ export interface OidcPluginOptions {
68
+ /**
69
+ * OIDC provider configurations
70
+ * Key = provider name (used in URLs: /oidc/{provider}/initiate)
71
+ * Value = provider configuration
72
+ *
73
+ * At least one provider must be configured.
74
+ *
75
+ * Example:
76
+ * {
77
+ * acme: {
78
+ * issuer: "https://idp.acme.com",
79
+ * clientId: "...",
80
+ * clientSecret: "...",
81
+ * callbackUrl: "https://myapp.com/oidc/acme/callback",
82
+ * discoveryUrl: "https://idp.acme.com/.well-known/openid-configuration"
83
+ * },
84
+ * contoso: {
85
+ * issuer: "https://login.contoso.com",
86
+ * clientId: "...",
87
+ * clientSecret: "...",
88
+ * callbackUrl: "https://myapp.com/oidc/contoso/callback"
89
+ * }
90
+ * }
91
+ */
92
+ providers: Record<string, OidcProviderConfig>;
93
+
94
+ /**
95
+ * Whether to store OIDC tokens for future API access
96
+ * If false, tokens are discarded after authentication (auth-only mode)
97
+ * If true, encrypted tokens are stored in MongoDB for later use
98
+ *
99
+ * Default: false
100
+ *
101
+ * Set to true if you need to:
102
+ * - Call IdP APIs on behalf of users
103
+ * - Access user's resources at the IdP
104
+ * - Use refresh tokens to maintain long-term access
105
+ */
106
+ storeTokens?: boolean;
107
+
108
+ /**
109
+ * Callback invoked after successful OIDC authentication
110
+ *
111
+ * Application responsibilities:
112
+ * 1. Find or create user based on OIDC profile (JIT provisioning)
113
+ * 2. Link OIDC provider to user account
114
+ * 3. Generate JWT token using ctx.plugins.jwtAuth.createToken(payload, roles)
115
+ * 4. Return user object, JWT token, and optional redirect URL
116
+ *
117
+ * @param params - OIDC profile, claims, provider name, and tokens (if storeTokens enabled)
118
+ * @param ctx - Flink context with access to repos and plugins (including jwtAuth)
119
+ * @returns User object, JWT token, and optional redirect URL
120
+ *
121
+ * Example:
122
+ * ```typescript
123
+ * onAuthSuccess: async ({ profile, claims, provider }, ctx) => {
124
+ * // Find user by OIDC subject + issuer
125
+ * let user = await ctx.repos.userRepo.getOne({
126
+ * 'oidcConnections.subject': claims.sub,
127
+ * 'oidcConnections.issuer': claims.iss
128
+ * });
129
+ *
130
+ * if (!user) {
131
+ * // JIT provisioning - create new user
132
+ * user = await ctx.repos.userRepo.create({
133
+ * email: claims.email,
134
+ * name: claims.name,
135
+ * oidcConnections: [{
136
+ * issuer: claims.iss,
137
+ * subject: claims.sub,
138
+ * provider
139
+ * }]
140
+ * });
141
+ * }
142
+ *
143
+ * // Generate JWT token
144
+ * const token = await ctx.plugins.jwtAuth.createToken(
145
+ * { userId: user._id, email: user.email },
146
+ * ['user']
147
+ * );
148
+ *
149
+ * return {
150
+ * user,
151
+ * token,
152
+ * redirectUrl: '/dashboard'
153
+ * };
154
+ * }
155
+ * ```
156
+ */
157
+ onAuthSuccess: (
158
+ params: {
159
+ /**
160
+ * Normalized user profile from OIDC claims
161
+ */
162
+ profile: OidcProfile;
163
+
164
+ /**
165
+ * Raw OIDC claims from ID token
166
+ * Contains all standard and custom claims
167
+ */
168
+ claims: Record<string, any>;
169
+
170
+ /**
171
+ * Provider name (e.g., "acme", "contoso")
172
+ */
173
+ provider: string;
174
+
175
+ /**
176
+ * OIDC tokens (only if storeTokens: true)
177
+ * Includes accessToken, idToken, refreshToken
178
+ */
179
+ tokens?: OidcTokenSet;
180
+ },
181
+ ctx: any
182
+ ) => Promise<AuthSuccessCallbackResponse>;
183
+
184
+ /**
185
+ * Callback invoked on OIDC authentication errors
186
+ *
187
+ * Application responsibilities:
188
+ * - Log error for debugging
189
+ * - Optionally provide redirect URL for error page
190
+ *
191
+ * @param params - Error information and provider name
192
+ * @returns Optional redirect URL for error page
193
+ *
194
+ * Example:
195
+ * ```typescript
196
+ * onAuthError: async ({ error, provider }) => {
197
+ * console.error(`OIDC error for ${provider}:`, error);
198
+ *
199
+ * if (error.code === 'access_denied') {
200
+ * return {
201
+ * redirectUrl: '/login?error=user_cancelled'
202
+ * };
203
+ * }
204
+ *
205
+ * return {
206
+ * redirectUrl: '/login?error=authentication_failed'
207
+ * };
208
+ * }
209
+ * ```
210
+ */
211
+ onAuthError?: (params: { error: OidcError; provider: string }) => Promise<AuthErrorCallbackResponse>;
212
+
213
+ /**
214
+ * Dynamic provider loader callback
215
+ *
216
+ * Called when a provider is accessed but not in the static providers config.
217
+ * Allows loading provider configurations from database at runtime.
218
+ *
219
+ * Use this for multi-tenant scenarios where each organization has different IdPs.
220
+ *
221
+ * @param providerName - Name of the provider to load
222
+ * @returns Provider configuration or null if not found
223
+ *
224
+ * Example:
225
+ * ```typescript
226
+ * providerLoader: async (providerName) => {
227
+ * const config = await ctx.repos.oidcProviderRepo.getByName(providerName);
228
+ * if (!config || !config.enabled) {
229
+ * return null;
230
+ * }
231
+ * return {
232
+ * issuer: config.issuer,
233
+ * clientId: config.clientId,
234
+ * clientSecret: decryptSecret(config.clientSecret),
235
+ * callbackUrl: config.callbackUrl,
236
+ * discoveryUrl: config.discoveryUrl,
237
+ * scope: config.scope
238
+ * };
239
+ * }
240
+ * ```
241
+ */
242
+ providerLoader?: (providerName: string) => Promise<OidcProviderConfig | null>;
243
+
244
+ /**
245
+ * Custom collection name for OIDC sessions
246
+ * Default: 'oidc_sessions'
247
+ */
248
+ sessionsCollectionName?: string;
249
+
250
+ /**
251
+ * Custom collection name for OIDC connections (if storeTokens enabled)
252
+ * Default: 'oidc_connections'
253
+ */
254
+ connectionsCollectionName?: string;
255
+
256
+ /**
257
+ * Session TTL in seconds
258
+ * Sessions are automatically cleaned up after this duration
259
+ * Default: 600 (10 minutes)
260
+ *
261
+ * This is the maximum time a user has to complete the OIDC flow
262
+ * (from /initiate to /callback). Increase if users need more time.
263
+ */
264
+ sessionTTL?: number;
265
+
266
+ /**
267
+ * Encryption key for encrypting stored OIDC tokens
268
+ * If not provided, will be derived from first configured provider's client secret
269
+ *
270
+ * Recommended: Use a dedicated encryption key from environment variables
271
+ * Must be at least 32 characters
272
+ *
273
+ * Example: process.env.OIDC_ENCRYPTION_KEY
274
+ */
275
+ encryptionKey?: string;
276
+
277
+ /**
278
+ * Whether to register OIDC routes automatically
279
+ * If false, you must manually handle OIDC flow
280
+ * Default: true
281
+ *
282
+ * Set to false if you want to implement custom route handling
283
+ * or use a custom URL structure.
284
+ */
285
+ registerRoutes?: boolean;
286
+ }