@iqauth/sdk 2.2.0 → 2.5.0
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/README.md +134 -0
- package/dist/browser-session.d.mts +3 -3
- package/dist/browser-session.d.ts +3 -3
- package/dist/browser-session.js +89 -68
- package/dist/browser-session.mjs +2 -1
- package/dist/browser.d.mts +64 -29
- package/dist/browser.d.ts +64 -29
- package/dist/browser.js +794 -39
- package/dist/browser.mjs +44 -4
- package/dist/bundle-LUKDQYVQ.mjs +374 -0
- package/dist/chunk-3JULWS6F.mjs +106 -0
- package/dist/chunk-5T7GHBX6.mjs +1165 -0
- package/dist/{chunk-M4J6BPK7.mjs → chunk-6TDJJER7.mjs} +12 -3
- package/dist/{chunk-QZB745C2.mjs → chunk-76W5TLQQ.mjs} +264 -211
- package/dist/{chunk-D72UL5HL.mjs → chunk-BVV54LPI.mjs} +36 -4
- package/dist/chunk-LIZYFXH7.mjs +90 -0
- package/dist/chunk-MKKZULZR.mjs +241 -0
- package/dist/chunk-SL3KRS4W.mjs +54 -0
- package/dist/chunk-TKZTCPEK.mjs +232 -0
- package/dist/chunk-UKZLOHZG.mjs +83 -0
- package/dist/chunk-UNYDG2L4.mjs +209 -0
- package/dist/{chunk-MDUHPQMM.mjs → chunk-W3F4JYGP.mjs} +8 -180
- package/dist/{chunk-QEJB7WEQ.mjs → chunk-WQWBJSSS.mjs} +1 -1
- package/dist/cli/index.js +144 -36
- package/dist/cli/index.mjs +1 -1
- package/dist/{client-DXbHb2ul.d.ts → client-BNQe3AgF.d.ts} +3 -67
- package/dist/{client-Dv4v92Mj.d.mts → client-kYlJFgPv.d.mts} +3 -67
- package/dist/doctor-YYNHNMLD.mjs +198 -0
- package/dist/{express-BZmF1llh.d.mts → express-B6_1vBYZ.d.mts} +23 -2
- package/dist/{express-B4o3P8vK.d.ts → express-CHpfa7D_.d.ts} +23 -2
- package/dist/express.d.mts +77 -6
- package/dist/express.d.ts +77 -6
- package/dist/express.js +336 -74
- package/dist/express.mjs +209 -8
- package/dist/fastify.js +103 -72
- package/dist/fastify.mjs +6 -4
- package/dist/hono.js +102 -72
- package/dist/hono.mjs +5 -4
- package/dist/index.d.mts +8 -4
- package/dist/index.d.ts +8 -4
- package/dist/index.js +590 -73
- package/dist/index.mjs +30 -8
- package/dist/locales.d.mts +53 -0
- package/dist/locales.d.ts +53 -0
- package/dist/locales.js +1202 -0
- package/dist/locales.mjs +29 -0
- package/dist/mobile.d.mts +3 -3
- package/dist/mobile.d.ts +3 -3
- package/dist/mobile.js +89 -68
- package/dist/mobile.mjs +2 -1
- package/dist/next.d.mts +10 -1
- package/dist/next.d.ts +10 -1
- package/dist/next.js +101 -1618
- package/dist/next.mjs +9 -9
- package/dist/provisioningBridge-88xjOS2n.d.mts +86 -0
- package/dist/provisioningBridge-DnTfzdZK.d.ts +86 -0
- package/dist/react.d.mts +1349 -10
- package/dist/react.d.ts +1349 -10
- package/dist/react.js +2998 -569
- package/dist/react.mjs +1518 -95
- package/dist/reverify-4UEJXUS6.mjs +16 -0
- package/dist/server/handlers.d.mts +12 -1
- package/dist/server/handlers.d.ts +12 -1
- package/dist/server/handlers.js +12 -3
- package/dist/server/handlers.mjs +2 -2
- package/dist/server.d.mts +5 -4
- package/dist/server.d.ts +5 -4
- package/dist/server.js +188 -73
- package/dist/server.mjs +13 -8
- package/dist/service.d.mts +3 -3
- package/dist/service.d.ts +3 -3
- package/dist/service.js +89 -68
- package/dist/service.mjs +2 -1
- package/dist/signIn-CCY4JE5G.mjs +15 -0
- package/dist/{signIn-D_kP3v-c.d.mts → signIn-CiIBTJIh.d.mts} +232 -4
- package/dist/{signIn-BVDTIA_t.d.ts → signIn-OCr88Zf8.d.ts} +232 -4
- package/dist/test.d.mts +86 -0
- package/dist/test.d.ts +86 -0
- package/dist/test.js +289 -0
- package/dist/test.mjs +9 -0
- package/dist/tokens-DCyzzn8L.d.mts +63 -0
- package/dist/tokens-aHiGFr_E.d.ts +63 -0
- package/dist/types-6bNdxesb.d.mts +196 -0
- package/dist/types-6bNdxesb.d.ts +196 -0
- package/dist/{types-Cxl3bQHt.d.ts → types-DZAflmmq.d.mts} +6 -0
- package/dist/{types-Cxl3bQHt.d.mts → types-DZAflmmq.d.ts} +6 -0
- package/dist/webhooks.d.mts +61 -0
- package/dist/webhooks.d.ts +61 -0
- package/dist/webhooks.js +119 -0
- package/dist/webhooks.mjs +11 -0
- package/dist/ws.d.mts +73 -0
- package/dist/ws.d.ts +73 -0
- package/dist/ws.js +397 -0
- package/dist/ws.mjs +12 -0
- package/package.json +24 -3
- package/dist/doctor-XCI77BQS.mjs +0 -90
package/dist/express.js
CHANGED
|
@@ -445,8 +445,7 @@ function parseMfaResponse(data, browserSessionMode) {
|
|
|
445
445
|
}
|
|
446
446
|
|
|
447
447
|
// src/modules/tokens.ts
|
|
448
|
-
var
|
|
449
|
-
var import_jsonwebtoken = __toESM(require("jsonwebtoken"));
|
|
448
|
+
var import_jose = require("jose");
|
|
450
449
|
var JWKS_CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
451
450
|
var DEFAULT_TOKEN_ISSUER = [
|
|
452
451
|
"https://auth.dispositioniq.com",
|
|
@@ -459,6 +458,24 @@ var DEFAULT_TOKEN_AUDIENCE = [
|
|
|
459
458
|
"iqvalidate"
|
|
460
459
|
];
|
|
461
460
|
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
461
|
+
function decodeProtectedHeader(token) {
|
|
462
|
+
const parts = token.split(".");
|
|
463
|
+
if (parts.length < 2) return null;
|
|
464
|
+
try {
|
|
465
|
+
const padded = parts[0] + "=".repeat((4 - parts[0].length % 4) % 4);
|
|
466
|
+
const b64 = padded.replace(/-/g, "+").replace(/_/g, "/");
|
|
467
|
+
let json;
|
|
468
|
+
if (typeof atob === "function") {
|
|
469
|
+
json = atob(b64);
|
|
470
|
+
} else {
|
|
471
|
+
const { Buffer: Buffer2 } = require("buffer");
|
|
472
|
+
json = Buffer2.from(b64, "base64").toString("utf8");
|
|
473
|
+
}
|
|
474
|
+
return JSON.parse(json);
|
|
475
|
+
} catch {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
462
479
|
var TokensModule = class {
|
|
463
480
|
constructor(baseUrl, options = {}) {
|
|
464
481
|
this.jwksCache = null;
|
|
@@ -469,49 +486,49 @@ var TokensModule = class {
|
|
|
469
486
|
this.defaultClockTolerance = options.clockTolerance ?? DEFAULT_CLOCK_TOLERANCE_SECONDS;
|
|
470
487
|
}
|
|
471
488
|
/**
|
|
472
|
-
* Verify a JWT access token using RS256 via JWKS from
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
* clock tolerance default to client config but can be overridden per call.
|
|
489
|
+
* Verify a JWT access token using RS256/ES256 via JWKS from
|
|
490
|
+
* `/.well-known/jwks.json`. Backed by `jose` (Web Crypto) so it runs on
|
|
491
|
+
* Node, browser, and edge runtimes alike — no `node:crypto` dependency.
|
|
492
|
+
* Caches JWKS for 1 hour and refetches once on unknown `kid`.
|
|
477
493
|
*/
|
|
478
494
|
async verify(token, options = {}) {
|
|
479
|
-
const
|
|
480
|
-
if (!
|
|
495
|
+
const header = decodeProtectedHeader(token);
|
|
496
|
+
if (!header) {
|
|
481
497
|
throw new IQAuthError("TOKEN_INVALID", "Unable to decode token");
|
|
482
498
|
}
|
|
483
|
-
const kid =
|
|
499
|
+
const kid = header.kid;
|
|
484
500
|
if (!kid) {
|
|
485
501
|
throw new IQAuthError("TOKEN_INVALID", "Token missing kid header");
|
|
486
502
|
}
|
|
487
|
-
let
|
|
488
|
-
if (!
|
|
489
|
-
|
|
490
|
-
|
|
503
|
+
let cache = await this.ensureCache();
|
|
504
|
+
if (!cache.byKid.has(kid)) {
|
|
505
|
+
this.jwksCache = null;
|
|
506
|
+
cache = await this.ensureCache();
|
|
491
507
|
}
|
|
492
|
-
if (!
|
|
508
|
+
if (!cache.byKid.has(kid)) {
|
|
493
509
|
throw new IQAuthError("TOKEN_INVALID", `Unknown key ID: ${kid}`);
|
|
494
510
|
}
|
|
495
511
|
const issuer = options.issuer ?? this.defaultIssuer;
|
|
496
512
|
const audience = options.audience ?? this.defaultAudience;
|
|
497
513
|
const clockTolerance = options.clockTolerance ?? this.defaultClockTolerance;
|
|
498
|
-
const algorithms = options.algorithms ?? ["RS256"];
|
|
514
|
+
const algorithms = options.algorithms ?? ["RS256", "ES256"];
|
|
515
|
+
const verifyOptions = {
|
|
516
|
+
algorithms,
|
|
517
|
+
clockTolerance,
|
|
518
|
+
issuer,
|
|
519
|
+
audience
|
|
520
|
+
};
|
|
499
521
|
try {
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
clockTolerance,
|
|
503
|
-
// The jsonwebtoken types insist on tuple types for arrays; runtime
|
|
504
|
-
// accepts plain string[] so we cast to satisfy the compiler.
|
|
505
|
-
issuer,
|
|
506
|
-
audience
|
|
507
|
-
};
|
|
508
|
-
const verified = import_jsonwebtoken.default.verify(token, publicKey, verifyOptions);
|
|
509
|
-
return verified;
|
|
522
|
+
const { payload } = await (0, import_jose.jwtVerify)(token, cache.verifier, verifyOptions);
|
|
523
|
+
return payload;
|
|
510
524
|
} catch (err) {
|
|
525
|
+
if (err instanceof import_jose.errors.JWTExpired) {
|
|
526
|
+
throw new IQAuthError("TOKEN_EXPIRED", "Token has expired");
|
|
527
|
+
}
|
|
528
|
+
if (err instanceof import_jose.errors.JOSEError) {
|
|
529
|
+
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
530
|
+
}
|
|
511
531
|
if (err instanceof Error) {
|
|
512
|
-
if (err.name === "TokenExpiredError") {
|
|
513
|
-
throw new IQAuthError("TOKEN_EXPIRED", "Token has expired");
|
|
514
|
-
}
|
|
515
532
|
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
516
533
|
}
|
|
517
534
|
throw new IQAuthError("TOKEN_INVALID", "Token verification failed");
|
|
@@ -519,29 +536,40 @@ var TokensModule = class {
|
|
|
519
536
|
}
|
|
520
537
|
/**
|
|
521
538
|
* Decode a JWT without verification. Returns null if malformed.
|
|
522
|
-
*
|
|
523
|
-
* @remarks Local decode only — no network call
|
|
524
539
|
*/
|
|
525
540
|
decode(token) {
|
|
526
|
-
|
|
527
|
-
|
|
541
|
+
try {
|
|
542
|
+
const parts = token.split(".");
|
|
543
|
+
if (parts.length < 2) return null;
|
|
544
|
+
const payload = parts[1];
|
|
545
|
+
const padded = payload + "=".repeat((4 - payload.length % 4) % 4);
|
|
546
|
+
const b64 = padded.replace(/-/g, "+").replace(/_/g, "/");
|
|
547
|
+
let json;
|
|
548
|
+
if (typeof atob === "function") {
|
|
549
|
+
json = atob(b64);
|
|
550
|
+
} else {
|
|
551
|
+
const { Buffer: Buffer2 } = require("buffer");
|
|
552
|
+
json = Buffer2.from(b64, "base64").toString("utf8");
|
|
553
|
+
}
|
|
554
|
+
try {
|
|
555
|
+
json = decodeURIComponent(escape(json));
|
|
556
|
+
} catch {
|
|
557
|
+
}
|
|
558
|
+
const claims = JSON.parse(json);
|
|
559
|
+
if (!claims || typeof claims !== "object") return null;
|
|
560
|
+
return claims;
|
|
561
|
+
} catch {
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
528
564
|
}
|
|
529
|
-
/**
|
|
530
|
-
* Check if a token is expired based on the `exp` claim.
|
|
531
|
-
*
|
|
532
|
-
* @remarks Local check only — no network call
|
|
533
|
-
*/
|
|
565
|
+
/** Check if a token is expired based on the `exp` claim. */
|
|
534
566
|
isExpired(token) {
|
|
535
567
|
const claims = this.decode(token);
|
|
536
568
|
if (!claims?.exp) return true;
|
|
537
569
|
const now = Math.floor(Date.now() / 1e3);
|
|
538
570
|
return claims.exp <= now;
|
|
539
571
|
}
|
|
540
|
-
/**
|
|
541
|
-
* Get the claims from a token without verification.
|
|
542
|
-
*
|
|
543
|
-
* @remarks Local decode only — no network call
|
|
544
|
-
*/
|
|
572
|
+
/** Get the claims from a token without verification. */
|
|
545
573
|
getClaims(token) {
|
|
546
574
|
const claims = this.decode(token);
|
|
547
575
|
if (!claims) {
|
|
@@ -549,11 +577,15 @@ var TokensModule = class {
|
|
|
549
577
|
}
|
|
550
578
|
return claims;
|
|
551
579
|
}
|
|
552
|
-
async
|
|
553
|
-
if (
|
|
554
|
-
|
|
580
|
+
async ensureCache() {
|
|
581
|
+
if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) {
|
|
582
|
+
return this.jwksCache;
|
|
583
|
+
}
|
|
584
|
+
await this.refreshJwks();
|
|
585
|
+
if (!this.jwksCache) {
|
|
586
|
+
throw new IQAuthError("INTERNAL_ERROR", "JWKS cache unavailable after refresh");
|
|
555
587
|
}
|
|
556
|
-
return this.jwksCache
|
|
588
|
+
return this.jwksCache;
|
|
557
589
|
}
|
|
558
590
|
async refreshJwks() {
|
|
559
591
|
if (this.inFlightRefresh) {
|
|
@@ -580,35 +612,24 @@ var TokensModule = class {
|
|
|
580
612
|
"Malformed JWKS response: expected { keys: [...] }"
|
|
581
613
|
);
|
|
582
614
|
}
|
|
583
|
-
const
|
|
615
|
+
const byKid = /* @__PURE__ */ new Set();
|
|
584
616
|
for (const key of jwks.keys) {
|
|
585
|
-
if (!key || typeof key.kid !== "string" || typeof key.n !== "string" || typeof key.e !== "string") {
|
|
617
|
+
if (!key || typeof key.kid !== "string" || typeof key.n !== "string" && typeof key.x !== "string" || key.kty === "RSA" && (typeof key.n !== "string" || typeof key.e !== "string")) {
|
|
586
618
|
throw new IQAuthError(
|
|
587
619
|
"INTERNAL_ERROR",
|
|
588
620
|
"Malformed JWKS response: key missing required fields"
|
|
589
621
|
);
|
|
590
622
|
}
|
|
591
|
-
|
|
592
|
-
keys.set(key.kid, pem);
|
|
623
|
+
byKid.add(key.kid);
|
|
593
624
|
}
|
|
594
|
-
|
|
625
|
+
const verifier = (0, import_jose.createLocalJWKSet)({ keys: jwks.keys });
|
|
626
|
+
this.jwksCache = { raw: jwks.keys, byKid, verifier, fetchedAt: Date.now() };
|
|
595
627
|
} finally {
|
|
596
628
|
this.inFlightRefresh = null;
|
|
597
629
|
}
|
|
598
630
|
})();
|
|
599
631
|
return this.inFlightRefresh;
|
|
600
632
|
}
|
|
601
|
-
jwkToPem(jwk) {
|
|
602
|
-
const keyObject = import_crypto.default.createPublicKey({
|
|
603
|
-
key: {
|
|
604
|
-
kty: jwk.kty,
|
|
605
|
-
n: jwk.n,
|
|
606
|
-
e: jwk.e
|
|
607
|
-
},
|
|
608
|
-
format: "jwk"
|
|
609
|
-
});
|
|
610
|
-
return keyObject.export({ type: "spki", format: "pem" });
|
|
611
|
-
}
|
|
612
633
|
/** @internal Exposed for testing — clears JWKS cache */
|
|
613
634
|
clearCache() {
|
|
614
635
|
this.jwksCache = null;
|
|
@@ -816,7 +837,7 @@ var PermissionsModule = class {
|
|
|
816
837
|
};
|
|
817
838
|
|
|
818
839
|
// src/modules/oidc.ts
|
|
819
|
-
var
|
|
840
|
+
var import_crypto = __toESM(require("crypto"));
|
|
820
841
|
var InMemoryOidcStateStore = class {
|
|
821
842
|
constructor() {
|
|
822
843
|
this.map = /* @__PURE__ */ new Map();
|
|
@@ -897,12 +918,12 @@ var OidcModule = class {
|
|
|
897
918
|
* ready to redirect the user to.
|
|
898
919
|
*/
|
|
899
920
|
async createAuthRequest(params) {
|
|
900
|
-
const codeVerifier = base64UrlEncode(
|
|
921
|
+
const codeVerifier = base64UrlEncode(import_crypto.default.randomBytes(32));
|
|
901
922
|
const codeChallenge = base64UrlEncode(
|
|
902
|
-
|
|
923
|
+
import_crypto.default.createHash("sha256").update(codeVerifier).digest()
|
|
903
924
|
);
|
|
904
|
-
const state = base64UrlEncode(
|
|
905
|
-
const nonce = base64UrlEncode(
|
|
925
|
+
const state = base64UrlEncode(import_crypto.default.randomBytes(16));
|
|
926
|
+
const nonce = base64UrlEncode(import_crypto.default.randomBytes(16));
|
|
906
927
|
await this.stateStore.set(state, {
|
|
907
928
|
codeVerifier,
|
|
908
929
|
state,
|
|
@@ -1850,7 +1871,7 @@ function assertPublishableKey(raw, opts) {
|
|
|
1850
1871
|
if (!isValidIssuerUrl(decoded.iss)) {
|
|
1851
1872
|
throw new IQAuthError(
|
|
1852
1873
|
"CONFIG_INVALID",
|
|
1853
|
-
`${ctx}IQAuth publishable key encodes an invalid issuer (iss=${JSON.stringify(decoded.iss)}). Expected a fully-qualified URL like "https://auth.example.com" (scheme required). Regenerate the key from the IQAuth admin console
|
|
1874
|
+
`${ctx}IQAuth publishable key encodes an invalid issuer (iss=${JSON.stringify(decoded.iss)}). Expected a fully-qualified URL like "https://auth.example.com" (scheme required). Regenerate the key from the IQAuth admin console \u2014 the new key will encode a valid issuer URL.`
|
|
1854
1875
|
);
|
|
1855
1876
|
}
|
|
1856
1877
|
return { mode: shapeMatch[1], iss: decoded.iss, appId: decoded.appId, tenantId: decoded.tenantId, kid: decoded.kid, raw };
|
|
@@ -1896,6 +1917,22 @@ function readCookie(req, name) {
|
|
|
1896
1917
|
}
|
|
1897
1918
|
return void 0;
|
|
1898
1919
|
}
|
|
1920
|
+
function compileMatcher(pat) {
|
|
1921
|
+
if (pat instanceof RegExp) return (p) => pat.test(p);
|
|
1922
|
+
const re = new RegExp(
|
|
1923
|
+
"^" + pat.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "::DOUBLE::").replace(/\*/g, "[^/]*").replace(/::DOUBLE::/g, ".*") + "$"
|
|
1924
|
+
);
|
|
1925
|
+
return (p) => re.test(p);
|
|
1926
|
+
}
|
|
1927
|
+
function compileMatchers(pats) {
|
|
1928
|
+
return (pats ?? []).map(compileMatcher);
|
|
1929
|
+
}
|
|
1930
|
+
function pathOf(req) {
|
|
1931
|
+
const r = req;
|
|
1932
|
+
const raw = r.path || r.originalUrl || r.url || "/";
|
|
1933
|
+
const q = raw.indexOf("?");
|
|
1934
|
+
return q >= 0 ? raw.slice(0, q) : raw;
|
|
1935
|
+
}
|
|
1899
1936
|
function clientFromPublishableKey(opts) {
|
|
1900
1937
|
const parsed = assertPublishableKey(opts.publishableKey, { context: "iqAuthMiddleware" });
|
|
1901
1938
|
const issuer = (opts.issuer ?? (parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`)).replace(/\/+$/, "");
|
|
@@ -1923,10 +1960,26 @@ function iqAuthMiddleware(clientOrOptions, options = {}) {
|
|
|
1923
1960
|
onUnauthorized,
|
|
1924
1961
|
onForbidden,
|
|
1925
1962
|
onError,
|
|
1926
|
-
|
|
1927
|
-
|
|
1963
|
+
cookieAware = true,
|
|
1964
|
+
cookieNames,
|
|
1965
|
+
protect,
|
|
1966
|
+
publicRoutes
|
|
1928
1967
|
} = resolvedOptions;
|
|
1968
|
+
const accessCookieName = resolvedOptions.accessCookieName ?? cookieNames?.access ?? DEFAULT_ACCESS_COOKIE;
|
|
1969
|
+
const protectMatchers = compileMatchers(protect);
|
|
1970
|
+
const publicMatchers = compileMatchers(publicRoutes);
|
|
1971
|
+
const hasProtect = protectMatchers.length > 0;
|
|
1972
|
+
const hasPublic = publicMatchers.length > 0;
|
|
1929
1973
|
return async (req, res, next) => {
|
|
1974
|
+
if (hasProtect || hasPublic) {
|
|
1975
|
+
const path = pathOf(req);
|
|
1976
|
+
if (hasPublic && publicMatchers.some((m) => m(path))) {
|
|
1977
|
+
return next();
|
|
1978
|
+
}
|
|
1979
|
+
if (hasProtect && !protectMatchers.some((m) => m(path))) {
|
|
1980
|
+
return next();
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1930
1983
|
let token;
|
|
1931
1984
|
const authHeader = getAuthorizationHeader(req);
|
|
1932
1985
|
if (authHeader && authHeader.startsWith("Bearer ")) {
|
|
@@ -2045,8 +2098,8 @@ function resolve(config) {
|
|
|
2045
2098
|
publishableKey: config.publishableKey,
|
|
2046
2099
|
secretKey: config.secretKey,
|
|
2047
2100
|
issuer: (config.issuer ?? inferredIssuer).replace(/\/+$/, ""),
|
|
2048
|
-
accessCookieName: config.accessCookieName ?? "iqauth_at",
|
|
2049
|
-
refreshCookieName: config.refreshCookieName ?? "iqauth_rt",
|
|
2101
|
+
accessCookieName: config.accessCookieName ?? config.cookieNames?.access ?? "iqauth_at",
|
|
2102
|
+
refreshCookieName: config.refreshCookieName ?? config.cookieNames?.refresh ?? "iqauth_rt",
|
|
2050
2103
|
cookieDomain: config.cookieDomain,
|
|
2051
2104
|
sameSite: config.sameSite ?? "lax",
|
|
2052
2105
|
secure: config.secure ?? true,
|
|
@@ -2200,6 +2253,15 @@ async function handleSignout(config, input) {
|
|
|
2200
2253
|
} catch {
|
|
2201
2254
|
}
|
|
2202
2255
|
}
|
|
2256
|
+
if (input.endSsoSession !== false && input.ssoCookieHeader) {
|
|
2257
|
+
try {
|
|
2258
|
+
await cfg.fetchImpl(`${cfg.issuer}/oidc/sso-logout`, {
|
|
2259
|
+
method: "POST",
|
|
2260
|
+
headers: { Cookie: input.ssoCookieHeader }
|
|
2261
|
+
});
|
|
2262
|
+
} catch {
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2203
2265
|
return {
|
|
2204
2266
|
status: 200,
|
|
2205
2267
|
body: { success: true, data: { signedOut: true } },
|
|
@@ -2208,6 +2270,78 @@ async function handleSignout(config, input) {
|
|
|
2208
2270
|
}
|
|
2209
2271
|
|
|
2210
2272
|
// src/express.ts
|
|
2273
|
+
var PKCE_COOKIE = "iqauth_pkce";
|
|
2274
|
+
function escapeHtml(s) {
|
|
2275
|
+
return s.replace(/[&<>"']/g, (c) => {
|
|
2276
|
+
switch (c) {
|
|
2277
|
+
case "&":
|
|
2278
|
+
return "&";
|
|
2279
|
+
case "<":
|
|
2280
|
+
return "<";
|
|
2281
|
+
case ">":
|
|
2282
|
+
return ">";
|
|
2283
|
+
case '"':
|
|
2284
|
+
return """;
|
|
2285
|
+
case "'":
|
|
2286
|
+
return "'";
|
|
2287
|
+
default:
|
|
2288
|
+
return c;
|
|
2289
|
+
}
|
|
2290
|
+
});
|
|
2291
|
+
}
|
|
2292
|
+
function appendErrorParam(path, errorCode) {
|
|
2293
|
+
const sep = path.includes("?") ? "&" : "?";
|
|
2294
|
+
return `${path}${sep}error=${encodeURIComponent(errorCode)}`;
|
|
2295
|
+
}
|
|
2296
|
+
function defaultBrandedSpinner(args) {
|
|
2297
|
+
return `<!doctype html>
|
|
2298
|
+
<html lang="en">
|
|
2299
|
+
<head>
|
|
2300
|
+
<meta charset="utf-8" />
|
|
2301
|
+
<title>Signing you in\u2026</title>
|
|
2302
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
2303
|
+
<style>
|
|
2304
|
+
body { margin:0; min-height:100vh; display:flex; align-items:center; justify-content:center; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; background:#f7f7f8; color:#111; }
|
|
2305
|
+
.iqauth-card { text-align:center; padding:2rem; }
|
|
2306
|
+
.iqauth-spinner { width:36px; height:36px; border:3px solid #e5e7eb; border-top-color:#111; border-radius:50%; margin:0 auto 1rem; animation:iqauth-spin 0.9s linear infinite; }
|
|
2307
|
+
@keyframes iqauth-spin { to { transform: rotate(360deg); } }
|
|
2308
|
+
.iqauth-msg { font-size:0.95rem; color:#374151; }
|
|
2309
|
+
</style>
|
|
2310
|
+
</head>
|
|
2311
|
+
<body>
|
|
2312
|
+
<div class="iqauth-card" data-testid="iqauth-inline-callback-spinner">
|
|
2313
|
+
<div class="iqauth-spinner" aria-hidden="true"></div>
|
|
2314
|
+
<div class="iqauth-msg">Signing you in\u2026</div>
|
|
2315
|
+
</div>
|
|
2316
|
+
<script>
|
|
2317
|
+
(function(){
|
|
2318
|
+
var code = ${JSON.stringify(args.code)};
|
|
2319
|
+
var state = ${JSON.stringify(args.state)};
|
|
2320
|
+
var errorPath = ${JSON.stringify(args.errorPath || "")};
|
|
2321
|
+
function fail(reason){
|
|
2322
|
+
if (errorPath) { window.location.replace(errorPath + (errorPath.indexOf("?")>=0?"&":"?") + "error=" + encodeURIComponent(reason)); return; }
|
|
2323
|
+
window.location.replace("/");
|
|
2324
|
+
}
|
|
2325
|
+
var verifier = (document.cookie.split('; ').find(function(c){return c.indexOf('${PKCE_COOKIE}=')===0;})||'').slice(${PKCE_COOKIE.length + 1});
|
|
2326
|
+
try { verifier = decodeURIComponent(verifier); } catch (e) {}
|
|
2327
|
+
fetch(${JSON.stringify(args.exchangePath)}, {
|
|
2328
|
+
method: "POST",
|
|
2329
|
+
credentials: "include",
|
|
2330
|
+
headers: { "Content-Type": "application/json" },
|
|
2331
|
+
body: JSON.stringify({ code: code, state: state, codeVerifier: verifier, redirectUri: window.location.origin + window.location.pathname })
|
|
2332
|
+
}).then(function(r){ return r.json().then(function(j){ return { status:r.status, body:j }; }); })
|
|
2333
|
+
.then(function(out){
|
|
2334
|
+
if (out.status >= 400) { fail((out.body && out.body.error && out.body.error.code) || "exchange_failed"); return; }
|
|
2335
|
+
var dest = (out.body && out.body.returnTo) || sessionStorage.getItem("iqauth_return_to") || "/";
|
|
2336
|
+
sessionStorage.removeItem("iqauth_return_to");
|
|
2337
|
+
window.location.replace(dest);
|
|
2338
|
+
})
|
|
2339
|
+
.catch(function(){ fail("network_error"); });
|
|
2340
|
+
})();
|
|
2341
|
+
</script>
|
|
2342
|
+
</body>
|
|
2343
|
+
</html>`;
|
|
2344
|
+
}
|
|
2211
2345
|
function applyHandlerResponse(res, hr) {
|
|
2212
2346
|
for (const c of hr.cookies) {
|
|
2213
2347
|
if (typeof res.cookie === "function") {
|
|
@@ -2271,6 +2405,8 @@ function iqAuth(options) {
|
|
|
2271
2405
|
if (mountHelpers && path.startsWith(mount + "/")) return next();
|
|
2272
2406
|
return verify(req, res, next);
|
|
2273
2407
|
};
|
|
2408
|
+
const inline = options.inlineCallback === true ? {} : options.inlineCallback && typeof options.inlineCallback === "object" ? options.inlineCallback : null;
|
|
2409
|
+
const inlineBranded = inline?.branded === true ? {} : inline?.branded && typeof inline.branded === "object" ? inline.branded : null;
|
|
2274
2410
|
const attachHelpers = (app) => {
|
|
2275
2411
|
app.post(`${mount}/callback`, async (req, res) => {
|
|
2276
2412
|
const body = readBody(req);
|
|
@@ -2281,6 +2417,131 @@ function iqAuth(options) {
|
|
|
2281
2417
|
});
|
|
2282
2418
|
applyHandlerResponse(res, hr);
|
|
2283
2419
|
});
|
|
2420
|
+
if (inline && typeof app.get === "function") {
|
|
2421
|
+
const callbackPath = `${mount}/callback`;
|
|
2422
|
+
const exchangePath = `${callbackPath}/exchange`;
|
|
2423
|
+
const stateCookie = inline.stateCookieName ?? "iqauth_state";
|
|
2424
|
+
const returnToCookie = inline.returnToCookieName ?? "iqauth_return_to";
|
|
2425
|
+
const errorPath = inline.errorPath;
|
|
2426
|
+
const clearCookie = (res, name) => {
|
|
2427
|
+
if (typeof res.clearCookie === "function") {
|
|
2428
|
+
res.clearCookie(name, { path: "/" });
|
|
2429
|
+
return;
|
|
2430
|
+
}
|
|
2431
|
+
const existing = res.getHeader?.("Set-Cookie") || [];
|
|
2432
|
+
const list = Array.isArray(existing) ? existing : [existing];
|
|
2433
|
+
list.push(`${name}=; Path=/; Max-Age=0; SameSite=Lax`);
|
|
2434
|
+
res.setHeader?.("Set-Cookie", list);
|
|
2435
|
+
};
|
|
2436
|
+
const failPlain = (res, errorCode, fallback) => {
|
|
2437
|
+
if (errorPath) {
|
|
2438
|
+
const dest = appendErrorParam(errorPath, errorCode);
|
|
2439
|
+
if (typeof res.redirect === "function") return res.redirect(302, dest);
|
|
2440
|
+
res.status(302);
|
|
2441
|
+
res.setHeader?.("Location", dest);
|
|
2442
|
+
return res.end?.();
|
|
2443
|
+
}
|
|
2444
|
+
fallback();
|
|
2445
|
+
};
|
|
2446
|
+
if (inlineBranded) {
|
|
2447
|
+
const render = inlineBranded.render ?? defaultBrandedSpinner;
|
|
2448
|
+
app.get(callbackPath, (req, res) => {
|
|
2449
|
+
const q = req.query || {};
|
|
2450
|
+
const html = render({
|
|
2451
|
+
issuer,
|
|
2452
|
+
exchangePath,
|
|
2453
|
+
code: escapeHtml(q.code ?? ""),
|
|
2454
|
+
state: escapeHtml(q.state ?? ""),
|
|
2455
|
+
errorPath: errorPath ?? ""
|
|
2456
|
+
});
|
|
2457
|
+
res.status(200);
|
|
2458
|
+
if (typeof res.set === "function") res.set("Content-Type", "text/html; charset=utf-8");
|
|
2459
|
+
else if (typeof res.setHeader === "function") res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
2460
|
+
if (typeof res.send === "function") res.send(html);
|
|
2461
|
+
else res.end?.(html);
|
|
2462
|
+
});
|
|
2463
|
+
app.post(exchangePath, async (req, res) => {
|
|
2464
|
+
const body = readBody(req);
|
|
2465
|
+
const stateFromBody = body.state || void 0;
|
|
2466
|
+
const stateFromCookie = readCookieFromReq(req, stateCookie);
|
|
2467
|
+
if (stateFromCookie && stateFromBody !== stateFromCookie) {
|
|
2468
|
+
clearCookie(res, stateCookie);
|
|
2469
|
+
res.status(400);
|
|
2470
|
+
return res.json ? res.json({ success: false, error: { code: "STATE_MISMATCH", message: "OAuth state mismatch" } }) : res.end?.(JSON.stringify({ success: false, error: { code: "STATE_MISMATCH", message: "OAuth state mismatch" } }));
|
|
2471
|
+
}
|
|
2472
|
+
const hr = await handleCallback(helperConfig, {
|
|
2473
|
+
code: body.code,
|
|
2474
|
+
codeVerifier: body.codeVerifier || readCookieFromReq(req, PKCE_COOKIE) || "",
|
|
2475
|
+
redirectUri: body.redirectUri
|
|
2476
|
+
});
|
|
2477
|
+
clearCookie(res, stateCookie);
|
|
2478
|
+
clearCookie(res, PKCE_COOKIE);
|
|
2479
|
+
const returnTo = readCookieFromReq(req, returnToCookie) || hr.body?.returnTo || "/";
|
|
2480
|
+
if (hr.status < 400) clearCookie(res, returnToCookie);
|
|
2481
|
+
const enriched = {
|
|
2482
|
+
...hr,
|
|
2483
|
+
body: { ...hr.body, returnTo }
|
|
2484
|
+
};
|
|
2485
|
+
applyHandlerResponse(res, enriched);
|
|
2486
|
+
});
|
|
2487
|
+
} else {
|
|
2488
|
+
app.get(callbackPath, async (req, res) => {
|
|
2489
|
+
const q = req.query || {};
|
|
2490
|
+
const code = q.code;
|
|
2491
|
+
if (!code) {
|
|
2492
|
+
return failPlain(res, "missing_code", () => {
|
|
2493
|
+
res.status(400);
|
|
2494
|
+
if (res.json) res.json({ success: false, error: { code: "MISSING_CODE", message: "Missing authorization code" } });
|
|
2495
|
+
else res.end?.("Missing authorization code");
|
|
2496
|
+
});
|
|
2497
|
+
}
|
|
2498
|
+
const stateFromQuery = q.state;
|
|
2499
|
+
const stateFromCookie = readCookieFromReq(req, stateCookie);
|
|
2500
|
+
if (stateFromCookie && stateFromQuery !== stateFromCookie) {
|
|
2501
|
+
clearCookie(res, stateCookie);
|
|
2502
|
+
return failPlain(res, "state_mismatch", () => {
|
|
2503
|
+
res.status(400);
|
|
2504
|
+
if (res.json) res.json({ success: false, error: { code: "STATE_MISMATCH", message: "OAuth state mismatch" } });
|
|
2505
|
+
else res.end?.("OAuth state mismatch");
|
|
2506
|
+
});
|
|
2507
|
+
}
|
|
2508
|
+
const codeVerifier = readCookieFromReq(req, PKCE_COOKIE) || "";
|
|
2509
|
+
const proto = req.headers?.["x-forwarded-proto"] || req.protocol || "https";
|
|
2510
|
+
const host = req.headers?.["x-forwarded-host"] || req.headers?.host || "";
|
|
2511
|
+
const redirectUri = `${proto}://${host}${callbackPath}`;
|
|
2512
|
+
const hr = await handleCallback(helperConfig, { code, codeVerifier, redirectUri });
|
|
2513
|
+
for (const c of hr.cookies) {
|
|
2514
|
+
if (typeof res.cookie === "function") {
|
|
2515
|
+
const opts = {
|
|
2516
|
+
httpOnly: c.httpOnly,
|
|
2517
|
+
secure: c.secure,
|
|
2518
|
+
sameSite: c.sameSite,
|
|
2519
|
+
path: c.path,
|
|
2520
|
+
maxAge: c.maxAge * 1e3
|
|
2521
|
+
};
|
|
2522
|
+
if (c.domain) opts.domain = c.domain;
|
|
2523
|
+
res.cookie(c.name, c.value, opts);
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
clearCookie(res, stateCookie);
|
|
2527
|
+
clearCookie(res, PKCE_COOKIE);
|
|
2528
|
+
if (hr.status >= 400) {
|
|
2529
|
+
const code2 = hr.body?.error?.code || "exchange_failed";
|
|
2530
|
+
return failPlain(res, code2, () => {
|
|
2531
|
+
res.status(hr.status);
|
|
2532
|
+
if (res.json) res.json(hr.body);
|
|
2533
|
+
else res.end?.(JSON.stringify(hr.body));
|
|
2534
|
+
});
|
|
2535
|
+
}
|
|
2536
|
+
const returnTo = readCookieFromReq(req, returnToCookie) || hr.body?.returnTo || "/";
|
|
2537
|
+
clearCookie(res, returnToCookie);
|
|
2538
|
+
if (typeof res.redirect === "function") return res.redirect(302, returnTo);
|
|
2539
|
+
res.status(302);
|
|
2540
|
+
if (typeof res.setHeader === "function") res.setHeader("Location", returnTo);
|
|
2541
|
+
res.end?.();
|
|
2542
|
+
});
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2284
2545
|
app.post(`${mount}/refresh`, async (req, res) => {
|
|
2285
2546
|
const body = readBody(req);
|
|
2286
2547
|
const refreshToken = body.refreshToken || readCookieFromReq(req, refreshCookie);
|
|
@@ -2289,7 +2550,8 @@ function iqAuth(options) {
|
|
|
2289
2550
|
});
|
|
2290
2551
|
app.post(`${mount}/signout`, async (req, res) => {
|
|
2291
2552
|
const accessToken = req.headers?.authorization?.replace(/^Bearer /i, "") || readCookieFromReq(req, accessCookie);
|
|
2292
|
-
const
|
|
2553
|
+
const ssoCookieHeader = req.headers?.cookie;
|
|
2554
|
+
const hr = await handleSignout(helperConfig, { accessToken, ssoCookieHeader });
|
|
2293
2555
|
applyHandlerResponse(res, hr);
|
|
2294
2556
|
});
|
|
2295
2557
|
};
|