@camstack/addon-auth 1.2.144 → 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-ChwITXNa.mjs → dist-B2h4Ziv7.mjs} +442 -8
- package/dist/{dist-0YTEFDYk.js → dist-Be0i-dLp.js} +465 -7
- package/dist/oidc/auth-oidc.addon.js +126 -15
- package/dist/oidc/auth-oidc.addon.mjs +126 -15
- package/dist/webauthn/_stub.js +1 -1
- package/dist/webauthn/{_virtual_mf-localSharedImportMap___mfe_internal__addon_auth_webauthn_widgets-ZRpCO_gp.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-YhJ9npQM.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-xhY74yma.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
|
|
@@ -938,6 +938,28 @@ var STATE_TTL_MS = 5 * 6e4;
|
|
|
938
938
|
var LINK_INTENT_PATTERN = /^[A-Za-z0-9_-]{16,128}$/;
|
|
939
939
|
/** Bound on in-flight logins: `/start` is public, so a flood must not grow the map. */
|
|
940
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
|
+
}
|
|
941
963
|
/**
|
|
942
964
|
* Origin + mount prefix of a pinned `redirect_uri`: the part before the
|
|
943
965
|
* callback path when it ends with it, else just its origin.
|
|
@@ -1108,7 +1130,11 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
|
|
|
1108
1130
|
icon: require_dist.resolveIconKey(inst.config.icon, DEFAULT_PROVIDER.icon),
|
|
1109
1131
|
startUrl: `/addon/auth-oidc/${id}/start`,
|
|
1110
1132
|
stage: "primary",
|
|
1111
|
-
linkable: true
|
|
1133
|
+
linkable: true,
|
|
1134
|
+
app: {
|
|
1135
|
+
kind: "browser",
|
|
1136
|
+
startPath: `/addon/auth-oidc/${id}/start`
|
|
1137
|
+
}
|
|
1112
1138
|
});
|
|
1113
1139
|
}
|
|
1114
1140
|
return out;
|
|
@@ -1244,12 +1270,10 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
|
|
|
1244
1270
|
const nonce = node_crypto.randomBytes(16).toString("base64url");
|
|
1245
1271
|
const { redirectUri, returnBase } = await this.resolveRedirect(providerId, options.headers ?? null);
|
|
1246
1272
|
this.pruneExpiredStates();
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
if (oldest.done) break;
|
|
1250
|
-
this.pendingStates.delete(oldest.value);
|
|
1251
|
-
}
|
|
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");
|
|
1252
1275
|
this.pendingStates.set(state, {
|
|
1276
|
+
clientKey,
|
|
1253
1277
|
codeVerifier,
|
|
1254
1278
|
nonce,
|
|
1255
1279
|
providerId,
|
|
@@ -1258,7 +1282,8 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
|
|
|
1258
1282
|
popup: options.popup === true,
|
|
1259
1283
|
createdAt: Date.now(),
|
|
1260
1284
|
...linkIntent !== void 0 ? { linkIntent } : {},
|
|
1261
|
-
...options.browserBinding !== void 0 ? { browserBinding: options.browserBinding } : {}
|
|
1285
|
+
...options.browserBinding !== void 0 ? { browserBinding: options.browserBinding } : {},
|
|
1286
|
+
...options.app !== void 0 ? { app: options.app } : {}
|
|
1262
1287
|
});
|
|
1263
1288
|
const params = new URLSearchParams({
|
|
1264
1289
|
response_type: "code",
|
|
@@ -1270,17 +1295,27 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
|
|
|
1270
1295
|
code_challenge: codeChallenge,
|
|
1271
1296
|
code_challenge_method: "S256"
|
|
1272
1297
|
});
|
|
1298
|
+
if (options.app?.purpose === "step-up") {
|
|
1299
|
+
params.set("prompt", "login");
|
|
1300
|
+
params.set("max_age", "0");
|
|
1301
|
+
}
|
|
1273
1302
|
return {
|
|
1274
1303
|
url: `${doc.authorization_endpoint}?${params.toString()}`,
|
|
1275
1304
|
returnBase
|
|
1276
1305
|
};
|
|
1277
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
|
+
}
|
|
1278
1313
|
pruneExpiredStates() {
|
|
1279
1314
|
const now = Date.now();
|
|
1280
1315
|
for (const [k, v] of this.pendingStates.entries()) if (now - v.createdAt > STATE_TTL_MS) this.pendingStates.delete(k);
|
|
1281
1316
|
}
|
|
1282
1317
|
async handleCallback(code, state) {
|
|
1283
|
-
const pending = this.
|
|
1318
|
+
const pending = this.livePending(state);
|
|
1284
1319
|
if (!pending) throw new Error("Unknown or expired OIDC state — login session timed out, please retry");
|
|
1285
1320
|
this.pendingStates.delete(state);
|
|
1286
1321
|
const providerId = pending.providerId;
|
|
@@ -1321,6 +1356,10 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
|
|
|
1321
1356
|
throw new Error(`OIDC id_token verification failed: ${require_dist.errMsg(err)}`, { cause: err });
|
|
1322
1357
|
}
|
|
1323
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
|
+
}
|
|
1324
1363
|
const sub = typeof payload.sub === "string" ? payload.sub : null;
|
|
1325
1364
|
if (!sub) throw new Error("OIDC callback: id_token missing required `sub` claim");
|
|
1326
1365
|
const emailClaim = typeof payload["email"] === "string" ? payload["email"] : void 0;
|
|
@@ -1339,7 +1378,8 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
|
|
|
1339
1378
|
providerId,
|
|
1340
1379
|
returnBase: pending.returnBase,
|
|
1341
1380
|
popup: pending.popup,
|
|
1342
|
-
...pending.browserBinding !== void 0 ? { browserBinding: pending.browserBinding } : {}
|
|
1381
|
+
...pending.browserBinding !== void 0 ? { browserBinding: pending.browserBinding } : {},
|
|
1382
|
+
...pending.app !== void 0 ? { app: pending.app } : {}
|
|
1343
1383
|
};
|
|
1344
1384
|
}
|
|
1345
1385
|
validateLocalSessionToken(_token) {
|
|
@@ -1358,21 +1398,52 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
|
|
|
1358
1398
|
reply.send({ error: "Malformed link request" });
|
|
1359
1399
|
return;
|
|
1360
1400
|
}
|
|
1361
|
-
const
|
|
1362
|
-
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")}`;
|
|
1363
1420
|
const browserState = node_crypto.randomBytes(32).toString("base64url");
|
|
1364
1421
|
try {
|
|
1365
1422
|
const { url, returnBase } = await this.prepareLogin(providerId, state, {
|
|
1366
1423
|
...linkParam !== void 0 ? { linkIntent: linkParam } : {},
|
|
1367
1424
|
headers: req.headers,
|
|
1368
1425
|
popup: popupParam === "1",
|
|
1369
|
-
browserBinding: node_crypto.createHash("sha256").update(browserState).digest("hex")
|
|
1426
|
+
browserBinding: node_crypto.createHash("sha256").update(browserState).digest("hex"),
|
|
1427
|
+
...app !== void 0 ? { app } : {}
|
|
1370
1428
|
});
|
|
1371
1429
|
reply.code(302);
|
|
1372
1430
|
reply.header("Set-Cookie", ssoStateCookie(browserState, returnBase.startsWith("https:")));
|
|
1373
1431
|
reply.header("Location", url);
|
|
1374
1432
|
reply.send("");
|
|
1375
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
|
+
}
|
|
1376
1447
|
reply.code(500);
|
|
1377
1448
|
reply.send({
|
|
1378
1449
|
error: "OIDC start failed",
|
|
@@ -1391,6 +1462,27 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
|
|
|
1391
1462
|
const code = query["code"];
|
|
1392
1463
|
const state = query["state"];
|
|
1393
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
|
+
}
|
|
1394
1486
|
if (errorParam) {
|
|
1395
1487
|
reply.code(400);
|
|
1396
1488
|
reply.send({
|
|
@@ -1408,6 +1500,10 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
|
|
|
1408
1500
|
try {
|
|
1409
1501
|
const result = await this.handleCallback(code, state);
|
|
1410
1502
|
if (result.providerId !== providerId) {
|
|
1503
|
+
if (result.app !== void 0) {
|
|
1504
|
+
answerApp("provider-mismatch");
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1411
1507
|
reply.code(400);
|
|
1412
1508
|
reply.send({ error: "OIDC callback does not match the provider the login started at" });
|
|
1413
1509
|
return;
|
|
@@ -1422,14 +1518,25 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
|
|
|
1422
1518
|
...result.linkIntent !== void 0 ? { linkIntent: result.linkIntent } : {},
|
|
1423
1519
|
issuer: result.issuer,
|
|
1424
1520
|
jti: node_crypto.randomBytes(16).toString("base64url"),
|
|
1425
|
-
...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
|
+
} } : {}
|
|
1426
1528
|
} });
|
|
1427
1529
|
const finish = new URLSearchParams({ bridge: bridgeToken });
|
|
1428
1530
|
if (result.popup) finish.set("popup", "1");
|
|
1531
|
+
const finishPath = result.app !== void 0 ? "finish-app" : "finish";
|
|
1429
1532
|
reply.code(302);
|
|
1430
|
-
reply.header("Location", `${result.returnBase}/api/auth/sso
|
|
1533
|
+
reply.header("Location", `${result.returnBase}/api/auth/sso/${finishPath}?${finish.toString()}`);
|
|
1431
1534
|
reply.send("");
|
|
1432
1535
|
} catch (err) {
|
|
1536
|
+
if (pendingApp !== void 0) {
|
|
1537
|
+
answerApp(err instanceof StepUpNotFreshError ? "step-up-not-fresh" : "idp-failed");
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1433
1540
|
reply.code(500);
|
|
1434
1541
|
reply.send({
|
|
1435
1542
|
error: "OIDC callback failed",
|
|
@@ -1438,6 +1545,10 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
|
|
|
1438
1545
|
}
|
|
1439
1546
|
}
|
|
1440
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
|
+
}
|
|
1441
1552
|
/**
|
|
1442
1553
|
* The browser-binding cookie (`SSO_STATE_COOKIE`). HttpOnly, Lax (the IdP
|
|
1443
1554
|
* returns with a top-level GET), path `/` so it reaches the hub's finish under
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { C as globalSecretScope, E as toSecretFieldView, S as arrayItemFieldPath, T as isSecretMarker, _ as resolveIconKey, b as BaseAddon, c as authProviderCapability, d as loginMethodCapability, f as normalizeOrigin, g as requestMountPrefix, h as pickRequestOrigin, i as addonRoutesCapability, l as buildAddonRouteProvider, m as parseAppFlowRequest, n as APP_FLOW_REFUSAL_MESSAGES, o as advertisedHubOrigins, p as oidcConnectionsCapability, r as SSO_STATE_COOKIE, s as appFlowLocation, t as APP_FLOW_DEFAULT_REDIRECT, u as claimedRequestOrigins, w as isSecretClear, x as SECRET_CLEAR, y as errMsg } from "../dist-B2h4Ziv7.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 });
|
|
@@ -933,6 +933,28 @@ var STATE_TTL_MS = 5 * 6e4;
|
|
|
933
933
|
var LINK_INTENT_PATTERN = /^[A-Za-z0-9_-]{16,128}$/;
|
|
934
934
|
/** Bound on in-flight logins: `/start` is public, so a flood must not grow the map. */
|
|
935
935
|
var MAX_PENDING_STATES = 1e3;
|
|
936
|
+
/** In-flight logins one client may hold (D858): one browser flooding `/start` cannot fill the map for everyone. */
|
|
937
|
+
var MAX_PENDING_PER_CLIENT = 20;
|
|
938
|
+
/**
|
|
939
|
+
* A step-up must follow a login that happened no earlier than this before the
|
|
940
|
+
* flow began (D858). The authorize request asks for `prompt=login&max_age=0`;
|
|
941
|
+
* this is the check that the IdP actually honoured it.
|
|
942
|
+
*/
|
|
943
|
+
var STEP_UP_MAX_AUTH_AGE_MS = 6e4;
|
|
944
|
+
/** The pending map is full, or this client already holds its share. Live states are never evicted for it. */
|
|
945
|
+
var PendingStatesFullError = class extends Error {};
|
|
946
|
+
/** A step-up whose id_token proves no login just before the flow began. */
|
|
947
|
+
var StepUpNotFreshError = class extends Error {};
|
|
948
|
+
/**
|
|
949
|
+
* Who a `/start` is counted against. The hub already limits each real client
|
|
950
|
+
* address per minute before a route runs; the addon sees only headers, so this
|
|
951
|
+
* is a best-effort key (a spoofed header only rotates the bucket, and the
|
|
952
|
+
* hub's limit and the global cap still bound the total).
|
|
953
|
+
*/
|
|
954
|
+
function clientKeyOf(headers) {
|
|
955
|
+
const forwarded = headers?.["x-forwarded-for"]?.split(",")[0]?.trim();
|
|
956
|
+
return headers?.["cf-connecting-ip"] ?? headers?.["x-real-ip"] ?? forwarded ?? "unknown";
|
|
957
|
+
}
|
|
936
958
|
/**
|
|
937
959
|
* Origin + mount prefix of a pinned `redirect_uri`: the part before the
|
|
938
960
|
* callback path when it ends with it, else just its origin.
|
|
@@ -1103,7 +1125,11 @@ var AuthOidcAddon = class extends BaseAddon {
|
|
|
1103
1125
|
icon: resolveIconKey(inst.config.icon, DEFAULT_PROVIDER.icon),
|
|
1104
1126
|
startUrl: `/addon/auth-oidc/${id}/start`,
|
|
1105
1127
|
stage: "primary",
|
|
1106
|
-
linkable: true
|
|
1128
|
+
linkable: true,
|
|
1129
|
+
app: {
|
|
1130
|
+
kind: "browser",
|
|
1131
|
+
startPath: `/addon/auth-oidc/${id}/start`
|
|
1132
|
+
}
|
|
1107
1133
|
});
|
|
1108
1134
|
}
|
|
1109
1135
|
return out;
|
|
@@ -1239,12 +1265,10 @@ var AuthOidcAddon = class extends BaseAddon {
|
|
|
1239
1265
|
const nonce = crypto$1.randomBytes(16).toString("base64url");
|
|
1240
1266
|
const { redirectUri, returnBase } = await this.resolveRedirect(providerId, options.headers ?? null);
|
|
1241
1267
|
this.pruneExpiredStates();
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
if (oldest.done) break;
|
|
1245
|
-
this.pendingStates.delete(oldest.value);
|
|
1246
|
-
}
|
|
1268
|
+
const clientKey = clientKeyOf(options.headers);
|
|
1269
|
+
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");
|
|
1247
1270
|
this.pendingStates.set(state, {
|
|
1271
|
+
clientKey,
|
|
1248
1272
|
codeVerifier,
|
|
1249
1273
|
nonce,
|
|
1250
1274
|
providerId,
|
|
@@ -1253,7 +1277,8 @@ var AuthOidcAddon = class extends BaseAddon {
|
|
|
1253
1277
|
popup: options.popup === true,
|
|
1254
1278
|
createdAt: Date.now(),
|
|
1255
1279
|
...linkIntent !== void 0 ? { linkIntent } : {},
|
|
1256
|
-
...options.browserBinding !== void 0 ? { browserBinding: options.browserBinding } : {}
|
|
1280
|
+
...options.browserBinding !== void 0 ? { browserBinding: options.browserBinding } : {},
|
|
1281
|
+
...options.app !== void 0 ? { app: options.app } : {}
|
|
1257
1282
|
});
|
|
1258
1283
|
const params = new URLSearchParams({
|
|
1259
1284
|
response_type: "code",
|
|
@@ -1265,17 +1290,27 @@ var AuthOidcAddon = class extends BaseAddon {
|
|
|
1265
1290
|
code_challenge: codeChallenge,
|
|
1266
1291
|
code_challenge_method: "S256"
|
|
1267
1292
|
});
|
|
1293
|
+
if (options.app?.purpose === "step-up") {
|
|
1294
|
+
params.set("prompt", "login");
|
|
1295
|
+
params.set("max_age", "0");
|
|
1296
|
+
}
|
|
1268
1297
|
return {
|
|
1269
1298
|
url: `${doc.authorization_endpoint}?${params.toString()}`,
|
|
1270
1299
|
returnBase
|
|
1271
1300
|
};
|
|
1272
1301
|
}
|
|
1302
|
+
/** The pending state for `state`, unless it is gone or older than its TTL. */
|
|
1303
|
+
livePending(state) {
|
|
1304
|
+
const pending = this.pendingStates.get(state);
|
|
1305
|
+
if (pending === void 0) return void 0;
|
|
1306
|
+
return Date.now() - pending.createdAt > STATE_TTL_MS ? void 0 : pending;
|
|
1307
|
+
}
|
|
1273
1308
|
pruneExpiredStates() {
|
|
1274
1309
|
const now = Date.now();
|
|
1275
1310
|
for (const [k, v] of this.pendingStates.entries()) if (now - v.createdAt > STATE_TTL_MS) this.pendingStates.delete(k);
|
|
1276
1311
|
}
|
|
1277
1312
|
async handleCallback(code, state) {
|
|
1278
|
-
const pending = this.
|
|
1313
|
+
const pending = this.livePending(state);
|
|
1279
1314
|
if (!pending) throw new Error("Unknown or expired OIDC state — login session timed out, please retry");
|
|
1280
1315
|
this.pendingStates.delete(state);
|
|
1281
1316
|
const providerId = pending.providerId;
|
|
@@ -1316,6 +1351,10 @@ var AuthOidcAddon = class extends BaseAddon {
|
|
|
1316
1351
|
throw new Error(`OIDC id_token verification failed: ${errMsg(err)}`, { cause: err });
|
|
1317
1352
|
}
|
|
1318
1353
|
if (typeof payload["nonce"] !== "string" || payload["nonce"] !== pending.nonce) throw new Error("OIDC id_token nonce mismatch — possible replay attack");
|
|
1354
|
+
if (pending.app?.purpose === "step-up") {
|
|
1355
|
+
const authTime = payload["auth_time"];
|
|
1356
|
+
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");
|
|
1357
|
+
}
|
|
1319
1358
|
const sub = typeof payload.sub === "string" ? payload.sub : null;
|
|
1320
1359
|
if (!sub) throw new Error("OIDC callback: id_token missing required `sub` claim");
|
|
1321
1360
|
const emailClaim = typeof payload["email"] === "string" ? payload["email"] : void 0;
|
|
@@ -1334,7 +1373,8 @@ var AuthOidcAddon = class extends BaseAddon {
|
|
|
1334
1373
|
providerId,
|
|
1335
1374
|
returnBase: pending.returnBase,
|
|
1336
1375
|
popup: pending.popup,
|
|
1337
|
-
...pending.browserBinding !== void 0 ? { browserBinding: pending.browserBinding } : {}
|
|
1376
|
+
...pending.browserBinding !== void 0 ? { browserBinding: pending.browserBinding } : {},
|
|
1377
|
+
...pending.app !== void 0 ? { app: pending.app } : {}
|
|
1338
1378
|
};
|
|
1339
1379
|
}
|
|
1340
1380
|
validateLocalSessionToken(_token) {
|
|
@@ -1353,21 +1393,52 @@ var AuthOidcAddon = class extends BaseAddon {
|
|
|
1353
1393
|
reply.send({ error: "Malformed link request" });
|
|
1354
1394
|
return;
|
|
1355
1395
|
}
|
|
1356
|
-
const
|
|
1357
|
-
const
|
|
1396
|
+
const query = req.query;
|
|
1397
|
+
const popupParam = query["popup"];
|
|
1398
|
+
let app;
|
|
1399
|
+
if (query["app"] === "1") {
|
|
1400
|
+
const parsed = parseAppFlowRequest(query);
|
|
1401
|
+
if (linkParam !== void 0 || popupParam === "1" || !parsed.ok) {
|
|
1402
|
+
this.ctx.logger.warn("OIDC app sign-in refused at start", { meta: {
|
|
1403
|
+
provider: providerId,
|
|
1404
|
+
reason: parsed.ok ? "combined-flow" : parsed.reason
|
|
1405
|
+
} });
|
|
1406
|
+
reply.code(400);
|
|
1407
|
+
reply.type("text/html; charset=utf-8");
|
|
1408
|
+
reply.header("Cache-Control", "no-store");
|
|
1409
|
+
reply.send(appRefusalPage(parsed.ok ? "An app sign-in cannot be combined with linking or a popup." : APP_FLOW_REFUSAL_MESSAGES[parsed.reason]));
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
app = parsed.request;
|
|
1413
|
+
}
|
|
1414
|
+
const state = `${app !== void 0 ? "a." : ""}${crypto$1.randomBytes(16).toString("base64url")}`;
|
|
1358
1415
|
const browserState = crypto$1.randomBytes(32).toString("base64url");
|
|
1359
1416
|
try {
|
|
1360
1417
|
const { url, returnBase } = await this.prepareLogin(providerId, state, {
|
|
1361
1418
|
...linkParam !== void 0 ? { linkIntent: linkParam } : {},
|
|
1362
1419
|
headers: req.headers,
|
|
1363
1420
|
popup: popupParam === "1",
|
|
1364
|
-
browserBinding: crypto$1.createHash("sha256").update(browserState).digest("hex")
|
|
1421
|
+
browserBinding: crypto$1.createHash("sha256").update(browserState).digest("hex"),
|
|
1422
|
+
...app !== void 0 ? { app } : {}
|
|
1365
1423
|
});
|
|
1366
1424
|
reply.code(302);
|
|
1367
1425
|
reply.header("Set-Cookie", ssoStateCookie(browserState, returnBase.startsWith("https:")));
|
|
1368
1426
|
reply.header("Location", url);
|
|
1369
1427
|
reply.send("");
|
|
1370
1428
|
} catch (err) {
|
|
1429
|
+
if (err instanceof PendingStatesFullError) {
|
|
1430
|
+
this.ctx.logger.warn("OIDC sign-in refused: too many in progress", { meta: { provider: providerId } });
|
|
1431
|
+
if (app !== void 0) {
|
|
1432
|
+
reply.code(302);
|
|
1433
|
+
reply.header("Location", appFlowLocation(app.redirect, { error: "too-many-requests" }, app.state));
|
|
1434
|
+
reply.send("");
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
reply.code(429);
|
|
1438
|
+
reply.header("Retry-After", "30");
|
|
1439
|
+
reply.send({ error: "Too many sign-ins in progress - try again shortly" });
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1371
1442
|
reply.code(500);
|
|
1372
1443
|
reply.send({
|
|
1373
1444
|
error: "OIDC start failed",
|
|
@@ -1386,6 +1457,27 @@ var AuthOidcAddon = class extends BaseAddon {
|
|
|
1386
1457
|
const code = query["code"];
|
|
1387
1458
|
const state = query["state"];
|
|
1388
1459
|
const errorParam = query["error"];
|
|
1460
|
+
const pendingApp = state !== void 0 ? this.livePending(state)?.app : void 0;
|
|
1461
|
+
const appFlowState = state?.startsWith("a.") === true;
|
|
1462
|
+
const answerApp = (error) => {
|
|
1463
|
+
this.ctx.logger.warn("OIDC app sign-in refused at callback", { meta: {
|
|
1464
|
+
provider: providerId,
|
|
1465
|
+
error
|
|
1466
|
+
} });
|
|
1467
|
+
if (state !== void 0) this.pendingStates.delete(state);
|
|
1468
|
+
reply.code(302);
|
|
1469
|
+
reply.header("Cache-Control", "no-store");
|
|
1470
|
+
reply.header("Location", pendingApp !== void 0 ? appFlowLocation(pendingApp.redirect, { error }, pendingApp.state) : appFlowLocation(APP_FLOW_DEFAULT_REDIRECT, { error }));
|
|
1471
|
+
reply.send("");
|
|
1472
|
+
};
|
|
1473
|
+
if (appFlowState && pendingApp === void 0) {
|
|
1474
|
+
answerApp(errorParam ? "idp-error" : "state-expired");
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
if (errorParam && pendingApp !== void 0) {
|
|
1478
|
+
answerApp("idp-error");
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1389
1481
|
if (errorParam) {
|
|
1390
1482
|
reply.code(400);
|
|
1391
1483
|
reply.send({
|
|
@@ -1403,6 +1495,10 @@ var AuthOidcAddon = class extends BaseAddon {
|
|
|
1403
1495
|
try {
|
|
1404
1496
|
const result = await this.handleCallback(code, state);
|
|
1405
1497
|
if (result.providerId !== providerId) {
|
|
1498
|
+
if (result.app !== void 0) {
|
|
1499
|
+
answerApp("provider-mismatch");
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1406
1502
|
reply.code(400);
|
|
1407
1503
|
reply.send({ error: "OIDC callback does not match the provider the login started at" });
|
|
1408
1504
|
return;
|
|
@@ -1417,14 +1513,25 @@ var AuthOidcAddon = class extends BaseAddon {
|
|
|
1417
1513
|
...result.linkIntent !== void 0 ? { linkIntent: result.linkIntent } : {},
|
|
1418
1514
|
issuer: result.issuer,
|
|
1419
1515
|
jti: crypto$1.randomBytes(16).toString("base64url"),
|
|
1420
|
-
...result.browserBinding !== void 0 ? { browserBinding: result.browserBinding } : {}
|
|
1516
|
+
...result.browserBinding !== void 0 ? { browserBinding: result.browserBinding } : {},
|
|
1517
|
+
...result.app !== void 0 ? { app: {
|
|
1518
|
+
codeChallenge: result.app.codeChallenge,
|
|
1519
|
+
state: result.app.state,
|
|
1520
|
+
purpose: result.app.purpose,
|
|
1521
|
+
redirect: result.app.redirect
|
|
1522
|
+
} } : {}
|
|
1421
1523
|
} });
|
|
1422
1524
|
const finish = new URLSearchParams({ bridge: bridgeToken });
|
|
1423
1525
|
if (result.popup) finish.set("popup", "1");
|
|
1526
|
+
const finishPath = result.app !== void 0 ? "finish-app" : "finish";
|
|
1424
1527
|
reply.code(302);
|
|
1425
|
-
reply.header("Location", `${result.returnBase}/api/auth/sso
|
|
1528
|
+
reply.header("Location", `${result.returnBase}/api/auth/sso/${finishPath}?${finish.toString()}`);
|
|
1426
1529
|
reply.send("");
|
|
1427
1530
|
} catch (err) {
|
|
1531
|
+
if (pendingApp !== void 0) {
|
|
1532
|
+
answerApp(err instanceof StepUpNotFreshError ? "step-up-not-fresh" : "idp-failed");
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1428
1535
|
reply.code(500);
|
|
1429
1536
|
reply.send({
|
|
1430
1537
|
error: "OIDC callback failed",
|
|
@@ -1433,6 +1540,10 @@ var AuthOidcAddon = class extends BaseAddon {
|
|
|
1433
1540
|
}
|
|
1434
1541
|
}
|
|
1435
1542
|
};
|
|
1543
|
+
/** Minimal standalone page for a refused app sign-in request (D858) - no assets, no script. */
|
|
1544
|
+
function appRefusalPage(message) {
|
|
1545
|
+
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>`;
|
|
1546
|
+
}
|
|
1436
1547
|
/**
|
|
1437
1548
|
* The browser-binding cookie (`SSO_STATE_COOKIE`). HttpOnly, Lax (the IdP
|
|
1438
1549
|
* returns with a top-level GET), path `/` so it reaches the hub's finish under
|
package/dist/webauthn/_stub.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as e, r as t, t as n } from "./_virtual_mf___mfe_internal__addon_auth_webauthn_widgets__loadShare__react__loadShare__.js-BIIa6vDX.mjs";
|
|
2
2
|
import { n as r, t as i } from "./_virtual_mf___mfe_internal__addon_auth_webauthn_widgets__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-CQ-aEQ9b.mjs";
|
|
3
|
-
import { t as a } from "./_virtual_mf___mfe_internal__addon_auth_webauthn_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-
|
|
3
|
+
import { t as a } from "./_virtual_mf___mfe_internal__addon_auth_webauthn_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DSXWgur5.mjs";
|
|
4
4
|
import { n as o, r as s, t as c } from "./_virtual_mf___mfe_internal__addon_auth_webauthn_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-BL2etuqg.mjs";
|
|
5
5
|
//#region ../../node_modules/lucide-react/dist/esm/shared/src/utils/mergeClasses.js
|
|
6
6
|
var l = (...e) => e.filter((e, t, n) => !!e && e.trim() !== "" && n.indexOf(e) === t).join(" ").trim(), u = (e) => e.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(), d = (e) => e.replace(/^([A-Z])|[\s-_]+(\w)/g, (e, t, n) => n ? n.toUpperCase() : t.toLowerCase()), f = (e) => {
|
|
@@ -18,7 +18,7 @@ var e = {
|
|
|
18
18
|
},
|
|
19
19
|
"@camstack/types": {
|
|
20
20
|
name: "@camstack/types",
|
|
21
|
-
version: "1.2.
|
|
21
|
+
version: "1.2.370",
|
|
22
22
|
scope: ["default"],
|
|
23
23
|
loaded: !1,
|
|
24
24
|
from: "addon_auth_webauthn_widgets",
|
|
@@ -33,7 +33,7 @@ var e = {
|
|
|
33
33
|
},
|
|
34
34
|
"@camstack/ui-library": {
|
|
35
35
|
name: "@camstack/ui-library",
|
|
36
|
-
version: "1.2.
|
|
36
|
+
version: "1.2.289",
|
|
37
37
|
scope: ["default"],
|
|
38
38
|
loaded: !1,
|
|
39
39
|
from: "addon_auth_webauthn_widgets",
|