@iqauth/sdk 2.0.1 → 2.0.3
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.d.mts +1 -1
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +37 -2
- package/dist/browser.mjs +1 -1
- package/dist/{chunk-5HF3OBNO.mjs → chunk-JQRTY5MY.mjs} +29 -5
- package/dist/{chunk-YDO2RDWQ.mjs → chunk-S3M2IXCE.mjs} +37 -2
- package/dist/express.js +29 -5
- package/dist/express.mjs +1 -1
- package/dist/fastify.js +29 -5
- package/dist/fastify.mjs +1 -1
- package/dist/hono.js +29 -5
- package/dist/hono.mjs +1 -1
- package/dist/next.js +30 -6
- package/dist/next.mjs +2 -2
- package/dist/react.d.mts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +37 -2
- package/dist/react.mjs +1 -1
- package/dist/server/handlers.d.mts +29 -2
- package/dist/server/handlers.d.ts +29 -2
- package/dist/server/handlers.js +29 -5
- package/dist/server/handlers.mjs +1 -1
- package/dist/server.js +29 -5
- package/dist/server.mjs +1 -1
- package/dist/{signIn-C8f6qVjD.d.mts → signIn-CEMdUAwd.d.mts} +22 -0
- package/dist/{signIn-Cy2lbEXb.d.ts → signIn-VRNzlNyG.d.ts} +22 -0
- package/docs/BROWSER_SESSION_MIGRATION.md +34 -0
- package/package.json +1 -1
package/dist/browser.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { C as CallbackResult, S as SessionManager, d as SessionManagerOptions, a as SessionSnapshot, e as SessionStatus, b as SignInOptions, c as SignOutOptions, f as buildSignInUrl, h as handleAuthCallback, r as redirectToSignIn, s as signIn, g as signOut } from './signIn-
|
|
1
|
+
export { C as CallbackResult, S as SessionManager, d as SessionManagerOptions, a as SessionSnapshot, e as SessionStatus, b as SignInOptions, c as SignOutOptions, f as buildSignInUrl, h as handleAuthCallback, r as redirectToSignIn, s as signIn, g as signOut } from './signIn-CEMdUAwd.mjs';
|
|
2
2
|
export { K as KeyMode, b as ParsedPublishableKey, P as PublishableKeyPayload, e as encodePublishableKey, i as isPublishableKey, a as isSecretKey, p as parsePublishableKey } from './publishableKey-B5DIK81A.mjs';
|
|
3
3
|
export { a as ErrorCode, E as ErrorCodes, I as IQAuthError } from './errors-CDdl24MP.mjs';
|
|
4
4
|
import './types-Cxl3bQHt.mjs';
|
package/dist/browser.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { C as CallbackResult, S as SessionManager, d as SessionManagerOptions, a as SessionSnapshot, e as SessionStatus, b as SignInOptions, c as SignOutOptions, f as buildSignInUrl, h as handleAuthCallback, r as redirectToSignIn, s as signIn, g as signOut } from './signIn-
|
|
1
|
+
export { C as CallbackResult, S as SessionManager, d as SessionManagerOptions, a as SessionSnapshot, e as SessionStatus, b as SignInOptions, c as SignOutOptions, f as buildSignInUrl, h as handleAuthCallback, r as redirectToSignIn, s as signIn, g as signOut } from './signIn-VRNzlNyG.js';
|
|
2
2
|
export { K as KeyMode, b as ParsedPublishableKey, P as PublishableKeyPayload, e as encodePublishableKey, i as isPublishableKey, a as isSecretKey, p as parsePublishableKey } from './publishableKey-B5DIK81A.js';
|
|
3
3
|
export { a as ErrorCode, E as ErrorCodes, I as IQAuthError } from './errors-CDdl24MP.js';
|
|
4
4
|
import './types-Cxl3bQHt.js';
|
package/dist/browser.js
CHANGED
|
@@ -270,8 +270,9 @@ var SessionManager = class {
|
|
|
270
270
|
this.refreshPath = options.refreshPath ?? DEFAULT_REFRESH_PATH;
|
|
271
271
|
this.userinfoPath = options.userinfoPath ?? DEFAULT_USERINFO_PATH;
|
|
272
272
|
this.useCookies = options.useCookies ?? true;
|
|
273
|
-
this.
|
|
274
|
-
this.
|
|
273
|
+
this.serverManagedSession = options.serverManagedSession ?? false;
|
|
274
|
+
this.proactiveRefresh = this.serverManagedSession ? false : options.proactiveRefresh ?? true;
|
|
275
|
+
this.tokenStore = options.tokenStore ?? (this.serverManagedSession ? NO_OP_STORE : this.useCookies ? defaultCookieStore() : NO_OP_STORE);
|
|
275
276
|
this.crossTabLockTimeoutMs = options.crossTabLockTimeoutMs ?? 4e3;
|
|
276
277
|
this.fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : (() => {
|
|
277
278
|
throw new Error("global fetch is not available; pass fetchImpl");
|
|
@@ -315,6 +316,40 @@ var SessionManager = class {
|
|
|
315
316
|
async bootstrap() {
|
|
316
317
|
if (this.bootstrapped) return;
|
|
317
318
|
this.bootstrapped = true;
|
|
319
|
+
if (this.serverManagedSession) {
|
|
320
|
+
try {
|
|
321
|
+
const res = await this.fetchImpl(`${this.issuer}${this.userinfoPath}`, {
|
|
322
|
+
method: "GET",
|
|
323
|
+
credentials: "include",
|
|
324
|
+
headers: { Accept: "application/json" }
|
|
325
|
+
});
|
|
326
|
+
if (!res.ok) {
|
|
327
|
+
this.setStatus("unauthenticated");
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const body = await res.json().catch(() => ({}));
|
|
331
|
+
const data = body?.data;
|
|
332
|
+
const user = data?.user ?? claimsToSessionUser(data?.claims ?? null);
|
|
333
|
+
if (!user) {
|
|
334
|
+
this.setStatus("unauthenticated");
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
this.update({
|
|
338
|
+
status: "authenticated",
|
|
339
|
+
accessToken: null,
|
|
340
|
+
user,
|
|
341
|
+
claims: data?.claims ?? null,
|
|
342
|
+
tenantId: data?.tenantId ?? user.tenantId ?? this.key.tenantId,
|
|
343
|
+
error: null,
|
|
344
|
+
version: this.snapshot.version + 1
|
|
345
|
+
});
|
|
346
|
+
this.broadcast("session:update");
|
|
347
|
+
return;
|
|
348
|
+
} catch {
|
|
349
|
+
this.setStatus("unauthenticated");
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
318
353
|
const stored = await Promise.resolve(this.tokenStore.read());
|
|
319
354
|
if (!stored) {
|
|
320
355
|
this.setStatus("unauthenticated");
|
package/dist/browser.mjs
CHANGED
|
@@ -3,6 +3,22 @@ import {
|
|
|
3
3
|
} from "./chunk-5WFR6Y33.mjs";
|
|
4
4
|
|
|
5
5
|
// src/server/handlers.ts
|
|
6
|
+
var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
7
|
+
"TOKEN_REVOKED",
|
|
8
|
+
"SESSION_REVOKED",
|
|
9
|
+
"INVALID_GRANT",
|
|
10
|
+
"invalid_grant",
|
|
11
|
+
"USER_DEACTIVATED",
|
|
12
|
+
"USER_DISABLED",
|
|
13
|
+
"TENANT_SUSPENDED"
|
|
14
|
+
]);
|
|
15
|
+
function shouldClearCookiesOnFailure(policy, status, errorCode) {
|
|
16
|
+
if (policy === "always") return true;
|
|
17
|
+
if (policy === "never") return false;
|
|
18
|
+
if (status === 410) return true;
|
|
19
|
+
if (errorCode && TERMINAL_REFRESH_ERROR_CODES.has(errorCode)) return true;
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
6
22
|
var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
|
|
7
23
|
var REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
|
|
8
24
|
function resolve(config) {
|
|
@@ -30,7 +46,8 @@ function resolve(config) {
|
|
|
30
46
|
throw new Error("global fetch is unavailable; pass fetchImpl");
|
|
31
47
|
})),
|
|
32
48
|
appId: parsed.appId,
|
|
33
|
-
tenantId: parsed.tenantId
|
|
49
|
+
tenantId: parsed.tenantId,
|
|
50
|
+
clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only"
|
|
34
51
|
};
|
|
35
52
|
}
|
|
36
53
|
function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
|
|
@@ -126,7 +143,7 @@ async function handleRefresh(config, input) {
|
|
|
126
143
|
return {
|
|
127
144
|
status: 401,
|
|
128
145
|
body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing refresh token" } },
|
|
129
|
-
cookies: clearCookies(cfg)
|
|
146
|
+
cookies: cfg.clearCookiesOnRefreshFailure === "always" ? clearCookies(cfg) : []
|
|
130
147
|
};
|
|
131
148
|
}
|
|
132
149
|
const res = await cfg.fetchImpl(`${cfg.issuer}${cfg.refreshPath}`, {
|
|
@@ -136,16 +153,23 @@ async function handleRefresh(config, input) {
|
|
|
136
153
|
});
|
|
137
154
|
const json = await res.json().catch(() => ({}));
|
|
138
155
|
if (!res.ok || !json.success || !json.data?.accessToken) {
|
|
156
|
+
const status = res.status || 401;
|
|
157
|
+
const errorCode = json.error?.code || "TOKEN_INVALID";
|
|
158
|
+
const shouldClear = shouldClearCookiesOnFailure(
|
|
159
|
+
cfg.clearCookiesOnRefreshFailure,
|
|
160
|
+
status,
|
|
161
|
+
errorCode
|
|
162
|
+
);
|
|
139
163
|
return {
|
|
140
|
-
status
|
|
164
|
+
status,
|
|
141
165
|
body: {
|
|
142
166
|
success: false,
|
|
143
167
|
error: {
|
|
144
|
-
code:
|
|
168
|
+
code: errorCode,
|
|
145
169
|
message: json.error?.message || "Refresh failed"
|
|
146
170
|
}
|
|
147
171
|
},
|
|
148
|
-
cookies: clearCookies(cfg)
|
|
172
|
+
cookies: shouldClear ? clearCookies(cfg) : []
|
|
149
173
|
};
|
|
150
174
|
}
|
|
151
175
|
const cookies = [
|
|
@@ -137,8 +137,9 @@ var SessionManager = class {
|
|
|
137
137
|
this.refreshPath = options.refreshPath ?? DEFAULT_REFRESH_PATH;
|
|
138
138
|
this.userinfoPath = options.userinfoPath ?? DEFAULT_USERINFO_PATH;
|
|
139
139
|
this.useCookies = options.useCookies ?? true;
|
|
140
|
-
this.
|
|
141
|
-
this.
|
|
140
|
+
this.serverManagedSession = options.serverManagedSession ?? false;
|
|
141
|
+
this.proactiveRefresh = this.serverManagedSession ? false : options.proactiveRefresh ?? true;
|
|
142
|
+
this.tokenStore = options.tokenStore ?? (this.serverManagedSession ? NO_OP_STORE : this.useCookies ? defaultCookieStore() : NO_OP_STORE);
|
|
142
143
|
this.crossTabLockTimeoutMs = options.crossTabLockTimeoutMs ?? 4e3;
|
|
143
144
|
this.fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : (() => {
|
|
144
145
|
throw new Error("global fetch is not available; pass fetchImpl");
|
|
@@ -182,6 +183,40 @@ var SessionManager = class {
|
|
|
182
183
|
async bootstrap() {
|
|
183
184
|
if (this.bootstrapped) return;
|
|
184
185
|
this.bootstrapped = true;
|
|
186
|
+
if (this.serverManagedSession) {
|
|
187
|
+
try {
|
|
188
|
+
const res = await this.fetchImpl(`${this.issuer}${this.userinfoPath}`, {
|
|
189
|
+
method: "GET",
|
|
190
|
+
credentials: "include",
|
|
191
|
+
headers: { Accept: "application/json" }
|
|
192
|
+
});
|
|
193
|
+
if (!res.ok) {
|
|
194
|
+
this.setStatus("unauthenticated");
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const body = await res.json().catch(() => ({}));
|
|
198
|
+
const data = body?.data;
|
|
199
|
+
const user = data?.user ?? claimsToSessionUser(data?.claims ?? null);
|
|
200
|
+
if (!user) {
|
|
201
|
+
this.setStatus("unauthenticated");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
this.update({
|
|
205
|
+
status: "authenticated",
|
|
206
|
+
accessToken: null,
|
|
207
|
+
user,
|
|
208
|
+
claims: data?.claims ?? null,
|
|
209
|
+
tenantId: data?.tenantId ?? user.tenantId ?? this.key.tenantId,
|
|
210
|
+
error: null,
|
|
211
|
+
version: this.snapshot.version + 1
|
|
212
|
+
});
|
|
213
|
+
this.broadcast("session:update");
|
|
214
|
+
return;
|
|
215
|
+
} catch {
|
|
216
|
+
this.setStatus("unauthenticated");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
185
220
|
const stored = await Promise.resolve(this.tokenStore.read());
|
|
186
221
|
if (!stored) {
|
|
187
222
|
this.setStatus("unauthenticated");
|
package/dist/express.js
CHANGED
|
@@ -1980,6 +1980,22 @@ function iqAuthMiddleware(clientOrOptions, options = {}) {
|
|
|
1980
1980
|
}
|
|
1981
1981
|
|
|
1982
1982
|
// src/server/handlers.ts
|
|
1983
|
+
var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
1984
|
+
"TOKEN_REVOKED",
|
|
1985
|
+
"SESSION_REVOKED",
|
|
1986
|
+
"INVALID_GRANT",
|
|
1987
|
+
"invalid_grant",
|
|
1988
|
+
"USER_DEACTIVATED",
|
|
1989
|
+
"USER_DISABLED",
|
|
1990
|
+
"TENANT_SUSPENDED"
|
|
1991
|
+
]);
|
|
1992
|
+
function shouldClearCookiesOnFailure(policy, status, errorCode) {
|
|
1993
|
+
if (policy === "always") return true;
|
|
1994
|
+
if (policy === "never") return false;
|
|
1995
|
+
if (status === 410) return true;
|
|
1996
|
+
if (errorCode && TERMINAL_REFRESH_ERROR_CODES.has(errorCode)) return true;
|
|
1997
|
+
return false;
|
|
1998
|
+
}
|
|
1983
1999
|
var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
|
|
1984
2000
|
var REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
|
|
1985
2001
|
function resolve(config) {
|
|
@@ -2007,7 +2023,8 @@ function resolve(config) {
|
|
|
2007
2023
|
throw new Error("global fetch is unavailable; pass fetchImpl");
|
|
2008
2024
|
})),
|
|
2009
2025
|
appId: parsed.appId,
|
|
2010
|
-
tenantId: parsed.tenantId
|
|
2026
|
+
tenantId: parsed.tenantId,
|
|
2027
|
+
clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only"
|
|
2011
2028
|
};
|
|
2012
2029
|
}
|
|
2013
2030
|
function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
|
|
@@ -2093,7 +2110,7 @@ async function handleRefresh(config, input) {
|
|
|
2093
2110
|
return {
|
|
2094
2111
|
status: 401,
|
|
2095
2112
|
body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing refresh token" } },
|
|
2096
|
-
cookies: clearCookies(cfg)
|
|
2113
|
+
cookies: cfg.clearCookiesOnRefreshFailure === "always" ? clearCookies(cfg) : []
|
|
2097
2114
|
};
|
|
2098
2115
|
}
|
|
2099
2116
|
const res = await cfg.fetchImpl(`${cfg.issuer}${cfg.refreshPath}`, {
|
|
@@ -2103,16 +2120,23 @@ async function handleRefresh(config, input) {
|
|
|
2103
2120
|
});
|
|
2104
2121
|
const json = await res.json().catch(() => ({}));
|
|
2105
2122
|
if (!res.ok || !json.success || !json.data?.accessToken) {
|
|
2123
|
+
const status = res.status || 401;
|
|
2124
|
+
const errorCode = json.error?.code || "TOKEN_INVALID";
|
|
2125
|
+
const shouldClear = shouldClearCookiesOnFailure(
|
|
2126
|
+
cfg.clearCookiesOnRefreshFailure,
|
|
2127
|
+
status,
|
|
2128
|
+
errorCode
|
|
2129
|
+
);
|
|
2106
2130
|
return {
|
|
2107
|
-
status
|
|
2131
|
+
status,
|
|
2108
2132
|
body: {
|
|
2109
2133
|
success: false,
|
|
2110
2134
|
error: {
|
|
2111
|
-
code:
|
|
2135
|
+
code: errorCode,
|
|
2112
2136
|
message: json.error?.message || "Refresh failed"
|
|
2113
2137
|
}
|
|
2114
2138
|
},
|
|
2115
|
-
cookies: clearCookies(cfg)
|
|
2139
|
+
cookies: shouldClear ? clearCookies(cfg) : []
|
|
2116
2140
|
};
|
|
2117
2141
|
}
|
|
2118
2142
|
const cookies = [
|
package/dist/express.mjs
CHANGED
package/dist/fastify.js
CHANGED
|
@@ -1781,6 +1781,22 @@ function parsePublishableKey(raw) {
|
|
|
1781
1781
|
}
|
|
1782
1782
|
|
|
1783
1783
|
// src/server/handlers.ts
|
|
1784
|
+
var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
1785
|
+
"TOKEN_REVOKED",
|
|
1786
|
+
"SESSION_REVOKED",
|
|
1787
|
+
"INVALID_GRANT",
|
|
1788
|
+
"invalid_grant",
|
|
1789
|
+
"USER_DEACTIVATED",
|
|
1790
|
+
"USER_DISABLED",
|
|
1791
|
+
"TENANT_SUSPENDED"
|
|
1792
|
+
]);
|
|
1793
|
+
function shouldClearCookiesOnFailure(policy, status, errorCode) {
|
|
1794
|
+
if (policy === "always") return true;
|
|
1795
|
+
if (policy === "never") return false;
|
|
1796
|
+
if (status === 410) return true;
|
|
1797
|
+
if (errorCode && TERMINAL_REFRESH_ERROR_CODES.has(errorCode)) return true;
|
|
1798
|
+
return false;
|
|
1799
|
+
}
|
|
1784
1800
|
var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
|
|
1785
1801
|
var REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
|
|
1786
1802
|
function resolve(config) {
|
|
@@ -1808,7 +1824,8 @@ function resolve(config) {
|
|
|
1808
1824
|
throw new Error("global fetch is unavailable; pass fetchImpl");
|
|
1809
1825
|
})),
|
|
1810
1826
|
appId: parsed.appId,
|
|
1811
|
-
tenantId: parsed.tenantId
|
|
1827
|
+
tenantId: parsed.tenantId,
|
|
1828
|
+
clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only"
|
|
1812
1829
|
};
|
|
1813
1830
|
}
|
|
1814
1831
|
function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
|
|
@@ -1904,7 +1921,7 @@ async function handleRefresh(config, input) {
|
|
|
1904
1921
|
return {
|
|
1905
1922
|
status: 401,
|
|
1906
1923
|
body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing refresh token" } },
|
|
1907
|
-
cookies: clearCookies(cfg)
|
|
1924
|
+
cookies: cfg.clearCookiesOnRefreshFailure === "always" ? clearCookies(cfg) : []
|
|
1908
1925
|
};
|
|
1909
1926
|
}
|
|
1910
1927
|
const res = await cfg.fetchImpl(`${cfg.issuer}${cfg.refreshPath}`, {
|
|
@@ -1914,16 +1931,23 @@ async function handleRefresh(config, input) {
|
|
|
1914
1931
|
});
|
|
1915
1932
|
const json = await res.json().catch(() => ({}));
|
|
1916
1933
|
if (!res.ok || !json.success || !json.data?.accessToken) {
|
|
1934
|
+
const status = res.status || 401;
|
|
1935
|
+
const errorCode = json.error?.code || "TOKEN_INVALID";
|
|
1936
|
+
const shouldClear = shouldClearCookiesOnFailure(
|
|
1937
|
+
cfg.clearCookiesOnRefreshFailure,
|
|
1938
|
+
status,
|
|
1939
|
+
errorCode
|
|
1940
|
+
);
|
|
1917
1941
|
return {
|
|
1918
|
-
status
|
|
1942
|
+
status,
|
|
1919
1943
|
body: {
|
|
1920
1944
|
success: false,
|
|
1921
1945
|
error: {
|
|
1922
|
-
code:
|
|
1946
|
+
code: errorCode,
|
|
1923
1947
|
message: json.error?.message || "Refresh failed"
|
|
1924
1948
|
}
|
|
1925
1949
|
},
|
|
1926
|
-
cookies: clearCookies(cfg)
|
|
1950
|
+
cookies: shouldClear ? clearCookies(cfg) : []
|
|
1927
1951
|
};
|
|
1928
1952
|
}
|
|
1929
1953
|
const cookies = [
|
package/dist/fastify.mjs
CHANGED
package/dist/hono.js
CHANGED
|
@@ -1780,6 +1780,22 @@ function parsePublishableKey(raw) {
|
|
|
1780
1780
|
}
|
|
1781
1781
|
|
|
1782
1782
|
// src/server/handlers.ts
|
|
1783
|
+
var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
1784
|
+
"TOKEN_REVOKED",
|
|
1785
|
+
"SESSION_REVOKED",
|
|
1786
|
+
"INVALID_GRANT",
|
|
1787
|
+
"invalid_grant",
|
|
1788
|
+
"USER_DEACTIVATED",
|
|
1789
|
+
"USER_DISABLED",
|
|
1790
|
+
"TENANT_SUSPENDED"
|
|
1791
|
+
]);
|
|
1792
|
+
function shouldClearCookiesOnFailure(policy, status, errorCode) {
|
|
1793
|
+
if (policy === "always") return true;
|
|
1794
|
+
if (policy === "never") return false;
|
|
1795
|
+
if (status === 410) return true;
|
|
1796
|
+
if (errorCode && TERMINAL_REFRESH_ERROR_CODES.has(errorCode)) return true;
|
|
1797
|
+
return false;
|
|
1798
|
+
}
|
|
1783
1799
|
var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
|
|
1784
1800
|
var REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
|
|
1785
1801
|
function resolve(config) {
|
|
@@ -1807,7 +1823,8 @@ function resolve(config) {
|
|
|
1807
1823
|
throw new Error("global fetch is unavailable; pass fetchImpl");
|
|
1808
1824
|
})),
|
|
1809
1825
|
appId: parsed.appId,
|
|
1810
|
-
tenantId: parsed.tenantId
|
|
1826
|
+
tenantId: parsed.tenantId,
|
|
1827
|
+
clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only"
|
|
1811
1828
|
};
|
|
1812
1829
|
}
|
|
1813
1830
|
function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
|
|
@@ -1903,7 +1920,7 @@ async function handleRefresh(config, input) {
|
|
|
1903
1920
|
return {
|
|
1904
1921
|
status: 401,
|
|
1905
1922
|
body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing refresh token" } },
|
|
1906
|
-
cookies: clearCookies(cfg)
|
|
1923
|
+
cookies: cfg.clearCookiesOnRefreshFailure === "always" ? clearCookies(cfg) : []
|
|
1907
1924
|
};
|
|
1908
1925
|
}
|
|
1909
1926
|
const res = await cfg.fetchImpl(`${cfg.issuer}${cfg.refreshPath}`, {
|
|
@@ -1913,16 +1930,23 @@ async function handleRefresh(config, input) {
|
|
|
1913
1930
|
});
|
|
1914
1931
|
const json = await res.json().catch(() => ({}));
|
|
1915
1932
|
if (!res.ok || !json.success || !json.data?.accessToken) {
|
|
1933
|
+
const status = res.status || 401;
|
|
1934
|
+
const errorCode = json.error?.code || "TOKEN_INVALID";
|
|
1935
|
+
const shouldClear = shouldClearCookiesOnFailure(
|
|
1936
|
+
cfg.clearCookiesOnRefreshFailure,
|
|
1937
|
+
status,
|
|
1938
|
+
errorCode
|
|
1939
|
+
);
|
|
1916
1940
|
return {
|
|
1917
|
-
status
|
|
1941
|
+
status,
|
|
1918
1942
|
body: {
|
|
1919
1943
|
success: false,
|
|
1920
1944
|
error: {
|
|
1921
|
-
code:
|
|
1945
|
+
code: errorCode,
|
|
1922
1946
|
message: json.error?.message || "Refresh failed"
|
|
1923
1947
|
}
|
|
1924
1948
|
},
|
|
1925
|
-
cookies: clearCookies(cfg)
|
|
1949
|
+
cookies: shouldClear ? clearCookies(cfg) : []
|
|
1926
1950
|
};
|
|
1927
1951
|
}
|
|
1928
1952
|
const cookies = [
|
package/dist/hono.mjs
CHANGED
package/dist/next.js
CHANGED
|
@@ -66,6 +66,22 @@ function parsePublishableKey(raw) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
// src/server/handlers.ts
|
|
69
|
+
var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
70
|
+
"TOKEN_REVOKED",
|
|
71
|
+
"SESSION_REVOKED",
|
|
72
|
+
"INVALID_GRANT",
|
|
73
|
+
"invalid_grant",
|
|
74
|
+
"USER_DEACTIVATED",
|
|
75
|
+
"USER_DISABLED",
|
|
76
|
+
"TENANT_SUSPENDED"
|
|
77
|
+
]);
|
|
78
|
+
function shouldClearCookiesOnFailure(policy, status, errorCode) {
|
|
79
|
+
if (policy === "always") return true;
|
|
80
|
+
if (policy === "never") return false;
|
|
81
|
+
if (status === 410) return true;
|
|
82
|
+
if (errorCode && TERMINAL_REFRESH_ERROR_CODES.has(errorCode)) return true;
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
69
85
|
var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
|
|
70
86
|
var REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
|
|
71
87
|
function resolve(config) {
|
|
@@ -93,7 +109,8 @@ function resolve(config) {
|
|
|
93
109
|
throw new Error("global fetch is unavailable; pass fetchImpl");
|
|
94
110
|
})),
|
|
95
111
|
appId: parsed.appId,
|
|
96
|
-
tenantId: parsed.tenantId
|
|
112
|
+
tenantId: parsed.tenantId,
|
|
113
|
+
clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only"
|
|
97
114
|
};
|
|
98
115
|
}
|
|
99
116
|
function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
|
|
@@ -189,7 +206,7 @@ async function handleRefresh(config, input) {
|
|
|
189
206
|
return {
|
|
190
207
|
status: 401,
|
|
191
208
|
body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing refresh token" } },
|
|
192
|
-
cookies: clearCookies(cfg)
|
|
209
|
+
cookies: cfg.clearCookiesOnRefreshFailure === "always" ? clearCookies(cfg) : []
|
|
193
210
|
};
|
|
194
211
|
}
|
|
195
212
|
const res = await cfg.fetchImpl(`${cfg.issuer}${cfg.refreshPath}`, {
|
|
@@ -199,16 +216,23 @@ async function handleRefresh(config, input) {
|
|
|
199
216
|
});
|
|
200
217
|
const json = await res.json().catch(() => ({}));
|
|
201
218
|
if (!res.ok || !json.success || !json.data?.accessToken) {
|
|
219
|
+
const status = res.status || 401;
|
|
220
|
+
const errorCode = json.error?.code || "TOKEN_INVALID";
|
|
221
|
+
const shouldClear = shouldClearCookiesOnFailure(
|
|
222
|
+
cfg.clearCookiesOnRefreshFailure,
|
|
223
|
+
status,
|
|
224
|
+
errorCode
|
|
225
|
+
);
|
|
202
226
|
return {
|
|
203
|
-
status
|
|
227
|
+
status,
|
|
204
228
|
body: {
|
|
205
229
|
success: false,
|
|
206
230
|
error: {
|
|
207
|
-
code:
|
|
231
|
+
code: errorCode,
|
|
208
232
|
message: json.error?.message || "Refresh failed"
|
|
209
233
|
}
|
|
210
234
|
},
|
|
211
|
-
cookies: clearCookies(cfg)
|
|
235
|
+
cookies: shouldClear ? clearCookies(cfg) : []
|
|
212
236
|
};
|
|
213
237
|
}
|
|
214
238
|
const cookies = [
|
|
@@ -2057,7 +2081,7 @@ async function getAuth(options) {
|
|
|
2057
2081
|
/* webpackIgnore: true */
|
|
2058
2082
|
specifier
|
|
2059
2083
|
);
|
|
2060
|
-
cookieJar = mod.cookies ? mod.cookies() : null;
|
|
2084
|
+
cookieJar = mod.cookies ? await mod.cookies() : null;
|
|
2061
2085
|
} catch {
|
|
2062
2086
|
cookieJar = null;
|
|
2063
2087
|
}
|
package/dist/next.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
handleRefresh,
|
|
4
4
|
handleSignout,
|
|
5
5
|
serializeCookie
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-JQRTY5MY.mjs";
|
|
7
7
|
import {
|
|
8
8
|
parsePublishableKey
|
|
9
9
|
} from "./chunk-5WFR6Y33.mjs";
|
|
@@ -110,7 +110,7 @@ async function getAuth(options) {
|
|
|
110
110
|
/* webpackIgnore: true */
|
|
111
111
|
specifier
|
|
112
112
|
);
|
|
113
|
-
cookieJar = mod.cookies ? mod.cookies() : null;
|
|
113
|
+
cookieJar = mod.cookies ? await mod.cookies() : null;
|
|
114
114
|
} catch {
|
|
115
115
|
cookieJar = null;
|
|
116
116
|
}
|
package/dist/react.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
import { ReactNode } from 'react';
|
|
4
|
-
import { S as SessionManager, a as SessionSnapshot, b as SignInOptions, c as SignOutOptions, C as CallbackResult } from './signIn-
|
|
4
|
+
import { S as SessionManager, a as SessionSnapshot, b as SignInOptions, c as SignOutOptions, C as CallbackResult } from './signIn-CEMdUAwd.mjs';
|
|
5
5
|
import { d as SessionUser, J as JwtClaims } from './types-Cxl3bQHt.mjs';
|
|
6
6
|
import './publishableKey-B5DIK81A.mjs';
|
|
7
7
|
|
package/dist/react.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
import { ReactNode } from 'react';
|
|
4
|
-
import { S as SessionManager, a as SessionSnapshot, b as SignInOptions, c as SignOutOptions, C as CallbackResult } from './signIn-
|
|
4
|
+
import { S as SessionManager, a as SessionSnapshot, b as SignInOptions, c as SignOutOptions, C as CallbackResult } from './signIn-VRNzlNyG.js';
|
|
5
5
|
import { d as SessionUser, J as JwtClaims } from './types-Cxl3bQHt.js';
|
|
6
6
|
import './publishableKey-B5DIK81A.js';
|
|
7
7
|
|
package/dist/react.js
CHANGED
|
@@ -215,8 +215,9 @@ var SessionManager = class {
|
|
|
215
215
|
this.refreshPath = options.refreshPath ?? DEFAULT_REFRESH_PATH;
|
|
216
216
|
this.userinfoPath = options.userinfoPath ?? DEFAULT_USERINFO_PATH;
|
|
217
217
|
this.useCookies = options.useCookies ?? true;
|
|
218
|
-
this.
|
|
219
|
-
this.
|
|
218
|
+
this.serverManagedSession = options.serverManagedSession ?? false;
|
|
219
|
+
this.proactiveRefresh = this.serverManagedSession ? false : options.proactiveRefresh ?? true;
|
|
220
|
+
this.tokenStore = options.tokenStore ?? (this.serverManagedSession ? NO_OP_STORE : this.useCookies ? defaultCookieStore() : NO_OP_STORE);
|
|
220
221
|
this.crossTabLockTimeoutMs = options.crossTabLockTimeoutMs ?? 4e3;
|
|
221
222
|
this.fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : (() => {
|
|
222
223
|
throw new Error("global fetch is not available; pass fetchImpl");
|
|
@@ -260,6 +261,40 @@ var SessionManager = class {
|
|
|
260
261
|
async bootstrap() {
|
|
261
262
|
if (this.bootstrapped) return;
|
|
262
263
|
this.bootstrapped = true;
|
|
264
|
+
if (this.serverManagedSession) {
|
|
265
|
+
try {
|
|
266
|
+
const res = await this.fetchImpl(`${this.issuer}${this.userinfoPath}`, {
|
|
267
|
+
method: "GET",
|
|
268
|
+
credentials: "include",
|
|
269
|
+
headers: { Accept: "application/json" }
|
|
270
|
+
});
|
|
271
|
+
if (!res.ok) {
|
|
272
|
+
this.setStatus("unauthenticated");
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const body = await res.json().catch(() => ({}));
|
|
276
|
+
const data = body?.data;
|
|
277
|
+
const user = data?.user ?? claimsToSessionUser(data?.claims ?? null);
|
|
278
|
+
if (!user) {
|
|
279
|
+
this.setStatus("unauthenticated");
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
this.update({
|
|
283
|
+
status: "authenticated",
|
|
284
|
+
accessToken: null,
|
|
285
|
+
user,
|
|
286
|
+
claims: data?.claims ?? null,
|
|
287
|
+
tenantId: data?.tenantId ?? user.tenantId ?? this.key.tenantId,
|
|
288
|
+
error: null,
|
|
289
|
+
version: this.snapshot.version + 1
|
|
290
|
+
});
|
|
291
|
+
this.broadcast("session:update");
|
|
292
|
+
return;
|
|
293
|
+
} catch {
|
|
294
|
+
this.setStatus("unauthenticated");
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
263
298
|
const stored = await Promise.resolve(this.tokenStore.read());
|
|
264
299
|
if (!stored) {
|
|
265
300
|
this.setStatus("unauthenticated");
|
package/dist/react.mjs
CHANGED
|
@@ -63,14 +63,33 @@ interface IQAuthHelperConfig {
|
|
|
63
63
|
logoutPath?: string;
|
|
64
64
|
/** Optional fetch implementation override. */
|
|
65
65
|
fetchImpl?: typeof fetch;
|
|
66
|
+
/**
|
|
67
|
+
* Policy for clearing the access + refresh cookies when `/refresh` fails.
|
|
68
|
+
*
|
|
69
|
+
* - `"terminal-only"` (default, recommended): only clear cookies when the
|
|
70
|
+
* issuer indicates the session is unrecoverable
|
|
71
|
+
* (`TOKEN_REVOKED`, `SESSION_REVOKED`, `INVALID_GRANT`,
|
|
72
|
+
* `USER_DEACTIVATED`, or HTTP 410 Gone). Transient failures
|
|
73
|
+
* (`TOKEN_INVALID` from a rotated-out token, `TOKEN_EXPIRED`, network
|
|
74
|
+
* errors, 5xx) leave cookies intact so the next legitimate request can
|
|
75
|
+
* either succeed against a still-valid access cookie or be redirected
|
|
76
|
+
* cleanly to sign-in by the middleware. Fixes the multi-tab /
|
|
77
|
+
* proactive-refresh race that previously silently signed users out.
|
|
78
|
+
* - `"always"`: pre-2.0.3 behavior — wipe both cookies on any non-2xx.
|
|
79
|
+
* Use only if you have an external reason to depend on the old semantics.
|
|
80
|
+
* - `"never"`: leave cookies untouched on every failure path. Suitable for
|
|
81
|
+
* apps that manage cookie lifecycle entirely outside the SDK helpers.
|
|
82
|
+
*/
|
|
83
|
+
clearCookiesOnRefreshFailure?: "terminal-only" | "always" | "never";
|
|
66
84
|
}
|
|
67
|
-
interface ResolvedConfig extends Required<Omit<IQAuthHelperConfig, "secretKey" | "cookieDomain" | "issuer" | "fetchImpl">> {
|
|
85
|
+
interface ResolvedConfig extends Required<Omit<IQAuthHelperConfig, "secretKey" | "cookieDomain" | "issuer" | "fetchImpl" | "clearCookiesOnRefreshFailure">> {
|
|
68
86
|
secretKey?: string;
|
|
69
87
|
cookieDomain?: string;
|
|
70
88
|
issuer: string;
|
|
71
89
|
fetchImpl: typeof fetch;
|
|
72
90
|
appId: string;
|
|
73
91
|
tenantId: string;
|
|
92
|
+
clearCookiesOnRefreshFailure: "terminal-only" | "always" | "never";
|
|
74
93
|
}
|
|
75
94
|
/**
|
|
76
95
|
* Serialize a cookie directive to a Set-Cookie header value. Adapters that
|
|
@@ -84,7 +103,15 @@ declare function handleCallback(config: IQAuthHelperConfig, input: {
|
|
|
84
103
|
codeVerifier?: string;
|
|
85
104
|
redirectUri?: string;
|
|
86
105
|
}): Promise<HandlerResponse>;
|
|
87
|
-
/** POST /api/iqauth/refresh — rotate refresh + access cookies.
|
|
106
|
+
/** POST /api/iqauth/refresh — rotate refresh + access cookies.
|
|
107
|
+
*
|
|
108
|
+
* Cookie-clearing policy is governed by `config.clearCookiesOnRefreshFailure`
|
|
109
|
+
* (default `"terminal-only"`). Prior to 2.0.3 this helper wiped both cookies
|
|
110
|
+
* on any non-2xx, which converted survivable refresh-token races (multi-tab,
|
|
111
|
+
* proactive-refresh timer, React StrictMode double-mount) into silent forced
|
|
112
|
+
* sign-outs. The default behavior now preserves cookies on transient failures
|
|
113
|
+
* and only clears them when the issuer signals the session is truly dead.
|
|
114
|
+
*/
|
|
88
115
|
declare function handleRefresh(config: IQAuthHelperConfig, input: {
|
|
89
116
|
refreshToken?: string;
|
|
90
117
|
}): Promise<HandlerResponse>;
|
|
@@ -63,14 +63,33 @@ interface IQAuthHelperConfig {
|
|
|
63
63
|
logoutPath?: string;
|
|
64
64
|
/** Optional fetch implementation override. */
|
|
65
65
|
fetchImpl?: typeof fetch;
|
|
66
|
+
/**
|
|
67
|
+
* Policy for clearing the access + refresh cookies when `/refresh` fails.
|
|
68
|
+
*
|
|
69
|
+
* - `"terminal-only"` (default, recommended): only clear cookies when the
|
|
70
|
+
* issuer indicates the session is unrecoverable
|
|
71
|
+
* (`TOKEN_REVOKED`, `SESSION_REVOKED`, `INVALID_GRANT`,
|
|
72
|
+
* `USER_DEACTIVATED`, or HTTP 410 Gone). Transient failures
|
|
73
|
+
* (`TOKEN_INVALID` from a rotated-out token, `TOKEN_EXPIRED`, network
|
|
74
|
+
* errors, 5xx) leave cookies intact so the next legitimate request can
|
|
75
|
+
* either succeed against a still-valid access cookie or be redirected
|
|
76
|
+
* cleanly to sign-in by the middleware. Fixes the multi-tab /
|
|
77
|
+
* proactive-refresh race that previously silently signed users out.
|
|
78
|
+
* - `"always"`: pre-2.0.3 behavior — wipe both cookies on any non-2xx.
|
|
79
|
+
* Use only if you have an external reason to depend on the old semantics.
|
|
80
|
+
* - `"never"`: leave cookies untouched on every failure path. Suitable for
|
|
81
|
+
* apps that manage cookie lifecycle entirely outside the SDK helpers.
|
|
82
|
+
*/
|
|
83
|
+
clearCookiesOnRefreshFailure?: "terminal-only" | "always" | "never";
|
|
66
84
|
}
|
|
67
|
-
interface ResolvedConfig extends Required<Omit<IQAuthHelperConfig, "secretKey" | "cookieDomain" | "issuer" | "fetchImpl">> {
|
|
85
|
+
interface ResolvedConfig extends Required<Omit<IQAuthHelperConfig, "secretKey" | "cookieDomain" | "issuer" | "fetchImpl" | "clearCookiesOnRefreshFailure">> {
|
|
68
86
|
secretKey?: string;
|
|
69
87
|
cookieDomain?: string;
|
|
70
88
|
issuer: string;
|
|
71
89
|
fetchImpl: typeof fetch;
|
|
72
90
|
appId: string;
|
|
73
91
|
tenantId: string;
|
|
92
|
+
clearCookiesOnRefreshFailure: "terminal-only" | "always" | "never";
|
|
74
93
|
}
|
|
75
94
|
/**
|
|
76
95
|
* Serialize a cookie directive to a Set-Cookie header value. Adapters that
|
|
@@ -84,7 +103,15 @@ declare function handleCallback(config: IQAuthHelperConfig, input: {
|
|
|
84
103
|
codeVerifier?: string;
|
|
85
104
|
redirectUri?: string;
|
|
86
105
|
}): Promise<HandlerResponse>;
|
|
87
|
-
/** POST /api/iqauth/refresh — rotate refresh + access cookies.
|
|
106
|
+
/** POST /api/iqauth/refresh — rotate refresh + access cookies.
|
|
107
|
+
*
|
|
108
|
+
* Cookie-clearing policy is governed by `config.clearCookiesOnRefreshFailure`
|
|
109
|
+
* (default `"terminal-only"`). Prior to 2.0.3 this helper wiped both cookies
|
|
110
|
+
* on any non-2xx, which converted survivable refresh-token races (multi-tab,
|
|
111
|
+
* proactive-refresh timer, React StrictMode double-mount) into silent forced
|
|
112
|
+
* sign-outs. The default behavior now preserves cookies on transient failures
|
|
113
|
+
* and only clears them when the issuer signals the session is truly dead.
|
|
114
|
+
*/
|
|
88
115
|
declare function handleRefresh(config: IQAuthHelperConfig, input: {
|
|
89
116
|
refreshToken?: string;
|
|
90
117
|
}): Promise<HandlerResponse>;
|
package/dist/server/handlers.js
CHANGED
|
@@ -57,6 +57,22 @@ function parsePublishableKey(raw) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// src/server/handlers.ts
|
|
60
|
+
var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
61
|
+
"TOKEN_REVOKED",
|
|
62
|
+
"SESSION_REVOKED",
|
|
63
|
+
"INVALID_GRANT",
|
|
64
|
+
"invalid_grant",
|
|
65
|
+
"USER_DEACTIVATED",
|
|
66
|
+
"USER_DISABLED",
|
|
67
|
+
"TENANT_SUSPENDED"
|
|
68
|
+
]);
|
|
69
|
+
function shouldClearCookiesOnFailure(policy, status, errorCode) {
|
|
70
|
+
if (policy === "always") return true;
|
|
71
|
+
if (policy === "never") return false;
|
|
72
|
+
if (status === 410) return true;
|
|
73
|
+
if (errorCode && TERMINAL_REFRESH_ERROR_CODES.has(errorCode)) return true;
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
60
76
|
var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
|
|
61
77
|
var REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
|
|
62
78
|
function resolve(config) {
|
|
@@ -84,7 +100,8 @@ function resolve(config) {
|
|
|
84
100
|
throw new Error("global fetch is unavailable; pass fetchImpl");
|
|
85
101
|
})),
|
|
86
102
|
appId: parsed.appId,
|
|
87
|
-
tenantId: parsed.tenantId
|
|
103
|
+
tenantId: parsed.tenantId,
|
|
104
|
+
clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only"
|
|
88
105
|
};
|
|
89
106
|
}
|
|
90
107
|
function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
|
|
@@ -180,7 +197,7 @@ async function handleRefresh(config, input) {
|
|
|
180
197
|
return {
|
|
181
198
|
status: 401,
|
|
182
199
|
body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing refresh token" } },
|
|
183
|
-
cookies: clearCookies(cfg)
|
|
200
|
+
cookies: cfg.clearCookiesOnRefreshFailure === "always" ? clearCookies(cfg) : []
|
|
184
201
|
};
|
|
185
202
|
}
|
|
186
203
|
const res = await cfg.fetchImpl(`${cfg.issuer}${cfg.refreshPath}`, {
|
|
@@ -190,16 +207,23 @@ async function handleRefresh(config, input) {
|
|
|
190
207
|
});
|
|
191
208
|
const json = await res.json().catch(() => ({}));
|
|
192
209
|
if (!res.ok || !json.success || !json.data?.accessToken) {
|
|
210
|
+
const status = res.status || 401;
|
|
211
|
+
const errorCode = json.error?.code || "TOKEN_INVALID";
|
|
212
|
+
const shouldClear = shouldClearCookiesOnFailure(
|
|
213
|
+
cfg.clearCookiesOnRefreshFailure,
|
|
214
|
+
status,
|
|
215
|
+
errorCode
|
|
216
|
+
);
|
|
193
217
|
return {
|
|
194
|
-
status
|
|
218
|
+
status,
|
|
195
219
|
body: {
|
|
196
220
|
success: false,
|
|
197
221
|
error: {
|
|
198
|
-
code:
|
|
222
|
+
code: errorCode,
|
|
199
223
|
message: json.error?.message || "Refresh failed"
|
|
200
224
|
}
|
|
201
225
|
},
|
|
202
|
-
cookies: clearCookies(cfg)
|
|
226
|
+
cookies: shouldClear ? clearCookies(cfg) : []
|
|
203
227
|
};
|
|
204
228
|
}
|
|
205
229
|
const cookies = [
|
package/dist/server/handlers.mjs
CHANGED
package/dist/server.js
CHANGED
|
@@ -1988,6 +1988,22 @@ function iqAuthMiddleware(clientOrOptions, options = {}) {
|
|
|
1988
1988
|
}
|
|
1989
1989
|
|
|
1990
1990
|
// src/server/handlers.ts
|
|
1991
|
+
var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
1992
|
+
"TOKEN_REVOKED",
|
|
1993
|
+
"SESSION_REVOKED",
|
|
1994
|
+
"INVALID_GRANT",
|
|
1995
|
+
"invalid_grant",
|
|
1996
|
+
"USER_DEACTIVATED",
|
|
1997
|
+
"USER_DISABLED",
|
|
1998
|
+
"TENANT_SUSPENDED"
|
|
1999
|
+
]);
|
|
2000
|
+
function shouldClearCookiesOnFailure(policy, status, errorCode) {
|
|
2001
|
+
if (policy === "always") return true;
|
|
2002
|
+
if (policy === "never") return false;
|
|
2003
|
+
if (status === 410) return true;
|
|
2004
|
+
if (errorCode && TERMINAL_REFRESH_ERROR_CODES.has(errorCode)) return true;
|
|
2005
|
+
return false;
|
|
2006
|
+
}
|
|
1991
2007
|
var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
|
|
1992
2008
|
var REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
|
|
1993
2009
|
function resolve(config) {
|
|
@@ -2015,7 +2031,8 @@ function resolve(config) {
|
|
|
2015
2031
|
throw new Error("global fetch is unavailable; pass fetchImpl");
|
|
2016
2032
|
})),
|
|
2017
2033
|
appId: parsed.appId,
|
|
2018
|
-
tenantId: parsed.tenantId
|
|
2034
|
+
tenantId: parsed.tenantId,
|
|
2035
|
+
clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only"
|
|
2019
2036
|
};
|
|
2020
2037
|
}
|
|
2021
2038
|
function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
|
|
@@ -2111,7 +2128,7 @@ async function handleRefresh(config, input) {
|
|
|
2111
2128
|
return {
|
|
2112
2129
|
status: 401,
|
|
2113
2130
|
body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing refresh token" } },
|
|
2114
|
-
cookies: clearCookies(cfg)
|
|
2131
|
+
cookies: cfg.clearCookiesOnRefreshFailure === "always" ? clearCookies(cfg) : []
|
|
2115
2132
|
};
|
|
2116
2133
|
}
|
|
2117
2134
|
const res = await cfg.fetchImpl(`${cfg.issuer}${cfg.refreshPath}`, {
|
|
@@ -2121,16 +2138,23 @@ async function handleRefresh(config, input) {
|
|
|
2121
2138
|
});
|
|
2122
2139
|
const json = await res.json().catch(() => ({}));
|
|
2123
2140
|
if (!res.ok || !json.success || !json.data?.accessToken) {
|
|
2141
|
+
const status = res.status || 401;
|
|
2142
|
+
const errorCode = json.error?.code || "TOKEN_INVALID";
|
|
2143
|
+
const shouldClear = shouldClearCookiesOnFailure(
|
|
2144
|
+
cfg.clearCookiesOnRefreshFailure,
|
|
2145
|
+
status,
|
|
2146
|
+
errorCode
|
|
2147
|
+
);
|
|
2124
2148
|
return {
|
|
2125
|
-
status
|
|
2149
|
+
status,
|
|
2126
2150
|
body: {
|
|
2127
2151
|
success: false,
|
|
2128
2152
|
error: {
|
|
2129
|
-
code:
|
|
2153
|
+
code: errorCode,
|
|
2130
2154
|
message: json.error?.message || "Refresh failed"
|
|
2131
2155
|
}
|
|
2132
2156
|
},
|
|
2133
|
-
cookies: clearCookies(cfg)
|
|
2157
|
+
cookies: shouldClear ? clearCookies(cfg) : []
|
|
2134
2158
|
};
|
|
2135
2159
|
}
|
|
2136
2160
|
const cookies = [
|
package/dist/server.mjs
CHANGED
|
@@ -80,6 +80,27 @@ interface SessionManagerOptions {
|
|
|
80
80
|
* refresh to finish before falling back to its own. Defaults to 4000.
|
|
81
81
|
*/
|
|
82
82
|
crossTabLockTimeoutMs?: number;
|
|
83
|
+
/**
|
|
84
|
+
* "Server-managed" session mode for apps where the backend (via the
|
|
85
|
+
* `@iqauth/sdk/{express,fastify,hono,next}` adapters) owns the HttpOnly
|
|
86
|
+
* `iqauth_at` + `iqauth_rt` cookies and is the sole authority on token
|
|
87
|
+
* rotation. When `true`:
|
|
88
|
+
*
|
|
89
|
+
* - `bootstrap()` learns session state with a single `GET userinfoPath`
|
|
90
|
+
* call (no token rotation, no race surface), instead of POSTing to
|
|
91
|
+
* `/refresh` with an empty body.
|
|
92
|
+
* - `scheduleProactiveRefresh()` is suppressed — the server middleware
|
|
93
|
+
* refreshes on real navigation, which is single-flight per request.
|
|
94
|
+
* - `tokenStore` defaults to a no-op store so JS never tries to read the
|
|
95
|
+
* (HttpOnly, invisible) refresh cookie.
|
|
96
|
+
*
|
|
97
|
+
* This is the recommended mode for any app with its own backend, especially
|
|
98
|
+
* confidential-client OIDC flows. It eliminates the multi-tab refresh-token
|
|
99
|
+
* rotation race that previously caused silent sign-outs.
|
|
100
|
+
*
|
|
101
|
+
* Defaults to `false` to preserve pre-2.0.3 behavior.
|
|
102
|
+
*/
|
|
103
|
+
serverManagedSession?: boolean;
|
|
83
104
|
}
|
|
84
105
|
declare class SessionManager {
|
|
85
106
|
private snapshot;
|
|
@@ -96,6 +117,7 @@ declare class SessionManager {
|
|
|
96
117
|
private readonly proactiveRefresh;
|
|
97
118
|
private readonly tokenStore;
|
|
98
119
|
private readonly crossTabLockTimeoutMs;
|
|
120
|
+
private readonly serverManagedSession;
|
|
99
121
|
private proactiveTimer;
|
|
100
122
|
private bootstrapped;
|
|
101
123
|
/** Pending refresh awaited by other tabs after a `refresh:claim` from us. */
|
|
@@ -80,6 +80,27 @@ interface SessionManagerOptions {
|
|
|
80
80
|
* refresh to finish before falling back to its own. Defaults to 4000.
|
|
81
81
|
*/
|
|
82
82
|
crossTabLockTimeoutMs?: number;
|
|
83
|
+
/**
|
|
84
|
+
* "Server-managed" session mode for apps where the backend (via the
|
|
85
|
+
* `@iqauth/sdk/{express,fastify,hono,next}` adapters) owns the HttpOnly
|
|
86
|
+
* `iqauth_at` + `iqauth_rt` cookies and is the sole authority on token
|
|
87
|
+
* rotation. When `true`:
|
|
88
|
+
*
|
|
89
|
+
* - `bootstrap()` learns session state with a single `GET userinfoPath`
|
|
90
|
+
* call (no token rotation, no race surface), instead of POSTing to
|
|
91
|
+
* `/refresh` with an empty body.
|
|
92
|
+
* - `scheduleProactiveRefresh()` is suppressed — the server middleware
|
|
93
|
+
* refreshes on real navigation, which is single-flight per request.
|
|
94
|
+
* - `tokenStore` defaults to a no-op store so JS never tries to read the
|
|
95
|
+
* (HttpOnly, invisible) refresh cookie.
|
|
96
|
+
*
|
|
97
|
+
* This is the recommended mode for any app with its own backend, especially
|
|
98
|
+
* confidential-client OIDC flows. It eliminates the multi-tab refresh-token
|
|
99
|
+
* rotation race that previously caused silent sign-outs.
|
|
100
|
+
*
|
|
101
|
+
* Defaults to `false` to preserve pre-2.0.3 behavior.
|
|
102
|
+
*/
|
|
103
|
+
serverManagedSession?: boolean;
|
|
83
104
|
}
|
|
84
105
|
declare class SessionManager {
|
|
85
106
|
private snapshot;
|
|
@@ -96,6 +117,7 @@ declare class SessionManager {
|
|
|
96
117
|
private readonly proactiveRefresh;
|
|
97
118
|
private readonly tokenStore;
|
|
98
119
|
private readonly crossTabLockTimeoutMs;
|
|
120
|
+
private readonly serverManagedSession;
|
|
99
121
|
private proactiveTimer;
|
|
100
122
|
private bootstrapped;
|
|
101
123
|
/** Pending refresh awaited by other tabs after a `refresh:claim` from us. */
|
|
@@ -2,6 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
Use this guide when a first-party web app currently owns IQAuth tokens in browser code and needs to move to the supported session model.
|
|
4
4
|
|
|
5
|
+
## Cookie-managed / confidential-client (recommended for any app with a backend)
|
|
6
|
+
|
|
7
|
+
If your app uses one of the framework adapters (`@iqauth/sdk/{express,fastify,hono,next}`) the backend owns the HttpOnly `iqauth_at` + `iqauth_rt` cookies and should be the **sole** authority on token rotation. In that setup the browser `SessionManager` must NOT also try to rotate refresh tokens — otherwise the proactive-refresh timer + multi-tab BroadcastChannel + React StrictMode double-mount can race against the backend's own refreshes, and one lost race used to wipe the cookies.
|
|
8
|
+
|
|
9
|
+
As of `@iqauth/sdk@2.0.3`, opt into "server-managed" mode with a single flag:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { SessionManager } from "@iqauth/sdk/browser";
|
|
13
|
+
|
|
14
|
+
const manager = new SessionManager({
|
|
15
|
+
publishableKey: process.env.NEXT_PUBLIC_IQAUTH_PUBLISHABLE_KEY!,
|
|
16
|
+
issuer: window.location.origin,
|
|
17
|
+
refreshPath: "/api/iqauth/refresh", // mounted by the framework adapter
|
|
18
|
+
serverManagedSession: true, // ← do this
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
What `serverManagedSession: true` does:
|
|
23
|
+
|
|
24
|
+
- `bootstrap()` learns session state with a single read-only `GET /api/v1/auth/me` (override with `userinfoPath`) instead of POSTing to `/refresh`. No rotation, no race surface.
|
|
25
|
+
- `scheduleProactiveRefresh()` is suppressed. The server middleware refreshes on real navigation, which is single-flight per request and can't race itself.
|
|
26
|
+
- `tokenStore` defaults to a no-op store, so JS never tries to read the (HttpOnly, invisible) refresh cookie.
|
|
27
|
+
|
|
28
|
+
`<SignedIn>` / `<SignedOut>` / `useUser()` continue to work — they read from the userinfo bootstrap.
|
|
29
|
+
|
|
30
|
+
### Cookie-clearing policy on `/refresh` failures
|
|
31
|
+
|
|
32
|
+
`@iqauth/sdk/server::handleRefresh` now defaults to `clearCookiesOnRefreshFailure: "terminal-only"`. Cookies are only wiped when the issuer signals the session is unrecoverable (`TOKEN_REVOKED`, `SESSION_REVOKED`, `INVALID_GRANT`, `USER_DEACTIVATED`, `TENANT_SUSPENDED`, or HTTP 410 Gone). Transient failures — `TOKEN_INVALID` from a rotated-out token, `TOKEN_EXPIRED`, network blips, 5xx — return 401 without touching cookies, so the next legitimate request can either succeed against a still-valid access cookie or get redirected to sign-in cleanly by the middleware.
|
|
33
|
+
|
|
34
|
+
Pre-2.0.3 behavior is available with `clearCookiesOnRefreshFailure: "always"`.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
|
|
5
39
|
## Target Model
|
|
6
40
|
|
|
7
41
|
- browser talks to app backend
|
package/package.json
CHANGED