@nihplod/sso-sdk 1.4.1 → 1.5.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 +82 -9
- package/dist/index.d.mts +27 -3
- package/dist/index.d.ts +27 -3
- package/dist/index.js +169 -38
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +169 -38
- package/dist/index.mjs.map +1 -1
- package/dist/next/index.d.mts +50 -0
- package/dist/next/index.d.ts +50 -0
- package/dist/next/index.js +304 -95
- package/dist/next/index.js.map +1 -1
- package/dist/next/index.mjs +304 -95
- package/dist/next/index.mjs.map +1 -1
- package/dist/react/index.d.mts +25 -1
- package/dist/react/index.d.ts +25 -1
- package/dist/react/index.js +329 -71
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +329 -71
- package/dist/react/index.mjs.map +1 -1
- package/package.json +9 -3
package/dist/next/index.mjs
CHANGED
|
@@ -156,8 +156,7 @@ function createSsoMiddleware(config) {
|
|
|
156
156
|
scopes = "openid profile",
|
|
157
157
|
publicPaths = [],
|
|
158
158
|
callbackPath = "/api/auth/callback",
|
|
159
|
-
|
|
160
|
-
validateSsoCookie = true,
|
|
159
|
+
failClosedOnIntrospectionError = false,
|
|
161
160
|
insecureLocalDev: insecureLocalDevOpt = false
|
|
162
161
|
} = config;
|
|
163
162
|
const insecureLocalDev = resolveInsecureLocalDev(insecureLocalDevOpt, ssoBaseUrl);
|
|
@@ -169,9 +168,9 @@ function createSsoMiddleware(config) {
|
|
|
169
168
|
const verifierCookieName = insecureLocalDev ? toInsecureCookieName(config.verifierCookieName ?? DEFAULT_VERIFIER_COOKIE_NAME) : config.verifierCookieName ?? DEFAULT_VERIFIER_COOKIE_NAME;
|
|
170
169
|
const normalizedBase = ssoBaseUrl.replace(/\/+$/, "");
|
|
171
170
|
const normalizedServerBase = (config.serverBaseUrl ?? ssoBaseUrl).replace(/\/+$/, "");
|
|
172
|
-
if (
|
|
171
|
+
if (config.validateSsoCookie !== void 0 || config.ssoCookieName !== void 0) {
|
|
173
172
|
console.warn(
|
|
174
|
-
|
|
173
|
+
'[SSO SDK] validateSsoCookie / ssoCookieName \u5DF2\u5E9F\u5F03\uFF1A\u4E3B\u7AD9\u4F1A\u8BDD Cookie\uFF08__Host-user_token\uFF09\u662F\u4E3B\u7AD9\u5185\u90E8 token\uFF08type="user"\uFF09\uFF0Cintrospect \u7AEF\u70B9\u53EA\u63A5\u53D7 access_token\uFF0C\u6821\u9A8C\u5FC5\u7136\u5931\u8D25\uFF1B\u4E14 __Host- Cookie \u4E0D\u4F1A\u4E0B\u53D1\u5230\u5B50\u57DF\u3002\u8BE5\u5FEB\u901F\u901A\u9053\u4ECE\u672A\u751F\u6548\uFF0C\u5DF2\u79FB\u9664\u3002\u8BF7\u5220\u9664\u8FD9\u4E24\u4E2A\u914D\u7F6E\u9879\uFF1B\u4F1A\u8BDD\u68C0\u6D4B\u4EE5\u672C\u7AD9 access_token Cookie \u4E3A\u51C6\u3002'
|
|
175
174
|
);
|
|
176
175
|
}
|
|
177
176
|
if (!clientSecret) {
|
|
@@ -191,22 +190,6 @@ function createSsoMiddleware(config) {
|
|
|
191
190
|
if (matchesPath(pathname, allPublicPaths)) {
|
|
192
191
|
return NextResponse.next();
|
|
193
192
|
}
|
|
194
|
-
const ssoSession = request.cookies.get(ssoCookieName);
|
|
195
|
-
if (ssoSession?.value) {
|
|
196
|
-
if (validateSsoCookie) {
|
|
197
|
-
const introspectResult = await introspectAccessToken(
|
|
198
|
-
ssoSession.value,
|
|
199
|
-
normalizedServerBase,
|
|
200
|
-
clientId,
|
|
201
|
-
clientSecret
|
|
202
|
-
);
|
|
203
|
-
if (introspectResult === "active") {
|
|
204
|
-
return NextResponse.next();
|
|
205
|
-
}
|
|
206
|
-
} else {
|
|
207
|
-
return NextResponse.next();
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
193
|
const accessTokenCookie = request.cookies.get(accessTokenCookieName);
|
|
211
194
|
if (accessTokenCookie?.value) {
|
|
212
195
|
const introspectResult = await introspectAccessToken(
|
|
@@ -219,6 +202,15 @@ function createSsoMiddleware(config) {
|
|
|
219
202
|
return NextResponse.next();
|
|
220
203
|
}
|
|
221
204
|
if (introspectResult === "unreachable") {
|
|
205
|
+
if (failClosedOnIntrospectionError) {
|
|
206
|
+
console.warn(
|
|
207
|
+
"[SSO SDK] introspection \u4E0D\u53EF\u8FBE\uFF08\u7F51\u7EDC\u5F02\u5E38/\u8D85\u65F6/5xx\uFF09\uFF0CfailClosedOnIntrospectionError=true\uFF0C\u5BF9\u6301\u6709 SSO access token Cookie \u7684\u8BF7\u6C42 fail-closed \u8FD4\u56DE 502\u3002"
|
|
208
|
+
);
|
|
209
|
+
return new NextResponse(
|
|
210
|
+
"SSO \u8BA4\u8BC1\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF08introspection unreachable\uFF09\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
|
|
211
|
+
{ status: 502 }
|
|
212
|
+
);
|
|
213
|
+
}
|
|
222
214
|
console.warn(
|
|
223
215
|
"[SSO SDK] introspection \u4E0D\u53EF\u8FBE\uFF08\u7F51\u7EDC\u5F02\u5E38/\u8D85\u65F6/5xx\uFF09\uFF0C\u5BF9\u6301\u6709 SSO access token Cookie \u7684\u8BF7\u6C42 fail-open \u653E\u884C\uFF1B\u654F\u611F\u6570\u636E\u7684\u9274\u6743\u7531 Route Handler \u515C\u5E95\u3002"
|
|
224
216
|
);
|
|
@@ -241,11 +233,28 @@ function createSsoMiddleware(config) {
|
|
|
241
233
|
const loginUrl = new URL("/api/oauth/authorize", normalizedBase);
|
|
242
234
|
loginUrl.search = authorizeParams.toString();
|
|
243
235
|
const response = NextResponse.redirect(loginUrl);
|
|
244
|
-
|
|
245
|
-
response.cookies.set(
|
|
246
|
-
|
|
236
|
+
const attemptSuffix = `_${state}`;
|
|
237
|
+
response.cookies.set(
|
|
238
|
+
`${stateCookieName}${attemptSuffix}`,
|
|
239
|
+
state,
|
|
240
|
+
getHostCookieOptions(600, secureCookies)
|
|
241
|
+
);
|
|
242
|
+
response.cookies.set(
|
|
243
|
+
`${nonceCookieName}${attemptSuffix}`,
|
|
244
|
+
nonce,
|
|
245
|
+
getHostCookieOptions(600, secureCookies)
|
|
246
|
+
);
|
|
247
|
+
response.cookies.set(
|
|
248
|
+
`${verifierCookieName}${attemptSuffix}`,
|
|
249
|
+
verifier,
|
|
250
|
+
getSecureCookieOptions(600, callbackPath, secureCookies)
|
|
251
|
+
);
|
|
247
252
|
const safeReturnUrl = (request.nextUrl.pathname + request.nextUrl.search).slice(0, 2048);
|
|
248
|
-
response.cookies.set(
|
|
253
|
+
response.cookies.set(
|
|
254
|
+
`${returnUrlCookieName}${attemptSuffix}`,
|
|
255
|
+
safeReturnUrl,
|
|
256
|
+
getHostCookieOptions(600, secureCookies)
|
|
257
|
+
);
|
|
249
258
|
if (accessTokenCookie?.value) {
|
|
250
259
|
response.cookies.set(accessTokenCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
251
260
|
}
|
|
@@ -268,12 +277,17 @@ var SsoError = class extends Error {
|
|
|
268
277
|
};
|
|
269
278
|
|
|
270
279
|
// src/core/security.ts
|
|
280
|
+
var UNSAFE_URL_CHAR_PATTERN = /[\\\u0000-\u001F\u007F]/;
|
|
271
281
|
function isTrustedReturnUrl(url, currentOrigin) {
|
|
272
282
|
if (!url) return false;
|
|
273
|
-
if (
|
|
274
|
-
if (url.startsWith("
|
|
283
|
+
if (UNSAFE_URL_CHAR_PATTERN.test(url)) return false;
|
|
284
|
+
if (url.startsWith("//")) return false;
|
|
285
|
+
if (url.startsWith("/")) return true;
|
|
275
286
|
try {
|
|
276
|
-
|
|
287
|
+
const parsed = new URL(url);
|
|
288
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
|
|
289
|
+
if (parsed.username || parsed.password) return false;
|
|
290
|
+
return parsed.origin === currentOrigin;
|
|
277
291
|
} catch {
|
|
278
292
|
return false;
|
|
279
293
|
}
|
|
@@ -507,6 +521,92 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
|
|
|
507
521
|
}
|
|
508
522
|
|
|
509
523
|
// src/next/callback.ts
|
|
524
|
+
function escapeHtml(value) {
|
|
525
|
+
return value.replace(/[&<>"']/g, (ch) => {
|
|
526
|
+
switch (ch) {
|
|
527
|
+
case "&":
|
|
528
|
+
return "&";
|
|
529
|
+
case "<":
|
|
530
|
+
return "<";
|
|
531
|
+
case ">":
|
|
532
|
+
return ">";
|
|
533
|
+
case '"':
|
|
534
|
+
return """;
|
|
535
|
+
default:
|
|
536
|
+
return "'";
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
function buildErrorPage(status, error, errorDescription) {
|
|
541
|
+
const safeDescription = escapeHtml(errorDescription);
|
|
542
|
+
const safeError = escapeHtml(error);
|
|
543
|
+
return `<!doctype html>
|
|
544
|
+
<html lang="zh-CN">
|
|
545
|
+
<head>
|
|
546
|
+
<meta charset="utf-8" />
|
|
547
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
548
|
+
<meta name="robots" content="noindex" />
|
|
549
|
+
<title>\u767B\u5F55\u5931\u8D25</title>
|
|
550
|
+
<style>
|
|
551
|
+
:root { color-scheme: light; }
|
|
552
|
+
body { margin: 0; font-family: system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; background: #fafafa; color: #2c2c2c; }
|
|
553
|
+
main { min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 2rem; text-align: center; }
|
|
554
|
+
h1 { font-size: 1.25rem; font-weight: 500; letter-spacing: 0.08em; margin: 0 0 0.75rem; }
|
|
555
|
+
p { margin: 0 0 1.5rem; color: #6b7280; font-size: 0.875rem; line-height: 1.6; max-width: 28rem; }
|
|
556
|
+
.actions { display: flex; gap: 0.75rem; }
|
|
557
|
+
a { display: inline-block; padding: 0.6rem 1.4rem; font-size: 0.8125rem; letter-spacing: 0.08em; text-decoration: none; }
|
|
558
|
+
a.primary { background: #2c2c2c; color: #fff; }
|
|
559
|
+
a.secondary { border: 1px solid rgba(44, 44, 44, 0.25); color: #2c2c2c; }
|
|
560
|
+
.code { margin-top: 2rem; font-size: 0.6875rem; color: #9ca3af; }
|
|
561
|
+
</style>
|
|
562
|
+
</head>
|
|
563
|
+
<body>
|
|
564
|
+
<main>
|
|
565
|
+
<h1>\u767B\u5F55\u5931\u8D25</h1>
|
|
566
|
+
<p>${safeDescription}</p>
|
|
567
|
+
<div class="actions">
|
|
568
|
+
<a class="primary" href="/">\u91CD\u65B0\u767B\u5F55</a>
|
|
569
|
+
<a class="secondary" href="/">\u8FD4\u56DE\u9996\u9875</a>
|
|
570
|
+
</div>
|
|
571
|
+
<p class="code">\u9519\u8BEF\u7801\uFF1A${safeError}\uFF08\u53CD\u9988\u95EE\u9898\u65F6\u8BF7\u9644\u4E0A\uFF09</p>
|
|
572
|
+
</main>
|
|
573
|
+
</body>
|
|
574
|
+
</html>`;
|
|
575
|
+
}
|
|
576
|
+
function buildErrorResponse(request, status, error, errorDescription, custom) {
|
|
577
|
+
if (custom) {
|
|
578
|
+
const handled = custom({ error, errorDescription, status, request });
|
|
579
|
+
if (handled instanceof Promise) {
|
|
580
|
+
return handled.then(
|
|
581
|
+
(res) => res ? toNextResponse(res) : defaultErrorResponse(request, status, error, errorDescription)
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
if (handled) return toNextResponse(handled);
|
|
585
|
+
}
|
|
586
|
+
return defaultErrorResponse(request, status, error, errorDescription);
|
|
587
|
+
}
|
|
588
|
+
function toNextResponse(res) {
|
|
589
|
+
if (res instanceof NextResponse2) return res;
|
|
590
|
+
return new NextResponse2(res.body, {
|
|
591
|
+
status: res.status,
|
|
592
|
+
statusText: res.statusText,
|
|
593
|
+
headers: res.headers
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
function defaultErrorResponse(request, status, error, errorDescription) {
|
|
597
|
+
const accept = request.headers.get("accept") ?? "";
|
|
598
|
+
const wantsHtml = accept.includes("text/html") && request.nextUrl.searchParams.get("format") !== "json";
|
|
599
|
+
if (wantsHtml) {
|
|
600
|
+
return new NextResponse2(buildErrorPage(status, error, errorDescription), {
|
|
601
|
+
status,
|
|
602
|
+
headers: {
|
|
603
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
604
|
+
"Cache-Control": "no-store"
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
return NextResponse2.json({ error, error_description: errorDescription }, { status });
|
|
609
|
+
}
|
|
510
610
|
function createCallbackRouteHandler(config) {
|
|
511
611
|
const {
|
|
512
612
|
clientId,
|
|
@@ -514,6 +614,7 @@ function createCallbackRouteHandler(config) {
|
|
|
514
614
|
redirectUri,
|
|
515
615
|
clientSecret,
|
|
516
616
|
defaultReturnPath = "/",
|
|
617
|
+
scopes,
|
|
517
618
|
insecureLocalDev: insecureLocalDevOpt = false
|
|
518
619
|
} = config;
|
|
519
620
|
const insecureLocalDev = resolveInsecureLocalDev(insecureLocalDevOpt, ssoBaseUrl);
|
|
@@ -531,7 +632,21 @@ function createCallbackRouteHandler(config) {
|
|
|
531
632
|
return async function GET(request) {
|
|
532
633
|
const response = await handleCallback(request);
|
|
533
634
|
if (response.status >= 400) {
|
|
635
|
+
if (config.debug) {
|
|
636
|
+
console.warn(
|
|
637
|
+
`[SSO SDK] \u56DE\u8C03\u5931\u8D25 status=${response.status}`,
|
|
638
|
+
request.nextUrl.searchParams.get("error") ?? ""
|
|
639
|
+
);
|
|
640
|
+
}
|
|
534
641
|
response.cookies.set(nonceCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
642
|
+
const returnedState = request.nextUrl.searchParams.get("state");
|
|
643
|
+
if (returnedState && /^[A-Za-z0-9\-._~]{8,512}$/.test(returnedState)) {
|
|
644
|
+
response.cookies.set(
|
|
645
|
+
`${nonceCookieName}_${returnedState}`,
|
|
646
|
+
"",
|
|
647
|
+
getHostCookieOptions(0, secureCookies)
|
|
648
|
+
);
|
|
649
|
+
}
|
|
535
650
|
}
|
|
536
651
|
return response;
|
|
537
652
|
};
|
|
@@ -540,50 +655,75 @@ function createCallbackRouteHandler(config) {
|
|
|
540
655
|
const error = searchParams.get("error");
|
|
541
656
|
if (error) {
|
|
542
657
|
const desc = searchParams.get("error_description") || error;
|
|
543
|
-
return
|
|
544
|
-
|
|
545
|
-
|
|
658
|
+
return buildErrorResponse(
|
|
659
|
+
request,
|
|
660
|
+
400,
|
|
661
|
+
"authorization_failed",
|
|
662
|
+
desc,
|
|
663
|
+
config.renderErrorPage
|
|
546
664
|
);
|
|
547
665
|
}
|
|
548
666
|
const code = searchParams.get("code");
|
|
549
667
|
const returnedState = searchParams.get("state");
|
|
550
668
|
if (!code) {
|
|
551
|
-
return
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
669
|
+
return buildErrorResponse(
|
|
670
|
+
request,
|
|
671
|
+
400,
|
|
672
|
+
"invalid_request",
|
|
673
|
+
"\u767B\u5F55\u4FE1\u606F\u4E0D\u5B8C\u6574\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u767B\u5F55",
|
|
674
|
+
config.renderErrorPage
|
|
557
675
|
);
|
|
558
676
|
}
|
|
559
|
-
|
|
677
|
+
if (!returnedState || !/^[A-Za-z0-9\-._~]{8,512}$/.test(returnedState)) {
|
|
678
|
+
return buildErrorResponse(
|
|
679
|
+
request,
|
|
680
|
+
400,
|
|
681
|
+
"invalid_request",
|
|
682
|
+
"State \u53C2\u6570\u7F3A\u5931\u6216\u683C\u5F0F\u975E\u6CD5\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u6388\u6743\u8BF7\u6C42",
|
|
683
|
+
config.renderErrorPage
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
const attemptSuffix = `_${returnedState}`;
|
|
687
|
+
const readTransientCookie = (name) => request.cookies.get(`${name}${attemptSuffix}`)?.value ?? request.cookies.get(name)?.value;
|
|
688
|
+
const savedState = readTransientCookie(stateCookieName);
|
|
560
689
|
if (!savedState) {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
690
|
+
const alternateName = secureCookies ? toInsecureCookieName(stateCookieName) : DEFAULT_STATE_COOKIE_NAME;
|
|
691
|
+
const alternateValue = request.cookies.get(`${alternateName}${attemptSuffix}`)?.value ?? request.cookies.get(alternateName)?.value;
|
|
692
|
+
if (alternateValue) {
|
|
693
|
+
return buildErrorResponse(
|
|
694
|
+
request,
|
|
695
|
+
500,
|
|
696
|
+
"invalid_config",
|
|
697
|
+
"\u68C0\u6D4B\u5230 SSO SDK \u914D\u7F6E\u4E0D\u4E00\u81F4\uFF1Amiddleware \u4E0E callback \u7684 insecureLocalDev \u6216 Cookie \u540D\u79F0\u4E0D\u5339\u914D\uFF0C\u8BF7\u7EDF\u4E00\u914D\u7F6E",
|
|
698
|
+
config.renderErrorPage
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
return buildErrorResponse(
|
|
702
|
+
request,
|
|
703
|
+
400,
|
|
704
|
+
"invalid_request",
|
|
705
|
+
"\u767B\u5F55\u4F1A\u8BDD\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u6388\u6743\u8BF7\u6C42",
|
|
706
|
+
config.renderErrorPage
|
|
567
707
|
);
|
|
568
708
|
}
|
|
569
|
-
if (!timingSafeEqualString(returnedState
|
|
570
|
-
return
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
709
|
+
if (!timingSafeEqualString(returnedState, savedState)) {
|
|
710
|
+
return buildErrorResponse(
|
|
711
|
+
request,
|
|
712
|
+
400,
|
|
713
|
+
"invalid_request",
|
|
714
|
+
"\u767B\u5F55\u4F1A\u8BDD\u6821\u9A8C\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u6388\u6743\u8BF7\u6C42",
|
|
715
|
+
config.renderErrorPage
|
|
576
716
|
);
|
|
577
717
|
}
|
|
578
|
-
const expectedNonce =
|
|
579
|
-
const verifier =
|
|
718
|
+
const expectedNonce = readTransientCookie(nonceCookieName);
|
|
719
|
+
const verifier = readTransientCookie(verifierCookieName);
|
|
580
720
|
if (!verifier) {
|
|
581
|
-
return
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
721
|
+
return buildErrorResponse(
|
|
722
|
+
request,
|
|
723
|
+
400,
|
|
724
|
+
"invalid_request",
|
|
725
|
+
"\u767B\u5F55\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF08\u53EF\u80FD\u5207\u6362\u4E86\u6807\u7B7E\u9875\uFF09\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u767B\u5F55",
|
|
726
|
+
config.renderErrorPage
|
|
587
727
|
);
|
|
588
728
|
}
|
|
589
729
|
const tokenEndpoint = `${normalizedServerBase}/api/oauth/token`;
|
|
@@ -614,17 +754,23 @@ function createCallbackRouteHandler(config) {
|
|
|
614
754
|
} catch (err) {
|
|
615
755
|
lastError = err;
|
|
616
756
|
if (attempt >= maxRetries) {
|
|
617
|
-
return
|
|
618
|
-
|
|
619
|
-
|
|
757
|
+
return buildErrorResponse(
|
|
758
|
+
request,
|
|
759
|
+
502,
|
|
760
|
+
"server_error",
|
|
761
|
+
"\u767B\u5F55\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF08Token \u8BF7\u6C42\u5931\u8D25\uFF09\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
|
|
762
|
+
config.renderErrorPage
|
|
620
763
|
);
|
|
621
764
|
}
|
|
622
765
|
}
|
|
623
766
|
}
|
|
624
767
|
if (lastError || !res) {
|
|
625
|
-
return
|
|
626
|
-
|
|
627
|
-
|
|
768
|
+
return buildErrorResponse(
|
|
769
|
+
request,
|
|
770
|
+
502,
|
|
771
|
+
"server_error",
|
|
772
|
+
"\u767B\u5F55\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF08Token \u8BF7\u6C42\u5931\u8D25\uFF09\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
|
|
773
|
+
config.renderErrorPage
|
|
628
774
|
);
|
|
629
775
|
}
|
|
630
776
|
if (!res.ok) {
|
|
@@ -633,22 +779,32 @@ function createCallbackRouteHandler(config) {
|
|
|
633
779
|
errData = await res.json();
|
|
634
780
|
} catch {
|
|
635
781
|
}
|
|
636
|
-
return
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
}
|
|
641
|
-
|
|
782
|
+
return buildErrorResponse(
|
|
783
|
+
request,
|
|
784
|
+
502,
|
|
785
|
+
"token_request_failed",
|
|
786
|
+
errData.error_description || `\u767B\u5F55\u5931\u8D25\uFF08Token \u8BF7\u6C42\u5931\u8D25: HTTP ${res.status}\uFF09\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5`,
|
|
787
|
+
config.renderErrorPage
|
|
642
788
|
);
|
|
643
789
|
}
|
|
644
790
|
const tokenData = await res.json();
|
|
645
|
-
if (!tokenData.access_token || !tokenData.refresh_token) {
|
|
646
|
-
return
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
791
|
+
if (!tokenData.access_token || !tokenData.refresh_token || typeof tokenData.expires_in !== "number" || !Number.isFinite(tokenData.expires_in) || tokenData.expires_in <= 0) {
|
|
792
|
+
return buildErrorResponse(
|
|
793
|
+
request,
|
|
794
|
+
502,
|
|
795
|
+
"server_error",
|
|
796
|
+
"\u767B\u5F55\u670D\u52A1\u8FD4\u56DE\u5F02\u5E38\uFF08Token \u54CD\u5E94\u4E0D\u5B8C\u6574\uFF09\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
|
|
797
|
+
config.renderErrorPage
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
const requiresIdToken = scopes !== void 0 && scopes.split(" ").filter(Boolean).includes("openid");
|
|
801
|
+
if (requiresIdToken && !tokenData.id_token) {
|
|
802
|
+
return buildErrorResponse(
|
|
803
|
+
request,
|
|
804
|
+
400,
|
|
805
|
+
"id_token_invalid",
|
|
806
|
+
"\u767B\u5F55\u6821\u9A8C\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u767B\u5F55",
|
|
807
|
+
config.renderErrorPage
|
|
652
808
|
);
|
|
653
809
|
}
|
|
654
810
|
if (tokenData.id_token) {
|
|
@@ -661,17 +817,17 @@ function createCallbackRouteHandler(config) {
|
|
|
661
817
|
{ expectedNonce }
|
|
662
818
|
);
|
|
663
819
|
} catch (err) {
|
|
664
|
-
return
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
820
|
+
return buildErrorResponse(
|
|
821
|
+
request,
|
|
822
|
+
400,
|
|
823
|
+
"id_token_invalid",
|
|
824
|
+
err instanceof Error && /[\u4e00-\u9fff]/.test(err.message) ? err.message : "\u767B\u5F55\u6821\u9A8C\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u767B\u5F55",
|
|
825
|
+
config.renderErrorPage
|
|
670
826
|
);
|
|
671
827
|
}
|
|
672
828
|
}
|
|
673
829
|
const callbackOrigin = new URL(redirectUri).origin;
|
|
674
|
-
const rawReturnUrl =
|
|
830
|
+
const rawReturnUrl = readTransientCookie(returnUrlCookieName) || defaultReturnPath;
|
|
675
831
|
const returnUrl = isTrustedReturnUrl(rawReturnUrl, callbackOrigin) ? rawReturnUrl : "/";
|
|
676
832
|
const response = NextResponse2.redirect(new URL(returnUrl, callbackOrigin));
|
|
677
833
|
response.cookies.set(accessTokenCookieName, tokenData.access_token, {
|
|
@@ -701,11 +857,18 @@ function createCallbackRouteHandler(config) {
|
|
|
701
857
|
});
|
|
702
858
|
}
|
|
703
859
|
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
860
|
+
for (const name of [stateCookieName, nonceCookieName, returnUrlCookieName]) {
|
|
861
|
+
response.cookies.set(name, "", getHostCookieOptions(0, secureCookies));
|
|
862
|
+
response.cookies.set(`${name}${attemptSuffix}`, "", getHostCookieOptions(0, secureCookies));
|
|
863
|
+
}
|
|
864
|
+
for (const path of ["/", request.nextUrl.pathname]) {
|
|
865
|
+
response.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, path, secureCookies));
|
|
866
|
+
response.cookies.set(
|
|
867
|
+
`${verifierCookieName}${attemptSuffix}`,
|
|
868
|
+
"",
|
|
869
|
+
getSecureCookieOptions(0, path, secureCookies)
|
|
870
|
+
);
|
|
871
|
+
}
|
|
709
872
|
return response;
|
|
710
873
|
}
|
|
711
874
|
}
|
|
@@ -835,6 +998,20 @@ function createLogoutRouteHandler(config) {
|
|
|
835
998
|
headers: { "content-type": "text/html; charset=utf-8" }
|
|
836
999
|
});
|
|
837
1000
|
}
|
|
1001
|
+
const requestOrigin = request.headers.get("origin");
|
|
1002
|
+
if (requestOrigin && requestOrigin !== callbackOrigin) {
|
|
1003
|
+
return NextResponse3.json(
|
|
1004
|
+
{ error: "forbidden", error_description: "\u8DE8\u7AD9\u8BF7\u6C42\u88AB\u62D2\u7EDD" },
|
|
1005
|
+
{ status: 403 }
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
const secFetchSite = request.headers.get("sec-fetch-site");
|
|
1009
|
+
if (!requestOrigin && secFetchSite && secFetchSite !== "same-origin" && secFetchSite !== "none") {
|
|
1010
|
+
return NextResponse3.json(
|
|
1011
|
+
{ error: "forbidden", error_description: "\u8DE8\u7AD9\u8BF7\u6C42\u88AB\u62D2\u7EDD" },
|
|
1012
|
+
{ status: 403 }
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
838
1015
|
let formGlobal = null;
|
|
839
1016
|
try {
|
|
840
1017
|
formGlobal = new URLSearchParams(await request.text()).get("global");
|
|
@@ -873,6 +1050,21 @@ function createLogoutRouteHandler(config) {
|
|
|
873
1050
|
res.cookies.set(returnUrlCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
874
1051
|
res.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, "/", secureCookies));
|
|
875
1052
|
res.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, callbackPath, secureCookies));
|
|
1053
|
+
const suffixedPrefixes = [
|
|
1054
|
+
`${stateCookieName}_`,
|
|
1055
|
+
`${nonceCookieName}_`,
|
|
1056
|
+
`${returnUrlCookieName}_`,
|
|
1057
|
+
`${verifierCookieName}_`
|
|
1058
|
+
];
|
|
1059
|
+
for (const cookie of request.cookies.getAll()) {
|
|
1060
|
+
if (!suffixedPrefixes.some((prefix) => cookie.name.startsWith(prefix))) continue;
|
|
1061
|
+
if (cookie.name.startsWith(`${verifierCookieName}_`)) {
|
|
1062
|
+
res.cookies.set(cookie.name, "", getSecureCookieOptions(0, "/", secureCookies));
|
|
1063
|
+
res.cookies.set(cookie.name, "", getSecureCookieOptions(0, callbackPath, secureCookies));
|
|
1064
|
+
} else {
|
|
1065
|
+
res.cookies.set(cookie.name, "", getHostCookieOptions(0, secureCookies));
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
876
1068
|
return res;
|
|
877
1069
|
};
|
|
878
1070
|
if (effectiveScope === "global") {
|
|
@@ -906,6 +1098,9 @@ import { NextResponse as NextResponse4 } from "next/server";
|
|
|
906
1098
|
var BACKCHANNEL_LOGOUT_EVENT = "http://schemas.openid.net/event/backchannel-logout";
|
|
907
1099
|
var JTI_CACHE_CAPACITY = 1e3;
|
|
908
1100
|
var seenJti = /* @__PURE__ */ new Map();
|
|
1101
|
+
function releaseLogoutTokenJti(jti) {
|
|
1102
|
+
seenJti.delete(jti);
|
|
1103
|
+
}
|
|
909
1104
|
function recordJti(jti, expiresAtMs) {
|
|
910
1105
|
const now = Date.now();
|
|
911
1106
|
const existing = seenJti.get(jti);
|
|
@@ -921,7 +1116,7 @@ function recordJti(jti, expiresAtMs) {
|
|
|
921
1116
|
seenJti.set(jti, expiresAtMs);
|
|
922
1117
|
return true;
|
|
923
1118
|
}
|
|
924
|
-
async function
|
|
1119
|
+
async function verifyLogoutTokenDetailed(logoutToken, ssoBaseUrl, clientId) {
|
|
925
1120
|
const baseUrl = ssoBaseUrl.replace(/\/+$/, "");
|
|
926
1121
|
const header = decodeJwtHeader(logoutToken);
|
|
927
1122
|
if (!header) {
|
|
@@ -998,8 +1193,21 @@ async function verifyLogoutToken(logoutToken, ssoBaseUrl, clientId) {
|
|
|
998
1193
|
if (Date.now() >= payload.exp * 1e3 + 6e4) {
|
|
999
1194
|
throw new SsoError("logout_token_expired", "Logout Token \u5DF2\u8FC7\u671F");
|
|
1000
1195
|
}
|
|
1196
|
+
if (payload.type !== "logout_token") {
|
|
1197
|
+
throw new SsoError(
|
|
1198
|
+
"logout_token_invalid",
|
|
1199
|
+
"Logout Token type \u5FC5\u987B\u4E3A logout_token"
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1001
1202
|
const events = payload.events;
|
|
1002
|
-
if (!events || typeof events !== "object" ||
|
|
1203
|
+
if (!events || typeof events !== "object" || Array.isArray(events)) {
|
|
1204
|
+
throw new SsoError(
|
|
1205
|
+
"logout_token_invalid",
|
|
1206
|
+
"Logout Token \u7F3A\u5C11 backchannel-logout events \u58F0\u660E"
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
const logoutEvent = events[BACKCHANNEL_LOGOUT_EVENT];
|
|
1210
|
+
if (!logoutEvent || typeof logoutEvent !== "object" || Array.isArray(logoutEvent)) {
|
|
1003
1211
|
throw new SsoError(
|
|
1004
1212
|
"logout_token_invalid",
|
|
1005
1213
|
"Logout Token \u7F3A\u5C11 backchannel-logout events \u58F0\u660E"
|
|
@@ -1020,7 +1228,7 @@ async function verifyLogoutToken(logoutToken, ssoBaseUrl, clientId) {
|
|
|
1020
1228
|
if (!recordJti(jti, payload.exp * 1e3 + 6e4)) {
|
|
1021
1229
|
throw new SsoError("logout_token_replay", "Logout Token jti \u91CD\u653E");
|
|
1022
1230
|
}
|
|
1023
|
-
return { sub, sid };
|
|
1231
|
+
return { payload: { sub, sid }, jti };
|
|
1024
1232
|
}
|
|
1025
1233
|
|
|
1026
1234
|
// src/next/backchannel-logout.ts
|
|
@@ -1060,9 +1268,9 @@ function createBackchannelLogoutRouteHandler(config) {
|
|
|
1060
1268
|
{ status: 400 }
|
|
1061
1269
|
);
|
|
1062
1270
|
}
|
|
1063
|
-
let
|
|
1271
|
+
let verified;
|
|
1064
1272
|
try {
|
|
1065
|
-
|
|
1273
|
+
verified = await verifyLogoutTokenDetailed(logoutToken, ssoBaseUrl, clientId);
|
|
1066
1274
|
} catch (err) {
|
|
1067
1275
|
const code = err instanceof SsoError ? err.code : "logout_token_invalid";
|
|
1068
1276
|
const description = err instanceof SsoError ? err.description : "Logout Token \u9A8C\u8BC1\u5931\u8D25";
|
|
@@ -1072,8 +1280,9 @@ function createBackchannelLogoutRouteHandler(config) {
|
|
|
1072
1280
|
);
|
|
1073
1281
|
}
|
|
1074
1282
|
try {
|
|
1075
|
-
await onLogout?.(payload, request);
|
|
1283
|
+
await onLogout?.(verified.payload, request);
|
|
1076
1284
|
} catch (err) {
|
|
1285
|
+
releaseLogoutTokenJti(verified.jti);
|
|
1077
1286
|
console.error(
|
|
1078
1287
|
"[SSO SDK] backchannel logout onLogout \u94A9\u5B50\u6267\u884C\u5931\u8D25:",
|
|
1079
1288
|
err
|