@iqauth/sdk 2.2.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 +24 -0
- 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 +1 -1
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +13 -2
- package/dist/browser.mjs +2 -2
- package/dist/{chunk-D72UL5HL.mjs → chunk-EKTNEZIH.mjs} +4 -4
- package/dist/{chunk-M4J6BPK7.mjs → chunk-KGEPDXHU.mjs} +10 -1
- package/dist/{chunk-QZB745C2.mjs → chunk-RACIPVLD.mjs} +13 -2
- 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.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-XCI77BQS.mjs → doctor-A5E7LSFW.mjs} +1 -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 +300 -70
- package/dist/express.mjs +208 -7
- package/dist/fastify.js +101 -70
- package/dist/fastify.mjs +8 -6
- package/dist/hono.js +100 -70
- package/dist/hono.mjs +7 -6
- package/dist/index.d.mts +2 -3
- package/dist/index.d.ts +2 -3
- package/dist/index.js +90 -69
- package/dist/index.mjs +15 -13
- 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 +99 -1616
- package/dist/next.mjs +9 -9
- package/dist/react.d.mts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +13 -2
- package/dist/react.mjs +2 -2
- package/dist/server/handlers.d.mts +2 -0
- package/dist/server/handlers.d.ts +2 -0
- package/dist/server/handlers.js +10 -1
- package/dist/server/handlers.mjs +2 -2
- package/dist/server.d.mts +2 -3
- package/dist/server.d.ts +2 -3
- package/dist/server.js +99 -69
- 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-D_kP3v-c.d.mts → signIn-Cd0P4y9d.d.mts} +8 -0
- package/dist/{signIn-BVDTIA_t.d.ts → signIn-DKakyzeu.d.ts} +8 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -477,6 +477,30 @@ Common codes you'll handle:
|
|
|
477
477
|
|
|
478
478
|
---
|
|
479
479
|
|
|
480
|
+
## Don't intercept `/sign-in?code=…`
|
|
481
|
+
|
|
482
|
+
If your app uses an SSO bridge route (e.g. `your-app.com/sign-in` redirects users to `auth.dispositioniq.com`) and that same route is **also** the configured OAuth `redirect_uri`, your bridge logic will fire on the return trip and immediately bounce the user back to the issuer **before** the SDK can exchange the `?code=` for tokens. Symptoms: an infinite redirect loop, or a successful login that the app never sees.
|
|
483
|
+
|
|
484
|
+
**Pick one:**
|
|
485
|
+
|
|
486
|
+
1. **Recommended — use a dedicated callback path.** Configure `redirect_uri` to point at `/api/iqauth/callback` (the helper route mounted by `iqAuth({...}).attachHelpers(app)` for Express, or by `iqAuth({...})` for Next/Fastify/Hono). Your `/sign-in` route stays purely a "send user to the issuer" bridge.
|
|
487
|
+
|
|
488
|
+
2. **If you must reuse `/sign-in` as the redirect target,** guard the bridge:
|
|
489
|
+
|
|
490
|
+
```ts
|
|
491
|
+
app.get("/sign-in", (req, res, next) => {
|
|
492
|
+
// OAuth return trip — let the SDK handle it, don't redirect away.
|
|
493
|
+
if (req.query.code || req.query.error) return next();
|
|
494
|
+
return res.redirect(buildIssuerAuthorizeUrl(req));
|
|
495
|
+
});
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
…and add `inlineCallback: true` to your `iqAuth({...})` options so a GET handler is mounted on the callback path to complete the exchange and 302 to the final destination.
|
|
499
|
+
|
|
500
|
+
This is the single most common bug we see when teams add IQAuth to an app that already had its own session redirect logic.
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
480
504
|
## Bundled docs
|
|
481
505
|
|
|
482
506
|
Long-form integration guides ship **inside the npm tarball** at `node_modules/@iqauth/sdk/docs/`. List them with:
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { c as IQAuthBrowserSessionClientConfig, d as SessionUser } from './types-Cxl3bQHt.mjs';
|
|
2
|
-
import { I as IQAuthClient } from './client-
|
|
2
|
+
import { I as IQAuthClient } from './client-vdh2a9fJ.mjs';
|
|
3
3
|
export { E as ErrorCodes, I as IQAuthError } from './errors-CDdl24MP.mjs';
|
|
4
|
-
import 'jsonwebtoken';
|
|
5
4
|
|
|
6
5
|
declare class BrowserSessionIQAuthClient extends IQAuthClient {
|
|
7
6
|
constructor(config: Omit<IQAuthBrowserSessionClientConfig, "environment">);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { c as IQAuthBrowserSessionClientConfig, d as SessionUser } from './types-Cxl3bQHt.js';
|
|
2
|
-
import { I as IQAuthClient } from './client-
|
|
2
|
+
import { I as IQAuthClient } from './client-DTX4hNdS.js';
|
|
3
3
|
export { E as ErrorCodes, I as IQAuthError } from './errors-CDdl24MP.js';
|
|
4
|
-
import 'jsonwebtoken';
|
|
5
4
|
|
|
6
5
|
declare class BrowserSessionIQAuthClient extends IQAuthClient {
|
|
7
6
|
constructor(config: Omit<IQAuthBrowserSessionClientConfig, "environment">);
|
package/dist/browser-session.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,
|
package/dist/browser-session.mjs
CHANGED
package/dist/browser.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { C as CallbackResult, S as SessionManager, d as SessionManagerOptions, a as SessionSnapshot, e as SessionStatus, b as SignInOptions, c as SignOutOptions, f as buildSignInUrl, h as handleAuthCallback, r as redirectToSignIn, s as signIn, g as signOut } from './signIn-
|
|
1
|
+
export { C as CallbackResult, S as SessionManager, d as SessionManagerOptions, a as SessionSnapshot, e as SessionStatus, b as SignInOptions, c as SignOutOptions, f as buildSignInUrl, h as handleAuthCallback, r as redirectToSignIn, s as signIn, g as signOut } from './signIn-Cd0P4y9d.mjs';
|
|
2
2
|
export { K as KeyMode, c as ParsedPublishableKey, P as PublishableKeyPayload, e as encodePublishableKey, i as isPublishableKey, b as isSecretKey, p as parsePublishableKey } from './publishableKey-BaR0HoAH.mjs';
|
|
3
3
|
export { a as ErrorCode, E as ErrorCodes, I as IQAuthError } from './errors-CDdl24MP.mjs';
|
|
4
4
|
import './types-Cxl3bQHt.mjs';
|
package/dist/browser.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { C as CallbackResult, S as SessionManager, d as SessionManagerOptions, a as SessionSnapshot, e as SessionStatus, b as SignInOptions, c as SignOutOptions, f as buildSignInUrl, h as handleAuthCallback, r as redirectToSignIn, s as signIn, g as signOut } from './signIn-
|
|
1
|
+
export { C as CallbackResult, S as SessionManager, d as SessionManagerOptions, a as SessionSnapshot, e as SessionStatus, b as SignInOptions, c as SignOutOptions, f as buildSignInUrl, h as handleAuthCallback, r as redirectToSignIn, s as signIn, g as signOut } from './signIn-DKakyzeu.js';
|
|
2
2
|
export { K as KeyMode, c as ParsedPublishableKey, P as PublishableKeyPayload, e as encodePublishableKey, i as isPublishableKey, b as isSecretKey, p as parsePublishableKey } from './publishableKey-BaR0HoAH.js';
|
|
3
3
|
export { a as ErrorCode, E as ErrorCodes, I as IQAuthError } from './errors-CDdl24MP.js';
|
|
4
4
|
import './types-Cxl3bQHt.js';
|
package/dist/browser.js
CHANGED
|
@@ -177,7 +177,7 @@ function assertPublishableKey(raw, opts) {
|
|
|
177
177
|
if (!isValidIssuerUrl(decoded.iss)) {
|
|
178
178
|
throw new IQAuthError(
|
|
179
179
|
"CONFIG_INVALID",
|
|
180
|
-
`${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
|
|
180
|
+
`${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.`
|
|
181
181
|
);
|
|
182
182
|
}
|
|
183
183
|
return { mode: shapeMatch[1], iss: decoded.iss, appId: decoded.appId, tenantId: decoded.tenantId, kid: decoded.kid, raw };
|
|
@@ -715,6 +715,7 @@ async function createPkcePair() {
|
|
|
715
715
|
// src/browser/signIn.ts
|
|
716
716
|
var DEFAULT_SIGN_IN_PATH = "/sign-in";
|
|
717
717
|
var DEFAULT_LOGOUT_PATH = "/api/v1/auth/logout";
|
|
718
|
+
var DEFAULT_SSO_LOGOUT_PATH = "/oidc/sso-logout";
|
|
718
719
|
var DEFAULT_TOKEN_PATH = "/oidc/token";
|
|
719
720
|
var DEFAULT_CALLBACK_PATH = "/auth/callback";
|
|
720
721
|
function defaultRedirectUri() {
|
|
@@ -813,11 +814,21 @@ async function handleAuthCallback(manager, options = {}) {
|
|
|
813
814
|
}
|
|
814
815
|
async function signOut(manager, opts = {}) {
|
|
815
816
|
if (!opts.localOnly) {
|
|
817
|
+
const issuer = manager.issuerUrl.replace(/\/$/, "");
|
|
816
818
|
try {
|
|
817
|
-
const url = `${
|
|
819
|
+
const url = `${issuer}${opts.logoutPath ?? DEFAULT_LOGOUT_PATH}`;
|
|
818
820
|
await manager.fetch(url, { method: "POST" }).catch(() => void 0);
|
|
819
821
|
} catch {
|
|
820
822
|
}
|
|
823
|
+
if (opts.endSsoSession !== false) {
|
|
824
|
+
try {
|
|
825
|
+
await fetch(`${issuer}${DEFAULT_SSO_LOGOUT_PATH}`, {
|
|
826
|
+
method: "POST",
|
|
827
|
+
credentials: "include"
|
|
828
|
+
}).catch(() => void 0);
|
|
829
|
+
} catch {
|
|
830
|
+
}
|
|
831
|
+
}
|
|
821
832
|
}
|
|
822
833
|
clearCookie(REFRESH_COOKIE);
|
|
823
834
|
manager.signOutLocal();
|
package/dist/browser.mjs
CHANGED
|
@@ -12,13 +12,13 @@ import {
|
|
|
12
12
|
setCookie,
|
|
13
13
|
signIn,
|
|
14
14
|
signOut
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-RACIPVLD.mjs";
|
|
16
16
|
import {
|
|
17
17
|
encodePublishableKey,
|
|
18
18
|
isPublishableKey,
|
|
19
19
|
isSecretKey,
|
|
20
20
|
parsePublishableKey
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-WQWBJSSS.mjs";
|
|
22
22
|
import {
|
|
23
23
|
ErrorCodes,
|
|
24
24
|
IQAuthError
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
assertPublishableKey
|
|
3
|
-
} from "./chunk-QEJB7WEQ.mjs";
|
|
4
1
|
import {
|
|
5
2
|
IQAuthClient
|
|
6
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-W3F4JYGP.mjs";
|
|
4
|
+
import {
|
|
5
|
+
assertPublishableKey
|
|
6
|
+
} from "./chunk-WQWBJSSS.mjs";
|
|
7
7
|
import {
|
|
8
8
|
IQAuthError
|
|
9
9
|
} from "./chunk-6I6RM4MN.mjs";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
assertPublishableKey
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-WQWBJSSS.mjs";
|
|
4
4
|
|
|
5
5
|
// src/server/handlers.ts
|
|
6
6
|
var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
@@ -193,6 +193,15 @@ async function handleSignout(config, input) {
|
|
|
193
193
|
} catch {
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
|
+
if (input.endSsoSession !== false && input.ssoCookieHeader) {
|
|
197
|
+
try {
|
|
198
|
+
await cfg.fetchImpl(`${cfg.issuer}/oidc/sso-logout`, {
|
|
199
|
+
method: "POST",
|
|
200
|
+
headers: { Cookie: input.ssoCookieHeader }
|
|
201
|
+
});
|
|
202
|
+
} catch {
|
|
203
|
+
}
|
|
204
|
+
}
|
|
196
205
|
return {
|
|
197
206
|
status: 200,
|
|
198
207
|
body: { success: true, data: { signedOut: true } },
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
assertPublishableKey
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-WQWBJSSS.mjs";
|
|
4
4
|
import {
|
|
5
5
|
IQAuthError
|
|
6
6
|
} from "./chunk-6I6RM4MN.mjs";
|
|
@@ -526,6 +526,7 @@ async function createPkcePair() {
|
|
|
526
526
|
// src/browser/signIn.ts
|
|
527
527
|
var DEFAULT_SIGN_IN_PATH = "/sign-in";
|
|
528
528
|
var DEFAULT_LOGOUT_PATH = "/api/v1/auth/logout";
|
|
529
|
+
var DEFAULT_SSO_LOGOUT_PATH = "/oidc/sso-logout";
|
|
529
530
|
var DEFAULT_TOKEN_PATH = "/oidc/token";
|
|
530
531
|
var DEFAULT_CALLBACK_PATH = "/auth/callback";
|
|
531
532
|
function defaultRedirectUri() {
|
|
@@ -624,11 +625,21 @@ async function handleAuthCallback(manager, options = {}) {
|
|
|
624
625
|
}
|
|
625
626
|
async function signOut(manager, opts = {}) {
|
|
626
627
|
if (!opts.localOnly) {
|
|
628
|
+
const issuer = manager.issuerUrl.replace(/\/$/, "");
|
|
627
629
|
try {
|
|
628
|
-
const url = `${
|
|
630
|
+
const url = `${issuer}${opts.logoutPath ?? DEFAULT_LOGOUT_PATH}`;
|
|
629
631
|
await manager.fetch(url, { method: "POST" }).catch(() => void 0);
|
|
630
632
|
} catch {
|
|
631
633
|
}
|
|
634
|
+
if (opts.endSsoSession !== false) {
|
|
635
|
+
try {
|
|
636
|
+
await fetch(`${issuer}${DEFAULT_SSO_LOGOUT_PATH}`, {
|
|
637
|
+
method: "POST",
|
|
638
|
+
credentials: "include"
|
|
639
|
+
}).catch(() => void 0);
|
|
640
|
+
} catch {
|
|
641
|
+
}
|
|
642
|
+
}
|
|
632
643
|
}
|
|
633
644
|
clearCookie(REFRESH_COOKIE);
|
|
634
645
|
manager.signOutLocal();
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IQAuthError
|
|
3
|
+
} from "./chunk-6I6RM4MN.mjs";
|
|
4
|
+
import {
|
|
5
|
+
__require
|
|
6
|
+
} from "./chunk-Y6FXYEAI.mjs";
|
|
7
|
+
|
|
8
|
+
// src/modules/tokens.ts
|
|
9
|
+
import {
|
|
10
|
+
createLocalJWKSet,
|
|
11
|
+
jwtVerify,
|
|
12
|
+
errors as joseErrors
|
|
13
|
+
} from "jose";
|
|
14
|
+
var JWKS_CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
15
|
+
var DEFAULT_TOKEN_ISSUER = [
|
|
16
|
+
"https://auth.dispositioniq.com",
|
|
17
|
+
"auth.dispositioniq.com"
|
|
18
|
+
];
|
|
19
|
+
var DEFAULT_TOKEN_AUDIENCE = [
|
|
20
|
+
"dispositioniq",
|
|
21
|
+
"iqcapture",
|
|
22
|
+
"iqreuse",
|
|
23
|
+
"iqvalidate"
|
|
24
|
+
];
|
|
25
|
+
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
26
|
+
function decodeProtectedHeader(token) {
|
|
27
|
+
const parts = token.split(".");
|
|
28
|
+
if (parts.length < 2) return null;
|
|
29
|
+
try {
|
|
30
|
+
const padded = parts[0] + "=".repeat((4 - parts[0].length % 4) % 4);
|
|
31
|
+
const b64 = padded.replace(/-/g, "+").replace(/_/g, "/");
|
|
32
|
+
let json;
|
|
33
|
+
if (typeof atob === "function") {
|
|
34
|
+
json = atob(b64);
|
|
35
|
+
} else {
|
|
36
|
+
const { Buffer } = __require("buffer");
|
|
37
|
+
json = Buffer.from(b64, "base64").toString("utf8");
|
|
38
|
+
}
|
|
39
|
+
return JSON.parse(json);
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
var TokensModule = class {
|
|
45
|
+
constructor(baseUrl, options = {}) {
|
|
46
|
+
this.jwksCache = null;
|
|
47
|
+
this.inFlightRefresh = null;
|
|
48
|
+
this.baseUrl = baseUrl;
|
|
49
|
+
this.defaultIssuer = options.issuer ?? DEFAULT_TOKEN_ISSUER;
|
|
50
|
+
this.defaultAudience = options.audience ?? DEFAULT_TOKEN_AUDIENCE;
|
|
51
|
+
this.defaultClockTolerance = options.clockTolerance ?? DEFAULT_CLOCK_TOLERANCE_SECONDS;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Verify a JWT access token using RS256/ES256 via JWKS from
|
|
55
|
+
* `/.well-known/jwks.json`. Backed by `jose` (Web Crypto) so it runs on
|
|
56
|
+
* Node, browser, and edge runtimes alike — no `node:crypto` dependency.
|
|
57
|
+
* Caches JWKS for 1 hour and refetches once on unknown `kid`.
|
|
58
|
+
*/
|
|
59
|
+
async verify(token, options = {}) {
|
|
60
|
+
const header = decodeProtectedHeader(token);
|
|
61
|
+
if (!header) {
|
|
62
|
+
throw new IQAuthError("TOKEN_INVALID", "Unable to decode token");
|
|
63
|
+
}
|
|
64
|
+
const kid = header.kid;
|
|
65
|
+
if (!kid) {
|
|
66
|
+
throw new IQAuthError("TOKEN_INVALID", "Token missing kid header");
|
|
67
|
+
}
|
|
68
|
+
let cache = await this.ensureCache();
|
|
69
|
+
if (!cache.byKid.has(kid)) {
|
|
70
|
+
this.jwksCache = null;
|
|
71
|
+
cache = await this.ensureCache();
|
|
72
|
+
}
|
|
73
|
+
if (!cache.byKid.has(kid)) {
|
|
74
|
+
throw new IQAuthError("TOKEN_INVALID", `Unknown key ID: ${kid}`);
|
|
75
|
+
}
|
|
76
|
+
const issuer = options.issuer ?? this.defaultIssuer;
|
|
77
|
+
const audience = options.audience ?? this.defaultAudience;
|
|
78
|
+
const clockTolerance = options.clockTolerance ?? this.defaultClockTolerance;
|
|
79
|
+
const algorithms = options.algorithms ?? ["RS256", "ES256"];
|
|
80
|
+
const verifyOptions = {
|
|
81
|
+
algorithms,
|
|
82
|
+
clockTolerance,
|
|
83
|
+
issuer,
|
|
84
|
+
audience
|
|
85
|
+
};
|
|
86
|
+
try {
|
|
87
|
+
const { payload } = await jwtVerify(token, cache.verifier, verifyOptions);
|
|
88
|
+
return payload;
|
|
89
|
+
} catch (err) {
|
|
90
|
+
if (err instanceof joseErrors.JWTExpired) {
|
|
91
|
+
throw new IQAuthError("TOKEN_EXPIRED", "Token has expired");
|
|
92
|
+
}
|
|
93
|
+
if (err instanceof joseErrors.JOSEError) {
|
|
94
|
+
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
95
|
+
}
|
|
96
|
+
if (err instanceof Error) {
|
|
97
|
+
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
98
|
+
}
|
|
99
|
+
throw new IQAuthError("TOKEN_INVALID", "Token verification failed");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Decode a JWT without verification. Returns null if malformed.
|
|
104
|
+
*/
|
|
105
|
+
decode(token) {
|
|
106
|
+
try {
|
|
107
|
+
const parts = token.split(".");
|
|
108
|
+
if (parts.length < 2) return null;
|
|
109
|
+
const payload = parts[1];
|
|
110
|
+
const padded = payload + "=".repeat((4 - payload.length % 4) % 4);
|
|
111
|
+
const b64 = padded.replace(/-/g, "+").replace(/_/g, "/");
|
|
112
|
+
let json;
|
|
113
|
+
if (typeof atob === "function") {
|
|
114
|
+
json = atob(b64);
|
|
115
|
+
} else {
|
|
116
|
+
const { Buffer } = __require("buffer");
|
|
117
|
+
json = Buffer.from(b64, "base64").toString("utf8");
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
json = decodeURIComponent(escape(json));
|
|
121
|
+
} catch {
|
|
122
|
+
}
|
|
123
|
+
const claims = JSON.parse(json);
|
|
124
|
+
if (!claims || typeof claims !== "object") return null;
|
|
125
|
+
return claims;
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** Check if a token is expired based on the `exp` claim. */
|
|
131
|
+
isExpired(token) {
|
|
132
|
+
const claims = this.decode(token);
|
|
133
|
+
if (!claims?.exp) return true;
|
|
134
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
135
|
+
return claims.exp <= now;
|
|
136
|
+
}
|
|
137
|
+
/** Get the claims from a token without verification. */
|
|
138
|
+
getClaims(token) {
|
|
139
|
+
const claims = this.decode(token);
|
|
140
|
+
if (!claims) {
|
|
141
|
+
throw new IQAuthError("TOKEN_INVALID", "Unable to decode token claims");
|
|
142
|
+
}
|
|
143
|
+
return claims;
|
|
144
|
+
}
|
|
145
|
+
async ensureCache() {
|
|
146
|
+
if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) {
|
|
147
|
+
return this.jwksCache;
|
|
148
|
+
}
|
|
149
|
+
await this.refreshJwks();
|
|
150
|
+
if (!this.jwksCache) {
|
|
151
|
+
throw new IQAuthError("INTERNAL_ERROR", "JWKS cache unavailable after refresh");
|
|
152
|
+
}
|
|
153
|
+
return this.jwksCache;
|
|
154
|
+
}
|
|
155
|
+
async refreshJwks() {
|
|
156
|
+
if (this.inFlightRefresh) {
|
|
157
|
+
return this.inFlightRefresh;
|
|
158
|
+
}
|
|
159
|
+
this.inFlightRefresh = (async () => {
|
|
160
|
+
try {
|
|
161
|
+
const res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
|
|
162
|
+
if (!res.ok) {
|
|
163
|
+
throw new IQAuthError(
|
|
164
|
+
"INTERNAL_ERROR",
|
|
165
|
+
`Failed to fetch JWKS: ${res.status}`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
let jwks;
|
|
169
|
+
try {
|
|
170
|
+
jwks = await res.json();
|
|
171
|
+
} catch {
|
|
172
|
+
throw new IQAuthError("INTERNAL_ERROR", "Malformed JWKS response: invalid JSON");
|
|
173
|
+
}
|
|
174
|
+
if (!jwks || !Array.isArray(jwks.keys)) {
|
|
175
|
+
throw new IQAuthError(
|
|
176
|
+
"INTERNAL_ERROR",
|
|
177
|
+
"Malformed JWKS response: expected { keys: [...] }"
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
const byKid = /* @__PURE__ */ new Set();
|
|
181
|
+
for (const key of jwks.keys) {
|
|
182
|
+
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")) {
|
|
183
|
+
throw new IQAuthError(
|
|
184
|
+
"INTERNAL_ERROR",
|
|
185
|
+
"Malformed JWKS response: key missing required fields"
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
byKid.add(key.kid);
|
|
189
|
+
}
|
|
190
|
+
const verifier = createLocalJWKSet({ keys: jwks.keys });
|
|
191
|
+
this.jwksCache = { raw: jwks.keys, byKid, verifier, fetchedAt: Date.now() };
|
|
192
|
+
} finally {
|
|
193
|
+
this.inFlightRefresh = null;
|
|
194
|
+
}
|
|
195
|
+
})();
|
|
196
|
+
return this.inFlightRefresh;
|
|
197
|
+
}
|
|
198
|
+
/** @internal Exposed for testing — clears JWKS cache */
|
|
199
|
+
clearCache() {
|
|
200
|
+
this.jwksCache = null;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
export {
|
|
205
|
+
DEFAULT_TOKEN_ISSUER,
|
|
206
|
+
DEFAULT_TOKEN_AUDIENCE,
|
|
207
|
+
DEFAULT_CLOCK_TOLERANCE_SECONDS,
|
|
208
|
+
TokensModule
|
|
209
|
+
};
|