@nihplod/sso-sdk 1.6.0 → 1.7.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/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +69 -78
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +69 -78
- package/dist/index.mjs.map +1 -1
- package/dist/next/index.d.mts +50 -1
- package/dist/next/index.d.ts +50 -1
- package/dist/next/index.js +260 -56
- package/dist/next/index.js.map +1 -1
- package/dist/next/index.mjs +259 -56
- package/dist/next/index.mjs.map +1 -1
- package/dist/react/index.d.mts +7 -2
- package/dist/react/index.d.ts +7 -2
- package/dist/react/index.js +106 -96
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +106 -96
- package/dist/react/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/next/index.mjs
CHANGED
|
@@ -86,6 +86,7 @@ var INTROSPECT_CACHE_TTL_MS = 3e4;
|
|
|
86
86
|
var INTROSPECT_CACHE_MAX_ENTRIES = 500;
|
|
87
87
|
var INTROSPECT_TIMEOUT_MS = 5e3;
|
|
88
88
|
var warnedIntrospectClientAuthFailure = false;
|
|
89
|
+
var warnedIntrospectRateLimited = false;
|
|
89
90
|
async function introspectCacheKey(token, clientId) {
|
|
90
91
|
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(token));
|
|
91
92
|
const hex = Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
@@ -136,6 +137,15 @@ async function introspectAccessToken(token, ssoBaseUrl, clientId, clientSecret)
|
|
|
136
137
|
signal: AbortSignal.timeout(INTROSPECT_TIMEOUT_MS)
|
|
137
138
|
});
|
|
138
139
|
if (!res.ok) {
|
|
140
|
+
if (res.status === 429) {
|
|
141
|
+
if (!warnedIntrospectRateLimited) {
|
|
142
|
+
warnedIntrospectRateLimited = true;
|
|
143
|
+
console.warn(
|
|
144
|
+
"[SSO SDK] introspection \u88AB\u9650\u6D41\uFF08HTTP 429\uFF09\uFF1A\u8BF7\u4E3A\u5B50\u7AD9 BFF \u914D\u7F6E\u72EC\u7ACB\u7684 client \u7EA7\u914D\u989D\uFF0C\u6216\u964D\u4F4E middleware introspection \u9891\u7387\u3002\u672C\u6B21\u6309 unreachable \u5904\u7406\uFF08\u9ED8\u8BA4 fail-open\uFF09\u3002"
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
return "unreachable";
|
|
148
|
+
}
|
|
139
149
|
if (res.status !== 401 && res.status !== 403) {
|
|
140
150
|
return "unreachable";
|
|
141
151
|
}
|
|
@@ -377,7 +387,19 @@ async function fetchJwks(baseUrl, options = {}) {
|
|
|
377
387
|
return cachedJwks.jwks;
|
|
378
388
|
}
|
|
379
389
|
const discovery = await fetchDiscoveryDoc(baseUrl);
|
|
380
|
-
|
|
390
|
+
let jwksUri = `${baseUrl}/api/oauth/jwks`;
|
|
391
|
+
if (discovery?.jwks_uri) {
|
|
392
|
+
try {
|
|
393
|
+
if (new URL(discovery.jwks_uri).origin === new URL(baseUrl).origin) {
|
|
394
|
+
jwksUri = discovery.jwks_uri;
|
|
395
|
+
} else {
|
|
396
|
+
console.warn(
|
|
397
|
+
"[SSO SDK] Discovery \u7684 jwks_uri \u4E0E ssoBaseUrl \u4E0D\u540C\u6E90\uFF0C\u5DF2\u5FFD\u7565\u5E76\u4F7F\u7528\u9ED8\u8BA4 JWKS \u7AEF\u70B9"
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
} catch {
|
|
401
|
+
}
|
|
402
|
+
}
|
|
381
403
|
try {
|
|
382
404
|
const res = await fetch(jwksUri, {
|
|
383
405
|
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
@@ -648,8 +670,14 @@ function createCallbackRouteHandler(config) {
|
|
|
648
670
|
const insecureLocalDev = resolveInsecureLocalDev(insecureLocalDevOpt, ssoBaseUrl);
|
|
649
671
|
const secureCookies = !insecureLocalDev;
|
|
650
672
|
const pickName = (explicit, fallback) => insecureLocalDev ? toInsecureCookieName(explicit ?? fallback) : explicit ?? fallback;
|
|
651
|
-
const accessTokenCookieName = pickName(
|
|
652
|
-
|
|
673
|
+
const accessTokenCookieName = pickName(
|
|
674
|
+
config.accessTokenCookieName,
|
|
675
|
+
DEFAULT_ACCESS_TOKEN_COOKIE_NAME
|
|
676
|
+
);
|
|
677
|
+
const refreshTokenCookieName = pickName(
|
|
678
|
+
config.refreshTokenCookieName,
|
|
679
|
+
DEFAULT_REFRESH_TOKEN_COOKIE_NAME
|
|
680
|
+
);
|
|
653
681
|
const idTokenCookieName = pickName(config.idTokenCookieName, DEFAULT_ID_TOKEN_COOKIE_NAME);
|
|
654
682
|
const stateCookieName = pickName(config.stateCookieName, DEFAULT_STATE_COOKIE_NAME);
|
|
655
683
|
const nonceCookieName = pickName(config.nonceCookieName, DEFAULT_NONCE_COOKIE_NAME);
|
|
@@ -683,13 +711,7 @@ function createCallbackRouteHandler(config) {
|
|
|
683
711
|
const error = searchParams.get("error");
|
|
684
712
|
if (error) {
|
|
685
713
|
const desc = searchParams.get("error_description") || error;
|
|
686
|
-
return buildErrorResponse(
|
|
687
|
-
request,
|
|
688
|
-
400,
|
|
689
|
-
"authorization_failed",
|
|
690
|
-
desc,
|
|
691
|
-
config.renderErrorPage
|
|
692
|
-
);
|
|
714
|
+
return buildErrorResponse(request, 400, "authorization_failed", desc, config.renderErrorPage);
|
|
693
715
|
}
|
|
694
716
|
const code = searchParams.get("code");
|
|
695
717
|
const returnedState = searchParams.get("state");
|
|
@@ -743,6 +765,20 @@ function createCallbackRouteHandler(config) {
|
|
|
743
765
|
config.renderErrorPage
|
|
744
766
|
);
|
|
745
767
|
}
|
|
768
|
+
const returnedIss = searchParams.get("iss");
|
|
769
|
+
if (returnedIss) {
|
|
770
|
+
const discovery = await fetchDiscoveryDoc(normalizedBase);
|
|
771
|
+
const expectedIssuer = (discovery?.issuer || normalizedBase).replace(/\/+$/, "");
|
|
772
|
+
if (returnedIss.replace(/\/+$/, "") !== expectedIssuer) {
|
|
773
|
+
return buildErrorResponse(
|
|
774
|
+
request,
|
|
775
|
+
400,
|
|
776
|
+
"issuer_mismatch",
|
|
777
|
+
"\u6388\u6743\u54CD\u5E94 issuer \u4E0D\u5339\u914D\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u767B\u5F55",
|
|
778
|
+
config.renderErrorPage
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
746
782
|
const expectedNonce = readTransientCookie(nonceCookieName);
|
|
747
783
|
const verifier = readTransientCookie(verifierCookieName);
|
|
748
784
|
if (!verifier) {
|
|
@@ -766,35 +802,18 @@ function createCallbackRouteHandler(config) {
|
|
|
766
802
|
}
|
|
767
803
|
let res = null;
|
|
768
804
|
let lastError;
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
},
|
|
782
|
-
1e4
|
|
783
|
-
);
|
|
784
|
-
lastError = null;
|
|
785
|
-
break;
|
|
786
|
-
} catch (err) {
|
|
787
|
-
lastError = err;
|
|
788
|
-
if (attempt >= maxRetries) {
|
|
789
|
-
return buildErrorResponse(
|
|
790
|
-
request,
|
|
791
|
-
502,
|
|
792
|
-
"server_error",
|
|
793
|
-
"\u767B\u5F55\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF08Token \u8BF7\u6C42\u5931\u8D25\uFF09\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
|
|
794
|
-
config.renderErrorPage
|
|
795
|
-
);
|
|
796
|
-
}
|
|
797
|
-
}
|
|
805
|
+
try {
|
|
806
|
+
res = await fetchWithTimeout(
|
|
807
|
+
tokenEndpoint,
|
|
808
|
+
{
|
|
809
|
+
method: "POST",
|
|
810
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
811
|
+
body: body.toString()
|
|
812
|
+
},
|
|
813
|
+
1e4
|
|
814
|
+
);
|
|
815
|
+
} catch (err) {
|
|
816
|
+
lastError = err;
|
|
798
817
|
}
|
|
799
818
|
if (lastError || !res) {
|
|
800
819
|
return buildErrorResponse(
|
|
@@ -905,8 +924,169 @@ function createCallbackRouteHandler(config) {
|
|
|
905
924
|
}
|
|
906
925
|
}
|
|
907
926
|
|
|
908
|
-
// src/next/
|
|
927
|
+
// src/next/refresh.ts
|
|
909
928
|
import { NextResponse as NextResponse3 } from "next/server";
|
|
929
|
+
function noStoreJson(body, status = 200) {
|
|
930
|
+
return NextResponse3.json(body, {
|
|
931
|
+
status,
|
|
932
|
+
headers: { "Cache-Control": "no-store", Pragma: "no-cache" }
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
function clearAuthCookies(response, names, secureCookies) {
|
|
936
|
+
response.cookies.set(names.access, "", getHostCookieOptions(0, secureCookies));
|
|
937
|
+
response.cookies.set(names.refresh, "", getHostCookieOptions(0, secureCookies));
|
|
938
|
+
response.cookies.set(names.idToken, "", getHostCookieOptions(0, secureCookies));
|
|
939
|
+
return response;
|
|
940
|
+
}
|
|
941
|
+
function createRefreshRouteHandler(config) {
|
|
942
|
+
const {
|
|
943
|
+
clientId,
|
|
944
|
+
ssoBaseUrl,
|
|
945
|
+
redirectUri,
|
|
946
|
+
clientSecret,
|
|
947
|
+
insecureLocalDev: insecureLocalDevOpt = false,
|
|
948
|
+
debug = false
|
|
949
|
+
} = config;
|
|
950
|
+
const insecureLocalDev = resolveInsecureLocalDev(insecureLocalDevOpt, ssoBaseUrl);
|
|
951
|
+
const secureCookies = !insecureLocalDev;
|
|
952
|
+
const pickName = (explicit, fallback) => insecureLocalDev ? toInsecureCookieName(explicit ?? fallback) : explicit ?? fallback;
|
|
953
|
+
const accessTokenCookieName = pickName(
|
|
954
|
+
config.accessTokenCookieName,
|
|
955
|
+
DEFAULT_ACCESS_TOKEN_COOKIE_NAME
|
|
956
|
+
);
|
|
957
|
+
const refreshTokenCookieName = pickName(
|
|
958
|
+
config.refreshTokenCookieName,
|
|
959
|
+
DEFAULT_REFRESH_TOKEN_COOKIE_NAME
|
|
960
|
+
);
|
|
961
|
+
const idTokenCookieName = pickName(config.idTokenCookieName, DEFAULT_ID_TOKEN_COOKIE_NAME);
|
|
962
|
+
const normalizedBase = ssoBaseUrl.replace(/\/+$/, "");
|
|
963
|
+
const normalizedServerBase = (config.serverBaseUrl ?? ssoBaseUrl).replace(/\/+$/, "");
|
|
964
|
+
const callbackOrigin = new URL(redirectUri).origin;
|
|
965
|
+
const cookieNames = {
|
|
966
|
+
access: accessTokenCookieName,
|
|
967
|
+
refresh: refreshTokenCookieName,
|
|
968
|
+
idToken: idTokenCookieName
|
|
969
|
+
};
|
|
970
|
+
return async function POST(request) {
|
|
971
|
+
const requestOrigin = request.headers.get("origin");
|
|
972
|
+
if (requestOrigin && requestOrigin !== callbackOrigin) {
|
|
973
|
+
return noStoreJson({ error: "forbidden", error_description: "\u8DE8\u7AD9\u8BF7\u6C42\u88AB\u62D2\u7EDD" }, 403);
|
|
974
|
+
}
|
|
975
|
+
const secFetchSite = request.headers.get("sec-fetch-site");
|
|
976
|
+
if (!requestOrigin && secFetchSite && secFetchSite !== "same-origin" && secFetchSite !== "none") {
|
|
977
|
+
return noStoreJson({ error: "forbidden", error_description: "\u8DE8\u7AD9\u8BF7\u6C42\u88AB\u62D2\u7EDD" }, 403);
|
|
978
|
+
}
|
|
979
|
+
const refreshToken = request.cookies.get(refreshTokenCookieName)?.value;
|
|
980
|
+
if (!refreshToken) {
|
|
981
|
+
return noStoreJson({ error: "no_refresh_token", error_description: "\u7F3A\u5C11 refresh_token" }, 401);
|
|
982
|
+
}
|
|
983
|
+
const body = new URLSearchParams({
|
|
984
|
+
grant_type: "refresh_token",
|
|
985
|
+
refresh_token: refreshToken,
|
|
986
|
+
client_id: clientId
|
|
987
|
+
});
|
|
988
|
+
if (clientSecret) body.set("client_secret", clientSecret);
|
|
989
|
+
let res = null;
|
|
990
|
+
let lastError;
|
|
991
|
+
try {
|
|
992
|
+
res = await fetchWithTimeout(
|
|
993
|
+
`${normalizedServerBase}/api/oauth/token`,
|
|
994
|
+
{
|
|
995
|
+
method: "POST",
|
|
996
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
997
|
+
body: body.toString()
|
|
998
|
+
},
|
|
999
|
+
1e4
|
|
1000
|
+
);
|
|
1001
|
+
} catch (err) {
|
|
1002
|
+
lastError = err;
|
|
1003
|
+
}
|
|
1004
|
+
if (lastError || !res) {
|
|
1005
|
+
if (debug) console.warn("[SSO SDK] refresh: token \u8BF7\u6C42\u5931\u8D25", lastError);
|
|
1006
|
+
return noStoreJson(
|
|
1007
|
+
{ error: "server_error", error_description: "\u5237\u65B0\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528" },
|
|
1008
|
+
502
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
let data = {};
|
|
1012
|
+
try {
|
|
1013
|
+
data = await res.json();
|
|
1014
|
+
} catch {
|
|
1015
|
+
}
|
|
1016
|
+
if (!res.ok) {
|
|
1017
|
+
const error = typeof data.error === "string" ? data.error : "";
|
|
1018
|
+
const status = res.status === 429 ? 429 : res.status >= 500 ? 502 : 401;
|
|
1019
|
+
const response2 = noStoreJson(
|
|
1020
|
+
{
|
|
1021
|
+
error: error || "refresh_failed",
|
|
1022
|
+
error_description: data.error_description || "\u5237\u65B0\u767B\u5F55\u6001\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55"
|
|
1023
|
+
},
|
|
1024
|
+
status
|
|
1025
|
+
);
|
|
1026
|
+
if (res.status === 400 || res.status === 401) {
|
|
1027
|
+
return clearAuthCookies(response2, cookieNames, secureCookies);
|
|
1028
|
+
}
|
|
1029
|
+
return response2;
|
|
1030
|
+
}
|
|
1031
|
+
if (!data.access_token || typeof data.expires_in !== "number" || !Number.isFinite(data.expires_in) || data.expires_in <= 0) {
|
|
1032
|
+
return clearAuthCookies(
|
|
1033
|
+
noStoreJson({ error: "server_error", error_description: "\u5237\u65B0\u54CD\u5E94\u4E0D\u5B8C\u6574" }, 502),
|
|
1034
|
+
cookieNames,
|
|
1035
|
+
secureCookies
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
if (data.id_token) {
|
|
1039
|
+
try {
|
|
1040
|
+
await validateIdToken(
|
|
1041
|
+
data.id_token,
|
|
1042
|
+
data.access_token,
|
|
1043
|
+
normalizedBase,
|
|
1044
|
+
clientId
|
|
1045
|
+
);
|
|
1046
|
+
} catch (err) {
|
|
1047
|
+
if (debug) console.warn("[SSO SDK] refresh: ID Token \u6821\u9A8C\u5931\u8D25", err);
|
|
1048
|
+
return clearAuthCookies(
|
|
1049
|
+
noStoreJson({ error: "id_token_invalid", error_description: "\u767B\u5F55\u6821\u9A8C\u5931\u8D25" }, 401),
|
|
1050
|
+
cookieNames,
|
|
1051
|
+
secureCookies
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
const response = noStoreJson({ success: true, expires_in: data.expires_in });
|
|
1056
|
+
response.cookies.set(accessTokenCookieName, data.access_token, {
|
|
1057
|
+
...getHostCookieOptions(data.expires_in, secureCookies)
|
|
1058
|
+
});
|
|
1059
|
+
response.cookies.set(refreshTokenCookieName, data.refresh_token || refreshToken, {
|
|
1060
|
+
...getHostCookieOptions(
|
|
1061
|
+
typeof data.refresh_expires_in === "number" && data.refresh_expires_in > 0 ? data.refresh_expires_in : 30 * 24 * 60 * 60,
|
|
1062
|
+
secureCookies
|
|
1063
|
+
)
|
|
1064
|
+
});
|
|
1065
|
+
if (data.id_token) {
|
|
1066
|
+
let idTokenMaxAge = 3600;
|
|
1067
|
+
try {
|
|
1068
|
+
const payload = JSON.parse(
|
|
1069
|
+
Buffer.from(data.id_token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/"), "base64").toString(
|
|
1070
|
+
"utf8"
|
|
1071
|
+
)
|
|
1072
|
+
);
|
|
1073
|
+
if (typeof payload.exp === "number") {
|
|
1074
|
+
idTokenMaxAge = payload.exp - Math.floor(Date.now() / 1e3);
|
|
1075
|
+
}
|
|
1076
|
+
} catch {
|
|
1077
|
+
}
|
|
1078
|
+
if (idTokenMaxAge > 0) {
|
|
1079
|
+
response.cookies.set(idTokenCookieName, data.id_token, {
|
|
1080
|
+
...getHostCookieOptions(idTokenMaxAge, secureCookies)
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
return response;
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// src/next/logout.ts
|
|
1089
|
+
import { NextResponse as NextResponse4 } from "next/server";
|
|
910
1090
|
|
|
911
1091
|
// src/core/discovery.ts
|
|
912
1092
|
var DISCOVERY_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
@@ -977,6 +1157,14 @@ function generateRandomString2(length) {
|
|
|
977
1157
|
}
|
|
978
1158
|
return result;
|
|
979
1159
|
}
|
|
1160
|
+
function trustedSameOriginEndpoint(endpoint, baseUrl) {
|
|
1161
|
+
if (!endpoint) return null;
|
|
1162
|
+
try {
|
|
1163
|
+
return new URL(endpoint).origin === new URL(baseUrl).origin ? endpoint : null;
|
|
1164
|
+
} catch {
|
|
1165
|
+
return null;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
980
1168
|
function createLogoutRouteHandler(config) {
|
|
981
1169
|
const {
|
|
982
1170
|
clientId,
|
|
@@ -1015,31 +1203,31 @@ function createLogoutRouteHandler(config) {
|
|
|
1015
1203
|
if (returnedState) {
|
|
1016
1204
|
const savedState = request.cookies.get(logoutStateCookieName)?.value;
|
|
1017
1205
|
if (!savedState || !timingSafeEqualString(savedState, returnedState)) {
|
|
1018
|
-
return
|
|
1206
|
+
return NextResponse4.json(
|
|
1019
1207
|
{ error: "invalid_request", error_description: "Logout state \u4E0D\u5339\u914D" },
|
|
1020
1208
|
{ status: 400 }
|
|
1021
1209
|
);
|
|
1022
1210
|
}
|
|
1023
|
-
const res =
|
|
1211
|
+
const res = NextResponse4.redirect(callbackOrigin + "/");
|
|
1024
1212
|
res.cookies.set(logoutStateCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
1025
1213
|
return res;
|
|
1026
1214
|
}
|
|
1027
1215
|
if (request.method === "GET") {
|
|
1028
|
-
return new
|
|
1216
|
+
return new NextResponse4(buildLogoutConfirmHtml(), {
|
|
1029
1217
|
status: 200,
|
|
1030
1218
|
headers: { "content-type": "text/html; charset=utf-8" }
|
|
1031
1219
|
});
|
|
1032
1220
|
}
|
|
1033
1221
|
const requestOrigin = request.headers.get("origin");
|
|
1034
1222
|
if (requestOrigin && requestOrigin !== callbackOrigin) {
|
|
1035
|
-
return
|
|
1223
|
+
return NextResponse4.json(
|
|
1036
1224
|
{ error: "forbidden", error_description: "\u8DE8\u7AD9\u8BF7\u6C42\u88AB\u62D2\u7EDD" },
|
|
1037
1225
|
{ status: 403 }
|
|
1038
1226
|
);
|
|
1039
1227
|
}
|
|
1040
1228
|
const secFetchSite = request.headers.get("sec-fetch-site");
|
|
1041
1229
|
if (!requestOrigin && secFetchSite && secFetchSite !== "same-origin" && secFetchSite !== "none") {
|
|
1042
|
-
return
|
|
1230
|
+
return NextResponse4.json(
|
|
1043
1231
|
{ error: "forbidden", error_description: "\u8DE8\u7AD9\u8BF7\u6C42\u88AB\u62D2\u7EDD" },
|
|
1044
1232
|
{ status: 403 }
|
|
1045
1233
|
);
|
|
@@ -1055,7 +1243,7 @@ function createLogoutRouteHandler(config) {
|
|
|
1055
1243
|
if (refreshToken) {
|
|
1056
1244
|
try {
|
|
1057
1245
|
const discovery = config.serverBaseUrl ? null : await fetchDiscoveryCached(normalizedServerBase);
|
|
1058
|
-
const revokeUrl = config.serverBaseUrl ? `${normalizedServerBase}/api/oauth/revoke` : discovery?.revocation_endpoint || `${normalizedBase}/api/oauth/revoke`;
|
|
1246
|
+
const revokeUrl = (config.serverBaseUrl ? `${normalizedServerBase}/api/oauth/revoke` : trustedSameOriginEndpoint(discovery?.revocation_endpoint, normalizedBase)) || `${normalizedBase}/api/oauth/revoke`;
|
|
1059
1247
|
const body = new URLSearchParams({
|
|
1060
1248
|
token: refreshToken,
|
|
1061
1249
|
token_type_hint: "refresh_token",
|
|
@@ -1064,12 +1252,20 @@ function createLogoutRouteHandler(config) {
|
|
|
1064
1252
|
if (clientSecret) {
|
|
1065
1253
|
body.set("client_secret", clientSecret);
|
|
1066
1254
|
}
|
|
1067
|
-
|
|
1255
|
+
const doRevoke = () => fetch(revokeUrl, {
|
|
1068
1256
|
method: "POST",
|
|
1069
1257
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1070
1258
|
body: body.toString(),
|
|
1071
1259
|
signal: AbortSignal.timeout(3e3)
|
|
1072
1260
|
});
|
|
1261
|
+
let revokeRes = await doRevoke();
|
|
1262
|
+
if (!revokeRes.ok && (revokeRes.status >= 500 || revokeRes.status === 429)) {
|
|
1263
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
1264
|
+
revokeRes = await doRevoke();
|
|
1265
|
+
}
|
|
1266
|
+
if (!revokeRes.ok) {
|
|
1267
|
+
console.warn(`[SSO SDK] \u767B\u51FA\u64A4\u9500 refresh_token \u5931\u8D25\uFF08HTTP ${revokeRes.status}\uFF09`);
|
|
1268
|
+
}
|
|
1073
1269
|
} catch {
|
|
1074
1270
|
}
|
|
1075
1271
|
}
|
|
@@ -1101,7 +1297,7 @@ function createLogoutRouteHandler(config) {
|
|
|
1101
1297
|
};
|
|
1102
1298
|
if (effectiveScope === "global") {
|
|
1103
1299
|
const discovery = await fetchDiscoveryCached(normalizedServerBase);
|
|
1104
|
-
const endSessionEndpoint = discovery?.end_session_endpoint || `${normalizedBase}/api/oauth/end-session`;
|
|
1300
|
+
const endSessionEndpoint = trustedSameOriginEndpoint(discovery?.end_session_endpoint, normalizedBase) || `${normalizedBase}/api/oauth/end-session`;
|
|
1105
1301
|
const logoutUrl = new URL(endSessionEndpoint);
|
|
1106
1302
|
logoutUrl.searchParams.set("client_id", clientId);
|
|
1107
1303
|
logoutUrl.searchParams.set(
|
|
@@ -1113,18 +1309,18 @@ function createLogoutRouteHandler(config) {
|
|
|
1113
1309
|
}
|
|
1114
1310
|
const logoutState = generateRandomString2(32);
|
|
1115
1311
|
logoutUrl.searchParams.set("state", logoutState);
|
|
1116
|
-
const res = clearCookies(
|
|
1312
|
+
const res = clearCookies(NextResponse4.redirect(logoutUrl.toString()));
|
|
1117
1313
|
res.cookies.set(logoutStateCookieName, logoutState, getHostCookieOptions(600, secureCookies));
|
|
1118
1314
|
return res;
|
|
1119
1315
|
}
|
|
1120
1316
|
return clearCookies(
|
|
1121
|
-
|
|
1317
|
+
NextResponse4.redirect(callbackOrigin + "/")
|
|
1122
1318
|
);
|
|
1123
1319
|
};
|
|
1124
1320
|
}
|
|
1125
1321
|
|
|
1126
1322
|
// src/next/backchannel-logout.ts
|
|
1127
|
-
import { NextResponse as
|
|
1323
|
+
import { NextResponse as NextResponse5 } from "next/server";
|
|
1128
1324
|
|
|
1129
1325
|
// src/core/logout-token.ts
|
|
1130
1326
|
var BACKCHANNEL_LOGOUT_EVENT = "http://schemas.openid.net/event/backchannel-logout";
|
|
@@ -1236,6 +1432,12 @@ async function verifyLogoutTokenDetailed(logoutToken, ssoBaseUrl, clientId, opti
|
|
|
1236
1432
|
if (Date.now() >= payload.exp * 1e3 + 6e4) {
|
|
1237
1433
|
throw new SsoError("logout_token_expired", "Logout Token \u5DF2\u8FC7\u671F");
|
|
1238
1434
|
}
|
|
1435
|
+
if (typeof payload.iat !== "number") {
|
|
1436
|
+
throw new SsoError("logout_token_invalid", "Logout Token \u7F3A\u5C11 iat \u58F0\u660E");
|
|
1437
|
+
}
|
|
1438
|
+
if (payload.nonce !== void 0) {
|
|
1439
|
+
throw new SsoError("logout_token_invalid", "Logout Token \u4E0D\u5F97\u643A\u5E26 nonce");
|
|
1440
|
+
}
|
|
1239
1441
|
if (payload.type !== "logout_token") {
|
|
1240
1442
|
throw new SsoError(
|
|
1241
1443
|
"logout_token_invalid",
|
|
@@ -1314,7 +1516,7 @@ function createBackchannelLogoutRouteHandler(config) {
|
|
|
1314
1516
|
);
|
|
1315
1517
|
return async function handler(request) {
|
|
1316
1518
|
if (request.method !== "POST") {
|
|
1317
|
-
return
|
|
1519
|
+
return NextResponse5.json(
|
|
1318
1520
|
{ error: "method_not_allowed", error_description: "\u4EC5\u63A5\u53D7 POST \u8BF7\u6C42" },
|
|
1319
1521
|
{ status: 405 }
|
|
1320
1522
|
);
|
|
@@ -1323,7 +1525,7 @@ function createBackchannelLogoutRouteHandler(config) {
|
|
|
1323
1525
|
"logout_token"
|
|
1324
1526
|
);
|
|
1325
1527
|
if (!logoutToken) {
|
|
1326
|
-
return
|
|
1528
|
+
return NextResponse5.json(
|
|
1327
1529
|
{ error: "invalid_request", error_description: "\u7F3A\u5C11 logout_token" },
|
|
1328
1530
|
{ status: 400 }
|
|
1329
1531
|
);
|
|
@@ -1336,7 +1538,7 @@ function createBackchannelLogoutRouteHandler(config) {
|
|
|
1336
1538
|
} catch (err) {
|
|
1337
1539
|
const code = err instanceof SsoError ? err.code : "logout_token_invalid";
|
|
1338
1540
|
const description = err instanceof SsoError ? err.description : "Logout Token \u9A8C\u8BC1\u5931\u8D25";
|
|
1339
|
-
return
|
|
1541
|
+
return NextResponse5.json(
|
|
1340
1542
|
{ error: code, error_description: description },
|
|
1341
1543
|
{ status: 400 }
|
|
1342
1544
|
);
|
|
@@ -1349,12 +1551,12 @@ function createBackchannelLogoutRouteHandler(config) {
|
|
|
1349
1551
|
"[SSO SDK] backchannel logout onLogout \u94A9\u5B50\u6267\u884C\u5931\u8D25:",
|
|
1350
1552
|
err
|
|
1351
1553
|
);
|
|
1352
|
-
return
|
|
1554
|
+
return NextResponse5.json(
|
|
1353
1555
|
{ error: "server_error", error_description: "onLogout \u5904\u7406\u5931\u8D25" },
|
|
1354
1556
|
{ status: 500 }
|
|
1355
1557
|
);
|
|
1356
1558
|
}
|
|
1357
|
-
const res = new
|
|
1559
|
+
const res = new NextResponse5(null, { status: 200 });
|
|
1358
1560
|
res.cookies.set(accessTokenCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
1359
1561
|
res.cookies.set(refreshTokenCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
1360
1562
|
res.cookies.set(idTokenCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
@@ -1373,6 +1575,7 @@ export {
|
|
|
1373
1575
|
createBackchannelLogoutRouteHandler,
|
|
1374
1576
|
createCallbackRouteHandler,
|
|
1375
1577
|
createLogoutRouteHandler,
|
|
1578
|
+
createRefreshRouteHandler,
|
|
1376
1579
|
createSsoMiddleware,
|
|
1377
1580
|
getHostCookieOptions,
|
|
1378
1581
|
getSecureCookieOptions,
|