@nihplod/sso-sdk 1.5.0 → 1.6.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 +21 -2
- package/dist/index.d.mts +25 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +79 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +79 -6
- package/dist/index.mjs.map +1 -1
- package/dist/next/index.d.mts +38 -2
- package/dist/next/index.d.ts +38 -2
- package/dist/next/index.js +82 -20
- package/dist/next/index.js.map +1 -1
- package/dist/next/index.mjs +82 -20
- package/dist/next/index.mjs.map +1 -1
- package/dist/react/index.d.mts +25 -0
- package/dist/react/index.d.ts +25 -0
- package/dist/react/index.js +90 -9
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +90 -9
- package/dist/react/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/next/index.mjs
CHANGED
|
@@ -74,15 +74,18 @@ async function computeCodeChallenge(verifier) {
|
|
|
74
74
|
function matchesPath(pathname, paths) {
|
|
75
75
|
return paths.some((path) => {
|
|
76
76
|
if (pathname === path) return true;
|
|
77
|
-
if (path.endsWith("/:path*")
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
if (path.endsWith("/:path*")) {
|
|
78
|
+
const base = path.slice(0, -"/:path*".length);
|
|
79
|
+
return pathname === base || pathname.startsWith(base + "/");
|
|
80
|
+
}
|
|
81
|
+
return pathname.startsWith(path + "/");
|
|
80
82
|
});
|
|
81
83
|
}
|
|
82
84
|
var introspectionCache = /* @__PURE__ */ new Map();
|
|
83
85
|
var INTROSPECT_CACHE_TTL_MS = 3e4;
|
|
84
86
|
var INTROSPECT_CACHE_MAX_ENTRIES = 500;
|
|
85
87
|
var INTROSPECT_TIMEOUT_MS = 5e3;
|
|
88
|
+
var warnedIntrospectClientAuthFailure = false;
|
|
86
89
|
async function introspectCacheKey(token, clientId) {
|
|
87
90
|
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(token));
|
|
88
91
|
const hex = Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
@@ -136,8 +139,13 @@ async function introspectAccessToken(token, ssoBaseUrl, clientId, clientSecret)
|
|
|
136
139
|
if (res.status !== 401 && res.status !== 403) {
|
|
137
140
|
return "unreachable";
|
|
138
141
|
}
|
|
139
|
-
|
|
140
|
-
|
|
142
|
+
if (!warnedIntrospectClientAuthFailure) {
|
|
143
|
+
warnedIntrospectClientAuthFailure = true;
|
|
144
|
+
console.warn(
|
|
145
|
+
`[SSO SDK] introspection \u5BA2\u6237\u7AEF\u8BA4\u8BC1\u5931\u8D25\uFF08HTTP ${res.status}\uFF09\uFF1A\u8BF7\u68C0\u67E5 clientId/clientSecret \u4E0E\u4E3B\u7AD9\u6CE8\u518C\u4FE1\u606F\u662F\u5426\u4E00\u81F4\u3002\u8BE5\u6545\u969C\u4E0D\u4F1A\u88AB\u7F13\u5B58\u4E3A token \u5931\u6548\uFF0C\u4E5F\u4E0D\u4F1A\u89E6\u53D1\u9759\u9ED8\u91CD\u767B\u5F55\u5FAA\u73AF\u3002`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return "unreachable";
|
|
141
149
|
}
|
|
142
150
|
const data = await res.json();
|
|
143
151
|
const active = data.active === true;
|
|
@@ -180,7 +188,7 @@ function createSsoMiddleware(config) {
|
|
|
180
188
|
}
|
|
181
189
|
return async function ssoMiddleware(request) {
|
|
182
190
|
const { pathname } = request.nextUrl;
|
|
183
|
-
if (pathname.startsWith("/_next/") || pathname.startsWith("/favicon.ico") || pathname.match(/\.(ico|png|jpg|jpeg|svg|css|js|woff2?)$/)) {
|
|
191
|
+
if (!pathname.startsWith("/api/") && (pathname.startsWith("/_next/") || pathname.startsWith("/favicon.ico") || pathname.match(/\.(ico|png|jpg|jpeg|svg|css|js|woff2?)$/))) {
|
|
184
192
|
return NextResponse.next();
|
|
185
193
|
}
|
|
186
194
|
if (pathname === callbackPath) {
|
|
@@ -499,7 +507,10 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
|
|
|
499
507
|
if (Date.now() >= payload.exp * 1e3 + 6e4) {
|
|
500
508
|
throw new SsoError("id_token_expired", "ID Token \u5DF2\u8FC7\u671F");
|
|
501
509
|
}
|
|
502
|
-
if (typeof payload.iat
|
|
510
|
+
if (typeof payload.iat !== "number") {
|
|
511
|
+
throw new SsoError("id_token_invalid", "ID Token \u7F3A\u5C11 iat \u58F0\u660E");
|
|
512
|
+
}
|
|
513
|
+
if (payload.iat * 1e3 > Date.now() + 6e4) {
|
|
503
514
|
throw new SsoError("id_token_invalid", "ID Token iat \u5728\u672A\u6765\uFF0C\u7591\u4F3C\u4F2A\u9020\u6216\u65F6\u949F\u5F02\u5E38");
|
|
504
515
|
}
|
|
505
516
|
if (typeof payload.sub !== "string" || !payload.sub) {
|
|
@@ -516,10 +527,27 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
|
|
|
516
527
|
if (!tokenNonce || !timingSafeEqualString(expectedNonce, tokenNonce)) {
|
|
517
528
|
throw new SsoError("id_token_nonce_mismatch", "ID Token nonce \u4E0D\u5339\u914D");
|
|
518
529
|
}
|
|
530
|
+
} else if (typeof payload.nonce === "string" && payload.nonce) {
|
|
531
|
+
throw new SsoError(
|
|
532
|
+
"id_token_nonce_mismatch",
|
|
533
|
+
"ID Token \u643A\u5E26 nonce \u4F46\u672C\u5730\u65E0\u671F\u671B\u503C\uFF08\u53EF\u80FD\u975E\u672C\u6B21\u767B\u5F55\u4F1A\u8BDD\u7B7E\u53D1\uFF09"
|
|
534
|
+
);
|
|
519
535
|
}
|
|
520
536
|
return { sub: payload.sub };
|
|
521
537
|
}
|
|
522
538
|
|
|
539
|
+
// src/core/http.ts
|
|
540
|
+
var REQUEST_TIMEOUT_MS = 1e4;
|
|
541
|
+
async function fetchWithTimeout(input, init = {}, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
542
|
+
const controller = new AbortController();
|
|
543
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
544
|
+
try {
|
|
545
|
+
return await fetch(input, { ...init, signal: controller.signal });
|
|
546
|
+
} finally {
|
|
547
|
+
clearTimeout(timer);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
523
551
|
// src/next/callback.ts
|
|
524
552
|
function escapeHtml(value) {
|
|
525
553
|
return value.replace(/[&<>"']/g, (ch) => {
|
|
@@ -744,11 +772,15 @@ function createCallbackRouteHandler(config) {
|
|
|
744
772
|
await new Promise((r) => setTimeout(r, 1e3 * Math.pow(2, attempt - 1)));
|
|
745
773
|
}
|
|
746
774
|
try {
|
|
747
|
-
res = await
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
775
|
+
res = await fetchWithTimeout(
|
|
776
|
+
tokenEndpoint,
|
|
777
|
+
{
|
|
778
|
+
method: "POST",
|
|
779
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
780
|
+
body: body.toString()
|
|
781
|
+
},
|
|
782
|
+
1e4
|
|
783
|
+
);
|
|
752
784
|
lastError = null;
|
|
753
785
|
break;
|
|
754
786
|
} catch (err) {
|
|
@@ -982,7 +1014,7 @@ function createLogoutRouteHandler(config) {
|
|
|
982
1014
|
const returnedState = request.nextUrl.searchParams.get("state");
|
|
983
1015
|
if (returnedState) {
|
|
984
1016
|
const savedState = request.cookies.get(logoutStateCookieName)?.value;
|
|
985
|
-
if (!savedState || savedState
|
|
1017
|
+
if (!savedState || !timingSafeEqualString(savedState, returnedState)) {
|
|
986
1018
|
return NextResponse3.json(
|
|
987
1019
|
{ error: "invalid_request", error_description: "Logout state \u4E0D\u5339\u914D" },
|
|
988
1020
|
{ status: 400 }
|
|
@@ -1098,8 +1130,19 @@ import { NextResponse as NextResponse4 } from "next/server";
|
|
|
1098
1130
|
var BACKCHANNEL_LOGOUT_EVENT = "http://schemas.openid.net/event/backchannel-logout";
|
|
1099
1131
|
var JTI_CACHE_CAPACITY = 1e3;
|
|
1100
1132
|
var seenJti = /* @__PURE__ */ new Map();
|
|
1101
|
-
function releaseLogoutTokenJti(
|
|
1102
|
-
|
|
1133
|
+
async function releaseLogoutTokenJti(replayKey, options = {}) {
|
|
1134
|
+
const store = options.jtiStore;
|
|
1135
|
+
if (store?.remove) {
|
|
1136
|
+
await store.remove(replayKey);
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
if (store) {
|
|
1140
|
+
console.warn(
|
|
1141
|
+
"[SSO SDK] \u81EA\u5B9A\u4E49 jtiStore \u672A\u5B9E\u73B0 remove()\uFF1AonLogout \u5931\u8D25\u540E\u65E0\u6CD5\u91CA\u653E jti\uFF0CIdP \u7528\u540C\u4E00 logout_token \u91CD\u6295\u5C06\u88AB\u91CD\u653E\u68C0\u67E5\u62D2\u7EDD\uFF08logout_token_replay\uFF09"
|
|
1142
|
+
);
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
seenJti.delete(replayKey);
|
|
1103
1146
|
}
|
|
1104
1147
|
function recordJti(jti, expiresAtMs) {
|
|
1105
1148
|
const now = Date.now();
|
|
@@ -1116,7 +1159,7 @@ function recordJti(jti, expiresAtMs) {
|
|
|
1116
1159
|
seenJti.set(jti, expiresAtMs);
|
|
1117
1160
|
return true;
|
|
1118
1161
|
}
|
|
1119
|
-
async function verifyLogoutTokenDetailed(logoutToken, ssoBaseUrl, clientId) {
|
|
1162
|
+
async function verifyLogoutTokenDetailed(logoutToken, ssoBaseUrl, clientId, options = {}) {
|
|
1120
1163
|
const baseUrl = ssoBaseUrl.replace(/\/+$/, "");
|
|
1121
1164
|
const header = decodeJwtHeader(logoutToken);
|
|
1122
1165
|
if (!header) {
|
|
@@ -1225,10 +1268,27 @@ async function verifyLogoutTokenDetailed(logoutToken, ssoBaseUrl, clientId) {
|
|
|
1225
1268
|
if (!jti) {
|
|
1226
1269
|
throw new SsoError("logout_token_invalid", "Logout Token \u7F3A\u5C11 jti \u58F0\u660E");
|
|
1227
1270
|
}
|
|
1228
|
-
|
|
1271
|
+
const replayKey = `${normalizedIssuer}:${jti}`;
|
|
1272
|
+
const replayTtlSeconds = Math.max(
|
|
1273
|
+
1,
|
|
1274
|
+
Math.ceil((payload.exp * 1e3 + 6e4 - Date.now()) / 1e3)
|
|
1275
|
+
);
|
|
1276
|
+
const jtiStore = options.jtiStore;
|
|
1277
|
+
if (jtiStore) {
|
|
1278
|
+
if (jtiStore.addIfAbsent) {
|
|
1279
|
+
if (!await jtiStore.addIfAbsent(replayKey, replayTtlSeconds)) {
|
|
1280
|
+
throw new SsoError("logout_token_replay", "Logout Token jti \u91CD\u653E");
|
|
1281
|
+
}
|
|
1282
|
+
} else {
|
|
1283
|
+
if (await jtiStore.has(replayKey)) {
|
|
1284
|
+
throw new SsoError("logout_token_replay", "Logout Token jti \u91CD\u653E");
|
|
1285
|
+
}
|
|
1286
|
+
await jtiStore.add(replayKey, replayTtlSeconds);
|
|
1287
|
+
}
|
|
1288
|
+
} else if (!recordJti(replayKey, payload.exp * 1e3 + 6e4)) {
|
|
1229
1289
|
throw new SsoError("logout_token_replay", "Logout Token jti \u91CD\u653E");
|
|
1230
1290
|
}
|
|
1231
|
-
return { payload: { sub, sid }, jti };
|
|
1291
|
+
return { payload: { sub, sid }, jti, replayKey };
|
|
1232
1292
|
}
|
|
1233
1293
|
|
|
1234
1294
|
// src/next/backchannel-logout.ts
|
|
@@ -1270,7 +1330,9 @@ function createBackchannelLogoutRouteHandler(config) {
|
|
|
1270
1330
|
}
|
|
1271
1331
|
let verified;
|
|
1272
1332
|
try {
|
|
1273
|
-
verified = await verifyLogoutTokenDetailed(logoutToken, ssoBaseUrl, clientId
|
|
1333
|
+
verified = await verifyLogoutTokenDetailed(logoutToken, ssoBaseUrl, clientId, {
|
|
1334
|
+
jtiStore: config.jtiStore
|
|
1335
|
+
});
|
|
1274
1336
|
} catch (err) {
|
|
1275
1337
|
const code = err instanceof SsoError ? err.code : "logout_token_invalid";
|
|
1276
1338
|
const description = err instanceof SsoError ? err.description : "Logout Token \u9A8C\u8BC1\u5931\u8D25";
|
|
@@ -1282,7 +1344,7 @@ function createBackchannelLogoutRouteHandler(config) {
|
|
|
1282
1344
|
try {
|
|
1283
1345
|
await onLogout?.(verified.payload, request);
|
|
1284
1346
|
} catch (err) {
|
|
1285
|
-
releaseLogoutTokenJti(verified.
|
|
1347
|
+
await releaseLogoutTokenJti(verified.replayKey, { jtiStore: config.jtiStore });
|
|
1286
1348
|
console.error(
|
|
1287
1349
|
"[SSO SDK] backchannel logout onLogout \u94A9\u5B50\u6267\u884C\u5931\u8D25:",
|
|
1288
1350
|
err
|