@camstack/addon-auth 1.2.144 → 1.2.146

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.
@@ -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-0YTEFDYk.js");
5
+ const require_dist = require("../dist-DtqB-MTf.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
- while (this.pendingStates.size >= MAX_PENDING_STATES) {
1248
- const oldest = this.pendingStates.keys().next();
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.pendingStates.get(state);
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,53 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
1358
1398
  reply.send({ error: "Malformed link request" });
1359
1399
  return;
1360
1400
  }
1361
- const popupParam = req.query["popup"];
1362
- const state = node_crypto.randomBytes(16).toString("base64url");
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
+ const reason = parsed.ok ? "combined-flow" : parsed.reason;
1412
+ reply.code(303);
1413
+ reply.header("Cache-Control", "no-store");
1414
+ reply.header("Location", `${require_dist.APP_REFUSED_PATH}?reason=${reason}`);
1415
+ reply.send("");
1416
+ return;
1417
+ }
1418
+ app = parsed.request;
1419
+ }
1420
+ const state = `${app !== void 0 ? "a." : ""}${node_crypto.randomBytes(16).toString("base64url")}`;
1363
1421
  const browserState = node_crypto.randomBytes(32).toString("base64url");
1364
1422
  try {
1365
1423
  const { url, returnBase } = await this.prepareLogin(providerId, state, {
1366
1424
  ...linkParam !== void 0 ? { linkIntent: linkParam } : {},
1367
1425
  headers: req.headers,
1368
1426
  popup: popupParam === "1",
1369
- browserBinding: node_crypto.createHash("sha256").update(browserState).digest("hex")
1427
+ browserBinding: node_crypto.createHash("sha256").update(browserState).digest("hex"),
1428
+ ...app !== void 0 ? { app } : {}
1370
1429
  });
1371
1430
  reply.code(302);
1372
1431
  reply.header("Set-Cookie", ssoStateCookie(browserState, returnBase.startsWith("https:")));
1373
1432
  reply.header("Location", url);
1374
1433
  reply.send("");
1375
1434
  } catch (err) {
1435
+ if (err instanceof PendingStatesFullError) {
1436
+ this.ctx.logger.warn("OIDC sign-in refused: too many in progress", { meta: { provider: providerId } });
1437
+ if (app !== void 0) {
1438
+ reply.code(302);
1439
+ reply.header("Location", require_dist.appFlowLocation(app.redirect, { error: "too-many-requests" }, app.state));
1440
+ reply.send("");
1441
+ return;
1442
+ }
1443
+ reply.code(429);
1444
+ reply.header("Retry-After", "30");
1445
+ reply.send({ error: "Too many sign-ins in progress - try again shortly" });
1446
+ return;
1447
+ }
1376
1448
  reply.code(500);
1377
1449
  reply.send({
1378
1450
  error: "OIDC start failed",
@@ -1391,6 +1463,27 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
1391
1463
  const code = query["code"];
1392
1464
  const state = query["state"];
1393
1465
  const errorParam = query["error"];
1466
+ const pendingApp = state !== void 0 ? this.livePending(state)?.app : void 0;
1467
+ const appFlowState = state?.startsWith("a.") === true;
1468
+ const answerApp = (error) => {
1469
+ this.ctx.logger.warn("OIDC app sign-in refused at callback", { meta: {
1470
+ provider: providerId,
1471
+ error
1472
+ } });
1473
+ if (state !== void 0) this.pendingStates.delete(state);
1474
+ reply.code(302);
1475
+ reply.header("Cache-Control", "no-store");
1476
+ reply.header("Location", pendingApp !== void 0 ? require_dist.appFlowLocation(pendingApp.redirect, { error }, pendingApp.state) : require_dist.appFlowLocation(require_dist.APP_FLOW_DEFAULT_REDIRECT, { error }));
1477
+ reply.send("");
1478
+ };
1479
+ if (appFlowState && pendingApp === void 0) {
1480
+ answerApp(errorParam ? "idp-error" : "state-expired");
1481
+ return;
1482
+ }
1483
+ if (errorParam && pendingApp !== void 0) {
1484
+ answerApp("idp-error");
1485
+ return;
1486
+ }
1394
1487
  if (errorParam) {
1395
1488
  reply.code(400);
1396
1489
  reply.send({
@@ -1408,6 +1501,10 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
1408
1501
  try {
1409
1502
  const result = await this.handleCallback(code, state);
1410
1503
  if (result.providerId !== providerId) {
1504
+ if (result.app !== void 0) {
1505
+ answerApp("provider-mismatch");
1506
+ return;
1507
+ }
1411
1508
  reply.code(400);
1412
1509
  reply.send({ error: "OIDC callback does not match the provider the login started at" });
1413
1510
  return;
@@ -1422,14 +1519,25 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
1422
1519
  ...result.linkIntent !== void 0 ? { linkIntent: result.linkIntent } : {},
1423
1520
  issuer: result.issuer,
1424
1521
  jti: node_crypto.randomBytes(16).toString("base64url"),
1425
- ...result.browserBinding !== void 0 ? { browserBinding: result.browserBinding } : {}
1522
+ ...result.browserBinding !== void 0 ? { browserBinding: result.browserBinding } : {},
1523
+ ...result.app !== void 0 ? { app: {
1524
+ codeChallenge: result.app.codeChallenge,
1525
+ state: result.app.state,
1526
+ purpose: result.app.purpose,
1527
+ redirect: result.app.redirect
1528
+ } } : {}
1426
1529
  } });
1427
1530
  const finish = new URLSearchParams({ bridge: bridgeToken });
1428
1531
  if (result.popup) finish.set("popup", "1");
1532
+ const finishPath = result.app !== void 0 ? "finish-app" : "finish";
1429
1533
  reply.code(302);
1430
- reply.header("Location", `${result.returnBase}/api/auth/sso/finish?${finish.toString()}`);
1534
+ reply.header("Location", `${result.returnBase}/api/auth/sso/${finishPath}?${finish.toString()}`);
1431
1535
  reply.send("");
1432
1536
  } catch (err) {
1537
+ if (pendingApp !== void 0) {
1538
+ answerApp(err instanceof StepUpNotFreshError ? "step-up-not-fresh" : "idp-failed");
1539
+ return;
1540
+ }
1433
1541
  reply.code(500);
1434
1542
  reply.send({
1435
1543
  error: "OIDC callback failed",
@@ -1,4 +1,4 @@
1
- import { S as toSecretFieldView, _ as SECRET_CLEAR, a as authProviderCapability, b as isSecretClear, c as loginMethodCapability, d as pickRequestOrigin, f as requestMountPrefix, g as BaseAddon, h as errMsg, i as advertisedHubOrigins, l as normalizeOrigin, n as addonRoutesCapability, o as buildAddonRouteProvider, p as resolveIconKey, s as claimedRequestOrigins, t as SSO_STATE_COOKIE, u as oidcConnectionsCapability, v as arrayItemFieldPath, x as isSecretMarker, y as globalSecretScope } from "../dist-ChwITXNa.mjs";
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_REFUSED_PATH, 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-BtJOajm3.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
- while (this.pendingStates.size >= MAX_PENDING_STATES) {
1243
- const oldest = this.pendingStates.keys().next();
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.pendingStates.get(state);
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,53 @@ var AuthOidcAddon = class extends BaseAddon {
1353
1393
  reply.send({ error: "Malformed link request" });
1354
1394
  return;
1355
1395
  }
1356
- const popupParam = req.query["popup"];
1357
- const state = crypto$1.randomBytes(16).toString("base64url");
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
+ const reason = parsed.ok ? "combined-flow" : parsed.reason;
1407
+ reply.code(303);
1408
+ reply.header("Cache-Control", "no-store");
1409
+ reply.header("Location", `${APP_REFUSED_PATH}?reason=${reason}`);
1410
+ reply.send("");
1411
+ return;
1412
+ }
1413
+ app = parsed.request;
1414
+ }
1415
+ const state = `${app !== void 0 ? "a." : ""}${crypto$1.randomBytes(16).toString("base64url")}`;
1358
1416
  const browserState = crypto$1.randomBytes(32).toString("base64url");
1359
1417
  try {
1360
1418
  const { url, returnBase } = await this.prepareLogin(providerId, state, {
1361
1419
  ...linkParam !== void 0 ? { linkIntent: linkParam } : {},
1362
1420
  headers: req.headers,
1363
1421
  popup: popupParam === "1",
1364
- browserBinding: crypto$1.createHash("sha256").update(browserState).digest("hex")
1422
+ browserBinding: crypto$1.createHash("sha256").update(browserState).digest("hex"),
1423
+ ...app !== void 0 ? { app } : {}
1365
1424
  });
1366
1425
  reply.code(302);
1367
1426
  reply.header("Set-Cookie", ssoStateCookie(browserState, returnBase.startsWith("https:")));
1368
1427
  reply.header("Location", url);
1369
1428
  reply.send("");
1370
1429
  } catch (err) {
1430
+ if (err instanceof PendingStatesFullError) {
1431
+ this.ctx.logger.warn("OIDC sign-in refused: too many in progress", { meta: { provider: providerId } });
1432
+ if (app !== void 0) {
1433
+ reply.code(302);
1434
+ reply.header("Location", appFlowLocation(app.redirect, { error: "too-many-requests" }, app.state));
1435
+ reply.send("");
1436
+ return;
1437
+ }
1438
+ reply.code(429);
1439
+ reply.header("Retry-After", "30");
1440
+ reply.send({ error: "Too many sign-ins in progress - try again shortly" });
1441
+ return;
1442
+ }
1371
1443
  reply.code(500);
1372
1444
  reply.send({
1373
1445
  error: "OIDC start failed",
@@ -1386,6 +1458,27 @@ var AuthOidcAddon = class extends BaseAddon {
1386
1458
  const code = query["code"];
1387
1459
  const state = query["state"];
1388
1460
  const errorParam = query["error"];
1461
+ const pendingApp = state !== void 0 ? this.livePending(state)?.app : void 0;
1462
+ const appFlowState = state?.startsWith("a.") === true;
1463
+ const answerApp = (error) => {
1464
+ this.ctx.logger.warn("OIDC app sign-in refused at callback", { meta: {
1465
+ provider: providerId,
1466
+ error
1467
+ } });
1468
+ if (state !== void 0) this.pendingStates.delete(state);
1469
+ reply.code(302);
1470
+ reply.header("Cache-Control", "no-store");
1471
+ reply.header("Location", pendingApp !== void 0 ? appFlowLocation(pendingApp.redirect, { error }, pendingApp.state) : appFlowLocation(APP_FLOW_DEFAULT_REDIRECT, { error }));
1472
+ reply.send("");
1473
+ };
1474
+ if (appFlowState && pendingApp === void 0) {
1475
+ answerApp(errorParam ? "idp-error" : "state-expired");
1476
+ return;
1477
+ }
1478
+ if (errorParam && pendingApp !== void 0) {
1479
+ answerApp("idp-error");
1480
+ return;
1481
+ }
1389
1482
  if (errorParam) {
1390
1483
  reply.code(400);
1391
1484
  reply.send({
@@ -1403,6 +1496,10 @@ var AuthOidcAddon = class extends BaseAddon {
1403
1496
  try {
1404
1497
  const result = await this.handleCallback(code, state);
1405
1498
  if (result.providerId !== providerId) {
1499
+ if (result.app !== void 0) {
1500
+ answerApp("provider-mismatch");
1501
+ return;
1502
+ }
1406
1503
  reply.code(400);
1407
1504
  reply.send({ error: "OIDC callback does not match the provider the login started at" });
1408
1505
  return;
@@ -1417,14 +1514,25 @@ var AuthOidcAddon = class extends BaseAddon {
1417
1514
  ...result.linkIntent !== void 0 ? { linkIntent: result.linkIntent } : {},
1418
1515
  issuer: result.issuer,
1419
1516
  jti: crypto$1.randomBytes(16).toString("base64url"),
1420
- ...result.browserBinding !== void 0 ? { browserBinding: result.browserBinding } : {}
1517
+ ...result.browserBinding !== void 0 ? { browserBinding: result.browserBinding } : {},
1518
+ ...result.app !== void 0 ? { app: {
1519
+ codeChallenge: result.app.codeChallenge,
1520
+ state: result.app.state,
1521
+ purpose: result.app.purpose,
1522
+ redirect: result.app.redirect
1523
+ } } : {}
1421
1524
  } });
1422
1525
  const finish = new URLSearchParams({ bridge: bridgeToken });
1423
1526
  if (result.popup) finish.set("popup", "1");
1527
+ const finishPath = result.app !== void 0 ? "finish-app" : "finish";
1424
1528
  reply.code(302);
1425
- reply.header("Location", `${result.returnBase}/api/auth/sso/finish?${finish.toString()}`);
1529
+ reply.header("Location", `${result.returnBase}/api/auth/sso/${finishPath}?${finish.toString()}`);
1426
1530
  reply.send("");
1427
1531
  } catch (err) {
1532
+ if (pendingApp !== void 0) {
1533
+ answerApp(err instanceof StepUpNotFreshError ? "step-up-not-fresh" : "idp-failed");
1534
+ return;
1535
+ }
1428
1536
  reply.code(500);
1429
1537
  reply.send({
1430
1538
  error: "OIDC callback failed",
@@ -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-xhY74yma.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-C_j9tv24.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.365",
21
+ version: "1.2.372",
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.288",
36
+ version: "1.2.290",
37
37
  scope: ["default"],
38
38
  loaded: !1,
39
39
  from: "addon_auth_webauthn_widgets",