@iqauth/sdk 2.1.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -3
- package/dist/browser-session.d.mts +1 -2
- package/dist/browser-session.d.ts +1 -2
- package/dist/browser-session.js +89 -68
- package/dist/browser-session.mjs +2 -1
- package/dist/browser.d.mts +2 -2
- package/dist/browser.d.ts +2 -2
- package/dist/browser.js +69 -7
- package/dist/browser.mjs +2 -2
- package/dist/{chunk-ZESHDJDU.mjs → chunk-EKTNEZIH.mjs} +5 -8
- package/dist/{chunk-JQRTY5MY.mjs → chunk-KGEPDXHU.mjs} +12 -8
- package/dist/{chunk-S3M2IXCE.mjs → chunk-RACIPVLD.mjs} +15 -9
- package/dist/chunk-UNYDG2L4.mjs +209 -0
- package/dist/{chunk-MDUHPQMM.mjs → chunk-W3F4JYGP.mjs} +8 -180
- package/dist/chunk-WQWBJSSS.mjs +119 -0
- package/dist/cli/index.js +21 -0
- package/dist/cli/index.mjs +1 -1
- package/dist/{client-DXbHb2ul.d.ts → client-DTX4hNdS.d.ts} +16 -21
- package/dist/{client-Dv4v92Mj.d.mts → client-vdh2a9fJ.d.mts} +16 -21
- package/dist/{doctor-OHJRZBBT.mjs → doctor-A5E7LSFW.mjs} +2 -1
- package/dist/{express-BZmF1llh.d.mts → express-A0-dWEMy.d.mts} +1 -1
- package/dist/{express-B4o3P8vK.d.ts → express-Bo_pJKHN.d.ts} +1 -1
- package/dist/express.d.mts +75 -5
- package/dist/express.d.ts +75 -5
- package/dist/express.js +353 -94
- package/dist/express.mjs +210 -12
- package/dist/fastify.js +153 -88
- package/dist/fastify.mjs +10 -9
- package/dist/hono.js +152 -88
- package/dist/hono.mjs +9 -9
- package/dist/index.d.mts +3 -4
- package/dist/index.d.ts +3 -4
- package/dist/index.js +148 -72
- package/dist/index.mjs +16 -12
- package/dist/mobile.d.mts +1 -2
- package/dist/mobile.d.ts +1 -2
- package/dist/mobile.js +89 -68
- package/dist/mobile.mjs +2 -1
- package/dist/next.d.mts +9 -0
- package/dist/next.d.ts +9 -0
- package/dist/next.js +164 -1649
- package/dist/next.mjs +13 -16
- package/dist/{publishableKey-B5DIK81A.d.mts → publishableKey-BaR0HoAH.d.mts} +10 -1
- package/dist/{publishableKey-B5DIK81A.d.ts → publishableKey-BaR0HoAH.d.ts} +10 -1
- package/dist/react.d.mts +35 -3
- package/dist/react.d.ts +35 -3
- package/dist/react.js +78 -18
- package/dist/react.mjs +14 -2
- package/dist/server/handlers.d.mts +2 -0
- package/dist/server/handlers.d.ts +2 -0
- package/dist/server/handlers.js +72 -17
- package/dist/server/handlers.mjs +3 -2
- package/dist/server.d.mts +2 -3
- package/dist/server.d.ts +2 -3
- package/dist/server.js +151 -89
- package/dist/server.mjs +7 -6
- package/dist/service.d.mts +1 -2
- package/dist/service.d.ts +1 -2
- package/dist/service.js +89 -68
- package/dist/service.mjs +2 -1
- package/dist/{signIn-CEMdUAwd.d.mts → signIn-Cd0P4y9d.d.mts} +9 -1
- package/dist/{signIn-VRNzlNyG.d.ts → signIn-DKakyzeu.d.ts} +9 -1
- package/package.json +3 -2
- package/dist/chunk-5WFR6Y33.mjs +0 -59
package/dist/express.mjs
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DEFAULT_REFRESH_COOKIE,
|
|
3
3
|
iqAuthMiddleware
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-EKTNEZIH.mjs";
|
|
5
|
+
import {
|
|
6
|
+
IQAuthClient
|
|
7
|
+
} from "./chunk-W3F4JYGP.mjs";
|
|
5
8
|
import {
|
|
6
9
|
handleCallback,
|
|
7
10
|
handleRefresh,
|
|
8
11
|
handleSignout
|
|
9
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-KGEPDXHU.mjs";
|
|
10
13
|
import {
|
|
11
|
-
|
|
12
|
-
} from "./chunk-
|
|
13
|
-
import
|
|
14
|
-
IQAuthClient
|
|
15
|
-
} from "./chunk-MDUHPQMM.mjs";
|
|
14
|
+
assertPublishableKey
|
|
15
|
+
} from "./chunk-WQWBJSSS.mjs";
|
|
16
|
+
import "./chunk-UNYDG2L4.mjs";
|
|
16
17
|
import {
|
|
17
18
|
ErrorCodes,
|
|
18
19
|
IQAuthError
|
|
@@ -20,6 +21,78 @@ import {
|
|
|
20
21
|
import "./chunk-Y6FXYEAI.mjs";
|
|
21
22
|
|
|
22
23
|
// src/express.ts
|
|
24
|
+
var PKCE_COOKIE = "iqauth_pkce";
|
|
25
|
+
function escapeHtml(s) {
|
|
26
|
+
return s.replace(/[&<>"']/g, (c) => {
|
|
27
|
+
switch (c) {
|
|
28
|
+
case "&":
|
|
29
|
+
return "&";
|
|
30
|
+
case "<":
|
|
31
|
+
return "<";
|
|
32
|
+
case ">":
|
|
33
|
+
return ">";
|
|
34
|
+
case '"':
|
|
35
|
+
return """;
|
|
36
|
+
case "'":
|
|
37
|
+
return "'";
|
|
38
|
+
default:
|
|
39
|
+
return c;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function appendErrorParam(path, errorCode) {
|
|
44
|
+
const sep = path.includes("?") ? "&" : "?";
|
|
45
|
+
return `${path}${sep}error=${encodeURIComponent(errorCode)}`;
|
|
46
|
+
}
|
|
47
|
+
function defaultBrandedSpinner(args) {
|
|
48
|
+
return `<!doctype html>
|
|
49
|
+
<html lang="en">
|
|
50
|
+
<head>
|
|
51
|
+
<meta charset="utf-8" />
|
|
52
|
+
<title>Signing you in\u2026</title>
|
|
53
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
54
|
+
<style>
|
|
55
|
+
body { margin:0; min-height:100vh; display:flex; align-items:center; justify-content:center; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; background:#f7f7f8; color:#111; }
|
|
56
|
+
.iqauth-card { text-align:center; padding:2rem; }
|
|
57
|
+
.iqauth-spinner { width:36px; height:36px; border:3px solid #e5e7eb; border-top-color:#111; border-radius:50%; margin:0 auto 1rem; animation:iqauth-spin 0.9s linear infinite; }
|
|
58
|
+
@keyframes iqauth-spin { to { transform: rotate(360deg); } }
|
|
59
|
+
.iqauth-msg { font-size:0.95rem; color:#374151; }
|
|
60
|
+
</style>
|
|
61
|
+
</head>
|
|
62
|
+
<body>
|
|
63
|
+
<div class="iqauth-card" data-testid="iqauth-inline-callback-spinner">
|
|
64
|
+
<div class="iqauth-spinner" aria-hidden="true"></div>
|
|
65
|
+
<div class="iqauth-msg">Signing you in\u2026</div>
|
|
66
|
+
</div>
|
|
67
|
+
<script>
|
|
68
|
+
(function(){
|
|
69
|
+
var code = ${JSON.stringify(args.code)};
|
|
70
|
+
var state = ${JSON.stringify(args.state)};
|
|
71
|
+
var errorPath = ${JSON.stringify(args.errorPath || "")};
|
|
72
|
+
function fail(reason){
|
|
73
|
+
if (errorPath) { window.location.replace(errorPath + (errorPath.indexOf("?")>=0?"&":"?") + "error=" + encodeURIComponent(reason)); return; }
|
|
74
|
+
window.location.replace("/");
|
|
75
|
+
}
|
|
76
|
+
var verifier = (document.cookie.split('; ').find(function(c){return c.indexOf('${PKCE_COOKIE}=')===0;})||'').slice(${PKCE_COOKIE.length + 1});
|
|
77
|
+
try { verifier = decodeURIComponent(verifier); } catch (e) {}
|
|
78
|
+
fetch(${JSON.stringify(args.exchangePath)}, {
|
|
79
|
+
method: "POST",
|
|
80
|
+
credentials: "include",
|
|
81
|
+
headers: { "Content-Type": "application/json" },
|
|
82
|
+
body: JSON.stringify({ code: code, state: state, codeVerifier: verifier, redirectUri: window.location.origin + window.location.pathname })
|
|
83
|
+
}).then(function(r){ return r.json().then(function(j){ return { status:r.status, body:j }; }); })
|
|
84
|
+
.then(function(out){
|
|
85
|
+
if (out.status >= 400) { fail((out.body && out.body.error && out.body.error.code) || "exchange_failed"); return; }
|
|
86
|
+
var dest = (out.body && out.body.returnTo) || sessionStorage.getItem("iqauth_return_to") || "/";
|
|
87
|
+
sessionStorage.removeItem("iqauth_return_to");
|
|
88
|
+
window.location.replace(dest);
|
|
89
|
+
})
|
|
90
|
+
.catch(function(){ fail("network_error"); });
|
|
91
|
+
})();
|
|
92
|
+
</script>
|
|
93
|
+
</body>
|
|
94
|
+
</html>`;
|
|
95
|
+
}
|
|
23
96
|
function applyHandlerResponse(res, hr) {
|
|
24
97
|
for (const c of hr.cookies) {
|
|
25
98
|
if (typeof res.cookie === "function") {
|
|
@@ -66,10 +139,7 @@ function readCookieFromReq(req, name) {
|
|
|
66
139
|
return void 0;
|
|
67
140
|
}
|
|
68
141
|
function iqAuth(options) {
|
|
69
|
-
const parsed =
|
|
70
|
-
if (!parsed) {
|
|
71
|
-
throw new Error("@iqauth/sdk/express: invalid publishable key");
|
|
72
|
-
}
|
|
142
|
+
const parsed = assertPublishableKey(options.publishableKey, { context: "@iqauth/sdk/express" });
|
|
73
143
|
const issuer = (options.issuer ?? (parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`)).replace(/\/+$/, "");
|
|
74
144
|
const client = new IQAuthClient({
|
|
75
145
|
baseUrl: issuer,
|
|
@@ -86,6 +156,8 @@ function iqAuth(options) {
|
|
|
86
156
|
if (mountHelpers && path.startsWith(mount + "/")) return next();
|
|
87
157
|
return verify(req, res, next);
|
|
88
158
|
};
|
|
159
|
+
const inline = options.inlineCallback === true ? {} : options.inlineCallback && typeof options.inlineCallback === "object" ? options.inlineCallback : null;
|
|
160
|
+
const inlineBranded = inline?.branded === true ? {} : inline?.branded && typeof inline.branded === "object" ? inline.branded : null;
|
|
89
161
|
const attachHelpers = (app) => {
|
|
90
162
|
app.post(`${mount}/callback`, async (req, res) => {
|
|
91
163
|
const body = readBody(req);
|
|
@@ -96,6 +168,131 @@ function iqAuth(options) {
|
|
|
96
168
|
});
|
|
97
169
|
applyHandlerResponse(res, hr);
|
|
98
170
|
});
|
|
171
|
+
if (inline && typeof app.get === "function") {
|
|
172
|
+
const callbackPath = `${mount}/callback`;
|
|
173
|
+
const exchangePath = `${callbackPath}/exchange`;
|
|
174
|
+
const stateCookie = inline.stateCookieName ?? "iqauth_state";
|
|
175
|
+
const returnToCookie = inline.returnToCookieName ?? "iqauth_return_to";
|
|
176
|
+
const errorPath = inline.errorPath;
|
|
177
|
+
const clearCookie = (res, name) => {
|
|
178
|
+
if (typeof res.clearCookie === "function") {
|
|
179
|
+
res.clearCookie(name, { path: "/" });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const existing = res.getHeader?.("Set-Cookie") || [];
|
|
183
|
+
const list = Array.isArray(existing) ? existing : [existing];
|
|
184
|
+
list.push(`${name}=; Path=/; Max-Age=0; SameSite=Lax`);
|
|
185
|
+
res.setHeader?.("Set-Cookie", list);
|
|
186
|
+
};
|
|
187
|
+
const failPlain = (res, errorCode, fallback) => {
|
|
188
|
+
if (errorPath) {
|
|
189
|
+
const dest = appendErrorParam(errorPath, errorCode);
|
|
190
|
+
if (typeof res.redirect === "function") return res.redirect(302, dest);
|
|
191
|
+
res.status(302);
|
|
192
|
+
res.setHeader?.("Location", dest);
|
|
193
|
+
return res.end?.();
|
|
194
|
+
}
|
|
195
|
+
fallback();
|
|
196
|
+
};
|
|
197
|
+
if (inlineBranded) {
|
|
198
|
+
const render = inlineBranded.render ?? defaultBrandedSpinner;
|
|
199
|
+
app.get(callbackPath, (req, res) => {
|
|
200
|
+
const q = req.query || {};
|
|
201
|
+
const html = render({
|
|
202
|
+
issuer,
|
|
203
|
+
exchangePath,
|
|
204
|
+
code: escapeHtml(q.code ?? ""),
|
|
205
|
+
state: escapeHtml(q.state ?? ""),
|
|
206
|
+
errorPath: errorPath ?? ""
|
|
207
|
+
});
|
|
208
|
+
res.status(200);
|
|
209
|
+
if (typeof res.set === "function") res.set("Content-Type", "text/html; charset=utf-8");
|
|
210
|
+
else if (typeof res.setHeader === "function") res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
211
|
+
if (typeof res.send === "function") res.send(html);
|
|
212
|
+
else res.end?.(html);
|
|
213
|
+
});
|
|
214
|
+
app.post(exchangePath, async (req, res) => {
|
|
215
|
+
const body = readBody(req);
|
|
216
|
+
const stateFromBody = body.state || void 0;
|
|
217
|
+
const stateFromCookie = readCookieFromReq(req, stateCookie);
|
|
218
|
+
if (stateFromCookie && stateFromBody !== stateFromCookie) {
|
|
219
|
+
clearCookie(res, stateCookie);
|
|
220
|
+
res.status(400);
|
|
221
|
+
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" } }));
|
|
222
|
+
}
|
|
223
|
+
const hr = await handleCallback(helperConfig, {
|
|
224
|
+
code: body.code,
|
|
225
|
+
codeVerifier: body.codeVerifier || readCookieFromReq(req, PKCE_COOKIE) || "",
|
|
226
|
+
redirectUri: body.redirectUri
|
|
227
|
+
});
|
|
228
|
+
clearCookie(res, stateCookie);
|
|
229
|
+
clearCookie(res, PKCE_COOKIE);
|
|
230
|
+
const returnTo = readCookieFromReq(req, returnToCookie) || hr.body?.returnTo || "/";
|
|
231
|
+
if (hr.status < 400) clearCookie(res, returnToCookie);
|
|
232
|
+
const enriched = {
|
|
233
|
+
...hr,
|
|
234
|
+
body: { ...hr.body, returnTo }
|
|
235
|
+
};
|
|
236
|
+
applyHandlerResponse(res, enriched);
|
|
237
|
+
});
|
|
238
|
+
} else {
|
|
239
|
+
app.get(callbackPath, async (req, res) => {
|
|
240
|
+
const q = req.query || {};
|
|
241
|
+
const code = q.code;
|
|
242
|
+
if (!code) {
|
|
243
|
+
return failPlain(res, "missing_code", () => {
|
|
244
|
+
res.status(400);
|
|
245
|
+
if (res.json) res.json({ success: false, error: { code: "MISSING_CODE", message: "Missing authorization code" } });
|
|
246
|
+
else res.end?.("Missing authorization code");
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
const stateFromQuery = q.state;
|
|
250
|
+
const stateFromCookie = readCookieFromReq(req, stateCookie);
|
|
251
|
+
if (stateFromCookie && stateFromQuery !== stateFromCookie) {
|
|
252
|
+
clearCookie(res, stateCookie);
|
|
253
|
+
return failPlain(res, "state_mismatch", () => {
|
|
254
|
+
res.status(400);
|
|
255
|
+
if (res.json) res.json({ success: false, error: { code: "STATE_MISMATCH", message: "OAuth state mismatch" } });
|
|
256
|
+
else res.end?.("OAuth state mismatch");
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
const codeVerifier = readCookieFromReq(req, PKCE_COOKIE) || "";
|
|
260
|
+
const proto = req.headers?.["x-forwarded-proto"] || req.protocol || "https";
|
|
261
|
+
const host = req.headers?.["x-forwarded-host"] || req.headers?.host || "";
|
|
262
|
+
const redirectUri = `${proto}://${host}${callbackPath}`;
|
|
263
|
+
const hr = await handleCallback(helperConfig, { code, codeVerifier, redirectUri });
|
|
264
|
+
for (const c of hr.cookies) {
|
|
265
|
+
if (typeof res.cookie === "function") {
|
|
266
|
+
const opts = {
|
|
267
|
+
httpOnly: c.httpOnly,
|
|
268
|
+
secure: c.secure,
|
|
269
|
+
sameSite: c.sameSite,
|
|
270
|
+
path: c.path,
|
|
271
|
+
maxAge: c.maxAge * 1e3
|
|
272
|
+
};
|
|
273
|
+
if (c.domain) opts.domain = c.domain;
|
|
274
|
+
res.cookie(c.name, c.value, opts);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
clearCookie(res, stateCookie);
|
|
278
|
+
clearCookie(res, PKCE_COOKIE);
|
|
279
|
+
if (hr.status >= 400) {
|
|
280
|
+
const code2 = hr.body?.error?.code || "exchange_failed";
|
|
281
|
+
return failPlain(res, code2, () => {
|
|
282
|
+
res.status(hr.status);
|
|
283
|
+
if (res.json) res.json(hr.body);
|
|
284
|
+
else res.end?.(JSON.stringify(hr.body));
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
const returnTo = readCookieFromReq(req, returnToCookie) || hr.body?.returnTo || "/";
|
|
288
|
+
clearCookie(res, returnToCookie);
|
|
289
|
+
if (typeof res.redirect === "function") return res.redirect(302, returnTo);
|
|
290
|
+
res.status(302);
|
|
291
|
+
if (typeof res.setHeader === "function") res.setHeader("Location", returnTo);
|
|
292
|
+
res.end?.();
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
99
296
|
app.post(`${mount}/refresh`, async (req, res) => {
|
|
100
297
|
const body = readBody(req);
|
|
101
298
|
const refreshToken = body.refreshToken || readCookieFromReq(req, refreshCookie);
|
|
@@ -104,7 +301,8 @@ function iqAuth(options) {
|
|
|
104
301
|
});
|
|
105
302
|
app.post(`${mount}/signout`, async (req, res) => {
|
|
106
303
|
const accessToken = req.headers?.authorization?.replace(/^Bearer /i, "") || readCookieFromReq(req, accessCookie);
|
|
107
|
-
const
|
|
304
|
+
const ssoCookieHeader = req.headers?.cookie;
|
|
305
|
+
const hr = await handleSignout(helperConfig, { accessToken, ssoCookieHeader });
|
|
108
306
|
applyHandlerResponse(res, hr);
|
|
109
307
|
});
|
|
110
308
|
};
|
package/dist/fastify.js
CHANGED
|
@@ -407,8 +407,7 @@ function parseMfaResponse(data, browserSessionMode) {
|
|
|
407
407
|
}
|
|
408
408
|
|
|
409
409
|
// src/modules/tokens.ts
|
|
410
|
-
var
|
|
411
|
-
var import_jsonwebtoken = __toESM(require("jsonwebtoken"));
|
|
410
|
+
var import_jose = require("jose");
|
|
412
411
|
var JWKS_CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
413
412
|
var DEFAULT_TOKEN_ISSUER = [
|
|
414
413
|
"https://auth.dispositioniq.com",
|
|
@@ -421,6 +420,24 @@ var DEFAULT_TOKEN_AUDIENCE = [
|
|
|
421
420
|
"iqvalidate"
|
|
422
421
|
];
|
|
423
422
|
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
423
|
+
function decodeProtectedHeader(token) {
|
|
424
|
+
const parts = token.split(".");
|
|
425
|
+
if (parts.length < 2) return null;
|
|
426
|
+
try {
|
|
427
|
+
const padded = parts[0] + "=".repeat((4 - parts[0].length % 4) % 4);
|
|
428
|
+
const b64 = padded.replace(/-/g, "+").replace(/_/g, "/");
|
|
429
|
+
let json;
|
|
430
|
+
if (typeof atob === "function") {
|
|
431
|
+
json = atob(b64);
|
|
432
|
+
} else {
|
|
433
|
+
const { Buffer: Buffer2 } = require("buffer");
|
|
434
|
+
json = Buffer2.from(b64, "base64").toString("utf8");
|
|
435
|
+
}
|
|
436
|
+
return JSON.parse(json);
|
|
437
|
+
} catch {
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
424
441
|
var TokensModule = class {
|
|
425
442
|
constructor(baseUrl, options = {}) {
|
|
426
443
|
this.jwksCache = null;
|
|
@@ -431,49 +448,49 @@ var TokensModule = class {
|
|
|
431
448
|
this.defaultClockTolerance = options.clockTolerance ?? DEFAULT_CLOCK_TOLERANCE_SECONDS;
|
|
432
449
|
}
|
|
433
450
|
/**
|
|
434
|
-
* Verify a JWT access token using RS256 via JWKS from
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
* clock tolerance default to client config but can be overridden per call.
|
|
451
|
+
* Verify a JWT access token using RS256/ES256 via JWKS from
|
|
452
|
+
* `/.well-known/jwks.json`. Backed by `jose` (Web Crypto) so it runs on
|
|
453
|
+
* Node, browser, and edge runtimes alike — no `node:crypto` dependency.
|
|
454
|
+
* Caches JWKS for 1 hour and refetches once on unknown `kid`.
|
|
439
455
|
*/
|
|
440
456
|
async verify(token, options = {}) {
|
|
441
|
-
const
|
|
442
|
-
if (!
|
|
457
|
+
const header = decodeProtectedHeader(token);
|
|
458
|
+
if (!header) {
|
|
443
459
|
throw new IQAuthError("TOKEN_INVALID", "Unable to decode token");
|
|
444
460
|
}
|
|
445
|
-
const kid =
|
|
461
|
+
const kid = header.kid;
|
|
446
462
|
if (!kid) {
|
|
447
463
|
throw new IQAuthError("TOKEN_INVALID", "Token missing kid header");
|
|
448
464
|
}
|
|
449
|
-
let
|
|
450
|
-
if (!
|
|
451
|
-
|
|
452
|
-
|
|
465
|
+
let cache = await this.ensureCache();
|
|
466
|
+
if (!cache.byKid.has(kid)) {
|
|
467
|
+
this.jwksCache = null;
|
|
468
|
+
cache = await this.ensureCache();
|
|
453
469
|
}
|
|
454
|
-
if (!
|
|
470
|
+
if (!cache.byKid.has(kid)) {
|
|
455
471
|
throw new IQAuthError("TOKEN_INVALID", `Unknown key ID: ${kid}`);
|
|
456
472
|
}
|
|
457
473
|
const issuer = options.issuer ?? this.defaultIssuer;
|
|
458
474
|
const audience = options.audience ?? this.defaultAudience;
|
|
459
475
|
const clockTolerance = options.clockTolerance ?? this.defaultClockTolerance;
|
|
460
|
-
const algorithms = options.algorithms ?? ["RS256"];
|
|
476
|
+
const algorithms = options.algorithms ?? ["RS256", "ES256"];
|
|
477
|
+
const verifyOptions = {
|
|
478
|
+
algorithms,
|
|
479
|
+
clockTolerance,
|
|
480
|
+
issuer,
|
|
481
|
+
audience
|
|
482
|
+
};
|
|
461
483
|
try {
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
clockTolerance,
|
|
465
|
-
// The jsonwebtoken types insist on tuple types for arrays; runtime
|
|
466
|
-
// accepts plain string[] so we cast to satisfy the compiler.
|
|
467
|
-
issuer,
|
|
468
|
-
audience
|
|
469
|
-
};
|
|
470
|
-
const verified = import_jsonwebtoken.default.verify(token, publicKey, verifyOptions);
|
|
471
|
-
return verified;
|
|
484
|
+
const { payload } = await (0, import_jose.jwtVerify)(token, cache.verifier, verifyOptions);
|
|
485
|
+
return payload;
|
|
472
486
|
} catch (err) {
|
|
487
|
+
if (err instanceof import_jose.errors.JWTExpired) {
|
|
488
|
+
throw new IQAuthError("TOKEN_EXPIRED", "Token has expired");
|
|
489
|
+
}
|
|
490
|
+
if (err instanceof import_jose.errors.JOSEError) {
|
|
491
|
+
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
492
|
+
}
|
|
473
493
|
if (err instanceof Error) {
|
|
474
|
-
if (err.name === "TokenExpiredError") {
|
|
475
|
-
throw new IQAuthError("TOKEN_EXPIRED", "Token has expired");
|
|
476
|
-
}
|
|
477
494
|
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
478
495
|
}
|
|
479
496
|
throw new IQAuthError("TOKEN_INVALID", "Token verification failed");
|
|
@@ -481,29 +498,40 @@ var TokensModule = class {
|
|
|
481
498
|
}
|
|
482
499
|
/**
|
|
483
500
|
* Decode a JWT without verification. Returns null if malformed.
|
|
484
|
-
*
|
|
485
|
-
* @remarks Local decode only — no network call
|
|
486
501
|
*/
|
|
487
502
|
decode(token) {
|
|
488
|
-
|
|
489
|
-
|
|
503
|
+
try {
|
|
504
|
+
const parts = token.split(".");
|
|
505
|
+
if (parts.length < 2) return null;
|
|
506
|
+
const payload = parts[1];
|
|
507
|
+
const padded = payload + "=".repeat((4 - payload.length % 4) % 4);
|
|
508
|
+
const b64 = padded.replace(/-/g, "+").replace(/_/g, "/");
|
|
509
|
+
let json;
|
|
510
|
+
if (typeof atob === "function") {
|
|
511
|
+
json = atob(b64);
|
|
512
|
+
} else {
|
|
513
|
+
const { Buffer: Buffer2 } = require("buffer");
|
|
514
|
+
json = Buffer2.from(b64, "base64").toString("utf8");
|
|
515
|
+
}
|
|
516
|
+
try {
|
|
517
|
+
json = decodeURIComponent(escape(json));
|
|
518
|
+
} catch {
|
|
519
|
+
}
|
|
520
|
+
const claims = JSON.parse(json);
|
|
521
|
+
if (!claims || typeof claims !== "object") return null;
|
|
522
|
+
return claims;
|
|
523
|
+
} catch {
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
490
526
|
}
|
|
491
|
-
/**
|
|
492
|
-
* Check if a token is expired based on the `exp` claim.
|
|
493
|
-
*
|
|
494
|
-
* @remarks Local check only — no network call
|
|
495
|
-
*/
|
|
527
|
+
/** Check if a token is expired based on the `exp` claim. */
|
|
496
528
|
isExpired(token) {
|
|
497
529
|
const claims = this.decode(token);
|
|
498
530
|
if (!claims?.exp) return true;
|
|
499
531
|
const now = Math.floor(Date.now() / 1e3);
|
|
500
532
|
return claims.exp <= now;
|
|
501
533
|
}
|
|
502
|
-
/**
|
|
503
|
-
* Get the claims from a token without verification.
|
|
504
|
-
*
|
|
505
|
-
* @remarks Local decode only — no network call
|
|
506
|
-
*/
|
|
534
|
+
/** Get the claims from a token without verification. */
|
|
507
535
|
getClaims(token) {
|
|
508
536
|
const claims = this.decode(token);
|
|
509
537
|
if (!claims) {
|
|
@@ -511,11 +539,15 @@ var TokensModule = class {
|
|
|
511
539
|
}
|
|
512
540
|
return claims;
|
|
513
541
|
}
|
|
514
|
-
async
|
|
515
|
-
if (
|
|
516
|
-
|
|
542
|
+
async ensureCache() {
|
|
543
|
+
if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) {
|
|
544
|
+
return this.jwksCache;
|
|
517
545
|
}
|
|
518
|
-
|
|
546
|
+
await this.refreshJwks();
|
|
547
|
+
if (!this.jwksCache) {
|
|
548
|
+
throw new IQAuthError("INTERNAL_ERROR", "JWKS cache unavailable after refresh");
|
|
549
|
+
}
|
|
550
|
+
return this.jwksCache;
|
|
519
551
|
}
|
|
520
552
|
async refreshJwks() {
|
|
521
553
|
if (this.inFlightRefresh) {
|
|
@@ -542,35 +574,24 @@ var TokensModule = class {
|
|
|
542
574
|
"Malformed JWKS response: expected { keys: [...] }"
|
|
543
575
|
);
|
|
544
576
|
}
|
|
545
|
-
const
|
|
577
|
+
const byKid = /* @__PURE__ */ new Set();
|
|
546
578
|
for (const key of jwks.keys) {
|
|
547
|
-
if (!key || typeof key.kid !== "string" || typeof key.n !== "string" || typeof key.e !== "string") {
|
|
579
|
+
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")) {
|
|
548
580
|
throw new IQAuthError(
|
|
549
581
|
"INTERNAL_ERROR",
|
|
550
582
|
"Malformed JWKS response: key missing required fields"
|
|
551
583
|
);
|
|
552
584
|
}
|
|
553
|
-
|
|
554
|
-
keys.set(key.kid, pem);
|
|
585
|
+
byKid.add(key.kid);
|
|
555
586
|
}
|
|
556
|
-
|
|
587
|
+
const verifier = (0, import_jose.createLocalJWKSet)({ keys: jwks.keys });
|
|
588
|
+
this.jwksCache = { raw: jwks.keys, byKid, verifier, fetchedAt: Date.now() };
|
|
557
589
|
} finally {
|
|
558
590
|
this.inFlightRefresh = null;
|
|
559
591
|
}
|
|
560
592
|
})();
|
|
561
593
|
return this.inFlightRefresh;
|
|
562
594
|
}
|
|
563
|
-
jwkToPem(jwk) {
|
|
564
|
-
const keyObject = import_crypto.default.createPublicKey({
|
|
565
|
-
key: {
|
|
566
|
-
kty: jwk.kty,
|
|
567
|
-
n: jwk.n,
|
|
568
|
-
e: jwk.e
|
|
569
|
-
},
|
|
570
|
-
format: "jwk"
|
|
571
|
-
});
|
|
572
|
-
return keyObject.export({ type: "spki", format: "pem" });
|
|
573
|
-
}
|
|
574
595
|
/** @internal Exposed for testing — clears JWKS cache */
|
|
575
596
|
clearCache() {
|
|
576
597
|
this.jwksCache = null;
|
|
@@ -778,7 +799,7 @@ var PermissionsModule = class {
|
|
|
778
799
|
};
|
|
779
800
|
|
|
780
801
|
// src/modules/oidc.ts
|
|
781
|
-
var
|
|
802
|
+
var import_crypto = __toESM(require("crypto"));
|
|
782
803
|
var InMemoryOidcStateStore = class {
|
|
783
804
|
constructor() {
|
|
784
805
|
this.map = /* @__PURE__ */ new Map();
|
|
@@ -859,12 +880,12 @@ var OidcModule = class {
|
|
|
859
880
|
* ready to redirect the user to.
|
|
860
881
|
*/
|
|
861
882
|
async createAuthRequest(params) {
|
|
862
|
-
const codeVerifier = base64UrlEncode(
|
|
883
|
+
const codeVerifier = base64UrlEncode(import_crypto.default.randomBytes(32));
|
|
863
884
|
const codeChallenge = base64UrlEncode(
|
|
864
|
-
|
|
885
|
+
import_crypto.default.createHash("sha256").update(codeVerifier).digest()
|
|
865
886
|
);
|
|
866
|
-
const state = base64UrlEncode(
|
|
867
|
-
const nonce = base64UrlEncode(
|
|
887
|
+
const state = base64UrlEncode(import_crypto.default.randomBytes(16));
|
|
888
|
+
const nonce = base64UrlEncode(import_crypto.default.randomBytes(16));
|
|
868
889
|
await this.stateStore.set(state, {
|
|
869
890
|
codeVerifier,
|
|
870
891
|
state,
|
|
@@ -1767,21 +1788,61 @@ function b64urlDecode(input) {
|
|
|
1767
1788
|
const { Buffer: Buffer2 } = require("buffer");
|
|
1768
1789
|
return Buffer2.from(normalized, "base64").toString("utf8");
|
|
1769
1790
|
}
|
|
1770
|
-
function
|
|
1771
|
-
if (typeof
|
|
1772
|
-
|
|
1773
|
-
if (!m) return null;
|
|
1791
|
+
function isValidIssuerUrl(iss) {
|
|
1792
|
+
if (typeof iss !== "string" || iss.length === 0) return false;
|
|
1793
|
+
if (!iss.startsWith("http://") && !iss.startsWith("https://")) return false;
|
|
1774
1794
|
try {
|
|
1775
|
-
const
|
|
1776
|
-
if (
|
|
1777
|
-
if (
|
|
1778
|
-
|
|
1779
|
-
}
|
|
1780
|
-
return { mode: m[1], iss: json.iss, appId: json.appId, tenantId: json.tenantId, kid: json.kid, raw };
|
|
1795
|
+
const u = new URL(iss);
|
|
1796
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") return false;
|
|
1797
|
+
if (!u.hostname) return false;
|
|
1798
|
+
return true;
|
|
1781
1799
|
} catch {
|
|
1782
|
-
return
|
|
1800
|
+
return false;
|
|
1783
1801
|
}
|
|
1784
1802
|
}
|
|
1803
|
+
function assertPublishableKey(raw, opts) {
|
|
1804
|
+
const ctx = opts?.context ? `${opts.context}: ` : "";
|
|
1805
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
1806
|
+
throw new IQAuthError(
|
|
1807
|
+
"CONFIG_INVALID",
|
|
1808
|
+
`${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
|
+
);
|
|
1810
|
+
}
|
|
1811
|
+
const shapeMatch = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
|
|
1812
|
+
if (!shapeMatch) {
|
|
1813
|
+
throw new IQAuthError(
|
|
1814
|
+
"CONFIG_INVALID",
|
|
1815
|
+
`${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
|
+
);
|
|
1817
|
+
}
|
|
1818
|
+
let decoded;
|
|
1819
|
+
try {
|
|
1820
|
+
decoded = JSON.parse(b64urlDecode(shapeMatch[2]));
|
|
1821
|
+
} catch {
|
|
1822
|
+
throw new IQAuthError(
|
|
1823
|
+
"CONFIG_INVALID",
|
|
1824
|
+
`${ctx}IQAuth publishable key payload is not valid base64url JSON. Regenerate the key from the IQAuth admin console.`
|
|
1825
|
+
);
|
|
1826
|
+
}
|
|
1827
|
+
if (!isPublishableKeyPayload(decoded)) {
|
|
1828
|
+
throw new IQAuthError(
|
|
1829
|
+
"CONFIG_INVALID",
|
|
1830
|
+
`${ctx}IQAuth publishable key payload is missing required fields {iss, appId, tenantId, kid}. Regenerate the key from the IQAuth admin console.`
|
|
1831
|
+
);
|
|
1832
|
+
}
|
|
1833
|
+
if (!isValidIssuerUrl(decoded.iss)) {
|
|
1834
|
+
throw new IQAuthError(
|
|
1835
|
+
"CONFIG_INVALID",
|
|
1836
|
+
`${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
|
+
);
|
|
1838
|
+
}
|
|
1839
|
+
return { mode: shapeMatch[1], iss: decoded.iss, appId: decoded.appId, tenantId: decoded.tenantId, kid: decoded.kid, raw };
|
|
1840
|
+
}
|
|
1841
|
+
function isPublishableKeyPayload(value) {
|
|
1842
|
+
if (!value || typeof value !== "object") return false;
|
|
1843
|
+
const v = value;
|
|
1844
|
+
return typeof v.iss === "string" && typeof v.appId === "string" && typeof v.tenantId === "string" && typeof v.kid === "string";
|
|
1845
|
+
}
|
|
1785
1846
|
|
|
1786
1847
|
// src/server/handlers.ts
|
|
1787
1848
|
var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
@@ -1803,12 +1864,7 @@ function shouldClearCookiesOnFailure(policy, status, errorCode) {
|
|
|
1803
1864
|
var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
|
|
1804
1865
|
var REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
|
|
1805
1866
|
function resolve(config) {
|
|
1806
|
-
const parsed =
|
|
1807
|
-
if (!parsed) {
|
|
1808
|
-
throw new Error(
|
|
1809
|
-
"@iqauth/sdk: invalid publishable key passed to iqAuth helpers (expected pk_test_\u2026 or pk_live_\u2026)"
|
|
1810
|
-
);
|
|
1811
|
-
}
|
|
1867
|
+
const parsed = assertPublishableKey(config.publishableKey, { context: "@iqauth/sdk helpers" });
|
|
1812
1868
|
const inferredIssuer = parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`;
|
|
1813
1869
|
return {
|
|
1814
1870
|
publishableKey: config.publishableKey,
|
|
@@ -1979,6 +2035,15 @@ async function handleSignout(config, input) {
|
|
|
1979
2035
|
} catch {
|
|
1980
2036
|
}
|
|
1981
2037
|
}
|
|
2038
|
+
if (input.endSsoSession !== false && input.ssoCookieHeader) {
|
|
2039
|
+
try {
|
|
2040
|
+
await cfg.fetchImpl(`${cfg.issuer}/oidc/sso-logout`, {
|
|
2041
|
+
method: "POST",
|
|
2042
|
+
headers: { Cookie: input.ssoCookieHeader }
|
|
2043
|
+
});
|
|
2044
|
+
} catch {
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
1982
2047
|
return {
|
|
1983
2048
|
status: 200,
|
|
1984
2049
|
body: { success: true, data: { signedOut: true } },
|
|
@@ -2023,8 +2088,7 @@ function readCookie(req, name) {
|
|
|
2023
2088
|
return void 0;
|
|
2024
2089
|
}
|
|
2025
2090
|
async function iqAuth(fastify, options) {
|
|
2026
|
-
const parsed =
|
|
2027
|
-
if (!parsed) throw new Error("@iqauth/sdk/fastify: invalid publishable key");
|
|
2091
|
+
const parsed = assertPublishableKey(options.publishableKey, { context: "@iqauth/sdk/fastify" });
|
|
2028
2092
|
const issuer = (options.issuer ?? (parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`)).replace(/\/+$/, "");
|
|
2029
2093
|
const helperConfig = { ...options, issuer };
|
|
2030
2094
|
const client = new IQAuthClient({
|
|
@@ -2086,7 +2150,8 @@ async function iqAuth(fastify, options) {
|
|
|
2086
2150
|
fastify.post(`${mount}/signout`, async (req, reply) => {
|
|
2087
2151
|
const auth = req.headers?.authorization;
|
|
2088
2152
|
const accessToken = (typeof auth === "string" ? auth.replace(/^Bearer /i, "") : void 0) || readCookie(req, accessCookie);
|
|
2089
|
-
|
|
2153
|
+
const ssoCookieHeader = typeof req.headers?.cookie === "string" ? req.headers.cookie : void 0;
|
|
2154
|
+
applyResponse(reply, await handleSignout(helperConfig, { accessToken, ssoCookieHeader }));
|
|
2090
2155
|
});
|
|
2091
2156
|
}
|
|
2092
2157
|
fastify.decorate("iqauth", { client, issuer });
|