@flink-app/oidc-plugin 2.0.0-alpha.84 → 2.0.0-alpha.86

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.86
4
+
5
+ ### Patch Changes
6
+
7
+ - 4219f07: Add `tokenDelivery` option to control how the JWT is attached to the post-callback redirect URL. Defaults to `"fragment"` (unchanged — `#token=...` is still appended for browser flows where URL fragments are not sent to the server). Set to `"query"` for native mobile flows where iOS `ASWebAuthenticationSession` strips fragments from custom-scheme callbacks, or pass a function `({ redirectUrl, provider, metadata }) => "fragment" | "query"` to decide per-request when one app serves both web and native clients. The plugin makes no scheme-based assumptions — policy lives in the host app.
8
+ - @flink-app/flink@2.0.0-alpha.86
9
+ - @flink-app/jwt-auth-plugin@2.0.0-alpha.86
10
+
11
+ ## 2.0.0-alpha.85
12
+
13
+ ### Patch Changes
14
+
15
+ - Updated dependencies [179c085]
16
+ - @flink-app/jwt-auth-plugin@2.0.0-alpha.85
17
+ - @flink-app/flink@2.0.0-alpha.85
18
+
3
19
  ## 2.0.0-alpha.84
4
20
 
5
21
  ### Patch Changes
@@ -1,6 +1,7 @@
1
1
  import OidcProfile from "./schemas/OidcProfile";
2
2
  import OidcTokenSet from "./schemas/OidcTokenSet";
3
3
  import { OidcProviderConfig } from "./OidcProviderConfig";
4
+ import { TokenDeliveryMode } from "./utils/response-utils";
4
5
  /**
5
6
  * OIDC error information passed to onAuthError callback
6
7
  */
@@ -35,7 +36,9 @@ export interface AuthSuccessCallbackResponse {
35
36
  token: string;
36
37
  /**
37
38
  * Optional redirect URL after authentication
38
- * Plugin will append #token=... to this URL
39
+ *
40
+ * The plugin will attach the JWT token to this URL according to the
41
+ * top-level `tokenDelivery` option (fragment by default, or query).
39
42
  */
40
43
  redirectUrl?: string;
41
44
  }
