@iqauth/sdk 2.1.0 → 2.3.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 +43 -3
- package/dist/browser-session.d.mts +1 -2
- package/dist/browser-session.d.ts +1 -2
- package/dist/browser-session.js +89 -68
- package/dist/browser-session.mjs +2 -1
- package/dist/browser.d.mts +2 -2
- package/dist/browser.d.ts +2 -2
- package/dist/browser.js +69 -7
- package/dist/browser.mjs +2 -2
- package/dist/{chunk-ZESHDJDU.mjs → chunk-EKTNEZIH.mjs} +5 -8
- package/dist/{chunk-JQRTY5MY.mjs → chunk-KGEPDXHU.mjs} +12 -8
- package/dist/{chunk-S3M2IXCE.mjs → chunk-RACIPVLD.mjs} +15 -9
- package/dist/chunk-UNYDG2L4.mjs +209 -0
- package/dist/{chunk-MDUHPQMM.mjs → chunk-W3F4JYGP.mjs} +8 -180
- package/dist/chunk-WQWBJSSS.mjs +119 -0
- package/dist/cli/index.js +21 -0
- package/dist/cli/index.mjs +1 -1
- package/dist/{client-DXbHb2ul.d.ts → client-DTX4hNdS.d.ts} +16 -21
- package/dist/{client-Dv4v92Mj.d.mts → client-vdh2a9fJ.d.mts} +16 -21
- package/dist/{doctor-OHJRZBBT.mjs → doctor-A5E7LSFW.mjs} +2 -1
- package/dist/{express-BZmF1llh.d.mts → express-A0-dWEMy.d.mts} +1 -1
- package/dist/{express-B4o3P8vK.d.ts → express-Bo_pJKHN.d.ts} +1 -1
- package/dist/express.d.mts +75 -5
- package/dist/express.d.ts +75 -5
- package/dist/express.js +353 -94
- package/dist/express.mjs +210 -12
- package/dist/fastify.js +153 -88
- package/dist/fastify.mjs +10 -9
- package/dist/hono.js +152 -88
- package/dist/hono.mjs +9 -9
- package/dist/index.d.mts +3 -4
- package/dist/index.d.ts +3 -4
- package/dist/index.js +148 -72
- package/dist/index.mjs +16 -12
- package/dist/mobile.d.mts +1 -2
- package/dist/mobile.d.ts +1 -2
- package/dist/mobile.js +89 -68
- package/dist/mobile.mjs +2 -1
- package/dist/next.d.mts +9 -0
- package/dist/next.d.ts +9 -0
- package/dist/next.js +164 -1649
- package/dist/next.mjs +13 -16
- package/dist/{publishableKey-B5DIK81A.d.mts → publishableKey-BaR0HoAH.d.mts} +10 -1
- package/dist/{publishableKey-B5DIK81A.d.ts → publishableKey-BaR0HoAH.d.ts} +10 -1
- package/dist/react.d.mts +35 -3
- package/dist/react.d.ts +35 -3
- package/dist/react.js +78 -18
- package/dist/react.mjs +14 -2
- package/dist/server/handlers.d.mts +2 -0
- package/dist/server/handlers.d.ts +2 -0
- package/dist/server/handlers.js +72 -17
- package/dist/server/handlers.mjs +3 -2
- package/dist/server.d.mts +2 -3
- package/dist/server.d.ts +2 -3
- package/dist/server.js +151 -89
- package/dist/server.mjs +7 -6
- package/dist/service.d.mts +1 -2
- package/dist/service.d.ts +1 -2
- package/dist/service.js +89 -68
- package/dist/service.mjs +2 -1
- package/dist/{signIn-CEMdUAwd.d.mts → signIn-Cd0P4y9d.d.mts} +9 -1
- package/dist/{signIn-VRNzlNyG.d.ts → signIn-DKakyzeu.d.ts} +9 -1
- package/package.json +3 -2
- package/dist/chunk-5WFR6Y33.mjs +0 -59
package/dist/server.js
CHANGED
|
@@ -453,8 +453,7 @@ function parseMfaResponse(data, browserSessionMode) {
|
|
|
453
453
|
}
|
|
454
454
|
|
|
455
455
|
// src/modules/tokens.ts
|
|
456
|
-
var
|
|
457
|
-
var import_jsonwebtoken = __toESM(require("jsonwebtoken"));
|
|
456
|
+
var import_jose = require("jose");
|
|
458
457
|
var JWKS_CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
459
458
|
var DEFAULT_TOKEN_ISSUER = [
|
|
460
459
|
"https://auth.dispositioniq.com",
|
|
@@ -467,6 +466,24 @@ var DEFAULT_TOKEN_AUDIENCE = [
|
|
|
467
466
|
"iqvalidate"
|
|
468
467
|
];
|
|
469
468
|
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
469
|
+
function decodeProtectedHeader(token) {
|
|
470
|
+
const parts = token.split(".");
|
|
471
|
+
if (parts.length < 2) return null;
|
|
472
|
+
try {
|
|
473
|
+
const padded = parts[0] + "=".repeat((4 - parts[0].length % 4) % 4);
|
|
474
|
+
const b64 = padded.replace(/-/g, "+").replace(/_/g, "/");
|
|
475
|
+
let json;
|
|
476
|
+
if (typeof atob === "function") {
|
|
477
|
+
json = atob(b64);
|
|
478
|
+
} else {
|
|
479
|
+
const { Buffer: Buffer2 } = require("buffer");
|
|
480
|
+
json = Buffer2.from(b64, "base64").toString("utf8");
|
|
481
|
+
}
|
|
482
|
+
return JSON.parse(json);
|
|
483
|
+
} catch {
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
470
487
|
var TokensModule = class {
|
|
471
488
|
constructor(baseUrl, options = {}) {
|
|
472
489
|
this.jwksCache = null;
|
|
@@ -477,49 +494,49 @@ var TokensModule = class {
|
|
|
477
494
|
this.defaultClockTolerance = options.clockTolerance ?? DEFAULT_CLOCK_TOLERANCE_SECONDS;
|
|
478
495
|
}
|
|
479
496
|
/**
|
|
480
|
-
* Verify a JWT access token using RS256 via JWKS from
|
|
481
|
-
*
|
|
482
|
-
*
|
|
483
|
-
*
|
|
484
|
-
* clock tolerance default to client config but can be overridden per call.
|
|
497
|
+
* Verify a JWT access token using RS256/ES256 via JWKS from
|
|
498
|
+
* `/.well-known/jwks.json`. Backed by `jose` (Web Crypto) so it runs on
|
|
499
|
+
* Node, browser, and edge runtimes alike — no `node:crypto` dependency.
|
|
500
|
+
* Caches JWKS for 1 hour and refetches once on unknown `kid`.
|
|
485
501
|
*/
|
|
486
502
|
async verify(token, options = {}) {
|
|
487
|
-
const
|
|
488
|
-
if (!
|
|
503
|
+
const header = decodeProtectedHeader(token);
|
|
504
|
+
if (!header) {
|
|
489
505
|
throw new IQAuthError("TOKEN_INVALID", "Unable to decode token");
|
|
490
506
|
}
|
|
491
|
-
const kid =
|
|
507
|
+
const kid = header.kid;
|
|
492
508
|
if (!kid) {
|
|
493
509
|
throw new IQAuthError("TOKEN_INVALID", "Token missing kid header");
|
|
494
510
|
}
|
|
495
|
-
let
|
|
496
|
-
if (!
|
|
497
|
-
|
|
498
|
-
|
|
511
|
+
let cache = await this.ensureCache();
|
|
512
|
+
if (!cache.byKid.has(kid)) {
|
|
513
|
+
this.jwksCache = null;
|
|
514
|
+
cache = await this.ensureCache();
|
|
499
515
|
}
|
|
500
|
-
if (!
|
|
516
|
+
if (!cache.byKid.has(kid)) {
|
|
501
517
|
throw new IQAuthError("TOKEN_INVALID", `Unknown key ID: ${kid}`);
|
|
502
518
|
}
|
|
503
519
|
const issuer = options.issuer ?? this.defaultIssuer;
|
|
504
520
|
const audience = options.audience ?? this.defaultAudience;
|
|
505
521
|
const clockTolerance = options.clockTolerance ?? this.defaultClockTolerance;
|
|
506
|
-
const algorithms = options.algorithms ?? ["RS256"];
|
|
522
|
+
const algorithms = options.algorithms ?? ["RS256", "ES256"];
|
|
523
|
+
const verifyOptions = {
|
|
524
|
+
algorithms,
|
|
525
|
+
clockTolerance,
|
|
526
|
+
issuer,
|
|
527
|
+
audience
|
|
528
|
+
};
|
|
507
529
|
try {
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
clockTolerance,
|
|
511
|
-
// The jsonwebtoken types insist on tuple types for arrays; runtime
|
|
512
|
-
// accepts plain string[] so we cast to satisfy the compiler.
|
|
513
|
-
issuer,
|
|
514
|
-
audience
|
|
515
|
-
};
|
|
516
|
-
const verified = import_jsonwebtoken.default.verify(token, publicKey, verifyOptions);
|
|
517
|
-
return verified;
|
|
530
|
+
const { payload } = await (0, import_jose.jwtVerify)(token, cache.verifier, verifyOptions);
|
|
531
|
+
return payload;
|
|
518
532
|
} catch (err) {
|
|
533
|
+
if (err instanceof import_jose.errors.JWTExpired) {
|
|
534
|
+
throw new IQAuthError("TOKEN_EXPIRED", "Token has expired");
|
|
535
|
+
}
|
|
536
|
+
if (err instanceof import_jose.errors.JOSEError) {
|
|
537
|
+
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
538
|
+
}
|
|
519
539
|
if (err instanceof Error) {
|
|
520
|
-
if (err.name === "TokenExpiredError") {
|
|
521
|
-
throw new IQAuthError("TOKEN_EXPIRED", "Token has expired");
|
|
522
|
-
}
|
|
523
540
|
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
524
541
|
}
|
|
525
542
|
throw new IQAuthError("TOKEN_INVALID", "Token verification failed");
|
|
@@ -527,29 +544,40 @@ var TokensModule = class {
|
|
|
527
544
|
}
|
|
528
545
|
/**
|
|
529
546
|
* Decode a JWT without verification. Returns null if malformed.
|
|
530
|
-
*
|
|
531
|
-
* @remarks Local decode only — no network call
|
|
532
547
|
*/
|
|
533
548
|
decode(token) {
|
|
534
|
-
|
|
535
|
-
|
|
549
|
+
try {
|
|
550
|
+
const parts = token.split(".");
|
|
551
|
+
if (parts.length < 2) return null;
|
|
552
|
+
const payload = parts[1];
|
|
553
|
+
const padded = payload + "=".repeat((4 - payload.length % 4) % 4);
|
|
554
|
+
const b64 = padded.replace(/-/g, "+").replace(/_/g, "/");
|
|
555
|
+
let json;
|
|
556
|
+
if (typeof atob === "function") {
|
|
557
|
+
json = atob(b64);
|
|
558
|
+
} else {
|
|
559
|
+
const { Buffer: Buffer2 } = require("buffer");
|
|
560
|
+
json = Buffer2.from(b64, "base64").toString("utf8");
|
|
561
|
+
}
|
|
562
|
+
try {
|
|
563
|
+
json = decodeURIComponent(escape(json));
|
|
564
|
+
} catch {
|
|
565
|
+
}
|
|
566
|
+
const claims = JSON.parse(json);
|
|
567
|
+
if (!claims || typeof claims !== "object") return null;
|
|
568
|
+
return claims;
|
|
569
|
+
} catch {
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
536
572
|
}
|
|
537
|
-
/**
|
|
538
|
-
* Check if a token is expired based on the `exp` claim.
|
|
539
|
-
*
|
|
540
|
-
* @remarks Local check only — no network call
|
|
541
|
-
*/
|
|
573
|
+
/** Check if a token is expired based on the `exp` claim. */
|
|
542
574
|
isExpired(token) {
|
|
543
575
|
const claims = this.decode(token);
|
|
544
576
|
if (!claims?.exp) return true;
|
|
545
577
|
const now = Math.floor(Date.now() / 1e3);
|
|
546
578
|
return claims.exp <= now;
|
|
547
579
|
}
|
|
548
|
-
/**
|
|
549
|
-
* Get the claims from a token without verification.
|
|
550
|
-
*
|
|
551
|
-
* @remarks Local decode only — no network call
|
|
552
|
-
*/
|
|
580
|
+
/** Get the claims from a token without verification. */
|
|
553
581
|
getClaims(token) {
|
|
554
582
|
const claims = this.decode(token);
|
|
555
583
|
if (!claims) {
|
|
@@ -557,11 +585,15 @@ var TokensModule = class {
|
|
|
557
585
|
}
|
|
558
586
|
return claims;
|
|
559
587
|
}
|
|
560
|
-
async
|
|
561
|
-
if (
|
|
562
|
-
|
|
588
|
+
async ensureCache() {
|
|
589
|
+
if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) {
|
|
590
|
+
return this.jwksCache;
|
|
563
591
|
}
|
|
564
|
-
|
|
592
|
+
await this.refreshJwks();
|
|
593
|
+
if (!this.jwksCache) {
|
|
594
|
+
throw new IQAuthError("INTERNAL_ERROR", "JWKS cache unavailable after refresh");
|
|
595
|
+
}
|
|
596
|
+
return this.jwksCache;
|
|
565
597
|
}
|
|
566
598
|
async refreshJwks() {
|
|
567
599
|
if (this.inFlightRefresh) {
|
|
@@ -588,35 +620,24 @@ var TokensModule = class {
|
|
|
588
620
|
"Malformed JWKS response: expected { keys: [...] }"
|
|
589
621
|
);
|
|
590
622
|
}
|
|
591
|
-
const
|
|
623
|
+
const byKid = /* @__PURE__ */ new Set();
|
|
592
624
|
for (const key of jwks.keys) {
|
|
593
|
-
if (!key || typeof key.kid !== "string" || typeof key.n !== "string" || typeof key.e !== "string") {
|
|
625
|
+
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")) {
|
|
594
626
|
throw new IQAuthError(
|
|
595
627
|
"INTERNAL_ERROR",
|
|
596
628
|
"Malformed JWKS response: key missing required fields"
|
|
597
629
|
);
|
|
598
630
|
}
|
|
599
|
-
|
|
600
|
-
keys.set(key.kid, pem);
|
|
631
|
+
byKid.add(key.kid);
|
|
601
632
|
}
|
|
602
|
-
|
|
633
|
+
const verifier = (0, import_jose.createLocalJWKSet)({ keys: jwks.keys });
|
|
634
|
+
this.jwksCache = { raw: jwks.keys, byKid, verifier, fetchedAt: Date.now() };
|
|
603
635
|
} finally {
|
|
604
636
|
this.inFlightRefresh = null;
|
|
605
637
|
}
|
|
606
638
|
})();
|
|
607
639
|
return this.inFlightRefresh;
|
|
608
640
|
}
|
|
609
|
-
jwkToPem(jwk) {
|
|
610
|
-
const keyObject = import_crypto.default.createPublicKey({
|
|
611
|
-
key: {
|
|
612
|
-
kty: jwk.kty,
|
|
613
|
-
n: jwk.n,
|
|
614
|
-
e: jwk.e
|
|
615
|
-
},
|
|
616
|
-
format: "jwk"
|
|
617
|
-
});
|
|
618
|
-
return keyObject.export({ type: "spki", format: "pem" });
|
|
619
|
-
}
|
|
620
641
|
/** @internal Exposed for testing — clears JWKS cache */
|
|
621
642
|
clearCache() {
|
|
622
643
|
this.jwksCache = null;
|
|
@@ -824,7 +845,7 @@ var PermissionsModule = class {
|
|
|
824
845
|
};
|
|
825
846
|
|
|
826
847
|
// src/modules/oidc.ts
|
|
827
|
-
var
|
|
848
|
+
var import_crypto = __toESM(require("crypto"));
|
|
828
849
|
var InMemoryOidcStateStore = class {
|
|
829
850
|
constructor() {
|
|
830
851
|
this.map = /* @__PURE__ */ new Map();
|
|
@@ -905,12 +926,12 @@ var OidcModule = class {
|
|
|
905
926
|
* ready to redirect the user to.
|
|
906
927
|
*/
|
|
907
928
|
async createAuthRequest(params) {
|
|
908
|
-
const codeVerifier = base64UrlEncode(
|
|
929
|
+
const codeVerifier = base64UrlEncode(import_crypto.default.randomBytes(32));
|
|
909
930
|
const codeChallenge = base64UrlEncode(
|
|
910
|
-
|
|
931
|
+
import_crypto.default.createHash("sha256").update(codeVerifier).digest()
|
|
911
932
|
);
|
|
912
|
-
const state = base64UrlEncode(
|
|
913
|
-
const nonce = base64UrlEncode(
|
|
933
|
+
const state = base64UrlEncode(import_crypto.default.randomBytes(16));
|
|
934
|
+
const nonce = base64UrlEncode(import_crypto.default.randomBytes(16));
|
|
914
935
|
await this.stateStore.set(state, {
|
|
915
936
|
codeVerifier,
|
|
916
937
|
state,
|
|
@@ -1813,21 +1834,61 @@ function b64urlDecode(input) {
|
|
|
1813
1834
|
const { Buffer: Buffer2 } = require("buffer");
|
|
1814
1835
|
return Buffer2.from(normalized, "base64").toString("utf8");
|
|
1815
1836
|
}
|
|
1816
|
-
function
|
|
1817
|
-
if (typeof
|
|
1818
|
-
|
|
1819
|
-
if (!m) return null;
|
|
1837
|
+
function isValidIssuerUrl(iss) {
|
|
1838
|
+
if (typeof iss !== "string" || iss.length === 0) return false;
|
|
1839
|
+
if (!iss.startsWith("http://") && !iss.startsWith("https://")) return false;
|
|
1820
1840
|
try {
|
|
1821
|
-
const
|
|
1822
|
-
if (
|
|
1823
|
-
if (
|
|
1824
|
-
|
|
1825
|
-
}
|
|
1826
|
-
return { mode: m[1], iss: json.iss, appId: json.appId, tenantId: json.tenantId, kid: json.kid, raw };
|
|
1841
|
+
const u = new URL(iss);
|
|
1842
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") return false;
|
|
1843
|
+
if (!u.hostname) return false;
|
|
1844
|
+
return true;
|
|
1827
1845
|
} catch {
|
|
1828
|
-
return
|
|
1846
|
+
return false;
|
|
1829
1847
|
}
|
|
1830
1848
|
}
|
|
1849
|
+
function assertPublishableKey(raw, opts) {
|
|
1850
|
+
const ctx = opts?.context ? `${opts.context}: ` : "";
|
|
1851
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
1852
|
+
throw new IQAuthError(
|
|
1853
|
+
"CONFIG_INVALID",
|
|
1854
|
+
`${ctx}IQAuth publishable key is missing. Set IQAUTH_PUBLISHABLE_KEY (or pass publishableKey) to a pk_test_\u2026 or pk_live_\u2026 value from the IQAuth admin console.`
|
|
1855
|
+
);
|
|
1856
|
+
}
|
|
1857
|
+
const shapeMatch = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
|
|
1858
|
+
if (!shapeMatch) {
|
|
1859
|
+
throw new IQAuthError(
|
|
1860
|
+
"CONFIG_INVALID",
|
|
1861
|
+
`${ctx}IQAuth publishable key is malformed (got ${raw.slice(0, 12)}\u2026). Expected pk_test_\u2026 or pk_live_\u2026; regenerate the key from the IQAuth admin console.`
|
|
1862
|
+
);
|
|
1863
|
+
}
|
|
1864
|
+
let decoded;
|
|
1865
|
+
try {
|
|
1866
|
+
decoded = JSON.parse(b64urlDecode(shapeMatch[2]));
|
|
1867
|
+
} catch {
|
|
1868
|
+
throw new IQAuthError(
|
|
1869
|
+
"CONFIG_INVALID",
|
|
1870
|
+
`${ctx}IQAuth publishable key payload is not valid base64url JSON. Regenerate the key from the IQAuth admin console.`
|
|
1871
|
+
);
|
|
1872
|
+
}
|
|
1873
|
+
if (!isPublishableKeyPayload(decoded)) {
|
|
1874
|
+
throw new IQAuthError(
|
|
1875
|
+
"CONFIG_INVALID",
|
|
1876
|
+
`${ctx}IQAuth publishable key payload is missing required fields {iss, appId, tenantId, kid}. Regenerate the key from the IQAuth admin console.`
|
|
1877
|
+
);
|
|
1878
|
+
}
|
|
1879
|
+
if (!isValidIssuerUrl(decoded.iss)) {
|
|
1880
|
+
throw new IQAuthError(
|
|
1881
|
+
"CONFIG_INVALID",
|
|
1882
|
+
`${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.`
|
|
1883
|
+
);
|
|
1884
|
+
}
|
|
1885
|
+
return { mode: shapeMatch[1], iss: decoded.iss, appId: decoded.appId, tenantId: decoded.tenantId, kid: decoded.kid, raw };
|
|
1886
|
+
}
|
|
1887
|
+
function isPublishableKeyPayload(value) {
|
|
1888
|
+
if (!value || typeof value !== "object") return false;
|
|
1889
|
+
const v = value;
|
|
1890
|
+
return typeof v.iss === "string" && typeof v.appId === "string" && typeof v.tenantId === "string" && typeof v.kid === "string";
|
|
1891
|
+
}
|
|
1831
1892
|
|
|
1832
1893
|
// src/middleware/express.ts
|
|
1833
1894
|
var KNOWN_AUTH_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
@@ -1865,10 +1926,7 @@ function readCookie(req, name) {
|
|
|
1865
1926
|
return void 0;
|
|
1866
1927
|
}
|
|
1867
1928
|
function clientFromPublishableKey(opts) {
|
|
1868
|
-
const parsed =
|
|
1869
|
-
if (!parsed) {
|
|
1870
|
-
throw new Error("iqAuthMiddleware: invalid publishable key");
|
|
1871
|
-
}
|
|
1929
|
+
const parsed = assertPublishableKey(opts.publishableKey, { context: "iqAuthMiddleware" });
|
|
1872
1930
|
const issuer = (opts.issuer ?? (parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`)).replace(/\/+$/, "");
|
|
1873
1931
|
return new IQAuthClient({ baseUrl: issuer, environment: "server" });
|
|
1874
1932
|
}
|
|
@@ -2010,12 +2068,7 @@ function shouldClearCookiesOnFailure(policy, status, errorCode) {
|
|
|
2010
2068
|
var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
|
|
2011
2069
|
var REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
|
|
2012
2070
|
function resolve(config) {
|
|
2013
|
-
const parsed =
|
|
2014
|
-
if (!parsed) {
|
|
2015
|
-
throw new Error(
|
|
2016
|
-
"@iqauth/sdk: invalid publishable key passed to iqAuth helpers (expected pk_test_\u2026 or pk_live_\u2026)"
|
|
2017
|
-
);
|
|
2018
|
-
}
|
|
2071
|
+
const parsed = assertPublishableKey(config.publishableKey, { context: "@iqauth/sdk helpers" });
|
|
2019
2072
|
const inferredIssuer = parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`;
|
|
2020
2073
|
return {
|
|
2021
2074
|
publishableKey: config.publishableKey,
|
|
@@ -2186,6 +2239,15 @@ async function handleSignout(config, input) {
|
|
|
2186
2239
|
} catch {
|
|
2187
2240
|
}
|
|
2188
2241
|
}
|
|
2242
|
+
if (input.endSsoSession !== false && input.ssoCookieHeader) {
|
|
2243
|
+
try {
|
|
2244
|
+
await cfg.fetchImpl(`${cfg.issuer}/oidc/sso-logout`, {
|
|
2245
|
+
method: "POST",
|
|
2246
|
+
headers: { Cookie: input.ssoCookieHeader }
|
|
2247
|
+
});
|
|
2248
|
+
} catch {
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2189
2251
|
return {
|
|
2190
2252
|
status: 200,
|
|
2191
2253
|
body: { success: true, data: { signedOut: true } },
|
package/dist/server.mjs
CHANGED
|
@@ -2,17 +2,18 @@ import {
|
|
|
2
2
|
DEFAULT_ACCESS_COOKIE,
|
|
3
3
|
DEFAULT_REFRESH_COOKIE,
|
|
4
4
|
iqAuthMiddleware
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-EKTNEZIH.mjs";
|
|
6
|
+
import {
|
|
7
|
+
IQAuthClient
|
|
8
|
+
} from "./chunk-W3F4JYGP.mjs";
|
|
6
9
|
import {
|
|
7
10
|
handleCallback,
|
|
8
11
|
handleRefresh,
|
|
9
12
|
handleSignout,
|
|
10
13
|
serializeCookie
|
|
11
|
-
} from "./chunk-
|
|
12
|
-
import "./chunk-
|
|
13
|
-
import
|
|
14
|
-
IQAuthClient
|
|
15
|
-
} from "./chunk-MDUHPQMM.mjs";
|
|
14
|
+
} from "./chunk-KGEPDXHU.mjs";
|
|
15
|
+
import "./chunk-WQWBJSSS.mjs";
|
|
16
|
+
import "./chunk-UNYDG2L4.mjs";
|
|
16
17
|
import {
|
|
17
18
|
ErrorCodes,
|
|
18
19
|
IQAuthError
|
package/dist/service.d.mts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { I as IQAuthClient } from './client-
|
|
1
|
+
import { I as IQAuthClient } from './client-vdh2a9fJ.mjs';
|
|
2
2
|
import { b as IQAuthTokenClientConfig } from './types-Cxl3bQHt.mjs';
|
|
3
3
|
export { E as ErrorCodes, I as IQAuthError } from './errors-CDdl24MP.mjs';
|
|
4
|
-
import 'jsonwebtoken';
|
|
5
4
|
|
|
6
5
|
declare class ServiceIQAuthClient extends IQAuthClient {
|
|
7
6
|
constructor(config: IQAuthTokenClientConfig);
|
package/dist/service.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { I as IQAuthClient } from './client-
|
|
1
|
+
import { I as IQAuthClient } from './client-DTX4hNdS.js';
|
|
2
2
|
import { b as IQAuthTokenClientConfig } from './types-Cxl3bQHt.js';
|
|
3
3
|
export { E as ErrorCodes, I as IQAuthError } from './errors-CDdl24MP.js';
|
|
4
|
-
import 'jsonwebtoken';
|
|
5
4
|
|
|
6
5
|
declare class ServiceIQAuthClient extends IQAuthClient {
|
|
7
6
|
constructor(config: IQAuthTokenClientConfig);
|
package/dist/service.js
CHANGED
|
@@ -446,8 +446,7 @@ function parseMfaResponse(data, browserSessionMode) {
|
|
|
446
446
|
}
|
|
447
447
|
|
|
448
448
|
// src/modules/tokens.ts
|
|
449
|
-
var
|
|
450
|
-
var import_jsonwebtoken = __toESM(require("jsonwebtoken"));
|
|
449
|
+
var import_jose = require("jose");
|
|
451
450
|
var JWKS_CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
452
451
|
var DEFAULT_TOKEN_ISSUER = [
|
|
453
452
|
"https://auth.dispositioniq.com",
|
|
@@ -460,6 +459,24 @@ var DEFAULT_TOKEN_AUDIENCE = [
|
|
|
460
459
|
"iqvalidate"
|
|
461
460
|
];
|
|
462
461
|
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
462
|
+
function decodeProtectedHeader(token) {
|
|
463
|
+
const parts = token.split(".");
|
|
464
|
+
if (parts.length < 2) return null;
|
|
465
|
+
try {
|
|
466
|
+
const padded = parts[0] + "=".repeat((4 - parts[0].length % 4) % 4);
|
|
467
|
+
const b64 = padded.replace(/-/g, "+").replace(/_/g, "/");
|
|
468
|
+
let json;
|
|
469
|
+
if (typeof atob === "function") {
|
|
470
|
+
json = atob(b64);
|
|
471
|
+
} else {
|
|
472
|
+
const { Buffer: Buffer2 } = require("buffer");
|
|
473
|
+
json = Buffer2.from(b64, "base64").toString("utf8");
|
|
474
|
+
}
|
|
475
|
+
return JSON.parse(json);
|
|
476
|
+
} catch {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
463
480
|
var TokensModule = class {
|
|
464
481
|
constructor(baseUrl, options = {}) {
|
|
465
482
|
this.jwksCache = null;
|
|
@@ -470,49 +487,49 @@ var TokensModule = class {
|
|
|
470
487
|
this.defaultClockTolerance = options.clockTolerance ?? DEFAULT_CLOCK_TOLERANCE_SECONDS;
|
|
471
488
|
}
|
|
472
489
|
/**
|
|
473
|
-
* Verify a JWT access token using RS256 via JWKS from
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
* clock tolerance default to client config but can be overridden per call.
|
|
490
|
+
* Verify a JWT access token using RS256/ES256 via JWKS from
|
|
491
|
+
* `/.well-known/jwks.json`. Backed by `jose` (Web Crypto) so it runs on
|
|
492
|
+
* Node, browser, and edge runtimes alike — no `node:crypto` dependency.
|
|
493
|
+
* Caches JWKS for 1 hour and refetches once on unknown `kid`.
|
|
478
494
|
*/
|
|
479
495
|
async verify(token, options = {}) {
|
|
480
|
-
const
|
|
481
|
-
if (!
|
|
496
|
+
const header = decodeProtectedHeader(token);
|
|
497
|
+
if (!header) {
|
|
482
498
|
throw new IQAuthError("TOKEN_INVALID", "Unable to decode token");
|
|
483
499
|
}
|
|
484
|
-
const kid =
|
|
500
|
+
const kid = header.kid;
|
|
485
501
|
if (!kid) {
|
|
486
502
|
throw new IQAuthError("TOKEN_INVALID", "Token missing kid header");
|
|
487
503
|
}
|
|
488
|
-
let
|
|
489
|
-
if (!
|
|
490
|
-
|
|
491
|
-
|
|
504
|
+
let cache = await this.ensureCache();
|
|
505
|
+
if (!cache.byKid.has(kid)) {
|
|
506
|
+
this.jwksCache = null;
|
|
507
|
+
cache = await this.ensureCache();
|
|
492
508
|
}
|
|
493
|
-
if (!
|
|
509
|
+
if (!cache.byKid.has(kid)) {
|
|
494
510
|
throw new IQAuthError("TOKEN_INVALID", `Unknown key ID: ${kid}`);
|
|
495
511
|
}
|
|
496
512
|
const issuer = options.issuer ?? this.defaultIssuer;
|
|
497
513
|
const audience = options.audience ?? this.defaultAudience;
|
|
498
514
|
const clockTolerance = options.clockTolerance ?? this.defaultClockTolerance;
|
|
499
|
-
const algorithms = options.algorithms ?? ["RS256"];
|
|
515
|
+
const algorithms = options.algorithms ?? ["RS256", "ES256"];
|
|
516
|
+
const verifyOptions = {
|
|
517
|
+
algorithms,
|
|
518
|
+
clockTolerance,
|
|
519
|
+
issuer,
|
|
520
|
+
audience
|
|
521
|
+
};
|
|
500
522
|
try {
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
clockTolerance,
|
|
504
|
-
// The jsonwebtoken types insist on tuple types for arrays; runtime
|
|
505
|
-
// accepts plain string[] so we cast to satisfy the compiler.
|
|
506
|
-
issuer,
|
|
507
|
-
audience
|
|
508
|
-
};
|
|
509
|
-
const verified = import_jsonwebtoken.default.verify(token, publicKey, verifyOptions);
|
|
510
|
-
return verified;
|
|
523
|
+
const { payload } = await (0, import_jose.jwtVerify)(token, cache.verifier, verifyOptions);
|
|
524
|
+
return payload;
|
|
511
525
|
} catch (err) {
|
|
526
|
+
if (err instanceof import_jose.errors.JWTExpired) {
|
|
527
|
+
throw new IQAuthError("TOKEN_EXPIRED", "Token has expired");
|
|
528
|
+
}
|
|
529
|
+
if (err instanceof import_jose.errors.JOSEError) {
|
|
530
|
+
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
531
|
+
}
|
|
512
532
|
if (err instanceof Error) {
|
|
513
|
-
if (err.name === "TokenExpiredError") {
|
|
514
|
-
throw new IQAuthError("TOKEN_EXPIRED", "Token has expired");
|
|
515
|
-
}
|
|
516
533
|
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
517
534
|
}
|
|
518
535
|
throw new IQAuthError("TOKEN_INVALID", "Token verification failed");
|
|
@@ -520,29 +537,40 @@ var TokensModule = class {
|
|
|
520
537
|
}
|
|
521
538
|
/**
|
|
522
539
|
* Decode a JWT without verification. Returns null if malformed.
|
|
523
|
-
*
|
|
524
|
-
* @remarks Local decode only — no network call
|
|
525
540
|
*/
|
|
526
541
|
decode(token) {
|
|
527
|
-
|
|
528
|
-
|
|
542
|
+
try {
|
|
543
|
+
const parts = token.split(".");
|
|
544
|
+
if (parts.length < 2) return null;
|
|
545
|
+
const payload = parts[1];
|
|
546
|
+
const padded = payload + "=".repeat((4 - payload.length % 4) % 4);
|
|
547
|
+
const b64 = padded.replace(/-/g, "+").replace(/_/g, "/");
|
|
548
|
+
let json;
|
|
549
|
+
if (typeof atob === "function") {
|
|
550
|
+
json = atob(b64);
|
|
551
|
+
} else {
|
|
552
|
+
const { Buffer: Buffer2 } = require("buffer");
|
|
553
|
+
json = Buffer2.from(b64, "base64").toString("utf8");
|
|
554
|
+
}
|
|
555
|
+
try {
|
|
556
|
+
json = decodeURIComponent(escape(json));
|
|
557
|
+
} catch {
|
|
558
|
+
}
|
|
559
|
+
const claims = JSON.parse(json);
|
|
560
|
+
if (!claims || typeof claims !== "object") return null;
|
|
561
|
+
return claims;
|
|
562
|
+
} catch {
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
529
565
|
}
|
|
530
|
-
/**
|
|
531
|
-
* Check if a token is expired based on the `exp` claim.
|
|
532
|
-
*
|
|
533
|
-
* @remarks Local check only — no network call
|
|
534
|
-
*/
|
|
566
|
+
/** Check if a token is expired based on the `exp` claim. */
|
|
535
567
|
isExpired(token) {
|
|
536
568
|
const claims = this.decode(token);
|
|
537
569
|
if (!claims?.exp) return true;
|
|
538
570
|
const now = Math.floor(Date.now() / 1e3);
|
|
539
571
|
return claims.exp <= now;
|
|
540
572
|
}
|
|
541
|
-
/**
|
|
542
|
-
* Get the claims from a token without verification.
|
|
543
|
-
*
|
|
544
|
-
* @remarks Local decode only — no network call
|
|
545
|
-
*/
|
|
573
|
+
/** Get the claims from a token without verification. */
|
|
546
574
|
getClaims(token) {
|
|
547
575
|
const claims = this.decode(token);
|
|
548
576
|
if (!claims) {
|
|
@@ -550,11 +578,15 @@ var TokensModule = class {
|
|
|
550
578
|
}
|
|
551
579
|
return claims;
|
|
552
580
|
}
|
|
553
|
-
async
|
|
554
|
-
if (
|
|
555
|
-
|
|
581
|
+
async ensureCache() {
|
|
582
|
+
if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) {
|
|
583
|
+
return this.jwksCache;
|
|
584
|
+
}
|
|
585
|
+
await this.refreshJwks();
|
|
586
|
+
if (!this.jwksCache) {
|
|
587
|
+
throw new IQAuthError("INTERNAL_ERROR", "JWKS cache unavailable after refresh");
|
|
556
588
|
}
|
|
557
|
-
return this.jwksCache
|
|
589
|
+
return this.jwksCache;
|
|
558
590
|
}
|
|
559
591
|
async refreshJwks() {
|
|
560
592
|
if (this.inFlightRefresh) {
|
|
@@ -581,35 +613,24 @@ var TokensModule = class {
|
|
|
581
613
|
"Malformed JWKS response: expected { keys: [...] }"
|
|
582
614
|
);
|
|
583
615
|
}
|
|
584
|
-
const
|
|
616
|
+
const byKid = /* @__PURE__ */ new Set();
|
|
585
617
|
for (const key of jwks.keys) {
|
|
586
|
-
if (!key || typeof key.kid !== "string" || typeof key.n !== "string" || typeof key.e !== "string") {
|
|
618
|
+
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")) {
|
|
587
619
|
throw new IQAuthError(
|
|
588
620
|
"INTERNAL_ERROR",
|
|
589
621
|
"Malformed JWKS response: key missing required fields"
|
|
590
622
|
);
|
|
591
623
|
}
|
|
592
|
-
|
|
593
|
-
keys.set(key.kid, pem);
|
|
624
|
+
byKid.add(key.kid);
|
|
594
625
|
}
|
|
595
|
-
|
|
626
|
+
const verifier = (0, import_jose.createLocalJWKSet)({ keys: jwks.keys });
|
|
627
|
+
this.jwksCache = { raw: jwks.keys, byKid, verifier, fetchedAt: Date.now() };
|
|
596
628
|
} finally {
|
|
597
629
|
this.inFlightRefresh = null;
|
|
598
630
|
}
|
|
599
631
|
})();
|
|
600
632
|
return this.inFlightRefresh;
|
|
601
633
|
}
|
|
602
|
-
jwkToPem(jwk) {
|
|
603
|
-
const keyObject = import_crypto.default.createPublicKey({
|
|
604
|
-
key: {
|
|
605
|
-
kty: jwk.kty,
|
|
606
|
-
n: jwk.n,
|
|
607
|
-
e: jwk.e
|
|
608
|
-
},
|
|
609
|
-
format: "jwk"
|
|
610
|
-
});
|
|
611
|
-
return keyObject.export({ type: "spki", format: "pem" });
|
|
612
|
-
}
|
|
613
634
|
/** @internal Exposed for testing — clears JWKS cache */
|
|
614
635
|
clearCache() {
|
|
615
636
|
this.jwksCache = null;
|
|
@@ -817,7 +838,7 @@ var PermissionsModule = class {
|
|
|
817
838
|
};
|
|
818
839
|
|
|
819
840
|
// src/modules/oidc.ts
|
|
820
|
-
var
|
|
841
|
+
var import_crypto = __toESM(require("crypto"));
|
|
821
842
|
var InMemoryOidcStateStore = class {
|
|
822
843
|
constructor() {
|
|
823
844
|
this.map = /* @__PURE__ */ new Map();
|
|
@@ -898,12 +919,12 @@ var OidcModule = class {
|
|
|
898
919
|
* ready to redirect the user to.
|
|
899
920
|
*/
|
|
900
921
|
async createAuthRequest(params) {
|
|
901
|
-
const codeVerifier = base64UrlEncode(
|
|
922
|
+
const codeVerifier = base64UrlEncode(import_crypto.default.randomBytes(32));
|
|
902
923
|
const codeChallenge = base64UrlEncode(
|
|
903
|
-
|
|
924
|
+
import_crypto.default.createHash("sha256").update(codeVerifier).digest()
|
|
904
925
|
);
|
|
905
|
-
const state = base64UrlEncode(
|
|
906
|
-
const nonce = base64UrlEncode(
|
|
926
|
+
const state = base64UrlEncode(import_crypto.default.randomBytes(16));
|
|
927
|
+
const nonce = base64UrlEncode(import_crypto.default.randomBytes(16));
|
|
907
928
|
await this.stateStore.set(state, {
|
|
908
929
|
codeVerifier,
|
|
909
930
|
state,
|