@nihplod/sso-sdk 1.3.0 → 1.3.1
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 +6 -4
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +54 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +54 -6
- package/dist/index.mjs.map +1 -1
- package/dist/next/index.d.mts +9 -2
- package/dist/next/index.d.ts +9 -2
- package/dist/next/index.js +74 -21
- package/dist/next/index.js.map +1 -1
- package/dist/next/index.mjs +73 -21
- package/dist/next/index.mjs.map +1 -1
- package/dist/react/index.d.mts +2 -2
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.js +104 -12
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +104 -12
- package/dist/react/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/next/index.mjs
CHANGED
|
@@ -6,6 +6,7 @@ var DEFAULT_ACCESS_TOKEN_COOKIE_NAME = "__Host-nihplod_sso_at";
|
|
|
6
6
|
var DEFAULT_REFRESH_TOKEN_COOKIE_NAME = "__Host-nihplod_sso_rt";
|
|
7
7
|
var DEFAULT_ID_TOKEN_COOKIE_NAME = "__Host-nihplod_sso_id";
|
|
8
8
|
var DEFAULT_STATE_COOKIE_NAME = "__Host-nihplod_sso_state";
|
|
9
|
+
var DEFAULT_NONCE_COOKIE_NAME = "__Host-nihplod_sso_nonce";
|
|
9
10
|
var DEFAULT_RETURN_COOKIE_NAME = "__Host-nihplod_sso_return";
|
|
10
11
|
var DEFAULT_VERIFIER_COOKIE_NAME = "__Secure-nihplod_sso_verifier";
|
|
11
12
|
var DEFAULT_LOGOUT_STATE_COOKIE_NAME = "__Host-nihplod_sso_logout_state";
|
|
@@ -161,21 +162,20 @@ function createSsoMiddleware(config) {
|
|
|
161
162
|
const secureCookies = !insecureLocalDev;
|
|
162
163
|
const accessTokenCookieName = insecureLocalDev ? toInsecureCookieName(config.accessTokenCookieName ?? DEFAULT_ACCESS_TOKEN_COOKIE_NAME) : config.accessTokenCookieName ?? DEFAULT_ACCESS_TOKEN_COOKIE_NAME;
|
|
163
164
|
const stateCookieName = insecureLocalDev ? toInsecureCookieName(config.stateCookieName ?? DEFAULT_STATE_COOKIE_NAME) : config.stateCookieName ?? DEFAULT_STATE_COOKIE_NAME;
|
|
165
|
+
const nonceCookieName = insecureLocalDev ? toInsecureCookieName(config.nonceCookieName ?? DEFAULT_NONCE_COOKIE_NAME) : config.nonceCookieName ?? DEFAULT_NONCE_COOKIE_NAME;
|
|
164
166
|
const returnUrlCookieName = insecureLocalDev ? toInsecureCookieName(config.returnUrlCookieName ?? DEFAULT_RETURN_COOKIE_NAME) : config.returnUrlCookieName ?? DEFAULT_RETURN_COOKIE_NAME;
|
|
165
167
|
const verifierCookieName = insecureLocalDev ? toInsecureCookieName(config.verifierCookieName ?? DEFAULT_VERIFIER_COOKIE_NAME) : config.verifierCookieName ?? DEFAULT_VERIFIER_COOKIE_NAME;
|
|
166
168
|
const normalizedBase = ssoBaseUrl.replace(/\/+$/, "");
|
|
167
169
|
const normalizedServerBase = (config.serverBaseUrl ?? ssoBaseUrl).replace(/\/+$/, "");
|
|
168
|
-
if (
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
);
|
|
178
|
-
}
|
|
170
|
+
if (!validateSsoCookie) {
|
|
171
|
+
console.warn(
|
|
172
|
+
"[SSO SDK] validateSsoCookie=false\uFF1A\u4E2D\u95F4\u4EF6\u4EC5\u68C0\u67E5 Cookie \u5B58\u5728\u6027\uFF0C\u53EF\u80FD\u653E\u884C\u5DF2\u5931\u6548\u7684\u4F1A\u8BDD\u3002\u4E2D\u95F4\u4EF6\u53EA\u662F UX \u5C42\uFF0C\u654F\u611F\u6570\u636E\u7684\u9274\u6743\u5FC5\u987B\u5728 Route Handler / Server Component \u4E2D\u5B8C\u6210\u3002"
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
if (!clientSecret) {
|
|
176
|
+
console.warn(
|
|
177
|
+
"[SSO SDK] \u672A\u914D\u7F6E clientSecret\uFF08Public Client \u6A21\u5F0F\uFF09\uFF1Aintrospection \u65E0\u5BA2\u6237\u7AEF\u8BA4\u8BC1\uFF0C\u4E2D\u95F4\u4EF6\u5224\u5B9A\u7ED3\u679C\u4EC5\u4F5C UX \u53C2\u8003\u3002Confidential Client\uFF08BFF\uFF09\u8BF7\u914D\u7F6E clientSecret\u3002"
|
|
178
|
+
);
|
|
179
179
|
}
|
|
180
180
|
return async function ssoMiddleware(request) {
|
|
181
181
|
const { pathname } = request.nextUrl;
|
|
@@ -218,6 +218,7 @@ function createSsoMiddleware(config) {
|
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
220
|
const state = generateRandomString(32);
|
|
221
|
+
const nonce = generateRandomString(43);
|
|
221
222
|
const verifier = generateRandomString(64);
|
|
222
223
|
const challenge = await computeCodeChallenge(verifier);
|
|
223
224
|
const authorizeParams = new URLSearchParams();
|
|
@@ -226,12 +227,14 @@ function createSsoMiddleware(config) {
|
|
|
226
227
|
authorizeParams.set("redirect_uri", redirectUri);
|
|
227
228
|
authorizeParams.set("scope", scopes);
|
|
228
229
|
authorizeParams.set("state", state);
|
|
230
|
+
authorizeParams.set("nonce", nonce);
|
|
229
231
|
authorizeParams.set("code_challenge", challenge);
|
|
230
232
|
authorizeParams.set("code_challenge_method", "S256");
|
|
231
233
|
const loginUrl = new URL("/api/oauth/authorize", normalizedBase);
|
|
232
234
|
loginUrl.search = authorizeParams.toString();
|
|
233
235
|
const response = NextResponse.redirect(loginUrl);
|
|
234
236
|
response.cookies.set(stateCookieName, state, getHostCookieOptions(600, secureCookies));
|
|
237
|
+
response.cookies.set(nonceCookieName, nonce, getHostCookieOptions(600, secureCookies));
|
|
235
238
|
response.cookies.set(verifierCookieName, verifier, getSecureCookieOptions(600, callbackPath, secureCookies));
|
|
236
239
|
const safeReturnUrl = (request.nextUrl.pathname + request.nextUrl.search).slice(0, 2048);
|
|
237
240
|
response.cookies.set(returnUrlCookieName, safeReturnUrl, getHostCookieOptions(600, secureCookies));
|
|
@@ -320,18 +323,21 @@ function normalizeIssuer(url) {
|
|
|
320
323
|
var cachedJwks = null;
|
|
321
324
|
var cachedDiscovery = null;
|
|
322
325
|
var JWKS_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
326
|
+
var FETCH_TIMEOUT_MS = 1e4;
|
|
323
327
|
async function fetchDiscoveryDoc(baseUrl) {
|
|
324
328
|
const now = Date.now();
|
|
325
329
|
if (cachedDiscovery && cachedDiscovery.baseUrl === baseUrl && now - cachedDiscovery.fetchedAt < JWKS_CACHE_TTL_MS) {
|
|
326
330
|
return cachedDiscovery.doc;
|
|
327
331
|
}
|
|
328
332
|
try {
|
|
329
|
-
const res = await fetch(`${baseUrl}/api/oauth/.well-known/openid-configuration
|
|
330
|
-
|
|
333
|
+
const res = await fetch(`${baseUrl}/api/oauth/.well-known/openid-configuration`, {
|
|
334
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
335
|
+
});
|
|
336
|
+
if (!res.ok) return null;
|
|
337
|
+
const doc = await res.json();
|
|
331
338
|
cachedDiscovery = { baseUrl, doc, fetchedAt: now };
|
|
332
339
|
return doc;
|
|
333
340
|
} catch {
|
|
334
|
-
cachedDiscovery = { baseUrl, doc: null, fetchedAt: now };
|
|
335
341
|
return null;
|
|
336
342
|
}
|
|
337
343
|
}
|
|
@@ -343,7 +349,10 @@ async function fetchJwks(baseUrl, options = {}) {
|
|
|
343
349
|
const discovery = await fetchDiscoveryDoc(baseUrl);
|
|
344
350
|
const jwksUri = discovery?.jwks_uri || `${baseUrl}/api/oauth/jwks`;
|
|
345
351
|
try {
|
|
346
|
-
const res = await fetch(jwksUri,
|
|
352
|
+
const res = await fetch(jwksUri, {
|
|
353
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
354
|
+
...options.forceRefresh ? { cache: "no-cache" } : {}
|
|
355
|
+
});
|
|
347
356
|
if (!res.ok) return null;
|
|
348
357
|
const jwks = await res.json();
|
|
349
358
|
cachedJwks = { baseUrl, jwks, fetchedAt: now };
|
|
@@ -390,7 +399,7 @@ async function computeAtHash(accessToken) {
|
|
|
390
399
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
391
400
|
}
|
|
392
401
|
async function validateIdToken(idToken, accessToken, expectedIssuer, expectedClientId, options = {}) {
|
|
393
|
-
const { rejectHs256WhenRs256Available = true } = options;
|
|
402
|
+
const { rejectHs256WhenRs256Available = true, expectedNonce } = options;
|
|
394
403
|
const header = decodeJwtHeader(idToken);
|
|
395
404
|
if (!header) {
|
|
396
405
|
throw new SsoError("id_token_invalid", "ID Token \u683C\u5F0F\u9519\u8BEF");
|
|
@@ -480,6 +489,12 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
|
|
|
480
489
|
throw new SsoError("id_token_at_hash_mismatch", "ID Token at_hash \u4E0D\u5339\u914D");
|
|
481
490
|
}
|
|
482
491
|
}
|
|
492
|
+
if (expectedNonce !== void 0) {
|
|
493
|
+
const tokenNonce = typeof payload.nonce === "string" ? payload.nonce : "";
|
|
494
|
+
if (!tokenNonce || !timingSafeEqualString(expectedNonce, tokenNonce)) {
|
|
495
|
+
throw new SsoError("id_token_nonce_mismatch", "ID Token nonce \u4E0D\u5339\u914D");
|
|
496
|
+
}
|
|
497
|
+
}
|
|
483
498
|
return { sub: payload.sub };
|
|
484
499
|
}
|
|
485
500
|
|
|
@@ -500,11 +515,19 @@ function createCallbackRouteHandler(config) {
|
|
|
500
515
|
const refreshTokenCookieName = pickName(config.refreshTokenCookieName, DEFAULT_REFRESH_TOKEN_COOKIE_NAME);
|
|
501
516
|
const idTokenCookieName = pickName(config.idTokenCookieName, DEFAULT_ID_TOKEN_COOKIE_NAME);
|
|
502
517
|
const stateCookieName = pickName(config.stateCookieName, DEFAULT_STATE_COOKIE_NAME);
|
|
518
|
+
const nonceCookieName = pickName(config.nonceCookieName, DEFAULT_NONCE_COOKIE_NAME);
|
|
503
519
|
const returnUrlCookieName = pickName(config.returnUrlCookieName, DEFAULT_RETURN_COOKIE_NAME);
|
|
504
520
|
const verifierCookieName = pickName(config.verifierCookieName, DEFAULT_VERIFIER_COOKIE_NAME);
|
|
505
521
|
const normalizedBase = ssoBaseUrl.replace(/\/+$/, "");
|
|
506
522
|
const normalizedServerBase = (config.serverBaseUrl ?? ssoBaseUrl).replace(/\/+$/, "");
|
|
507
523
|
return async function GET(request) {
|
|
524
|
+
const response = await handleCallback(request);
|
|
525
|
+
if (response.status >= 400) {
|
|
526
|
+
response.cookies.set(nonceCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
527
|
+
}
|
|
528
|
+
return response;
|
|
529
|
+
};
|
|
530
|
+
async function handleCallback(request) {
|
|
508
531
|
const { searchParams } = request.nextUrl;
|
|
509
532
|
const error = searchParams.get("error");
|
|
510
533
|
if (error) {
|
|
@@ -535,7 +558,7 @@ function createCallbackRouteHandler(config) {
|
|
|
535
558
|
{ status: 400 }
|
|
536
559
|
);
|
|
537
560
|
}
|
|
538
|
-
if (returnedState
|
|
561
|
+
if (!timingSafeEqualString(returnedState ?? "", savedState)) {
|
|
539
562
|
return NextResponse2.json(
|
|
540
563
|
{
|
|
541
564
|
error: "invalid_request",
|
|
@@ -544,6 +567,7 @@ function createCallbackRouteHandler(config) {
|
|
|
544
567
|
{ status: 400 }
|
|
545
568
|
);
|
|
546
569
|
}
|
|
570
|
+
const expectedNonce = request.cookies.get(nonceCookieName)?.value;
|
|
547
571
|
const verifier = request.cookies.get(verifierCookieName)?.value;
|
|
548
572
|
if (!verifier) {
|
|
549
573
|
return NextResponse2.json(
|
|
@@ -625,7 +649,8 @@ function createCallbackRouteHandler(config) {
|
|
|
625
649
|
tokenData.id_token,
|
|
626
650
|
tokenData.access_token,
|
|
627
651
|
normalizedBase,
|
|
628
|
-
clientId
|
|
652
|
+
clientId,
|
|
653
|
+
{ expectedNonce }
|
|
629
654
|
);
|
|
630
655
|
} catch (err) {
|
|
631
656
|
return NextResponse2.json(
|
|
@@ -669,11 +694,12 @@ function createCallbackRouteHandler(config) {
|
|
|
669
694
|
}
|
|
670
695
|
}
|
|
671
696
|
response.cookies.set(stateCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
697
|
+
response.cookies.set(nonceCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
672
698
|
response.cookies.set(returnUrlCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
673
699
|
response.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, "/", secureCookies));
|
|
674
700
|
response.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, request.nextUrl.pathname, secureCookies));
|
|
675
701
|
return response;
|
|
676
|
-
}
|
|
702
|
+
}
|
|
677
703
|
}
|
|
678
704
|
|
|
679
705
|
// src/next/logout.ts
|
|
@@ -681,7 +707,7 @@ import { NextResponse as NextResponse3 } from "next/server";
|
|
|
681
707
|
|
|
682
708
|
// src/core/discovery.ts
|
|
683
709
|
var DISCOVERY_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
684
|
-
var
|
|
710
|
+
var FETCH_TIMEOUT_MS2 = 5e3;
|
|
685
711
|
var cache = /* @__PURE__ */ new Map();
|
|
686
712
|
var inflight = /* @__PURE__ */ new Map();
|
|
687
713
|
function fetchDiscoveryCached(baseUrl) {
|
|
@@ -693,7 +719,7 @@ function fetchDiscoveryCached(baseUrl) {
|
|
|
693
719
|
if (existing) return existing;
|
|
694
720
|
const promise = (async () => {
|
|
695
721
|
const controller = new AbortController();
|
|
696
|
-
const timeoutId = setTimeout(() => controller.abort(),
|
|
722
|
+
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS2);
|
|
697
723
|
try {
|
|
698
724
|
const res = await fetch(`${baseUrl}/api/oauth/.well-known/openid-configuration`, {
|
|
699
725
|
signal: controller.signal
|
|
@@ -714,6 +740,23 @@ function fetchDiscoveryCached(baseUrl) {
|
|
|
714
740
|
}
|
|
715
741
|
|
|
716
742
|
// src/next/logout.ts
|
|
743
|
+
function buildLogoutConfirmHtml() {
|
|
744
|
+
return `<!doctype html>
|
|
745
|
+
<html lang="zh-CN">
|
|
746
|
+
<head>
|
|
747
|
+
<meta charset="utf-8">
|
|
748
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
749
|
+
<title>\u786E\u8BA4\u9000\u51FA\u767B\u5F55</title>
|
|
750
|
+
</head>
|
|
751
|
+
<body style="display:flex;align-items:center;justify-content:center;min-height:100vh;font-family:system-ui,sans-serif;">
|
|
752
|
+
<form method="post" style="text-align:center;">
|
|
753
|
+
<p>\u786E\u5B9A\u8981\u9000\u51FA\u767B\u5F55\u5417\uFF1F</p>
|
|
754
|
+
<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
|
+
<p><a href="/" style="color:#2563eb;text-decoration:underline;">\u53D6\u6D88\u5E76\u8FD4\u56DE\u9996\u9875</a></p>
|
|
756
|
+
</form>
|
|
757
|
+
</body>
|
|
758
|
+
</html>`;
|
|
759
|
+
}
|
|
717
760
|
function generateRandomString2(length) {
|
|
718
761
|
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
719
762
|
const maxValid = Math.floor(256 / chars.length) * chars.length;
|
|
@@ -746,6 +789,7 @@ function createLogoutRouteHandler(config) {
|
|
|
746
789
|
const refreshTokenCookieName = pickName(config.refreshTokenCookieName, DEFAULT_REFRESH_TOKEN_COOKIE_NAME);
|
|
747
790
|
const idTokenCookieName = pickName(config.idTokenCookieName, DEFAULT_ID_TOKEN_COOKIE_NAME);
|
|
748
791
|
const stateCookieName = pickName(config.stateCookieName, DEFAULT_STATE_COOKIE_NAME);
|
|
792
|
+
const nonceCookieName = pickName(config.nonceCookieName, DEFAULT_NONCE_COOKIE_NAME);
|
|
749
793
|
const returnUrlCookieName = pickName(config.returnUrlCookieName, DEFAULT_RETURN_COOKIE_NAME);
|
|
750
794
|
const verifierCookieName = pickName(config.verifierCookieName, DEFAULT_VERIFIER_COOKIE_NAME);
|
|
751
795
|
const logoutStateCookieName = pickName(config.logoutStateCookieName, DEFAULT_LOGOUT_STATE_COOKIE_NAME);
|
|
@@ -766,6 +810,12 @@ function createLogoutRouteHandler(config) {
|
|
|
766
810
|
res.cookies.set(logoutStateCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
767
811
|
return res;
|
|
768
812
|
}
|
|
813
|
+
if (request.method === "GET") {
|
|
814
|
+
return new NextResponse3(buildLogoutConfirmHtml(), {
|
|
815
|
+
status: 200,
|
|
816
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
817
|
+
});
|
|
818
|
+
}
|
|
769
819
|
const refreshToken = request.cookies.get(refreshTokenCookieName)?.value;
|
|
770
820
|
const idTokenHint = request.cookies.get(idTokenCookieName)?.value;
|
|
771
821
|
if (refreshToken) {
|
|
@@ -794,6 +844,7 @@ function createLogoutRouteHandler(config) {
|
|
|
794
844
|
res.cookies.set(refreshTokenCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
795
845
|
res.cookies.set(idTokenCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
796
846
|
res.cookies.set(stateCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
847
|
+
res.cookies.set(nonceCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
797
848
|
res.cookies.set(returnUrlCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
798
849
|
res.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, "/", secureCookies));
|
|
799
850
|
res.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, callbackPath, secureCookies));
|
|
@@ -826,6 +877,7 @@ export {
|
|
|
826
877
|
DEFAULT_ACCESS_TOKEN_COOKIE_NAME,
|
|
827
878
|
DEFAULT_ID_TOKEN_COOKIE_NAME,
|
|
828
879
|
DEFAULT_LOGOUT_STATE_COOKIE_NAME,
|
|
880
|
+
DEFAULT_NONCE_COOKIE_NAME,
|
|
829
881
|
DEFAULT_REFRESH_TOKEN_COOKIE_NAME,
|
|
830
882
|
DEFAULT_RETURN_COOKIE_NAME,
|
|
831
883
|
DEFAULT_STATE_COOKIE_NAME,
|