@iqauth/sdk 2.7.0 → 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/dist/browser-session.d.mts +3 -3
- package/dist/browser-session.d.ts +3 -3
- package/dist/browser-session.js +31 -5
- package/dist/browser-session.mjs +1 -1
- package/dist/browser.d.mts +3 -3
- package/dist/browser.d.ts +3 -3
- package/dist/browser.js +23 -3
- package/dist/browser.mjs +1 -1
- package/dist/{chunk-YVALAG3B.mjs → chunk-25SSYDIP.mjs} +1 -1
- package/dist/{chunk-RTJAIBXY.mjs → chunk-4V7FKOTG.mjs} +23 -3
- package/dist/{chunk-SL3KRS4W.mjs → chunk-CIJORODR.mjs} +23 -1
- package/dist/chunk-JRDVUWAL.mjs +46 -0
- package/dist/{chunk-5T7GHBX6.mjs → chunk-TLET552H.mjs} +36 -0
- package/dist/{chunk-PMAFENVI.mjs → chunk-VYQ3ETCK.mjs} +27 -12
- package/dist/{chunk-RR2MGPTK.mjs → chunk-WHT6WKTY.mjs} +539 -83
- package/dist/{chunk-RUJXRTEW.mjs → chunk-WSH4SW7F.mjs} +122 -8
- package/dist/{chunk-JXQI62A7.mjs → chunk-ZLJPABB7.mjs} +31 -5
- package/dist/{client-BGFnBpfc.d.mts → client-D8L-PaWr.d.mts} +14 -4
- package/dist/{client-CDQ21LvW.d.ts → client-DkPL0EPZ.d.ts} +14 -4
- package/dist/{express-Piv2WhWM.d.ts → express-Budysq4h.d.ts} +2 -2
- package/dist/{express-CVNQEkOr.d.mts → express-DDTA3qV1.d.mts} +2 -2
- package/dist/express.d.mts +5 -5
- package/dist/express.d.ts +5 -5
- package/dist/express.js +217 -36
- package/dist/express.mjs +38 -26
- package/dist/fastify.d.mts +10 -2
- package/dist/fastify.d.ts +10 -2
- package/dist/fastify.js +260 -16
- package/dist/fastify.mjs +80 -5
- package/dist/hono.d.mts +10 -2
- package/dist/hono.d.ts +10 -2
- package/dist/hono.js +240 -16
- package/dist/hono.mjs +60 -5
- package/dist/{index-5KSZEnDe.d.ts → index-Cko-d5po.d.mts} +227 -5
- package/dist/{index-CKoZHAoc.d.mts → index-RNqwEcmY.d.ts} +227 -5
- package/dist/index.d.mts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +149 -26
- package/dist/index.mjs +5 -5
- 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 +3 -3
- package/dist/mobile.d.ts +3 -3
- package/dist/mobile.js +31 -5
- package/dist/mobile.mjs +1 -1
- package/dist/next.d.mts +10 -2
- package/dist/next.d.ts +10 -2
- package/dist/next.js +212 -11
- package/dist/next.mjs +62 -4
- package/dist/{provisioningBridge-M5G47LWO.d.mts → provisioningBridge-BXPMZCLe.d.ts} +30 -2
- package/dist/{provisioningBridge-CGpMRie4.d.ts → provisioningBridge-IEycmsgb.d.mts} +30 -2
- package/dist/react-permissions.d.mts +4 -4
- package/dist/react-permissions.d.ts +4 -4
- package/dist/react-permissions.mjs +4 -3
- package/dist/react.d.mts +4 -4
- package/dist/react.d.ts +4 -4
- package/dist/react.js +570 -41
- package/dist/react.mjs +19 -5
- package/dist/server/handlers.d.mts +56 -5
- package/dist/server/handlers.d.ts +56 -5
- package/dist/server/handlers.js +123 -8
- package/dist/server/handlers.mjs +3 -1
- package/dist/server.d.mts +28 -8
- package/dist/server.d.ts +28 -8
- package/dist/server.js +176 -14
- package/dist/server.mjs +9 -4
- package/dist/service.d.mts +3 -3
- package/dist/service.d.ts +3 -3
- package/dist/service.js +31 -5
- package/dist/service.mjs +1 -1
- package/dist/{signIn-T-CZ6t6r.d.mts → signIn-CReqfXsh.d.mts} +18 -1
- package/dist/{signIn-BLFnz8SV.d.ts → signIn-Cfa1GTpO.d.ts} +18 -1
- package/dist/{tokens-Bqhmqq_R.d.ts → tokens-9F6ETrzk.d.ts} +1 -1
- package/dist/{tokens-CITeoG6P.d.mts → tokens-B06VtvUi.d.mts} +1 -1
- package/dist/{types-XOV9XPVi.d.mts → types-Bn8O-OEd.d.mts} +66 -2
- package/dist/{types-XOV9XPVi.d.ts → types-Bn8O-OEd.d.ts} +66 -2
- package/dist/{types-BdQ2lqfT.d.mts → types-DnU2LhXR.d.mts} +6 -0
- package/dist/{types-BdQ2lqfT.d.ts → types-DnU2LhXR.d.ts} +6 -0
- package/dist/webhooks.d.mts +22 -9
- package/dist/webhooks.d.ts +22 -9
- package/dist/webhooks.js +27 -12
- package/dist/webhooks.mjs +1 -1
- package/dist/ws.d.mts +2 -2
- package/dist/ws.d.ts +2 -2
- package/docs/guides/invitations.md +65 -0
- package/package.json +7 -2
package/dist/express.js
CHANGED
|
@@ -334,17 +334,27 @@ function parseLoginResponse(data, browserSessionMode) {
|
|
|
334
334
|
tenants: data.tenants
|
|
335
335
|
};
|
|
336
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
|
+
}
|
|
337
345
|
throw new Error("Unexpected login response shape");
|
|
338
346
|
}
|
|
339
347
|
var AuthModule = class {
|
|
340
348
|
constructor(http) {
|
|
341
349
|
this.http = http;
|
|
342
350
|
}
|
|
343
|
-
async login(email, password) {
|
|
351
|
+
async login(email, password, opts) {
|
|
352
|
+
const body = { email, password };
|
|
353
|
+
if (opts?.scopeHint) body.scopeHint = opts.scopeHint;
|
|
344
354
|
const data = await this.http.request(
|
|
345
355
|
"POST",
|
|
346
356
|
"/api/v1/auth/login",
|
|
347
|
-
|
|
357
|
+
body,
|
|
348
358
|
{ skipAutoRefresh: true }
|
|
349
359
|
);
|
|
350
360
|
return parseLoginResponse(data, this.http.isBrowserSession());
|
|
@@ -382,13 +392,29 @@ var AuthModule = class {
|
|
|
382
392
|
method
|
|
383
393
|
}, { skipAutoRefresh: true });
|
|
384
394
|
}
|
|
385
|
-
async selectTenant(tenantSelectionToken, tenantId) {
|
|
395
|
+
async selectTenant(tenantSelectionToken, tenantId, opts) {
|
|
396
|
+
const body = { tenantSelectionToken, tenantId };
|
|
397
|
+
if (opts?.scopeHint) body.scopeHint = opts.scopeHint;
|
|
386
398
|
const data = await this.http.request(
|
|
387
399
|
"POST",
|
|
388
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",
|
|
389
415
|
{
|
|
390
|
-
|
|
391
|
-
|
|
416
|
+
scopeSelectionToken,
|
|
417
|
+
membershipId
|
|
392
418
|
},
|
|
393
419
|
{ skipAutoRefresh: true }
|
|
394
420
|
);
|
|
@@ -2227,7 +2253,11 @@ async function buildUserinfoResponse(claims, opts = {}) {
|
|
|
2227
2253
|
tenantId: claims.tenantId,
|
|
2228
2254
|
vendorId: claims.vendorId,
|
|
2229
2255
|
roles: claims.roles ?? [],
|
|
2230
|
-
entitlements: claims.entitlements ?? []
|
|
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 } : {}
|
|
2231
2261
|
};
|
|
2232
2262
|
const enriched = opts.enrich ? await opts.enrich(claims) : null;
|
|
2233
2263
|
const user = enriched ? { ...baseUser, ...enriched } : baseUser;
|
|
@@ -2272,19 +2302,62 @@ function shouldClearCookiesOnFailure(policy, status, errorCode) {
|
|
|
2272
2302
|
}
|
|
2273
2303
|
var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
|
|
2274
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
|
+
}
|
|
2275
2332
|
function resolve(config) {
|
|
2276
2333
|
const parsed = assertPublishableKey(config.publishableKey, { context: "@iqauth/sdk helpers" });
|
|
2277
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
|
+
}
|
|
2278
2351
|
return {
|
|
2279
2352
|
publishableKey: config.publishableKey,
|
|
2280
2353
|
secretKey: config.secretKey,
|
|
2281
2354
|
issuer: (config.issuer ?? inferredIssuer).replace(/\/+$/, ""),
|
|
2282
|
-
accessCookieName
|
|
2283
|
-
refreshCookieName
|
|
2284
|
-
cookieDomain
|
|
2355
|
+
accessCookieName,
|
|
2356
|
+
refreshCookieName,
|
|
2357
|
+
cookieDomain,
|
|
2285
2358
|
sameSite: config.sameSite ?? "lax",
|
|
2286
|
-
secure
|
|
2287
|
-
cookiePath
|
|
2359
|
+
secure,
|
|
2360
|
+
cookiePath,
|
|
2288
2361
|
tokenPath: config.tokenPath ?? "/oidc/token",
|
|
2289
2362
|
refreshPath: config.refreshPath ?? "/api/v1/auth/refresh",
|
|
2290
2363
|
logoutPath: config.logoutPath ?? "/api/v1/auth/logout",
|
|
@@ -2297,9 +2370,19 @@ function resolve(config) {
|
|
|
2297
2370
|
debug: config.debug,
|
|
2298
2371
|
onTimingEvent: config.onTimingEvent,
|
|
2299
2372
|
signoutRegistry: config.signoutRegistry ?? defaultSignoutRegistry,
|
|
2300
|
-
signoutMarkerTtlMs: config.signoutMarkerTtlMs ?? DEFAULT_SIGNOUT_TTL_MS
|
|
2373
|
+
signoutMarkerTtlMs: config.signoutMarkerTtlMs ?? DEFAULT_SIGNOUT_TTL_MS,
|
|
2374
|
+
requireOAuthState: config.requireOAuthState ?? true,
|
|
2375
|
+
stateCookieName: config.stateCookieName ?? "iqauth_state"
|
|
2301
2376
|
};
|
|
2302
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
|
+
}
|
|
2303
2386
|
function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
|
|
2304
2387
|
return {
|
|
2305
2388
|
name,
|
|
@@ -2318,6 +2401,9 @@ function clearCookies(cfg) {
|
|
|
2318
2401
|
{ ...makeCookie(cfg, cfg.refreshCookieName, "", 0), clear: true }
|
|
2319
2402
|
];
|
|
2320
2403
|
}
|
|
2404
|
+
function clearStateCookie(cfg) {
|
|
2405
|
+
return { ...makeCookie(cfg, cfg.stateCookieName, "", 0, false), clear: true };
|
|
2406
|
+
}
|
|
2321
2407
|
var DEFAULT_SIGNOUT_TTL_MS = 6e4;
|
|
2322
2408
|
var inMemorySignoutMarkers = /* @__PURE__ */ new Map();
|
|
2323
2409
|
function pruneInMemoryMarkers(now) {
|
|
@@ -2343,6 +2429,15 @@ var defaultSignoutRegistry = {
|
|
|
2343
2429
|
return true;
|
|
2344
2430
|
}
|
|
2345
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
|
+
}
|
|
2346
2441
|
async function handleCallback(config, input) {
|
|
2347
2442
|
const cfg = resolve(config);
|
|
2348
2443
|
const t0 = Date.now();
|
|
@@ -2354,6 +2449,23 @@ async function handleCallback(config, input) {
|
|
|
2354
2449
|
cookies: []
|
|
2355
2450
|
};
|
|
2356
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
|
+
}
|
|
2357
2469
|
if (!cfg.secretKey) {
|
|
2358
2470
|
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "INTERNAL_ERROR" });
|
|
2359
2471
|
return {
|
|
@@ -2392,6 +2504,26 @@ async function handleCallback(config, input) {
|
|
|
2392
2504
|
cookies: []
|
|
2393
2505
|
};
|
|
2394
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
|
+
}
|
|
2395
2527
|
const cookies = [];
|
|
2396
2528
|
cookies.push(
|
|
2397
2529
|
makeCookie(cfg, cfg.accessCookieName, json.access_token, json.expires_in ?? ACCESS_TOKEN_TTL_SECONDS)
|
|
@@ -2399,6 +2531,7 @@ async function handleCallback(config, input) {
|
|
|
2399
2531
|
if (json.refresh_token) {
|
|
2400
2532
|
cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.refresh_token, REFRESH_TOKEN_TTL_SECONDS));
|
|
2401
2533
|
}
|
|
2534
|
+
cookies.push(clearStateCookie(cfg));
|
|
2402
2535
|
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: true });
|
|
2403
2536
|
return {
|
|
2404
2537
|
status: 200,
|
|
@@ -2520,7 +2653,10 @@ async function handleUserinfo(config, input) {
|
|
|
2520
2653
|
}
|
|
2521
2654
|
let claims;
|
|
2522
2655
|
try {
|
|
2523
|
-
claims = await getTokensFor(cfg.issuer).verify(input.accessToken,
|
|
2656
|
+
claims = await getTokensFor(cfg.issuer).verify(input.accessToken, {
|
|
2657
|
+
issuer: cfg.issuer,
|
|
2658
|
+
...config.verify
|
|
2659
|
+
});
|
|
2524
2660
|
} catch (err) {
|
|
2525
2661
|
const code = err instanceof IQAuthError ? err.code : err.code || "TOKEN_INVALID";
|
|
2526
2662
|
const message = err instanceof Error ? err.message : "Access token verification failed";
|
|
@@ -2540,6 +2676,42 @@ async function handleUserinfo(config, input) {
|
|
|
2540
2676
|
};
|
|
2541
2677
|
}
|
|
2542
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
|
+
}
|
|
2714
|
+
|
|
2543
2715
|
// src/express.ts
|
|
2544
2716
|
var PKCE_COOKIE = "iqauth_pkce";
|
|
2545
2717
|
var IDEMPOTENCY_HEADER = "x-iqauth-idempotency";
|
|
@@ -2655,6 +2827,11 @@ function applyHandlerResponse(res, hr) {
|
|
|
2655
2827
|
function readBody(req) {
|
|
2656
2828
|
return req.body && typeof req.body === "object" ? req.body : {};
|
|
2657
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
|
+
}
|
|
2658
2835
|
function readCookieFromReq(req, name) {
|
|
2659
2836
|
if (req.cookies && typeof req.cookies[name] === "string") return req.cookies[name];
|
|
2660
2837
|
const header = req.headers?.cookie;
|
|
@@ -2699,7 +2876,13 @@ function iqAuth(options) {
|
|
|
2699
2876
|
const hr = await handleCallback(helperConfig, {
|
|
2700
2877
|
code: body.code,
|
|
2701
2878
|
codeVerifier: body.codeVerifier,
|
|
2702
|
-
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")
|
|
2703
2886
|
});
|
|
2704
2887
|
applyHandlerResponse(res, hr);
|
|
2705
2888
|
});
|
|
@@ -2748,21 +2931,19 @@ function iqAuth(options) {
|
|
|
2748
2931
|
});
|
|
2749
2932
|
app.post(exchangePath, async (req, res) => {
|
|
2750
2933
|
const body = readBody(req);
|
|
2751
|
-
const stateFromBody = body.state || void 0;
|
|
2752
|
-
const stateFromCookie = readCookieFromReq(req, stateCookie);
|
|
2753
|
-
if (stateFromCookie && stateFromBody !== stateFromCookie) {
|
|
2754
|
-
clearCookie(res, stateCookie);
|
|
2755
|
-
res.status(400);
|
|
2756
|
-
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" } }));
|
|
2757
|
-
}
|
|
2758
2934
|
const hr = await handleCallback(helperConfig, {
|
|
2759
2935
|
code: body.code,
|
|
2760
2936
|
codeVerifier: body.codeVerifier || readCookieFromReq(req, PKCE_COOKIE) || "",
|
|
2761
|
-
redirectUri: body.redirectUri
|
|
2937
|
+
redirectUri: body.redirectUri,
|
|
2938
|
+
state: body.state,
|
|
2939
|
+
expectedState: readCookieFromReq(req, stateCookie)
|
|
2762
2940
|
});
|
|
2763
2941
|
clearCookie(res, stateCookie);
|
|
2764
2942
|
clearCookie(res, PKCE_COOKIE);
|
|
2765
|
-
const returnTo =
|
|
2943
|
+
const returnTo = sanitizeReturnTo(
|
|
2944
|
+
readCookieFromReq(req, returnToCookie) || hr.body?.returnTo,
|
|
2945
|
+
{ currentOrigin: requestOriginOf(req), fallback: "/" }
|
|
2946
|
+
);
|
|
2766
2947
|
if (hr.status < 400) clearCookie(res, returnToCookie);
|
|
2767
2948
|
const enriched = {
|
|
2768
2949
|
...hr,
|
|
@@ -2781,21 +2962,17 @@ function iqAuth(options) {
|
|
|
2781
2962
|
else res.end?.("Missing authorization code");
|
|
2782
2963
|
});
|
|
2783
2964
|
}
|
|
2784
|
-
const stateFromQuery = q.state;
|
|
2785
|
-
const stateFromCookie = readCookieFromReq(req, stateCookie);
|
|
2786
|
-
if (stateFromCookie && stateFromQuery !== stateFromCookie) {
|
|
2787
|
-
clearCookie(res, stateCookie);
|
|
2788
|
-
return failPlain(res, "state_mismatch", () => {
|
|
2789
|
-
res.status(400);
|
|
2790
|
-
if (res.json) res.json({ success: false, error: { code: "STATE_MISMATCH", message: "OAuth state mismatch" } });
|
|
2791
|
-
else res.end?.("OAuth state mismatch");
|
|
2792
|
-
});
|
|
2793
|
-
}
|
|
2794
2965
|
const codeVerifier = readCookieFromReq(req, PKCE_COOKIE) || "";
|
|
2795
2966
|
const proto = req.headers?.["x-forwarded-proto"] || req.protocol || "https";
|
|
2796
2967
|
const host = req.headers?.["x-forwarded-host"] || req.headers?.host || "";
|
|
2797
2968
|
const redirectUri = `${proto}://${host}${callbackPath}`;
|
|
2798
|
-
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
|
+
});
|
|
2799
2976
|
for (const c of hr.cookies) {
|
|
2800
2977
|
if (typeof res.cookie === "function") {
|
|
2801
2978
|
const opts = {
|
|
@@ -2812,14 +2989,18 @@ function iqAuth(options) {
|
|
|
2812
2989
|
clearCookie(res, stateCookie);
|
|
2813
2990
|
clearCookie(res, PKCE_COOKIE);
|
|
2814
2991
|
if (hr.status >= 400) {
|
|
2815
|
-
const
|
|
2992
|
+
const rawCode = hr.body?.error?.code || "exchange_failed";
|
|
2993
|
+
const code2 = rawCode === "STATE_MISMATCH" ? "state_mismatch" : rawCode;
|
|
2816
2994
|
return failPlain(res, code2, () => {
|
|
2817
2995
|
res.status(hr.status);
|
|
2818
2996
|
if (res.json) res.json(hr.body);
|
|
2819
2997
|
else res.end?.(JSON.stringify(hr.body));
|
|
2820
2998
|
});
|
|
2821
2999
|
}
|
|
2822
|
-
const returnTo =
|
|
3000
|
+
const returnTo = sanitizeReturnTo(
|
|
3001
|
+
readCookieFromReq(req, returnToCookie) || hr.body?.returnTo,
|
|
3002
|
+
{ currentOrigin: requestOriginOf(req), fallback: "/" }
|
|
3003
|
+
);
|
|
2823
3004
|
clearCookie(res, returnToCookie);
|
|
2824
3005
|
if (typeof res.redirect === "function") return res.redirect(302, returnTo);
|
|
2825
3006
|
res.status(302);
|
package/dist/express.mjs
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
|
+
import {
|
|
2
|
+
sanitizeReturnTo
|
|
3
|
+
} from "./chunk-JRDVUWAL.mjs";
|
|
1
4
|
import {
|
|
2
5
|
DEFAULT_REFRESH_COOKIE,
|
|
3
6
|
iqAuthMiddleware
|
|
4
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-25SSYDIP.mjs";
|
|
5
8
|
import {
|
|
6
9
|
handleCallback,
|
|
7
10
|
handleRefresh,
|
|
8
11
|
handleSignout,
|
|
9
12
|
handleUserinfo
|
|
10
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-WSH4SW7F.mjs";
|
|
11
14
|
import {
|
|
12
15
|
assertPublishableKey
|
|
13
16
|
} from "./chunk-HVHNYPDC.mjs";
|
|
14
17
|
import {
|
|
15
18
|
IQAuthClient
|
|
16
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-ZLJPABB7.mjs";
|
|
17
20
|
import "./chunk-NUO2I65G.mjs";
|
|
18
21
|
import {
|
|
19
22
|
ErrorCodes,
|
|
@@ -136,6 +139,11 @@ function applyHandlerResponse(res, hr) {
|
|
|
136
139
|
function readBody(req) {
|
|
137
140
|
return req.body && typeof req.body === "object" ? req.body : {};
|
|
138
141
|
}
|
|
142
|
+
function requestOriginOf(req) {
|
|
143
|
+
const proto = req.headers?.["x-forwarded-proto"]?.split(",")[0]?.trim() || (typeof req.protocol === "string" ? req.protocol : void 0) || "https";
|
|
144
|
+
const host = req.headers?.["x-forwarded-host"]?.split(",")[0]?.trim() || req.headers?.host || "";
|
|
145
|
+
return host ? `${proto}://${host}` : "";
|
|
146
|
+
}
|
|
139
147
|
function readCookieFromReq(req, name) {
|
|
140
148
|
if (req.cookies && typeof req.cookies[name] === "string") return req.cookies[name];
|
|
141
149
|
const header = req.headers?.cookie;
|
|
@@ -180,7 +188,13 @@ function iqAuth(options) {
|
|
|
180
188
|
const hr = await handleCallback(helperConfig, {
|
|
181
189
|
code: body.code,
|
|
182
190
|
codeVerifier: body.codeVerifier,
|
|
183
|
-
redirectUri: body.redirectUri
|
|
191
|
+
redirectUri: body.redirectUri,
|
|
192
|
+
// M-2: bind the callback to this browser. `state` is echoed back by the
|
|
193
|
+
// OAuth redirect (body); `expectedState` is the value the SDK published
|
|
194
|
+
// in a first-party cookie before redirect. handleCallback fails closed
|
|
195
|
+
// on mismatch/missing when requireOAuthState (default) is on.
|
|
196
|
+
state: body.state,
|
|
197
|
+
expectedState: readCookieFromReq(req, helperConfig.stateCookieName ?? "iqauth_state")
|
|
184
198
|
});
|
|
185
199
|
applyHandlerResponse(res, hr);
|
|
186
200
|
});
|
|
@@ -229,21 +243,19 @@ function iqAuth(options) {
|
|
|
229
243
|
});
|
|
230
244
|
app.post(exchangePath, async (req, res) => {
|
|
231
245
|
const body = readBody(req);
|
|
232
|
-
const stateFromBody = body.state || void 0;
|
|
233
|
-
const stateFromCookie = readCookieFromReq(req, stateCookie);
|
|
234
|
-
if (stateFromCookie && stateFromBody !== stateFromCookie) {
|
|
235
|
-
clearCookie(res, stateCookie);
|
|
236
|
-
res.status(400);
|
|
237
|
-
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" } }));
|
|
238
|
-
}
|
|
239
246
|
const hr = await handleCallback(helperConfig, {
|
|
240
247
|
code: body.code,
|
|
241
248
|
codeVerifier: body.codeVerifier || readCookieFromReq(req, PKCE_COOKIE) || "",
|
|
242
|
-
redirectUri: body.redirectUri
|
|
249
|
+
redirectUri: body.redirectUri,
|
|
250
|
+
state: body.state,
|
|
251
|
+
expectedState: readCookieFromReq(req, stateCookie)
|
|
243
252
|
});
|
|
244
253
|
clearCookie(res, stateCookie);
|
|
245
254
|
clearCookie(res, PKCE_COOKIE);
|
|
246
|
-
const returnTo =
|
|
255
|
+
const returnTo = sanitizeReturnTo(
|
|
256
|
+
readCookieFromReq(req, returnToCookie) || hr.body?.returnTo,
|
|
257
|
+
{ currentOrigin: requestOriginOf(req), fallback: "/" }
|
|
258
|
+
);
|
|
247
259
|
if (hr.status < 400) clearCookie(res, returnToCookie);
|
|
248
260
|
const enriched = {
|
|
249
261
|
...hr,
|
|
@@ -262,21 +274,17 @@ function iqAuth(options) {
|
|
|
262
274
|
else res.end?.("Missing authorization code");
|
|
263
275
|
});
|
|
264
276
|
}
|
|
265
|
-
const stateFromQuery = q.state;
|
|
266
|
-
const stateFromCookie = readCookieFromReq(req, stateCookie);
|
|
267
|
-
if (stateFromCookie && stateFromQuery !== stateFromCookie) {
|
|
268
|
-
clearCookie(res, stateCookie);
|
|
269
|
-
return failPlain(res, "state_mismatch", () => {
|
|
270
|
-
res.status(400);
|
|
271
|
-
if (res.json) res.json({ success: false, error: { code: "STATE_MISMATCH", message: "OAuth state mismatch" } });
|
|
272
|
-
else res.end?.("OAuth state mismatch");
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
277
|
const codeVerifier = readCookieFromReq(req, PKCE_COOKIE) || "";
|
|
276
278
|
const proto = req.headers?.["x-forwarded-proto"] || req.protocol || "https";
|
|
277
279
|
const host = req.headers?.["x-forwarded-host"] || req.headers?.host || "";
|
|
278
280
|
const redirectUri = `${proto}://${host}${callbackPath}`;
|
|
279
|
-
const hr = await handleCallback(helperConfig, {
|
|
281
|
+
const hr = await handleCallback(helperConfig, {
|
|
282
|
+
code,
|
|
283
|
+
codeVerifier,
|
|
284
|
+
redirectUri,
|
|
285
|
+
state: q.state,
|
|
286
|
+
expectedState: readCookieFromReq(req, stateCookie)
|
|
287
|
+
});
|
|
280
288
|
for (const c of hr.cookies) {
|
|
281
289
|
if (typeof res.cookie === "function") {
|
|
282
290
|
const opts = {
|
|
@@ -293,14 +301,18 @@ function iqAuth(options) {
|
|
|
293
301
|
clearCookie(res, stateCookie);
|
|
294
302
|
clearCookie(res, PKCE_COOKIE);
|
|
295
303
|
if (hr.status >= 400) {
|
|
296
|
-
const
|
|
304
|
+
const rawCode = hr.body?.error?.code || "exchange_failed";
|
|
305
|
+
const code2 = rawCode === "STATE_MISMATCH" ? "state_mismatch" : rawCode;
|
|
297
306
|
return failPlain(res, code2, () => {
|
|
298
307
|
res.status(hr.status);
|
|
299
308
|
if (res.json) res.json(hr.body);
|
|
300
309
|
else res.end?.(JSON.stringify(hr.body));
|
|
301
310
|
});
|
|
302
311
|
}
|
|
303
|
-
const returnTo =
|
|
312
|
+
const returnTo = sanitizeReturnTo(
|
|
313
|
+
readCookieFromReq(req, returnToCookie) || hr.body?.returnTo,
|
|
314
|
+
{ currentOrigin: requestOriginOf(req), fallback: "/" }
|
|
315
|
+
);
|
|
304
316
|
clearCookie(res, returnToCookie);
|
|
305
317
|
if (typeof res.redirect === "function") return res.redirect(302, returnTo);
|
|
306
318
|
res.status(302);
|
package/dist/fastify.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { IQAuthHelperConfig } from './server/handlers.mjs';
|
|
2
|
-
import './tokens-
|
|
3
|
-
import './types-
|
|
2
|
+
import './tokens-B06VtvUi.mjs';
|
|
3
|
+
import './types-Bn8O-OEd.mjs';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @iqauth/sdk/fastify — Fastify adapter.
|
|
@@ -17,6 +17,14 @@ import './types-XOV9XPVi.mjs';
|
|
|
17
17
|
interface IQAuthFastifyOptions extends IQAuthHelperConfig {
|
|
18
18
|
mountPath?: string;
|
|
19
19
|
mountHelperRoutes?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Cookie name the browser SDK publishes the post-login destination into
|
|
22
|
+
* before redirect. The callback handler reads it, surfaces it as `returnTo`
|
|
23
|
+
* in the JSON response body after a successful exchange, and clears it on
|
|
24
|
+
* success. Mirrors the express inline-callback adapter. Defaults to
|
|
25
|
+
* `iqauth_return_to`.
|
|
26
|
+
*/
|
|
27
|
+
returnToCookieName?: string;
|
|
20
28
|
/** Routes that bypass verification (e.g. health checks). */
|
|
21
29
|
publicPaths?: string[] | ((path: string) => boolean);
|
|
22
30
|
/** Override token verification options (issuer / audience / clock tolerance). */
|
package/dist/fastify.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { IQAuthHelperConfig } from './server/handlers.js';
|
|
2
|
-
import './tokens-
|
|
3
|
-
import './types-
|
|
2
|
+
import './tokens-9F6ETrzk.js';
|
|
3
|
+
import './types-Bn8O-OEd.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @iqauth/sdk/fastify — Fastify adapter.
|
|
@@ -17,6 +17,14 @@ import './types-XOV9XPVi.js';
|
|
|
17
17
|
interface IQAuthFastifyOptions extends IQAuthHelperConfig {
|
|
18
18
|
mountPath?: string;
|
|
19
19
|
mountHelperRoutes?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Cookie name the browser SDK publishes the post-login destination into
|
|
22
|
+
* before redirect. The callback handler reads it, surfaces it as `returnTo`
|
|
23
|
+
* in the JSON response body after a successful exchange, and clears it on
|
|
24
|
+
* success. Mirrors the express inline-callback adapter. Defaults to
|
|
25
|
+
* `iqauth_return_to`.
|
|
26
|
+
*/
|
|
27
|
+
returnToCookieName?: string;
|
|
20
28
|
/** Routes that bypass verification (e.g. health checks). */
|
|
21
29
|
publicPaths?: string[] | ((path: string) => boolean);
|
|
22
30
|
/** Override token verification options (issuer / audience / clock tolerance). */
|