@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 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
@@ -1 +1 @@
1
- {"version":3,"file":"OidcPlugin.d.ts","sourceRoot":"","sources":["../src/OidcPlugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,WAAW,EAAO,MAAM,kBAAkB,CAAC;AAE9D,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,CAmNpF"}
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"}
@@ -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;QAAE,KAAK,EAAE,SAAS,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,yBAAyB,CAAC,CAAC;IAErG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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
+ {"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;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;;;;;;GAMG;AACH,QAAA,MAAM,YAAY,EAAE,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,UAAU,EAAE,eAAe,CAgNnE,CAAC;AAEF,eAAe,YAAY,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, redirectUrl || session.redirectUri, response_type);
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;AAKzD;;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,CAiEnE,CAAC;AAEF,eAAe,YAAY,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;AAG3D;;;;;;;;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;IAgC9D;;;;;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"}
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.78",
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.78"
14
+ "@flink-app/jwt-auth-plugin": "2.0.0-alpha.80"
15
15
  },
16
16
  "peerDependencies": {
17
- "@flink-app/flink": ">=2.0.0-alpha.78",
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.78",
31
- "@flink-app/test-utils": "2.0.0-alpha.78",
32
- "@flink-app/flink": "2.0.0-alpha.78"
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 { encryptToken, decryptToken, validateEncryptionSecret } from "./utils/encryption-utils";
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
 
@@ -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: { error: OidcError; provider: string }) => Promise<AuthErrorCallbackResponse>;
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, redirectUrl || session.redirectUri, response_type);
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);