@@ -288,6 +291,36 @@ export interface OidcPluginOptions<TCtx = any> {
288
291
  * Example: process.env.OIDC_ENCRYPTION_KEY
289
292
  */
290
293
  encryptionKey?: string;
294
+ /**
295
+ * Controls how the JWT token is delivered back to the client on the
296
+ * post-callback redirect (ignored when the caller uses `?response_type=json`).
297
+ *
298
+ * - `"fragment"` (default): appends `#token=...` to the redirectUrl. Best
299
+ * for web browsers — URL fragments are not sent to the server in HTTP
300
+ * requests, which preserves a small but real security property.
301
+ * - `"query"`: appends `?token=...` (or `&token=...`). Required for native
302
+ * mobile flows where the callback comes back via a custom URL scheme,
303
+ * because iOS `ASWebAuthenticationSession` strips URL fragments before
304
+ * delivering the URL to the app.
305
+ * - function: called once per callback to decide the delivery mode. Use
306
+ * this when one app serves both web and native clients. The function
307
+ * receives the resolved redirectUrl and the metadata bag from the
308
+ * initiate request, so the host can route by URL scheme, by
309
+ * `meta.intent`, or any other signal — the plugin makes no assumptions.
310
+ *
311
+ * Default: `"fragment"` (unchanged from prior versions).
312
+ *
313
+ * Example — route native (custom-scheme) clients to query, browsers to fragment:
314
+ * ```typescript
315
+ * tokenDelivery: ({ redirectUrl }) =>
316
+ * /^https?:/i.test(redirectUrl) ? "fragment" : "query",
317
+ * ```
318
+ */
319
+ tokenDelivery?: TokenDeliveryMode | ((params: {
320
+ redirectUrl: string;
321
+ provider: string;
322
+ metadata: Record<string, string>;
323
+ }) => TokenDeliveryMode);
291
324
  /**
292
325
  * Whether to register OIDC routes automatically
293
326
  * If false, you must manually handle OIDC flow
@@ -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;;;;;;;;WAQG;QACH,WAAW,EAAE,MAAM,CAAC;QAEpB;;;;;;;;WAQG;QACH,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAEjC;;;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;QACrB;;;WAGG;QACH,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACpC,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
+ {"version":3,"file":"OidcPluginOptions.d.ts","sourceRoot":"","sources":["../src/OidcPluginOptions.ts"],"names":[],"mappings":"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"}
@@ -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;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,CAqQnE,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,CAiRnE,CAAC;AAEF,eAAe,YAAY,CAAC"}
@@ -219,8 +219,18 @@ const CallbackOidc = async ({ ctx, req }) => {
219
219
  }
220
220
  const finalRedirectUrl = redirectUrl || session.redirectUri;
221
221
  log_1.oidcLog.debug(`Callback: auth complete, responding via ${response_type === "json" ? "JSON" : `redirect to "${finalRedirectUrl}"`}`);
222
+ // Resolve token delivery mode: literal, function, or default "fragment".
223
+ // The plugin stays a dumb transport — the host app makes the policy decision.
224
+ const tokenDeliverySetting = options.tokenDelivery ?? "fragment";
225
+ const resolvedTokenDelivery = typeof tokenDeliverySetting === "function"
226
+ ? tokenDeliverySetting({
227
+ redirectUrl: finalRedirectUrl,
228
+ provider,
229
+ metadata: sessionMetadata,
230
+ })
231
+ : tokenDeliverySetting;
222
232
  // Return JWT token in requested format
223
- return (0, response_utils_1.formatTokenResponse)(token, user, finalRedirectUrl, response_type);
233
+ return (0, response_utils_1.formatTokenResponse)(token, user, finalRedirectUrl, response_type, resolvedTokenDelivery);
224
234
  }
225
235
  catch (error) {
226
236
  flink_1.log.error("OIDC callback error:", error);
package/dist/index.d.ts CHANGED
@@ -22,6 +22,6 @@ export { ProviderRegistry } from "./providers/ProviderRegistry";
22
22
  export { generateState, generateSessionId, generateNonce, validateState } from "./utils/state-utils";
23
23
  export { encryptToken, decryptToken, validateEncryptionSecret } from "./utils/encryption-utils";
24
24
  export { mapClaimsToProfile, extractCustomClaims } from "./utils/claims-mapper";
25
- export { formatTokenResponse } from "./utils/response-utils";
25
+ export { formatTokenResponse, TokenDeliveryMode } from "./utils/response-utils";
26
26
  export { createOidcError, validateProvider, handleProviderError, OidcErrorCodes } from "./utils/error-utils";
27
27
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG1C,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,2BAA2B,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAC3H,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAG1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAGxD,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,0BAA0B,CAAC;AACrE,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,2BAA2B,CAAC;AACvE,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAGvE,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAGhE,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACrG,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AAChG,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAChF,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG1C,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,2BAA2B,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAC3H,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAG1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAGxD,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,0BAA0B,CAAC;AACrE,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,2BAA2B,CAAC;AACvE,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAGvE,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAGhE,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACrG,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AAChG,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAChF,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAChF,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC"}
@@ -1,18 +1,29 @@
1
+ /**
2
+ * Controls how the JWT token is delivered back to the client on the
3
+ * post-callback redirect.
4
+ *
5
+ * - "fragment": appends `#token=...` to the redirectUrl. Best for web
6
+ * browsers — URL fragments are not sent to the server in HTTP requests
7
+ * and are only accessible to client-side JavaScript.
8
+ * - "query": appends `?token=...` (or `&token=...`). Required for native
9
+ * mobile flows where the callback comes back via a custom URL scheme,
10
+ * because iOS `ASWebAuthenticationSession` strips URL fragments before
11
+ * delivering the URL to the app.
12
+ */
13
+ export type TokenDeliveryMode = "fragment" | "query";
1
14
  /**
2
15
  * Format token response for the client
3
16
  *
4
17
  * Supports two response formats:
5
18
  * 1. JSON response (for API clients)
6
- * 2. Redirect with token in URL fragment (for web browsers)
7
- *
8
- * URL fragments are used for security - they are NOT sent to the server
9
- * in HTTP requests and are only accessible to client-side JavaScript.
19
+ * 2. Redirect with token in URL fragment or query (for web/native clients)
10
20
  *
11
21
  * @param token - JWT token for the application
12
22
  * @param user - User object
13
23
  * @param redirectUrl - URL to redirect to
14
24
  * @param responseType - "json" or undefined (redirect)
25
+ * @param tokenDelivery - How to attach the token on redirect. Defaults to "fragment".
15
26
  * @returns Flink response object
16
27
  */
17
- export declare function formatTokenResponse(token: string, user: any, redirectUrl: string, responseType?: "json"): any;
28
+ export declare function formatTokenResponse(token: string, user: any, redirectUrl: string, responseType?: "json", tokenDelivery?: TokenDeliveryMode): any;
18
29
  //# sourceMappingURL=response-utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"response-utils.d.ts","sourceRoot":"","sources":["../../src/utils/response-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,GAAG,CAwB7G"}
1
+ {"version":3,"file":"response-utils.d.ts","sourceRoot":"","sources":["../../src/utils/response-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,iBAAiB,GAAG,UAAU,GAAG,OAAO,CAAC;AAErD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAC/B,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,GAAG,EACT,WAAW,EAAE,MAAM,EACnB,YAAY,CAAC,EAAE,MAAM,EACrB,aAAa,GAAE,iBAA8B,GAC9C,GAAG,CAqBL"}
@@ -6,18 +6,16 @@ exports.formatTokenResponse = formatTokenResponse;
6
6
  *
7
7
  * Supports two response formats:
8
8
  * 1. JSON response (for API clients)
9
- * 2. Redirect with token in URL fragment (for web browsers)
10
- *
11
- * URL fragments are used for security - they are NOT sent to the server
12
- * in HTTP requests and are only accessible to client-side JavaScript.
9
+ * 2. Redirect with token in URL fragment or query (for web/native clients)
13
10
  *
14
11
  * @param token - JWT token for the application
15
12
  * @param user - User object
16
13
  * @param redirectUrl - URL to redirect to
17
14
  * @param responseType - "json" or undefined (redirect)
15
+ * @param tokenDelivery - How to attach the token on redirect. Defaults to "fragment".
18
16
  * @returns Flink response object
19
17
  */
20
- function formatTokenResponse(token, user, redirectUrl, responseType) {
18
+ function formatTokenResponse(token, user, redirectUrl, responseType, tokenDelivery = "fragment") {
21
19
  // JSON response for API clients
22
20
  if (responseType === "json") {
23
21
  return {
@@ -27,11 +25,8 @@ function formatTokenResponse(token, user, redirectUrl, responseType) {
27
25
  },
28
26
  };
29
27
  }
30
- // Redirect response for web browsers
31
- // Token is in URL fragment (#token=...) for security
32
- const separator = redirectUrl.includes("#") ? "&" : "#";
33
- const tokenFragment = `token=${encodeURIComponent(token)}`;
34
- const finalUrl = `${redirectUrl}${separator}${tokenFragment}`;
28
+ const tokenParam = `token=${encodeURIComponent(token)}`;
29
+ const finalUrl = appendToken(redirectUrl, tokenParam, tokenDelivery);
35
30
  return {
36
31
  status: 302,
37
32
  headers: {
@@ -40,3 +35,29 @@ function formatTokenResponse(token, user, redirectUrl, responseType) {
40
35
  data: {},
41
36
  };
42
37
  }
38
+ /**
39
+ * Append a token parameter to a URL using the specified delivery mode.
40
+ *
41
+ * For "query" mode, any existing `#fragment` on the URL is preserved: the
42
+ * query parameter is spliced in *before* the fragment so the final URL is
43
+ * still well-formed.
44
+ *
45
+ * For "fragment" mode, a second token on an existing fragment is joined
46
+ * with `&` (same as the old behavior).
47
+ */
48
+ function appendToken(redirectUrl, tokenParam, mode) {
49
+ if (mode === "query") {
50
+ const hashIdx = redirectUrl.indexOf("#");
51
+ if (hashIdx >= 0) {
52
+ const base = redirectUrl.slice(0, hashIdx);
53
+ const frag = redirectUrl.slice(hashIdx);
54
+ const sep = base.includes("?") ? "&" : "?";
55
+ return `${base}${sep}${tokenParam}${frag}`;
56
+ }
57
+ const sep = redirectUrl.includes("?") ? "&" : "?";
58
+ return `${redirectUrl}${sep}${tokenParam}`;
59
+ }
60
+ // fragment (default)
61
+ const sep = redirectUrl.includes("#") ? "&" : "#";
62
+ return `${redirectUrl}${sep}${tokenParam}`;
63
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flink-app/oidc-plugin",
3
- "version": "2.0.0-alpha.84",
3
+ "version": "2.0.0-alpha.86",
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.84"
14
+ "@flink-app/jwt-auth-plugin": "2.0.0-alpha.86"
15
15
  },
16
16
  "peerDependencies": {
17
- "@flink-app/flink": ">=2.0.0-alpha.84",
17
+ "@flink-app/flink": ">=2.0.0-alpha.86",
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.84",
31
- "@flink-app/flink": "2.0.0-alpha.84",
32
- "@flink-app/test-utils": "2.0.0-alpha.84"
30
+ "@flink-app/flink": "2.0.0-alpha.86",
31
+ "@flink-app/jwt-auth-plugin": "2.0.0-alpha.86",
32
+ "@flink-app/test-utils": "2.0.0-alpha.86"
33
33
  },
34
34
  "scripts": {
35
35
  "test": "jasmine-ts --config=./spec/support/jasmine.json",
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Tests for formatTokenResponse — covers JSON mode, fragment mode (default,
3
+ * backwards compatible), and query mode (including the tricky case where
4
+ * the redirectUrl already carries a fragment that must be preserved).
5
+ */
6
+
7
+ import { formatTokenResponse } from "../../src/utils/response-utils";
8
+
9
+ describe("response-utils", () => {
10
+ describe("formatTokenResponse", () => {
11
+ const token = "jwt.token.value";
12
+ const user = { id: "u1", email: "a@b.c" };
13
+
14
+ describe("JSON mode", () => {
15
+ it("returns { data: { user, token } } when response_type=json", () => {
16
+ const result = formatTokenResponse(token, user, "https://app/cb", "json");
17
+ expect(result).toEqual({ data: { user, token } });
18
+ });
19
+
20
+ it("ignores tokenDelivery when response_type=json", () => {
21
+ const result = formatTokenResponse(token, user, "https://app/cb", "json", "query");
22
+ expect(result).toEqual({ data: { user, token } });
23
+ });
24
+ });
25
+
26
+ describe("fragment mode (default)", () => {
27
+ it("appends #token=... to a plain URL", () => {
28
+ const result = formatTokenResponse(token, user, "https://app/cb");
29
+ expect(result.status).toBe(302);
30
+ expect(result.headers.Location).toBe(`https://app/cb#token=${encodeURIComponent(token)}`);
31
+ });
32
+
33
+ it("uses & when the URL already has a fragment", () => {
34
+ const result = formatTokenResponse(token, user, "https://app/cb#foo=bar");
35
+ expect(result.headers.Location).toBe(`https://app/cb#foo=bar&token=${encodeURIComponent(token)}`);
36
+ });
37
+
38
+ it("preserves existing query string", () => {
39
+ const result = formatTokenResponse(token, user, "https://app/cb?next=/dashboard");
40
+ expect(result.headers.Location).toBe(`https://app/cb?next=/dashboard#token=${encodeURIComponent(token)}`);
41
+ });
42
+
43
+ it("URL-encodes tokens with special characters", () => {
44
+ const weird = "a b+c/d=e";
45
+ const result = formatTokenResponse(weird, user, "https://app/cb");
46
+ expect(result.headers.Location).toBe(`https://app/cb#token=${encodeURIComponent(weird)}`);
47
+ });
48
+
49
+ it("is the default when tokenDelivery is omitted", () => {
50
+ const implicit = formatTokenResponse(token, user, "https://app/cb");
51
+ const explicit = formatTokenResponse(token, user, "https://app/cb", undefined, "fragment");
52
+ expect(implicit.headers.Location).toBe(explicit.headers.Location);
53
+ });
54
+ });
55
+
56
+ describe("query mode", () => {
57
+ it("appends ?token=... to a plain URL", () => {
58
+ const result = formatTokenResponse(token, user, "myapp://cb", undefined, "query");
59
+ expect(result.status).toBe(302);
60
+ expect(result.headers.Location).toBe(`myapp://cb?token=${encodeURIComponent(token)}`);
61
+ });
62
+
63
+ it("uses & when the URL already has a query string", () => {
64
+ const result = formatTokenResponse(token, user, "myapp://cb?foo=1", undefined, "query");
65
+ expect(result.headers.Location).toBe(`myapp://cb?foo=1&token=${encodeURIComponent(token)}`);
66
+ });
67
+
68
+ it("splices the query param BEFORE an existing fragment", () => {
69
+ const result = formatTokenResponse(token, user, "myapp://cb#state=xyz", undefined, "query");
70
+ expect(result.headers.Location).toBe(`myapp://cb?token=${encodeURIComponent(token)}#state=xyz`);
71
+ });
72
+
73
+ it("splices with & before fragment when query already present", () => {
74
+ const result = formatTokenResponse(token, user, "myapp://cb?foo=1#bar", undefined, "query");
75
+ expect(result.headers.Location).toBe(`myapp://cb?foo=1&token=${encodeURIComponent(token)}#bar`);
76
+ });
77
+
78
+ it("URL-encodes tokens with special characters", () => {
79
+ const weird = "a b+c/d=e";
80
+ const result = formatTokenResponse(weird, user, "myapp://cb", undefined, "query");
81
+ expect(result.headers.Location).toBe(`myapp://cb?token=${encodeURIComponent(weird)}`);
82
+ });
83
+ });
84
+ });
85
+ });
@@ -1,6 +1,7 @@
1
1
  import OidcProfile from "./schemas/OidcProfile";
2
2
  import OidcTokenSet from "./schemas/OidcTokenSet";
3
3
  import { OidcProviderConfig } from "./OidcProviderConfig";
4
+ import { TokenDeliveryMode } from "./utils/response-utils";
4
5
 
5
6
  /**
6
7
  * OIDC error information passed to onAuthError callback
@@ -41,7 +42,9 @@ export interface AuthSuccessCallbackResponse {
41
42
 
42
43
  /**
43
44
  * Optional redirect URL after authentication
44
- * Plugin will append #token=... to this URL
45
+ *
46
+ * The plugin will attach the JWT token to this URL according to the
47
+ * top-level `tokenDelivery` option (fragment by default, or query).
45
48
  */
46
49
  redirectUrl?: string;
47
50
  }
@@ -313,6 +316,39 @@ export interface OidcPluginOptions<TCtx = any> {
313
316
  */
314
317
  encryptionKey?: string;
315
318
 
319
+ /**
320
+ * Controls how the JWT token is delivered back to the client on the
321
+ * post-callback redirect (ignored when the caller uses `?response_type=json`).
322
+ *
323
+ * - `"fragment"` (default): appends `#token=...` to the redirectUrl. Best
324
+ * for web browsers — URL fragments are not sent to the server in HTTP
325
+ * requests, which preserves a small but real security property.
326
+ * - `"query"`: appends `?token=...` (or `&token=...`). Required for native
327
+ * mobile flows where the callback comes back via a custom URL scheme,
328
+ * because iOS `ASWebAuthenticationSession` strips URL fragments before
329
+ * delivering the URL to the app.
330
+ * - function: called once per callback to decide the delivery mode. Use
331
+ * this when one app serves both web and native clients. The function
332
+ * receives the resolved redirectUrl and the metadata bag from the
333
+ * initiate request, so the host can route by URL scheme, by
334
+ * `meta.intent`, or any other signal — the plugin makes no assumptions.
335
+ *
336
+ * Default: `"fragment"` (unchanged from prior versions).
337
+ *
338
+ * Example — route native (custom-scheme) clients to query, browsers to fragment:
339
+ * ```typescript
340
+ * tokenDelivery: ({ redirectUrl }) =>
341
+ * /^https?:/i.test(redirectUrl) ? "fragment" : "query",
342
+ * ```
343
+ */
344
+ tokenDelivery?:
345
+ | TokenDeliveryMode
346
+ | ((params: {
347
+ redirectUrl: string;
348
+ provider: string;
349
+ metadata: Record<string, string>;
350
+ }) => TokenDeliveryMode);
351
+
316
352
  /**
317
353
  * Whether to register OIDC routes automatically
318
354
  * If false, you must manually handle OIDC flow
@@ -269,8 +269,20 @@ const CallbackOidc: GetHandler<any, any, PathParams, CallbackRequest> = async ({
269
269
  const finalRedirectUrl = redirectUrl || session.redirectUri;
270
270
  oidcLog.debug(`Callback: auth complete, responding via ${response_type === "json" ? "JSON" : `redirect to "${finalRedirectUrl}"`}`);
271
271
 
272
+ // Resolve token delivery mode: literal, function, or default "fragment".
273
+ // The plugin stays a dumb transport — the host app makes the policy decision.
274
+ const tokenDeliverySetting = options.tokenDelivery ?? "fragment";
275
+ const resolvedTokenDelivery =
276
+ typeof tokenDeliverySetting === "function"
277
+ ? tokenDeliverySetting({
278
+ redirectUrl: finalRedirectUrl,
279
+ provider,
280
+ metadata: sessionMetadata,
281
+ })
282
+ : tokenDeliverySetting;
283
+
272
284
  // Return JWT token in requested format
273
- return formatTokenResponse(token, user, finalRedirectUrl, response_type);
285
+ return formatTokenResponse(token, user, finalRedirectUrl, response_type, resolvedTokenDelivery);
274
286
  } catch (error: any) {
275
287
  log.error("OIDC callback error:", error);
276
288
 
package/src/index.ts CHANGED
@@ -34,5 +34,5 @@ export { ProviderRegistry } from "./providers/ProviderRegistry";
34
34
  export { generateState, generateSessionId, generateNonce, validateState } from "./utils/state-utils";
35
35
  export { encryptToken, decryptToken, validateEncryptionSecret } from "./utils/encryption-utils";
36
36
  export { mapClaimsToProfile, extractCustomClaims } from "./utils/claims-mapper";
37
- export { formatTokenResponse } from "./utils/response-utils";
37
+ export { formatTokenResponse, TokenDeliveryMode } from "./utils/response-utils";
38
38
  export { createOidcError, validateProvider, handleProviderError, OidcErrorCodes } from "./utils/error-utils";
@@ -1,20 +1,38 @@
1
+ /**
2
+ * Controls how the JWT token is delivered back to the client on the
3
+ * post-callback redirect.
4
+ *
5
+ * - "fragment": appends `#token=...` to the redirectUrl. Best for web
6
+ * browsers — URL fragments are not sent to the server in HTTP requests
7
+ * and are only accessible to client-side JavaScript.
8
+ * - "query": appends `?token=...` (or `&token=...`). Required for native
9
+ * mobile flows where the callback comes back via a custom URL scheme,
10
+ * because iOS `ASWebAuthenticationSession` strips URL fragments before
11
+ * delivering the URL to the app.
12
+ */
13
+ export type TokenDeliveryMode = "fragment" | "query";
14
+
1
15
  /**
2
16
  * Format token response for the client
3
17
  *
4
18
  * Supports two response formats:
5
19
  * 1. JSON response (for API clients)
6
- * 2. Redirect with token in URL fragment (for web browsers)
7
- *
8
- * URL fragments are used for security - they are NOT sent to the server
9
- * in HTTP requests and are only accessible to client-side JavaScript.
20
+ * 2. Redirect with token in URL fragment or query (for web/native clients)
10
21
  *
11
22
  * @param token - JWT token for the application
12
23
  * @param user - User object
13
24
  * @param redirectUrl - URL to redirect to
14
25
  * @param responseType - "json" or undefined (redirect)
26
+ * @param tokenDelivery - How to attach the token on redirect. Defaults to "fragment".
15
27
  * @returns Flink response object
16
28
  */
17
- export function formatTokenResponse(token: string, user: any, redirectUrl: string, responseType?: "json"): any {
29
+ export function formatTokenResponse(
30
+ token: string,
31
+ user: any,
32
+ redirectUrl: string,
33
+ responseType?: "json",
34
+ tokenDelivery: TokenDeliveryMode = "fragment"
35
+ ): any {
18
36
  // JSON response for API clients
19
37
  if (responseType === "json") {
20
38
  return {
@@ -25,11 +43,8 @@ export function formatTokenResponse(token: string, user: any, redirectUrl: strin
25
43
  };
26
44
  }
27
45
 
28
- // Redirect response for web browsers
29
- // Token is in URL fragment (#token=...) for security
30
- const separator = redirectUrl.includes("#") ? "&" : "#";
31
- const tokenFragment = `token=${encodeURIComponent(token)}`;
32
- const finalUrl = `${redirectUrl}${separator}${tokenFragment}`;
46
+ const tokenParam = `token=${encodeURIComponent(token)}`;
47
+ const finalUrl = appendToken(redirectUrl, tokenParam, tokenDelivery);
33
48
 
34
49
  return {
35
50
  status: 302,
@@ -39,3 +54,31 @@ export function formatTokenResponse(token: string, user: any, redirectUrl: strin
39
54
  data: {},
40
55
  };
41
56
  }
57
+
58
+ /**
59
+ * Append a token parameter to a URL using the specified delivery mode.
60
+ *
61
+ * For "query" mode, any existing `#fragment` on the URL is preserved: the
62
+ * query parameter is spliced in *before* the fragment so the final URL is
63
+ * still well-formed.
64
+ *
65
+ * For "fragment" mode, a second token on an existing fragment is joined
66
+ * with `&` (same as the old behavior).
67
+ */
68
+ function appendToken(redirectUrl: string, tokenParam: string, mode: TokenDeliveryMode): string {
69
+ if (mode === "query") {
70
+ const hashIdx = redirectUrl.indexOf("#");
71
+ if (hashIdx >= 0) {
72
+ const base = redirectUrl.slice(0, hashIdx);
73
+ const frag = redirectUrl.slice(hashIdx);
74
+ const sep = base.includes("?") ? "&" : "?";
75
+ return `${base}${sep}${tokenParam}${frag}`;
76
+ }
77
+ const sep = redirectUrl.includes("?") ? "&" : "?";
78
+ return `${redirectUrl}${sep}${tokenParam}`;
79
+ }
80
+
81
+ // fragment (default)
82
+ const sep = redirectUrl.includes("#") ? "&" : "#";
83
+ return `${redirectUrl}${sep}${tokenParam}`;
84
+ }