@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.js
CHANGED
|
@@ -196,8 +196,7 @@ function createSsoMiddleware(config) {
|
|
|
196
196
|
scopes = "openid profile",
|
|
197
197
|
publicPaths = [],
|
|
198
198
|
callbackPath = "/api/auth/callback",
|
|
199
|
-
|
|
200
|
-
validateSsoCookie = true,
|
|
199
|
+
failClosedOnIntrospectionError = false,
|
|
201
200
|
insecureLocalDev: insecureLocalDevOpt = false
|
|
202
201
|
} = config;
|
|
203
202
|
const insecureLocalDev = resolveInsecureLocalDev(insecureLocalDevOpt, ssoBaseUrl);
|
|
@@ -209,9 +208,9 @@ function createSsoMiddleware(config) {
|
|
|
209
208
|
const verifierCookieName = insecureLocalDev ? toInsecureCookieName(config.verifierCookieName ?? DEFAULT_VERIFIER_COOKIE_NAME) : config.verifierCookieName ?? DEFAULT_VERIFIER_COOKIE_NAME;
|
|
210
209
|
const normalizedBase = ssoBaseUrl.replace(/\/+$/, "");
|
|
211
210
|
const normalizedServerBase = (config.serverBaseUrl ?? ssoBaseUrl).replace(/\/+$/, "");
|
|
212
|
-
if (
|
|
211
|
+
if (config.validateSsoCookie !== void 0 || config.ssoCookieName !== void 0) {
|
|
213
212
|
console.warn(
|
|
214
|
-
|
|
213
|
+
'[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'
|
|
215
214
|
);
|
|
216
215
|
}
|
|
217
216
|
if (!clientSecret) {
|
|
@@ -231,22 +230,6 @@ function createSsoMiddleware(config) {
|
|
|
231
230
|
if (matchesPath(pathname, allPublicPaths)) {
|
|
232
231
|
return import_server.NextResponse.next();
|
|
233
232
|
}
|
|
234
|
-
const ssoSession = request.cookies.get(ssoCookieName);
|
|
235
|
-
if (ssoSession?.value) {
|
|
236
|
-
if (validateSsoCookie) {
|
|
237
|
-
const introspectResult = await introspectAccessToken(
|
|
238
|
-
ssoSession.value,
|
|
239
|
-
normalizedServerBase,
|
|
240
|
-
clientId,
|
|
241
|
-
clientSecret
|
|
242
|
-
);
|
|
243
|
-
if (introspectResult === "active") {
|
|
244
|
-
return import_server.NextResponse.next();
|
|
245
|
-
}
|
|
246
|
-
} else {
|
|
247
|
-
return import_server.NextResponse.next();
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
233
|
const accessTokenCookie = request.cookies.get(accessTokenCookieName);
|
|
251
234
|
if (accessTokenCookie?.value) {
|
|
252
235
|
const introspectResult = await introspectAccessToken(
|
|
@@ -259,6 +242,15 @@ function createSsoMiddleware(config) {
|
|
|
259
242
|
return import_server.NextResponse.next();
|
|
260
243
|
}
|
|
261
244
|
if (introspectResult === "unreachable") {
|
|
245
|
+
if (failClosedOnIntrospectionError) {
|
|
246
|
+
console.warn(
|
|
247
|
+
"[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"
|
|
248
|
+
);
|
|
249
|
+
return new import_server.NextResponse(
|
|
250
|
+
"SSO \u8BA4\u8BC1\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF08introspection unreachable\uFF09\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
|
|
251
|
+
{ status: 502 }
|
|
252
|
+
);
|
|
253
|
+
}
|
|
262
254
|
console.warn(
|
|
263
255
|
"[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"
|
|
264
256
|
);
|
|
@@ -281,11 +273,28 @@ function createSsoMiddleware(config) {
|
|
|
281
273
|
const loginUrl = new URL("/api/oauth/authorize", normalizedBase);
|
|
282
274
|
loginUrl.search = authorizeParams.toString();
|
|
283
275
|
const response = import_server.NextResponse.redirect(loginUrl);
|
|
284
|
-
|
|
285
|
-
response.cookies.set(
|
|
286
|
-
|
|
276
|
+
const attemptSuffix = `_${state}`;
|
|
277
|
+
response.cookies.set(
|
|
278
|
+
`${stateCookieName}${attemptSuffix}`,
|
|
279
|
+
state,
|
|
280
|
+
getHostCookieOptions(600, secureCookies)
|
|
281
|
+
);
|
|
282
|
+
response.cookies.set(
|
|
283
|
+
`${nonceCookieName}${attemptSuffix}`,
|
|
284
|
+
nonce,
|
|
285
|
+
getHostCookieOptions(600, secureCookies)
|
|
286
|
+
);
|
|
287
|
+
response.cookies.set(
|
|
288
|
+
`${verifierCookieName}${attemptSuffix}`,
|
|
289
|
+
verifier,
|
|
290
|
+
getSecureCookieOptions(600, callbackPath, secureCookies)
|
|
291
|
+
);
|
|
287
292
|
const safeReturnUrl = (request.nextUrl.pathname + request.nextUrl.search).slice(0, 2048);
|
|
288
|
-
response.cookies.set(
|
|
293
|
+
response.cookies.set(
|
|
294
|
+
`${returnUrlCookieName}${attemptSuffix}`,
|
|
295
|
+
safeReturnUrl,
|
|
296
|
+
getHostCookieOptions(600, secureCookies)
|
|
297
|
+
);
|
|
289
298
|
if (accessTokenCookie?.value) {
|
|
290
299
|
response.cookies.set(accessTokenCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
291
300
|
}
|
|
@@ -308,12 +317,17 @@ var SsoError = class extends Error {
|
|
|
308
317
|
};
|
|
309
318
|
|
|
310
319
|
// src/core/security.ts
|
|
320
|
+
var UNSAFE_URL_CHAR_PATTERN = /[\\\u0000-\u001F\u007F]/;
|
|
311
321
|
function isTrustedReturnUrl(url, currentOrigin) {
|
|
312
322
|
if (!url) return false;
|
|
313
|
-
if (
|
|
314
|
-
if (url.startsWith("
|
|
323
|
+
if (UNSAFE_URL_CHAR_PATTERN.test(url)) return false;
|
|
324
|
+
if (url.startsWith("//")) return false;
|
|
325
|
+
if (url.startsWith("/")) return true;
|
|
315
326
|
try {
|
|
316
|
-
|
|
327
|
+
const parsed = new URL(url);
|
|
328
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
|
|
329
|
+
if (parsed.username || parsed.password) return false;
|
|
330
|
+
return parsed.origin === currentOrigin;
|
|
317
331
|
} catch {
|
|
318
332
|
return false;
|
|
319
333
|
}
|
|
@@ -547,6 +561,92 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
|
|
|
547
561
|
}
|
|
548
562
|
|
|
549
563
|
// src/next/callback.ts
|
|
564
|
+
function escapeHtml(value) {
|
|
565
|
+
return value.replace(/[&<>"']/g, (ch) => {
|
|
566
|
+
switch (ch) {
|
|
567
|
+
case "&":
|
|
568
|
+
return "&";
|
|
569
|
+
case "<":
|
|
570
|
+
return "<";
|
|
571
|
+
case ">":
|
|
572
|
+
return ">";
|
|
573
|
+
case '"':
|
|
574
|
+
return """;
|
|
575
|
+
default:
|
|
576
|
+
return "'";
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
function buildErrorPage(status, error, errorDescription) {
|
|
581
|
+
const safeDescription = escapeHtml(errorDescription);
|
|
582
|
+
const safeError = escapeHtml(error);
|
|
583
|
+
return `<!doctype html>
|
|
584
|
+
<html lang="zh-CN">
|
|
585
|
+
<head>
|
|
586
|
+
<meta charset="utf-8" />
|
|
587
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
588
|
+
<meta name="robots" content="noindex" />
|
|
589
|
+
<title>\u767B\u5F55\u5931\u8D25</title>
|
|
590
|
+
<style>
|
|
591
|
+
:root { color-scheme: light; }
|
|
592
|
+
body { margin: 0; font-family: system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; background: #fafafa; color: #2c2c2c; }
|
|
593
|
+
main { min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 2rem; text-align: center; }
|
|
594
|
+
h1 { font-size: 1.25rem; font-weight: 500; letter-spacing: 0.08em; margin: 0 0 0.75rem; }
|
|
595
|
+
p { margin: 0 0 1.5rem; color: #6b7280; font-size: 0.875rem; line-height: 1.6; max-width: 28rem; }
|
|
596
|
+
.actions { display: flex; gap: 0.75rem; }
|
|
597
|
+
a { display: inline-block; padding: 0.6rem 1.4rem; font-size: 0.8125rem; letter-spacing: 0.08em; text-decoration: none; }
|
|
598
|
+
a.primary { background: #2c2c2c; color: #fff; }
|
|
599
|
+
a.secondary { border: 1px solid rgba(44, 44, 44, 0.25); color: #2c2c2c; }
|
|
600
|
+
.code { margin-top: 2rem; font-size: 0.6875rem; color: #9ca3af; }
|
|
601
|
+
</style>
|
|
602
|
+
</head>
|
|
603
|
+
<body>
|
|
604
|
+
<main>
|
|
605
|
+
<h1>\u767B\u5F55\u5931\u8D25</h1>
|
|
606
|
+
<p>${safeDescription}</p>
|
|
607
|
+
<div class="actions">
|
|
608
|
+
<a class="primary" href="/">\u91CD\u65B0\u767B\u5F55</a>
|
|
609
|
+
<a class="secondary" href="/">\u8FD4\u56DE\u9996\u9875</a>
|
|
610
|
+
</div>
|
|
611
|
+
<p class="code">\u9519\u8BEF\u7801\uFF1A${safeError}\uFF08\u53CD\u9988\u95EE\u9898\u65F6\u8BF7\u9644\u4E0A\uFF09</p>
|
|
612
|
+
</main>
|
|
613
|
+
</body>
|
|
614
|
+
</html>`;
|
|
615
|
+
}
|
|
616
|
+
function buildErrorResponse(request, status, error, errorDescription, custom) {
|
|
617
|
+
if (custom) {
|
|
618
|
+
const handled = custom({ error, errorDescription, status, request });
|
|
619
|
+
if (handled instanceof Promise) {
|
|
620
|
+
return handled.then(
|
|
621
|
+
(res) => res ? toNextResponse(res) : defaultErrorResponse(request, status, error, errorDescription)
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
if (handled) return toNextResponse(handled);
|
|
625
|
+
}
|
|
626
|
+
return defaultErrorResponse(request, status, error, errorDescription);
|
|
627
|
+
}
|
|
628
|
+
function toNextResponse(res) {
|
|
629
|
+
if (res instanceof import_server2.NextResponse) return res;
|
|
630
|
+
return new import_server2.NextResponse(res.body, {
|
|
631
|
+
status: res.status,
|
|
632
|
+
statusText: res.statusText,
|
|
633
|
+
headers: res.headers
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
function defaultErrorResponse(request, status, error, errorDescription) {
|
|
637
|
+
const accept = request.headers.get("accept") ?? "";
|
|
638
|
+
const wantsHtml = accept.includes("text/html") && request.nextUrl.searchParams.get("format") !== "json";
|
|
639
|
+
if (wantsHtml) {
|
|
640
|
+
return new import_server2.NextResponse(buildErrorPage(status, error, errorDescription), {
|
|
641
|
+
status,
|
|
642
|
+
headers: {
|
|
643
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
644
|
+
"Cache-Control": "no-store"
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
return import_server2.NextResponse.json({ error, error_description: errorDescription }, { status });
|
|
649
|
+
}
|
|
550
650
|
function createCallbackRouteHandler(config) {
|
|
551
651
|
const {
|
|
552
652
|
clientId,
|
|
@@ -554,6 +654,7 @@ function createCallbackRouteHandler(config) {
|
|
|
554
654
|
redirectUri,
|
|
555
655
|
clientSecret,
|
|
556
656
|
defaultReturnPath = "/",
|
|
657
|
+
scopes,
|
|
557
658
|
insecureLocalDev: insecureLocalDevOpt = false
|
|
558
659
|
} = config;
|
|
559
660
|
const insecureLocalDev = resolveInsecureLocalDev(insecureLocalDevOpt, ssoBaseUrl);
|
|
@@ -571,7 +672,21 @@ function createCallbackRouteHandler(config) {
|
|
|
571
672
|
return async function GET(request) {
|
|
572
673
|
const response = await handleCallback(request);
|
|
573
674
|
if (response.status >= 400) {
|
|
675
|
+
if (config.debug) {
|
|
676
|
+
console.warn(
|
|
677
|
+
`[SSO SDK] \u56DE\u8C03\u5931\u8D25 status=${response.status}`,
|
|
678
|
+
request.nextUrl.searchParams.get("error") ?? ""
|
|
679
|
+
);
|
|
680
|
+
}
|
|
574
681
|
response.cookies.set(nonceCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
682
|
+
const returnedState = request.nextUrl.searchParams.get("state");
|
|
683
|
+
if (returnedState && /^[A-Za-z0-9\-._~]{8,512}$/.test(returnedState)) {
|
|
684
|
+
response.cookies.set(
|
|
685
|
+
`${nonceCookieName}_${returnedState}`,
|
|
686
|
+
"",
|
|
687
|
+
getHostCookieOptions(0, secureCookies)
|
|
688
|
+
);
|
|
689
|
+
}
|
|
575
690
|
}
|
|
576
691
|
return response;
|
|
577
692
|
};
|
|
@@ -580,50 +695,75 @@ function createCallbackRouteHandler(config) {
|
|
|
580
695
|
const error = searchParams.get("error");
|
|
581
696
|
if (error) {
|
|
582
697
|
const desc = searchParams.get("error_description") || error;
|
|
583
|
-
return
|
|
584
|
-
|
|
585
|
-
|
|
698
|
+
return buildErrorResponse(
|
|
699
|
+
request,
|
|
700
|
+
400,
|
|
701
|
+
"authorization_failed",
|
|
702
|
+
desc,
|
|
703
|
+
config.renderErrorPage
|
|
586
704
|
);
|
|
587
705
|
}
|
|
588
706
|
const code = searchParams.get("code");
|
|
589
707
|
const returnedState = searchParams.get("state");
|
|
590
708
|
if (!code) {
|
|
591
|
-
return
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
709
|
+
return buildErrorResponse(
|
|
710
|
+
request,
|
|
711
|
+
400,
|
|
712
|
+
"invalid_request",
|
|
713
|
+
"\u767B\u5F55\u4FE1\u606F\u4E0D\u5B8C\u6574\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u767B\u5F55",
|
|
714
|
+
config.renderErrorPage
|
|
597
715
|
);
|
|
598
716
|
}
|
|
599
|
-
|
|
717
|
+
if (!returnedState || !/^[A-Za-z0-9\-._~]{8,512}$/.test(returnedState)) {
|
|
718
|
+
return buildErrorResponse(
|
|
719
|
+
request,
|
|
720
|
+
400,
|
|
721
|
+
"invalid_request",
|
|
722
|
+
"State \u53C2\u6570\u7F3A\u5931\u6216\u683C\u5F0F\u975E\u6CD5\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u6388\u6743\u8BF7\u6C42",
|
|
723
|
+
config.renderErrorPage
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
const attemptSuffix = `_${returnedState}`;
|
|
727
|
+
const readTransientCookie = (name) => request.cookies.get(`${name}${attemptSuffix}`)?.value ?? request.cookies.get(name)?.value;
|
|
728
|
+
const savedState = readTransientCookie(stateCookieName);
|
|
600
729
|
if (!savedState) {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
730
|
+
const alternateName = secureCookies ? toInsecureCookieName(stateCookieName) : DEFAULT_STATE_COOKIE_NAME;
|
|
731
|
+
const alternateValue = request.cookies.get(`${alternateName}${attemptSuffix}`)?.value ?? request.cookies.get(alternateName)?.value;
|
|
732
|
+
if (alternateValue) {
|
|
733
|
+
return buildErrorResponse(
|
|
734
|
+
request,
|
|
735
|
+
500,
|
|
736
|
+
"invalid_config",
|
|
737
|
+
"\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",
|
|
738
|
+
config.renderErrorPage
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
return buildErrorResponse(
|
|
742
|
+
request,
|
|
743
|
+
400,
|
|
744
|
+
"invalid_request",
|
|
745
|
+
"\u767B\u5F55\u4F1A\u8BDD\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u6388\u6743\u8BF7\u6C42",
|
|
746
|
+
config.renderErrorPage
|
|
607
747
|
);
|
|
608
748
|
}
|
|
609
|
-
if (!timingSafeEqualString(returnedState
|
|
610
|
-
return
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
749
|
+
if (!timingSafeEqualString(returnedState, savedState)) {
|
|
750
|
+
return buildErrorResponse(
|
|
751
|
+
request,
|
|
752
|
+
400,
|
|
753
|
+
"invalid_request",
|
|
754
|
+
"\u767B\u5F55\u4F1A\u8BDD\u6821\u9A8C\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u6388\u6743\u8BF7\u6C42",
|
|
755
|
+
config.renderErrorPage
|
|
616
756
|
);
|
|
617
757
|
}
|
|
618
|
-
const expectedNonce =
|
|
619
|
-
const verifier =
|
|
758
|
+
const expectedNonce = readTransientCookie(nonceCookieName);
|
|
759
|
+
const verifier = readTransientCookie(verifierCookieName);
|
|
620
760
|
if (!verifier) {
|
|
621
|
-
return
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
761
|
+
return buildErrorResponse(
|
|
762
|
+
request,
|
|
763
|
+
400,
|
|
764
|
+
"invalid_request",
|
|
765
|
+
"\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",
|
|
766
|
+
config.renderErrorPage
|
|
627
767
|
);
|
|
628
768
|
}
|
|
629
769
|
const tokenEndpoint = `${normalizedServerBase}/api/oauth/token`;
|
|
@@ -654,17 +794,23 @@ function createCallbackRouteHandler(config) {
|
|
|
654
794
|
} catch (err) {
|
|
655
795
|
lastError = err;
|
|
656
796
|
if (attempt >= maxRetries) {
|
|
657
|
-
return
|
|
658
|
-
|
|
659
|
-
|
|
797
|
+
return buildErrorResponse(
|
|
798
|
+
request,
|
|
799
|
+
502,
|
|
800
|
+
"server_error",
|
|
801
|
+
"\u767B\u5F55\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF08Token \u8BF7\u6C42\u5931\u8D25\uFF09\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
|
|
802
|
+
config.renderErrorPage
|
|
660
803
|
);
|
|
661
804
|
}
|
|
662
805
|
}
|
|
663
806
|
}
|
|
664
807
|
if (lastError || !res) {
|
|
665
|
-
return
|
|
666
|
-
|
|
667
|
-
|
|
808
|
+
return buildErrorResponse(
|
|
809
|
+
request,
|
|
810
|
+
502,
|
|
811
|
+
"server_error",
|
|
812
|
+
"\u767B\u5F55\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF08Token \u8BF7\u6C42\u5931\u8D25\uFF09\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
|
|
813
|
+
config.renderErrorPage
|
|
668
814
|
);
|
|
669
815
|
}
|
|
670
816
|
if (!res.ok) {
|
|
@@ -673,22 +819,32 @@ function createCallbackRouteHandler(config) {
|
|
|
673
819
|
errData = await res.json();
|
|
674
820
|
} catch {
|
|
675
821
|
}
|
|
676
|
-
return
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
}
|
|
681
|
-
|
|
822
|
+
return buildErrorResponse(
|
|
823
|
+
request,
|
|
824
|
+
502,
|
|
825
|
+
"token_request_failed",
|
|
826
|
+
errData.error_description || `\u767B\u5F55\u5931\u8D25\uFF08Token \u8BF7\u6C42\u5931\u8D25: HTTP ${res.status}\uFF09\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5`,
|
|
827
|
+
config.renderErrorPage
|
|
682
828
|
);
|
|
683
829
|
}
|
|
684
830
|
const tokenData = await res.json();
|
|
685
|
-
if (!tokenData.access_token || !tokenData.refresh_token) {
|
|
686
|
-
return
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
831
|
+
if (!tokenData.access_token || !tokenData.refresh_token || typeof tokenData.expires_in !== "number" || !Number.isFinite(tokenData.expires_in) || tokenData.expires_in <= 0) {
|
|
832
|
+
return buildErrorResponse(
|
|
833
|
+
request,
|
|
834
|
+
502,
|
|
835
|
+
"server_error",
|
|
836
|
+
"\u767B\u5F55\u670D\u52A1\u8FD4\u56DE\u5F02\u5E38\uFF08Token \u54CD\u5E94\u4E0D\u5B8C\u6574\uFF09\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
|
|
837
|
+
config.renderErrorPage
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
const requiresIdToken = scopes !== void 0 && scopes.split(" ").filter(Boolean).includes("openid");
|
|
841
|
+
if (requiresIdToken && !tokenData.id_token) {
|
|
842
|
+
return buildErrorResponse(
|
|
843
|
+
request,
|
|
844
|
+
400,
|
|
845
|
+
"id_token_invalid",
|
|
846
|
+
"\u767B\u5F55\u6821\u9A8C\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u767B\u5F55",
|
|
847
|
+
config.renderErrorPage
|
|
692
848
|
);
|
|
693
849
|
}
|
|
694
850
|
if (tokenData.id_token) {
|
|
@@ -701,17 +857,17 @@ function createCallbackRouteHandler(config) {
|
|
|
701
857
|
{ expectedNonce }
|
|
702
858
|
);
|
|
703
859
|
} catch (err) {
|
|
704
|
-
return
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
860
|
+
return buildErrorResponse(
|
|
861
|
+
request,
|
|
862
|
+
400,
|
|
863
|
+
"id_token_invalid",
|
|
864
|
+
err instanceof Error && /[\u4e00-\u9fff]/.test(err.message) ? err.message : "\u767B\u5F55\u6821\u9A8C\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u767B\u5F55",
|
|
865
|
+
config.renderErrorPage
|
|
710
866
|
);
|
|
711
867
|
}
|
|
712
868
|
}
|
|
713
869
|
const callbackOrigin = new URL(redirectUri).origin;
|
|
714
|
-
const rawReturnUrl =
|
|
870
|
+
const rawReturnUrl = readTransientCookie(returnUrlCookieName) || defaultReturnPath;
|
|
715
871
|
const returnUrl = isTrustedReturnUrl(rawReturnUrl, callbackOrigin) ? rawReturnUrl : "/";
|
|
716
872
|
const response = import_server2.NextResponse.redirect(new URL(returnUrl, callbackOrigin));
|
|
717
873
|
response.cookies.set(accessTokenCookieName, tokenData.access_token, {
|
|
@@ -741,11 +897,18 @@ function createCallbackRouteHandler(config) {
|
|
|
741
897
|
});
|
|
742
898
|
}
|
|
743
899
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
900
|
+
for (const name of [stateCookieName, nonceCookieName, returnUrlCookieName]) {
|
|
901
|
+
response.cookies.set(name, "", getHostCookieOptions(0, secureCookies));
|
|
902
|
+
response.cookies.set(`${name}${attemptSuffix}`, "", getHostCookieOptions(0, secureCookies));
|
|
903
|
+
}
|
|
904
|
+
for (const path of ["/", request.nextUrl.pathname]) {
|
|
905
|
+
response.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, path, secureCookies));
|
|
906
|
+
response.cookies.set(
|
|
907
|
+
`${verifierCookieName}${attemptSuffix}`,
|
|
908
|
+
"",
|
|
909
|
+
getSecureCookieOptions(0, path, secureCookies)
|
|
910
|
+
);
|
|
911
|
+
}
|
|
749
912
|
return response;
|
|
750
913
|
}
|
|
751
914
|
}
|
|
@@ -875,6 +1038,20 @@ function createLogoutRouteHandler(config) {
|
|
|
875
1038
|
headers: { "content-type": "text/html; charset=utf-8" }
|
|
876
1039
|
});
|
|
877
1040
|
}
|
|
1041
|
+
const requestOrigin = request.headers.get("origin");
|
|
1042
|
+
if (requestOrigin && requestOrigin !== callbackOrigin) {
|
|
1043
|
+
return import_server3.NextResponse.json(
|
|
1044
|
+
{ error: "forbidden", error_description: "\u8DE8\u7AD9\u8BF7\u6C42\u88AB\u62D2\u7EDD" },
|
|
1045
|
+
{ status: 403 }
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
const secFetchSite = request.headers.get("sec-fetch-site");
|
|
1049
|
+
if (!requestOrigin && secFetchSite && secFetchSite !== "same-origin" && secFetchSite !== "none") {
|
|
1050
|
+
return import_server3.NextResponse.json(
|
|
1051
|
+
{ error: "forbidden", error_description: "\u8DE8\u7AD9\u8BF7\u6C42\u88AB\u62D2\u7EDD" },
|
|
1052
|
+
{ status: 403 }
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
878
1055
|
let formGlobal = null;
|
|
879
1056
|
try {
|
|
880
1057
|
formGlobal = new URLSearchParams(await request.text()).get("global");
|
|
@@ -913,6 +1090,21 @@ function createLogoutRouteHandler(config) {
|
|
|
913
1090
|
res.cookies.set(returnUrlCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
914
1091
|
res.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, "/", secureCookies));
|
|
915
1092
|
res.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, callbackPath, secureCookies));
|
|
1093
|
+
const suffixedPrefixes = [
|
|
1094
|
+
`${stateCookieName}_`,
|
|
1095
|
+
`${nonceCookieName}_`,
|
|
1096
|
+
`${returnUrlCookieName}_`,
|
|
1097
|
+
`${verifierCookieName}_`
|
|
1098
|
+
];
|
|
1099
|
+
for (const cookie of request.cookies.getAll()) {
|
|
1100
|
+
if (!suffixedPrefixes.some((prefix) => cookie.name.startsWith(prefix))) continue;
|
|
1101
|
+
if (cookie.name.startsWith(`${verifierCookieName}_`)) {
|
|
1102
|
+
res.cookies.set(cookie.name, "", getSecureCookieOptions(0, "/", secureCookies));
|
|
1103
|
+
res.cookies.set(cookie.name, "", getSecureCookieOptions(0, callbackPath, secureCookies));
|
|
1104
|
+
} else {
|
|
1105
|
+
res.cookies.set(cookie.name, "", getHostCookieOptions(0, secureCookies));
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
916
1108
|
return res;
|
|
917
1109
|
};
|
|
918
1110
|
if (effectiveScope === "global") {
|
|
@@ -946,6 +1138,9 @@ var import_server4 = require("next/server");
|
|
|
946
1138
|
var BACKCHANNEL_LOGOUT_EVENT = "http://schemas.openid.net/event/backchannel-logout";
|
|
947
1139
|
var JTI_CACHE_CAPACITY = 1e3;
|
|
948
1140
|
var seenJti = /* @__PURE__ */ new Map();
|
|
1141
|
+
function releaseLogoutTokenJti(jti) {
|
|
1142
|
+
seenJti.delete(jti);
|
|
1143
|
+
}
|
|
949
1144
|
function recordJti(jti, expiresAtMs) {
|
|
950
1145
|
const now = Date.now();
|
|
951
1146
|
const existing = seenJti.get(jti);
|
|
@@ -961,7 +1156,7 @@ function recordJti(jti, expiresAtMs) {
|
|
|
961
1156
|
seenJti.set(jti, expiresAtMs);
|
|
962
1157
|
return true;
|
|
963
1158
|
}
|
|
964
|
-
async function
|
|
1159
|
+
async function verifyLogoutTokenDetailed(logoutToken, ssoBaseUrl, clientId) {
|
|
965
1160
|
const baseUrl = ssoBaseUrl.replace(/\/+$/, "");
|
|
966
1161
|
const header = decodeJwtHeader(logoutToken);
|
|
967
1162
|
if (!header) {
|
|
@@ -1038,8 +1233,21 @@ async function verifyLogoutToken(logoutToken, ssoBaseUrl, clientId) {
|
|
|
1038
1233
|
if (Date.now() >= payload.exp * 1e3 + 6e4) {
|
|
1039
1234
|
throw new SsoError("logout_token_expired", "Logout Token \u5DF2\u8FC7\u671F");
|
|
1040
1235
|
}
|
|
1236
|
+
if (payload.type !== "logout_token") {
|
|
1237
|
+
throw new SsoError(
|
|
1238
|
+
"logout_token_invalid",
|
|
1239
|
+
"Logout Token type \u5FC5\u987B\u4E3A logout_token"
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1041
1242
|
const events = payload.events;
|
|
1042
|
-
if (!events || typeof events !== "object" ||
|
|
1243
|
+
if (!events || typeof events !== "object" || Array.isArray(events)) {
|
|
1244
|
+
throw new SsoError(
|
|
1245
|
+
"logout_token_invalid",
|
|
1246
|
+
"Logout Token \u7F3A\u5C11 backchannel-logout events \u58F0\u660E"
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
const logoutEvent = events[BACKCHANNEL_LOGOUT_EVENT];
|
|
1250
|
+
if (!logoutEvent || typeof logoutEvent !== "object" || Array.isArray(logoutEvent)) {
|
|
1043
1251
|
throw new SsoError(
|
|
1044
1252
|
"logout_token_invalid",
|
|
1045
1253
|
"Logout Token \u7F3A\u5C11 backchannel-logout events \u58F0\u660E"
|
|
@@ -1060,7 +1268,7 @@ async function verifyLogoutToken(logoutToken, ssoBaseUrl, clientId) {
|
|
|
1060
1268
|
if (!recordJti(jti, payload.exp * 1e3 + 6e4)) {
|
|
1061
1269
|
throw new SsoError("logout_token_replay", "Logout Token jti \u91CD\u653E");
|
|
1062
1270
|
}
|
|
1063
|
-
return { sub, sid };
|
|
1271
|
+
return { payload: { sub, sid }, jti };
|
|
1064
1272
|
}
|
|
1065
1273
|
|
|
1066
1274
|
// src/next/backchannel-logout.ts
|
|
@@ -1100,9 +1308,9 @@ function createBackchannelLogoutRouteHandler(config) {
|
|
|
1100
1308
|
{ status: 400 }
|
|
1101
1309
|
);
|
|
1102
1310
|
}
|
|
1103
|
-
let
|
|
1311
|
+
let verified;
|
|
1104
1312
|
try {
|
|
1105
|
-
|
|
1313
|
+
verified = await verifyLogoutTokenDetailed(logoutToken, ssoBaseUrl, clientId);
|
|
1106
1314
|
} catch (err) {
|
|
1107
1315
|
const code = err instanceof SsoError ? err.code : "logout_token_invalid";
|
|
1108
1316
|
const description = err instanceof SsoError ? err.description : "Logout Token \u9A8C\u8BC1\u5931\u8D25";
|
|
@@ -1112,8 +1320,9 @@ function createBackchannelLogoutRouteHandler(config) {
|
|
|
1112
1320
|
);
|
|
1113
1321
|
}
|
|
1114
1322
|
try {
|
|
1115
|
-
await onLogout?.(payload, request);
|
|
1323
|
+
await onLogout?.(verified.payload, request);
|
|
1116
1324
|
} catch (err) {
|
|
1325
|
+
releaseLogoutTokenJti(verified.jti);
|
|
1117
1326
|
console.error(
|
|
1118
1327
|
"[SSO SDK] backchannel logout onLogout \u94A9\u5B50\u6267\u884C\u5931\u8D25:",
|
|
1119
1328
|
err
|