@flink-app/oidc-plugin 2.0.0-alpha.78 → 2.0.0-alpha.80
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 +16 -0
- package/dist/OidcPlugin.d.ts.map +1 -1
- package/dist/OidcPlugin.js +7 -0
- package/dist/OidcPluginOptions.d.ts +16 -0
- package/dist/OidcPluginOptions.d.ts.map +1 -1
- package/dist/handlers/CallbackOidc.d.ts.map +1 -1
- package/dist/handlers/CallbackOidc.js +31 -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/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/OidcPluginOptions.ts +21 -1
- package/src/handlers/CallbackOidc.ts +46 -1
- package/src/handlers/InitiateOidc.ts +9 -0
- package/src/log.ts +33 -0
- package/src/providers/ProviderRegistry.ts +18 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @flink-app/oidc-plugin
|
|
2
2
|
|
|
3
|
+
## 2.0.0-alpha.80
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Add flink.oidc named logger with debug instrumentation across init, provider resolution, and auth flow
|
|
8
|
+
- @flink-app/flink@2.0.0-alpha.80
|
|
9
|
+
- @flink-app/jwt-auth-plugin@2.0.0-alpha.80
|
|
10
|
+
|
|
11
|
+
## 2.0.0-alpha.79
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Expose redirectUri from session in onAuthSuccess and onAuthError callbacks, enabling multi-client redirect decisions
|
|
16
|
+
- @flink-app/flink@2.0.0-alpha.79
|
|
17
|
+
- @flink-app/jwt-auth-plugin@2.0.0-alpha.79
|
|
18
|
+
|
|
3
19
|
## 2.0.0-alpha.78
|
|
4
20
|
|
|
5
21
|
### 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);
|
|
@@ -158,6 +158,16 @@ export interface OidcPluginOptions<TCtx = any> {
|
|
|
158
158
|
* Provider name (e.g., "acme", "contoso")
|
|
159
159
|
*/
|
|
160
160
|
provider: string;
|
|
161
|
+
/**
|
|
162
|
+
* The redirectUri passed on the initiate request (stored in session)
|
|
163
|
+
* Use this to redirect back to the originating client application.
|
|
164
|
+
*
|
|
165
|
+
* Example:
|
|
166
|
+
* ```typescript
|
|
167
|
+
* return { user, token, redirectUrl: redirectUri || DEFAULT_REDIRECT_URL };
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
redirectUri: string;
|
|
161
171
|
/**
|
|
162
172
|
* OIDC tokens (only if storeTokens: true)
|
|
163
173
|
* Includes accessToken, idToken, refreshToken
|
|
@@ -194,6 +204,12 @@ export interface OidcPluginOptions<TCtx = any> {
|
|
|
194
204
|
onAuthError?: (params: {
|
|
195
205
|
error: OidcError;
|
|
196
206
|
provider: string;
|
|
207
|
+
/**
|
|
208
|
+
* The redirectUri from the initiate request (if available)
|
|
209
|
+
* May be undefined for errors that occur before session lookup
|
|
210
|
+
* (e.g., IdP returns an error before we can retrieve the session)
|
|
211
|
+
*/
|
|
212
|
+
redirectUri?: string;
|
|
197
213
|
}) => Promise<AuthErrorCallbackResponse>;
|
|
198
214
|
/**
|
|
199
215
|
* Dynamic provider loader callback
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OidcPluginOptions.d.ts","sourceRoot":"","sources":["../src/OidcPluginOptions.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,MAAM,uBAAuB,CAAC;AAChD,OAAO,YAAY,MAAM,wBAAwB,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,SAAS;IACtB;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,OAAO,CAAC,EAAE,GAAG,CAAC;CACjB;AAED;;;GAGG;AACH,MAAM,WAAW,2BAA2B;IACxC;;;OAGG;IACH,IAAI,EAAE,GAAG,CAAC;IAEV;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACtC;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB,CAAC,IAAI,GAAG,GAAG;IACzC;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IAE/C;;;;;;;;;;;OAWG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgDG;IACH,aAAa,EAAE,CACX,MAAM,EAAE;QACJ;;WAEG;QACH,OAAO,EAAE,WAAW,CAAC;QAErB;;;WAGG;QACH,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAE5B;;WAEG;QACH,QAAQ,EAAE,MAAM,CAAC;QAEjB;;;WAGG;QACH,MAAM,CAAC,EAAE,YAAY,CAAC;KACzB,EACD,GAAG,EAAE,IAAI,KACR,OAAO,CAAC,2BAA2B,CAAC,CAAC;IAE1C;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACH,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE;
|
|
1
|
+
{"version":3,"file":"OidcPluginOptions.d.ts","sourceRoot":"","sources":["../src/OidcPluginOptions.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,MAAM,uBAAuB,CAAC;AAChD,OAAO,YAAY,MAAM,wBAAwB,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,SAAS;IACtB;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,OAAO,CAAC,EAAE,GAAG,CAAC;CACjB;AAED;;;GAGG;AACH,MAAM,WAAW,2BAA2B;IACxC;;;OAGG;IACH,IAAI,EAAE,GAAG,CAAC;IAEV;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACtC;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB,CAAC,IAAI,GAAG,GAAG;IACzC;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IAE/C;;;;;;;;;;;OAWG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgDG;IACH,aAAa,EAAE,CACX,MAAM,EAAE;QACJ;;WAEG;QACH,OAAO,EAAE,WAAW,CAAC;QAErB;;;WAGG;QACH,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAE5B;;WAEG;QACH,QAAQ,EAAE,MAAM,CAAC;QAEjB;;;;;;;;WAQG;QACH,WAAW,EAAE,MAAM,CAAC;QAEpB;;;WAGG;QACH,MAAM,CAAC,EAAE,YAAY,CAAC;KACzB,EACD,GAAG,EAAE,IAAI,KACR,OAAO,CAAC,2BAA2B,CAAC,CAAC;IAE1C;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACH,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE;QACnB,KAAK,EAAE,SAAS,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB;;;;WAIG;QACH,WAAW,CAAC,EAAE,MAAM,CAAC;KACxB,KAAK,OAAO,CAAC,yBAAyB,CAAC,CAAC;IAEzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACH,cAAc,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,KAAK,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC;IAEzF;;;OAGG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAEhC;;;OAGG;IACH,yBAAyB,CAAC,EAAE,MAAM,CAAC;IAEnC;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC5B"}
|
|
@@ -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,CA4PnE,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
|
|
@@ -38,19 +39,32 @@ exports.Route = {
|
|
|
38
39
|
const CallbackOidc = async ({ ctx, req }) => {
|
|
39
40
|
const { provider } = req.params;
|
|
40
41
|
const { code, state, error: oidcError, error_description, response_type } = req.query;
|
|
42
|
+
// Track session outside try/catch so redirectUri is available in error handling
|
|
43
|
+
let sessionRedirectUri;
|
|
41
44
|
try {
|
|
42
45
|
// Validate provider and response_type
|
|
43
46
|
(0, error_utils_1.validateProvider)(provider);
|
|
44
47
|
(0, error_utils_1.validateResponseType)(response_type);
|
|
48
|
+
log_1.oidcLog.debug(`Callback: provider="${provider}" hasCode=${!!code} hasState=${!!state} responseType="${response_type || "redirect"}"`);
|
|
45
49
|
// Check for OIDC provider errors (e.g., user denied access)
|
|
46
50
|
if (oidcError) {
|
|
51
|
+
log_1.oidcLog.debug(`Callback: IdP returned error="${oidcError}" description="${error_description || ""}"`);
|
|
47
52
|
const error = (0, error_utils_1.handleProviderError)({ error: oidcError, error_description });
|
|
53
|
+
// Try to retrieve session redirectUri (state may be available even on IdP errors)
|
|
54
|
+
if (state) {
|
|
55
|
+
const errorSession = await ctx.repos.oidcSessionRepo.getByState(state);
|
|
56
|
+
sessionRedirectUri = errorSession?.redirectUri;
|
|
57
|
+
if (errorSession) {
|
|
58
|
+
await ctx.repos.oidcSessionRepo.deleteBySessionId(errorSession.sessionId);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
48
61
|
// Call onAuthError callback if provided
|
|
49
62
|
const { options } = ctx.plugins.oidc;
|
|
50
63
|
if (options.onAuthError) {
|
|
51
64
|
const errorResult = await options.onAuthError({
|
|
52
65
|
error,
|
|
53
66
|
provider,
|
|
67
|
+
redirectUri: sessionRedirectUri,
|
|
54
68
|
});
|
|
55
69
|
if (errorResult.redirectUrl) {
|
|
56
70
|
return {
|
|
@@ -72,16 +86,21 @@ const CallbackOidc = async ({ ctx, req }) => {
|
|
|
72
86
|
// Find OIDC session by state
|
|
73
87
|
const session = await ctx.repos.oidcSessionRepo.getByState(state);
|
|
74
88
|
if (!session) {
|
|
89
|
+
log_1.oidcLog.debug(`Callback: no session found for state (may be expired)`);
|
|
75
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 });
|
|
76
91
|
}
|
|
92
|
+
log_1.oidcLog.debug(`Callback: session found sessionId=${session.sessionId} provider="${session.provider}" redirectUri="${session.redirectUri}"`);
|
|
77
93
|
// Validate state parameter (CSRF protection)
|
|
78
94
|
if (!(0, state_utils_1.validateState)(state, session.state)) {
|
|
79
95
|
throw (0, error_utils_1.createOidcError)(error_utils_1.OidcErrorCodes.INVALID_STATE, "Invalid state parameter. Possible CSRF attack detected.", {
|
|
80
96
|
providedState: state.substring(0, 10) + "...",
|
|
81
97
|
});
|
|
82
98
|
}
|
|
99
|
+
// Track redirectUri for error handling
|
|
100
|
+
sessionRedirectUri = session.redirectUri;
|
|
83
101
|
// Delete session immediately after validation (one-time use)
|
|
84
102
|
await ctx.repos.oidcSessionRepo.deleteBySessionId(session.sessionId);
|
|
103
|
+
log_1.oidcLog.debug(`Callback: state validated, session deleted (one-time use)`);
|
|
85
104
|
// Get plugin options
|
|
86
105
|
const { options } = ctx.plugins.oidc;
|
|
87
106
|
// Get provider instance
|
|
@@ -91,19 +110,25 @@ const CallbackOidc = async ({ ctx, req }) => {
|
|
|
91
110
|
}
|
|
92
111
|
const oidcProvider = await providerRegistry.getProvider(provider);
|
|
93
112
|
// Exchange authorization code for tokens with PKCE validation
|
|
113
|
+
log_1.oidcLog.debug(`Callback: exchanging authorization code for tokens`);
|
|
94
114
|
const tokenSet = await oidcProvider.exchangeCodeForToken({
|
|
95
115
|
code,
|
|
96
116
|
codeVerifier: session.codeVerifier,
|
|
97
117
|
state: session.state,
|
|
98
118
|
nonce: session.nonce,
|
|
99
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`);
|
|
100
121
|
// Build user profile from ID token and UserInfo
|
|
122
|
+
log_1.oidcLog.debug(`Callback: building user profile`);
|
|
101
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)"}"`);
|
|
102
125
|
// Call onAuthSuccess callback to create/link user and generate JWT token
|
|
126
|
+
log_1.oidcLog.debug(`Callback: calling onAuthSuccess`);
|
|
103
127
|
const authSuccessParams = {
|
|
104
128
|
profile,
|
|
105
129
|
claims: tokenSet.claims,
|
|
106
130
|
provider,
|
|
131
|
+
redirectUri: session.redirectUri,
|
|
107
132
|
...(options.storeTokens ? { tokens: tokenSet } : {}),
|
|
108
133
|
};
|
|
109
134
|
let authResult;
|
|
@@ -121,6 +146,7 @@ const CallbackOidc = async ({ ctx, req }) => {
|
|
|
121
146
|
const errorResult = await options.onAuthError({
|
|
122
147
|
error: oidcError,
|
|
123
148
|
provider,
|
|
149
|
+
redirectUri: session.redirectUri,
|
|
124
150
|
});
|
|
125
151
|
if (errorResult.redirectUrl) {
|
|
126
152
|
return {
|
|
@@ -132,6 +158,7 @@ const CallbackOidc = async ({ ctx, req }) => {
|
|
|
132
158
|
}
|
|
133
159
|
return (0, flink_1.internalServerError)("Authentication failed. Please try again.");
|
|
134
160
|
}
|
|
161
|
+
log_1.oidcLog.debug(`Callback: onAuthSuccess completed`);
|
|
135
162
|
// Extract user and JWT token from callback result
|
|
136
163
|
const { user, token, redirectUrl } = authResult;
|
|
137
164
|
if (!token) {
|
|
@@ -182,8 +209,10 @@ const CallbackOidc = async ({ ctx, req }) => {
|
|
|
182
209
|
}
|
|
183
210
|
}
|
|
184
211
|
}
|
|
212
|
+
const finalRedirectUrl = redirectUrl || session.redirectUri;
|
|
213
|
+
log_1.oidcLog.debug(`Callback: auth complete, responding via ${response_type === "json" ? "JSON" : `redirect to "${finalRedirectUrl}"`}`);
|
|
185
214
|
// Return JWT token in requested format
|
|
186
|
-
return (0, response_utils_1.formatTokenResponse)(token, user,
|
|
215
|
+
return (0, response_utils_1.formatTokenResponse)(token, user, finalRedirectUrl, response_type);
|
|
187
216
|
}
|
|
188
217
|
catch (error) {
|
|
189
218
|
flink_1.log.error("OIDC callback error:", error);
|
|
@@ -196,6 +225,7 @@ const CallbackOidc = async ({ ctx, req }) => {
|
|
|
196
225
|
const errorResult = await options.onAuthError({
|
|
197
226
|
error,
|
|
198
227
|
provider,
|
|
228
|
+
redirectUri: sessionRedirectUri,
|
|
199
229
|
});
|
|
200
230
|
if (errorResult.redirectUrl) {
|
|
201
231
|
return {
|
|
@@ -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`);
|
|
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":"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.80",
|
|
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.80"
|
|
15
15
|
},
|
|
16
16
|
"peerDependencies": {
|
|
17
|
-
"@flink-app/flink": ">=2.0.0-alpha.
|
|
17
|
+
"@flink-app/flink": ">=2.0.0-alpha.80",
|
|
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/jwt-auth-plugin": "2.0.0-alpha.
|
|
31
|
-
"@flink-app/test-utils": "2.0.0-alpha.
|
|
32
|
-
"@flink-app/flink": "2.0.0-alpha.
|
|
30
|
+
"@flink-app/jwt-auth-plugin": "2.0.0-alpha.80",
|
|
31
|
+
"@flink-app/test-utils": "2.0.0-alpha.80",
|
|
32
|
+
"@flink-app/flink": "2.0.0-alpha.80"
|
|
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
|
|
package/src/OidcPluginOptions.ts
CHANGED
|
@@ -172,6 +172,17 @@ export interface OidcPluginOptions<TCtx = any> {
|
|
|
172
172
|
*/
|
|
173
173
|
provider: string;
|
|
174
174
|
|
|
175
|
+
/**
|
|
176
|
+
* The redirectUri passed on the initiate request (stored in session)
|
|
177
|
+
* Use this to redirect back to the originating client application.
|
|
178
|
+
*
|
|
179
|
+
* Example:
|
|
180
|
+
* ```typescript
|
|
181
|
+
* return { user, token, redirectUrl: redirectUri || DEFAULT_REDIRECT_URL };
|
|
182
|
+
* ```
|
|
183
|
+
*/
|
|
184
|
+
redirectUri: string;
|
|
185
|
+
|
|
175
186
|
/**
|
|
176
187
|
* OIDC tokens (only if storeTokens: true)
|
|
177
188
|
* Includes accessToken, idToken, refreshToken
|
|
@@ -208,7 +219,16 @@ export interface OidcPluginOptions<TCtx = any> {
|
|
|
208
219
|
* }
|
|
209
220
|
* ```
|
|
210
221
|
*/
|
|
211
|
-
onAuthError?: (params: {
|
|
222
|
+
onAuthError?: (params: {
|
|
223
|
+
error: OidcError;
|
|
224
|
+
provider: string;
|
|
225
|
+
/**
|
|
226
|
+
* The redirectUri from the initiate request (if available)
|
|
227
|
+
* May be undefined for errors that occur before session lookup
|
|
228
|
+
* (e.g., IdP returns an error before we can retrieve the session)
|
|
229
|
+
*/
|
|
230
|
+
redirectUri?: string;
|
|
231
|
+
}) => Promise<AuthErrorCallbackResponse>;
|
|
212
232
|
|
|
213
233
|
/**
|
|
214
234
|
* Dynamic provider loader callback
|
|
@@ -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
|
|
@@ -48,21 +49,37 @@ const CallbackOidc: GetHandler<any, any, PathParams, CallbackRequest> = async ({
|
|
|
48
49
|
const { provider } = req.params;
|
|
49
50
|
const { code, state, error: oidcError, error_description, response_type } = req.query;
|
|
50
51
|
|
|
52
|
+
// Track session outside try/catch so redirectUri is available in error handling
|
|
53
|
+
let sessionRedirectUri: string | undefined;
|
|
54
|
+
|
|
51
55
|
try {
|
|
52
56
|
// Validate provider and response_type
|
|
53
57
|
validateProvider(provider);
|
|
54
58
|
validateResponseType(response_type);
|
|
55
59
|
|
|
60
|
+
oidcLog.debug(`Callback: provider="${provider}" hasCode=${!!code} hasState=${!!state} responseType="${response_type || "redirect"}"`);
|
|
61
|
+
|
|
56
62
|
// Check for OIDC provider errors (e.g., user denied access)
|
|
57
63
|
if (oidcError) {
|
|
64
|
+
oidcLog.debug(`Callback: IdP returned error="${oidcError}" description="${error_description || ""}"`);
|
|
58
65
|
const error = handleProviderError({ error: oidcError, error_description });
|
|
59
66
|
|
|
67
|
+
// Try to retrieve session redirectUri (state may be available even on IdP errors)
|
|
68
|
+
if (state) {
|
|
69
|
+
const errorSession = await ctx.repos.oidcSessionRepo.getByState(state);
|
|
70
|
+
sessionRedirectUri = errorSession?.redirectUri;
|
|
71
|
+
if (errorSession) {
|
|
72
|
+
await ctx.repos.oidcSessionRepo.deleteBySessionId(errorSession.sessionId);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
60
76
|
// Call onAuthError callback if provided
|
|
61
77
|
const { options } = ctx.plugins.oidc;
|
|
62
78
|
if (options.onAuthError) {
|
|
63
79
|
const errorResult = await options.onAuthError({
|
|
64
80
|
error,
|
|
65
81
|
provider,
|
|
82
|
+
redirectUri: sessionRedirectUri,
|
|
66
83
|
});
|
|
67
84
|
|
|
68
85
|
if (errorResult.redirectUrl) {
|
|
@@ -89,9 +106,12 @@ const CallbackOidc: GetHandler<any, any, PathParams, CallbackRequest> = async ({
|
|
|
89
106
|
const session = await ctx.repos.oidcSessionRepo.getByState(state);
|
|
90
107
|
|
|
91
108
|
if (!session) {
|
|
109
|
+
oidcLog.debug(`Callback: no session found for state (may be expired)`);
|
|
92
110
|
throw createOidcError(OidcErrorCodes.SESSION_EXPIRED, "OIDC session not found or expired. Please try logging in again.", { state });
|
|
93
111
|
}
|
|
94
112
|
|
|
113
|
+
oidcLog.debug(`Callback: session found sessionId=${session.sessionId} provider="${session.provider}" redirectUri="${session.redirectUri}"`);
|
|
114
|
+
|
|
95
115
|
// Validate state parameter (CSRF protection)
|
|
96
116
|
if (!validateState(state, session.state)) {
|
|
97
117
|
throw createOidcError(OidcErrorCodes.INVALID_STATE, "Invalid state parameter. Possible CSRF attack detected.", {
|
|
@@ -99,8 +119,12 @@ const CallbackOidc: GetHandler<any, any, PathParams, CallbackRequest> = async ({
|
|
|
99
119
|
});
|
|
100
120
|
}
|
|
101
121
|
|
|
122
|
+
// Track redirectUri for error handling
|
|
123
|
+
sessionRedirectUri = session.redirectUri;
|
|
124
|
+
|
|
102
125
|
// Delete session immediately after validation (one-time use)
|
|
103
126
|
await ctx.repos.oidcSessionRepo.deleteBySessionId(session.sessionId);
|
|
127
|
+
oidcLog.debug(`Callback: state validated, session deleted (one-time use)`);
|
|
104
128
|
|
|
105
129
|
// Get plugin options
|
|
106
130
|
const { options } = ctx.plugins.oidc;
|
|
@@ -114,6 +138,7 @@ const CallbackOidc: GetHandler<any, any, PathParams, CallbackRequest> = async ({
|
|
|
114
138
|
const oidcProvider = await providerRegistry.getProvider(provider);
|
|
115
139
|
|
|
116
140
|
// Exchange authorization code for tokens with PKCE validation
|
|
141
|
+
oidcLog.debug(`Callback: exchanging authorization code for tokens`);
|
|
117
142
|
const tokenSet = await oidcProvider.exchangeCodeForToken({
|
|
118
143
|
code,
|
|
119
144
|
codeVerifier: session.codeVerifier,
|
|
@@ -121,14 +146,27 @@ const CallbackOidc: GetHandler<any, any, PathParams, CallbackRequest> = async ({
|
|
|
121
146
|
nonce: session.nonce,
|
|
122
147
|
});
|
|
123
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
|
+
|
|
124
158
|
// Build user profile from ID token and UserInfo
|
|
159
|
+
oidcLog.debug(`Callback: building user profile`);
|
|
125
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)"}"`);
|
|
126
162
|
|
|
127
163
|
// Call onAuthSuccess callback to create/link user and generate JWT token
|
|
164
|
+
oidcLog.debug(`Callback: calling onAuthSuccess`);
|
|
128
165
|
const authSuccessParams = {
|
|
129
166
|
profile,
|
|
130
167
|
claims: tokenSet.claims,
|
|
131
168
|
provider,
|
|
169
|
+
redirectUri: session.redirectUri,
|
|
132
170
|
...(options.storeTokens ? { tokens: tokenSet } : {}),
|
|
133
171
|
};
|
|
134
172
|
|
|
@@ -148,6 +186,7 @@ const CallbackOidc: GetHandler<any, any, PathParams, CallbackRequest> = async ({
|
|
|
148
186
|
const errorResult = await options.onAuthError({
|
|
149
187
|
error: oidcError,
|
|
150
188
|
provider,
|
|
189
|
+
redirectUri: session.redirectUri,
|
|
151
190
|
});
|
|
152
191
|
|
|
153
192
|
if (errorResult.redirectUrl) {
|
|
@@ -162,6 +201,8 @@ const CallbackOidc: GetHandler<any, any, PathParams, CallbackRequest> = async ({
|
|
|
162
201
|
return internalServerError("Authentication failed. Please try again.");
|
|
163
202
|
}
|
|
164
203
|
|
|
204
|
+
oidcLog.debug(`Callback: onAuthSuccess completed`);
|
|
205
|
+
|
|
165
206
|
// Extract user and JWT token from callback result
|
|
166
207
|
const { user, token, redirectUrl } = authResult;
|
|
167
208
|
|
|
@@ -217,8 +258,11 @@ const CallbackOidc: GetHandler<any, any, PathParams, CallbackRequest> = async ({
|
|
|
217
258
|
}
|
|
218
259
|
}
|
|
219
260
|
|
|
261
|
+
const finalRedirectUrl = redirectUrl || session.redirectUri;
|
|
262
|
+
oidcLog.debug(`Callback: auth complete, responding via ${response_type === "json" ? "JSON" : `redirect to "${finalRedirectUrl}"`}`);
|
|
263
|
+
|
|
220
264
|
// Return JWT token in requested format
|
|
221
|
-
return formatTokenResponse(token, user,
|
|
265
|
+
return formatTokenResponse(token, user, finalRedirectUrl, response_type);
|
|
222
266
|
} catch (error: any) {
|
|
223
267
|
log.error("OIDC callback error:", error);
|
|
224
268
|
|
|
@@ -231,6 +275,7 @@ const CallbackOidc: GetHandler<any, any, PathParams, CallbackRequest> = async ({
|
|
|
231
275
|
const errorResult = await options.onAuthError({
|
|
232
276
|
error,
|
|
233
277
|
provider,
|
|
278
|
+
redirectUri: sessionRedirectUri,
|
|
234
279
|
});
|
|
235
280
|
|
|
236
281
|
if (errorResult.redirectUrl) {
|
|
@@ -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`);
|
|
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
|
+
}
|
|
@@ -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);
|