@flink-app/oidc-plugin 2.0.0-alpha.79 → 2.0.0-alpha.81
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 +15 -0
- package/dist/OidcPlugin.d.ts.map +1 -1
- package/dist/OidcPlugin.js +7 -0
- package/dist/handlers/CallbackOidc.d.ts.map +1 -1
- package/dist/handlers/CallbackOidc.js +17 -1
- package/dist/handlers/InitiateOidc.d.ts.map +1 -1
- package/dist/handlers/InitiateOidc.js +5 -0
- package/dist/log.d.ts +27 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +37 -0
- package/dist/providers/OidcProvider.d.ts.map +1 -1
- package/dist/providers/OidcProvider.js +4 -1
- package/dist/providers/ProviderRegistry.d.ts.map +1 -1
- package/dist/providers/ProviderRegistry.js +9 -0
- package/package.json +6 -6
- package/src/OidcPlugin.ts +26 -1
- package/src/handlers/CallbackOidc.ts +29 -1
- package/src/handlers/InitiateOidc.ts +9 -0
- package/src/log.ts +33 -0
- package/src/providers/OidcProvider.ts +10 -1
- package/src/providers/ProviderRegistry.ts +18 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @flink-app/oidc-plugin
|
|
2
2
|
|
|
3
|
+
## 2.0.0-alpha.81
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- @flink-app/flink@2.0.0-alpha.81
|
|
8
|
+
- @flink-app/jwt-auth-plugin@2.0.0-alpha.81
|
|
9
|
+
|
|
10
|
+
## 2.0.0-alpha.80
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Add flink.oidc named logger with debug instrumentation across init, provider resolution, and auth flow
|
|
15
|
+
- @flink-app/flink@2.0.0-alpha.80
|
|
16
|
+
- @flink-app/jwt-auth-plugin@2.0.0-alpha.80
|
|
17
|
+
|
|
3
18
|
## 2.0.0-alpha.79
|
|
4
19
|
|
|
5
20
|
### Patch Changes
|
package/dist/OidcPlugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OidcPlugin.d.ts","sourceRoot":"","sources":["../src/OidcPlugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,WAAW,EAAO,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"OidcPlugin.d.ts","sourceRoot":"","sources":["../src/OidcPlugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,WAAW,EAAO,MAAM,kBAAkB,CAAC;AAG9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAWxD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwEG;AACH,wBAAgB,UAAU,CAAC,IAAI,GAAG,GAAG,EAAE,OAAO,EAAE,iBAAiB,CAAC,IAAI,CAAC,GAAG,WAAW,CA2OpF"}
|
package/dist/OidcPlugin.js
CHANGED
|
@@ -28,6 +28,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
28
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
29
|
exports.oidcPlugin = oidcPlugin;
|
|
30
30
|
const flink_1 = require("@flink-app/flink");
|
|
31
|
+
const log_1 = require("./log");
|
|
31
32
|
const OidcSessionRepo_1 = __importDefault(require("./repos/OidcSessionRepo"));
|
|
32
33
|
const OidcConnectionRepo_1 = __importDefault(require("./repos/OidcConnectionRepo"));
|
|
33
34
|
const encryption_utils_1 = require("./utils/encryption-utils");
|
|
@@ -135,6 +136,7 @@ function oidcPlugin(options) {
|
|
|
135
136
|
`(authorizationEndpoint, tokenEndpoint, jwksUri)`);
|
|
136
137
|
}
|
|
137
138
|
}
|
|
139
|
+
log_1.oidcLog.debug(`Provider "${providerName}" config:`, `issuer=${providerConfig.issuer}`, `clientId=${providerConfig.clientId}`, `clientSecret=${(0, log_1.maskSecret)(providerConfig.clientSecret)}`, `callbackUrl=${providerConfig.callbackUrl}`, providerConfig.discoveryUrl ? `discoveryUrl=${providerConfig.discoveryUrl}` : `authorizationEndpoint=${providerConfig.authorizationEndpoint} tokenEndpoint=${providerConfig.tokenEndpoint}`, `scope=${(providerConfig.scope || ["openid", "email", "profile"]).join(" ")}`, providerConfig.tokenEndpointAuthMethod ? `authMethod=${providerConfig.tokenEndpointAuthMethod}` : "");
|
|
138
140
|
}
|
|
139
141
|
if (!options.onAuthSuccess) {
|
|
140
142
|
throw new Error("OIDC Plugin: onAuthSuccess callback is required");
|
|
@@ -147,8 +149,12 @@ function oidcPlugin(options) {
|
|
|
147
149
|
if (providerWithSecret) {
|
|
148
150
|
encryptionKey = providers[providerWithSecret].clientSecret;
|
|
149
151
|
flink_1.log.warn("OIDC Plugin: No encryption key provided, deriving from client secret. " + "For better security, provide a dedicated encryptionKey in options.");
|
|
152
|
+
log_1.oidcLog.debug(`Encryption key derived from provider "${providerWithSecret}" clientSecret`);
|
|
150
153
|
}
|
|
151
154
|
}
|
|
155
|
+
else {
|
|
156
|
+
log_1.oidcLog.debug(`Encryption key: ${(0, log_1.maskSecret)(encryptionKey)} (explicit)`);
|
|
157
|
+
}
|
|
152
158
|
// Encryption key is required when storing tokens
|
|
153
159
|
if (options.storeTokens) {
|
|
154
160
|
if (!encryptionKey || encryptionKey.length < 32) {
|
|
@@ -181,6 +187,7 @@ function oidcPlugin(options) {
|
|
|
181
187
|
// Initialize repositories
|
|
182
188
|
const sessionsCollectionName = options.sessionsCollectionName || "oidc_sessions";
|
|
183
189
|
const connectionsCollectionName = options.connectionsCollectionName || "oidc_connections";
|
|
190
|
+
log_1.oidcLog.debug(`Init: sessionsCollection=${sessionsCollectionName}`, `connectionsCollection=${connectionsCollectionName}`, `storeTokens=${!!options.storeTokens}`, `sessionTTL=${options.sessionTTL ?? 600}s`, `registerRoutes=${options.registerRoutes !== false}`, configuredProviders.length > 0 ? `staticProviders=[${configuredProviders.join(", ")}]` : "staticProviders=[] (dynamic only)", options.providerLoader ? "providerLoader=yes" : "providerLoader=no");
|
|
184
191
|
sessionRepo = new OidcSessionRepo_1.default(sessionsCollectionName, db);
|
|
185
192
|
connectionRepo = new OidcConnectionRepo_1.default(connectionsCollectionName, db);
|
|
186
193
|
flinkApp.addRepo("oidcSessionRepo", sessionRepo);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CallbackOidc.d.ts","sourceRoot":"","sources":["../../src/handlers/CallbackOidc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,UAAU,EAAc,UAAU,EAAwC,MAAM,kBAAkB,CAAC;AAC5G,OAAO,eAAe,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"CallbackOidc.d.ts","sourceRoot":"","sources":["../../src/handlers/CallbackOidc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,UAAU,EAAc,UAAU,EAAwC,MAAM,kBAAkB,CAAC;AAC5G,OAAO,eAAe,MAAM,4BAA4B,CAAC;AAOzD;;GAEG;AACH,UAAU,UAAU;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,eAAO,MAAM,KAAK,EAAE,UAGnB,CAAC;AAEF;;;;;;GAMG;AACH,QAAA,MAAM,YAAY,EAAE,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,UAAU,EAAE,eAAe,CA8PnE,CAAC;AAEF,eAAe,YAAY,CAAC"}
|
|
@@ -20,6 +20,7 @@ const state_utils_1 = require("../utils/state-utils");
|
|
|
20
20
|
const response_utils_1 = require("../utils/response-utils");
|
|
21
21
|
const encryption_utils_1 = require("../utils/encryption-utils");
|
|
22
22
|
const error_utils_1 = require("../utils/error-utils");
|
|
23
|
+
const log_1 = require("../log");
|
|
23
24
|
/**
|
|
24
25
|
* Route configuration
|
|
25
26
|
* This handler is registered programmatically by the plugin
|
|
@@ -44,8 +45,10 @@ const CallbackOidc = async ({ ctx, req }) => {
|
|
|
44
45
|
// Validate provider and response_type
|
|
45
46
|
(0, error_utils_1.validateProvider)(provider);
|
|
46
47
|
(0, error_utils_1.validateResponseType)(response_type);
|
|
48
|
+
log_1.oidcLog.debug(`Callback: provider="${provider}" hasCode=${!!code} hasState=${!!state} responseType="${response_type || "redirect"}"`);
|
|
47
49
|
// Check for OIDC provider errors (e.g., user denied access)
|
|
48
50
|
if (oidcError) {
|
|
51
|
+
log_1.oidcLog.debug(`Callback: IdP returned error="${oidcError}" description="${error_description || ""}"`);
|
|
49
52
|
const error = (0, error_utils_1.handleProviderError)({ error: oidcError, error_description });
|
|
50
53
|
// Try to retrieve session redirectUri (state may be available even on IdP errors)
|
|
51
54
|
if (state) {
|
|
@@ -83,8 +86,10 @@ const CallbackOidc = async ({ ctx, req }) => {
|
|
|
83
86
|
// Find OIDC session by state
|
|
84
87
|
const session = await ctx.repos.oidcSessionRepo.getByState(state);
|
|
85
88
|
if (!session) {
|
|
89
|
+
log_1.oidcLog.debug(`Callback: no session found for state (may be expired)`);
|
|
86
90
|
throw (0, error_utils_1.createOidcError)(error_utils_1.OidcErrorCodes.SESSION_EXPIRED, "OIDC session not found or expired. Please try logging in again.", { state });
|
|
87
91
|
}
|
|
92
|
+
log_1.oidcLog.debug(`Callback: session found sessionId=${session.sessionId} provider="${session.provider}" redirectUri="${session.redirectUri}"`);
|
|
88
93
|
// Validate state parameter (CSRF protection)
|
|
89
94
|
if (!(0, state_utils_1.validateState)(state, session.state)) {
|
|
90
95
|
throw (0, error_utils_1.createOidcError)(error_utils_1.OidcErrorCodes.INVALID_STATE, "Invalid state parameter. Possible CSRF attack detected.", {
|
|
@@ -95,6 +100,7 @@ const CallbackOidc = async ({ ctx, req }) => {
|
|
|
95
100
|
sessionRedirectUri = session.redirectUri;
|
|
96
101
|
// Delete session immediately after validation (one-time use)
|
|
97
102
|
await ctx.repos.oidcSessionRepo.deleteBySessionId(session.sessionId);
|
|
103
|
+
log_1.oidcLog.debug(`Callback: state validated, session deleted (one-time use)`);
|
|
98
104
|
// Get plugin options
|
|
99
105
|
const { options } = ctx.plugins.oidc;
|
|
100
106
|
// Get provider instance
|
|
@@ -104,15 +110,20 @@ const CallbackOidc = async ({ ctx, req }) => {
|
|
|
104
110
|
}
|
|
105
111
|
const oidcProvider = await providerRegistry.getProvider(provider);
|
|
106
112
|
// Exchange authorization code for tokens with PKCE validation
|
|
113
|
+
log_1.oidcLog.debug(`Callback: exchanging authorization code for tokens`);
|
|
107
114
|
const tokenSet = await oidcProvider.exchangeCodeForToken({
|
|
108
115
|
code,
|
|
109
116
|
codeVerifier: session.codeVerifier,
|
|
110
117
|
state: session.state,
|
|
111
118
|
nonce: session.nonce,
|
|
112
119
|
});
|
|
120
|
+
log_1.oidcLog.debug(`Callback: token exchange successful`, `sub="${tokenSet.claims.sub}"`, `iss="${tokenSet.claims.iss}"`, `email="${tokenSet.claims.email || "(none)"}"`, `hasRefreshToken=${!!tokenSet.refreshToken}`, `expiresIn=${tokenSet.expiresIn ?? "(none)"}s`);
|
|
113
121
|
// Build user profile from ID token and UserInfo
|
|
122
|
+
log_1.oidcLog.debug(`Callback: building user profile`);
|
|
114
123
|
const profile = await oidcProvider.buildProfile(tokenSet, true);
|
|
124
|
+
log_1.oidcLog.debug(`Callback: profile built id="${profile.id}" email="${profile.email || "(none)"}" name="${profile.name || "(none)"}"`);
|
|
115
125
|
// Call onAuthSuccess callback to create/link user and generate JWT token
|
|
126
|
+
log_1.oidcLog.debug(`Callback: calling onAuthSuccess`);
|
|
116
127
|
const authSuccessParams = {
|
|
117
128
|
profile,
|
|
118
129
|
claims: tokenSet.claims,
|
|
@@ -147,6 +158,7 @@ const CallbackOidc = async ({ ctx, req }) => {
|
|
|
147
158
|
}
|
|
148
159
|
return (0, flink_1.internalServerError)("Authentication failed. Please try again.");
|
|
149
160
|
}
|
|
161
|
+
log_1.oidcLog.debug(`Callback: onAuthSuccess completed`);
|
|
150
162
|
// Extract user and JWT token from callback result
|
|
151
163
|
const { user, token, redirectUrl } = authResult;
|
|
152
164
|
if (!token) {
|
|
@@ -170,6 +182,7 @@ const CallbackOidc = async ({ ctx, req }) => {
|
|
|
170
182
|
// Create or update OIDC connection
|
|
171
183
|
const existingConnection = await ctx.repos.oidcConnectionRepo.findByUserAndProvider(user._id, provider);
|
|
172
184
|
if (existingConnection) {
|
|
185
|
+
log_1.oidcLog.debug(`Callback: updating existing connection for userId=${user._id} provider="${provider}"`);
|
|
173
186
|
await ctx.repos.oidcConnectionRepo.updateById(existingConnection._id, {
|
|
174
187
|
accessToken: encryptedAccessToken,
|
|
175
188
|
idToken: encryptedIdToken,
|
|
@@ -180,6 +193,7 @@ const CallbackOidc = async ({ ctx, req }) => {
|
|
|
180
193
|
});
|
|
181
194
|
}
|
|
182
195
|
else {
|
|
196
|
+
log_1.oidcLog.debug(`Callback: creating new connection for userId=${user._id} provider="${provider}" subject="${tokenSet.claims.sub}"`);
|
|
183
197
|
await ctx.repos.oidcConnectionRepo.create({
|
|
184
198
|
userId: user._id,
|
|
185
199
|
provider,
|
|
@@ -197,8 +211,10 @@ const CallbackOidc = async ({ ctx, req }) => {
|
|
|
197
211
|
}
|
|
198
212
|
}
|
|
199
213
|
}
|
|
214
|
+
const finalRedirectUrl = redirectUrl || session.redirectUri;
|
|
215
|
+
log_1.oidcLog.debug(`Callback: auth complete, responding via ${response_type === "json" ? "JSON" : `redirect to "${finalRedirectUrl}"`}`);
|
|
200
216
|
// Return JWT token in requested format
|
|
201
|
-
return (0, response_utils_1.formatTokenResponse)(token, user,
|
|
217
|
+
return (0, response_utils_1.formatTokenResponse)(token, user, finalRedirectUrl, response_type);
|
|
202
218
|
}
|
|
203
219
|
catch (error) {
|
|
204
220
|
flink_1.log.error("OIDC callback error:", error);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InitiateOidc.d.ts","sourceRoot":"","sources":["../../src/handlers/InitiateOidc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,UAAU,EAAc,UAAU,EAAmC,MAAM,kBAAkB,CAAC;AACvG,OAAO,eAAe,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"InitiateOidc.d.ts","sourceRoot":"","sources":["../../src/handlers/InitiateOidc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,UAAU,EAAc,UAAU,EAAmC,MAAM,kBAAkB,CAAC;AACvG,OAAO,eAAe,MAAM,4BAA4B,CAAC;AAMzD;;GAEG;AACH,UAAU,UAAU;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,eAAO,MAAM,KAAK,EAAE,UAGnB,CAAC;AAEF;;;;;GAKG;AACH,QAAA,MAAM,YAAY,EAAE,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,UAAU,EAAE,eAAe,CAyEnE,CAAC;AAEF,eAAe,YAAY,CAAC"}
|
|
@@ -17,6 +17,7 @@ const flink_1 = require("@flink-app/flink");
|
|
|
17
17
|
const state_utils_1 = require("../utils/state-utils");
|
|
18
18
|
const error_utils_1 = require("../utils/error-utils");
|
|
19
19
|
const openid_client_1 = require("openid-client");
|
|
20
|
+
const log_1 = require("../log");
|
|
20
21
|
/**
|
|
21
22
|
* Route configuration
|
|
22
23
|
* This handler is registered programmatically by the plugin
|
|
@@ -37,6 +38,7 @@ const InitiateOidc = async ({ ctx, req }) => {
|
|
|
37
38
|
try {
|
|
38
39
|
// Validate provider name format
|
|
39
40
|
(0, error_utils_1.validateProvider)(provider);
|
|
41
|
+
log_1.oidcLog.debug(`Initiate: provider="${provider}" redirectUri="${redirectUri || "(not set)"}"`);
|
|
40
42
|
// Get provider registry from context
|
|
41
43
|
const providerRegistry = ctx.oidcProviderRegistry;
|
|
42
44
|
if (!providerRegistry) {
|
|
@@ -49,6 +51,7 @@ const InitiateOidc = async ({ ctx, req }) => {
|
|
|
49
51
|
// Determine redirect URI (use provided or default to callback URL)
|
|
50
52
|
const staticProviderConfig = options.providers?.[provider];
|
|
51
53
|
const finalRedirectUri = redirectUri || staticProviderConfig?.callbackUrl || `${req.protocol}://${req.get("host")}/oidc/${provider}/callback`;
|
|
54
|
+
log_1.oidcLog.debug(`Initiate: resolved redirectUri="${finalRedirectUri}" (source: ${redirectUri ? "query param" : staticProviderConfig?.callbackUrl ? "provider config" : "auto-detected"})`);
|
|
52
55
|
// Generate cryptographically secure parameters
|
|
53
56
|
const state = (0, state_utils_1.generateState)();
|
|
54
57
|
const sessionId = (0, state_utils_1.generateSessionId)();
|
|
@@ -64,12 +67,14 @@ const InitiateOidc = async ({ ctx, req }) => {
|
|
|
64
67
|
redirectUri: finalRedirectUri,
|
|
65
68
|
createdAt: new Date(),
|
|
66
69
|
});
|
|
70
|
+
log_1.oidcLog.debug(`Initiate: session created sessionId=${sessionId}`);
|
|
67
71
|
// Build authorization URL with PKCE and nonce
|
|
68
72
|
const authorizationUrl = await oidcProvider.getAuthorizationUrl({
|
|
69
73
|
state,
|
|
70
74
|
codeVerifier,
|
|
71
75
|
nonce,
|
|
72
76
|
});
|
|
77
|
+
log_1.oidcLog.debug(`Initiate: redirecting to IdP authorization URL: ${authorizationUrl}`);
|
|
73
78
|
// Redirect user to provider's authorization page
|
|
74
79
|
return {
|
|
75
80
|
status: 302,
|
package/dist/log.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Named logger for the OIDC plugin.
|
|
3
|
+
*
|
|
4
|
+
* Enables independent log level control via flink.config.js:
|
|
5
|
+
* ```js
|
|
6
|
+
* module.exports = {
|
|
7
|
+
* logging: {
|
|
8
|
+
* components: {
|
|
9
|
+
* "flink.oidc": "debug"
|
|
10
|
+
* }
|
|
11
|
+
* }
|
|
12
|
+
* };
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* Or at runtime:
|
|
16
|
+
* ```ts
|
|
17
|
+
* FlinkLogFactory.setComponentLevel("flink.oidc", "debug");
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare const oidcLog: import("@flink-app/flink").ComponentLogger;
|
|
21
|
+
/**
|
|
22
|
+
* Mask a secret value for safe logging.
|
|
23
|
+
* Shows the first 4 characters followed by **** to help identify which secret
|
|
24
|
+
* is in use without exposing the full value.
|
|
25
|
+
*/
|
|
26
|
+
export declare function maskSecret(value: string | undefined): string;
|
|
27
|
+
//# sourceMappingURL=log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,OAAO,4CAA6C,CAAC;AAElE;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAI5D"}
|
package/dist/log.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.oidcLog = void 0;
|
|
4
|
+
exports.maskSecret = maskSecret;
|
|
5
|
+
const flink_1 = require("@flink-app/flink");
|
|
6
|
+
/**
|
|
7
|
+
* Named logger for the OIDC plugin.
|
|
8
|
+
*
|
|
9
|
+
* Enables independent log level control via flink.config.js:
|
|
10
|
+
* ```js
|
|
11
|
+
* module.exports = {
|
|
12
|
+
* logging: {
|
|
13
|
+
* components: {
|
|
14
|
+
* "flink.oidc": "debug"
|
|
15
|
+
* }
|
|
16
|
+
* }
|
|
17
|
+
* };
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Or at runtime:
|
|
21
|
+
* ```ts
|
|
22
|
+
* FlinkLogFactory.setComponentLevel("flink.oidc", "debug");
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
exports.oidcLog = flink_1.FlinkLogFactory.createLogger("flink.oidc");
|
|
26
|
+
/**
|
|
27
|
+
* Mask a secret value for safe logging.
|
|
28
|
+
* Shows the first 4 characters followed by **** to help identify which secret
|
|
29
|
+
* is in use without exposing the full value.
|
|
30
|
+
*/
|
|
31
|
+
function maskSecret(value) {
|
|
32
|
+
if (!value)
|
|
33
|
+
return "(not set)";
|
|
34
|
+
if (value.length <= 4)
|
|
35
|
+
return "****";
|
|
36
|
+
return `${value.slice(0, 4)}****`;
|
|
37
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OidcProvider.d.ts","sourceRoot":"","sources":["../../src/providers/OidcProvider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwD,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACvG,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,WAAW,MAAM,wBAAwB,CAAC;AACjD,OAAO,YAAY,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"OidcProvider.d.ts","sourceRoot":"","sources":["../../src/providers/OidcProvider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwD,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACvG,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,WAAW,MAAM,wBAAwB,CAAC;AACjD,OAAO,YAAY,MAAM,yBAAyB,CAAC;AAKnD;;;;;;;;;;GAUG;AACH,qBAAa,YAAY;IACrB,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,WAAW,CAAkB;gBAEzB,MAAM,EAAE,kBAAkB;IAItC;;;;;;;OAOG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAkEjC;;;;;OAKG;IACG,mBAAmB,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAiB1G;;;;;;;OAOG;IACG,oBAAoB,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,YAAY,CAAC;IAqC/H;;;;;;;;OAQG;IACG,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAajE;;;;;;;;;OASG;IACG,YAAY,CAAC,QAAQ,EAAE,YAAY,EAAE,eAAe,GAAE,OAAc,GAAG,OAAO,CAAC,WAAW,CAAC;IA2BjG;;;;OAIG;YACW,iBAAiB;IAU/B;;;;OAIG;IACH,iBAAiB,IAAI,GAAG;CAM3B"}
|
|
@@ -4,6 +4,7 @@ exports.OidcProvider = void 0;
|
|
|
4
4
|
const openid_client_1 = require("openid-client");
|
|
5
5
|
const claims_mapper_1 = require("../utils/claims-mapper");
|
|
6
6
|
const error_utils_1 = require("../utils/error-utils");
|
|
7
|
+
const log_1 = require("../log");
|
|
7
8
|
/**
|
|
8
9
|
* Generic OIDC Provider implementation using openid-client
|
|
9
10
|
*
|
|
@@ -37,7 +38,9 @@ class OidcProvider {
|
|
|
37
38
|
try {
|
|
38
39
|
// Option 1: OIDC Discovery
|
|
39
40
|
if (this.config.discoveryUrl) {
|
|
41
|
+
log_1.oidcLog.debug(`Provider "${this.config.issuer}": discovering from ${this.config.discoveryUrl}`);
|
|
40
42
|
this.issuer = await openid_client_1.Issuer.discover(this.config.discoveryUrl);
|
|
43
|
+
log_1.oidcLog.debug(`Provider "${this.config.issuer}": discovery complete`, `authorization_endpoint=${this.issuer.metadata.authorization_endpoint}`, `token_endpoint=${this.issuer.metadata.token_endpoint}`, `userinfo_endpoint=${this.issuer.metadata.userinfo_endpoint ?? "(none)"}`, `jwks_uri=${this.issuer.metadata.jwks_uri}`);
|
|
41
44
|
}
|
|
42
45
|
// Option 2: Manual configuration
|
|
43
46
|
else {
|
|
@@ -178,7 +181,7 @@ class OidcProvider {
|
|
|
178
181
|
}
|
|
179
182
|
catch (error) {
|
|
180
183
|
// UserInfo is optional - continue with ID token claims only
|
|
181
|
-
|
|
184
|
+
log_1.oidcLog.warn(`Failed to fetch UserInfo from ${this.config.userinfoEndpoint}, using ID token claims only:`, error);
|
|
182
185
|
}
|
|
183
186
|
}
|
|
184
187
|
// Apply custom claim mapping if configured
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProviderRegistry.d.ts","sourceRoot":"","sources":["../../src/providers/ProviderRegistry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"ProviderRegistry.d.ts","sourceRoot":"","sources":["../../src/providers/ProviderRegistry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAI3D;;;;;;;;GAQG;AACH,qBAAa,gBAAgB;IACzB,OAAO,CAAC,eAAe,CAAqC;IAC5D,OAAO,CAAC,iBAAiB,CAAwC;IACjE,OAAO,CAAC,cAAc,CAAC,CAAyE;IAChG,OAAO,CAAC,GAAG,CAAM;gBAGb,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,EACnD,cAAc,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,EACvF,GAAG,CAAC,EAAE,GAAG;IAOb;;;;;;;;;;;;;OAaG;IACG,WAAW,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAiD9D;;;;;OAKG;IACH,WAAW,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO;IAI1C;;;;;;;OAOG;IACH,UAAU,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI;IAQvC;;;;OAIG;IACH,gBAAgB,IAAI,MAAM,EAAE;CAG/B"}
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ProviderRegistry = void 0;
|
|
4
4
|
const OidcProvider_1 = require("./OidcProvider");
|
|
5
5
|
const error_utils_1 = require("../utils/error-utils");
|
|
6
|
+
const log_1 = require("../log");
|
|
6
7
|
/**
|
|
7
8
|
* Provider registry for managing OIDC provider instances
|
|
8
9
|
*
|
|
@@ -37,23 +38,31 @@ class ProviderRegistry {
|
|
|
37
38
|
// Check cache first
|
|
38
39
|
const cachedProvider = this.providerInstances.get(providerName);
|
|
39
40
|
if (cachedProvider) {
|
|
41
|
+
log_1.oidcLog.debug(`Provider "${providerName}": using cached instance`);
|
|
40
42
|
return cachedProvider;
|
|
41
43
|
}
|
|
42
44
|
// Try static configuration
|
|
43
45
|
let config = this.staticProviders[providerName] || null;
|
|
46
|
+
let configSource = "static";
|
|
44
47
|
// Try dynamic loader if not in static config
|
|
45
48
|
if (!config && this.providerLoader) {
|
|
49
|
+
log_1.oidcLog.debug(`Provider "${providerName}": not in static config, invoking providerLoader`);
|
|
46
50
|
config = await this.providerLoader(providerName, this.ctx);
|
|
51
|
+
configSource = "dynamic";
|
|
47
52
|
}
|
|
48
53
|
if (!config) {
|
|
54
|
+
log_1.oidcLog.debug(`Provider "${providerName}": not found (static=[${Object.keys(this.staticProviders).join(", ")}], loader=${!!this.providerLoader})`);
|
|
49
55
|
throw (0, error_utils_1.createOidcError)(error_utils_1.OidcErrorCodes.PROVIDER_NOT_CONFIGURED, `OIDC provider '${providerName}' is not configured`, {
|
|
50
56
|
providerName,
|
|
51
57
|
availableProviders: Object.keys(this.staticProviders),
|
|
52
58
|
});
|
|
53
59
|
}
|
|
60
|
+
log_1.oidcLog.debug(`Provider "${providerName}" resolved via ${configSource}:`, `issuer=${config.issuer}`, `clientId=${config.clientId}`, `clientSecret=${(0, log_1.maskSecret)(config.clientSecret)}`, `callbackUrl=${config.callbackUrl}`, config.discoveryUrl ? `discoveryUrl=${config.discoveryUrl}` : `authorizationEndpoint=${config.authorizationEndpoint}`, `scope=${(config.scope || ["openid", "email", "profile"]).join(" ")}`);
|
|
54
61
|
// Create and initialize provider
|
|
62
|
+
log_1.oidcLog.debug(`Provider "${providerName}": initializing OIDC client`);
|
|
55
63
|
const provider = new OidcProvider_1.OidcProvider(config);
|
|
56
64
|
await provider.initialize();
|
|
65
|
+
log_1.oidcLog.debug(`Provider "${providerName}": initialized successfully`);
|
|
57
66
|
// Cache the instance
|
|
58
67
|
this.providerInstances.set(providerName, provider);
|
|
59
68
|
return provider;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flink-app/oidc-plugin",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.81",
|
|
4
4
|
"description": "Flink plugin for OIDC authentication with generic IdP support",
|
|
5
5
|
"author": "joel@frost.se",
|
|
6
6
|
"license": "MIT",
|
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"openid-client": "^5.7.0",
|
|
14
|
-
"@flink-app/jwt-auth-plugin": "2.0.0-alpha.
|
|
14
|
+
"@flink-app/jwt-auth-plugin": "2.0.0-alpha.81"
|
|
15
15
|
},
|
|
16
16
|
"peerDependencies": {
|
|
17
|
-
"@flink-app/flink": ">=2.0.0-alpha.
|
|
17
|
+
"@flink-app/flink": ">=2.0.0-alpha.81",
|
|
18
18
|
"mongodb": "^6.15.0"
|
|
19
19
|
},
|
|
20
20
|
"peerDependenciesMeta": {
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"@types/node": "22.13.10",
|
|
28
28
|
"ts-node": "^10.9.2",
|
|
29
29
|
"tsc-watch": "^4.2.9",
|
|
30
|
-
"@flink-app/
|
|
31
|
-
"@flink-app/
|
|
32
|
-
"@flink-app/
|
|
30
|
+
"@flink-app/flink": "2.0.0-alpha.81",
|
|
31
|
+
"@flink-app/jwt-auth-plugin": "2.0.0-alpha.81",
|
|
32
|
+
"@flink-app/test-utils": "2.0.0-alpha.81"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
35
|
"test": "jasmine-ts --config=./spec/support/jasmine.json",
|
package/src/OidcPlugin.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { FlinkApp, FlinkPlugin, log } from "@flink-app/flink";
|
|
2
|
+
import { oidcLog, maskSecret } from "./log";
|
|
2
3
|
import { Db } from "mongodb";
|
|
3
4
|
import { OidcPluginOptions } from "./OidcPluginOptions";
|
|
4
5
|
import { OidcPluginContext } from "./OidcPluginContext";
|
|
5
6
|
import { OidcInternalContext } from "./OidcInternalContext";
|
|
6
7
|
import OidcSessionRepo from "./repos/OidcSessionRepo";
|
|
7
8
|
import OidcConnectionRepo from "./repos/OidcConnectionRepo";
|
|
8
|
-
import {
|
|
9
|
+
import { decryptToken, validateEncryptionSecret } from "./utils/encryption-utils";
|
|
9
10
|
import OidcConnection from "./schemas/OidcConnection";
|
|
10
11
|
import { ProviderRegistry } from "./providers/ProviderRegistry";
|
|
11
12
|
import * as InitiateOidc from "./handlers/InitiateOidc";
|
|
@@ -117,6 +118,17 @@ export function oidcPlugin<TCtx = any>(options: OidcPluginOptions<TCtx>): FlinkP
|
|
|
117
118
|
);
|
|
118
119
|
}
|
|
119
120
|
}
|
|
121
|
+
|
|
122
|
+
oidcLog.debug(
|
|
123
|
+
`Provider "${providerName}" config:`,
|
|
124
|
+
`issuer=${providerConfig.issuer}`,
|
|
125
|
+
`clientId=${providerConfig.clientId}`,
|
|
126
|
+
`clientSecret=${maskSecret(providerConfig.clientSecret)}`,
|
|
127
|
+
`callbackUrl=${providerConfig.callbackUrl}`,
|
|
128
|
+
providerConfig.discoveryUrl ? `discoveryUrl=${providerConfig.discoveryUrl}` : `authorizationEndpoint=${providerConfig.authorizationEndpoint} tokenEndpoint=${providerConfig.tokenEndpoint}`,
|
|
129
|
+
`scope=${(providerConfig.scope || ["openid", "email", "profile"]).join(" ")}`,
|
|
130
|
+
providerConfig.tokenEndpointAuthMethod ? `authMethod=${providerConfig.tokenEndpointAuthMethod}` : ""
|
|
131
|
+
);
|
|
120
132
|
}
|
|
121
133
|
|
|
122
134
|
if (!options.onAuthSuccess) {
|
|
@@ -133,7 +145,10 @@ export function oidcPlugin<TCtx = any>(options: OidcPluginOptions<TCtx>): FlinkP
|
|
|
133
145
|
log.warn(
|
|
134
146
|
"OIDC Plugin: No encryption key provided, deriving from client secret. " + "For better security, provide a dedicated encryptionKey in options."
|
|
135
147
|
);
|
|
148
|
+
oidcLog.debug(`Encryption key derived from provider "${providerWithSecret}" clientSecret`);
|
|
136
149
|
}
|
|
150
|
+
} else {
|
|
151
|
+
oidcLog.debug(`Encryption key: ${maskSecret(encryptionKey)} (explicit)`);
|
|
137
152
|
}
|
|
138
153
|
|
|
139
154
|
// Encryption key is required when storing tokens
|
|
@@ -175,6 +190,16 @@ export function oidcPlugin<TCtx = any>(options: OidcPluginOptions<TCtx>): FlinkP
|
|
|
175
190
|
const sessionsCollectionName = options.sessionsCollectionName || "oidc_sessions";
|
|
176
191
|
const connectionsCollectionName = options.connectionsCollectionName || "oidc_connections";
|
|
177
192
|
|
|
193
|
+
oidcLog.debug(
|
|
194
|
+
`Init: sessionsCollection=${sessionsCollectionName}`,
|
|
195
|
+
`connectionsCollection=${connectionsCollectionName}`,
|
|
196
|
+
`storeTokens=${!!options.storeTokens}`,
|
|
197
|
+
`sessionTTL=${options.sessionTTL ?? 600}s`,
|
|
198
|
+
`registerRoutes=${options.registerRoutes !== false}`,
|
|
199
|
+
configuredProviders.length > 0 ? `staticProviders=[${configuredProviders.join(", ")}]` : "staticProviders=[] (dynamic only)",
|
|
200
|
+
options.providerLoader ? "providerLoader=yes" : "providerLoader=no"
|
|
201
|
+
);
|
|
202
|
+
|
|
178
203
|
sessionRepo = new OidcSessionRepo(sessionsCollectionName, db);
|
|
179
204
|
connectionRepo = new OidcConnectionRepo(connectionsCollectionName, db);
|
|
180
205
|
|
|
@@ -19,6 +19,7 @@ import { validateState } from "../utils/state-utils";
|
|
|
19
19
|
import { formatTokenResponse } from "../utils/response-utils";
|
|
20
20
|
import { encryptToken } from "../utils/encryption-utils";
|
|
21
21
|
import { validateProvider, validateResponseType, createOidcError, OidcErrorCodes, handleProviderError } from "../utils/error-utils";
|
|
22
|
+
import { oidcLog } from "../log";
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Path parameters for the handler
|
|
@@ -56,8 +57,11 @@ const CallbackOidc: GetHandler<any, any, PathParams, CallbackRequest> = async ({
|
|
|
56
57
|
validateProvider(provider);
|
|
57
58
|
validateResponseType(response_type);
|
|
58
59
|
|
|
60
|
+
oidcLog.debug(`Callback: provider="${provider}" hasCode=${!!code} hasState=${!!state} responseType="${response_type || "redirect"}"`);
|
|
61
|
+
|
|
59
62
|
// Check for OIDC provider errors (e.g., user denied access)
|
|
60
63
|
if (oidcError) {
|
|
64
|
+
oidcLog.debug(`Callback: IdP returned error="${oidcError}" description="${error_description || ""}"`);
|
|
61
65
|
const error = handleProviderError({ error: oidcError, error_description });
|
|
62
66
|
|
|
63
67
|
// Try to retrieve session redirectUri (state may be available even on IdP errors)
|
|
@@ -102,9 +106,12 @@ const CallbackOidc: GetHandler<any, any, PathParams, CallbackRequest> = async ({
|
|
|
102
106
|
const session = await ctx.repos.oidcSessionRepo.getByState(state);
|
|
103
107
|
|
|
104
108
|
if (!session) {
|
|
109
|
+
oidcLog.debug(`Callback: no session found for state (may be expired)`);
|
|
105
110
|
throw createOidcError(OidcErrorCodes.SESSION_EXPIRED, "OIDC session not found or expired. Please try logging in again.", { state });
|
|
106
111
|
}
|
|
107
112
|
|
|
113
|
+
oidcLog.debug(`Callback: session found sessionId=${session.sessionId} provider="${session.provider}" redirectUri="${session.redirectUri}"`);
|
|
114
|
+
|
|
108
115
|
// Validate state parameter (CSRF protection)
|
|
109
116
|
if (!validateState(state, session.state)) {
|
|
110
117
|
throw createOidcError(OidcErrorCodes.INVALID_STATE, "Invalid state parameter. Possible CSRF attack detected.", {
|
|
@@ -117,6 +124,7 @@ const CallbackOidc: GetHandler<any, any, PathParams, CallbackRequest> = async ({
|
|
|
117
124
|
|
|
118
125
|
// Delete session immediately after validation (one-time use)
|
|
119
126
|
await ctx.repos.oidcSessionRepo.deleteBySessionId(session.sessionId);
|
|
127
|
+
oidcLog.debug(`Callback: state validated, session deleted (one-time use)`);
|
|
120
128
|
|
|
121
129
|
// Get plugin options
|
|
122
130
|
const { options } = ctx.plugins.oidc;
|
|
@@ -130,6 +138,7 @@ const CallbackOidc: GetHandler<any, any, PathParams, CallbackRequest> = async ({
|
|
|
130
138
|
const oidcProvider = await providerRegistry.getProvider(provider);
|
|
131
139
|
|
|
132
140
|
// Exchange authorization code for tokens with PKCE validation
|
|
141
|
+
oidcLog.debug(`Callback: exchanging authorization code for tokens`);
|
|
133
142
|
const tokenSet = await oidcProvider.exchangeCodeForToken({
|
|
134
143
|
code,
|
|
135
144
|
codeVerifier: session.codeVerifier,
|
|
@@ -137,10 +146,22 @@ const CallbackOidc: GetHandler<any, any, PathParams, CallbackRequest> = async ({
|
|
|
137
146
|
nonce: session.nonce,
|
|
138
147
|
});
|
|
139
148
|
|
|
149
|
+
oidcLog.debug(
|
|
150
|
+
`Callback: token exchange successful`,
|
|
151
|
+
`sub="${tokenSet.claims.sub}"`,
|
|
152
|
+
`iss="${tokenSet.claims.iss}"`,
|
|
153
|
+
`email="${tokenSet.claims.email || "(none)"}"`,
|
|
154
|
+
`hasRefreshToken=${!!tokenSet.refreshToken}`,
|
|
155
|
+
`expiresIn=${tokenSet.expiresIn ?? "(none)"}s`
|
|
156
|
+
);
|
|
157
|
+
|
|
140
158
|
// Build user profile from ID token and UserInfo
|
|
159
|
+
oidcLog.debug(`Callback: building user profile`);
|
|
141
160
|
const profile = await oidcProvider.buildProfile(tokenSet, true);
|
|
161
|
+
oidcLog.debug(`Callback: profile built id="${profile.id}" email="${profile.email || "(none)"}" name="${profile.name || "(none)"}"`);
|
|
142
162
|
|
|
143
163
|
// Call onAuthSuccess callback to create/link user and generate JWT token
|
|
164
|
+
oidcLog.debug(`Callback: calling onAuthSuccess`);
|
|
144
165
|
const authSuccessParams = {
|
|
145
166
|
profile,
|
|
146
167
|
claims: tokenSet.claims,
|
|
@@ -180,6 +201,8 @@ const CallbackOidc: GetHandler<any, any, PathParams, CallbackRequest> = async ({
|
|
|
180
201
|
return internalServerError("Authentication failed. Please try again.");
|
|
181
202
|
}
|
|
182
203
|
|
|
204
|
+
oidcLog.debug(`Callback: onAuthSuccess completed`);
|
|
205
|
+
|
|
183
206
|
// Extract user and JWT token from callback result
|
|
184
207
|
const { user, token, redirectUrl } = authResult;
|
|
185
208
|
|
|
@@ -208,6 +231,7 @@ const CallbackOidc: GetHandler<any, any, PathParams, CallbackRequest> = async ({
|
|
|
208
231
|
const existingConnection = await ctx.repos.oidcConnectionRepo.findByUserAndProvider(user._id, provider);
|
|
209
232
|
|
|
210
233
|
if (existingConnection) {
|
|
234
|
+
oidcLog.debug(`Callback: updating existing connection for userId=${user._id} provider="${provider}"`);
|
|
211
235
|
await ctx.repos.oidcConnectionRepo.updateById(existingConnection._id!, {
|
|
212
236
|
accessToken: encryptedAccessToken,
|
|
213
237
|
idToken: encryptedIdToken,
|
|
@@ -217,6 +241,7 @@ const CallbackOidc: GetHandler<any, any, PathParams, CallbackRequest> = async ({
|
|
|
217
241
|
updatedAt: new Date(),
|
|
218
242
|
});
|
|
219
243
|
} else {
|
|
244
|
+
oidcLog.debug(`Callback: creating new connection for userId=${user._id} provider="${provider}" subject="${tokenSet.claims.sub}"`);
|
|
220
245
|
await ctx.repos.oidcConnectionRepo.create({
|
|
221
246
|
userId: user._id,
|
|
222
247
|
provider,
|
|
@@ -235,8 +260,11 @@ const CallbackOidc: GetHandler<any, any, PathParams, CallbackRequest> = async ({
|
|
|
235
260
|
}
|
|
236
261
|
}
|
|
237
262
|
|
|
263
|
+
const finalRedirectUrl = redirectUrl || session.redirectUri;
|
|
264
|
+
oidcLog.debug(`Callback: auth complete, responding via ${response_type === "json" ? "JSON" : `redirect to "${finalRedirectUrl}"`}`);
|
|
265
|
+
|
|
238
266
|
// Return JWT token in requested format
|
|
239
|
-
return formatTokenResponse(token, user,
|
|
267
|
+
return formatTokenResponse(token, user, finalRedirectUrl, response_type);
|
|
240
268
|
} catch (error: any) {
|
|
241
269
|
log.error("OIDC callback error:", error);
|
|
242
270
|
|
|
@@ -16,6 +16,7 @@ import InitiateRequest from "../schemas/InitiateRequest";
|
|
|
16
16
|
import { generateState, generateSessionId, generateNonce } from "../utils/state-utils";
|
|
17
17
|
import { validateProvider, createOidcError, OidcErrorCodes } from "../utils/error-utils";
|
|
18
18
|
import { generators } from "openid-client";
|
|
19
|
+
import { oidcLog } from "../log";
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Path parameters for the handler
|
|
@@ -48,6 +49,8 @@ const InitiateOidc: GetHandler<any, any, PathParams, InitiateRequest> = async ({
|
|
|
48
49
|
// Validate provider name format
|
|
49
50
|
validateProvider(provider);
|
|
50
51
|
|
|
52
|
+
oidcLog.debug(`Initiate: provider="${provider}" redirectUri="${redirectUri || "(not set)"}"`);
|
|
53
|
+
|
|
51
54
|
// Get provider registry from context
|
|
52
55
|
const providerRegistry = (ctx as any).oidcProviderRegistry;
|
|
53
56
|
if (!providerRegistry) {
|
|
@@ -64,6 +67,8 @@ const InitiateOidc: GetHandler<any, any, PathParams, InitiateRequest> = async ({
|
|
|
64
67
|
const staticProviderConfig = options.providers?.[provider];
|
|
65
68
|
const finalRedirectUri = redirectUri || staticProviderConfig?.callbackUrl || `${req.protocol}://${req.get("host")}/oidc/${provider}/callback`;
|
|
66
69
|
|
|
70
|
+
oidcLog.debug(`Initiate: resolved redirectUri="${finalRedirectUri}" (source: ${redirectUri ? "query param" : staticProviderConfig?.callbackUrl ? "provider config" : "auto-detected"})`);
|
|
71
|
+
|
|
67
72
|
// Generate cryptographically secure parameters
|
|
68
73
|
const state = generateState();
|
|
69
74
|
const sessionId = generateSessionId();
|
|
@@ -81,6 +86,8 @@ const InitiateOidc: GetHandler<any, any, PathParams, InitiateRequest> = async ({
|
|
|
81
86
|
createdAt: new Date(),
|
|
82
87
|
});
|
|
83
88
|
|
|
89
|
+
oidcLog.debug(`Initiate: session created sessionId=${sessionId}`);
|
|
90
|
+
|
|
84
91
|
// Build authorization URL with PKCE and nonce
|
|
85
92
|
const authorizationUrl = await oidcProvider.getAuthorizationUrl({
|
|
86
93
|
state,
|
|
@@ -88,6 +95,8 @@ const InitiateOidc: GetHandler<any, any, PathParams, InitiateRequest> = async ({
|
|
|
88
95
|
nonce,
|
|
89
96
|
});
|
|
90
97
|
|
|
98
|
+
oidcLog.debug(`Initiate: redirecting to IdP authorization URL: ${authorizationUrl}`);
|
|
99
|
+
|
|
91
100
|
// Redirect user to provider's authorization page
|
|
92
101
|
return {
|
|
93
102
|
status: 302,
|
package/src/log.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { FlinkLogFactory } from "@flink-app/flink";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Named logger for the OIDC plugin.
|
|
5
|
+
*
|
|
6
|
+
* Enables independent log level control via flink.config.js:
|
|
7
|
+
* ```js
|
|
8
|
+
* module.exports = {
|
|
9
|
+
* logging: {
|
|
10
|
+
* components: {
|
|
11
|
+
* "flink.oidc": "debug"
|
|
12
|
+
* }
|
|
13
|
+
* }
|
|
14
|
+
* };
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* Or at runtime:
|
|
18
|
+
* ```ts
|
|
19
|
+
* FlinkLogFactory.setComponentLevel("flink.oidc", "debug");
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export const oidcLog = FlinkLogFactory.createLogger("flink.oidc");
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Mask a secret value for safe logging.
|
|
26
|
+
* Shows the first 4 characters followed by **** to help identify which secret
|
|
27
|
+
* is in use without exposing the full value.
|
|
28
|
+
*/
|
|
29
|
+
export function maskSecret(value: string | undefined): string {
|
|
30
|
+
if (!value) return "(not set)";
|
|
31
|
+
if (value.length <= 4) return "****";
|
|
32
|
+
return `${value.slice(0, 4)}****`;
|
|
33
|
+
}
|
|
@@ -4,6 +4,7 @@ import OidcProfile from "../schemas/OidcProfile";
|
|
|
4
4
|
import OidcTokenSet from "../schemas/OidcTokenSet";
|
|
5
5
|
import { mapClaimsToProfile, extractCustomClaims } from "../utils/claims-mapper";
|
|
6
6
|
import { createOidcError, OidcErrorCodes } from "../utils/error-utils";
|
|
7
|
+
import { oidcLog } from "../log";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Generic OIDC Provider implementation using openid-client
|
|
@@ -42,7 +43,15 @@ export class OidcProvider {
|
|
|
42
43
|
try {
|
|
43
44
|
// Option 1: OIDC Discovery
|
|
44
45
|
if (this.config.discoveryUrl) {
|
|
46
|
+
oidcLog.debug(`Provider "${this.config.issuer}": discovering from ${this.config.discoveryUrl}`);
|
|
45
47
|
this.issuer = await Issuer.discover(this.config.discoveryUrl);
|
|
48
|
+
oidcLog.debug(
|
|
49
|
+
`Provider "${this.config.issuer}": discovery complete`,
|
|
50
|
+
`authorization_endpoint=${this.issuer.metadata.authorization_endpoint}`,
|
|
51
|
+
`token_endpoint=${this.issuer.metadata.token_endpoint}`,
|
|
52
|
+
`userinfo_endpoint=${this.issuer.metadata.userinfo_endpoint ?? "(none)"}`,
|
|
53
|
+
`jwks_uri=${this.issuer.metadata.jwks_uri}`
|
|
54
|
+
);
|
|
46
55
|
}
|
|
47
56
|
// Option 2: Manual configuration
|
|
48
57
|
else {
|
|
@@ -203,7 +212,7 @@ export class OidcProvider {
|
|
|
203
212
|
claims = { ...claims, ...userinfo };
|
|
204
213
|
} catch (error) {
|
|
205
214
|
// UserInfo is optional - continue with ID token claims only
|
|
206
|
-
|
|
215
|
+
oidcLog.warn(`Failed to fetch UserInfo from ${this.config.userinfoEndpoint}, using ID token claims only:`, error);
|
|
207
216
|
}
|
|
208
217
|
}
|
|
209
218
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { OidcProvider } from "./OidcProvider";
|
|
2
2
|
import { OidcProviderConfig } from "../OidcProviderConfig";
|
|
3
3
|
import { createOidcError, OidcErrorCodes } from "../utils/error-utils";
|
|
4
|
+
import { oidcLog, maskSecret } from "../log";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Provider registry for managing OIDC provider instances
|
|
@@ -45,27 +46,44 @@ export class ProviderRegistry {
|
|
|
45
46
|
// Check cache first
|
|
46
47
|
const cachedProvider = this.providerInstances.get(providerName);
|
|
47
48
|
if (cachedProvider) {
|
|
49
|
+
oidcLog.debug(`Provider "${providerName}": using cached instance`);
|
|
48
50
|
return cachedProvider;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
// Try static configuration
|
|
52
54
|
let config: OidcProviderConfig | null = this.staticProviders[providerName] || null;
|
|
55
|
+
let configSource = "static";
|
|
53
56
|
|
|
54
57
|
// Try dynamic loader if not in static config
|
|
55
58
|
if (!config && this.providerLoader) {
|
|
59
|
+
oidcLog.debug(`Provider "${providerName}": not in static config, invoking providerLoader`);
|
|
56
60
|
config = await this.providerLoader(providerName, this.ctx);
|
|
61
|
+
configSource = "dynamic";
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
if (!config) {
|
|
65
|
+
oidcLog.debug(`Provider "${providerName}": not found (static=[${Object.keys(this.staticProviders).join(", ")}], loader=${!!this.providerLoader})`);
|
|
60
66
|
throw createOidcError(OidcErrorCodes.PROVIDER_NOT_CONFIGURED, `OIDC provider '${providerName}' is not configured`, {
|
|
61
67
|
providerName,
|
|
62
68
|
availableProviders: Object.keys(this.staticProviders),
|
|
63
69
|
});
|
|
64
70
|
}
|
|
65
71
|
|
|
72
|
+
oidcLog.debug(
|
|
73
|
+
`Provider "${providerName}" resolved via ${configSource}:`,
|
|
74
|
+
`issuer=${config.issuer}`,
|
|
75
|
+
`clientId=${config.clientId}`,
|
|
76
|
+
`clientSecret=${maskSecret(config.clientSecret)}`,
|
|
77
|
+
`callbackUrl=${config.callbackUrl}`,
|
|
78
|
+
config.discoveryUrl ? `discoveryUrl=${config.discoveryUrl}` : `authorizationEndpoint=${config.authorizationEndpoint}`,
|
|
79
|
+
`scope=${(config.scope || ["openid", "email", "profile"]).join(" ")}`
|
|
80
|
+
);
|
|
81
|
+
|
|
66
82
|
// Create and initialize provider
|
|
83
|
+
oidcLog.debug(`Provider "${providerName}": initializing OIDC client`);
|
|
67
84
|
const provider = new OidcProvider(config);
|
|
68
85
|
await provider.initialize();
|
|
86
|
+
oidcLog.debug(`Provider "${providerName}": initialized successfully`);
|
|
69
87
|
|
|
70
88
|
// Cache the instance
|
|
71
89
|
this.providerInstances.set(providerName, provider);
|