@flink-app/oidc-plugin 2.0.0-alpha.85 → 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 +8 -0
- package/dist/OidcPluginOptions.d.ts +34 -1
- package/dist/OidcPluginOptions.d.ts.map +1 -1
- package/dist/handlers/CallbackOidc.d.ts.map +1 -1
- package/dist/handlers/CallbackOidc.js +11 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/utils/response-utils.d.ts +16 -5
- package/dist/utils/response-utils.d.ts.map +1 -1
- package/dist/utils/response-utils.js +31 -10
- package/package.json +6 -6
- package/spec/utils/response-utils.spec.ts +85 -0
- package/src/OidcPluginOptions.ts +37 -1
- package/src/handlers/CallbackOidc.ts +13 -1
- package/src/index.ts +1 -1
- package/src/utils/response-utils.ts +53 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
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
|
+
|
|
3
11
|
## 2.0.0-alpha.85
|
|
4
12
|
|
|
5
13
|
### 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
|
-
*
|
|
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;
|
|
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;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAE3D;;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;;;;;OAKG;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;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,aAAa,CAAC,EACR,iBAAiB,GACjB,CAAC,CAAC,MAAM,EAAE;QACN,WAAW,EAAE,MAAM,CAAC;QACpB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACpC,KAAK,iBAAiB,CAAC,CAAC;IAE/B;;;;;;;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;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,
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
31
|
-
|
|
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.
|
|
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.
|
|
14
|
+
"@flink-app/jwt-auth-plugin": "2.0.0-alpha.86"
|
|
15
15
|
},
|
|
16
16
|
"peerDependencies": {
|
|
17
|
-
"@flink-app/flink": ">=2.0.0-alpha.
|
|
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/
|
|
31
|
-
"@flink-app/
|
|
32
|
-
"@flink-app/test-utils": "2.0.0-alpha.
|
|
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
|
+
});
|
package/src/OidcPluginOptions.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
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(
|
|
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
|
-
|
|
29
|
-
|
|
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
|
+
}
|