@flink-app/oidc-plugin 2.0.0-alpha.122 → 2.0.0-alpha.123
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 +17 -0
- package/README.md +28 -0
- package/dist/OidcPlugin.d.ts.map +1 -1
- package/dist/OidcPlugin.js +13 -2
- package/dist/OidcPluginOptions.d.ts +15 -0
- package/dist/OidcPluginOptions.d.ts.map +1 -1
- package/dist/handlers/InitiateOidc.d.ts.map +1 -1
- package/dist/handlers/InitiateOidc.js +2 -1
- package/dist/utils/base-path-utils.d.ts +9 -0
- package/dist/utils/base-path-utils.d.ts.map +1 -0
- package/dist/utils/base-path-utils.js +20 -0
- package/package.json +6 -6
- package/spec/handlers/InitiateOidc.spec.ts +37 -0
- package/spec/plugin/OidcPlugin.spec.ts +52 -0
- package/src/OidcPlugin.ts +15 -2
- package/src/OidcPluginOptions.ts +16 -0
- package/src/handlers/InitiateOidc.ts +2 -1
- package/src/utils/base-path-utils.ts +20 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @flink-app/oidc-plugin
|
|
2
2
|
|
|
3
|
+
## 2.0.0-alpha.123
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- f69d88e: Add a `basePath` option to both plugins, and `allowedRedirectUris` to `oauth-plugin`.
|
|
8
|
+
|
|
9
|
+
**`basePath` (oauth-plugin, oidc-plugin):** prefix for the routes the plugin registers, for apps that keep every backend route under e.g. `/api` so an SPA fallback can own the rest. `basePath: "/api"` registers `/api/oauth/:provider/{initiate,callback}` and `/api/oidc/:provider/{initiate,callback}`. `"api"`, `"/api"` and `"/api/"` are equivalent. The OIDC auto-detected callback fallback includes the prefix, and the effective paths are logged at startup. Default `""` keeps the routes at the root.
|
|
10
|
+
|
|
11
|
+
**`allowedRedirectUris` (oauth-plugin):** ported from `oidc-plugin` with the same semantics. The `?redirectUri=` on `/oauth/:provider/initiate` is checked against the list (exact URIs, bare origins or a predicate); a mismatch answers `400 invalid_redirect_uri` and no session is created. Relative paths and the provider's `callbackUrl` are always accepted. The `redirectUrl` returned by `onAuthSuccess` / `onAuthError` is checked too when it differs from the session's. `AllowedRedirectUris`, `isRedirectUriAllowed` and `isRelativeRedirect` are exported.
|
|
12
|
+
|
|
13
|
+
Non-breaking: both options are optional. Without `allowedRedirectUris` every `redirectUri` is still accepted and the plugin logs a warning at startup. Existing `400` responses keep their `code` (`badRequest`); only the new `invalid_redirect_uri` rejection carries its own code.
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- @flink-app/flink@2.0.0-alpha.123
|
|
18
|
+
- @flink-app/jwt-auth-plugin@2.0.0-alpha.123
|
|
19
|
+
|
|
3
20
|
## 2.0.0-alpha.122
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -178,6 +178,34 @@ await app.start();
|
|
|
178
178
|
| `encryptionKey` | `string` | No | (derived) | Encryption key for tokens (32+ chars recommended) |
|
|
179
179
|
| `tokenDelivery` | `"fragment"` \| `"query"` \| `Function` | No | `"fragment"` | How the JWT is attached on the post-callback redirect (see [Token Delivery](#token-delivery-fragment-vs-query)) |
|
|
180
180
|
| `registerRoutes` | `boolean` | No | `true` | Auto-register OIDC routes |
|
|
181
|
+
| `basePath` | `string` | No | `""` | Prefix for the registered routes (see [Route Prefix](#route-prefix-basepath)) |
|
|
182
|
+
|
|
183
|
+
### Route Prefix (`basePath`)
|
|
184
|
+
|
|
185
|
+
Apps that serve an SPA and the API from the same origin usually keep every backend route under
|
|
186
|
+
a prefix (e.g. `/api`) so the SPA fallback can own everything else. `basePath` moves the
|
|
187
|
+
plugin's routes under that prefix:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
oidcPlugin({
|
|
191
|
+
basePath: "/api", // routes become /api/oidc/:provider/initiate and /api/oidc/:provider/callback
|
|
192
|
+
providers: {
|
|
193
|
+
acme: {
|
|
194
|
+
issuer: "https://idp.acme.com",
|
|
195
|
+
clientId: process.env.OIDC_CLIENT_ID!,
|
|
196
|
+
clientSecret: process.env.OIDC_CLIENT_SECRET!,
|
|
197
|
+
callbackUrl: "https://myapp.com/api/oidc/acme/callback", // include the prefix
|
|
198
|
+
discoveryUrl: "https://idp.acme.com/.well-known/openid-configuration",
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
// ...
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
- `"api"`, `"/api"` and `"/api/"` are equivalent; the default `""` keeps the routes at the root.
|
|
206
|
+
- The prefix is **not** added to a provider's `callbackUrl` for you — include it there and in the redirect URI registered at the IdP.
|
|
207
|
+
- The auto-detected callback fallback (used when a provider has no `callbackUrl`) includes the prefix.
|
|
208
|
+
- The effective paths are logged at startup, which makes a mismatched callback URL easy to spot.
|
|
181
209
|
|
|
182
210
|
### Provider Configuration
|
|
183
211
|
|
package/dist/OidcPlugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OidcPlugin.d.ts","sourceRoot":"","sources":["../src/OidcPlugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,WAAW,EAAO,MAAM,kBAAkB,CAAC;AAG9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"OidcPlugin.d.ts","sourceRoot":"","sources":["../src/OidcPlugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,WAAW,EAAO,MAAM,kBAAkB,CAAC;AAG9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAYxD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwEG;AACH,wBAAgB,UAAU,CAAC,IAAI,GAAG,GAAG,EAAE,OAAO,EAAE,iBAAiB,CAAC,IAAI,CAAC,GAAG,WAAW,CA4RpF"}
|
package/dist/OidcPlugin.js
CHANGED
|
@@ -32,6 +32,7 @@ const log_1 = require("./log");
|
|
|
32
32
|
const OidcSessionRepo_1 = __importDefault(require("./repos/OidcSessionRepo"));
|
|
33
33
|
const OidcConnectionRepo_1 = __importDefault(require("./repos/OidcConnectionRepo"));
|
|
34
34
|
const encryption_utils_1 = require("./utils/encryption-utils");
|
|
35
|
+
const base_path_utils_1 = require("./utils/base-path-utils");
|
|
35
36
|
const ProviderRegistry_1 = require("./providers/ProviderRegistry");
|
|
36
37
|
const InitiateOidc = __importStar(require("./handlers/InitiateOidc"));
|
|
37
38
|
const CallbackOidc = __importStar(require("./handlers/CallbackOidc"));
|
|
@@ -155,6 +156,13 @@ function oidcPlugin(options) {
|
|
|
155
156
|
else {
|
|
156
157
|
log_1.oidcLog.debug("Redirect URI allow-list: predicate function");
|
|
157
158
|
}
|
|
159
|
+
let basePath;
|
|
160
|
+
try {
|
|
161
|
+
basePath = (0, base_path_utils_1.normalizeBasePath)(options.basePath);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
throw new Error(`OIDC Plugin: ${error.message}`);
|
|
165
|
+
}
|
|
158
166
|
// Provider cache TTL (0 = cache for the life of the process, unchanged behaviour)
|
|
159
167
|
const providerCacheTtlSec = options.providerCacheTtlSec ?? 0;
|
|
160
168
|
if (typeof providerCacheTtlSec !== "number" || !Number.isFinite(providerCacheTtlSec) || providerCacheTtlSec < 0) {
|
|
@@ -222,8 +230,11 @@ function oidcPlugin(options) {
|
|
|
222
230
|
// Register OIDC handlers
|
|
223
231
|
// Only register handlers if registerRoutes is enabled (default: true)
|
|
224
232
|
if (options.registerRoutes !== false) {
|
|
225
|
-
|
|
226
|
-
|
|
233
|
+
const initiatePath = basePath + InitiateOidc.Route.path;
|
|
234
|
+
const callbackPath = basePath + CallbackOidc.Route.path;
|
|
235
|
+
flinkApp.addHandler(InitiateOidc, { path: initiatePath });
|
|
236
|
+
flinkApp.addHandler(CallbackOidc, { path: callbackPath });
|
|
237
|
+
flink_1.log.info(`OIDC Plugin: registered routes GET ${initiatePath} and GET ${callbackPath}`);
|
|
227
238
|
}
|
|
228
239
|
flink_1.log.info(`OIDC Plugin initialized${configuredProviders.length > 0 ? ` with providers: ${configuredProviders.join(", ")}` : " with dynamic provider loader"}`);
|
|
229
240
|
}
|
|
@@ -450,5 +450,20 @@ export interface OidcPluginOptions<TCtx = any> {
|
|
|
450
450
|
* or use a custom URL structure.
|
|
451
451
|
*/
|
|
452
452
|
registerRoutes?: boolean;
|
|
453
|
+
/**
|
|
454
|
+
* Path prefix for the routes registered by the plugin
|
|
455
|
+
*
|
|
456
|
+
* With `basePath: "/api"` the routes become `/api/oidc/:provider/initiate` and
|
|
457
|
+
* `/api/oidc/:provider/callback`. Useful when the app serves an SPA from the same
|
|
458
|
+
* origin and keeps every backend route under a prefix.
|
|
459
|
+
*
|
|
460
|
+
* Normalised: `"api"`, `"/api"` and `"/api/"` are equivalent. Remember to include the
|
|
461
|
+
* prefix in each provider's `callbackUrl` and in the redirect URI registered at the IdP.
|
|
462
|
+
* The auto-detected callback fallback (used when a provider has no `callbackUrl`)
|
|
463
|
+
* includes the prefix.
|
|
464
|
+
*
|
|
465
|
+
* Default: "" (routes at the root)
|
|
466
|
+
*/
|
|
467
|
+
basePath?: string;
|
|
453
468
|
}
|
|
454
469
|
//# sourceMappingURL=OidcPluginOptions.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OidcPluginOptions.d.ts","sourceRoot":"","sources":["../src/OidcPluginOptions.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"OidcPluginOptions.d.ts","sourceRoot":"","sources":["../src/OidcPluginOptions.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InitiateOidc.d.ts","sourceRoot":"","sources":["../../src/handlers/InitiateOidc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,UAAU,EAAc,UAAU,EAAmC,MAAM,kBAAkB,CAAC;AACvG,OAAO,eAAe,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"InitiateOidc.d.ts","sourceRoot":"","sources":["../../src/handlers/InitiateOidc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,UAAU,EAAc,UAAU,EAAmC,MAAM,kBAAkB,CAAC;AACvG,OAAO,eAAe,MAAM,4BAA4B,CAAC;AAQzD;;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,CA4FnE,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 redirect_uri_utils_1 = require("../utils/redirect-uri-utils");
|
|
20
|
+
const base_path_utils_1 = require("../utils/base-path-utils");
|
|
20
21
|
const openid_client_1 = require("openid-client");
|
|
21
22
|
const log_1 = require("../log");
|
|
22
23
|
/**
|
|
@@ -59,7 +60,7 @@ const InitiateOidc = async ({ ctx, req }) => {
|
|
|
59
60
|
log_1.oidcLog.debug(`Initiate: rejected redirectUri="${redirectUri}" (not in allowedRedirectUris)`);
|
|
60
61
|
throw (0, error_utils_1.createOidcError)(error_utils_1.OidcErrorCodes.INVALID_REDIRECT_URI, "The provided redirectUri is not allowed", { redirectUri, provider });
|
|
61
62
|
}
|
|
62
|
-
const finalRedirectUri = redirectUri || callbackUrl || `${req.protocol}://${req.get("host")}/oidc/${provider}/callback`;
|
|
63
|
+
const finalRedirectUri = redirectUri || callbackUrl || `${req.protocol}://${req.get("host")}${(0, base_path_utils_1.normalizeBasePath)(options.basePath)}/oidc/${provider}/callback`;
|
|
63
64
|
// Collect meta.* query params as opaque metadata (prefix stripped)
|
|
64
65
|
const metadata = {};
|
|
65
66
|
for (const [key, value] of Object.entries(req.query)) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalise the `basePath` plugin option into a route prefix.
|
|
3
|
+
*
|
|
4
|
+
* A leading slash is added and trailing slashes are stripped, so `"api"`, `"/api"` and
|
|
5
|
+
* `"/api/"` all become `"/api"`. An empty value (or `"/"`) yields `""`, i.e. routes stay
|
|
6
|
+
* at the root.
|
|
7
|
+
*/
|
|
8
|
+
export declare function normalizeBasePath(basePath?: string): string;
|
|
9
|
+
//# sourceMappingURL=base-path-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base-path-utils.d.ts","sourceRoot":"","sources":["../../src/utils/base-path-utils.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAY3D"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeBasePath = normalizeBasePath;
|
|
4
|
+
/**
|
|
5
|
+
* Normalise the `basePath` plugin option into a route prefix.
|
|
6
|
+
*
|
|
7
|
+
* A leading slash is added and trailing slashes are stripped, so `"api"`, `"/api"` and
|
|
8
|
+
* `"/api/"` all become `"/api"`. An empty value (or `"/"`) yields `""`, i.e. routes stay
|
|
9
|
+
* at the root.
|
|
10
|
+
*/
|
|
11
|
+
function normalizeBasePath(basePath) {
|
|
12
|
+
if (basePath === undefined || basePath === null) {
|
|
13
|
+
return "";
|
|
14
|
+
}
|
|
15
|
+
if (typeof basePath !== "string") {
|
|
16
|
+
throw new Error("basePath must be a string");
|
|
17
|
+
}
|
|
18
|
+
const trimmed = basePath.trim().replace(/^\/+/, "").replace(/\/+$/, "");
|
|
19
|
+
return trimmed ? `/${trimmed}` : "";
|
|
20
|
+
}
|
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.123",
|
|
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.123"
|
|
15
15
|
},
|
|
16
16
|
"peerDependencies": {
|
|
17
|
-
"@flink-app/flink": ">=2.0.0-alpha.
|
|
17
|
+
"@flink-app/flink": ">=2.0.0-alpha.123",
|
|
18
18
|
"mongodb": ">=3.7.0 <7.0.0"
|
|
19
19
|
},
|
|
20
20
|
"peerDependenciesMeta": {
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"@types/node": "22.13.10",
|
|
28
28
|
"ts-node": "^10.9.2",
|
|
29
29
|
"tsc-watch": "^4.2.9",
|
|
30
|
-
"@flink-app/
|
|
31
|
-
"@flink-app/
|
|
32
|
-
"@flink-app/
|
|
30
|
+
"@flink-app/flink": "2.0.0-alpha.123",
|
|
31
|
+
"@flink-app/test-utils": "2.0.0-alpha.123",
|
|
32
|
+
"@flink-app/jwt-auth-plugin": "2.0.0-alpha.123"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
35
|
"test": "jasmine-ts --config=./spec/support/jasmine.json",
|
|
@@ -153,4 +153,41 @@ describe("InitiateOidc handler", () => {
|
|
|
153
153
|
expect(sessions[0].redirectUri).toBe("https://api.example.com/oidc/test/callback");
|
|
154
154
|
});
|
|
155
155
|
});
|
|
156
|
+
|
|
157
|
+
describe("auto-detected callback fallback", () => {
|
|
158
|
+
async function initiateWithoutCallbackUrl(pluginOptions: any) {
|
|
159
|
+
const ctx = createMockCtx(
|
|
160
|
+
{
|
|
161
|
+
providers: {},
|
|
162
|
+
providerLoader: async () => null,
|
|
163
|
+
onAuthSuccess: async () => ({ user: {}, token: "jwt" }),
|
|
164
|
+
...pluginOptions,
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
config: {},
|
|
168
|
+
getAuthorizationUrl: async () => "https://test-idp.example.com/authorize",
|
|
169
|
+
}
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const res: any = await (InitiateOidc as any)({ ctx, req: createMockReq({ provider: "test" }, {}) });
|
|
173
|
+
|
|
174
|
+
return { res, sessions: ctx.repos.oidcSessionRepo.sessions };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
it("should build the fallback from the request host", async () => {
|
|
178
|
+
const { res, sessions } = await initiateWithoutCallbackUrl({});
|
|
179
|
+
expect(res.status).toBe(302);
|
|
180
|
+
expect(sessions[0].redirectUri).toBe("https://api.example.com/oidc/test/callback");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("should include basePath in the fallback", async () => {
|
|
184
|
+
const { sessions } = await initiateWithoutCallbackUrl({ basePath: "/api" });
|
|
185
|
+
expect(sessions[0].redirectUri).toBe("https://api.example.com/api/oidc/test/callback");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("should normalise basePath in the fallback", async () => {
|
|
189
|
+
const { sessions } = await initiateWithoutCallbackUrl({ basePath: "api/" });
|
|
190
|
+
expect(sessions[0].redirectUri).toBe("https://api.example.com/api/oidc/test/callback");
|
|
191
|
+
});
|
|
192
|
+
});
|
|
156
193
|
});
|
|
@@ -389,4 +389,56 @@ describe("OidcPlugin", () => {
|
|
|
389
389
|
}).toThrowError(/allowedRedirectUris must be an array of non-empty strings or a predicate function/);
|
|
390
390
|
});
|
|
391
391
|
});
|
|
392
|
+
|
|
393
|
+
describe("basePath option", () => {
|
|
394
|
+
const fakeDb = {
|
|
395
|
+
collection: () => ({ createIndex: async () => undefined }),
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
async function registeredPaths(options: any): Promise<string[]> {
|
|
399
|
+
const paths: string[] = [];
|
|
400
|
+
const app = {
|
|
401
|
+
ctx: {} as any,
|
|
402
|
+
addRepo: () => undefined,
|
|
403
|
+
addHandler: (handler: any, routePropsOverride?: any) => {
|
|
404
|
+
paths.push({ ...handler.Route, ...routePropsOverride }.path);
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const plugin = oidcPlugin({
|
|
409
|
+
providers: { test: createTestProviderConfig() },
|
|
410
|
+
onAuthSuccess: async () => ({ user: {}, token: "" }),
|
|
411
|
+
...options,
|
|
412
|
+
});
|
|
413
|
+
await plugin.init!(app as any, fakeDb as any);
|
|
414
|
+
|
|
415
|
+
return paths;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
it("should register routes at the root by default", async () => {
|
|
419
|
+
expect(await registeredPaths({})).toEqual(["/oidc/:provider/initiate", "/oidc/:provider/callback"]);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it("should register routes at the root for an empty basePath", async () => {
|
|
423
|
+
expect(await registeredPaths({ basePath: "" })).toEqual(["/oidc/:provider/initiate", "/oidc/:provider/callback"]);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it("should prefix routes with basePath", async () => {
|
|
427
|
+
expect(await registeredPaths({ basePath: "/api" })).toEqual(["/api/oidc/:provider/initiate", "/api/oidc/:provider/callback"]);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("should normalise basePath before prefixing", async () => {
|
|
431
|
+
expect(await registeredPaths({ basePath: "api/" })).toEqual(["/api/oidc/:provider/initiate", "/api/oidc/:provider/callback"]);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it("should throw for a non-string basePath", () => {
|
|
435
|
+
expect(() =>
|
|
436
|
+
oidcPlugin({
|
|
437
|
+
providers: { test: createTestProviderConfig() },
|
|
438
|
+
basePath: 1 as any,
|
|
439
|
+
onAuthSuccess: async () => ({ user: {}, token: "" }),
|
|
440
|
+
})
|
|
441
|
+
).toThrowError(/OIDC Plugin: basePath must be a string/);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
392
444
|
});
|
package/src/OidcPlugin.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { OidcInternalContext } from "./OidcInternalContext";
|
|
|
7
7
|
import OidcSessionRepo from "./repos/OidcSessionRepo";
|
|
8
8
|
import OidcConnectionRepo from "./repos/OidcConnectionRepo";
|
|
9
9
|
import { decryptToken, validateEncryptionSecret } from "./utils/encryption-utils";
|
|
10
|
+
import { normalizeBasePath } from "./utils/base-path-utils";
|
|
10
11
|
import OidcConnection from "./schemas/OidcConnection";
|
|
11
12
|
import { ProviderRegistry } from "./providers/ProviderRegistry";
|
|
12
13
|
import * as InitiateOidc from "./handlers/InitiateOidc";
|
|
@@ -148,6 +149,13 @@ export function oidcPlugin<TCtx = any>(options: OidcPluginOptions<TCtx>): FlinkP
|
|
|
148
149
|
oidcLog.debug("Redirect URI allow-list: predicate function");
|
|
149
150
|
}
|
|
150
151
|
|
|
152
|
+
let basePath: string;
|
|
153
|
+
try {
|
|
154
|
+
basePath = normalizeBasePath(options.basePath);
|
|
155
|
+
} catch (error: any) {
|
|
156
|
+
throw new Error(`OIDC Plugin: ${error.message}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
151
159
|
// Provider cache TTL (0 = cache for the life of the process, unchanged behaviour)
|
|
152
160
|
const providerCacheTtlSec = options.providerCacheTtlSec ?? 0;
|
|
153
161
|
|
|
@@ -242,8 +250,13 @@ export function oidcPlugin<TCtx = any>(options: OidcPluginOptions<TCtx>): FlinkP
|
|
|
242
250
|
// Register OIDC handlers
|
|
243
251
|
// Only register handlers if registerRoutes is enabled (default: true)
|
|
244
252
|
if (options.registerRoutes !== false) {
|
|
245
|
-
|
|
246
|
-
|
|
253
|
+
const initiatePath = basePath + InitiateOidc.Route.path;
|
|
254
|
+
const callbackPath = basePath + CallbackOidc.Route.path;
|
|
255
|
+
|
|
256
|
+
flinkApp.addHandler(InitiateOidc, { path: initiatePath });
|
|
257
|
+
flinkApp.addHandler(CallbackOidc, { path: callbackPath });
|
|
258
|
+
|
|
259
|
+
log.info(`OIDC Plugin: registered routes GET ${initiatePath} and GET ${callbackPath}`);
|
|
247
260
|
}
|
|
248
261
|
|
|
249
262
|
log.info(`OIDC Plugin initialized${configuredProviders.length > 0 ? ` with providers: ${configuredProviders.join(", ")}` : " with dynamic provider loader"}`);
|
package/src/OidcPluginOptions.ts
CHANGED
|
@@ -487,4 +487,20 @@ export interface OidcPluginOptions<TCtx = any> {
|
|
|
487
487
|
* or use a custom URL structure.
|
|
488
488
|
*/
|
|
489
489
|
registerRoutes?: boolean;
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Path prefix for the routes registered by the plugin
|
|
493
|
+
*
|
|
494
|
+
* With `basePath: "/api"` the routes become `/api/oidc/:provider/initiate` and
|
|
495
|
+
* `/api/oidc/:provider/callback`. Useful when the app serves an SPA from the same
|
|
496
|
+
* origin and keeps every backend route under a prefix.
|
|
497
|
+
*
|
|
498
|
+
* Normalised: `"api"`, `"/api"` and `"/api/"` are equivalent. Remember to include the
|
|
499
|
+
* prefix in each provider's `callbackUrl` and in the redirect URI registered at the IdP.
|
|
500
|
+
* The auto-detected callback fallback (used when a provider has no `callbackUrl`)
|
|
501
|
+
* includes the prefix.
|
|
502
|
+
*
|
|
503
|
+
* Default: "" (routes at the root)
|
|
504
|
+
*/
|
|
505
|
+
basePath?: string;
|
|
490
506
|
}
|
|
@@ -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 { isRedirectUriAllowed } from "../utils/redirect-uri-utils";
|
|
19
|
+
import { normalizeBasePath } from "../utils/base-path-utils";
|
|
19
20
|
import { generators } from "openid-client";
|
|
20
21
|
import { oidcLog } from "../log";
|
|
21
22
|
|
|
@@ -76,7 +77,7 @@ const InitiateOidc: GetHandler<any, any, PathParams, InitiateRequest> = async ({
|
|
|
76
77
|
throw createOidcError(OidcErrorCodes.INVALID_REDIRECT_URI, "The provided redirectUri is not allowed", { redirectUri, provider });
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
const finalRedirectUri = redirectUri || callbackUrl || `${req.protocol}://${req.get("host")}/oidc/${provider}/callback`;
|
|
80
|
+
const finalRedirectUri = redirectUri || callbackUrl || `${req.protocol}://${req.get("host")}${normalizeBasePath(options.basePath)}/oidc/${provider}/callback`;
|
|
80
81
|
|
|
81
82
|
// Collect meta.* query params as opaque metadata (prefix stripped)
|
|
82
83
|
const metadata: Record<string, string> = {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalise the `basePath` plugin option into a route prefix.
|
|
3
|
+
*
|
|
4
|
+
* A leading slash is added and trailing slashes are stripped, so `"api"`, `"/api"` and
|
|
5
|
+
* `"/api/"` all become `"/api"`. An empty value (or `"/"`) yields `""`, i.e. routes stay
|
|
6
|
+
* at the root.
|
|
7
|
+
*/
|
|
8
|
+
export function normalizeBasePath(basePath?: string): string {
|
|
9
|
+
if (basePath === undefined || basePath === null) {
|
|
10
|
+
return "";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (typeof basePath !== "string") {
|
|
14
|
+
throw new Error("basePath must be a string");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const trimmed = basePath.trim().replace(/^\/+/, "").replace(/\/+$/, "");
|
|
18
|
+
|
|
19
|
+
return trimmed ? `/${trimmed}` : "";
|
|
20
|
+
}
|