@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/fastify.js
CHANGED
|
@@ -36,13 +36,30 @@ __export(fastify_exports, {
|
|
|
36
36
|
module.exports = __toCommonJS(fastify_exports);
|
|
37
37
|
|
|
38
38
|
// src/errors.ts
|
|
39
|
-
var IQAuthError = class extends Error {
|
|
40
|
-
constructor(code, message, status,
|
|
39
|
+
var IQAuthError = class _IQAuthError extends Error {
|
|
40
|
+
constructor(code, message, status, cause) {
|
|
41
41
|
super(message);
|
|
42
42
|
this.name = "IQAuthError";
|
|
43
43
|
this.code = code;
|
|
44
44
|
this.status = status;
|
|
45
|
-
this.
|
|
45
|
+
this.cause = cause;
|
|
46
|
+
this.raw = cause;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Type guard: true when `value` is an `IQAuthError`. Useful for adapters
|
|
50
|
+
* that round-trip errors through `unknown` (e.g. fastify's `setErrorHandler`).
|
|
51
|
+
*/
|
|
52
|
+
static isIQAuthError(value) {
|
|
53
|
+
return value instanceof _IQAuthError || typeof value === "object" && value !== null && value.name === "IQAuthError" && typeof value.code === "string";
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Type-narrowed code check. Lets callers write
|
|
57
|
+
* `if (err.is("token_expired")) …` with full IntelliSense for the typed
|
|
58
|
+
* taxonomy without losing the ability to handle server codes via
|
|
59
|
+
* `err.code === "TOKEN_REVOKED"`.
|
|
60
|
+
*/
|
|
61
|
+
is(code) {
|
|
62
|
+
return this.code === code;
|
|
46
63
|
}
|
|
47
64
|
};
|
|
48
65
|
|
|
@@ -157,7 +174,7 @@ var HttpClient = class {
|
|
|
157
174
|
headers: this.buildHeaders(),
|
|
158
175
|
...this.isBrowserSession() ? { credentials: "include" } : (() => {
|
|
159
176
|
const refreshToken = this.config.getRefreshToken();
|
|
160
|
-
if (!refreshToken) throw new IQAuthError("
|
|
177
|
+
if (!refreshToken) throw new IQAuthError("config_invalid", "No refresh token available");
|
|
161
178
|
return { body: JSON.stringify({ refreshToken }) };
|
|
162
179
|
})()
|
|
163
180
|
});
|
|
@@ -174,7 +191,7 @@ var HttpClient = class {
|
|
|
174
191
|
return;
|
|
175
192
|
}
|
|
176
193
|
if (!body.data.accessToken || !body.data.refreshToken) {
|
|
177
|
-
throw new IQAuthError("
|
|
194
|
+
throw new IQAuthError("token_invalid", "Refresh response did not include a token pair");
|
|
178
195
|
}
|
|
179
196
|
const tokens = {
|
|
180
197
|
accessToken: body.data.accessToken,
|
|
@@ -192,7 +209,7 @@ var HttpClient = class {
|
|
|
192
209
|
return this.requestWithRetry(method, path, body, options, false);
|
|
193
210
|
}
|
|
194
211
|
async requestWithRetry(method, path, body, options, hasRetried) {
|
|
195
|
-
if (this.config.autoRefresh && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
|
|
212
|
+
if (this.config.autoRefresh && this.config.proactiveRefresh !== false && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
|
|
196
213
|
await this.attemptRefresh();
|
|
197
214
|
}
|
|
198
215
|
const url = `${this.config.baseUrl}${path}`;
|
|
@@ -279,17 +296,27 @@ function parseLoginResponse(data, browserSessionMode) {
|
|
|
279
296
|
tenants: data.tenants
|
|
280
297
|
};
|
|
281
298
|
}
|
|
299
|
+
if (data.type === "scope_selection" && data.scopeSelectionToken && data.scopes && data.tenantId) {
|
|
300
|
+
return {
|
|
301
|
+
status: "scope_selection",
|
|
302
|
+
scopeSelectionToken: data.scopeSelectionToken,
|
|
303
|
+
tenantId: data.tenantId,
|
|
304
|
+
scopes: data.scopes
|
|
305
|
+
};
|
|
306
|
+
}
|
|
282
307
|
throw new Error("Unexpected login response shape");
|
|
283
308
|
}
|
|
284
309
|
var AuthModule = class {
|
|
285
310
|
constructor(http) {
|
|
286
311
|
this.http = http;
|
|
287
312
|
}
|
|
288
|
-
async login(email, password) {
|
|
313
|
+
async login(email, password, opts) {
|
|
314
|
+
const body = { email, password };
|
|
315
|
+
if (opts?.scopeHint) body.scopeHint = opts.scopeHint;
|
|
289
316
|
const data = await this.http.request(
|
|
290
317
|
"POST",
|
|
291
318
|
"/api/v1/auth/login",
|
|
292
|
-
|
|
319
|
+
body,
|
|
293
320
|
{ skipAutoRefresh: true }
|
|
294
321
|
);
|
|
295
322
|
return parseLoginResponse(data, this.http.isBrowserSession());
|
|
@@ -327,13 +354,29 @@ var AuthModule = class {
|
|
|
327
354
|
method
|
|
328
355
|
}, { skipAutoRefresh: true });
|
|
329
356
|
}
|
|
330
|
-
async selectTenant(tenantSelectionToken, tenantId) {
|
|
357
|
+
async selectTenant(tenantSelectionToken, tenantId, opts) {
|
|
358
|
+
const body = { tenantSelectionToken, tenantId };
|
|
359
|
+
if (opts?.scopeHint) body.scopeHint = opts.scopeHint;
|
|
331
360
|
const data = await this.http.request(
|
|
332
361
|
"POST",
|
|
333
362
|
"/api/v1/auth/select-tenant",
|
|
363
|
+
body,
|
|
364
|
+
{ skipAutoRefresh: true }
|
|
365
|
+
);
|
|
366
|
+
return parseLoginResponse(data, this.http.isBrowserSession());
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Task #171 — redeem a scope-selection token + chosen membership for a
|
|
370
|
+
* real authenticated session. `membershipId` must be one of the scopes
|
|
371
|
+
* returned in the prior `scope_selection` envelope.
|
|
372
|
+
*/
|
|
373
|
+
async selectScope(scopeSelectionToken, membershipId) {
|
|
374
|
+
const data = await this.http.request(
|
|
375
|
+
"POST",
|
|
376
|
+
"/api/v1/auth/select-scope",
|
|
334
377
|
{
|
|
335
|
-
|
|
336
|
-
|
|
378
|
+
scopeSelectionToken,
|
|
379
|
+
membershipId
|
|
337
380
|
},
|
|
338
381
|
{ skipAutoRefresh: true }
|
|
339
382
|
);
|
|
@@ -420,6 +463,18 @@ var DEFAULT_TOKEN_AUDIENCE = [
|
|
|
420
463
|
"iqvalidate"
|
|
421
464
|
];
|
|
422
465
|
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
466
|
+
function classifyJoseError(err) {
|
|
467
|
+
if (err instanceof import_jose.errors.JWTExpired) {
|
|
468
|
+
return { code: "token_expired", message: "Token has expired" };
|
|
469
|
+
}
|
|
470
|
+
if (err instanceof import_jose.errors.JOSEError) {
|
|
471
|
+
return { code: "token_invalid", message: err.message };
|
|
472
|
+
}
|
|
473
|
+
if (err instanceof Error) {
|
|
474
|
+
return { code: "token_invalid", message: err.message };
|
|
475
|
+
}
|
|
476
|
+
return { code: "token_invalid", message: "Token verification failed" };
|
|
477
|
+
}
|
|
423
478
|
function decodeProtectedHeader(token) {
|
|
424
479
|
const parts = token.split(".");
|
|
425
480
|
if (parts.length < 2) return null;
|
|
@@ -456,11 +511,11 @@ var TokensModule = class {
|
|
|
456
511
|
async verify(token, options = {}) {
|
|
457
512
|
const header = decodeProtectedHeader(token);
|
|
458
513
|
if (!header) {
|
|
459
|
-
throw new IQAuthError("
|
|
514
|
+
throw new IQAuthError("token_invalid", "Unable to decode token");
|
|
460
515
|
}
|
|
461
516
|
const kid = header.kid;
|
|
462
517
|
if (!kid) {
|
|
463
|
-
throw new IQAuthError("
|
|
518
|
+
throw new IQAuthError("token_invalid", "Token missing kid header");
|
|
464
519
|
}
|
|
465
520
|
let cache = await this.ensureCache();
|
|
466
521
|
if (!cache.byKid.has(kid)) {
|
|
@@ -468,7 +523,7 @@ var TokensModule = class {
|
|
|
468
523
|
cache = await this.ensureCache();
|
|
469
524
|
}
|
|
470
525
|
if (!cache.byKid.has(kid)) {
|
|
471
|
-
throw new IQAuthError("
|
|
526
|
+
throw new IQAuthError("token_invalid", `Unknown key ID: ${kid}`);
|
|
472
527
|
}
|
|
473
528
|
const issuer = options.issuer ?? this.defaultIssuer;
|
|
474
529
|
const audience = options.audience ?? this.defaultAudience;
|
|
@@ -484,16 +539,8 @@ var TokensModule = class {
|
|
|
484
539
|
const { payload } = await (0, import_jose.jwtVerify)(token, cache.verifier, verifyOptions);
|
|
485
540
|
return payload;
|
|
486
541
|
} catch (err) {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
}
|
|
490
|
-
if (err instanceof import_jose.errors.JOSEError) {
|
|
491
|
-
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
492
|
-
}
|
|
493
|
-
if (err instanceof Error) {
|
|
494
|
-
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
495
|
-
}
|
|
496
|
-
throw new IQAuthError("TOKEN_INVALID", "Token verification failed");
|
|
542
|
+
const classified = classifyJoseError(err);
|
|
543
|
+
throw new IQAuthError(classified.code, classified.message, void 0, err);
|
|
497
544
|
}
|
|
498
545
|
}
|
|
499
546
|
/**
|
|
@@ -535,7 +582,7 @@ var TokensModule = class {
|
|
|
535
582
|
getClaims(token) {
|
|
536
583
|
const claims = this.decode(token);
|
|
537
584
|
if (!claims) {
|
|
538
|
-
throw new IQAuthError("
|
|
585
|
+
throw new IQAuthError("token_invalid", "Unable to decode token claims");
|
|
539
586
|
}
|
|
540
587
|
return claims;
|
|
541
588
|
}
|
|
@@ -545,7 +592,7 @@ var TokensModule = class {
|
|
|
545
592
|
}
|
|
546
593
|
await this.refreshJwks();
|
|
547
594
|
if (!this.jwksCache) {
|
|
548
|
-
throw new IQAuthError("
|
|
595
|
+
throw new IQAuthError("jwks_unavailable", "JWKS cache unavailable after refresh");
|
|
549
596
|
}
|
|
550
597
|
return this.jwksCache;
|
|
551
598
|
}
|
|
@@ -555,22 +602,38 @@ var TokensModule = class {
|
|
|
555
602
|
}
|
|
556
603
|
this.inFlightRefresh = (async () => {
|
|
557
604
|
try {
|
|
558
|
-
|
|
605
|
+
let res;
|
|
606
|
+
try {
|
|
607
|
+
res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
|
|
608
|
+
} catch (err) {
|
|
609
|
+
throw new IQAuthError(
|
|
610
|
+
"network",
|
|
611
|
+
err instanceof Error ? err.message : "JWKS fetch network error",
|
|
612
|
+
void 0,
|
|
613
|
+
err
|
|
614
|
+
);
|
|
615
|
+
}
|
|
559
616
|
if (!res.ok) {
|
|
560
617
|
throw new IQAuthError(
|
|
561
|
-
"
|
|
562
|
-
`Failed to fetch JWKS: ${res.status}
|
|
618
|
+
"jwks_fetch_failed",
|
|
619
|
+
`Failed to fetch JWKS: ${res.status}`,
|
|
620
|
+
res.status
|
|
563
621
|
);
|
|
564
622
|
}
|
|
565
623
|
let jwks;
|
|
566
624
|
try {
|
|
567
625
|
jwks = await res.json();
|
|
568
|
-
} catch {
|
|
569
|
-
throw new IQAuthError(
|
|
626
|
+
} catch (err) {
|
|
627
|
+
throw new IQAuthError(
|
|
628
|
+
"jwks_fetch_failed",
|
|
629
|
+
"Malformed JWKS response: invalid JSON",
|
|
630
|
+
res.status,
|
|
631
|
+
err
|
|
632
|
+
);
|
|
570
633
|
}
|
|
571
634
|
if (!jwks || !Array.isArray(jwks.keys)) {
|
|
572
635
|
throw new IQAuthError(
|
|
573
|
-
"
|
|
636
|
+
"jwks_fetch_failed",
|
|
574
637
|
"Malformed JWKS response: expected { keys: [...] }"
|
|
575
638
|
);
|
|
576
639
|
}
|
|
@@ -578,7 +641,7 @@ var TokensModule = class {
|
|
|
578
641
|
for (const key of jwks.keys) {
|
|
579
642
|
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")) {
|
|
580
643
|
throw new IQAuthError(
|
|
581
|
-
"
|
|
644
|
+
"jwks_fetch_failed",
|
|
582
645
|
"Malformed JWKS response: key missing required fields"
|
|
583
646
|
);
|
|
584
647
|
}
|
|
@@ -596,6 +659,19 @@ var TokensModule = class {
|
|
|
596
659
|
clearCache() {
|
|
597
660
|
this.jwksCache = null;
|
|
598
661
|
}
|
|
662
|
+
/**
|
|
663
|
+
* Task #126: Eagerly populate the JWKS cache so the first verify() call
|
|
664
|
+
* doesn't pay a network round-trip. Safe to call repeatedly — single-flight
|
|
665
|
+
* behavior is shared with the lazy refresh path. Errors are swallowed so
|
|
666
|
+
* callers (e.g. `attachHelpers` auto-prewarm) can fire-and-forget.
|
|
667
|
+
*/
|
|
668
|
+
async prewarm() {
|
|
669
|
+
if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) return;
|
|
670
|
+
try {
|
|
671
|
+
await this.refreshJwks();
|
|
672
|
+
} catch {
|
|
673
|
+
}
|
|
674
|
+
}
|
|
599
675
|
};
|
|
600
676
|
|
|
601
677
|
// src/modules/sessions.ts
|
|
@@ -919,14 +995,14 @@ var OidcModule = class {
|
|
|
919
995
|
*/
|
|
920
996
|
async handleCallback(params) {
|
|
921
997
|
if (!params.state) {
|
|
922
|
-
throw new IQAuthError("
|
|
998
|
+
throw new IQAuthError("config_invalid", "OIDC callback missing state parameter");
|
|
923
999
|
}
|
|
924
1000
|
if (!params.code) {
|
|
925
|
-
throw new IQAuthError("
|
|
1001
|
+
throw new IQAuthError("config_invalid", "OIDC callback missing code parameter");
|
|
926
1002
|
}
|
|
927
1003
|
const stored = await this.stateStore.get(params.state);
|
|
928
1004
|
if (!stored) {
|
|
929
|
-
throw new IQAuthError("
|
|
1005
|
+
throw new IQAuthError("config_invalid", "Unknown or expired OIDC state");
|
|
930
1006
|
}
|
|
931
1007
|
let tokens;
|
|
932
1008
|
try {
|
|
@@ -944,7 +1020,7 @@ var OidcModule = class {
|
|
|
944
1020
|
if (tokens.id_token) {
|
|
945
1021
|
if (!this.tokensModule) {
|
|
946
1022
|
throw new IQAuthError(
|
|
947
|
-
"
|
|
1023
|
+
"config_invalid",
|
|
948
1024
|
"OIDC handleCallback received an id_token but no TokensModule is configured for verification"
|
|
949
1025
|
);
|
|
950
1026
|
}
|
|
@@ -955,7 +1031,7 @@ var OidcModule = class {
|
|
|
955
1031
|
const tokenNonce = typeof claimsBag.nonce === "string" ? claimsBag.nonce : void 0;
|
|
956
1032
|
if (!tokenNonce || tokenNonce !== stored.nonce) {
|
|
957
1033
|
throw new IQAuthError(
|
|
958
|
-
"
|
|
1034
|
+
"token_invalid",
|
|
959
1035
|
"OIDC id_token nonce did not match the stored value"
|
|
960
1036
|
);
|
|
961
1037
|
}
|
|
@@ -1156,6 +1232,9 @@ var AppsModule = class {
|
|
|
1156
1232
|
* @remarks Wraps GET /api/v1/apps/:appKey
|
|
1157
1233
|
*/
|
|
1158
1234
|
async get(appKey) {
|
|
1235
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1236
|
+
throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
|
|
1237
|
+
}
|
|
1159
1238
|
return this.http.request("GET", `/api/v1/apps/${encodeURIComponent(appKey)}`);
|
|
1160
1239
|
}
|
|
1161
1240
|
/**
|
|
@@ -1175,6 +1254,16 @@ var AppsModule = class {
|
|
|
1175
1254
|
401
|
|
1176
1255
|
);
|
|
1177
1256
|
}
|
|
1257
|
+
if (!manifest || typeof manifest.key !== "string" || manifest.key.trim() === "") {
|
|
1258
|
+
throw new IQAuthError("VALIDATION_ERROR", "manifest.key (appKey) is required for register().", 400);
|
|
1259
|
+
}
|
|
1260
|
+
if (!manifest.environment || !["production", "staging", "development"].includes(manifest.environment)) {
|
|
1261
|
+
throw new IQAuthError(
|
|
1262
|
+
"ENVIRONMENT_REQUIRED",
|
|
1263
|
+
"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.",
|
|
1264
|
+
400
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1178
1267
|
return this.http.request("POST", "/api/v1/apps/sync", manifest);
|
|
1179
1268
|
}
|
|
1180
1269
|
/**
|
|
@@ -1184,11 +1273,14 @@ var AppsModule = class {
|
|
|
1184
1273
|
* @remarks Uses GET /api/v1/apps/:appKey — catches 404 errors
|
|
1185
1274
|
*/
|
|
1186
1275
|
async isRegistered(appKey) {
|
|
1276
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1277
|
+
throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
|
|
1278
|
+
}
|
|
1187
1279
|
try {
|
|
1188
1280
|
await this.get(appKey);
|
|
1189
1281
|
return true;
|
|
1190
1282
|
} catch (err) {
|
|
1191
|
-
if (err.code === "NOT_FOUND" || err.status === 404) {
|
|
1283
|
+
if (err.code === "app_not_found" || err.code === "NOT_FOUND" || err.status === 404) {
|
|
1192
1284
|
return false;
|
|
1193
1285
|
}
|
|
1194
1286
|
throw err;
|
|
@@ -1225,6 +1317,20 @@ var RolesModule = class {
|
|
|
1225
1317
|
};
|
|
1226
1318
|
|
|
1227
1319
|
// src/modules/permissionGroups.ts
|
|
1320
|
+
function assertAppKey(appKey, callsite) {
|
|
1321
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1322
|
+
throw new IQAuthError(
|
|
1323
|
+
"VALIDATION_ERROR",
|
|
1324
|
+
`appKey is required for ${callsite} (no env-var fallback, no 'product' alias).`,
|
|
1325
|
+
400
|
|
1326
|
+
);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
function assertNodeKey(nodeKey, callsite) {
|
|
1330
|
+
if (typeof nodeKey !== "string" || nodeKey.trim() === "") {
|
|
1331
|
+
throw new IQAuthError("VALIDATION_ERROR", `nodeKey is required for ${callsite}.`, 400);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1228
1334
|
var PermissionGroupsModule = class {
|
|
1229
1335
|
constructor(http) {
|
|
1230
1336
|
this.http = http;
|
|
@@ -1245,7 +1351,14 @@ var PermissionGroupsModule = class {
|
|
|
1245
1351
|
return this.http.request("GET", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`);
|
|
1246
1352
|
}
|
|
1247
1353
|
async addPermission(tenantId, groupId, data) {
|
|
1248
|
-
|
|
1354
|
+
assertAppKey(data?.appKey, "permissionGroups.addPermission");
|
|
1355
|
+
assertNodeKey(data?.nodeKey, "permissionGroups.addPermission");
|
|
1356
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`, {
|
|
1357
|
+
appKey: data.appKey,
|
|
1358
|
+
nodeKey: data.nodeKey,
|
|
1359
|
+
effect: data.effect,
|
|
1360
|
+
weight: data.weight
|
|
1361
|
+
});
|
|
1249
1362
|
}
|
|
1250
1363
|
async removePermission(tenantId, groupId, permissionId) {
|
|
1251
1364
|
return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions/${permissionId}`);
|
|
@@ -1269,21 +1382,51 @@ var PermissionGroupsModule = class {
|
|
|
1269
1382
|
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`);
|
|
1270
1383
|
}
|
|
1271
1384
|
async addUserOverride(tenantId, userId, data) {
|
|
1272
|
-
|
|
1385
|
+
assertAppKey(data?.appKey, "permissionGroups.addUserOverride");
|
|
1386
|
+
assertNodeKey(data?.nodeKey, "permissionGroups.addUserOverride");
|
|
1387
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`, {
|
|
1388
|
+
appKey: data.appKey,
|
|
1389
|
+
nodeKey: data.nodeKey,
|
|
1390
|
+
effect: data.effect,
|
|
1391
|
+
weight: data.weight,
|
|
1392
|
+
expiresAt: data.expiresAt
|
|
1393
|
+
});
|
|
1273
1394
|
}
|
|
1274
1395
|
async removeUserOverride(tenantId, userId, overrideId) {
|
|
1275
1396
|
return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides/${overrideId}`);
|
|
1276
1397
|
}
|
|
1398
|
+
/**
|
|
1399
|
+
* Task #130 — `appKey` is REQUIRED. The legacy `product` query alias is no
|
|
1400
|
+
* longer accepted at the SDK boundary; pass it as `appKey` instead. The
|
|
1401
|
+
* server still accepts `product=` from raw HTTP callers during the
|
|
1402
|
+
* deprecation window, but the SDK will not silently translate it.
|
|
1403
|
+
*/
|
|
1277
1404
|
async getEffectivePermissions(tenantId, userId, params) {
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
const qs = query.toString();
|
|
1282
|
-
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective${qs ? `?${qs}` : ""}`);
|
|
1405
|
+
assertAppKey(params?.appKey, "permissionGroups.getEffectivePermissions");
|
|
1406
|
+
const qs = new URLSearchParams({ appKey: params.appKey }).toString();
|
|
1407
|
+
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective?${qs}`);
|
|
1283
1408
|
}
|
|
1284
1409
|
async checkPermission(tenantId, userId, appKey, nodeKey) {
|
|
1410
|
+
assertAppKey(appKey, "permissionGroups.checkPermission");
|
|
1411
|
+
assertNodeKey(nodeKey, "permissionGroups.checkPermission");
|
|
1285
1412
|
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/check`, { appKey, nodeKey });
|
|
1286
1413
|
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Task #130 — every entry in `checks` must include a non-empty `appKey`
|
|
1416
|
+
* AND `nodeKey`. The SDK validates the whole batch before sending so a
|
|
1417
|
+
* single misconfigured entry can't slip through and silently report
|
|
1418
|
+
* `allowed: false` from the server's per-entry validation branch.
|
|
1419
|
+
*/
|
|
1420
|
+
async batchCheckPermissions(tenantId, userId, checks) {
|
|
1421
|
+
if (!Array.isArray(checks) || checks.length === 0) {
|
|
1422
|
+
throw new IQAuthError("VALIDATION_ERROR", "checks must be a non-empty array of { appKey, nodeKey }.", 400);
|
|
1423
|
+
}
|
|
1424
|
+
checks.forEach((c, i) => {
|
|
1425
|
+
assertAppKey(c?.appKey, `permissionGroups.batchCheckPermissions[${i}]`);
|
|
1426
|
+
assertNodeKey(c?.nodeKey, `permissionGroups.batchCheckPermissions[${i}]`);
|
|
1427
|
+
});
|
|
1428
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/batch-check`, { checks });
|
|
1429
|
+
}
|
|
1287
1430
|
};
|
|
1288
1431
|
|
|
1289
1432
|
// src/modules/apiKeys.ts
|
|
@@ -1708,6 +1851,10 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1708
1851
|
this._refreshToken = tokens.refreshToken;
|
|
1709
1852
|
},
|
|
1710
1853
|
autoRefresh: "autoRefresh" in config ? config.autoRefresh !== false : true,
|
|
1854
|
+
// `'app-state'` is mobile-only — on any other environment we treat it
|
|
1855
|
+
// as the default `true` (proactive refresh ON). Only the mobile client
|
|
1856
|
+
// disables proactive refresh and replaces it with an AppState listener.
|
|
1857
|
+
proactiveRefresh: "autoRefresh" in config && config.autoRefresh === "app-state" && _IQAuthClient.resolveEnvironment(config) === "mobile" ? false : true,
|
|
1711
1858
|
onTokenRefresh: "onTokenRefresh" in config ? config.onTokenRefresh : void 0,
|
|
1712
1859
|
sessionHeaderName: config.sessionHeaderName,
|
|
1713
1860
|
sessionHeaderValue: config.sessionHeaderValue,
|
|
@@ -1748,6 +1895,13 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1748
1895
|
static forServer(config) {
|
|
1749
1896
|
return new _IQAuthClient({ ...config, environment: "server" });
|
|
1750
1897
|
}
|
|
1898
|
+
/**
|
|
1899
|
+
* Construct a mobile-environment client. NOTE: this constructor does NOT
|
|
1900
|
+
* subscribe to React Native's `AppState` even when `autoRefresh: 'app-state'`
|
|
1901
|
+
* is passed — it only disables the per-request proactive refresh. Use
|
|
1902
|
+
* `createMobileClient` from `@iqauth/sdk/mobile` if you want the full
|
|
1903
|
+
* AppState-driven refresh behavior (recommended for Expo / React Native).
|
|
1904
|
+
*/
|
|
1751
1905
|
static forMobile(config) {
|
|
1752
1906
|
return new _IQAuthClient({ ...config, environment: "mobile" });
|
|
1753
1907
|
}
|
|
@@ -1764,6 +1918,18 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1764
1918
|
getRefreshToken() {
|
|
1765
1919
|
return this._refreshToken;
|
|
1766
1920
|
}
|
|
1921
|
+
/**
|
|
1922
|
+
* Task #126: Eagerly fetch JWKS + OIDC discovery so the first verify() /
|
|
1923
|
+
* refresh round-trip on the request hot path doesn't pay the discovery
|
|
1924
|
+
* fetch latency. Safe to call repeatedly. Errors are swallowed; callers
|
|
1925
|
+
* may fire-and-forget. Called automatically by `iqAuth({...}).attachHelpers()`.
|
|
1926
|
+
*/
|
|
1927
|
+
async prewarm() {
|
|
1928
|
+
await Promise.all([
|
|
1929
|
+
this.tokens.prewarm(),
|
|
1930
|
+
this.oidc.getDiscovery().catch(() => void 0)
|
|
1931
|
+
]);
|
|
1932
|
+
}
|
|
1767
1933
|
getCurrentClaims() {
|
|
1768
1934
|
if (!this._accessToken) return null;
|
|
1769
1935
|
return this.tokens.decode(this._accessToken);
|
|
@@ -1804,14 +1970,14 @@ function assertPublishableKey(raw, opts) {
|
|
|
1804
1970
|
const ctx = opts?.context ? `${opts.context}: ` : "";
|
|
1805
1971
|
if (typeof raw !== "string" || raw.length === 0) {
|
|
1806
1972
|
throw new IQAuthError(
|
|
1807
|
-
"
|
|
1973
|
+
"config_invalid",
|
|
1808
1974
|
`${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.`
|
|
1809
1975
|
);
|
|
1810
1976
|
}
|
|
1811
1977
|
const shapeMatch = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
|
|
1812
1978
|
if (!shapeMatch) {
|
|
1813
1979
|
throw new IQAuthError(
|
|
1814
|
-
"
|
|
1980
|
+
"config_invalid",
|
|
1815
1981
|
`${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.`
|
|
1816
1982
|
);
|
|
1817
1983
|
}
|
|
@@ -1820,19 +1986,19 @@ function assertPublishableKey(raw, opts) {
|
|
|
1820
1986
|
decoded = JSON.parse(b64urlDecode(shapeMatch[2]));
|
|
1821
1987
|
} catch {
|
|
1822
1988
|
throw new IQAuthError(
|
|
1823
|
-
"
|
|
1989
|
+
"config_invalid",
|
|
1824
1990
|
`${ctx}IQAuth publishable key payload is not valid base64url JSON. Regenerate the key from the IQAuth admin console.`
|
|
1825
1991
|
);
|
|
1826
1992
|
}
|
|
1827
1993
|
if (!isPublishableKeyPayload(decoded)) {
|
|
1828
1994
|
throw new IQAuthError(
|
|
1829
|
-
"
|
|
1995
|
+
"config_invalid",
|
|
1830
1996
|
`${ctx}IQAuth publishable key payload is missing required fields {iss, appId, tenantId, kid}. Regenerate the key from the IQAuth admin console.`
|
|
1831
1997
|
);
|
|
1832
1998
|
}
|
|
1833
1999
|
if (!isValidIssuerUrl(decoded.iss)) {
|
|
1834
2000
|
throw new IQAuthError(
|
|
1835
|
-
"
|
|
2001
|
+
"config_invalid",
|
|
1836
2002
|
`${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.`
|
|
1837
2003
|
);
|
|
1838
2004
|
}
|
|
@@ -1845,6 +2011,45 @@ function isPublishableKeyPayload(value) {
|
|
|
1845
2011
|
}
|
|
1846
2012
|
|
|
1847
2013
|
// src/server/handlers.ts
|
|
2014
|
+
async function buildUserinfoResponse(claims, opts = {}) {
|
|
2015
|
+
const baseUser = {
|
|
2016
|
+
sub: claims.sub,
|
|
2017
|
+
email: claims.email,
|
|
2018
|
+
name: claims.name,
|
|
2019
|
+
tenantId: claims.tenantId,
|
|
2020
|
+
vendorId: claims.vendorId,
|
|
2021
|
+
roles: claims.roles ?? [],
|
|
2022
|
+
entitlements: claims.entitlements ?? [],
|
|
2023
|
+
// Task #171 — project the active source/client scope onto the userinfo
|
|
2024
|
+
// payload so server handlers (`getSessionUser`, `/api/iqauth/userinfo`)
|
|
2025
|
+
// expose it without consumers having to re-decode the JWT.
|
|
2026
|
+
...claims.scopeContext !== void 0 ? { scopeContext: claims.scopeContext } : {}
|
|
2027
|
+
};
|
|
2028
|
+
const enriched = opts.enrich ? await opts.enrich(claims) : null;
|
|
2029
|
+
const user = enriched ? { ...baseUser, ...enriched } : baseUser;
|
|
2030
|
+
return {
|
|
2031
|
+
success: true,
|
|
2032
|
+
data: {
|
|
2033
|
+
user,
|
|
2034
|
+
claims,
|
|
2035
|
+
tenantId: claims.tenantId ?? null
|
|
2036
|
+
}
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
function emitTiming(cfg, event) {
|
|
2040
|
+
if (cfg.debug) {
|
|
2041
|
+
try {
|
|
2042
|
+
console.debug("[iqauth_helper]", event);
|
|
2043
|
+
} catch {
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
if (cfg.onTimingEvent) {
|
|
2047
|
+
try {
|
|
2048
|
+
cfg.onTimingEvent(event);
|
|
2049
|
+
} catch {
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
1848
2053
|
var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
1849
2054
|
"TOKEN_REVOKED",
|
|
1850
2055
|
"SESSION_REVOKED",
|
|
@@ -1863,19 +2068,62 @@ function shouldClearCookiesOnFailure(policy, status, errorCode) {
|
|
|
1863
2068
|
}
|
|
1864
2069
|
var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
|
|
1865
2070
|
var REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
|
|
2071
|
+
function assertCookiePrefixInvariants(name, secure, path, domain) {
|
|
2072
|
+
if (name.startsWith("__Host-")) {
|
|
2073
|
+
if (!secure) {
|
|
2074
|
+
throw new IQAuthError(
|
|
2075
|
+
"config_invalid",
|
|
2076
|
+
`Cookie "${name}" uses the __Host- prefix, which browsers only accept on a Secure cookie. Set secure:true (and serve over HTTPS).`
|
|
2077
|
+
);
|
|
2078
|
+
}
|
|
2079
|
+
if (path !== "/") {
|
|
2080
|
+
throw new IQAuthError(
|
|
2081
|
+
"config_invalid",
|
|
2082
|
+
`Cookie "${name}" uses the __Host- prefix, which requires Path=/ (got "${path}"). Remove cookiePath or set it to "/".`
|
|
2083
|
+
);
|
|
2084
|
+
}
|
|
2085
|
+
if (domain) {
|
|
2086
|
+
throw new IQAuthError(
|
|
2087
|
+
"config_invalid",
|
|
2088
|
+
`Cookie "${name}" uses the __Host- prefix, which forbids a Domain attribute (the cookie is host-locked). Remove cookieDomain.`
|
|
2089
|
+
);
|
|
2090
|
+
}
|
|
2091
|
+
} else if (name.startsWith("__Secure-") && !secure) {
|
|
2092
|
+
throw new IQAuthError(
|
|
2093
|
+
"config_invalid",
|
|
2094
|
+
`Cookie "${name}" uses the __Secure- prefix, which browsers only accept on a Secure cookie. Set secure:true (and serve over HTTPS).`
|
|
2095
|
+
);
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
1866
2098
|
function resolve(config) {
|
|
1867
2099
|
const parsed = assertPublishableKey(config.publishableKey, { context: "@iqauth/sdk helpers" });
|
|
1868
2100
|
const inferredIssuer = parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`;
|
|
2101
|
+
maybeWarnDefaultSignoutRegistry(config);
|
|
2102
|
+
const secure = config.secure ?? true;
|
|
2103
|
+
if (config.secure === false && config.allowInsecureCookies !== true) {
|
|
2104
|
+
throw new IQAuthError(
|
|
2105
|
+
"config_invalid",
|
|
2106
|
+
"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."
|
|
2107
|
+
);
|
|
2108
|
+
}
|
|
2109
|
+
const accessCookieName = config.accessCookieName ?? config.cookieNames?.access ?? "iqauth_at";
|
|
2110
|
+
const refreshCookieName = config.refreshCookieName ?? config.cookieNames?.refresh ?? "iqauth_rt";
|
|
2111
|
+
const stateCookieName = config.stateCookieName ?? "iqauth_state";
|
|
2112
|
+
const cookiePath = config.cookiePath ?? "/";
|
|
2113
|
+
const cookieDomain = config.cookieDomain;
|
|
2114
|
+
for (const name of [accessCookieName, refreshCookieName, stateCookieName]) {
|
|
2115
|
+
assertCookiePrefixInvariants(name, secure, cookiePath, cookieDomain);
|
|
2116
|
+
}
|
|
1869
2117
|
return {
|
|
1870
2118
|
publishableKey: config.publishableKey,
|
|
1871
2119
|
secretKey: config.secretKey,
|
|
1872
2120
|
issuer: (config.issuer ?? inferredIssuer).replace(/\/+$/, ""),
|
|
1873
|
-
accessCookieName
|
|
1874
|
-
refreshCookieName
|
|
1875
|
-
cookieDomain
|
|
2121
|
+
accessCookieName,
|
|
2122
|
+
refreshCookieName,
|
|
2123
|
+
cookieDomain,
|
|
1876
2124
|
sameSite: config.sameSite ?? "lax",
|
|
1877
|
-
secure
|
|
1878
|
-
cookiePath
|
|
2125
|
+
secure,
|
|
2126
|
+
cookiePath,
|
|
1879
2127
|
tokenPath: config.tokenPath ?? "/oidc/token",
|
|
1880
2128
|
refreshPath: config.refreshPath ?? "/api/v1/auth/refresh",
|
|
1881
2129
|
logoutPath: config.logoutPath ?? "/api/v1/auth/logout",
|
|
@@ -1884,9 +2132,23 @@ function resolve(config) {
|
|
|
1884
2132
|
})),
|
|
1885
2133
|
appId: parsed.appId,
|
|
1886
2134
|
tenantId: parsed.tenantId,
|
|
1887
|
-
clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only"
|
|
2135
|
+
clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only",
|
|
2136
|
+
debug: config.debug,
|
|
2137
|
+
onTimingEvent: config.onTimingEvent,
|
|
2138
|
+
signoutRegistry: config.signoutRegistry ?? defaultSignoutRegistry,
|
|
2139
|
+
signoutMarkerTtlMs: config.signoutMarkerTtlMs ?? DEFAULT_SIGNOUT_TTL_MS,
|
|
2140
|
+
requireOAuthState: config.requireOAuthState ?? true,
|
|
2141
|
+
stateCookieName: config.stateCookieName ?? "iqauth_state"
|
|
1888
2142
|
};
|
|
1889
2143
|
}
|
|
2144
|
+
function timingSafeEqualStr(a, b) {
|
|
2145
|
+
const len = Math.max(a.length, b.length);
|
|
2146
|
+
let diff = a.length ^ b.length;
|
|
2147
|
+
for (let i = 0; i < len; i++) {
|
|
2148
|
+
diff |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0);
|
|
2149
|
+
}
|
|
2150
|
+
return diff === 0;
|
|
2151
|
+
}
|
|
1890
2152
|
function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
|
|
1891
2153
|
return {
|
|
1892
2154
|
name,
|
|
@@ -1901,15 +2163,53 @@ function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
|
|
|
1901
2163
|
}
|
|
1902
2164
|
function clearCookies(cfg) {
|
|
1903
2165
|
return [
|
|
1904
|
-
makeCookie(cfg, cfg.accessCookieName, "", 0),
|
|
1905
|
-
makeCookie(cfg, cfg.refreshCookieName, "", 0)
|
|
2166
|
+
{ ...makeCookie(cfg, cfg.accessCookieName, "", 0), clear: true },
|
|
2167
|
+
{ ...makeCookie(cfg, cfg.refreshCookieName, "", 0), clear: true }
|
|
1906
2168
|
];
|
|
1907
2169
|
}
|
|
2170
|
+
function clearStateCookie(cfg) {
|
|
2171
|
+
return { ...makeCookie(cfg, cfg.stateCookieName, "", 0, false), clear: true };
|
|
2172
|
+
}
|
|
2173
|
+
var DEFAULT_SIGNOUT_TTL_MS = 6e4;
|
|
2174
|
+
var inMemorySignoutMarkers = /* @__PURE__ */ new Map();
|
|
2175
|
+
function pruneInMemoryMarkers(now) {
|
|
2176
|
+
if (inMemorySignoutMarkers.size === 0) return;
|
|
2177
|
+
for (const [k, exp] of inMemorySignoutMarkers) {
|
|
2178
|
+
if (exp <= now) inMemorySignoutMarkers.delete(k);
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
var defaultSignoutRegistry = {
|
|
2182
|
+
mark(token, ttlMs) {
|
|
2183
|
+
const now = Date.now();
|
|
2184
|
+
pruneInMemoryMarkers(now);
|
|
2185
|
+
inMemorySignoutMarkers.set(token, now + ttlMs);
|
|
2186
|
+
},
|
|
2187
|
+
has(token) {
|
|
2188
|
+
const now = Date.now();
|
|
2189
|
+
const exp = inMemorySignoutMarkers.get(token);
|
|
2190
|
+
if (!exp) return false;
|
|
2191
|
+
if (exp <= now) {
|
|
2192
|
+
inMemorySignoutMarkers.delete(token);
|
|
2193
|
+
return false;
|
|
2194
|
+
}
|
|
2195
|
+
return true;
|
|
2196
|
+
}
|
|
2197
|
+
};
|
|
2198
|
+
var warnedDefaultSignoutRegistry = false;
|
|
2199
|
+
function maybeWarnDefaultSignoutRegistry(config) {
|
|
2200
|
+
if (warnedDefaultSignoutRegistry) return;
|
|
2201
|
+
if (config.signoutRegistry) return;
|
|
2202
|
+
warnedDefaultSignoutRegistry = true;
|
|
2203
|
+
console.warn(
|
|
2204
|
+
"[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."
|
|
2205
|
+
);
|
|
2206
|
+
}
|
|
1908
2207
|
function serializeCookie(d) {
|
|
1909
2208
|
const parts = [`${d.name}=${encodeURIComponent(d.value)}`];
|
|
1910
2209
|
parts.push(`Path=${d.path}`);
|
|
1911
2210
|
if (d.domain) parts.push(`Domain=${d.domain}`);
|
|
1912
2211
|
parts.push(`Max-Age=${d.maxAge}`);
|
|
2212
|
+
if (d.clear) parts.push("Expires=Thu, 01 Jan 1970 00:00:00 GMT");
|
|
1913
2213
|
if (d.secure) parts.push("Secure");
|
|
1914
2214
|
if (d.httpOnly) parts.push("HttpOnly");
|
|
1915
2215
|
parts.push(`SameSite=${d.sameSite}`);
|
|
@@ -1917,14 +2217,34 @@ function serializeCookie(d) {
|
|
|
1917
2217
|
}
|
|
1918
2218
|
async function handleCallback(config, input) {
|
|
1919
2219
|
const cfg = resolve(config);
|
|
2220
|
+
const t0 = Date.now();
|
|
1920
2221
|
if (!input.code || !input.redirectUri) {
|
|
2222
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "VALIDATION_ERROR" });
|
|
1921
2223
|
return {
|
|
1922
2224
|
status: 400,
|
|
1923
2225
|
body: { success: false, error: { code: "VALIDATION_ERROR", message: "code and redirectUri are required" } },
|
|
1924
2226
|
cookies: []
|
|
1925
2227
|
};
|
|
1926
2228
|
}
|
|
2229
|
+
const provided = input.state;
|
|
2230
|
+
const expected = input.expectedState;
|
|
2231
|
+
const stateOk = cfg.requireOAuthState ? !!expected && !!provided && timingSafeEqualStr(provided, expected) : !expected || !!provided && timingSafeEqualStr(provided, expected);
|
|
2232
|
+
if (!stateOk) {
|
|
2233
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "STATE_MISMATCH" });
|
|
2234
|
+
return {
|
|
2235
|
+
status: 400,
|
|
2236
|
+
body: {
|
|
2237
|
+
success: false,
|
|
2238
|
+
error: {
|
|
2239
|
+
code: "STATE_MISMATCH",
|
|
2240
|
+
message: "OAuth state validation failed; the sign-in could not be verified as originating from this browser."
|
|
2241
|
+
}
|
|
2242
|
+
},
|
|
2243
|
+
cookies: [clearStateCookie(cfg)]
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
1927
2246
|
if (!cfg.secretKey) {
|
|
2247
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "INTERNAL_ERROR" });
|
|
1928
2248
|
return {
|
|
1929
2249
|
status: 500,
|
|
1930
2250
|
body: { success: false, error: { code: "INTERNAL_ERROR", message: "secretKey is required for the callback handler" } },
|
|
@@ -1948,6 +2268,7 @@ async function handleCallback(config, input) {
|
|
|
1948
2268
|
});
|
|
1949
2269
|
const json = await res.json().catch(() => ({}));
|
|
1950
2270
|
if (!res.ok || !json.access_token) {
|
|
2271
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: json.error || "OIDC_EXCHANGE_FAILED" });
|
|
1951
2272
|
return {
|
|
1952
2273
|
status: res.status || 502,
|
|
1953
2274
|
body: {
|
|
@@ -1960,6 +2281,26 @@ async function handleCallback(config, input) {
|
|
|
1960
2281
|
cookies: []
|
|
1961
2282
|
};
|
|
1962
2283
|
}
|
|
2284
|
+
try {
|
|
2285
|
+
await getTokensFor(cfg.issuer).verify(json.access_token, {
|
|
2286
|
+
issuer: cfg.issuer,
|
|
2287
|
+
...config.verify
|
|
2288
|
+
});
|
|
2289
|
+
} catch (err) {
|
|
2290
|
+
const code = err instanceof IQAuthError ? err.code : err.code || "TOKEN_INVALID";
|
|
2291
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code });
|
|
2292
|
+
return {
|
|
2293
|
+
status: 502,
|
|
2294
|
+
body: {
|
|
2295
|
+
success: false,
|
|
2296
|
+
error: {
|
|
2297
|
+
code: "ACCESS_TOKEN_VERIFICATION_FAILED",
|
|
2298
|
+
message: "The issuer returned an access token that failed verification; no session was established."
|
|
2299
|
+
}
|
|
2300
|
+
},
|
|
2301
|
+
cookies: []
|
|
2302
|
+
};
|
|
2303
|
+
}
|
|
1963
2304
|
const cookies = [];
|
|
1964
2305
|
cookies.push(
|
|
1965
2306
|
makeCookie(cfg, cfg.accessCookieName, json.access_token, json.expires_in ?? ACCESS_TOKEN_TTL_SECONDS)
|
|
@@ -1967,6 +2308,8 @@ async function handleCallback(config, input) {
|
|
|
1967
2308
|
if (json.refresh_token) {
|
|
1968
2309
|
cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.refresh_token, REFRESH_TOKEN_TTL_SECONDS));
|
|
1969
2310
|
}
|
|
2311
|
+
cookies.push(clearStateCookie(cfg));
|
|
2312
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: true });
|
|
1970
2313
|
return {
|
|
1971
2314
|
status: 200,
|
|
1972
2315
|
body: { success: true, data: { authenticated: true } },
|
|
@@ -1975,8 +2318,18 @@ async function handleCallback(config, input) {
|
|
|
1975
2318
|
}
|
|
1976
2319
|
async function handleRefresh(config, input) {
|
|
1977
2320
|
const cfg = resolve(config);
|
|
2321
|
+
const t0 = Date.now();
|
|
1978
2322
|
const refreshToken = input.refreshToken;
|
|
2323
|
+
const idemKey = input.idempotencyToken;
|
|
2324
|
+
if (idemKey && await Promise.resolve(cfg.signoutRegistry.has(idemKey))) {
|
|
2325
|
+
return {
|
|
2326
|
+
status: 401,
|
|
2327
|
+
body: { success: false, error: { code: "SESSION_REVOKED", message: "Session was signed out" } },
|
|
2328
|
+
cookies: clearCookies(cfg)
|
|
2329
|
+
};
|
|
2330
|
+
}
|
|
1979
2331
|
if (!refreshToken) {
|
|
2332
|
+
emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: false, code: "TOKEN_INVALID" });
|
|
1980
2333
|
return {
|
|
1981
2334
|
status: 401,
|
|
1982
2335
|
body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing refresh token" } },
|
|
@@ -1992,6 +2345,7 @@ async function handleRefresh(config, input) {
|
|
|
1992
2345
|
if (!res.ok || !json.success || !json.data?.accessToken) {
|
|
1993
2346
|
const status = res.status || 401;
|
|
1994
2347
|
const errorCode = json.error?.code || "TOKEN_INVALID";
|
|
2348
|
+
emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: false, code: errorCode });
|
|
1995
2349
|
const shouldClear = shouldClearCookiesOnFailure(
|
|
1996
2350
|
cfg.clearCookiesOnRefreshFailure,
|
|
1997
2351
|
status,
|
|
@@ -2015,6 +2369,7 @@ async function handleRefresh(config, input) {
|
|
|
2015
2369
|
if (json.data.refreshToken) {
|
|
2016
2370
|
cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.data.refreshToken, REFRESH_TOKEN_TTL_SECONDS));
|
|
2017
2371
|
}
|
|
2372
|
+
emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: true });
|
|
2018
2373
|
return {
|
|
2019
2374
|
status: 200,
|
|
2020
2375
|
body: { success: true, data: { accessToken: json.data.accessToken } },
|
|
@@ -2023,6 +2378,10 @@ async function handleRefresh(config, input) {
|
|
|
2023
2378
|
}
|
|
2024
2379
|
async function handleSignout(config, input) {
|
|
2025
2380
|
const cfg = resolve(config);
|
|
2381
|
+
const t0 = Date.now();
|
|
2382
|
+
if (input.idempotencyToken) {
|
|
2383
|
+
await Promise.resolve(cfg.signoutRegistry.mark(input.idempotencyToken, cfg.signoutMarkerTtlMs));
|
|
2384
|
+
}
|
|
2026
2385
|
if (input.accessToken) {
|
|
2027
2386
|
try {
|
|
2028
2387
|
await cfg.fetchImpl(`${cfg.issuer}${cfg.logoutPath}`, {
|
|
@@ -2044,21 +2403,104 @@ async function handleSignout(config, input) {
|
|
|
2044
2403
|
} catch {
|
|
2045
2404
|
}
|
|
2046
2405
|
}
|
|
2406
|
+
emitTiming(cfg, { phase: "signout", durationMs: Date.now() - t0, ok: true });
|
|
2047
2407
|
return {
|
|
2048
2408
|
status: 200,
|
|
2049
2409
|
body: { success: true, data: { signedOut: true } },
|
|
2050
2410
|
cookies: clearCookies(cfg)
|
|
2051
2411
|
};
|
|
2052
2412
|
}
|
|
2413
|
+
var TOKENS_CACHE = /* @__PURE__ */ new Map();
|
|
2414
|
+
function getTokensFor(issuer) {
|
|
2415
|
+
let m = TOKENS_CACHE.get(issuer);
|
|
2416
|
+
if (!m) {
|
|
2417
|
+
m = new TokensModule(issuer);
|
|
2418
|
+
TOKENS_CACHE.set(issuer, m);
|
|
2419
|
+
}
|
|
2420
|
+
return m;
|
|
2421
|
+
}
|
|
2422
|
+
async function handleUserinfo(config, input) {
|
|
2423
|
+
const cfg = resolve(config);
|
|
2424
|
+
if (!input.accessToken) {
|
|
2425
|
+
return {
|
|
2426
|
+
status: 401,
|
|
2427
|
+
body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing access token" } },
|
|
2428
|
+
cookies: []
|
|
2429
|
+
};
|
|
2430
|
+
}
|
|
2431
|
+
let claims;
|
|
2432
|
+
try {
|
|
2433
|
+
claims = await getTokensFor(cfg.issuer).verify(input.accessToken, {
|
|
2434
|
+
issuer: cfg.issuer,
|
|
2435
|
+
...config.verify
|
|
2436
|
+
});
|
|
2437
|
+
} catch (err) {
|
|
2438
|
+
const code = err instanceof IQAuthError ? err.code : err.code || "TOKEN_INVALID";
|
|
2439
|
+
const message = err instanceof Error ? err.message : "Access token verification failed";
|
|
2440
|
+
return {
|
|
2441
|
+
status: 401,
|
|
2442
|
+
body: { success: false, error: { code, message } },
|
|
2443
|
+
cookies: []
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
2446
|
+
const envelope = await buildUserinfoResponse(claims, {
|
|
2447
|
+
enrich: config.userinfoEnricher ? (c) => config.userinfoEnricher(c, input.req) : void 0
|
|
2448
|
+
});
|
|
2449
|
+
return {
|
|
2450
|
+
status: 200,
|
|
2451
|
+
body: envelope,
|
|
2452
|
+
cookies: []
|
|
2453
|
+
};
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
// src/browser/returnTo.ts
|
|
2457
|
+
function normalizeOrigin(o) {
|
|
2458
|
+
try {
|
|
2459
|
+
return new URL(o).origin;
|
|
2460
|
+
} catch {
|
|
2461
|
+
return o.replace(/\/+$/, "");
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
function sanitizeReturnTo(input, options = {}) {
|
|
2465
|
+
const fallback = options.fallback ?? "/";
|
|
2466
|
+
if (!input || typeof input !== "string") return fallback;
|
|
2467
|
+
const trimmed = input.trim();
|
|
2468
|
+
if (!trimmed) return fallback;
|
|
2469
|
+
if (trimmed.includes("\\")) return fallback;
|
|
2470
|
+
if (trimmed.startsWith("//")) return fallback;
|
|
2471
|
+
if (trimmed.startsWith("/") || trimmed.startsWith("#") || trimmed.startsWith("?")) {
|
|
2472
|
+
return trimmed;
|
|
2473
|
+
}
|
|
2474
|
+
if (!/^[a-z][a-z0-9+\-.]*:/i.test(trimmed)) {
|
|
2475
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
2476
|
+
}
|
|
2477
|
+
let parsed;
|
|
2478
|
+
try {
|
|
2479
|
+
parsed = new URL(trimmed);
|
|
2480
|
+
} catch {
|
|
2481
|
+
return fallback;
|
|
2482
|
+
}
|
|
2483
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return fallback;
|
|
2484
|
+
const currentOrigin = options.currentOrigin ?? (typeof window !== "undefined" ? window.location.origin : "");
|
|
2485
|
+
const allowed = /* @__PURE__ */ new Set();
|
|
2486
|
+
if (currentOrigin) allowed.add(normalizeOrigin(currentOrigin));
|
|
2487
|
+
for (const o of options.allowedOrigins ?? []) allowed.add(normalizeOrigin(o));
|
|
2488
|
+
if (allowed.has(parsed.origin)) return parsed.toString();
|
|
2489
|
+
return fallback;
|
|
2490
|
+
}
|
|
2053
2491
|
|
|
2054
2492
|
// src/fastify.ts
|
|
2493
|
+
var PKCE_COOKIE = "iqauth_pkce";
|
|
2055
2494
|
var KNOWN_AUTH_ERRORS = /* @__PURE__ */ new Set([
|
|
2056
2495
|
"TOKEN_INVALID",
|
|
2057
2496
|
"TOKEN_EXPIRED",
|
|
2058
2497
|
"TOKEN_REVOKED",
|
|
2059
2498
|
"SESSION_EXPIRED",
|
|
2060
2499
|
"SESSION_INVALID",
|
|
2061
|
-
"AUTH_REQUIRED"
|
|
2500
|
+
"AUTH_REQUIRED",
|
|
2501
|
+
// Task #127 — typed `IQAuthErrorCode` taxonomy.
|
|
2502
|
+
"token_invalid",
|
|
2503
|
+
"token_expired"
|
|
2062
2504
|
]);
|
|
2063
2505
|
function applyResponse(reply, hr) {
|
|
2064
2506
|
for (const c of hr.cookies) {
|
|
@@ -2070,6 +2512,54 @@ function applyResponse(reply, hr) {
|
|
|
2070
2512
|
}
|
|
2071
2513
|
reply.code(hr.status).send(hr.body);
|
|
2072
2514
|
}
|
|
2515
|
+
function applyCallbackResponse(reply, hr, requestOrigin, returnToCookieValue, returnToCookieName) {
|
|
2516
|
+
const returnTo = sanitizeReturnTo(
|
|
2517
|
+
returnToCookieValue || hr.body?.returnTo,
|
|
2518
|
+
{ currentOrigin: requestOrigin, fallback: "/" }
|
|
2519
|
+
);
|
|
2520
|
+
for (const c of hr.cookies) {
|
|
2521
|
+
const cookie = serializeCookie(c);
|
|
2522
|
+
const existing = reply.getHeader?.("set-cookie") ?? [];
|
|
2523
|
+
const list = Array.isArray(existing) ? existing : [existing];
|
|
2524
|
+
list.push(cookie);
|
|
2525
|
+
reply.header("set-cookie", list);
|
|
2526
|
+
}
|
|
2527
|
+
if (hr.status < 400) {
|
|
2528
|
+
const existing = reply.getHeader?.("set-cookie") ?? [];
|
|
2529
|
+
const list = Array.isArray(existing) ? existing : [existing];
|
|
2530
|
+
list.push(`${returnToCookieName}=; Path=/; Max-Age=0; SameSite=Lax`);
|
|
2531
|
+
reply.header("set-cookie", list);
|
|
2532
|
+
}
|
|
2533
|
+
reply.code(hr.status).send({ ...hr.body, returnTo });
|
|
2534
|
+
}
|
|
2535
|
+
function applyCallbackRedirect(reply, hr, requestOrigin, returnToCookieValue, cookieNames) {
|
|
2536
|
+
const pushCookie = (value) => {
|
|
2537
|
+
const existing = reply.getHeader?.("set-cookie") ?? [];
|
|
2538
|
+
const list = Array.isArray(existing) ? existing : [existing];
|
|
2539
|
+
list.push(value);
|
|
2540
|
+
reply.header("set-cookie", list);
|
|
2541
|
+
};
|
|
2542
|
+
for (const c of hr.cookies) pushCookie(serializeCookie(c));
|
|
2543
|
+
pushCookie(`${cookieNames.state}=; Path=/; Max-Age=0; SameSite=Lax`);
|
|
2544
|
+
pushCookie(`${cookieNames.pkce}=; Path=/; Max-Age=0; SameSite=Lax`);
|
|
2545
|
+
if (hr.status >= 400) {
|
|
2546
|
+
reply.header("location", "/");
|
|
2547
|
+
reply.code(302).send();
|
|
2548
|
+
return;
|
|
2549
|
+
}
|
|
2550
|
+
const dest = sanitizeReturnTo(returnToCookieValue, {
|
|
2551
|
+
currentOrigin: requestOrigin,
|
|
2552
|
+
fallback: "/"
|
|
2553
|
+
});
|
|
2554
|
+
pushCookie(`${cookieNames.returnTo}=; Path=/; Max-Age=0; SameSite=Lax`);
|
|
2555
|
+
reply.header("location", dest);
|
|
2556
|
+
reply.code(302).send();
|
|
2557
|
+
}
|
|
2558
|
+
function requestOriginOf(req) {
|
|
2559
|
+
const proto = req.headers?.["x-forwarded-proto"]?.split(",")[0]?.trim() || (typeof req.protocol === "string" ? req.protocol : void 0) || "https";
|
|
2560
|
+
const host = req.headers?.["x-forwarded-host"]?.split(",")[0]?.trim() || req.headers?.host || "";
|
|
2561
|
+
return host ? `${proto}://${host}` : "";
|
|
2562
|
+
}
|
|
2073
2563
|
function readCookie(req, name) {
|
|
2074
2564
|
if (req.cookies && typeof req.cookies[name] === "string") return req.cookies[name];
|
|
2075
2565
|
const raw = req.headers?.cookie;
|
|
@@ -2103,10 +2593,12 @@ async function iqAuth(fastify, options) {
|
|
|
2103
2593
|
} : void 0;
|
|
2104
2594
|
const accessCookie = options.accessCookieName ?? "iqauth_at";
|
|
2105
2595
|
const refreshCookie = options.refreshCookieName ?? "iqauth_rt";
|
|
2596
|
+
const returnToCookie = options.returnToCookieName ?? "iqauth_return_to";
|
|
2106
2597
|
const mount = (options.mountPath ?? "/api/iqauth").replace(/\/+$/, "");
|
|
2107
2598
|
const mountHelpers = options.mountHelperRoutes !== false;
|
|
2108
2599
|
const isPublic = (p) => {
|
|
2109
2600
|
if (mountHelpers && p.startsWith(mount + "/")) return true;
|
|
2601
|
+
if (options.mountUserinfo && p === `${mount}/me`) return true;
|
|
2110
2602
|
if (Array.isArray(options.publicPaths)) return options.publicPaths.includes(p);
|
|
2111
2603
|
if (typeof options.publicPaths === "function") return options.publicPaths(p);
|
|
2112
2604
|
return false;
|
|
@@ -2136,22 +2628,54 @@ async function iqAuth(fastify, options) {
|
|
|
2136
2628
|
if (mountHelpers) {
|
|
2137
2629
|
fastify.post(`${mount}/callback`, async (req, reply) => {
|
|
2138
2630
|
const body = req.body || {};
|
|
2139
|
-
|
|
2631
|
+
const hr = await handleCallback(helperConfig, {
|
|
2140
2632
|
code: body.code,
|
|
2141
2633
|
codeVerifier: body.codeVerifier,
|
|
2142
|
-
redirectUri: body.redirectUri
|
|
2143
|
-
|
|
2634
|
+
redirectUri: body.redirectUri,
|
|
2635
|
+
// M-2: bind callback to this browser; handleCallback fails closed.
|
|
2636
|
+
state: body.state,
|
|
2637
|
+
expectedState: readCookie(req, helperConfig.stateCookieName ?? "iqauth_state")
|
|
2638
|
+
});
|
|
2639
|
+
applyCallbackResponse(reply, hr, requestOriginOf(req), readCookie(req, returnToCookie), returnToCookie);
|
|
2640
|
+
});
|
|
2641
|
+
fastify.get(`${mount}/callback`, async (req, reply) => {
|
|
2642
|
+
const stateCookie = helperConfig.stateCookieName ?? "iqauth_state";
|
|
2643
|
+
const q = req.query || {};
|
|
2644
|
+
const origin = requestOriginOf(req);
|
|
2645
|
+
const redirectUri = `${origin}${mount}/callback`;
|
|
2646
|
+
const hr = await handleCallback(helperConfig, {
|
|
2647
|
+
code: q.code,
|
|
2648
|
+
codeVerifier: readCookie(req, PKCE_COOKIE),
|
|
2649
|
+
redirectUri,
|
|
2650
|
+
state: q.state,
|
|
2651
|
+
expectedState: readCookie(req, stateCookie)
|
|
2652
|
+
});
|
|
2653
|
+
applyCallbackRedirect(reply, hr, origin, readCookie(req, returnToCookie), {
|
|
2654
|
+
returnTo: returnToCookie,
|
|
2655
|
+
state: stateCookie,
|
|
2656
|
+
pkce: PKCE_COOKIE
|
|
2657
|
+
});
|
|
2144
2658
|
});
|
|
2145
2659
|
fastify.post(`${mount}/refresh`, async (req, reply) => {
|
|
2146
2660
|
const body = req.body || {};
|
|
2147
2661
|
const refreshToken = body.refreshToken || readCookie(req, refreshCookie);
|
|
2148
|
-
|
|
2662
|
+
const idempotencyToken = req.headers?.["x-iqauth-idempotency"] || body.idempotencyToken;
|
|
2663
|
+
applyResponse(reply, await handleRefresh(helperConfig, { refreshToken, idempotencyToken }));
|
|
2149
2664
|
});
|
|
2150
2665
|
fastify.post(`${mount}/signout`, async (req, reply) => {
|
|
2151
2666
|
const auth = req.headers?.authorization;
|
|
2152
2667
|
const accessToken = (typeof auth === "string" ? auth.replace(/^Bearer /i, "") : void 0) || readCookie(req, accessCookie);
|
|
2668
|
+
const refreshToken = readCookie(req, refreshCookie);
|
|
2153
2669
|
const ssoCookieHeader = typeof req.headers?.cookie === "string" ? req.headers.cookie : void 0;
|
|
2154
|
-
|
|
2670
|
+
const idempotencyToken = req.headers?.["x-iqauth-idempotency"];
|
|
2671
|
+
applyResponse(reply, await handleSignout(helperConfig, { accessToken, refreshToken, idempotencyToken, ssoCookieHeader }));
|
|
2672
|
+
});
|
|
2673
|
+
}
|
|
2674
|
+
if (options.mountUserinfo) {
|
|
2675
|
+
fastify.get(`${mount}/me`, async (req, reply) => {
|
|
2676
|
+
const auth = req.headers?.authorization;
|
|
2677
|
+
const accessToken = (typeof auth === "string" ? auth.replace(/^Bearer /i, "") : void 0) || readCookie(req, accessCookie);
|
|
2678
|
+
applyResponse(reply, await handleUserinfo(helperConfig, { accessToken, req }));
|
|
2155
2679
|
});
|
|
2156
2680
|
}
|
|
2157
2681
|
fastify.decorate("iqauth", { client, issuer });
|