@nihplod/sso-sdk 1.3.1 → 1.4.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 +46 -5
- package/dist/index.d.mts +25 -5
- package/dist/index.d.ts +25 -5
- package/dist/index.js +64 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +64 -4
- package/dist/index.mjs.map +1 -1
- package/dist/next/index.d.mts +73 -3
- package/dist/next/index.d.ts +73 -3
- package/dist/next/index.js +213 -2
- package/dist/next/index.js.map +1 -1
- package/dist/next/index.mjs +212 -2
- package/dist/next/index.mjs.map +1 -1
- package/dist/react/index.d.mts +25 -5
- package/dist/react/index.d.ts +25 -5
- package/dist/react/index.js +72 -4
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +72 -4
- package/dist/react/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/next/index.mjs
CHANGED
|
@@ -751,6 +751,9 @@ function buildLogoutConfirmHtml() {
|
|
|
751
751
|
<body style="display:flex;align-items:center;justify-content:center;min-height:100vh;font-family:system-ui,sans-serif;">
|
|
752
752
|
<form method="post" style="text-align:center;">
|
|
753
753
|
<p>\u786E\u5B9A\u8981\u9000\u51FA\u767B\u5F55\u5417\uFF1F</p>
|
|
754
|
+
<label style="display:flex;align-items:center;justify-content:center;gap:6px;margin:12px 0;font-size:14px;color:#374151;cursor:pointer;">
|
|
755
|
+
<input type="checkbox" name="global" value="1">\u540C\u65F6\u9000\u51FA\u6240\u6709 NIHPLOD \u5E73\u53F0
|
|
756
|
+
</label>
|
|
754
757
|
<button type="submit" style="padding:10px 20px;background-color:#ef4444;color:white;border:none;border-radius:8px;cursor:pointer;font-size:14px;">\u9000\u51FA\u767B\u5F55</button>
|
|
755
758
|
<p><a href="/" style="color:#2563eb;text-decoration:underline;">\u53D6\u6D88\u5E76\u8FD4\u56DE\u9996\u9875</a></p>
|
|
756
759
|
</form>
|
|
@@ -778,10 +781,18 @@ function createLogoutRouteHandler(config) {
|
|
|
778
781
|
redirectUri,
|
|
779
782
|
clientSecret,
|
|
780
783
|
postLogoutRedirectUri = new URL(redirectUri).origin + "/",
|
|
781
|
-
redirectToSso = true,
|
|
782
784
|
callbackPath = "/api/auth/callback",
|
|
783
785
|
insecureLocalDev: insecureLocalDevOpt = false
|
|
784
786
|
} = config;
|
|
787
|
+
let defaultScope = config.defaultScope ?? "local";
|
|
788
|
+
if (config.redirectToSso !== void 0) {
|
|
789
|
+
console.warn(
|
|
790
|
+
'[SSO SDK] redirectToSso \u5DF2\u5F03\u7528\uFF0C\u8BF7\u6539\u7528 defaultScope\uFF08redirectToSso: true \u2192 defaultScope: "global"\uFF0Cfalse \u2192 "local"\uFF09\u3002\u6CE8\u610F\uFF1A\u9ED8\u8BA4\u9000\u51FA\u8303\u56F4\u5DF2\u53D8\u66F4\u4E3A "local"\uFF08\u4EC5\u9000\u51FA\u672C\u7AD9\uFF0C\u4E0D\u8DF3\u8F6C SSO \u4E2D\u5FC3\uFF09\u3002'
|
|
791
|
+
);
|
|
792
|
+
if (config.defaultScope === void 0) {
|
|
793
|
+
defaultScope = config.redirectToSso ? "global" : "local";
|
|
794
|
+
}
|
|
795
|
+
}
|
|
785
796
|
const insecureLocalDev = resolveInsecureLocalDev(insecureLocalDevOpt, ssoBaseUrl);
|
|
786
797
|
const secureCookies = !insecureLocalDev;
|
|
787
798
|
const pickName = (explicit, fallback) => insecureLocalDev ? toInsecureCookieName(explicit ?? fallback) : explicit ?? fallback;
|
|
@@ -816,6 +827,12 @@ function createLogoutRouteHandler(config) {
|
|
|
816
827
|
headers: { "content-type": "text/html; charset=utf-8" }
|
|
817
828
|
});
|
|
818
829
|
}
|
|
830
|
+
let formGlobal = null;
|
|
831
|
+
try {
|
|
832
|
+
formGlobal = new URLSearchParams(await request.text()).get("global");
|
|
833
|
+
} catch {
|
|
834
|
+
}
|
|
835
|
+
const effectiveScope = formGlobal !== null ? formGlobal === "1" || formGlobal === "true" ? "global" : "local" : defaultScope;
|
|
819
836
|
const refreshToken = request.cookies.get(refreshTokenCookieName)?.value;
|
|
820
837
|
const idTokenHint = request.cookies.get(idTokenCookieName)?.value;
|
|
821
838
|
if (refreshToken) {
|
|
@@ -850,7 +867,7 @@ function createLogoutRouteHandler(config) {
|
|
|
850
867
|
res.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, callbackPath, secureCookies));
|
|
851
868
|
return res;
|
|
852
869
|
};
|
|
853
|
-
if (
|
|
870
|
+
if (effectiveScope === "global") {
|
|
854
871
|
const discovery = await fetchDiscoveryCached(normalizedServerBase);
|
|
855
872
|
const endSessionEndpoint = discovery?.end_session_endpoint || `${normalizedBase}/api/oauth/end-session`;
|
|
856
873
|
const logoutUrl = new URL(endSessionEndpoint);
|
|
@@ -873,6 +890,198 @@ function createLogoutRouteHandler(config) {
|
|
|
873
890
|
);
|
|
874
891
|
};
|
|
875
892
|
}
|
|
893
|
+
|
|
894
|
+
// src/next/backchannel-logout.ts
|
|
895
|
+
import { NextResponse as NextResponse4 } from "next/server";
|
|
896
|
+
|
|
897
|
+
// src/core/logout-token.ts
|
|
898
|
+
var BACKCHANNEL_LOGOUT_EVENT = "http://schemas.openid.net/event/backchannel-logout";
|
|
899
|
+
var JTI_CACHE_CAPACITY = 1e3;
|
|
900
|
+
var seenJti = /* @__PURE__ */ new Map();
|
|
901
|
+
function recordJti(jti, expiresAtMs) {
|
|
902
|
+
const now = Date.now();
|
|
903
|
+
const existing = seenJti.get(jti);
|
|
904
|
+
if (existing !== void 0 && existing > now) return false;
|
|
905
|
+
for (const [key, exp] of seenJti) {
|
|
906
|
+
if (exp <= now) seenJti.delete(key);
|
|
907
|
+
}
|
|
908
|
+
while (seenJti.size >= JTI_CACHE_CAPACITY) {
|
|
909
|
+
const oldest = seenJti.keys().next().value;
|
|
910
|
+
if (oldest === void 0) break;
|
|
911
|
+
seenJti.delete(oldest);
|
|
912
|
+
}
|
|
913
|
+
seenJti.set(jti, expiresAtMs);
|
|
914
|
+
return true;
|
|
915
|
+
}
|
|
916
|
+
async function verifyLogoutToken(logoutToken, ssoBaseUrl, clientId) {
|
|
917
|
+
const baseUrl = ssoBaseUrl.replace(/\/+$/, "");
|
|
918
|
+
const header = decodeJwtHeader(logoutToken);
|
|
919
|
+
if (!header) {
|
|
920
|
+
throw new SsoError("logout_token_invalid", "Logout Token \u683C\u5F0F\u9519\u8BEF");
|
|
921
|
+
}
|
|
922
|
+
if (header.alg !== "RS256") {
|
|
923
|
+
throw new SsoError(
|
|
924
|
+
"logout_token_unsupported_alg",
|
|
925
|
+
`\u4E0D\u652F\u6301\u7684 Logout Token \u7B7E\u540D\u7B97\u6CD5: ${String(header.alg)}`
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
const discovery = await fetchDiscoveryDoc(baseUrl);
|
|
929
|
+
const normalizedIssuer = (discovery?.issuer || baseUrl).replace(/\/+$/, "");
|
|
930
|
+
const jwks = await fetchJwks(baseUrl);
|
|
931
|
+
if (!jwks) {
|
|
932
|
+
throw new SsoError(
|
|
933
|
+
"logout_token_invalid_signature",
|
|
934
|
+
"\u65E0\u6CD5\u83B7\u53D6 JWKS \u9A8C\u8BC1 Logout Token \u7B7E\u540D"
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
const kid = typeof header.kid === "string" ? header.kid : void 0;
|
|
938
|
+
const matchCandidates = (set) => set.keys.filter(
|
|
939
|
+
(k) => k.kty === "RSA" && k.alg === "RS256" && k.use === "sig" && (kid ? k.kid === kid : true)
|
|
940
|
+
);
|
|
941
|
+
const verifyAny = async (keys) => {
|
|
942
|
+
for (const key of keys) {
|
|
943
|
+
if (await verifyRs256Signature(logoutToken, key)) return true;
|
|
944
|
+
}
|
|
945
|
+
return false;
|
|
946
|
+
};
|
|
947
|
+
let candidates = matchCandidates(jwks);
|
|
948
|
+
let validSig = candidates.length > 0 ? await verifyAny(candidates) : false;
|
|
949
|
+
if (candidates.length === 0 || !validSig) {
|
|
950
|
+
const freshJwks = await fetchJwks(baseUrl, { forceRefresh: true });
|
|
951
|
+
if (freshJwks) {
|
|
952
|
+
candidates = matchCandidates(freshJwks);
|
|
953
|
+
validSig = candidates.length > 0 ? await verifyAny(candidates) : false;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
if (candidates.length === 0) {
|
|
957
|
+
throw new SsoError(
|
|
958
|
+
"logout_token_invalid_signature",
|
|
959
|
+
"JWKS \u4E2D\u672A\u627E\u5230\u5339\u914D\u7684 RS256 \u516C\u94A5"
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
if (!validSig) {
|
|
963
|
+
throw new SsoError(
|
|
964
|
+
"logout_token_invalid_signature",
|
|
965
|
+
"Logout Token \u7B7E\u540D\u9A8C\u8BC1\u5931\u8D25"
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
const payload = decodeJwtPayload(logoutToken);
|
|
969
|
+
if (!payload) {
|
|
970
|
+
throw new SsoError("logout_token_invalid", "Logout Token payload \u89E3\u6790\u5931\u8D25");
|
|
971
|
+
}
|
|
972
|
+
const tokenIssuer = typeof payload.iss === "string" ? payload.iss.replace(/\/+$/, "") : "";
|
|
973
|
+
if (tokenIssuer !== normalizedIssuer) {
|
|
974
|
+
throw new SsoError(
|
|
975
|
+
"logout_token_issuer_mismatch",
|
|
976
|
+
"Logout Token issuer \u4E0D\u5339\u914D"
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
const aud = payload.aud;
|
|
980
|
+
const audList = Array.isArray(aud) ? aud : typeof aud === "string" ? [aud] : [];
|
|
981
|
+
if (!audList.includes(clientId)) {
|
|
982
|
+
throw new SsoError(
|
|
983
|
+
"logout_token_audience_mismatch",
|
|
984
|
+
"Logout Token audience \u4E0D\u5339\u914D"
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
if (typeof payload.exp !== "number") {
|
|
988
|
+
throw new SsoError("logout_token_invalid", "Logout Token \u7F3A\u5C11 exp \u58F0\u660E");
|
|
989
|
+
}
|
|
990
|
+
if (Date.now() >= payload.exp * 1e3 + 6e4) {
|
|
991
|
+
throw new SsoError("logout_token_expired", "Logout Token \u5DF2\u8FC7\u671F");
|
|
992
|
+
}
|
|
993
|
+
const events = payload.events;
|
|
994
|
+
if (!events || typeof events !== "object" || !(BACKCHANNEL_LOGOUT_EVENT in events)) {
|
|
995
|
+
throw new SsoError(
|
|
996
|
+
"logout_token_invalid",
|
|
997
|
+
"Logout Token \u7F3A\u5C11 backchannel-logout events \u58F0\u660E"
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
const sub = typeof payload.sub === "string" && payload.sub ? payload.sub : void 0;
|
|
1001
|
+
const sid = typeof payload.sid === "string" && payload.sid ? payload.sid : void 0;
|
|
1002
|
+
if (!sub && !sid) {
|
|
1003
|
+
throw new SsoError(
|
|
1004
|
+
"logout_token_invalid",
|
|
1005
|
+
"Logout Token \u5FC5\u987B\u5305\u542B sub \u6216 sid \u81F3\u5C11\u5176\u4E00"
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
const jti = typeof payload.jti === "string" ? payload.jti : "";
|
|
1009
|
+
if (!jti) {
|
|
1010
|
+
throw new SsoError("logout_token_invalid", "Logout Token \u7F3A\u5C11 jti \u58F0\u660E");
|
|
1011
|
+
}
|
|
1012
|
+
if (!recordJti(jti, payload.exp * 1e3 + 6e4)) {
|
|
1013
|
+
throw new SsoError("logout_token_replay", "Logout Token jti \u91CD\u653E");
|
|
1014
|
+
}
|
|
1015
|
+
return { sub, sid };
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// src/next/backchannel-logout.ts
|
|
1019
|
+
function createBackchannelLogoutRouteHandler(config) {
|
|
1020
|
+
const { clientId, ssoBaseUrl, onLogout } = config;
|
|
1021
|
+
const insecureLocalDev = resolveInsecureLocalDev(
|
|
1022
|
+
config.insecureLocalDev ?? false,
|
|
1023
|
+
ssoBaseUrl
|
|
1024
|
+
);
|
|
1025
|
+
const secureCookies = !insecureLocalDev;
|
|
1026
|
+
const pickName = (explicit, fallback) => insecureLocalDev ? toInsecureCookieName(explicit ?? fallback) : explicit ?? fallback;
|
|
1027
|
+
const accessTokenCookieName = pickName(
|
|
1028
|
+
config.accessTokenCookieName,
|
|
1029
|
+
DEFAULT_ACCESS_TOKEN_COOKIE_NAME
|
|
1030
|
+
);
|
|
1031
|
+
const refreshTokenCookieName = pickName(
|
|
1032
|
+
config.refreshTokenCookieName,
|
|
1033
|
+
DEFAULT_REFRESH_TOKEN_COOKIE_NAME
|
|
1034
|
+
);
|
|
1035
|
+
const idTokenCookieName = pickName(
|
|
1036
|
+
config.idTokenCookieName,
|
|
1037
|
+
DEFAULT_ID_TOKEN_COOKIE_NAME
|
|
1038
|
+
);
|
|
1039
|
+
return async function handler(request) {
|
|
1040
|
+
if (request.method !== "POST") {
|
|
1041
|
+
return NextResponse4.json(
|
|
1042
|
+
{ error: "method_not_allowed", error_description: "\u4EC5\u63A5\u53D7 POST \u8BF7\u6C42" },
|
|
1043
|
+
{ status: 405 }
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
const logoutToken = new URLSearchParams(await request.text()).get(
|
|
1047
|
+
"logout_token"
|
|
1048
|
+
);
|
|
1049
|
+
if (!logoutToken) {
|
|
1050
|
+
return NextResponse4.json(
|
|
1051
|
+
{ error: "invalid_request", error_description: "\u7F3A\u5C11 logout_token" },
|
|
1052
|
+
{ status: 400 }
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
let payload;
|
|
1056
|
+
try {
|
|
1057
|
+
payload = await verifyLogoutToken(logoutToken, ssoBaseUrl, clientId);
|
|
1058
|
+
} catch (err) {
|
|
1059
|
+
const code = err instanceof SsoError ? err.code : "logout_token_invalid";
|
|
1060
|
+
const description = err instanceof SsoError ? err.description : "Logout Token \u9A8C\u8BC1\u5931\u8D25";
|
|
1061
|
+
return NextResponse4.json(
|
|
1062
|
+
{ error: code, error_description: description },
|
|
1063
|
+
{ status: 400 }
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
try {
|
|
1067
|
+
await onLogout?.(payload, request);
|
|
1068
|
+
} catch (err) {
|
|
1069
|
+
console.error(
|
|
1070
|
+
"[SSO SDK] backchannel logout onLogout \u94A9\u5B50\u6267\u884C\u5931\u8D25:",
|
|
1071
|
+
err
|
|
1072
|
+
);
|
|
1073
|
+
return NextResponse4.json(
|
|
1074
|
+
{ error: "server_error", error_description: "onLogout \u5904\u7406\u5931\u8D25" },
|
|
1075
|
+
{ status: 500 }
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
const res = new NextResponse4(null, { status: 200 });
|
|
1079
|
+
res.cookies.set(accessTokenCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
1080
|
+
res.cookies.set(refreshTokenCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
1081
|
+
res.cookies.set(idTokenCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
1082
|
+
return res;
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
876
1085
|
export {
|
|
877
1086
|
DEFAULT_ACCESS_TOKEN_COOKIE_NAME,
|
|
878
1087
|
DEFAULT_ID_TOKEN_COOKIE_NAME,
|
|
@@ -882,6 +1091,7 @@ export {
|
|
|
882
1091
|
DEFAULT_RETURN_COOKIE_NAME,
|
|
883
1092
|
DEFAULT_STATE_COOKIE_NAME,
|
|
884
1093
|
DEFAULT_VERIFIER_COOKIE_NAME,
|
|
1094
|
+
createBackchannelLogoutRouteHandler,
|
|
885
1095
|
createCallbackRouteHandler,
|
|
886
1096
|
createLogoutRouteHandler,
|
|
887
1097
|
createSsoMiddleware,
|