@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/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;
|
|
555
583
|
}
|
|
556
|
-
|
|
584
|
+
await this.refreshJwks();
|
|
585
|
+
if (!this.jwksCache) {
|
|
586
|
+
throw new IQAuthError("INTERNAL_ERROR", "JWKS cache unavailable after refresh");
|
|
587
|
+
}
|
|
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,
|
|
@@ -1805,21 +1826,61 @@ function b64urlDecode(input) {
|
|
|
1805
1826
|
const { Buffer: Buffer2 } = require("buffer");
|
|
1806
1827
|
return Buffer2.from(normalized, "base64").toString("utf8");
|
|
1807
1828
|
}
|
|
1808
|
-
function
|
|
1809
|
-
if (typeof
|
|
1810
|
-
|
|
1811
|
-
if (!m) return null;
|
|
1829
|
+
function isValidIssuerUrl(iss) {
|
|
1830
|
+
if (typeof iss !== "string" || iss.length === 0) return false;
|
|
1831
|
+
if (!iss.startsWith("http://") && !iss.startsWith("https://")) return false;
|
|
1812
1832
|
try {
|
|
1813
|
-
const
|
|
1814
|
-
if (
|
|
1815
|
-
if (
|
|
1816
|
-
|
|
1817
|
-
}
|
|
1818
|
-
return { mode: m[1], iss: json.iss, appId: json.appId, tenantId: json.tenantId, kid: json.kid, raw };
|
|
1833
|
+
const u = new URL(iss);
|
|
1834
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") return false;
|
|
1835
|
+
if (!u.hostname) return false;
|
|
1836
|
+
return true;
|
|
1819
1837
|
} catch {
|
|
1820
|
-
return
|
|
1838
|
+
return false;
|
|
1821
1839
|
}
|
|
1822
1840
|
}
|
|
1841
|
+
function assertPublishableKey(raw, opts) {
|
|
1842
|
+
const ctx = opts?.context ? `${opts.context}: ` : "";
|
|
1843
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
1844
|
+
throw new IQAuthError(
|
|
1845
|
+
"CONFIG_INVALID",
|
|
1846
|
+
`${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.`
|
|
1847
|
+
);
|
|
1848
|
+
}
|
|
1849
|
+
const shapeMatch = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
|
|
1850
|
+
if (!shapeMatch) {
|
|
1851
|
+
throw new IQAuthError(
|
|
1852
|
+
"CONFIG_INVALID",
|
|
1853
|
+
`${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.`
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1856
|
+
let decoded;
|
|
1857
|
+
try {
|
|
1858
|
+
decoded = JSON.parse(b64urlDecode(shapeMatch[2]));
|
|
1859
|
+
} catch {
|
|
1860
|
+
throw new IQAuthError(
|
|
1861
|
+
"CONFIG_INVALID",
|
|
1862
|
+
`${ctx}IQAuth publishable key payload is not valid base64url JSON. Regenerate the key from the IQAuth admin console.`
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1865
|
+
if (!isPublishableKeyPayload(decoded)) {
|
|
1866
|
+
throw new IQAuthError(
|
|
1867
|
+
"CONFIG_INVALID",
|
|
1868
|
+
`${ctx}IQAuth publishable key payload is missing required fields {iss, appId, tenantId, kid}. Regenerate the key from the IQAuth admin console.`
|
|
1869
|
+
);
|
|
1870
|
+
}
|
|
1871
|
+
if (!isValidIssuerUrl(decoded.iss)) {
|
|
1872
|
+
throw new IQAuthError(
|
|
1873
|
+
"CONFIG_INVALID",
|
|
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.`
|
|
1875
|
+
);
|
|
1876
|
+
}
|
|
1877
|
+
return { mode: shapeMatch[1], iss: decoded.iss, appId: decoded.appId, tenantId: decoded.tenantId, kid: decoded.kid, raw };
|
|
1878
|
+
}
|
|
1879
|
+
function isPublishableKeyPayload(value) {
|
|
1880
|
+
if (!value || typeof value !== "object") return false;
|
|
1881
|
+
const v = value;
|
|
1882
|
+
return typeof v.iss === "string" && typeof v.appId === "string" && typeof v.tenantId === "string" && typeof v.kid === "string";
|
|
1883
|
+
}
|
|
1823
1884
|
|
|
1824
1885
|
// src/middleware/express.ts
|
|
1825
1886
|
var KNOWN_AUTH_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
@@ -1857,10 +1918,7 @@ function readCookie(req, name) {
|
|
|
1857
1918
|
return void 0;
|
|
1858
1919
|
}
|
|
1859
1920
|
function clientFromPublishableKey(opts) {
|
|
1860
|
-
const parsed =
|
|
1861
|
-
if (!parsed) {
|
|
1862
|
-
throw new Error("iqAuthMiddleware: invalid publishable key");
|
|
1863
|
-
}
|
|
1921
|
+
const parsed = assertPublishableKey(opts.publishableKey, { context: "iqAuthMiddleware" });
|
|
1864
1922
|
const issuer = (opts.issuer ?? (parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`)).replace(/\/+$/, "");
|
|
1865
1923
|
return new IQAuthClient({ baseUrl: issuer, environment: "server" });
|
|
1866
1924
|
}
|
|
@@ -2002,12 +2060,7 @@ function shouldClearCookiesOnFailure(policy, status, errorCode) {
|
|
|
2002
2060
|
var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
|
|
2003
2061
|
var REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
|
|
2004
2062
|
function resolve(config) {
|
|
2005
|
-
const parsed =
|
|
2006
|
-
if (!parsed) {
|
|
2007
|
-
throw new Error(
|
|
2008
|
-
"@iqauth/sdk: invalid publishable key passed to iqAuth helpers (expected pk_test_\u2026 or pk_live_\u2026)"
|
|
2009
|
-
);
|
|
2010
|
-
}
|
|
2063
|
+
const parsed = assertPublishableKey(config.publishableKey, { context: "@iqauth/sdk helpers" });
|
|
2011
2064
|
const inferredIssuer = parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`;
|
|
2012
2065
|
return {
|
|
2013
2066
|
publishableKey: config.publishableKey,
|
|
@@ -2168,6 +2221,15 @@ async function handleSignout(config, input) {
|
|
|
2168
2221
|
} catch {
|
|
2169
2222
|
}
|
|
2170
2223
|
}
|
|
2224
|
+
if (input.endSsoSession !== false && input.ssoCookieHeader) {
|
|
2225
|
+
try {
|
|
2226
|
+
await cfg.fetchImpl(`${cfg.issuer}/oidc/sso-logout`, {
|
|
2227
|
+
method: "POST",
|
|
2228
|
+
headers: { Cookie: input.ssoCookieHeader }
|
|
2229
|
+
});
|
|
2230
|
+
} catch {
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2171
2233
|
return {
|
|
2172
2234
|
status: 200,
|
|
2173
2235
|
body: { success: true, data: { signedOut: true } },
|
|
@@ -2176,6 +2238,78 @@ async function handleSignout(config, input) {
|
|
|
2176
2238
|
}
|
|
2177
2239
|
|
|
2178
2240
|
// src/express.ts
|
|
2241
|
+
var PKCE_COOKIE = "iqauth_pkce";
|
|
2242
|
+
function escapeHtml(s) {
|
|
2243
|
+
return s.replace(/[&<>"']/g, (c) => {
|
|
2244
|
+
switch (c) {
|
|
2245
|
+
case "&":
|
|
2246
|
+
return "&";
|
|
2247
|
+
case "<":
|
|
2248
|
+
return "<";
|
|
2249
|
+
case ">":
|
|
2250
|
+
return ">";
|
|
2251
|
+
case '"':
|
|
2252
|
+
return """;
|
|
2253
|
+
case "'":
|
|
2254
|
+
return "'";
|
|
2255
|
+
default:
|
|
2256
|
+
return c;
|
|
2257
|
+
}
|
|
2258
|
+
});
|
|
2259
|
+
}
|
|
2260
|
+
function appendErrorParam(path, errorCode) {
|
|
2261
|
+
const sep = path.includes("?") ? "&" : "?";
|
|
2262
|
+
return `${path}${sep}error=${encodeURIComponent(errorCode)}`;
|
|
2263
|
+
}
|
|
2264
|
+
function defaultBrandedSpinner(args) {
|
|
2265
|
+
return `<!doctype html>
|
|
2266
|
+
<html lang="en">
|
|
2267
|
+
<head>
|
|
2268
|
+
<meta charset="utf-8" />
|
|
2269
|
+
<title>Signing you in\u2026</title>
|
|
2270
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
2271
|
+
<style>
|
|
2272
|
+
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; }
|
|
2273
|
+
.iqauth-card { text-align:center; padding:2rem; }
|
|
2274
|
+
.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; }
|
|
2275
|
+
@keyframes iqauth-spin { to { transform: rotate(360deg); } }
|
|
2276
|
+
.iqauth-msg { font-size:0.95rem; color:#374151; }
|
|
2277
|
+
</style>
|
|
2278
|
+
</head>
|
|
2279
|
+
<body>
|
|
2280
|
+
<div class="iqauth-card" data-testid="iqauth-inline-callback-spinner">
|
|
2281
|
+
<div class="iqauth-spinner" aria-hidden="true"></div>
|
|
2282
|
+
<div class="iqauth-msg">Signing you in\u2026</div>
|
|
2283
|
+
</div>
|
|
2284
|
+
<script>
|
|
2285
|
+
(function(){
|
|
2286
|
+
var code = ${JSON.stringify(args.code)};
|
|
2287
|
+
var state = ${JSON.stringify(args.state)};
|
|
2288
|
+
var errorPath = ${JSON.stringify(args.errorPath || "")};
|
|
2289
|
+
function fail(reason){
|
|
2290
|
+
if (errorPath) { window.location.replace(errorPath + (errorPath.indexOf("?")>=0?"&":"?") + "error=" + encodeURIComponent(reason)); return; }
|
|
2291
|
+
window.location.replace("/");
|
|
2292
|
+
}
|
|
2293
|
+
var verifier = (document.cookie.split('; ').find(function(c){return c.indexOf('${PKCE_COOKIE}=')===0;})||'').slice(${PKCE_COOKIE.length + 1});
|
|
2294
|
+
try { verifier = decodeURIComponent(verifier); } catch (e) {}
|
|
2295
|
+
fetch(${JSON.stringify(args.exchangePath)}, {
|
|
2296
|
+
method: "POST",
|
|
2297
|
+
credentials: "include",
|
|
2298
|
+
headers: { "Content-Type": "application/json" },
|
|
2299
|
+
body: JSON.stringify({ code: code, state: state, codeVerifier: verifier, redirectUri: window.location.origin + window.location.pathname })
|
|
2300
|
+
}).then(function(r){ return r.json().then(function(j){ return { status:r.status, body:j }; }); })
|
|
2301
|
+
.then(function(out){
|
|
2302
|
+
if (out.status >= 400) { fail((out.body && out.body.error && out.body.error.code) || "exchange_failed"); return; }
|
|
2303
|
+
var dest = (out.body && out.body.returnTo) || sessionStorage.getItem("iqauth_return_to") || "/";
|
|
2304
|
+
sessionStorage.removeItem("iqauth_return_to");
|
|
2305
|
+
window.location.replace(dest);
|
|
2306
|
+
})
|
|
2307
|
+
.catch(function(){ fail("network_error"); });
|
|
2308
|
+
})();
|
|
2309
|
+
</script>
|
|
2310
|
+
</body>
|
|
2311
|
+
</html>`;
|
|
2312
|
+
}
|
|
2179
2313
|
function applyHandlerResponse(res, hr) {
|
|
2180
2314
|
for (const c of hr.cookies) {
|
|
2181
2315
|
if (typeof res.cookie === "function") {
|
|
@@ -2222,10 +2356,7 @@ function readCookieFromReq(req, name) {
|
|
|
2222
2356
|
return void 0;
|
|
2223
2357
|
}
|
|
2224
2358
|
function iqAuth(options) {
|
|
2225
|
-
const parsed =
|
|
2226
|
-
if (!parsed) {
|
|
2227
|
-
throw new Error("@iqauth/sdk/express: invalid publishable key");
|
|
2228
|
-
}
|
|
2359
|
+
const parsed = assertPublishableKey(options.publishableKey, { context: "@iqauth/sdk/express" });
|
|
2229
2360
|
const issuer = (options.issuer ?? (parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`)).replace(/\/+$/, "");
|
|
2230
2361
|
const client = new IQAuthClient({
|
|
2231
2362
|
baseUrl: issuer,
|
|
@@ -2242,6 +2373,8 @@ function iqAuth(options) {
|
|
|
2242
2373
|
if (mountHelpers && path.startsWith(mount + "/")) return next();
|
|
2243
2374
|
return verify(req, res, next);
|
|
2244
2375
|
};
|
|
2376
|
+
const inline = options.inlineCallback === true ? {} : options.inlineCallback && typeof options.inlineCallback === "object" ? options.inlineCallback : null;
|
|
2377
|
+
const inlineBranded = inline?.branded === true ? {} : inline?.branded && typeof inline.branded === "object" ? inline.branded : null;
|
|
2245
2378
|
const attachHelpers = (app) => {
|
|
2246
2379
|
app.post(`${mount}/callback`, async (req, res) => {
|
|
2247
2380
|
const body = readBody(req);
|
|
@@ -2252,6 +2385,131 @@ function iqAuth(options) {
|
|
|
2252
2385
|
});
|
|
2253
2386
|
applyHandlerResponse(res, hr);
|
|
2254
2387
|
});
|
|
2388
|
+
if (inline && typeof app.get === "function") {
|
|
2389
|
+
const callbackPath = `${mount}/callback`;
|
|
2390
|
+
const exchangePath = `${callbackPath}/exchange`;
|
|
2391
|
+
const stateCookie = inline.stateCookieName ?? "iqauth_state";
|
|
2392
|
+
const returnToCookie = inline.returnToCookieName ?? "iqauth_return_to";
|
|
2393
|
+
const errorPath = inline.errorPath;
|
|
2394
|
+
const clearCookie = (res, name) => {
|
|
2395
|
+
if (typeof res.clearCookie === "function") {
|
|
2396
|
+
res.clearCookie(name, { path: "/" });
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
2399
|
+
const existing = res.getHeader?.("Set-Cookie") || [];
|
|
2400
|
+
const list = Array.isArray(existing) ? existing : [existing];
|
|
2401
|
+
list.push(`${name}=; Path=/; Max-Age=0; SameSite=Lax`);
|
|
2402
|
+
res.setHeader?.("Set-Cookie", list);
|
|
2403
|
+
};
|
|
2404
|
+
const failPlain = (res, errorCode, fallback) => {
|
|
2405
|
+
if (errorPath) {
|
|
2406
|
+
const dest = appendErrorParam(errorPath, errorCode);
|
|
2407
|
+
if (typeof res.redirect === "function") return res.redirect(302, dest);
|
|
2408
|
+
res.status(302);
|
|
2409
|
+
res.setHeader?.("Location", dest);
|
|
2410
|
+
return res.end?.();
|
|
2411
|
+
}
|
|
2412
|
+
fallback();
|
|
2413
|
+
};
|
|
2414
|
+
if (inlineBranded) {
|
|
2415
|
+
const render = inlineBranded.render ?? defaultBrandedSpinner;
|
|
2416
|
+
app.get(callbackPath, (req, res) => {
|
|
2417
|
+
const q = req.query || {};
|
|
2418
|
+
const html = render({
|
|
2419
|
+
issuer,
|
|
2420
|
+
exchangePath,
|
|
2421
|
+
code: escapeHtml(q.code ?? ""),
|
|
2422
|
+
state: escapeHtml(q.state ?? ""),
|
|
2423
|
+
errorPath: errorPath ?? ""
|
|
2424
|
+
});
|
|
2425
|
+
res.status(200);
|
|
2426
|
+
if (typeof res.set === "function") res.set("Content-Type", "text/html; charset=utf-8");
|
|
2427
|
+
else if (typeof res.setHeader === "function") res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
2428
|
+
if (typeof res.send === "function") res.send(html);
|
|
2429
|
+
else res.end?.(html);
|
|
2430
|
+
});
|
|
2431
|
+
app.post(exchangePath, async (req, res) => {
|
|
2432
|
+
const body = readBody(req);
|
|
2433
|
+
const stateFromBody = body.state || void 0;
|
|
2434
|
+
const stateFromCookie = readCookieFromReq(req, stateCookie);
|
|
2435
|
+
if (stateFromCookie && stateFromBody !== stateFromCookie) {
|
|
2436
|
+
clearCookie(res, stateCookie);
|
|
2437
|
+
res.status(400);
|
|
2438
|
+
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" } }));
|
|
2439
|
+
}
|
|
2440
|
+
const hr = await handleCallback(helperConfig, {
|
|
2441
|
+
code: body.code,
|
|
2442
|
+
codeVerifier: body.codeVerifier || readCookieFromReq(req, PKCE_COOKIE) || "",
|
|
2443
|
+
redirectUri: body.redirectUri
|
|
2444
|
+
});
|
|
2445
|
+
clearCookie(res, stateCookie);
|
|
2446
|
+
clearCookie(res, PKCE_COOKIE);
|
|
2447
|
+
const returnTo = readCookieFromReq(req, returnToCookie) || hr.body?.returnTo || "/";
|
|
2448
|
+
if (hr.status < 400) clearCookie(res, returnToCookie);
|
|
2449
|
+
const enriched = {
|
|
2450
|
+
...hr,
|
|
2451
|
+
body: { ...hr.body, returnTo }
|
|
2452
|
+
};
|
|
2453
|
+
applyHandlerResponse(res, enriched);
|
|
2454
|
+
});
|
|
2455
|
+
} else {
|
|
2456
|
+
app.get(callbackPath, async (req, res) => {
|
|
2457
|
+
const q = req.query || {};
|
|
2458
|
+
const code = q.code;
|
|
2459
|
+
if (!code) {
|
|
2460
|
+
return failPlain(res, "missing_code", () => {
|
|
2461
|
+
res.status(400);
|
|
2462
|
+
if (res.json) res.json({ success: false, error: { code: "MISSING_CODE", message: "Missing authorization code" } });
|
|
2463
|
+
else res.end?.("Missing authorization code");
|
|
2464
|
+
});
|
|
2465
|
+
}
|
|
2466
|
+
const stateFromQuery = q.state;
|
|
2467
|
+
const stateFromCookie = readCookieFromReq(req, stateCookie);
|
|
2468
|
+
if (stateFromCookie && stateFromQuery !== stateFromCookie) {
|
|
2469
|
+
clearCookie(res, stateCookie);
|
|
2470
|
+
return failPlain(res, "state_mismatch", () => {
|
|
2471
|
+
res.status(400);
|
|
2472
|
+
if (res.json) res.json({ success: false, error: { code: "STATE_MISMATCH", message: "OAuth state mismatch" } });
|
|
2473
|
+
else res.end?.("OAuth state mismatch");
|
|
2474
|
+
});
|
|
2475
|
+
}
|
|
2476
|
+
const codeVerifier = readCookieFromReq(req, PKCE_COOKIE) || "";
|
|
2477
|
+
const proto = req.headers?.["x-forwarded-proto"] || req.protocol || "https";
|
|
2478
|
+
const host = req.headers?.["x-forwarded-host"] || req.headers?.host || "";
|
|
2479
|
+
const redirectUri = `${proto}://${host}${callbackPath}`;
|
|
2480
|
+
const hr = await handleCallback(helperConfig, { code, codeVerifier, redirectUri });
|
|
2481
|
+
for (const c of hr.cookies) {
|
|
2482
|
+
if (typeof res.cookie === "function") {
|
|
2483
|
+
const opts = {
|
|
2484
|
+
httpOnly: c.httpOnly,
|
|
2485
|
+
secure: c.secure,
|
|
2486
|
+
sameSite: c.sameSite,
|
|
2487
|
+
path: c.path,
|
|
2488
|
+
maxAge: c.maxAge * 1e3
|
|
2489
|
+
};
|
|
2490
|
+
if (c.domain) opts.domain = c.domain;
|
|
2491
|
+
res.cookie(c.name, c.value, opts);
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
clearCookie(res, stateCookie);
|
|
2495
|
+
clearCookie(res, PKCE_COOKIE);
|
|
2496
|
+
if (hr.status >= 400) {
|
|
2497
|
+
const code2 = hr.body?.error?.code || "exchange_failed";
|
|
2498
|
+
return failPlain(res, code2, () => {
|
|
2499
|
+
res.status(hr.status);
|
|
2500
|
+
if (res.json) res.json(hr.body);
|
|
2501
|
+
else res.end?.(JSON.stringify(hr.body));
|
|
2502
|
+
});
|
|
2503
|
+
}
|
|
2504
|
+
const returnTo = readCookieFromReq(req, returnToCookie) || hr.body?.returnTo || "/";
|
|
2505
|
+
clearCookie(res, returnToCookie);
|
|
2506
|
+
if (typeof res.redirect === "function") return res.redirect(302, returnTo);
|
|
2507
|
+
res.status(302);
|
|
2508
|
+
if (typeof res.setHeader === "function") res.setHeader("Location", returnTo);
|
|
2509
|
+
res.end?.();
|
|
2510
|
+
});
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2255
2513
|
app.post(`${mount}/refresh`, async (req, res) => {
|
|
2256
2514
|
const body = readBody(req);
|
|
2257
2515
|
const refreshToken = body.refreshToken || readCookieFromReq(req, refreshCookie);
|
|
@@ -2260,7 +2518,8 @@ function iqAuth(options) {
|
|
|
2260
2518
|
});
|
|
2261
2519
|
app.post(`${mount}/signout`, async (req, res) => {
|
|
2262
2520
|
const accessToken = req.headers?.authorization?.replace(/^Bearer /i, "") || readCookieFromReq(req, accessCookie);
|
|
2263
|
-
const
|
|
2521
|
+
const ssoCookieHeader = req.headers?.cookie;
|
|
2522
|
+
const hr = await handleSignout(helperConfig, { accessToken, ssoCookieHeader });
|
|
2264
2523
|
applyHandlerResponse(res, hr);
|
|
2265
2524
|
});
|
|
2266
2525
|
};
|