@camstack/addon-auth 1.2.131 → 1.2.133
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/assets/magic-link/icon.svg +5 -0
- package/dist/{hub-browser-origin-CCWSbHYb.js → dist-D5CZYR3c.js} +1928 -71
- package/dist/{hub-browser-origin-BRU-HVqq.mjs → dist-aH7jUuvS.mjs} +1929 -66
- package/dist/hub-browser-origin-DohQUFPz.js +16 -0
- package/dist/hub-browser-origin-LBBO5r05.mjs +11 -0
- package/dist/magic-link/auth-magic-link.addon.js +13 -12
- package/dist/magic-link/auth-magic-link.addon.mjs +3 -2
- package/dist/oidc/auth-oidc.addon.js +158 -19
- package/dist/oidc/auth-oidc.addon.mjs +149 -10
- package/dist/webauthn/_stub.js +1 -1
- package/dist/webauthn/{_virtual_mf-localSharedImportMap___mfe_internal__addon_auth_webauthn_widgets-DjJfFQA5.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_auth_webauthn_widgets-BwZ2AewG.mjs} +3 -3
- package/dist/webauthn/_virtual_mf___mfe_internal__addon_auth_webauthn_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-SLo1_Up_.mjs +26 -0
- package/dist/webauthn/auth-webauthn.addon.js +13 -12
- package/dist/webauthn/auth-webauthn.addon.mjs +2 -1
- package/dist/webauthn/{hostInit-BCMxziLA.mjs → hostInit-CXUOixLE.mjs} +3 -3
- package/dist/webauthn/remoteEntry.js +1 -1
- package/package.json +2 -1
- package/dist/webauthn/_virtual_mf___mfe_internal__addon_auth_webauthn_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CMFCCqdv.mjs +0 -26
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/hub-browser-origin.ts
|
|
2
|
+
async function hubBrowserOrigin(localNetwork, logger) {
|
|
3
|
+
try {
|
|
4
|
+
return (await localNetwork.getAddresses.query()).roles.publicEndpoint.resolved;
|
|
5
|
+
} catch (err) {
|
|
6
|
+
logger.warn("hub address unavailable from local-network — using the localhost default", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
Object.defineProperty(exports, "hubBrowserOrigin", {
|
|
12
|
+
enumerable: true,
|
|
13
|
+
get: function() {
|
|
14
|
+
return hubBrowserOrigin;
|
|
15
|
+
}
|
|
16
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/hub-browser-origin.ts
|
|
2
|
+
async function hubBrowserOrigin(localNetwork, logger) {
|
|
3
|
+
try {
|
|
4
|
+
return (await localNetwork.getAddresses.query()).roles.publicEndpoint.resolved;
|
|
5
|
+
} catch (err) {
|
|
6
|
+
logger.warn("hub address unavailable from local-network — using the localhost default", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
export { hubBrowserOrigin as t };
|
|
@@ -3,7 +3,8 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
require("../chunk-Cek0wNdY.js");
|
|
6
|
-
const
|
|
6
|
+
const require_dist = require("../dist-D5CZYR3c.js");
|
|
7
|
+
const require_hub_browser_origin = require("../hub-browser-origin-DohQUFPz.js");
|
|
7
8
|
//#region src/magic-link/retire-enabled-flag.ts
|
|
8
9
|
/** The retired settings key. */
|
|
9
10
|
var RETIRED_ENABLED_KEY = "enabled";
|
|
@@ -83,7 +84,7 @@ var DEFAULT_CONFIG = {
|
|
|
83
84
|
defaultRole: "viewer",
|
|
84
85
|
ttlSec: 600
|
|
85
86
|
};
|
|
86
|
-
var AuthMagicLinkAddon = class extends
|
|
87
|
+
var AuthMagicLinkAddon = class extends require_dist.BaseAddon {
|
|
87
88
|
enabledFlagRetired = false;
|
|
88
89
|
retirementTimer = null;
|
|
89
90
|
shuttingDown = false;
|
|
@@ -101,7 +102,7 @@ var AuthMagicLinkAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
101
102
|
throw new Error("Magic link does not use callback — the email link redirects to /api/auth/sso/finish directly");
|
|
102
103
|
}
|
|
103
104
|
};
|
|
104
|
-
const routeProvider =
|
|
105
|
+
const routeProvider = require_dist.buildAddonRouteProvider("auth-magic-link", [
|
|
105
106
|
{
|
|
106
107
|
method: "GET",
|
|
107
108
|
path: "/start",
|
|
@@ -130,15 +131,15 @@ var AuthMagicLinkAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
130
131
|
this.attemptEnabledFlagRetirement(0);
|
|
131
132
|
return [
|
|
132
133
|
{
|
|
133
|
-
capability:
|
|
134
|
+
capability: require_dist.authProviderCapability,
|
|
134
135
|
provider: authProvider
|
|
135
136
|
},
|
|
136
137
|
{
|
|
137
|
-
capability:
|
|
138
|
+
capability: require_dist.addonRoutesCapability,
|
|
138
139
|
provider: routeProvider
|
|
139
140
|
},
|
|
140
141
|
{
|
|
141
|
-
capability:
|
|
142
|
+
capability: require_dist.loginMethodCapability,
|
|
142
143
|
provider: loginMethodProvider
|
|
143
144
|
}
|
|
144
145
|
];
|
|
@@ -149,7 +150,7 @@ var AuthMagicLinkAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
149
150
|
kind: "redirect",
|
|
150
151
|
id: "auth-magic-link",
|
|
151
152
|
label: this.config.displayName || DEFAULT_CONFIG.displayName,
|
|
152
|
-
icon:
|
|
153
|
+
icon: require_dist.isDisplayIconKey(this.config.icon) ? this.config.icon : DEFAULT_CONFIG.icon,
|
|
153
154
|
startUrl: "/addon/auth-magic-link/start",
|
|
154
155
|
stage: "primary"
|
|
155
156
|
}];
|
|
@@ -184,7 +185,7 @@ var AuthMagicLinkAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
184
185
|
} catch (err) {
|
|
185
186
|
outcome = {
|
|
186
187
|
kind: "deferred",
|
|
187
|
-
reason:
|
|
188
|
+
reason: require_dist.errMsg(err)
|
|
188
189
|
};
|
|
189
190
|
}
|
|
190
191
|
if (outcome.kind !== "deferred") {
|
|
@@ -282,7 +283,7 @@ var AuthMagicLinkAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
282
283
|
try {
|
|
283
284
|
users = await this.ctx.api.userManagement.listUsers.query();
|
|
284
285
|
} catch (err) {
|
|
285
|
-
this.ctx.logger.warn("Magic-link user lookup failed (user-management cap unavailable)", { meta: { message:
|
|
286
|
+
this.ctx.logger.warn("Magic-link user lookup failed (user-management cap unavailable)", { meta: { message: require_dist.errMsg(err) } });
|
|
286
287
|
return null;
|
|
287
288
|
}
|
|
288
289
|
const needle = email.trim().toLowerCase();
|
|
@@ -326,7 +327,7 @@ var AuthMagicLinkAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
326
327
|
<input id="email" name="email" type="email" autocomplete="email" required autofocus placeholder="you@example.com">
|
|
327
328
|
<button id="b" type="submit">Send login link</button>
|
|
328
329
|
<div class="msg" id="m" hidden></div>
|
|
329
|
-
<p style="margin-top:1rem"><a href="/
|
|
330
|
+
<p style="margin-top:1rem"><a href="/login">Back to sign in</a></p>
|
|
330
331
|
</form>
|
|
331
332
|
<script>
|
|
332
333
|
var f=document.getElementById('f'),b=document.getElementById('b'),m=document.getElementById('m');
|
|
@@ -371,7 +372,7 @@ var AuthMagicLinkAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
371
372
|
ttlSec: this.config.ttlSec || DEFAULT_CONFIG.ttlSec
|
|
372
373
|
}));
|
|
373
374
|
} catch (err) {
|
|
374
|
-
this.ctx.logger.error("Magic-link bridge-token minting failed (sso-bridge unavailable)", { meta: { message:
|
|
375
|
+
this.ctx.logger.error("Magic-link bridge-token minting failed (sso-bridge unavailable)", { meta: { message: require_dist.errMsg(err) } });
|
|
375
376
|
reply.code(503);
|
|
376
377
|
reply.send({ error: "sso-bridge capability unavailable" });
|
|
377
378
|
return;
|
|
@@ -392,7 +393,7 @@ var AuthMagicLinkAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
392
393
|
reply.code(500);
|
|
393
394
|
reply.send({
|
|
394
395
|
error: "Magic-link request failed",
|
|
395
|
-
message:
|
|
396
|
+
message: require_dist.errMsg(err)
|
|
396
397
|
});
|
|
397
398
|
}
|
|
398
399
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as isDisplayIconKey, c as errMsg, i as buildAddonRouteProvider, l as BaseAddon, o as loginMethodCapability, r as authProviderCapability, t as addonRoutesCapability } from "../dist-aH7jUuvS.mjs";
|
|
2
|
+
import { t as hubBrowserOrigin } from "../hub-browser-origin-LBBO5r05.mjs";
|
|
2
3
|
//#region src/magic-link/retire-enabled-flag.ts
|
|
3
4
|
/** The retired settings key. */
|
|
4
5
|
var RETIRED_ENABLED_KEY = "enabled";
|
|
@@ -321,7 +322,7 @@ var AuthMagicLinkAddon = class extends BaseAddon {
|
|
|
321
322
|
<input id="email" name="email" type="email" autocomplete="email" required autofocus placeholder="you@example.com">
|
|
322
323
|
<button id="b" type="submit">Send login link</button>
|
|
323
324
|
<div class="msg" id="m" hidden></div>
|
|
324
|
-
<p style="margin-top:1rem"><a href="/
|
|
325
|
+
<p style="margin-top:1rem"><a href="/login">Back to sign in</a></p>
|
|
325
326
|
</form>
|
|
326
327
|
<script>
|
|
327
328
|
var f=document.getElementById('f'),b=document.getElementById('b'),m=document.getElementById('m');
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../chunk-Cek0wNdY.js");
|
|
6
|
-
const
|
|
6
|
+
const require_dist = require("../dist-D5CZYR3c.js");
|
|
7
7
|
let node_crypto = require("node:crypto");
|
|
8
8
|
node_crypto = require_chunk.__toESM(node_crypto);
|
|
9
9
|
//#region ../../node_modules/jose/dist/webapi/lib/buffer_utils.js
|
|
@@ -705,6 +705,82 @@ function createRemoteJWKSet(url, options) {
|
|
|
705
705
|
});
|
|
706
706
|
}
|
|
707
707
|
//#endregion
|
|
708
|
+
//#region src/oidc/request-origin.ts
|
|
709
|
+
/** host[:port] — a DNS name, IPv4 or bracketed IPv6. No userinfo, path or spaces. */
|
|
710
|
+
var AUTHORITY_RE = /^(?:[a-z0-9.-]+|\[[0-9a-f:.]+\])(?::\d{1,5})?$/i;
|
|
711
|
+
/** Same grammar as the hub's `parseForwardedPrefix` (spa-mount.ts). */
|
|
712
|
+
var PREFIX_RE = /^\/[A-Za-z0-9_\-.]+(?:\/[A-Za-z0-9_\-.]+)*$/;
|
|
713
|
+
var MAX_PREFIX_LENGTH = 512;
|
|
714
|
+
/** First value of a possibly comma-joined forwarded header (the client-most proxy). */
|
|
715
|
+
function firstValue(value) {
|
|
716
|
+
if (value === void 0) return null;
|
|
717
|
+
const first = value.split(",")[0]?.trim() ?? "";
|
|
718
|
+
return first.length === 0 ? null : first;
|
|
719
|
+
}
|
|
720
|
+
/** `scheme://host[:port]` normalised by WHATWG URL, or `null` if it is not an http(s) origin. */
|
|
721
|
+
function normalizeOrigin(value) {
|
|
722
|
+
try {
|
|
723
|
+
const url = new URL(value);
|
|
724
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return null;
|
|
725
|
+
if (url.username !== "" || url.password !== "") return null;
|
|
726
|
+
return url.origin;
|
|
727
|
+
} catch {
|
|
728
|
+
return null;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
/** The mount prefix the relay named (no trailing slash), or `''`. Invalid = absent. */
|
|
732
|
+
function requestMountPrefix(headers) {
|
|
733
|
+
const raw = headers["x-forwarded-prefix"];
|
|
734
|
+
if (raw === void 0 || raw.length === 0 || raw.length > MAX_PREFIX_LENGTH) return "";
|
|
735
|
+
const trimmed = raw.endsWith("/") ? raw.slice(0, -1) : raw;
|
|
736
|
+
if (trimmed === "" || !PREFIX_RE.test(trimmed)) return "";
|
|
737
|
+
if (trimmed.split("/").some((segment) => segment === ".." || segment === ".")) return "";
|
|
738
|
+
return trimmed;
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* The origins the request CLAIMS, most specific first: the proxy's
|
|
742
|
+
* `X-Forwarded-Host` over `Host`; the named `X-Forwarded-Proto`, or https
|
|
743
|
+
* then http when none is named (the hub terminates TLS itself on 4443).
|
|
744
|
+
*/
|
|
745
|
+
function claimedRequestOrigins(headers) {
|
|
746
|
+
const authority = firstValue(headers["x-forwarded-host"]) ?? firstValue(headers["host"]);
|
|
747
|
+
if (authority === null || !AUTHORITY_RE.test(authority)) return [];
|
|
748
|
+
const proto = firstValue(headers["x-forwarded-proto"])?.toLowerCase() ?? null;
|
|
749
|
+
const schemes = proto === "https" || proto === "http" ? [proto] : proto === null ? ["https", "http"] : [];
|
|
750
|
+
const out = [];
|
|
751
|
+
for (const scheme of schemes) {
|
|
752
|
+
const origin = normalizeOrigin(`${scheme}://${authority}`);
|
|
753
|
+
if (origin !== null) out.push(origin);
|
|
754
|
+
}
|
|
755
|
+
return out;
|
|
756
|
+
}
|
|
757
|
+
/** Every origin the hub advertises itself at, normalised. Never the app link, never loopback. */
|
|
758
|
+
function advertisedHubOrigins(book) {
|
|
759
|
+
const candidates = [];
|
|
760
|
+
for (const row of book.rows) {
|
|
761
|
+
if (row.kind === "app-link" || row.kind === "loopback") continue;
|
|
762
|
+
candidates.push(row.baseUrl);
|
|
763
|
+
}
|
|
764
|
+
const { publicEndpoint, internal, appConnections } = book.roles;
|
|
765
|
+
if (publicEndpoint.resolved !== null) candidates.push(publicEndpoint.resolved);
|
|
766
|
+
if (internal.resolved !== null) candidates.push(internal.resolved);
|
|
767
|
+
candidates.push(...appConnections.resolved);
|
|
768
|
+
const out = /* @__PURE__ */ new Set();
|
|
769
|
+
for (const candidate of candidates) {
|
|
770
|
+
const origin = normalizeOrigin(candidate);
|
|
771
|
+
if (origin !== null) out.add(origin);
|
|
772
|
+
}
|
|
773
|
+
return out;
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* The origin to send the browser back to: the first claimed origin that is in
|
|
777
|
+
* `allowed`, or `null` (the caller then uses its default address).
|
|
778
|
+
*/
|
|
779
|
+
function pickRequestOrigin(headers, allowed) {
|
|
780
|
+
for (const origin of claimedRequestOrigins(headers)) if (allowed.has(origin)) return origin;
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
//#endregion
|
|
708
784
|
//#region src/oidc/auth-oidc.addon.ts
|
|
709
785
|
/**
|
|
710
786
|
* Generic OpenID Connect (OIDC) authentication provider.
|
|
@@ -771,7 +847,20 @@ var LINK_INTENT_PATTERN = /^[A-Za-z0-9_-]{16,128}$/;
|
|
|
771
847
|
var INSTANCE_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
772
848
|
/** Bound on in-flight logins: `/start` is public, so a flood must not grow the map. */
|
|
773
849
|
var MAX_PENDING_STATES = 1e3;
|
|
774
|
-
|
|
850
|
+
/**
|
|
851
|
+
* Origin + mount prefix of a pinned `redirect_uri`: the part before the
|
|
852
|
+
* callback path when it ends with it, else just its origin.
|
|
853
|
+
*/
|
|
854
|
+
function returnBaseOf(redirectUri, callbackPath) {
|
|
855
|
+
try {
|
|
856
|
+
const url = new URL(redirectUri);
|
|
857
|
+
const prefix = url.pathname.endsWith(callbackPath) ? url.pathname.slice(0, -callbackPath.length) : "";
|
|
858
|
+
return `${url.origin}${prefix.replace(/\/+$/, "")}`;
|
|
859
|
+
} catch {
|
|
860
|
+
return "";
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
var AuthOidcAddon = class extends require_dist.BaseAddon {
|
|
775
864
|
/** Per-provider runtime state, keyed by `provider.id`. */
|
|
776
865
|
instances = /* @__PURE__ */ new Map();
|
|
777
866
|
/** Pending OAuth states across ALL providers. The pending entry
|
|
@@ -797,7 +886,7 @@ var AuthOidcAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
797
886
|
return this.handleCallback(code, state);
|
|
798
887
|
}
|
|
799
888
|
};
|
|
800
|
-
const routeProvider =
|
|
889
|
+
const routeProvider = require_dist.buildAddonRouteProvider("auth-oidc", [{
|
|
801
890
|
method: "GET",
|
|
802
891
|
path: "/:providerId/start",
|
|
803
892
|
access: "public",
|
|
@@ -814,15 +903,15 @@ var AuthOidcAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
814
903
|
this.ctx.logger.info("OIDC auth provider initialized", { meta: { providers: [...this.instances.keys()] } });
|
|
815
904
|
return [
|
|
816
905
|
{
|
|
817
|
-
capability:
|
|
906
|
+
capability: require_dist.authProviderCapability,
|
|
818
907
|
provider: authProvider
|
|
819
908
|
},
|
|
820
909
|
{
|
|
821
|
-
capability:
|
|
910
|
+
capability: require_dist.addonRoutesCapability,
|
|
822
911
|
provider: routeProvider
|
|
823
912
|
},
|
|
824
913
|
{
|
|
825
|
-
capability:
|
|
914
|
+
capability: require_dist.loginMethodCapability,
|
|
826
915
|
provider: loginMethodProvider
|
|
827
916
|
}
|
|
828
917
|
];
|
|
@@ -840,7 +929,7 @@ var AuthOidcAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
840
929
|
kind: "redirect",
|
|
841
930
|
id: `auth-oidc/${id}`,
|
|
842
931
|
label: inst.config.displayName || DEFAULT_PROVIDER.displayName,
|
|
843
|
-
icon:
|
|
932
|
+
icon: require_dist.isDisplayIconKey(inst.config.icon) ? inst.config.icon : DEFAULT_PROVIDER.icon,
|
|
844
933
|
startUrl: `/addon/auth-oidc/${id}/start`,
|
|
845
934
|
stage: "primary",
|
|
846
935
|
linkable: true
|
|
@@ -905,7 +994,7 @@ var AuthOidcAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
905
994
|
type: "text",
|
|
906
995
|
key: "redirectUri",
|
|
907
996
|
label: "Redirect URI override",
|
|
908
|
-
description: "Optional.
|
|
997
|
+
description: "Optional — leave empty. Empty: a sign-in returns to the hub address it was started from (LAN, mesh or public, as listed on the Network page), so register one callback per address you sign in from at the IdP. Set: it PINS that one callback — every sign-in returns to its address, wherever it started.",
|
|
909
998
|
default: ""
|
|
910
999
|
},
|
|
911
1000
|
{
|
|
@@ -983,21 +1072,60 @@ var AuthOidcAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
983
1072
|
}
|
|
984
1073
|
return doc;
|
|
985
1074
|
}
|
|
986
|
-
|
|
1075
|
+
/** The hub's Network-page address book, or `null` when `local-network` is unreachable. */
|
|
1076
|
+
async addressBook() {
|
|
1077
|
+
try {
|
|
1078
|
+
return await this.ctx.api.localNetwork.getAddresses.query({ scheme: "https" });
|
|
1079
|
+
} catch (err) {
|
|
1080
|
+
this.ctx.logger.warn("OIDC: hub addresses unavailable from local-network", { meta: { error: require_dist.errMsg(err) } });
|
|
1081
|
+
return null;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* The `redirect_uri` of one sign-in, and the base the browser returns to
|
|
1086
|
+
* after the callback (D791). Precedence:
|
|
1087
|
+
*
|
|
1088
|
+
* 1. The instance's "Redirect URI override", when set: it PINS one origin —
|
|
1089
|
+
* every sign-in returns there, wherever it started.
|
|
1090
|
+
* 2. The origin the `/start` request was made at (Host / X-Forwarded-Host /
|
|
1091
|
+
* -Proto + mount prefix), ONLY when the hub advertises that origin
|
|
1092
|
+
* (`request-origin.ts`) — a forged Host never reaches the IdP.
|
|
1093
|
+
* 3. The Network page's public endpoint (`roles.publicEndpoint`), else the
|
|
1094
|
+
* development default.
|
|
1095
|
+
*/
|
|
1096
|
+
async resolveRedirect(providerId, headers) {
|
|
987
1097
|
const inst = this.instances.get(providerId);
|
|
988
1098
|
if (!inst) throw new Error(`OIDC: no provider configured with id "${providerId}"`);
|
|
1099
|
+
const callbackPath = `/addon/auth-oidc/${providerId}/callback`;
|
|
989
1100
|
const configured = inst.config.redirectUri?.trim();
|
|
990
|
-
if (configured) return
|
|
991
|
-
|
|
1101
|
+
if (configured) return {
|
|
1102
|
+
redirectUri: configured,
|
|
1103
|
+
returnBase: returnBaseOf(configured, callbackPath)
|
|
1104
|
+
};
|
|
1105
|
+
const book = await this.addressBook();
|
|
1106
|
+
const fromRequest = headers !== null && book !== null ? pickRequestOrigin(headers, advertisedHubOrigins(book)) : null;
|
|
1107
|
+
if (fromRequest !== null) {
|
|
1108
|
+
const base = `${fromRequest}${requestMountPrefix(headers ?? {})}`;
|
|
1109
|
+
return {
|
|
1110
|
+
redirectUri: `${base}${callbackPath}`,
|
|
1111
|
+
returnBase: base
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
const fallback = (book !== null ? normalizeOrigin(book.roles.publicEndpoint.resolved ?? "") : null) ?? "https://localhost:4443";
|
|
1115
|
+
return {
|
|
1116
|
+
redirectUri: `${fallback}${callbackPath}`,
|
|
1117
|
+
returnBase: fallback
|
|
1118
|
+
};
|
|
992
1119
|
}
|
|
993
|
-
async buildLoginUrl(providerId, state,
|
|
1120
|
+
async buildLoginUrl(providerId, state, options = {}) {
|
|
1121
|
+
const { linkIntent } = options;
|
|
994
1122
|
const inst = this.instances.get(providerId);
|
|
995
1123
|
if (!inst) throw new Error(`OIDC: no provider configured with id "${providerId}"`);
|
|
996
1124
|
const doc = await this.getDiscovery(providerId);
|
|
997
1125
|
const codeVerifier = node_crypto.randomBytes(32).toString("base64url");
|
|
998
1126
|
const codeChallenge = node_crypto.createHash("sha256").update(codeVerifier).digest("base64url");
|
|
999
1127
|
const nonce = node_crypto.randomBytes(16).toString("base64url");
|
|
1000
|
-
const redirectUri = await this.
|
|
1128
|
+
const { redirectUri, returnBase } = await this.resolveRedirect(providerId, options.headers ?? null);
|
|
1001
1129
|
this.pruneExpiredStates();
|
|
1002
1130
|
while (this.pendingStates.size >= MAX_PENDING_STATES) {
|
|
1003
1131
|
const oldest = this.pendingStates.keys().next();
|
|
@@ -1009,6 +1137,8 @@ var AuthOidcAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
1009
1137
|
nonce,
|
|
1010
1138
|
providerId,
|
|
1011
1139
|
redirectUri,
|
|
1140
|
+
returnBase,
|
|
1141
|
+
popup: options.popup === true,
|
|
1012
1142
|
createdAt: Date.now(),
|
|
1013
1143
|
...linkIntent !== void 0 ? { linkIntent } : {}
|
|
1014
1144
|
});
|
|
@@ -1065,7 +1195,7 @@ var AuthOidcAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
1065
1195
|
audience: inst.config.clientId
|
|
1066
1196
|
})).payload;
|
|
1067
1197
|
} catch (err) {
|
|
1068
|
-
throw new Error(`OIDC id_token verification failed: ${
|
|
1198
|
+
throw new Error(`OIDC id_token verification failed: ${require_dist.errMsg(err)}`, { cause: err });
|
|
1069
1199
|
}
|
|
1070
1200
|
if (typeof payload["nonce"] !== "string" || payload["nonce"] !== pending.nonce) throw new Error("OIDC id_token nonce mismatch — possible replay attack");
|
|
1071
1201
|
const sub = typeof payload.sub === "string" ? payload.sub : null;
|
|
@@ -1083,7 +1213,9 @@ var AuthOidcAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
1083
1213
|
isAdmin: false,
|
|
1084
1214
|
...pending.linkIntent !== void 0 ? { linkIntent: pending.linkIntent } : {},
|
|
1085
1215
|
issuer: doc.issuer,
|
|
1086
|
-
providerId
|
|
1216
|
+
providerId,
|
|
1217
|
+
returnBase: pending.returnBase,
|
|
1218
|
+
popup: pending.popup
|
|
1087
1219
|
};
|
|
1088
1220
|
}
|
|
1089
1221
|
validateLocalSessionToken(_token) {
|
|
@@ -1102,9 +1234,14 @@ var AuthOidcAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
1102
1234
|
reply.send({ error: "Malformed link request" });
|
|
1103
1235
|
return;
|
|
1104
1236
|
}
|
|
1237
|
+
const popupParam = req.query["popup"];
|
|
1105
1238
|
const state = node_crypto.randomBytes(16).toString("base64url");
|
|
1106
1239
|
try {
|
|
1107
|
-
const url = await this.buildLoginUrl(providerId, state,
|
|
1240
|
+
const url = await this.buildLoginUrl(providerId, state, {
|
|
1241
|
+
...linkParam !== void 0 ? { linkIntent: linkParam } : {},
|
|
1242
|
+
headers: req.headers,
|
|
1243
|
+
popup: popupParam === "1"
|
|
1244
|
+
});
|
|
1108
1245
|
reply.code(302);
|
|
1109
1246
|
reply.header("Location", url);
|
|
1110
1247
|
reply.send("");
|
|
@@ -1112,7 +1249,7 @@ var AuthOidcAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
1112
1249
|
reply.code(500);
|
|
1113
1250
|
reply.send({
|
|
1114
1251
|
error: "OIDC start failed",
|
|
1115
|
-
message:
|
|
1252
|
+
message: require_dist.errMsg(err)
|
|
1116
1253
|
});
|
|
1117
1254
|
}
|
|
1118
1255
|
}
|
|
@@ -1158,14 +1295,16 @@ var AuthOidcAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
1158
1295
|
...result.linkIntent !== void 0 ? { linkIntent: result.linkIntent } : {},
|
|
1159
1296
|
issuer: result.issuer
|
|
1160
1297
|
} });
|
|
1298
|
+
const finish = new URLSearchParams({ bridge: bridgeToken });
|
|
1299
|
+
if (result.popup) finish.set("popup", "1");
|
|
1161
1300
|
reply.code(302);
|
|
1162
|
-
reply.header("Location",
|
|
1301
|
+
reply.header("Location", `${result.returnBase}/api/auth/sso/finish?${finish.toString()}`);
|
|
1163
1302
|
reply.send("");
|
|
1164
1303
|
} catch (err) {
|
|
1165
1304
|
reply.code(500);
|
|
1166
1305
|
reply.send({
|
|
1167
1306
|
error: "OIDC callback failed",
|
|
1168
|
-
message:
|
|
1307
|
+
message: require_dist.errMsg(err)
|
|
1169
1308
|
});
|
|
1170
1309
|
}
|
|
1171
1310
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as isDisplayIconKey, c as errMsg, i as buildAddonRouteProvider, l as BaseAddon, o as loginMethodCapability, r as authProviderCapability, t as addonRoutesCapability } from "../dist-aH7jUuvS.mjs";
|
|
2
2
|
import * as crypto$1 from "node:crypto";
|
|
3
3
|
//#region ../../node_modules/jose/dist/webapi/lib/buffer_utils.js
|
|
4
4
|
var encoder = new TextEncoder(), decoder = new TextDecoder(), strictDecoder = new TextDecoder("utf-8", { fatal: !0 });
|
|
@@ -699,6 +699,82 @@ function createRemoteJWKSet(url, options) {
|
|
|
699
699
|
});
|
|
700
700
|
}
|
|
701
701
|
//#endregion
|
|
702
|
+
//#region src/oidc/request-origin.ts
|
|
703
|
+
/** host[:port] — a DNS name, IPv4 or bracketed IPv6. No userinfo, path or spaces. */
|
|
704
|
+
var AUTHORITY_RE = /^(?:[a-z0-9.-]+|\[[0-9a-f:.]+\])(?::\d{1,5})?$/i;
|
|
705
|
+
/** Same grammar as the hub's `parseForwardedPrefix` (spa-mount.ts). */
|
|
706
|
+
var PREFIX_RE = /^\/[A-Za-z0-9_\-.]+(?:\/[A-Za-z0-9_\-.]+)*$/;
|
|
707
|
+
var MAX_PREFIX_LENGTH = 512;
|
|
708
|
+
/** First value of a possibly comma-joined forwarded header (the client-most proxy). */
|
|
709
|
+
function firstValue(value) {
|
|
710
|
+
if (value === void 0) return null;
|
|
711
|
+
const first = value.split(",")[0]?.trim() ?? "";
|
|
712
|
+
return first.length === 0 ? null : first;
|
|
713
|
+
}
|
|
714
|
+
/** `scheme://host[:port]` normalised by WHATWG URL, or `null` if it is not an http(s) origin. */
|
|
715
|
+
function normalizeOrigin(value) {
|
|
716
|
+
try {
|
|
717
|
+
const url = new URL(value);
|
|
718
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return null;
|
|
719
|
+
if (url.username !== "" || url.password !== "") return null;
|
|
720
|
+
return url.origin;
|
|
721
|
+
} catch {
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
/** The mount prefix the relay named (no trailing slash), or `''`. Invalid = absent. */
|
|
726
|
+
function requestMountPrefix(headers) {
|
|
727
|
+
const raw = headers["x-forwarded-prefix"];
|
|
728
|
+
if (raw === void 0 || raw.length === 0 || raw.length > MAX_PREFIX_LENGTH) return "";
|
|
729
|
+
const trimmed = raw.endsWith("/") ? raw.slice(0, -1) : raw;
|
|
730
|
+
if (trimmed === "" || !PREFIX_RE.test(trimmed)) return "";
|
|
731
|
+
if (trimmed.split("/").some((segment) => segment === ".." || segment === ".")) return "";
|
|
732
|
+
return trimmed;
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* The origins the request CLAIMS, most specific first: the proxy's
|
|
736
|
+
* `X-Forwarded-Host` over `Host`; the named `X-Forwarded-Proto`, or https
|
|
737
|
+
* then http when none is named (the hub terminates TLS itself on 4443).
|
|
738
|
+
*/
|
|
739
|
+
function claimedRequestOrigins(headers) {
|
|
740
|
+
const authority = firstValue(headers["x-forwarded-host"]) ?? firstValue(headers["host"]);
|
|
741
|
+
if (authority === null || !AUTHORITY_RE.test(authority)) return [];
|
|
742
|
+
const proto = firstValue(headers["x-forwarded-proto"])?.toLowerCase() ?? null;
|
|
743
|
+
const schemes = proto === "https" || proto === "http" ? [proto] : proto === null ? ["https", "http"] : [];
|
|
744
|
+
const out = [];
|
|
745
|
+
for (const scheme of schemes) {
|
|
746
|
+
const origin = normalizeOrigin(`${scheme}://${authority}`);
|
|
747
|
+
if (origin !== null) out.push(origin);
|
|
748
|
+
}
|
|
749
|
+
return out;
|
|
750
|
+
}
|
|
751
|
+
/** Every origin the hub advertises itself at, normalised. Never the app link, never loopback. */
|
|
752
|
+
function advertisedHubOrigins(book) {
|
|
753
|
+
const candidates = [];
|
|
754
|
+
for (const row of book.rows) {
|
|
755
|
+
if (row.kind === "app-link" || row.kind === "loopback") continue;
|
|
756
|
+
candidates.push(row.baseUrl);
|
|
757
|
+
}
|
|
758
|
+
const { publicEndpoint, internal, appConnections } = book.roles;
|
|
759
|
+
if (publicEndpoint.resolved !== null) candidates.push(publicEndpoint.resolved);
|
|
760
|
+
if (internal.resolved !== null) candidates.push(internal.resolved);
|
|
761
|
+
candidates.push(...appConnections.resolved);
|
|
762
|
+
const out = /* @__PURE__ */ new Set();
|
|
763
|
+
for (const candidate of candidates) {
|
|
764
|
+
const origin = normalizeOrigin(candidate);
|
|
765
|
+
if (origin !== null) out.add(origin);
|
|
766
|
+
}
|
|
767
|
+
return out;
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* The origin to send the browser back to: the first claimed origin that is in
|
|
771
|
+
* `allowed`, or `null` (the caller then uses its default address).
|
|
772
|
+
*/
|
|
773
|
+
function pickRequestOrigin(headers, allowed) {
|
|
774
|
+
for (const origin of claimedRequestOrigins(headers)) if (allowed.has(origin)) return origin;
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
777
|
+
//#endregion
|
|
702
778
|
//#region src/oidc/auth-oidc.addon.ts
|
|
703
779
|
/**
|
|
704
780
|
* Generic OpenID Connect (OIDC) authentication provider.
|
|
@@ -765,6 +841,19 @@ var LINK_INTENT_PATTERN = /^[A-Za-z0-9_-]{16,128}$/;
|
|
|
765
841
|
var INSTANCE_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
766
842
|
/** Bound on in-flight logins: `/start` is public, so a flood must not grow the map. */
|
|
767
843
|
var MAX_PENDING_STATES = 1e3;
|
|
844
|
+
/**
|
|
845
|
+
* Origin + mount prefix of a pinned `redirect_uri`: the part before the
|
|
846
|
+
* callback path when it ends with it, else just its origin.
|
|
847
|
+
*/
|
|
848
|
+
function returnBaseOf(redirectUri, callbackPath) {
|
|
849
|
+
try {
|
|
850
|
+
const url = new URL(redirectUri);
|
|
851
|
+
const prefix = url.pathname.endsWith(callbackPath) ? url.pathname.slice(0, -callbackPath.length) : "";
|
|
852
|
+
return `${url.origin}${prefix.replace(/\/+$/, "")}`;
|
|
853
|
+
} catch {
|
|
854
|
+
return "";
|
|
855
|
+
}
|
|
856
|
+
}
|
|
768
857
|
var AuthOidcAddon = class extends BaseAddon {
|
|
769
858
|
/** Per-provider runtime state, keyed by `provider.id`. */
|
|
770
859
|
instances = /* @__PURE__ */ new Map();
|
|
@@ -899,7 +988,7 @@ var AuthOidcAddon = class extends BaseAddon {
|
|
|
899
988
|
type: "text",
|
|
900
989
|
key: "redirectUri",
|
|
901
990
|
label: "Redirect URI override",
|
|
902
|
-
description: "Optional.
|
|
991
|
+
description: "Optional — leave empty. Empty: a sign-in returns to the hub address it was started from (LAN, mesh or public, as listed on the Network page), so register one callback per address you sign in from at the IdP. Set: it PINS that one callback — every sign-in returns to its address, wherever it started.",
|
|
903
992
|
default: ""
|
|
904
993
|
},
|
|
905
994
|
{
|
|
@@ -977,21 +1066,60 @@ var AuthOidcAddon = class extends BaseAddon {
|
|
|
977
1066
|
}
|
|
978
1067
|
return doc;
|
|
979
1068
|
}
|
|
980
|
-
|
|
1069
|
+
/** The hub's Network-page address book, or `null` when `local-network` is unreachable. */
|
|
1070
|
+
async addressBook() {
|
|
1071
|
+
try {
|
|
1072
|
+
return await this.ctx.api.localNetwork.getAddresses.query({ scheme: "https" });
|
|
1073
|
+
} catch (err) {
|
|
1074
|
+
this.ctx.logger.warn("OIDC: hub addresses unavailable from local-network", { meta: { error: errMsg(err) } });
|
|
1075
|
+
return null;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* The `redirect_uri` of one sign-in, and the base the browser returns to
|
|
1080
|
+
* after the callback (D791). Precedence:
|
|
1081
|
+
*
|
|
1082
|
+
* 1. The instance's "Redirect URI override", when set: it PINS one origin —
|
|
1083
|
+
* every sign-in returns there, wherever it started.
|
|
1084
|
+
* 2. The origin the `/start` request was made at (Host / X-Forwarded-Host /
|
|
1085
|
+
* -Proto + mount prefix), ONLY when the hub advertises that origin
|
|
1086
|
+
* (`request-origin.ts`) — a forged Host never reaches the IdP.
|
|
1087
|
+
* 3. The Network page's public endpoint (`roles.publicEndpoint`), else the
|
|
1088
|
+
* development default.
|
|
1089
|
+
*/
|
|
1090
|
+
async resolveRedirect(providerId, headers) {
|
|
981
1091
|
const inst = this.instances.get(providerId);
|
|
982
1092
|
if (!inst) throw new Error(`OIDC: no provider configured with id "${providerId}"`);
|
|
1093
|
+
const callbackPath = `/addon/auth-oidc/${providerId}/callback`;
|
|
983
1094
|
const configured = inst.config.redirectUri?.trim();
|
|
984
|
-
if (configured) return
|
|
985
|
-
|
|
1095
|
+
if (configured) return {
|
|
1096
|
+
redirectUri: configured,
|
|
1097
|
+
returnBase: returnBaseOf(configured, callbackPath)
|
|
1098
|
+
};
|
|
1099
|
+
const book = await this.addressBook();
|
|
1100
|
+
const fromRequest = headers !== null && book !== null ? pickRequestOrigin(headers, advertisedHubOrigins(book)) : null;
|
|
1101
|
+
if (fromRequest !== null) {
|
|
1102
|
+
const base = `${fromRequest}${requestMountPrefix(headers ?? {})}`;
|
|
1103
|
+
return {
|
|
1104
|
+
redirectUri: `${base}${callbackPath}`,
|
|
1105
|
+
returnBase: base
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
const fallback = (book !== null ? normalizeOrigin(book.roles.publicEndpoint.resolved ?? "") : null) ?? "https://localhost:4443";
|
|
1109
|
+
return {
|
|
1110
|
+
redirectUri: `${fallback}${callbackPath}`,
|
|
1111
|
+
returnBase: fallback
|
|
1112
|
+
};
|
|
986
1113
|
}
|
|
987
|
-
async buildLoginUrl(providerId, state,
|
|
1114
|
+
async buildLoginUrl(providerId, state, options = {}) {
|
|
1115
|
+
const { linkIntent } = options;
|
|
988
1116
|
const inst = this.instances.get(providerId);
|
|
989
1117
|
if (!inst) throw new Error(`OIDC: no provider configured with id "${providerId}"`);
|
|
990
1118
|
const doc = await this.getDiscovery(providerId);
|
|
991
1119
|
const codeVerifier = crypto$1.randomBytes(32).toString("base64url");
|
|
992
1120
|
const codeChallenge = crypto$1.createHash("sha256").update(codeVerifier).digest("base64url");
|
|
993
1121
|
const nonce = crypto$1.randomBytes(16).toString("base64url");
|
|
994
|
-
const redirectUri = await this.
|
|
1122
|
+
const { redirectUri, returnBase } = await this.resolveRedirect(providerId, options.headers ?? null);
|
|
995
1123
|
this.pruneExpiredStates();
|
|
996
1124
|
while (this.pendingStates.size >= MAX_PENDING_STATES) {
|
|
997
1125
|
const oldest = this.pendingStates.keys().next();
|
|
@@ -1003,6 +1131,8 @@ var AuthOidcAddon = class extends BaseAddon {
|
|
|
1003
1131
|
nonce,
|
|
1004
1132
|
providerId,
|
|
1005
1133
|
redirectUri,
|
|
1134
|
+
returnBase,
|
|
1135
|
+
popup: options.popup === true,
|
|
1006
1136
|
createdAt: Date.now(),
|
|
1007
1137
|
...linkIntent !== void 0 ? { linkIntent } : {}
|
|
1008
1138
|
});
|
|
@@ -1077,7 +1207,9 @@ var AuthOidcAddon = class extends BaseAddon {
|
|
|
1077
1207
|
isAdmin: false,
|
|
1078
1208
|
...pending.linkIntent !== void 0 ? { linkIntent: pending.linkIntent } : {},
|
|
1079
1209
|
issuer: doc.issuer,
|
|
1080
|
-
providerId
|
|
1210
|
+
providerId,
|
|
1211
|
+
returnBase: pending.returnBase,
|
|
1212
|
+
popup: pending.popup
|
|
1081
1213
|
};
|
|
1082
1214
|
}
|
|
1083
1215
|
validateLocalSessionToken(_token) {
|
|
@@ -1096,9 +1228,14 @@ var AuthOidcAddon = class extends BaseAddon {
|
|
|
1096
1228
|
reply.send({ error: "Malformed link request" });
|
|
1097
1229
|
return;
|
|
1098
1230
|
}
|
|
1231
|
+
const popupParam = req.query["popup"];
|
|
1099
1232
|
const state = crypto$1.randomBytes(16).toString("base64url");
|
|
1100
1233
|
try {
|
|
1101
|
-
const url = await this.buildLoginUrl(providerId, state,
|
|
1234
|
+
const url = await this.buildLoginUrl(providerId, state, {
|
|
1235
|
+
...linkParam !== void 0 ? { linkIntent: linkParam } : {},
|
|
1236
|
+
headers: req.headers,
|
|
1237
|
+
popup: popupParam === "1"
|
|
1238
|
+
});
|
|
1102
1239
|
reply.code(302);
|
|
1103
1240
|
reply.header("Location", url);
|
|
1104
1241
|
reply.send("");
|
|
@@ -1152,8 +1289,10 @@ var AuthOidcAddon = class extends BaseAddon {
|
|
|
1152
1289
|
...result.linkIntent !== void 0 ? { linkIntent: result.linkIntent } : {},
|
|
1153
1290
|
issuer: result.issuer
|
|
1154
1291
|
} });
|
|
1292
|
+
const finish = new URLSearchParams({ bridge: bridgeToken });
|
|
1293
|
+
if (result.popup) finish.set("popup", "1");
|
|
1155
1294
|
reply.code(302);
|
|
1156
|
-
reply.header("Location",
|
|
1295
|
+
reply.header("Location", `${result.returnBase}/api/auth/sso/finish?${finish.toString()}`);
|
|
1157
1296
|
reply.send("");
|
|
1158
1297
|
} catch (err) {
|
|
1159
1298
|
reply.code(500);
|