@iqauth/sdk 2.6.4 → 2.8.1
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 +173 -1
- package/dist/browser-session.d.mts +4 -4
- package/dist/browser-session.d.ts +4 -4
- package/dist/browser-session.js +212 -46
- package/dist/browser-session.mjs +3 -3
- package/dist/browser.d.mts +5 -5
- package/dist/browser.d.ts +5 -5
- package/dist/browser.js +293 -34
- package/dist/browser.mjs +5 -5
- package/dist/{chunk-BVV54LPI.mjs → chunk-25SSYDIP.mjs} +10 -4
- package/dist/{chunk-XAWYUPMO.mjs → chunk-4V7FKOTG.mjs} +242 -22
- package/dist/{chunk-6I6RM4MN.mjs → chunk-6PJRLRB4.mjs} +33 -3
- package/dist/{chunk-SL3KRS4W.mjs → chunk-CIJORODR.mjs} +23 -1
- package/dist/{chunk-LIZYFXH7.mjs → chunk-DFWHSDYQ.mjs} +1 -1
- package/dist/chunk-GLXSIGVS.mjs +66 -0
- package/dist/{chunk-DJIBN2N7.mjs → chunk-GN37E64I.mjs} +29 -7
- package/dist/{chunk-WQWBJSSS.mjs → chunk-HVHNYPDC.mjs} +6 -6
- package/dist/chunk-JRDVUWAL.mjs +46 -0
- package/dist/{chunk-UNYDG2L4.mjs → chunk-NUO2I65G.mjs} +56 -23
- package/dist/{chunk-5T7GHBX6.mjs → chunk-TLET552H.mjs} +36 -0
- package/dist/chunk-VYQ3ETCK.mjs +244 -0
- package/dist/{chunk-3JULWS6F.mjs → chunk-WCELYTJ3.mjs} +3 -3
- package/dist/chunk-WHT6WKTY.mjs +3180 -0
- package/dist/{chunk-MKKZULZR.mjs → chunk-WIFG74IK.mjs} +1 -1
- package/dist/chunk-WSH4SW7F.mjs +490 -0
- package/dist/{chunk-W3F4JYGP.mjs → chunk-ZLJPABB7.mjs} +139 -23
- package/dist/cli/index.js +2 -2
- package/dist/cli/index.mjs +2 -2
- package/dist/{client-BNQe3AgF.d.ts → client-D8L-PaWr.d.mts} +59 -6
- package/dist/{client-kYlJFgPv.d.mts → client-DkPL0EPZ.d.ts} +59 -6
- package/dist/{doctor-YYNHNMLD.mjs → doctor-JAFXWU3X.mjs} +2 -2
- package/dist/errors-Jl1Jtm-6.d.mts +107 -0
- package/dist/errors-Jl1Jtm-6.d.ts +107 -0
- package/dist/{express-CHpfa7D_.d.ts → express-Budysq4h.d.ts} +2 -2
- package/dist/{express-B6_1vBYZ.d.mts → express-DDTA3qV1.d.mts} +2 -2
- package/dist/express.d.mts +7 -6
- package/dist/express.d.ts +7 -6
- package/dist/express.js +563 -85
- package/dist/express.mjs +73 -34
- package/dist/fastify.d.mts +10 -0
- package/dist/fastify.d.ts +10 -0
- package/dist/fastify.js +589 -65
- package/dist/fastify.mjs +101 -11
- package/dist/hono.d.mts +10 -0
- package/dist/hono.d.ts +10 -0
- package/dist/hono.js +566 -65
- package/dist/hono.mjs +78 -11
- package/dist/index-Cko-d5po.d.mts +1848 -0
- package/dist/index-RNqwEcmY.d.ts +1848 -0
- package/dist/index.d.mts +56 -8
- package/dist/index.d.ts +56 -8
- package/dist/index.js +694 -75
- package/dist/index.mjs +30 -10
- package/dist/{keys-NLWFAOEM.mjs → keys-6Y776TG2.mjs} +2 -2
- package/dist/locales.d.mts +1 -1
- package/dist/locales.d.ts +1 -1
- package/dist/locales.js +36 -0
- package/dist/locales.mjs +1 -1
- package/dist/mobile.d.mts +77 -7
- package/dist/mobile.d.ts +77 -7
- package/dist/mobile.js +307 -46
- package/dist/mobile.mjs +98 -3
- package/dist/next.d.mts +10 -1
- package/dist/next.d.ts +10 -1
- package/dist/next.js +596 -205
- package/dist/next.mjs +83 -10
- package/dist/{provisioningBridge-88xjOS2n.d.mts → provisioningBridge-BXPMZCLe.d.ts} +30 -2
- package/dist/{provisioningBridge-DnTfzdZK.d.ts → provisioningBridge-IEycmsgb.d.mts} +30 -2
- package/dist/{publishableKey-BaR0HoAH.d.ts → publishableKey-f2kq-rKw.d.mts} +1 -1
- package/dist/{publishableKey-BaR0HoAH.d.mts → publishableKey-f2kq-rKw.d.ts} +1 -1
- package/dist/react-permissions.d.mts +52 -0
- package/dist/react-permissions.d.ts +52 -0
- package/dist/react-permissions.js +239 -0
- package/dist/react-permissions.mjs +98 -0
- package/dist/react.d.mts +9 -1624
- package/dist/react.d.ts +9 -1624
- package/dist/react.js +882 -73
- package/dist/react.mjs +71 -2631
- package/dist/{reverify-4UEJXUS6.mjs → reverify-C64QXKJO.mjs} +2 -2
- package/dist/server/handlers.d.mts +200 -4
- package/dist/server/handlers.d.ts +200 -4
- package/dist/server/handlers.js +530 -16
- package/dist/server/handlers.mjs +14 -3
- package/dist/server.d.mts +171 -8
- package/dist/server.d.ts +171 -8
- package/dist/server.js +579 -61
- package/dist/server.mjs +99 -12
- package/dist/service.d.mts +4 -4
- package/dist/service.d.ts +4 -4
- package/dist/service.js +212 -46
- package/dist/service.mjs +3 -3
- package/dist/{signIn-CiIBTJIh.d.mts → signIn-CReqfXsh.d.mts} +95 -3
- package/dist/{signIn-OCr88Zf8.d.ts → signIn-Cfa1GTpO.d.ts} +95 -3
- package/dist/{signIn-4OKLDEIH.mjs → signIn-SHBW6Z4T.mjs} +1 -1
- package/dist/test.mjs +3 -3
- package/dist/{tokens-DCyzzn8L.d.mts → tokens-9F6ETrzk.d.ts} +9 -2
- package/dist/{tokens-aHiGFr_E.d.ts → tokens-B06VtvUi.d.mts} +9 -2
- package/dist/{types-DZAflmmq.d.mts → types-Bn8O-OEd.d.mts} +164 -11
- package/dist/{types-DZAflmmq.d.ts → types-Bn8O-OEd.d.ts} +164 -11
- package/dist/{types-6bNdxesb.d.ts → types-DnU2LhXR.d.mts} +7 -1
- package/dist/{types-6bNdxesb.d.mts → types-DnU2LhXR.d.ts} +7 -1
- package/dist/webhooks.d.mts +113 -17
- package/dist/webhooks.d.ts +113 -17
- package/dist/webhooks.js +179 -15
- package/dist/webhooks.mjs +7 -1
- package/dist/ws.d.mts +2 -2
- package/dist/ws.d.ts +2 -2
- package/dist/ws.js +80 -30
- package/dist/ws.mjs +4 -4
- package/docs/error-handling.md +101 -0
- package/docs/guides/effective-permissions.md +171 -0
- package/docs/guides/invitations.md +65 -0
- package/package.json +19 -4
- package/dist/chunk-6TDJJER7.mjs +0 -217
- package/dist/chunk-UKZLOHZG.mjs +0 -83
- package/dist/errors-CDdl24MP.d.mts +0 -52
- package/dist/errors-CDdl24MP.d.ts +0 -52
package/dist/express.js
CHANGED
|
@@ -38,13 +38,30 @@ __export(express_exports, {
|
|
|
38
38
|
module.exports = __toCommonJS(express_exports);
|
|
39
39
|
|
|
40
40
|
// src/errors.ts
|
|
41
|
-
var IQAuthError = class extends Error {
|
|
42
|
-
constructor(code, message, status,
|
|
41
|
+
var IQAuthError = class _IQAuthError extends Error {
|
|
42
|
+
constructor(code, message, status, cause) {
|
|
43
43
|
super(message);
|
|
44
44
|
this.name = "IQAuthError";
|
|
45
45
|
this.code = code;
|
|
46
46
|
this.status = status;
|
|
47
|
-
this.
|
|
47
|
+
this.cause = cause;
|
|
48
|
+
this.raw = cause;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Type guard: true when `value` is an `IQAuthError`. Useful for adapters
|
|
52
|
+
* that round-trip errors through `unknown` (e.g. fastify's `setErrorHandler`).
|
|
53
|
+
*/
|
|
54
|
+
static isIQAuthError(value) {
|
|
55
|
+
return value instanceof _IQAuthError || typeof value === "object" && value !== null && value.name === "IQAuthError" && typeof value.code === "string";
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Type-narrowed code check. Lets callers write
|
|
59
|
+
* `if (err.is("token_expired")) …` with full IntelliSense for the typed
|
|
60
|
+
* taxonomy without losing the ability to handle server codes via
|
|
61
|
+
* `err.code === "TOKEN_REVOKED"`.
|
|
62
|
+
*/
|
|
63
|
+
is(code) {
|
|
64
|
+
return this.code === code;
|
|
48
65
|
}
|
|
49
66
|
};
|
|
50
67
|
var ErrorCodes = {
|
|
@@ -195,7 +212,7 @@ var HttpClient = class {
|
|
|
195
212
|
headers: this.buildHeaders(),
|
|
196
213
|
...this.isBrowserSession() ? { credentials: "include" } : (() => {
|
|
197
214
|
const refreshToken = this.config.getRefreshToken();
|
|
198
|
-
if (!refreshToken) throw new IQAuthError("
|
|
215
|
+
if (!refreshToken) throw new IQAuthError("config_invalid", "No refresh token available");
|
|
199
216
|
return { body: JSON.stringify({ refreshToken }) };
|
|
200
217
|
})()
|
|
201
218
|
});
|
|
@@ -212,7 +229,7 @@ var HttpClient = class {
|
|
|
212
229
|
return;
|
|
213
230
|
}
|
|
214
231
|
if (!body.data.accessToken || !body.data.refreshToken) {
|
|
215
|
-
throw new IQAuthError("
|
|
232
|
+
throw new IQAuthError("token_invalid", "Refresh response did not include a token pair");
|
|
216
233
|
}
|
|
217
234
|
const tokens = {
|
|
218
235
|
accessToken: body.data.accessToken,
|
|
@@ -230,7 +247,7 @@ var HttpClient = class {
|
|
|
230
247
|
return this.requestWithRetry(method, path, body, options, false);
|
|
231
248
|
}
|
|
232
249
|
async requestWithRetry(method, path, body, options, hasRetried) {
|
|
233
|
-
if (this.config.autoRefresh && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
|
|
250
|
+
if (this.config.autoRefresh && this.config.proactiveRefresh !== false && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
|
|
234
251
|
await this.attemptRefresh();
|
|
235
252
|
}
|
|
236
253
|
const url = `${this.config.baseUrl}${path}`;
|
|
@@ -317,17 +334,27 @@ function parseLoginResponse(data, browserSessionMode) {
|
|
|
317
334
|
tenants: data.tenants
|
|
318
335
|
};
|
|
319
336
|
}
|
|
337
|
+
if (data.type === "scope_selection" && data.scopeSelectionToken && data.scopes && data.tenantId) {
|
|
338
|
+
return {
|
|
339
|
+
status: "scope_selection",
|
|
340
|
+
scopeSelectionToken: data.scopeSelectionToken,
|
|
341
|
+
tenantId: data.tenantId,
|
|
342
|
+
scopes: data.scopes
|
|
343
|
+
};
|
|
344
|
+
}
|
|
320
345
|
throw new Error("Unexpected login response shape");
|
|
321
346
|
}
|
|
322
347
|
var AuthModule = class {
|
|
323
348
|
constructor(http) {
|
|
324
349
|
this.http = http;
|
|
325
350
|
}
|
|
326
|
-
async login(email, password) {
|
|
351
|
+
async login(email, password, opts) {
|
|
352
|
+
const body = { email, password };
|
|
353
|
+
if (opts?.scopeHint) body.scopeHint = opts.scopeHint;
|
|
327
354
|
const data = await this.http.request(
|
|
328
355
|
"POST",
|
|
329
356
|
"/api/v1/auth/login",
|
|
330
|
-
|
|
357
|
+
body,
|
|
331
358
|
{ skipAutoRefresh: true }
|
|
332
359
|
);
|
|
333
360
|
return parseLoginResponse(data, this.http.isBrowserSession());
|
|
@@ -365,13 +392,29 @@ var AuthModule = class {
|
|
|
365
392
|
method
|
|
366
393
|
}, { skipAutoRefresh: true });
|
|
367
394
|
}
|
|
368
|
-
async selectTenant(tenantSelectionToken, tenantId) {
|
|
395
|
+
async selectTenant(tenantSelectionToken, tenantId, opts) {
|
|
396
|
+
const body = { tenantSelectionToken, tenantId };
|
|
397
|
+
if (opts?.scopeHint) body.scopeHint = opts.scopeHint;
|
|
369
398
|
const data = await this.http.request(
|
|
370
399
|
"POST",
|
|
371
400
|
"/api/v1/auth/select-tenant",
|
|
401
|
+
body,
|
|
402
|
+
{ skipAutoRefresh: true }
|
|
403
|
+
);
|
|
404
|
+
return parseLoginResponse(data, this.http.isBrowserSession());
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Task #171 — redeem a scope-selection token + chosen membership for a
|
|
408
|
+
* real authenticated session. `membershipId` must be one of the scopes
|
|
409
|
+
* returned in the prior `scope_selection` envelope.
|
|
410
|
+
*/
|
|
411
|
+
async selectScope(scopeSelectionToken, membershipId) {
|
|
412
|
+
const data = await this.http.request(
|
|
413
|
+
"POST",
|
|
414
|
+
"/api/v1/auth/select-scope",
|
|
372
415
|
{
|
|
373
|
-
|
|
374
|
-
|
|
416
|
+
scopeSelectionToken,
|
|
417
|
+
membershipId
|
|
375
418
|
},
|
|
376
419
|
{ skipAutoRefresh: true }
|
|
377
420
|
);
|
|
@@ -458,6 +501,18 @@ var DEFAULT_TOKEN_AUDIENCE = [
|
|
|
458
501
|
"iqvalidate"
|
|
459
502
|
];
|
|
460
503
|
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
504
|
+
function classifyJoseError(err) {
|
|
505
|
+
if (err instanceof import_jose.errors.JWTExpired) {
|
|
506
|
+
return { code: "token_expired", message: "Token has expired" };
|
|
507
|
+
}
|
|
508
|
+
if (err instanceof import_jose.errors.JOSEError) {
|
|
509
|
+
return { code: "token_invalid", message: err.message };
|
|
510
|
+
}
|
|
511
|
+
if (err instanceof Error) {
|
|
512
|
+
return { code: "token_invalid", message: err.message };
|
|
513
|
+
}
|
|
514
|
+
return { code: "token_invalid", message: "Token verification failed" };
|
|
515
|
+
}
|
|
461
516
|
function decodeProtectedHeader(token) {
|
|
462
517
|
const parts = token.split(".");
|
|
463
518
|
if (parts.length < 2) return null;
|
|
@@ -494,11 +549,11 @@ var TokensModule = class {
|
|
|
494
549
|
async verify(token, options = {}) {
|
|
495
550
|
const header = decodeProtectedHeader(token);
|
|
496
551
|
if (!header) {
|
|
497
|
-
throw new IQAuthError("
|
|
552
|
+
throw new IQAuthError("token_invalid", "Unable to decode token");
|
|
498
553
|
}
|
|
499
554
|
const kid = header.kid;
|
|
500
555
|
if (!kid) {
|
|
501
|
-
throw new IQAuthError("
|
|
556
|
+
throw new IQAuthError("token_invalid", "Token missing kid header");
|
|
502
557
|
}
|
|
503
558
|
let cache = await this.ensureCache();
|
|
504
559
|
if (!cache.byKid.has(kid)) {
|
|
@@ -506,7 +561,7 @@ var TokensModule = class {
|
|
|
506
561
|
cache = await this.ensureCache();
|
|
507
562
|
}
|
|
508
563
|
if (!cache.byKid.has(kid)) {
|
|
509
|
-
throw new IQAuthError("
|
|
564
|
+
throw new IQAuthError("token_invalid", `Unknown key ID: ${kid}`);
|
|
510
565
|
}
|
|
511
566
|
const issuer = options.issuer ?? this.defaultIssuer;
|
|
512
567
|
const audience = options.audience ?? this.defaultAudience;
|
|
@@ -522,16 +577,8 @@ var TokensModule = class {
|
|
|
522
577
|
const { payload } = await (0, import_jose.jwtVerify)(token, cache.verifier, verifyOptions);
|
|
523
578
|
return payload;
|
|
524
579
|
} catch (err) {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
}
|
|
528
|
-
if (err instanceof import_jose.errors.JOSEError) {
|
|
529
|
-
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
530
|
-
}
|
|
531
|
-
if (err instanceof Error) {
|
|
532
|
-
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
533
|
-
}
|
|
534
|
-
throw new IQAuthError("TOKEN_INVALID", "Token verification failed");
|
|
580
|
+
const classified = classifyJoseError(err);
|
|
581
|
+
throw new IQAuthError(classified.code, classified.message, void 0, err);
|
|
535
582
|
}
|
|
536
583
|
}
|
|
537
584
|
/**
|
|
@@ -573,7 +620,7 @@ var TokensModule = class {
|
|
|
573
620
|
getClaims(token) {
|
|
574
621
|
const claims = this.decode(token);
|
|
575
622
|
if (!claims) {
|
|
576
|
-
throw new IQAuthError("
|
|
623
|
+
throw new IQAuthError("token_invalid", "Unable to decode token claims");
|
|
577
624
|
}
|
|
578
625
|
return claims;
|
|
579
626
|
}
|
|
@@ -583,7 +630,7 @@ var TokensModule = class {
|
|
|
583
630
|
}
|
|
584
631
|
await this.refreshJwks();
|
|
585
632
|
if (!this.jwksCache) {
|
|
586
|
-
throw new IQAuthError("
|
|
633
|
+
throw new IQAuthError("jwks_unavailable", "JWKS cache unavailable after refresh");
|
|
587
634
|
}
|
|
588
635
|
return this.jwksCache;
|
|
589
636
|
}
|
|
@@ -593,22 +640,38 @@ var TokensModule = class {
|
|
|
593
640
|
}
|
|
594
641
|
this.inFlightRefresh = (async () => {
|
|
595
642
|
try {
|
|
596
|
-
|
|
643
|
+
let res;
|
|
644
|
+
try {
|
|
645
|
+
res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
|
|
646
|
+
} catch (err) {
|
|
647
|
+
throw new IQAuthError(
|
|
648
|
+
"network",
|
|
649
|
+
err instanceof Error ? err.message : "JWKS fetch network error",
|
|
650
|
+
void 0,
|
|
651
|
+
err
|
|
652
|
+
);
|
|
653
|
+
}
|
|
597
654
|
if (!res.ok) {
|
|
598
655
|
throw new IQAuthError(
|
|
599
|
-
"
|
|
600
|
-
`Failed to fetch JWKS: ${res.status}
|
|
656
|
+
"jwks_fetch_failed",
|
|
657
|
+
`Failed to fetch JWKS: ${res.status}`,
|
|
658
|
+
res.status
|
|
601
659
|
);
|
|
602
660
|
}
|
|
603
661
|
let jwks;
|
|
604
662
|
try {
|
|
605
663
|
jwks = await res.json();
|
|
606
|
-
} catch {
|
|
607
|
-
throw new IQAuthError(
|
|
664
|
+
} catch (err) {
|
|
665
|
+
throw new IQAuthError(
|
|
666
|
+
"jwks_fetch_failed",
|
|
667
|
+
"Malformed JWKS response: invalid JSON",
|
|
668
|
+
res.status,
|
|
669
|
+
err
|
|
670
|
+
);
|
|
608
671
|
}
|
|
609
672
|
if (!jwks || !Array.isArray(jwks.keys)) {
|
|
610
673
|
throw new IQAuthError(
|
|
611
|
-
"
|
|
674
|
+
"jwks_fetch_failed",
|
|
612
675
|
"Malformed JWKS response: expected { keys: [...] }"
|
|
613
676
|
);
|
|
614
677
|
}
|
|
@@ -616,7 +679,7 @@ var TokensModule = class {
|
|
|
616
679
|
for (const key of jwks.keys) {
|
|
617
680
|
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")) {
|
|
618
681
|
throw new IQAuthError(
|
|
619
|
-
"
|
|
682
|
+
"jwks_fetch_failed",
|
|
620
683
|
"Malformed JWKS response: key missing required fields"
|
|
621
684
|
);
|
|
622
685
|
}
|
|
@@ -634,6 +697,19 @@ var TokensModule = class {
|
|
|
634
697
|
clearCache() {
|
|
635
698
|
this.jwksCache = null;
|
|
636
699
|
}
|
|
700
|
+
/**
|
|
701
|
+
* Task #126: Eagerly populate the JWKS cache so the first verify() call
|
|
702
|
+
* doesn't pay a network round-trip. Safe to call repeatedly — single-flight
|
|
703
|
+
* behavior is shared with the lazy refresh path. Errors are swallowed so
|
|
704
|
+
* callers (e.g. `attachHelpers` auto-prewarm) can fire-and-forget.
|
|
705
|
+
*/
|
|
706
|
+
async prewarm() {
|
|
707
|
+
if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) return;
|
|
708
|
+
try {
|
|
709
|
+
await this.refreshJwks();
|
|
710
|
+
} catch {
|
|
711
|
+
}
|
|
712
|
+
}
|
|
637
713
|
};
|
|
638
714
|
|
|
639
715
|
// src/modules/sessions.ts
|
|
@@ -957,14 +1033,14 @@ var OidcModule = class {
|
|
|
957
1033
|
*/
|
|
958
1034
|
async handleCallback(params) {
|
|
959
1035
|
if (!params.state) {
|
|
960
|
-
throw new IQAuthError("
|
|
1036
|
+
throw new IQAuthError("config_invalid", "OIDC callback missing state parameter");
|
|
961
1037
|
}
|
|
962
1038
|
if (!params.code) {
|
|
963
|
-
throw new IQAuthError("
|
|
1039
|
+
throw new IQAuthError("config_invalid", "OIDC callback missing code parameter");
|
|
964
1040
|
}
|
|
965
1041
|
const stored = await this.stateStore.get(params.state);
|
|
966
1042
|
if (!stored) {
|
|
967
|
-
throw new IQAuthError("
|
|
1043
|
+
throw new IQAuthError("config_invalid", "Unknown or expired OIDC state");
|
|
968
1044
|
}
|
|
969
1045
|
let tokens;
|
|
970
1046
|
try {
|
|
@@ -982,7 +1058,7 @@ var OidcModule = class {
|
|
|
982
1058
|
if (tokens.id_token) {
|
|
983
1059
|
if (!this.tokensModule) {
|
|
984
1060
|
throw new IQAuthError(
|
|
985
|
-
"
|
|
1061
|
+
"config_invalid",
|
|
986
1062
|
"OIDC handleCallback received an id_token but no TokensModule is configured for verification"
|
|
987
1063
|
);
|
|
988
1064
|
}
|
|
@@ -993,7 +1069,7 @@ var OidcModule = class {
|
|
|
993
1069
|
const tokenNonce = typeof claimsBag.nonce === "string" ? claimsBag.nonce : void 0;
|
|
994
1070
|
if (!tokenNonce || tokenNonce !== stored.nonce) {
|
|
995
1071
|
throw new IQAuthError(
|
|
996
|
-
"
|
|
1072
|
+
"token_invalid",
|
|
997
1073
|
"OIDC id_token nonce did not match the stored value"
|
|
998
1074
|
);
|
|
999
1075
|
}
|
|
@@ -1194,6 +1270,9 @@ var AppsModule = class {
|
|
|
1194
1270
|
* @remarks Wraps GET /api/v1/apps/:appKey
|
|
1195
1271
|
*/
|
|
1196
1272
|
async get(appKey) {
|
|
1273
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1274
|
+
throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
|
|
1275
|
+
}
|
|
1197
1276
|
return this.http.request("GET", `/api/v1/apps/${encodeURIComponent(appKey)}`);
|
|
1198
1277
|
}
|
|
1199
1278
|
/**
|
|
@@ -1213,6 +1292,16 @@ var AppsModule = class {
|
|
|
1213
1292
|
401
|
|
1214
1293
|
);
|
|
1215
1294
|
}
|
|
1295
|
+
if (!manifest || typeof manifest.key !== "string" || manifest.key.trim() === "") {
|
|
1296
|
+
throw new IQAuthError("VALIDATION_ERROR", "manifest.key (appKey) is required for register().", 400);
|
|
1297
|
+
}
|
|
1298
|
+
if (!manifest.environment || !["production", "staging", "development"].includes(manifest.environment)) {
|
|
1299
|
+
throw new IQAuthError(
|
|
1300
|
+
"ENVIRONMENT_REQUIRED",
|
|
1301
|
+
"manifest.environment is required and must be 'production', 'staging', or 'development'. This guards against a dev workstation silently overwriting a production app's permission tree.",
|
|
1302
|
+
400
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1216
1305
|
return this.http.request("POST", "/api/v1/apps/sync", manifest);
|
|
1217
1306
|
}
|
|
1218
1307
|
/**
|
|
@@ -1222,11 +1311,14 @@ var AppsModule = class {
|
|
|
1222
1311
|
* @remarks Uses GET /api/v1/apps/:appKey — catches 404 errors
|
|
1223
1312
|
*/
|
|
1224
1313
|
async isRegistered(appKey) {
|
|
1314
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1315
|
+
throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
|
|
1316
|
+
}
|
|
1225
1317
|
try {
|
|
1226
1318
|
await this.get(appKey);
|
|
1227
1319
|
return true;
|
|
1228
1320
|
} catch (err) {
|
|
1229
|
-
if (err.code === "NOT_FOUND" || err.status === 404) {
|
|
1321
|
+
if (err.code === "app_not_found" || err.code === "NOT_FOUND" || err.status === 404) {
|
|
1230
1322
|
return false;
|
|
1231
1323
|
}
|
|
1232
1324
|
throw err;
|
|
@@ -1263,6 +1355,20 @@ var RolesModule = class {
|
|
|
1263
1355
|
};
|
|
1264
1356
|
|
|
1265
1357
|
// src/modules/permissionGroups.ts
|
|
1358
|
+
function assertAppKey(appKey, callsite) {
|
|
1359
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1360
|
+
throw new IQAuthError(
|
|
1361
|
+
"VALIDATION_ERROR",
|
|
1362
|
+
`appKey is required for ${callsite} (no env-var fallback, no 'product' alias).`,
|
|
1363
|
+
400
|
|
1364
|
+
);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
function assertNodeKey(nodeKey, callsite) {
|
|
1368
|
+
if (typeof nodeKey !== "string" || nodeKey.trim() === "") {
|
|
1369
|
+
throw new IQAuthError("VALIDATION_ERROR", `nodeKey is required for ${callsite}.`, 400);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1266
1372
|
var PermissionGroupsModule = class {
|
|
1267
1373
|
constructor(http) {
|
|
1268
1374
|
this.http = http;
|
|
@@ -1283,7 +1389,14 @@ var PermissionGroupsModule = class {
|
|
|
1283
1389
|
return this.http.request("GET", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`);
|
|
1284
1390
|
}
|
|
1285
1391
|
async addPermission(tenantId, groupId, data) {
|
|
1286
|
-
|
|
1392
|
+
assertAppKey(data?.appKey, "permissionGroups.addPermission");
|
|
1393
|
+
assertNodeKey(data?.nodeKey, "permissionGroups.addPermission");
|
|
1394
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`, {
|
|
1395
|
+
appKey: data.appKey,
|
|
1396
|
+
nodeKey: data.nodeKey,
|
|
1397
|
+
effect: data.effect,
|
|
1398
|
+
weight: data.weight
|
|
1399
|
+
});
|
|
1287
1400
|
}
|
|
1288
1401
|
async removePermission(tenantId, groupId, permissionId) {
|
|
1289
1402
|
return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions/${permissionId}`);
|
|
@@ -1307,21 +1420,51 @@ var PermissionGroupsModule = class {
|
|
|
1307
1420
|
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`);
|
|
1308
1421
|
}
|
|
1309
1422
|
async addUserOverride(tenantId, userId, data) {
|
|
1310
|
-
|
|
1423
|
+
assertAppKey(data?.appKey, "permissionGroups.addUserOverride");
|
|
1424
|
+
assertNodeKey(data?.nodeKey, "permissionGroups.addUserOverride");
|
|
1425
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`, {
|
|
1426
|
+
appKey: data.appKey,
|
|
1427
|
+
nodeKey: data.nodeKey,
|
|
1428
|
+
effect: data.effect,
|
|
1429
|
+
weight: data.weight,
|
|
1430
|
+
expiresAt: data.expiresAt
|
|
1431
|
+
});
|
|
1311
1432
|
}
|
|
1312
1433
|
async removeUserOverride(tenantId, userId, overrideId) {
|
|
1313
1434
|
return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides/${overrideId}`);
|
|
1314
1435
|
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Task #130 — `appKey` is REQUIRED. The legacy `product` query alias is no
|
|
1438
|
+
* longer accepted at the SDK boundary; pass it as `appKey` instead. The
|
|
1439
|
+
* server still accepts `product=` from raw HTTP callers during the
|
|
1440
|
+
* deprecation window, but the SDK will not silently translate it.
|
|
1441
|
+
*/
|
|
1315
1442
|
async getEffectivePermissions(tenantId, userId, params) {
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
const qs = query.toString();
|
|
1320
|
-
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective${qs ? `?${qs}` : ""}`);
|
|
1443
|
+
assertAppKey(params?.appKey, "permissionGroups.getEffectivePermissions");
|
|
1444
|
+
const qs = new URLSearchParams({ appKey: params.appKey }).toString();
|
|
1445
|
+
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective?${qs}`);
|
|
1321
1446
|
}
|
|
1322
1447
|
async checkPermission(tenantId, userId, appKey, nodeKey) {
|
|
1448
|
+
assertAppKey(appKey, "permissionGroups.checkPermission");
|
|
1449
|
+
assertNodeKey(nodeKey, "permissionGroups.checkPermission");
|
|
1323
1450
|
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/check`, { appKey, nodeKey });
|
|
1324
1451
|
}
|
|
1452
|
+
/**
|
|
1453
|
+
* Task #130 — every entry in `checks` must include a non-empty `appKey`
|
|
1454
|
+
* AND `nodeKey`. The SDK validates the whole batch before sending so a
|
|
1455
|
+
* single misconfigured entry can't slip through and silently report
|
|
1456
|
+
* `allowed: false` from the server's per-entry validation branch.
|
|
1457
|
+
*/
|
|
1458
|
+
async batchCheckPermissions(tenantId, userId, checks) {
|
|
1459
|
+
if (!Array.isArray(checks) || checks.length === 0) {
|
|
1460
|
+
throw new IQAuthError("VALIDATION_ERROR", "checks must be a non-empty array of { appKey, nodeKey }.", 400);
|
|
1461
|
+
}
|
|
1462
|
+
checks.forEach((c, i) => {
|
|
1463
|
+
assertAppKey(c?.appKey, `permissionGroups.batchCheckPermissions[${i}]`);
|
|
1464
|
+
assertNodeKey(c?.nodeKey, `permissionGroups.batchCheckPermissions[${i}]`);
|
|
1465
|
+
});
|
|
1466
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/batch-check`, { checks });
|
|
1467
|
+
}
|
|
1325
1468
|
};
|
|
1326
1469
|
|
|
1327
1470
|
// src/modules/apiKeys.ts
|
|
@@ -1746,6 +1889,10 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1746
1889
|
this._refreshToken = tokens.refreshToken;
|
|
1747
1890
|
},
|
|
1748
1891
|
autoRefresh: "autoRefresh" in config ? config.autoRefresh !== false : true,
|
|
1892
|
+
// `'app-state'` is mobile-only — on any other environment we treat it
|
|
1893
|
+
// as the default `true` (proactive refresh ON). Only the mobile client
|
|
1894
|
+
// disables proactive refresh and replaces it with an AppState listener.
|
|
1895
|
+
proactiveRefresh: "autoRefresh" in config && config.autoRefresh === "app-state" && _IQAuthClient.resolveEnvironment(config) === "mobile" ? false : true,
|
|
1749
1896
|
onTokenRefresh: "onTokenRefresh" in config ? config.onTokenRefresh : void 0,
|
|
1750
1897
|
sessionHeaderName: config.sessionHeaderName,
|
|
1751
1898
|
sessionHeaderValue: config.sessionHeaderValue,
|
|
@@ -1786,6 +1933,13 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1786
1933
|
static forServer(config) {
|
|
1787
1934
|
return new _IQAuthClient({ ...config, environment: "server" });
|
|
1788
1935
|
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Construct a mobile-environment client. NOTE: this constructor does NOT
|
|
1938
|
+
* subscribe to React Native's `AppState` even when `autoRefresh: 'app-state'`
|
|
1939
|
+
* is passed — it only disables the per-request proactive refresh. Use
|
|
1940
|
+
* `createMobileClient` from `@iqauth/sdk/mobile` if you want the full
|
|
1941
|
+
* AppState-driven refresh behavior (recommended for Expo / React Native).
|
|
1942
|
+
*/
|
|
1789
1943
|
static forMobile(config) {
|
|
1790
1944
|
return new _IQAuthClient({ ...config, environment: "mobile" });
|
|
1791
1945
|
}
|
|
@@ -1802,6 +1956,18 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1802
1956
|
getRefreshToken() {
|
|
1803
1957
|
return this._refreshToken;
|
|
1804
1958
|
}
|
|
1959
|
+
/**
|
|
1960
|
+
* Task #126: Eagerly fetch JWKS + OIDC discovery so the first verify() /
|
|
1961
|
+
* refresh round-trip on the request hot path doesn't pay the discovery
|
|
1962
|
+
* fetch latency. Safe to call repeatedly. Errors are swallowed; callers
|
|
1963
|
+
* may fire-and-forget. Called automatically by `iqAuth({...}).attachHelpers()`.
|
|
1964
|
+
*/
|
|
1965
|
+
async prewarm() {
|
|
1966
|
+
await Promise.all([
|
|
1967
|
+
this.tokens.prewarm(),
|
|
1968
|
+
this.oidc.getDiscovery().catch(() => void 0)
|
|
1969
|
+
]);
|
|
1970
|
+
}
|
|
1805
1971
|
getCurrentClaims() {
|
|
1806
1972
|
if (!this._accessToken) return null;
|
|
1807
1973
|
return this.tokens.decode(this._accessToken);
|
|
@@ -1842,14 +2008,14 @@ function assertPublishableKey(raw, opts) {
|
|
|
1842
2008
|
const ctx = opts?.context ? `${opts.context}: ` : "";
|
|
1843
2009
|
if (typeof raw !== "string" || raw.length === 0) {
|
|
1844
2010
|
throw new IQAuthError(
|
|
1845
|
-
"
|
|
2011
|
+
"config_invalid",
|
|
1846
2012
|
`${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
2013
|
);
|
|
1848
2014
|
}
|
|
1849
2015
|
const shapeMatch = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
|
|
1850
2016
|
if (!shapeMatch) {
|
|
1851
2017
|
throw new IQAuthError(
|
|
1852
|
-
"
|
|
2018
|
+
"config_invalid",
|
|
1853
2019
|
`${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
2020
|
);
|
|
1855
2021
|
}
|
|
@@ -1858,19 +2024,19 @@ function assertPublishableKey(raw, opts) {
|
|
|
1858
2024
|
decoded = JSON.parse(b64urlDecode(shapeMatch[2]));
|
|
1859
2025
|
} catch {
|
|
1860
2026
|
throw new IQAuthError(
|
|
1861
|
-
"
|
|
2027
|
+
"config_invalid",
|
|
1862
2028
|
`${ctx}IQAuth publishable key payload is not valid base64url JSON. Regenerate the key from the IQAuth admin console.`
|
|
1863
2029
|
);
|
|
1864
2030
|
}
|
|
1865
2031
|
if (!isPublishableKeyPayload(decoded)) {
|
|
1866
2032
|
throw new IQAuthError(
|
|
1867
|
-
"
|
|
2033
|
+
"config_invalid",
|
|
1868
2034
|
`${ctx}IQAuth publishable key payload is missing required fields {iss, appId, tenantId, kid}. Regenerate the key from the IQAuth admin console.`
|
|
1869
2035
|
);
|
|
1870
2036
|
}
|
|
1871
2037
|
if (!isValidIssuerUrl(decoded.iss)) {
|
|
1872
2038
|
throw new IQAuthError(
|
|
1873
|
-
"
|
|
2039
|
+
"config_invalid",
|
|
1874
2040
|
`${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
2041
|
);
|
|
1876
2042
|
}
|
|
@@ -1884,12 +2050,18 @@ function isPublishableKeyPayload(value) {
|
|
|
1884
2050
|
|
|
1885
2051
|
// src/middleware/express.ts
|
|
1886
2052
|
var KNOWN_AUTH_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
2053
|
+
// Legacy UPPER_SNAKE codes (server-originated and SDK ≤2.6.x throws).
|
|
1887
2054
|
"TOKEN_INVALID",
|
|
1888
2055
|
"TOKEN_EXPIRED",
|
|
1889
2056
|
"TOKEN_REVOKED",
|
|
1890
2057
|
"SESSION_EXPIRED",
|
|
1891
2058
|
"SESSION_INVALID",
|
|
1892
|
-
"AUTH_REQUIRED"
|
|
2059
|
+
"AUTH_REQUIRED",
|
|
2060
|
+
// Task #127 — typed `IQAuthErrorCode` taxonomy thrown by `tokens.verify`.
|
|
2061
|
+
// Mapped to 401 here so framework consumers don't have to learn the new
|
|
2062
|
+
// codes to keep their auth-failure handling working.
|
|
2063
|
+
"token_invalid",
|
|
2064
|
+
"token_expired"
|
|
1893
2065
|
]);
|
|
1894
2066
|
var DEFAULT_ACCESS_COOKIE = "iqauth_at";
|
|
1895
2067
|
var DEFAULT_REFRESH_COOKIE = "iqauth_rt";
|
|
@@ -2073,6 +2245,45 @@ function iqAuthMiddleware(clientOrOptions, options = {}) {
|
|
|
2073
2245
|
}
|
|
2074
2246
|
|
|
2075
2247
|
// src/server/handlers.ts
|
|
2248
|
+
async function buildUserinfoResponse(claims, opts = {}) {
|
|
2249
|
+
const baseUser = {
|
|
2250
|
+
sub: claims.sub,
|
|
2251
|
+
email: claims.email,
|
|
2252
|
+
name: claims.name,
|
|
2253
|
+
tenantId: claims.tenantId,
|
|
2254
|
+
vendorId: claims.vendorId,
|
|
2255
|
+
roles: claims.roles ?? [],
|
|
2256
|
+
entitlements: claims.entitlements ?? [],
|
|
2257
|
+
// Task #171 — project the active source/client scope onto the userinfo
|
|
2258
|
+
// payload so server handlers (`getSessionUser`, `/api/iqauth/userinfo`)
|
|
2259
|
+
// expose it without consumers having to re-decode the JWT.
|
|
2260
|
+
...claims.scopeContext !== void 0 ? { scopeContext: claims.scopeContext } : {}
|
|
2261
|
+
};
|
|
2262
|
+
const enriched = opts.enrich ? await opts.enrich(claims) : null;
|
|
2263
|
+
const user = enriched ? { ...baseUser, ...enriched } : baseUser;
|
|
2264
|
+
return {
|
|
2265
|
+
success: true,
|
|
2266
|
+
data: {
|
|
2267
|
+
user,
|
|
2268
|
+
claims,
|
|
2269
|
+
tenantId: claims.tenantId ?? null
|
|
2270
|
+
}
|
|
2271
|
+
};
|
|
2272
|
+
}
|
|
2273
|
+
function emitTiming(cfg, event) {
|
|
2274
|
+
if (cfg.debug) {
|
|
2275
|
+
try {
|
|
2276
|
+
console.debug("[iqauth_helper]", event);
|
|
2277
|
+
} catch {
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
if (cfg.onTimingEvent) {
|
|
2281
|
+
try {
|
|
2282
|
+
cfg.onTimingEvent(event);
|
|
2283
|
+
} catch {
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2076
2287
|
var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
2077
2288
|
"TOKEN_REVOKED",
|
|
2078
2289
|
"SESSION_REVOKED",
|
|
@@ -2091,19 +2302,62 @@ function shouldClearCookiesOnFailure(policy, status, errorCode) {
|
|
|
2091
2302
|
}
|
|
2092
2303
|
var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
|
|
2093
2304
|
var REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
|
|
2305
|
+
function assertCookiePrefixInvariants(name, secure, path, domain) {
|
|
2306
|
+
if (name.startsWith("__Host-")) {
|
|
2307
|
+
if (!secure) {
|
|
2308
|
+
throw new IQAuthError(
|
|
2309
|
+
"config_invalid",
|
|
2310
|
+
`Cookie "${name}" uses the __Host- prefix, which browsers only accept on a Secure cookie. Set secure:true (and serve over HTTPS).`
|
|
2311
|
+
);
|
|
2312
|
+
}
|
|
2313
|
+
if (path !== "/") {
|
|
2314
|
+
throw new IQAuthError(
|
|
2315
|
+
"config_invalid",
|
|
2316
|
+
`Cookie "${name}" uses the __Host- prefix, which requires Path=/ (got "${path}"). Remove cookiePath or set it to "/".`
|
|
2317
|
+
);
|
|
2318
|
+
}
|
|
2319
|
+
if (domain) {
|
|
2320
|
+
throw new IQAuthError(
|
|
2321
|
+
"config_invalid",
|
|
2322
|
+
`Cookie "${name}" uses the __Host- prefix, which forbids a Domain attribute (the cookie is host-locked). Remove cookieDomain.`
|
|
2323
|
+
);
|
|
2324
|
+
}
|
|
2325
|
+
} else if (name.startsWith("__Secure-") && !secure) {
|
|
2326
|
+
throw new IQAuthError(
|
|
2327
|
+
"config_invalid",
|
|
2328
|
+
`Cookie "${name}" uses the __Secure- prefix, which browsers only accept on a Secure cookie. Set secure:true (and serve over HTTPS).`
|
|
2329
|
+
);
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2094
2332
|
function resolve(config) {
|
|
2095
2333
|
const parsed = assertPublishableKey(config.publishableKey, { context: "@iqauth/sdk helpers" });
|
|
2096
2334
|
const inferredIssuer = parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`;
|
|
2335
|
+
maybeWarnDefaultSignoutRegistry(config);
|
|
2336
|
+
const secure = config.secure ?? true;
|
|
2337
|
+
if (config.secure === false && config.allowInsecureCookies !== true) {
|
|
2338
|
+
throw new IQAuthError(
|
|
2339
|
+
"config_invalid",
|
|
2340
|
+
"Refusing to issue auth cookies with secure:false \u2014 this exposes session cookies over plaintext HTTP. For local HTTP development, set allowInsecureCookies:true to acknowledge the risk. Production MUST use HTTPS with secure cookies."
|
|
2341
|
+
);
|
|
2342
|
+
}
|
|
2343
|
+
const accessCookieName = config.accessCookieName ?? config.cookieNames?.access ?? "iqauth_at";
|
|
2344
|
+
const refreshCookieName = config.refreshCookieName ?? config.cookieNames?.refresh ?? "iqauth_rt";
|
|
2345
|
+
const stateCookieName = config.stateCookieName ?? "iqauth_state";
|
|
2346
|
+
const cookiePath = config.cookiePath ?? "/";
|
|
2347
|
+
const cookieDomain = config.cookieDomain;
|
|
2348
|
+
for (const name of [accessCookieName, refreshCookieName, stateCookieName]) {
|
|
2349
|
+
assertCookiePrefixInvariants(name, secure, cookiePath, cookieDomain);
|
|
2350
|
+
}
|
|
2097
2351
|
return {
|
|
2098
2352
|
publishableKey: config.publishableKey,
|
|
2099
2353
|
secretKey: config.secretKey,
|
|
2100
2354
|
issuer: (config.issuer ?? inferredIssuer).replace(/\/+$/, ""),
|
|
2101
|
-
accessCookieName
|
|
2102
|
-
refreshCookieName
|
|
2103
|
-
cookieDomain
|
|
2355
|
+
accessCookieName,
|
|
2356
|
+
refreshCookieName,
|
|
2357
|
+
cookieDomain,
|
|
2104
2358
|
sameSite: config.sameSite ?? "lax",
|
|
2105
|
-
secure
|
|
2106
|
-
cookiePath
|
|
2359
|
+
secure,
|
|
2360
|
+
cookiePath,
|
|
2107
2361
|
tokenPath: config.tokenPath ?? "/oidc/token",
|
|
2108
2362
|
refreshPath: config.refreshPath ?? "/api/v1/auth/refresh",
|
|
2109
2363
|
logoutPath: config.logoutPath ?? "/api/v1/auth/logout",
|
|
@@ -2112,9 +2366,23 @@ function resolve(config) {
|
|
|
2112
2366
|
})),
|
|
2113
2367
|
appId: parsed.appId,
|
|
2114
2368
|
tenantId: parsed.tenantId,
|
|
2115
|
-
clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only"
|
|
2369
|
+
clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only",
|
|
2370
|
+
debug: config.debug,
|
|
2371
|
+
onTimingEvent: config.onTimingEvent,
|
|
2372
|
+
signoutRegistry: config.signoutRegistry ?? defaultSignoutRegistry,
|
|
2373
|
+
signoutMarkerTtlMs: config.signoutMarkerTtlMs ?? DEFAULT_SIGNOUT_TTL_MS,
|
|
2374
|
+
requireOAuthState: config.requireOAuthState ?? true,
|
|
2375
|
+
stateCookieName: config.stateCookieName ?? "iqauth_state"
|
|
2116
2376
|
};
|
|
2117
2377
|
}
|
|
2378
|
+
function timingSafeEqualStr(a, b) {
|
|
2379
|
+
const len = Math.max(a.length, b.length);
|
|
2380
|
+
let diff = a.length ^ b.length;
|
|
2381
|
+
for (let i = 0; i < len; i++) {
|
|
2382
|
+
diff |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0);
|
|
2383
|
+
}
|
|
2384
|
+
return diff === 0;
|
|
2385
|
+
}
|
|
2118
2386
|
function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
|
|
2119
2387
|
return {
|
|
2120
2388
|
name,
|
|
@@ -2129,20 +2397,77 @@ function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
|
|
|
2129
2397
|
}
|
|
2130
2398
|
function clearCookies(cfg) {
|
|
2131
2399
|
return [
|
|
2132
|
-
makeCookie(cfg, cfg.accessCookieName, "", 0),
|
|
2133
|
-
makeCookie(cfg, cfg.refreshCookieName, "", 0)
|
|
2400
|
+
{ ...makeCookie(cfg, cfg.accessCookieName, "", 0), clear: true },
|
|
2401
|
+
{ ...makeCookie(cfg, cfg.refreshCookieName, "", 0), clear: true }
|
|
2134
2402
|
];
|
|
2135
2403
|
}
|
|
2404
|
+
function clearStateCookie(cfg) {
|
|
2405
|
+
return { ...makeCookie(cfg, cfg.stateCookieName, "", 0, false), clear: true };
|
|
2406
|
+
}
|
|
2407
|
+
var DEFAULT_SIGNOUT_TTL_MS = 6e4;
|
|
2408
|
+
var inMemorySignoutMarkers = /* @__PURE__ */ new Map();
|
|
2409
|
+
function pruneInMemoryMarkers(now) {
|
|
2410
|
+
if (inMemorySignoutMarkers.size === 0) return;
|
|
2411
|
+
for (const [k, exp] of inMemorySignoutMarkers) {
|
|
2412
|
+
if (exp <= now) inMemorySignoutMarkers.delete(k);
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
var defaultSignoutRegistry = {
|
|
2416
|
+
mark(token, ttlMs) {
|
|
2417
|
+
const now = Date.now();
|
|
2418
|
+
pruneInMemoryMarkers(now);
|
|
2419
|
+
inMemorySignoutMarkers.set(token, now + ttlMs);
|
|
2420
|
+
},
|
|
2421
|
+
has(token) {
|
|
2422
|
+
const now = Date.now();
|
|
2423
|
+
const exp = inMemorySignoutMarkers.get(token);
|
|
2424
|
+
if (!exp) return false;
|
|
2425
|
+
if (exp <= now) {
|
|
2426
|
+
inMemorySignoutMarkers.delete(token);
|
|
2427
|
+
return false;
|
|
2428
|
+
}
|
|
2429
|
+
return true;
|
|
2430
|
+
}
|
|
2431
|
+
};
|
|
2432
|
+
var warnedDefaultSignoutRegistry = false;
|
|
2433
|
+
function maybeWarnDefaultSignoutRegistry(config) {
|
|
2434
|
+
if (warnedDefaultSignoutRegistry) return;
|
|
2435
|
+
if (config.signoutRegistry) return;
|
|
2436
|
+
warnedDefaultSignoutRegistry = true;
|
|
2437
|
+
console.warn(
|
|
2438
|
+
"[IQAuth] Using the in-memory signout registry (process-local). Signout idempotency is NOT shared across instances \u2014 in a multi-replica deployment a /refresh racing a /signout on another replica can reissue cookies after sign-out. Plug a shared backend (e.g. Redis) into IQAuthHelperConfig.signoutRegistry to fix this and silence this warning."
|
|
2439
|
+
);
|
|
2440
|
+
}
|
|
2136
2441
|
async function handleCallback(config, input) {
|
|
2137
2442
|
const cfg = resolve(config);
|
|
2443
|
+
const t0 = Date.now();
|
|
2138
2444
|
if (!input.code || !input.redirectUri) {
|
|
2445
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "VALIDATION_ERROR" });
|
|
2139
2446
|
return {
|
|
2140
2447
|
status: 400,
|
|
2141
2448
|
body: { success: false, error: { code: "VALIDATION_ERROR", message: "code and redirectUri are required" } },
|
|
2142
2449
|
cookies: []
|
|
2143
2450
|
};
|
|
2144
2451
|
}
|
|
2452
|
+
const provided = input.state;
|
|
2453
|
+
const expected = input.expectedState;
|
|
2454
|
+
const stateOk = cfg.requireOAuthState ? !!expected && !!provided && timingSafeEqualStr(provided, expected) : !expected || !!provided && timingSafeEqualStr(provided, expected);
|
|
2455
|
+
if (!stateOk) {
|
|
2456
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "STATE_MISMATCH" });
|
|
2457
|
+
return {
|
|
2458
|
+
status: 400,
|
|
2459
|
+
body: {
|
|
2460
|
+
success: false,
|
|
2461
|
+
error: {
|
|
2462
|
+
code: "STATE_MISMATCH",
|
|
2463
|
+
message: "OAuth state validation failed; the sign-in could not be verified as originating from this browser."
|
|
2464
|
+
}
|
|
2465
|
+
},
|
|
2466
|
+
cookies: [clearStateCookie(cfg)]
|
|
2467
|
+
};
|
|
2468
|
+
}
|
|
2145
2469
|
if (!cfg.secretKey) {
|
|
2470
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "INTERNAL_ERROR" });
|
|
2146
2471
|
return {
|
|
2147
2472
|
status: 500,
|
|
2148
2473
|
body: { success: false, error: { code: "INTERNAL_ERROR", message: "secretKey is required for the callback handler" } },
|
|
@@ -2166,6 +2491,7 @@ async function handleCallback(config, input) {
|
|
|
2166
2491
|
});
|
|
2167
2492
|
const json = await res.json().catch(() => ({}));
|
|
2168
2493
|
if (!res.ok || !json.access_token) {
|
|
2494
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: json.error || "OIDC_EXCHANGE_FAILED" });
|
|
2169
2495
|
return {
|
|
2170
2496
|
status: res.status || 502,
|
|
2171
2497
|
body: {
|
|
@@ -2178,6 +2504,26 @@ async function handleCallback(config, input) {
|
|
|
2178
2504
|
cookies: []
|
|
2179
2505
|
};
|
|
2180
2506
|
}
|
|
2507
|
+
try {
|
|
2508
|
+
await getTokensFor(cfg.issuer).verify(json.access_token, {
|
|
2509
|
+
issuer: cfg.issuer,
|
|
2510
|
+
...config.verify
|
|
2511
|
+
});
|
|
2512
|
+
} catch (err) {
|
|
2513
|
+
const code = err instanceof IQAuthError ? err.code : err.code || "TOKEN_INVALID";
|
|
2514
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code });
|
|
2515
|
+
return {
|
|
2516
|
+
status: 502,
|
|
2517
|
+
body: {
|
|
2518
|
+
success: false,
|
|
2519
|
+
error: {
|
|
2520
|
+
code: "ACCESS_TOKEN_VERIFICATION_FAILED",
|
|
2521
|
+
message: "The issuer returned an access token that failed verification; no session was established."
|
|
2522
|
+
}
|
|
2523
|
+
},
|
|
2524
|
+
cookies: []
|
|
2525
|
+
};
|
|
2526
|
+
}
|
|
2181
2527
|
const cookies = [];
|
|
2182
2528
|
cookies.push(
|
|
2183
2529
|
makeCookie(cfg, cfg.accessCookieName, json.access_token, json.expires_in ?? ACCESS_TOKEN_TTL_SECONDS)
|
|
@@ -2185,6 +2531,8 @@ async function handleCallback(config, input) {
|
|
|
2185
2531
|
if (json.refresh_token) {
|
|
2186
2532
|
cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.refresh_token, REFRESH_TOKEN_TTL_SECONDS));
|
|
2187
2533
|
}
|
|
2534
|
+
cookies.push(clearStateCookie(cfg));
|
|
2535
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: true });
|
|
2188
2536
|
return {
|
|
2189
2537
|
status: 200,
|
|
2190
2538
|
body: { success: true, data: { authenticated: true } },
|
|
@@ -2193,8 +2541,18 @@ async function handleCallback(config, input) {
|
|
|
2193
2541
|
}
|
|
2194
2542
|
async function handleRefresh(config, input) {
|
|
2195
2543
|
const cfg = resolve(config);
|
|
2544
|
+
const t0 = Date.now();
|
|
2196
2545
|
const refreshToken = input.refreshToken;
|
|
2546
|
+
const idemKey = input.idempotencyToken;
|
|
2547
|
+
if (idemKey && await Promise.resolve(cfg.signoutRegistry.has(idemKey))) {
|
|
2548
|
+
return {
|
|
2549
|
+
status: 401,
|
|
2550
|
+
body: { success: false, error: { code: "SESSION_REVOKED", message: "Session was signed out" } },
|
|
2551
|
+
cookies: clearCookies(cfg)
|
|
2552
|
+
};
|
|
2553
|
+
}
|
|
2197
2554
|
if (!refreshToken) {
|
|
2555
|
+
emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: false, code: "TOKEN_INVALID" });
|
|
2198
2556
|
return {
|
|
2199
2557
|
status: 401,
|
|
2200
2558
|
body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing refresh token" } },
|
|
@@ -2210,6 +2568,7 @@ async function handleRefresh(config, input) {
|
|
|
2210
2568
|
if (!res.ok || !json.success || !json.data?.accessToken) {
|
|
2211
2569
|
const status = res.status || 401;
|
|
2212
2570
|
const errorCode = json.error?.code || "TOKEN_INVALID";
|
|
2571
|
+
emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: false, code: errorCode });
|
|
2213
2572
|
const shouldClear = shouldClearCookiesOnFailure(
|
|
2214
2573
|
cfg.clearCookiesOnRefreshFailure,
|
|
2215
2574
|
status,
|
|
@@ -2233,6 +2592,7 @@ async function handleRefresh(config, input) {
|
|
|
2233
2592
|
if (json.data.refreshToken) {
|
|
2234
2593
|
cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.data.refreshToken, REFRESH_TOKEN_TTL_SECONDS));
|
|
2235
2594
|
}
|
|
2595
|
+
emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: true });
|
|
2236
2596
|
return {
|
|
2237
2597
|
status: 200,
|
|
2238
2598
|
body: { success: true, data: { accessToken: json.data.accessToken } },
|
|
@@ -2241,6 +2601,10 @@ async function handleRefresh(config, input) {
|
|
|
2241
2601
|
}
|
|
2242
2602
|
async function handleSignout(config, input) {
|
|
2243
2603
|
const cfg = resolve(config);
|
|
2604
|
+
const t0 = Date.now();
|
|
2605
|
+
if (input.idempotencyToken) {
|
|
2606
|
+
await Promise.resolve(cfg.signoutRegistry.mark(input.idempotencyToken, cfg.signoutMarkerTtlMs));
|
|
2607
|
+
}
|
|
2244
2608
|
if (input.accessToken) {
|
|
2245
2609
|
try {
|
|
2246
2610
|
await cfg.fetchImpl(`${cfg.issuer}${cfg.logoutPath}`, {
|
|
@@ -2262,15 +2626,95 @@ async function handleSignout(config, input) {
|
|
|
2262
2626
|
} catch {
|
|
2263
2627
|
}
|
|
2264
2628
|
}
|
|
2629
|
+
emitTiming(cfg, { phase: "signout", durationMs: Date.now() - t0, ok: true });
|
|
2265
2630
|
return {
|
|
2266
2631
|
status: 200,
|
|
2267
2632
|
body: { success: true, data: { signedOut: true } },
|
|
2268
2633
|
cookies: clearCookies(cfg)
|
|
2269
2634
|
};
|
|
2270
2635
|
}
|
|
2636
|
+
var TOKENS_CACHE = /* @__PURE__ */ new Map();
|
|
2637
|
+
function getTokensFor(issuer) {
|
|
2638
|
+
let m = TOKENS_CACHE.get(issuer);
|
|
2639
|
+
if (!m) {
|
|
2640
|
+
m = new TokensModule(issuer);
|
|
2641
|
+
TOKENS_CACHE.set(issuer, m);
|
|
2642
|
+
}
|
|
2643
|
+
return m;
|
|
2644
|
+
}
|
|
2645
|
+
async function handleUserinfo(config, input) {
|
|
2646
|
+
const cfg = resolve(config);
|
|
2647
|
+
if (!input.accessToken) {
|
|
2648
|
+
return {
|
|
2649
|
+
status: 401,
|
|
2650
|
+
body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing access token" } },
|
|
2651
|
+
cookies: []
|
|
2652
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
let claims;
|
|
2655
|
+
try {
|
|
2656
|
+
claims = await getTokensFor(cfg.issuer).verify(input.accessToken, {
|
|
2657
|
+
issuer: cfg.issuer,
|
|
2658
|
+
...config.verify
|
|
2659
|
+
});
|
|
2660
|
+
} catch (err) {
|
|
2661
|
+
const code = err instanceof IQAuthError ? err.code : err.code || "TOKEN_INVALID";
|
|
2662
|
+
const message = err instanceof Error ? err.message : "Access token verification failed";
|
|
2663
|
+
return {
|
|
2664
|
+
status: 401,
|
|
2665
|
+
body: { success: false, error: { code, message } },
|
|
2666
|
+
cookies: []
|
|
2667
|
+
};
|
|
2668
|
+
}
|
|
2669
|
+
const envelope = await buildUserinfoResponse(claims, {
|
|
2670
|
+
enrich: config.userinfoEnricher ? (c) => config.userinfoEnricher(c, input.req) : void 0
|
|
2671
|
+
});
|
|
2672
|
+
return {
|
|
2673
|
+
status: 200,
|
|
2674
|
+
body: envelope,
|
|
2675
|
+
cookies: []
|
|
2676
|
+
};
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
// src/browser/returnTo.ts
|
|
2680
|
+
function normalizeOrigin(o) {
|
|
2681
|
+
try {
|
|
2682
|
+
return new URL(o).origin;
|
|
2683
|
+
} catch {
|
|
2684
|
+
return o.replace(/\/+$/, "");
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
function sanitizeReturnTo(input, options = {}) {
|
|
2688
|
+
const fallback = options.fallback ?? "/";
|
|
2689
|
+
if (!input || typeof input !== "string") return fallback;
|
|
2690
|
+
const trimmed = input.trim();
|
|
2691
|
+
if (!trimmed) return fallback;
|
|
2692
|
+
if (trimmed.includes("\\")) return fallback;
|
|
2693
|
+
if (trimmed.startsWith("//")) return fallback;
|
|
2694
|
+
if (trimmed.startsWith("/") || trimmed.startsWith("#") || trimmed.startsWith("?")) {
|
|
2695
|
+
return trimmed;
|
|
2696
|
+
}
|
|
2697
|
+
if (!/^[a-z][a-z0-9+\-.]*:/i.test(trimmed)) {
|
|
2698
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
2699
|
+
}
|
|
2700
|
+
let parsed;
|
|
2701
|
+
try {
|
|
2702
|
+
parsed = new URL(trimmed);
|
|
2703
|
+
} catch {
|
|
2704
|
+
return fallback;
|
|
2705
|
+
}
|
|
2706
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return fallback;
|
|
2707
|
+
const currentOrigin = options.currentOrigin ?? (typeof window !== "undefined" ? window.location.origin : "");
|
|
2708
|
+
const allowed = /* @__PURE__ */ new Set();
|
|
2709
|
+
if (currentOrigin) allowed.add(normalizeOrigin(currentOrigin));
|
|
2710
|
+
for (const o of options.allowedOrigins ?? []) allowed.add(normalizeOrigin(o));
|
|
2711
|
+
if (allowed.has(parsed.origin)) return parsed.toString();
|
|
2712
|
+
return fallback;
|
|
2713
|
+
}
|
|
2271
2714
|
|
|
2272
2715
|
// src/express.ts
|
|
2273
2716
|
var PKCE_COOKIE = "iqauth_pkce";
|
|
2717
|
+
var IDEMPOTENCY_HEADER = "x-iqauth-idempotency";
|
|
2274
2718
|
function escapeHtml(s) {
|
|
2275
2719
|
return s.replace(/[&<>"']/g, (c) => {
|
|
2276
2720
|
switch (c) {
|
|
@@ -2344,6 +2788,17 @@ function defaultBrandedSpinner(args) {
|
|
|
2344
2788
|
}
|
|
2345
2789
|
function applyHandlerResponse(res, hr) {
|
|
2346
2790
|
for (const c of hr.cookies) {
|
|
2791
|
+
if (c.clear && typeof res.clearCookie === "function") {
|
|
2792
|
+
const opts = {
|
|
2793
|
+
httpOnly: c.httpOnly,
|
|
2794
|
+
secure: c.secure,
|
|
2795
|
+
sameSite: c.sameSite,
|
|
2796
|
+
path: c.path
|
|
2797
|
+
};
|
|
2798
|
+
if (c.domain) opts.domain = c.domain;
|
|
2799
|
+
res.clearCookie(c.name, opts);
|
|
2800
|
+
continue;
|
|
2801
|
+
}
|
|
2347
2802
|
if (typeof res.cookie === "function") {
|
|
2348
2803
|
const opts = {
|
|
2349
2804
|
httpOnly: c.httpOnly,
|
|
@@ -2353,11 +2808,13 @@ function applyHandlerResponse(res, hr) {
|
|
|
2353
2808
|
maxAge: c.maxAge * 1e3
|
|
2354
2809
|
};
|
|
2355
2810
|
if (c.domain) opts.domain = c.domain;
|
|
2811
|
+
if (c.clear) opts.expires = /* @__PURE__ */ new Date(0);
|
|
2356
2812
|
res.cookie(c.name, c.value, opts);
|
|
2357
2813
|
} else {
|
|
2358
2814
|
const existing = res.getHeader?.("Set-Cookie") || [];
|
|
2359
2815
|
const list = Array.isArray(existing) ? existing : [existing];
|
|
2360
2816
|
const parts = [`${c.name}=${encodeURIComponent(c.value)}`, `Path=${c.path}`, `Max-Age=${c.maxAge}`, `SameSite=${c.sameSite}`];
|
|
2817
|
+
if (c.clear) parts.push("Expires=Thu, 01 Jan 1970 00:00:00 GMT");
|
|
2361
2818
|
if (c.secure) parts.push("Secure");
|
|
2362
2819
|
if (c.httpOnly) parts.push("HttpOnly");
|
|
2363
2820
|
if (c.domain) parts.push(`Domain=${c.domain}`);
|
|
@@ -2370,6 +2827,11 @@ function applyHandlerResponse(res, hr) {
|
|
|
2370
2827
|
function readBody(req) {
|
|
2371
2828
|
return req.body && typeof req.body === "object" ? req.body : {};
|
|
2372
2829
|
}
|
|
2830
|
+
function requestOriginOf(req) {
|
|
2831
|
+
const proto = req.headers?.["x-forwarded-proto"]?.split(",")[0]?.trim() || (typeof req.protocol === "string" ? req.protocol : void 0) || "https";
|
|
2832
|
+
const host = req.headers?.["x-forwarded-host"]?.split(",")[0]?.trim() || req.headers?.host || "";
|
|
2833
|
+
return host ? `${proto}://${host}` : "";
|
|
2834
|
+
}
|
|
2373
2835
|
function readCookieFromReq(req, name) {
|
|
2374
2836
|
if (req.cookies && typeof req.cookies[name] === "string") return req.cookies[name];
|
|
2375
2837
|
const header = req.headers?.cookie;
|
|
@@ -2408,12 +2870,19 @@ function iqAuth(options) {
|
|
|
2408
2870
|
const inline = options.inlineCallback === true ? {} : options.inlineCallback && typeof options.inlineCallback === "object" ? options.inlineCallback : null;
|
|
2409
2871
|
const inlineBranded = inline?.branded === true ? {} : inline?.branded && typeof inline.branded === "object" ? inline.branded : null;
|
|
2410
2872
|
const attachHelpers = (app) => {
|
|
2873
|
+
void client.prewarm();
|
|
2411
2874
|
app.post(`${mount}/callback`, async (req, res) => {
|
|
2412
2875
|
const body = readBody(req);
|
|
2413
2876
|
const hr = await handleCallback(helperConfig, {
|
|
2414
2877
|
code: body.code,
|
|
2415
2878
|
codeVerifier: body.codeVerifier,
|
|
2416
|
-
redirectUri: body.redirectUri
|
|
2879
|
+
redirectUri: body.redirectUri,
|
|
2880
|
+
// M-2: bind the callback to this browser. `state` is echoed back by the
|
|
2881
|
+
// OAuth redirect (body); `expectedState` is the value the SDK published
|
|
2882
|
+
// in a first-party cookie before redirect. handleCallback fails closed
|
|
2883
|
+
// on mismatch/missing when requireOAuthState (default) is on.
|
|
2884
|
+
state: body.state,
|
|
2885
|
+
expectedState: readCookieFromReq(req, helperConfig.stateCookieName ?? "iqauth_state")
|
|
2417
2886
|
});
|
|
2418
2887
|
applyHandlerResponse(res, hr);
|
|
2419
2888
|
});
|
|
@@ -2462,21 +2931,19 @@ function iqAuth(options) {
|
|
|
2462
2931
|
});
|
|
2463
2932
|
app.post(exchangePath, async (req, res) => {
|
|
2464
2933
|
const body = readBody(req);
|
|
2465
|
-
const stateFromBody = body.state || void 0;
|
|
2466
|
-
const stateFromCookie = readCookieFromReq(req, stateCookie);
|
|
2467
|
-
if (stateFromCookie && stateFromBody !== stateFromCookie) {
|
|
2468
|
-
clearCookie(res, stateCookie);
|
|
2469
|
-
res.status(400);
|
|
2470
|
-
return res.json ? res.json({ success: false, error: { code: "STATE_MISMATCH", message: "OAuth state mismatch" } }) : res.end?.(JSON.stringify({ success: false, error: { code: "STATE_MISMATCH", message: "OAuth state mismatch" } }));
|
|
2471
|
-
}
|
|
2472
2934
|
const hr = await handleCallback(helperConfig, {
|
|
2473
2935
|
code: body.code,
|
|
2474
2936
|
codeVerifier: body.codeVerifier || readCookieFromReq(req, PKCE_COOKIE) || "",
|
|
2475
|
-
redirectUri: body.redirectUri
|
|
2937
|
+
redirectUri: body.redirectUri,
|
|
2938
|
+
state: body.state,
|
|
2939
|
+
expectedState: readCookieFromReq(req, stateCookie)
|
|
2476
2940
|
});
|
|
2477
2941
|
clearCookie(res, stateCookie);
|
|
2478
2942
|
clearCookie(res, PKCE_COOKIE);
|
|
2479
|
-
const returnTo =
|
|
2943
|
+
const returnTo = sanitizeReturnTo(
|
|
2944
|
+
readCookieFromReq(req, returnToCookie) || hr.body?.returnTo,
|
|
2945
|
+
{ currentOrigin: requestOriginOf(req), fallback: "/" }
|
|
2946
|
+
);
|
|
2480
2947
|
if (hr.status < 400) clearCookie(res, returnToCookie);
|
|
2481
2948
|
const enriched = {
|
|
2482
2949
|
...hr,
|
|
@@ -2495,21 +2962,17 @@ function iqAuth(options) {
|
|
|
2495
2962
|
else res.end?.("Missing authorization code");
|
|
2496
2963
|
});
|
|
2497
2964
|
}
|
|
2498
|
-
const stateFromQuery = q.state;
|
|
2499
|
-
const stateFromCookie = readCookieFromReq(req, stateCookie);
|
|
2500
|
-
if (stateFromCookie && stateFromQuery !== stateFromCookie) {
|
|
2501
|
-
clearCookie(res, stateCookie);
|
|
2502
|
-
return failPlain(res, "state_mismatch", () => {
|
|
2503
|
-
res.status(400);
|
|
2504
|
-
if (res.json) res.json({ success: false, error: { code: "STATE_MISMATCH", message: "OAuth state mismatch" } });
|
|
2505
|
-
else res.end?.("OAuth state mismatch");
|
|
2506
|
-
});
|
|
2507
|
-
}
|
|
2508
2965
|
const codeVerifier = readCookieFromReq(req, PKCE_COOKIE) || "";
|
|
2509
2966
|
const proto = req.headers?.["x-forwarded-proto"] || req.protocol || "https";
|
|
2510
2967
|
const host = req.headers?.["x-forwarded-host"] || req.headers?.host || "";
|
|
2511
2968
|
const redirectUri = `${proto}://${host}${callbackPath}`;
|
|
2512
|
-
const hr = await handleCallback(helperConfig, {
|
|
2969
|
+
const hr = await handleCallback(helperConfig, {
|
|
2970
|
+
code,
|
|
2971
|
+
codeVerifier,
|
|
2972
|
+
redirectUri,
|
|
2973
|
+
state: q.state,
|
|
2974
|
+
expectedState: readCookieFromReq(req, stateCookie)
|
|
2975
|
+
});
|
|
2513
2976
|
for (const c of hr.cookies) {
|
|
2514
2977
|
if (typeof res.cookie === "function") {
|
|
2515
2978
|
const opts = {
|
|
@@ -2526,14 +2989,18 @@ function iqAuth(options) {
|
|
|
2526
2989
|
clearCookie(res, stateCookie);
|
|
2527
2990
|
clearCookie(res, PKCE_COOKIE);
|
|
2528
2991
|
if (hr.status >= 400) {
|
|
2529
|
-
const
|
|
2992
|
+
const rawCode = hr.body?.error?.code || "exchange_failed";
|
|
2993
|
+
const code2 = rawCode === "STATE_MISMATCH" ? "state_mismatch" : rawCode;
|
|
2530
2994
|
return failPlain(res, code2, () => {
|
|
2531
2995
|
res.status(hr.status);
|
|
2532
2996
|
if (res.json) res.json(hr.body);
|
|
2533
2997
|
else res.end?.(JSON.stringify(hr.body));
|
|
2534
2998
|
});
|
|
2535
2999
|
}
|
|
2536
|
-
const returnTo =
|
|
3000
|
+
const returnTo = sanitizeReturnTo(
|
|
3001
|
+
readCookieFromReq(req, returnToCookie) || hr.body?.returnTo,
|
|
3002
|
+
{ currentOrigin: requestOriginOf(req), fallback: "/" }
|
|
3003
|
+
);
|
|
2537
3004
|
clearCookie(res, returnToCookie);
|
|
2538
3005
|
if (typeof res.redirect === "function") return res.redirect(302, returnTo);
|
|
2539
3006
|
res.status(302);
|
|
@@ -2545,13 +3012,23 @@ function iqAuth(options) {
|
|
|
2545
3012
|
app.post(`${mount}/refresh`, async (req, res) => {
|
|
2546
3013
|
const body = readBody(req);
|
|
2547
3014
|
const refreshToken = body.refreshToken || readCookieFromReq(req, refreshCookie);
|
|
2548
|
-
const
|
|
3015
|
+
const idempotencyToken = req.headers?.[IDEMPOTENCY_HEADER] || body.idempotencyToken;
|
|
3016
|
+
const hr = await handleRefresh(helperConfig, { refreshToken, idempotencyToken });
|
|
2549
3017
|
applyHandlerResponse(res, hr);
|
|
2550
3018
|
});
|
|
3019
|
+
if (options.mountUserinfo && typeof app.get === "function") {
|
|
3020
|
+
app.get(`${mount}/me`, async (req, res) => {
|
|
3021
|
+
const accessToken = req.headers?.authorization?.replace(/^Bearer /i, "") || readCookieFromReq(req, accessCookie);
|
|
3022
|
+
const hr = await handleUserinfo(helperConfig, { accessToken, req });
|
|
3023
|
+
applyHandlerResponse(res, hr);
|
|
3024
|
+
});
|
|
3025
|
+
}
|
|
2551
3026
|
app.post(`${mount}/signout`, async (req, res) => {
|
|
2552
3027
|
const accessToken = req.headers?.authorization?.replace(/^Bearer /i, "") || readCookieFromReq(req, accessCookie);
|
|
3028
|
+
const refreshToken = readCookieFromReq(req, refreshCookie);
|
|
2553
3029
|
const ssoCookieHeader = req.headers?.cookie;
|
|
2554
|
-
const
|
|
3030
|
+
const idempotencyToken = req.headers?.[IDEMPOTENCY_HEADER];
|
|
3031
|
+
const hr = await handleSignout(helperConfig, { accessToken, refreshToken, idempotencyToken, ssoCookieHeader });
|
|
2555
3032
|
applyHandlerResponse(res, hr);
|
|
2556
3033
|
});
|
|
2557
3034
|
};
|
|
@@ -2559,6 +3036,7 @@ function iqAuth(options) {
|
|
|
2559
3036
|
composed.middleware = middleware;
|
|
2560
3037
|
composed.attachHelpers = attachHelpers;
|
|
2561
3038
|
composed.client = client;
|
|
3039
|
+
composed.prewarm = () => client.prewarm();
|
|
2562
3040
|
return composed;
|
|
2563
3041
|
}
|
|
2564
3042
|
// Annotate the CommonJS export names for ESM import in node:
|