@camstack/addon-auth 1.2.143 → 1.2.145
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/dist/{dist-DscyrBBw.js → dist-B2h4Ziv7.mjs} +897 -202
- package/dist/{dist-fzt8gRGY.mjs → dist-Be0i-dLp.js} +1146 -7
- package/dist/oidc/auth-oidc.addon.js +131 -97
- package/dist/oidc/auth-oidc.addon.mjs +126 -92
- package/dist/webauthn/_stub.js +1 -1
- package/dist/webauthn/{_virtual_mf-localSharedImportMap___mfe_internal__addon_auth_webauthn_widgets-CDD1e9sj.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_auth_webauthn_widgets-DuRYyuGp.mjs} +2 -2
- package/dist/webauthn/_virtual_mf___mfe_internal__addon_auth_webauthn_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DSXWgur5.mjs +26 -0
- package/dist/webauthn/auth-webauthn.addon.js +16 -4
- package/dist/webauthn/auth-webauthn.addon.mjs +16 -5
- package/dist/webauthn/{hostInit-Ch3QCBTy.mjs → hostInit-B5ByDlK7.mjs} +2 -2
- package/dist/webauthn/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/webauthn/_virtual_mf___mfe_internal__addon_auth_webauthn_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BlpPaWg4.mjs +0 -26
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-Be0i-dLp.js");
|
|
6
6
|
let node_crypto = require("node:crypto");
|
|
7
7
|
node_crypto = require_dist.__toESM(node_crypto);
|
|
8
8
|
//#region ../../node_modules/jose/dist/webapi/lib/buffer_utils.js
|
|
@@ -886,83 +886,6 @@ function clientSecretPatchOf(value) {
|
|
|
886
886
|
return { clientSecret: value };
|
|
887
887
|
}
|
|
888
888
|
//#endregion
|
|
889
|
-
//#region src/oidc/request-origin.ts
|
|
890
|
-
/** host[:port] — a DNS name, IPv4 or bracketed IPv6. No userinfo, path or spaces. */
|
|
891
|
-
var AUTHORITY_RE = /^(?:[a-z0-9.-]+|\[[0-9a-f:.]+\])(?::\d{1,5})?$/i;
|
|
892
|
-
/** Same grammar as the hub's `parseForwardedPrefix` (spa-mount.ts). */
|
|
893
|
-
var PREFIX_RE = /^\/[A-Za-z0-9_\-.]+(?:\/[A-Za-z0-9_\-.]+)*$/;
|
|
894
|
-
var MAX_PREFIX_LENGTH = 512;
|
|
895
|
-
/** First value of a possibly comma-joined forwarded header (the client-most proxy). */
|
|
896
|
-
function firstValue(value) {
|
|
897
|
-
if (value === void 0) return null;
|
|
898
|
-
const first = value.split(",")[0]?.trim() ?? "";
|
|
899
|
-
return first.length === 0 ? null : first;
|
|
900
|
-
}
|
|
901
|
-
/** `scheme://host[:port]` normalised by WHATWG URL, or `null` if it is not an http(s) origin. */
|
|
902
|
-
function normalizeOrigin(value) {
|
|
903
|
-
try {
|
|
904
|
-
const url = new URL(value);
|
|
905
|
-
if (url.protocol !== "https:" && url.protocol !== "http:") return null;
|
|
906
|
-
if (url.username !== "" || url.password !== "") return null;
|
|
907
|
-
return url.origin;
|
|
908
|
-
} catch {
|
|
909
|
-
return null;
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
/** The mount prefix the relay named (no trailing slash), or `''`. Invalid = absent. */
|
|
913
|
-
function requestMountPrefix(headers) {
|
|
914
|
-
const raw = headers["x-forwarded-prefix"];
|
|
915
|
-
if (raw === void 0 || raw.length === 0 || raw.length > MAX_PREFIX_LENGTH) return "";
|
|
916
|
-
const trimmed = raw.endsWith("/") ? raw.slice(0, -1) : raw;
|
|
917
|
-
if (trimmed === "" || !PREFIX_RE.test(trimmed)) return "";
|
|
918
|
-
if (trimmed.split("/").some((segment) => segment === ".." || segment === ".")) return "";
|
|
919
|
-
return trimmed;
|
|
920
|
-
}
|
|
921
|
-
/**
|
|
922
|
-
* The origins the request CLAIMS, most specific first: the proxy's
|
|
923
|
-
* `X-Forwarded-Host` over `Host`; the named `X-Forwarded-Proto`, or https
|
|
924
|
-
* then http when none is named (the hub terminates TLS itself on 4443).
|
|
925
|
-
*/
|
|
926
|
-
function claimedRequestOrigins(headers) {
|
|
927
|
-
const authority = firstValue(headers["x-forwarded-host"]) ?? firstValue(headers["host"]);
|
|
928
|
-
if (authority === null || !AUTHORITY_RE.test(authority)) return [];
|
|
929
|
-
const proto = firstValue(headers["x-forwarded-proto"])?.toLowerCase() ?? null;
|
|
930
|
-
const schemes = proto === "https" || proto === "http" ? [proto] : proto === null ? ["https", "http"] : [];
|
|
931
|
-
const out = [];
|
|
932
|
-
for (const scheme of schemes) {
|
|
933
|
-
const origin = normalizeOrigin(`${scheme}://${authority}`);
|
|
934
|
-
if (origin !== null) out.push(origin);
|
|
935
|
-
}
|
|
936
|
-
return out;
|
|
937
|
-
}
|
|
938
|
-
/** Every origin the hub advertises itself at, normalised. Never the app link, never loopback. */
|
|
939
|
-
function advertisedHubOrigins(book) {
|
|
940
|
-
const candidates = [];
|
|
941
|
-
for (const row of book.rows) {
|
|
942
|
-
if (row.kind === "app-link" || row.kind === "loopback") continue;
|
|
943
|
-
candidates.push(row.baseUrl);
|
|
944
|
-
}
|
|
945
|
-
const { publicEndpoint, internal, appConnections } = book.roles;
|
|
946
|
-
if (publicEndpoint.resolved !== null) candidates.push(publicEndpoint.resolved);
|
|
947
|
-
if (internal.resolved !== null) candidates.push(internal.resolved);
|
|
948
|
-
candidates.push(...appConnections.resolved);
|
|
949
|
-
candidates.push(...book.listenerOrigins ?? []);
|
|
950
|
-
const out = /* @__PURE__ */ new Set();
|
|
951
|
-
for (const candidate of candidates) {
|
|
952
|
-
const origin = normalizeOrigin(candidate);
|
|
953
|
-
if (origin !== null) out.add(origin);
|
|
954
|
-
}
|
|
955
|
-
return out;
|
|
956
|
-
}
|
|
957
|
-
/**
|
|
958
|
-
* The origin to send the browser back to: the first claimed origin that is in
|
|
959
|
-
* `allowed`, or `null` (the caller then uses its default address).
|
|
960
|
-
*/
|
|
961
|
-
function pickRequestOrigin(headers, allowed) {
|
|
962
|
-
for (const origin of claimedRequestOrigins(headers)) if (allowed.has(origin)) return origin;
|
|
963
|
-
return null;
|
|
964
|
-
}
|
|
965
|
-
//#endregion
|
|
966
889
|
//#region src/oidc/auth-oidc.addon.ts
|
|
967
890
|
/**
|
|
968
891
|
* Generic OpenID Connect (OIDC) authentication provider.
|
|
@@ -1015,6 +938,28 @@ var STATE_TTL_MS = 5 * 6e4;
|
|
|
1015
938
|
var LINK_INTENT_PATTERN = /^[A-Za-z0-9_-]{16,128}$/;
|
|
1016
939
|
/** Bound on in-flight logins: `/start` is public, so a flood must not grow the map. */
|
|
1017
940
|
var MAX_PENDING_STATES = 1e3;
|
|
941
|
+
/** In-flight logins one client may hold (D858): one browser flooding `/start` cannot fill the map for everyone. */
|
|
942
|
+
var MAX_PENDING_PER_CLIENT = 20;
|
|
943
|
+
/**
|
|
944
|
+
* A step-up must follow a login that happened no earlier than this before the
|
|
945
|
+
* flow began (D858). The authorize request asks for `prompt=login&max_age=0`;
|
|
946
|
+
* this is the check that the IdP actually honoured it.
|
|
947
|
+
*/
|
|
948
|
+
var STEP_UP_MAX_AUTH_AGE_MS = 6e4;
|
|
949
|
+
/** The pending map is full, or this client already holds its share. Live states are never evicted for it. */
|
|
950
|
+
var PendingStatesFullError = class extends Error {};
|
|
951
|
+
/** A step-up whose id_token proves no login just before the flow began. */
|
|
952
|
+
var StepUpNotFreshError = class extends Error {};
|
|
953
|
+
/**
|
|
954
|
+
* Who a `/start` is counted against. The hub already limits each real client
|
|
955
|
+
* address per minute before a route runs; the addon sees only headers, so this
|
|
956
|
+
* is a best-effort key (a spoofed header only rotates the bucket, and the
|
|
957
|
+
* hub's limit and the global cap still bound the total).
|
|
958
|
+
*/
|
|
959
|
+
function clientKeyOf(headers) {
|
|
960
|
+
const forwarded = headers?.["x-forwarded-for"]?.split(",")[0]?.trim();
|
|
961
|
+
return headers?.["cf-connecting-ip"] ?? headers?.["x-real-ip"] ?? forwarded ?? "unknown";
|
|
962
|
+
}
|
|
1018
963
|
/**
|
|
1019
964
|
* Origin + mount prefix of a pinned `redirect_uri`: the part before the
|
|
1020
965
|
* callback path when it ends with it, else just its origin.
|
|
@@ -1185,7 +1130,11 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
|
|
|
1185
1130
|
icon: require_dist.resolveIconKey(inst.config.icon, DEFAULT_PROVIDER.icon),
|
|
1186
1131
|
startUrl: `/addon/auth-oidc/${id}/start`,
|
|
1187
1132
|
stage: "primary",
|
|
1188
|
-
linkable: true
|
|
1133
|
+
linkable: true,
|
|
1134
|
+
app: {
|
|
1135
|
+
kind: "browser",
|
|
1136
|
+
startPath: `/addon/auth-oidc/${id}/start`
|
|
1137
|
+
}
|
|
1189
1138
|
});
|
|
1190
1139
|
}
|
|
1191
1140
|
return out;
|
|
@@ -1267,23 +1216,23 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
|
|
|
1267
1216
|
const inst = this.instances.get(providerId);
|
|
1268
1217
|
if (!inst) throw new Error(`OIDC: no provider configured with id "${providerId}"`);
|
|
1269
1218
|
const callbackPath = `/addon/auth-oidc/${providerId}/callback`;
|
|
1270
|
-
const claimed = headers !== null ? claimedRequestOrigins(headers) : [];
|
|
1219
|
+
const claimed = headers !== null ? require_dist.claimedRequestOrigins(headers) : [];
|
|
1271
1220
|
const configured = inst.config.redirectUri?.trim();
|
|
1272
1221
|
if (configured) return this.returnsTo(providerId, "override", claimed, null, {
|
|
1273
1222
|
redirectUri: configured,
|
|
1274
1223
|
returnBase: returnBaseOf(configured, callbackPath)
|
|
1275
1224
|
});
|
|
1276
1225
|
const book = await this.addressBook();
|
|
1277
|
-
const advertised = book !== null ? advertisedHubOrigins(book) : null;
|
|
1278
|
-
const fromRequest = headers !== null && advertised !== null ? pickRequestOrigin(headers, advertised) : null;
|
|
1226
|
+
const advertised = book !== null ? require_dist.advertisedHubOrigins(book) : null;
|
|
1227
|
+
const fromRequest = headers !== null && advertised !== null ? require_dist.pickRequestOrigin(headers, advertised) : null;
|
|
1279
1228
|
if (fromRequest !== null) {
|
|
1280
|
-
const base = `${fromRequest}${requestMountPrefix(headers ?? {})}`;
|
|
1229
|
+
const base = `${fromRequest}${require_dist.requestMountPrefix(headers ?? {})}`;
|
|
1281
1230
|
return this.returnsTo(providerId, "request", claimed, advertised, {
|
|
1282
1231
|
redirectUri: `${base}${callbackPath}`,
|
|
1283
1232
|
returnBase: base
|
|
1284
1233
|
});
|
|
1285
1234
|
}
|
|
1286
|
-
const publicEndpoint = book !== null ? normalizeOrigin(book.roles.publicEndpoint.resolved ?? "") : null;
|
|
1235
|
+
const publicEndpoint = book !== null ? require_dist.normalizeOrigin(book.roles.publicEndpoint.resolved ?? "") : null;
|
|
1287
1236
|
const fallback = publicEndpoint ?? "https://localhost:4443";
|
|
1288
1237
|
return this.returnsTo(providerId, publicEndpoint !== null ? "public-endpoint" : "default", claimed, advertised, {
|
|
1289
1238
|
redirectUri: `${fallback}${callbackPath}`,
|
|
@@ -1321,12 +1270,10 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
|
|
|
1321
1270
|
const nonce = node_crypto.randomBytes(16).toString("base64url");
|
|
1322
1271
|
const { redirectUri, returnBase } = await this.resolveRedirect(providerId, options.headers ?? null);
|
|
1323
1272
|
this.pruneExpiredStates();
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
if (oldest.done) break;
|
|
1327
|
-
this.pendingStates.delete(oldest.value);
|
|
1328
|
-
}
|
|
1273
|
+
const clientKey = clientKeyOf(options.headers);
|
|
1274
|
+
if (this.pendingStates.size >= MAX_PENDING_STATES || [...this.pendingStates.values()].filter((p) => p.clientKey === clientKey).length >= MAX_PENDING_PER_CLIENT) throw new PendingStatesFullError("Too many sign-ins in progress - try again shortly");
|
|
1329
1275
|
this.pendingStates.set(state, {
|
|
1276
|
+
clientKey,
|
|
1330
1277
|
codeVerifier,
|
|
1331
1278
|
nonce,
|
|
1332
1279
|
providerId,
|
|
@@ -1335,7 +1282,8 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
|
|
|
1335
1282
|
popup: options.popup === true,
|
|
1336
1283
|
createdAt: Date.now(),
|
|
1337
1284
|
...linkIntent !== void 0 ? { linkIntent } : {},
|
|
1338
|
-
...options.browserBinding !== void 0 ? { browserBinding: options.browserBinding } : {}
|
|
1285
|
+
...options.browserBinding !== void 0 ? { browserBinding: options.browserBinding } : {},
|
|
1286
|
+
...options.app !== void 0 ? { app: options.app } : {}
|
|
1339
1287
|
});
|
|
1340
1288
|
const params = new URLSearchParams({
|
|
1341
1289
|
response_type: "code",
|
|
@@ -1347,17 +1295,27 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
|
|
|
1347
1295
|
code_challenge: codeChallenge,
|
|
1348
1296
|
code_challenge_method: "S256"
|
|
1349
1297
|
});
|
|
1298
|
+
if (options.app?.purpose === "step-up") {
|
|
1299
|
+
params.set("prompt", "login");
|
|
1300
|
+
params.set("max_age", "0");
|
|
1301
|
+
}
|
|
1350
1302
|
return {
|
|
1351
1303
|
url: `${doc.authorization_endpoint}?${params.toString()}`,
|
|
1352
1304
|
returnBase
|
|
1353
1305
|
};
|
|
1354
1306
|
}
|
|
1307
|
+
/** The pending state for `state`, unless it is gone or older than its TTL. */
|
|
1308
|
+
livePending(state) {
|
|
1309
|
+
const pending = this.pendingStates.get(state);
|
|
1310
|
+
if (pending === void 0) return void 0;
|
|
1311
|
+
return Date.now() - pending.createdAt > STATE_TTL_MS ? void 0 : pending;
|
|
1312
|
+
}
|
|
1355
1313
|
pruneExpiredStates() {
|
|
1356
1314
|
const now = Date.now();
|
|
1357
1315
|
for (const [k, v] of this.pendingStates.entries()) if (now - v.createdAt > STATE_TTL_MS) this.pendingStates.delete(k);
|
|
1358
1316
|
}
|
|
1359
1317
|
async handleCallback(code, state) {
|
|
1360
|
-
const pending = this.
|
|
1318
|
+
const pending = this.livePending(state);
|
|
1361
1319
|
if (!pending) throw new Error("Unknown or expired OIDC state — login session timed out, please retry");
|
|
1362
1320
|
this.pendingStates.delete(state);
|
|
1363
1321
|
const providerId = pending.providerId;
|
|
@@ -1398,6 +1356,10 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
|
|
|
1398
1356
|
throw new Error(`OIDC id_token verification failed: ${require_dist.errMsg(err)}`, { cause: err });
|
|
1399
1357
|
}
|
|
1400
1358
|
if (typeof payload["nonce"] !== "string" || payload["nonce"] !== pending.nonce) throw new Error("OIDC id_token nonce mismatch — possible replay attack");
|
|
1359
|
+
if (pending.app?.purpose === "step-up") {
|
|
1360
|
+
const authTime = payload["auth_time"];
|
|
1361
|
+
if (typeof authTime !== "number" || !Number.isFinite(authTime) || authTime * 1e3 < pending.createdAt - STEP_UP_MAX_AUTH_AGE_MS) throw new StepUpNotFreshError("OIDC step-up id_token proves no fresh login");
|
|
1362
|
+
}
|
|
1401
1363
|
const sub = typeof payload.sub === "string" ? payload.sub : null;
|
|
1402
1364
|
if (!sub) throw new Error("OIDC callback: id_token missing required `sub` claim");
|
|
1403
1365
|
const emailClaim = typeof payload["email"] === "string" ? payload["email"] : void 0;
|
|
@@ -1416,7 +1378,8 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
|
|
|
1416
1378
|
providerId,
|
|
1417
1379
|
returnBase: pending.returnBase,
|
|
1418
1380
|
popup: pending.popup,
|
|
1419
|
-
...pending.browserBinding !== void 0 ? { browserBinding: pending.browserBinding } : {}
|
|
1381
|
+
...pending.browserBinding !== void 0 ? { browserBinding: pending.browserBinding } : {},
|
|
1382
|
+
...pending.app !== void 0 ? { app: pending.app } : {}
|
|
1420
1383
|
};
|
|
1421
1384
|
}
|
|
1422
1385
|
validateLocalSessionToken(_token) {
|
|
@@ -1435,21 +1398,52 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
|
|
|
1435
1398
|
reply.send({ error: "Malformed link request" });
|
|
1436
1399
|
return;
|
|
1437
1400
|
}
|
|
1438
|
-
const
|
|
1439
|
-
const
|
|
1401
|
+
const query = req.query;
|
|
1402
|
+
const popupParam = query["popup"];
|
|
1403
|
+
let app;
|
|
1404
|
+
if (query["app"] === "1") {
|
|
1405
|
+
const parsed = require_dist.parseAppFlowRequest(query);
|
|
1406
|
+
if (linkParam !== void 0 || popupParam === "1" || !parsed.ok) {
|
|
1407
|
+
this.ctx.logger.warn("OIDC app sign-in refused at start", { meta: {
|
|
1408
|
+
provider: providerId,
|
|
1409
|
+
reason: parsed.ok ? "combined-flow" : parsed.reason
|
|
1410
|
+
} });
|
|
1411
|
+
reply.code(400);
|
|
1412
|
+
reply.type("text/html; charset=utf-8");
|
|
1413
|
+
reply.header("Cache-Control", "no-store");
|
|
1414
|
+
reply.send(appRefusalPage(parsed.ok ? "An app sign-in cannot be combined with linking or a popup." : require_dist.APP_FLOW_REFUSAL_MESSAGES[parsed.reason]));
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
app = parsed.request;
|
|
1418
|
+
}
|
|
1419
|
+
const state = `${app !== void 0 ? "a." : ""}${node_crypto.randomBytes(16).toString("base64url")}`;
|
|
1440
1420
|
const browserState = node_crypto.randomBytes(32).toString("base64url");
|
|
1441
1421
|
try {
|
|
1442
1422
|
const { url, returnBase } = await this.prepareLogin(providerId, state, {
|
|
1443
1423
|
...linkParam !== void 0 ? { linkIntent: linkParam } : {},
|
|
1444
1424
|
headers: req.headers,
|
|
1445
1425
|
popup: popupParam === "1",
|
|
1446
|
-
browserBinding: node_crypto.createHash("sha256").update(browserState).digest("hex")
|
|
1426
|
+
browserBinding: node_crypto.createHash("sha256").update(browserState).digest("hex"),
|
|
1427
|
+
...app !== void 0 ? { app } : {}
|
|
1447
1428
|
});
|
|
1448
1429
|
reply.code(302);
|
|
1449
1430
|
reply.header("Set-Cookie", ssoStateCookie(browserState, returnBase.startsWith("https:")));
|
|
1450
1431
|
reply.header("Location", url);
|
|
1451
1432
|
reply.send("");
|
|
1452
1433
|
} catch (err) {
|
|
1434
|
+
if (err instanceof PendingStatesFullError) {
|
|
1435
|
+
this.ctx.logger.warn("OIDC sign-in refused: too many in progress", { meta: { provider: providerId } });
|
|
1436
|
+
if (app !== void 0) {
|
|
1437
|
+
reply.code(302);
|
|
1438
|
+
reply.header("Location", require_dist.appFlowLocation(app.redirect, { error: "too-many-requests" }, app.state));
|
|
1439
|
+
reply.send("");
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
reply.code(429);
|
|
1443
|
+
reply.header("Retry-After", "30");
|
|
1444
|
+
reply.send({ error: "Too many sign-ins in progress - try again shortly" });
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1453
1447
|
reply.code(500);
|
|
1454
1448
|
reply.send({
|
|
1455
1449
|
error: "OIDC start failed",
|
|
@@ -1468,6 +1462,27 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
|
|
|
1468
1462
|
const code = query["code"];
|
|
1469
1463
|
const state = query["state"];
|
|
1470
1464
|
const errorParam = query["error"];
|
|
1465
|
+
const pendingApp = state !== void 0 ? this.livePending(state)?.app : void 0;
|
|
1466
|
+
const appFlowState = state?.startsWith("a.") === true;
|
|
1467
|
+
const answerApp = (error) => {
|
|
1468
|
+
this.ctx.logger.warn("OIDC app sign-in refused at callback", { meta: {
|
|
1469
|
+
provider: providerId,
|
|
1470
|
+
error
|
|
1471
|
+
} });
|
|
1472
|
+
if (state !== void 0) this.pendingStates.delete(state);
|
|
1473
|
+
reply.code(302);
|
|
1474
|
+
reply.header("Cache-Control", "no-store");
|
|
1475
|
+
reply.header("Location", pendingApp !== void 0 ? require_dist.appFlowLocation(pendingApp.redirect, { error }, pendingApp.state) : require_dist.appFlowLocation(require_dist.APP_FLOW_DEFAULT_REDIRECT, { error }));
|
|
1476
|
+
reply.send("");
|
|
1477
|
+
};
|
|
1478
|
+
if (appFlowState && pendingApp === void 0) {
|
|
1479
|
+
answerApp(errorParam ? "idp-error" : "state-expired");
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
if (errorParam && pendingApp !== void 0) {
|
|
1483
|
+
answerApp("idp-error");
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1471
1486
|
if (errorParam) {
|
|
1472
1487
|
reply.code(400);
|
|
1473
1488
|
reply.send({
|
|
@@ -1485,6 +1500,10 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
|
|
|
1485
1500
|
try {
|
|
1486
1501
|
const result = await this.handleCallback(code, state);
|
|
1487
1502
|
if (result.providerId !== providerId) {
|
|
1503
|
+
if (result.app !== void 0) {
|
|
1504
|
+
answerApp("provider-mismatch");
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1488
1507
|
reply.code(400);
|
|
1489
1508
|
reply.send({ error: "OIDC callback does not match the provider the login started at" });
|
|
1490
1509
|
return;
|
|
@@ -1499,14 +1518,25 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
|
|
|
1499
1518
|
...result.linkIntent !== void 0 ? { linkIntent: result.linkIntent } : {},
|
|
1500
1519
|
issuer: result.issuer,
|
|
1501
1520
|
jti: node_crypto.randomBytes(16).toString("base64url"),
|
|
1502
|
-
...result.browserBinding !== void 0 ? { browserBinding: result.browserBinding } : {}
|
|
1521
|
+
...result.browserBinding !== void 0 ? { browserBinding: result.browserBinding } : {},
|
|
1522
|
+
...result.app !== void 0 ? { app: {
|
|
1523
|
+
codeChallenge: result.app.codeChallenge,
|
|
1524
|
+
state: result.app.state,
|
|
1525
|
+
purpose: result.app.purpose,
|
|
1526
|
+
redirect: result.app.redirect
|
|
1527
|
+
} } : {}
|
|
1503
1528
|
} });
|
|
1504
1529
|
const finish = new URLSearchParams({ bridge: bridgeToken });
|
|
1505
1530
|
if (result.popup) finish.set("popup", "1");
|
|
1531
|
+
const finishPath = result.app !== void 0 ? "finish-app" : "finish";
|
|
1506
1532
|
reply.code(302);
|
|
1507
|
-
reply.header("Location", `${result.returnBase}/api/auth/sso
|
|
1533
|
+
reply.header("Location", `${result.returnBase}/api/auth/sso/${finishPath}?${finish.toString()}`);
|
|
1508
1534
|
reply.send("");
|
|
1509
1535
|
} catch (err) {
|
|
1536
|
+
if (pendingApp !== void 0) {
|
|
1537
|
+
answerApp(err instanceof StepUpNotFreshError ? "step-up-not-fresh" : "idp-failed");
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1510
1540
|
reply.code(500);
|
|
1511
1541
|
reply.send({
|
|
1512
1542
|
error: "OIDC callback failed",
|
|
@@ -1515,6 +1545,10 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
|
|
|
1515
1545
|
}
|
|
1516
1546
|
}
|
|
1517
1547
|
};
|
|
1548
|
+
/** Minimal standalone page for a refused app sign-in request (D858) - no assets, no script. */
|
|
1549
|
+
function appRefusalPage(message) {
|
|
1550
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Sign-in request refused</title></head><body style="font-family:system-ui,sans-serif;max-width:32rem;margin:3rem auto;padding:0 1rem"><h1>Sign-in request refused</h1><p>${message.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`)}</p><p>Close this window and try again from the app.</p></body></html>`;
|
|
1551
|
+
}
|
|
1518
1552
|
/**
|
|
1519
1553
|
* The browser-binding cookie (`SSO_STATE_COOKIE`). HttpOnly, Lax (the IdP
|
|
1520
1554
|
* returns with a top-level GET), path `/` so it reaches the hub's finish under
|