@flink-app/oidc-plugin 0.13.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
|
@@ -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
|
+
}
|