@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.js
CHANGED
|
@@ -23,6 +23,7 @@ __export(next_exports, {
|
|
|
23
23
|
DEFAULT_ACCESS_TOKEN_COOKIE_NAME: () => DEFAULT_ACCESS_TOKEN_COOKIE_NAME,
|
|
24
24
|
DEFAULT_ID_TOKEN_COOKIE_NAME: () => DEFAULT_ID_TOKEN_COOKIE_NAME,
|
|
25
25
|
DEFAULT_LOGOUT_STATE_COOKIE_NAME: () => DEFAULT_LOGOUT_STATE_COOKIE_NAME,
|
|
26
|
+
DEFAULT_NONCE_COOKIE_NAME: () => DEFAULT_NONCE_COOKIE_NAME,
|
|
26
27
|
DEFAULT_REFRESH_TOKEN_COOKIE_NAME: () => DEFAULT_REFRESH_TOKEN_COOKIE_NAME,
|
|
27
28
|
DEFAULT_RETURN_COOKIE_NAME: () => DEFAULT_RETURN_COOKIE_NAME,
|
|
28
29
|
DEFAULT_STATE_COOKIE_NAME: () => DEFAULT_STATE_COOKIE_NAME,
|
|
@@ -44,6 +45,7 @@ var DEFAULT_ACCESS_TOKEN_COOKIE_NAME = "__Host-nihplod_sso_at";
|
|
|
44
45
|
var DEFAULT_REFRESH_TOKEN_COOKIE_NAME = "__Host-nihplod_sso_rt";
|
|
45
46
|
var DEFAULT_ID_TOKEN_COOKIE_NAME = "__Host-nihplod_sso_id";
|
|
46
47
|
var DEFAULT_STATE_COOKIE_NAME = "__Host-nihplod_sso_state";
|
|
48
|
+
var DEFAULT_NONCE_COOKIE_NAME = "__Host-nihplod_sso_nonce";
|
|
47
49
|
var DEFAULT_RETURN_COOKIE_NAME = "__Host-nihplod_sso_return";
|
|
48
50
|
var DEFAULT_VERIFIER_COOKIE_NAME = "__Secure-nihplod_sso_verifier";
|
|
49
51
|
var DEFAULT_LOGOUT_STATE_COOKIE_NAME = "__Host-nihplod_sso_logout_state";
|
|
@@ -199,21 +201,20 @@ function createSsoMiddleware(config) {
|
|
|
199
201
|
const secureCookies = !insecureLocalDev;
|
|
200
202
|
const accessTokenCookieName = insecureLocalDev ? toInsecureCookieName(config.accessTokenCookieName ?? DEFAULT_ACCESS_TOKEN_COOKIE_NAME) : config.accessTokenCookieName ?? DEFAULT_ACCESS_TOKEN_COOKIE_NAME;
|
|
201
203
|
const stateCookieName = insecureLocalDev ? toInsecureCookieName(config.stateCookieName ?? DEFAULT_STATE_COOKIE_NAME) : config.stateCookieName ?? DEFAULT_STATE_COOKIE_NAME;
|
|
204
|
+
const nonceCookieName = insecureLocalDev ? toInsecureCookieName(config.nonceCookieName ?? DEFAULT_NONCE_COOKIE_NAME) : config.nonceCookieName ?? DEFAULT_NONCE_COOKIE_NAME;
|
|
202
205
|
const returnUrlCookieName = insecureLocalDev ? toInsecureCookieName(config.returnUrlCookieName ?? DEFAULT_RETURN_COOKIE_NAME) : config.returnUrlCookieName ?? DEFAULT_RETURN_COOKIE_NAME;
|
|
203
206
|
const verifierCookieName = insecureLocalDev ? toInsecureCookieName(config.verifierCookieName ?? DEFAULT_VERIFIER_COOKIE_NAME) : config.verifierCookieName ?? DEFAULT_VERIFIER_COOKIE_NAME;
|
|
204
207
|
const normalizedBase = ssoBaseUrl.replace(/\/+$/, "");
|
|
205
208
|
const normalizedServerBase = (config.serverBaseUrl ?? ssoBaseUrl).replace(/\/+$/, "");
|
|
206
|
-
if (
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
);
|
|
216
|
-
}
|
|
209
|
+
if (!validateSsoCookie) {
|
|
210
|
+
console.warn(
|
|
211
|
+
"[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"
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
if (!clientSecret) {
|
|
215
|
+
console.warn(
|
|
216
|
+
"[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"
|
|
217
|
+
);
|
|
217
218
|
}
|
|
218
219
|
return async function ssoMiddleware(request) {
|
|
219
220
|
const { pathname } = request.nextUrl;
|
|
@@ -256,6 +257,7 @@ function createSsoMiddleware(config) {
|
|
|
256
257
|
}
|
|
257
258
|
}
|
|
258
259
|
const state = generateRandomString(32);
|
|
260
|
+
const nonce = generateRandomString(43);
|
|
259
261
|
const verifier = generateRandomString(64);
|
|
260
262
|
const challenge = await computeCodeChallenge(verifier);
|
|
261
263
|
const authorizeParams = new URLSearchParams();
|
|
@@ -264,12 +266,14 @@ function createSsoMiddleware(config) {
|
|
|
264
266
|
authorizeParams.set("redirect_uri", redirectUri);
|
|
265
267
|
authorizeParams.set("scope", scopes);
|
|
266
268
|
authorizeParams.set("state", state);
|
|
269
|
+
authorizeParams.set("nonce", nonce);
|
|
267
270
|
authorizeParams.set("code_challenge", challenge);
|
|
268
271
|
authorizeParams.set("code_challenge_method", "S256");
|
|
269
272
|
const loginUrl = new URL("/api/oauth/authorize", normalizedBase);
|
|
270
273
|
loginUrl.search = authorizeParams.toString();
|
|
271
274
|
const response = import_server.NextResponse.redirect(loginUrl);
|
|
272
275
|
response.cookies.set(stateCookieName, state, getHostCookieOptions(600, secureCookies));
|
|
276
|
+
response.cookies.set(nonceCookieName, nonce, getHostCookieOptions(600, secureCookies));
|
|
273
277
|
response.cookies.set(verifierCookieName, verifier, getSecureCookieOptions(600, callbackPath, secureCookies));
|
|
274
278
|
const safeReturnUrl = (request.nextUrl.pathname + request.nextUrl.search).slice(0, 2048);
|
|
275
279
|
response.cookies.set(returnUrlCookieName, safeReturnUrl, getHostCookieOptions(600, secureCookies));
|
|
@@ -358,18 +362,21 @@ function normalizeIssuer(url) {
|
|
|
358
362
|
var cachedJwks = null;
|
|
359
363
|
var cachedDiscovery = null;
|
|
360
364
|
var JWKS_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
365
|
+
var FETCH_TIMEOUT_MS = 1e4;
|
|
361
366
|
async function fetchDiscoveryDoc(baseUrl) {
|
|
362
367
|
const now = Date.now();
|
|
363
368
|
if (cachedDiscovery && cachedDiscovery.baseUrl === baseUrl && now - cachedDiscovery.fetchedAt < JWKS_CACHE_TTL_MS) {
|
|
364
369
|
return cachedDiscovery.doc;
|
|
365
370
|
}
|
|
366
371
|
try {
|
|
367
|
-
const res = await fetch(`${baseUrl}/api/oauth/.well-known/openid-configuration
|
|
368
|
-
|
|
372
|
+
const res = await fetch(`${baseUrl}/api/oauth/.well-known/openid-configuration`, {
|
|
373
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
374
|
+
});
|
|
375
|
+
if (!res.ok) return null;
|
|
376
|
+
const doc = await res.json();
|
|
369
377
|
cachedDiscovery = { baseUrl, doc, fetchedAt: now };
|
|
370
378
|
return doc;
|
|
371
379
|
} catch {
|
|
372
|
-
cachedDiscovery = { baseUrl, doc: null, fetchedAt: now };
|
|
373
380
|
return null;
|
|
374
381
|
}
|
|
375
382
|
}
|
|
@@ -381,7 +388,10 @@ async function fetchJwks(baseUrl, options = {}) {
|
|
|
381
388
|
const discovery = await fetchDiscoveryDoc(baseUrl);
|
|
382
389
|
const jwksUri = discovery?.jwks_uri || `${baseUrl}/api/oauth/jwks`;
|
|
383
390
|
try {
|
|
384
|
-
const res = await fetch(jwksUri,
|
|
391
|
+
const res = await fetch(jwksUri, {
|
|
392
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
393
|
+
...options.forceRefresh ? { cache: "no-cache" } : {}
|
|
394
|
+
});
|
|
385
395
|
if (!res.ok) return null;
|
|
386
396
|
const jwks = await res.json();
|
|
387
397
|
cachedJwks = { baseUrl, jwks, fetchedAt: now };
|
|
@@ -428,7 +438,7 @@ async function computeAtHash(accessToken) {
|
|
|
428
438
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
429
439
|
}
|
|
430
440
|
async function validateIdToken(idToken, accessToken, expectedIssuer, expectedClientId, options = {}) {
|
|
431
|
-
const { rejectHs256WhenRs256Available = true } = options;
|
|
441
|
+
const { rejectHs256WhenRs256Available = true, expectedNonce } = options;
|
|
432
442
|
const header = decodeJwtHeader(idToken);
|
|
433
443
|
if (!header) {
|
|
434
444
|
throw new SsoError("id_token_invalid", "ID Token \u683C\u5F0F\u9519\u8BEF");
|
|
@@ -518,6 +528,12 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
|
|
|
518
528
|
throw new SsoError("id_token_at_hash_mismatch", "ID Token at_hash \u4E0D\u5339\u914D");
|
|
519
529
|
}
|
|
520
530
|
}
|
|
531
|
+
if (expectedNonce !== void 0) {
|
|
532
|
+
const tokenNonce = typeof payload.nonce === "string" ? payload.nonce : "";
|
|
533
|
+
if (!tokenNonce || !timingSafeEqualString(expectedNonce, tokenNonce)) {
|
|
534
|
+
throw new SsoError("id_token_nonce_mismatch", "ID Token nonce \u4E0D\u5339\u914D");
|
|
535
|
+
}
|
|
536
|
+
}
|
|
521
537
|
return { sub: payload.sub };
|
|
522
538
|
}
|
|
523
539
|
|
|
@@ -538,11 +554,19 @@ function createCallbackRouteHandler(config) {
|
|
|
538
554
|
const refreshTokenCookieName = pickName(config.refreshTokenCookieName, DEFAULT_REFRESH_TOKEN_COOKIE_NAME);
|
|
539
555
|
const idTokenCookieName = pickName(config.idTokenCookieName, DEFAULT_ID_TOKEN_COOKIE_NAME);
|
|
540
556
|
const stateCookieName = pickName(config.stateCookieName, DEFAULT_STATE_COOKIE_NAME);
|
|
557
|
+
const nonceCookieName = pickName(config.nonceCookieName, DEFAULT_NONCE_COOKIE_NAME);
|
|
541
558
|
const returnUrlCookieName = pickName(config.returnUrlCookieName, DEFAULT_RETURN_COOKIE_NAME);
|
|
542
559
|
const verifierCookieName = pickName(config.verifierCookieName, DEFAULT_VERIFIER_COOKIE_NAME);
|
|
543
560
|
const normalizedBase = ssoBaseUrl.replace(/\/+$/, "");
|
|
544
561
|
const normalizedServerBase = (config.serverBaseUrl ?? ssoBaseUrl).replace(/\/+$/, "");
|
|
545
562
|
return async function GET(request) {
|
|
563
|
+
const response = await handleCallback(request);
|
|
564
|
+
if (response.status >= 400) {
|
|
565
|
+
response.cookies.set(nonceCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
566
|
+
}
|
|
567
|
+
return response;
|
|
568
|
+
};
|
|
569
|
+
async function handleCallback(request) {
|
|
546
570
|
const { searchParams } = request.nextUrl;
|
|
547
571
|
const error = searchParams.get("error");
|
|
548
572
|
if (error) {
|
|
@@ -573,7 +597,7 @@ function createCallbackRouteHandler(config) {
|
|
|
573
597
|
{ status: 400 }
|
|
574
598
|
);
|
|
575
599
|
}
|
|
576
|
-
if (returnedState
|
|
600
|
+
if (!timingSafeEqualString(returnedState ?? "", savedState)) {
|
|
577
601
|
return import_server2.NextResponse.json(
|
|
578
602
|
{
|
|
579
603
|
error: "invalid_request",
|
|
@@ -582,6 +606,7 @@ function createCallbackRouteHandler(config) {
|
|
|
582
606
|
{ status: 400 }
|
|
583
607
|
);
|
|
584
608
|
}
|
|
609
|
+
const expectedNonce = request.cookies.get(nonceCookieName)?.value;
|
|
585
610
|
const verifier = request.cookies.get(verifierCookieName)?.value;
|
|
586
611
|
if (!verifier) {
|
|
587
612
|
return import_server2.NextResponse.json(
|
|
@@ -663,7 +688,8 @@ function createCallbackRouteHandler(config) {
|
|
|
663
688
|
tokenData.id_token,
|
|
664
689
|
tokenData.access_token,
|
|
665
690
|
normalizedBase,
|
|
666
|
-
clientId
|
|
691
|
+
clientId,
|
|
692
|
+
{ expectedNonce }
|
|
667
693
|
);
|
|
668
694
|
} catch (err) {
|
|
669
695
|
return import_server2.NextResponse.json(
|
|
@@ -707,11 +733,12 @@ function createCallbackRouteHandler(config) {
|
|
|
707
733
|
}
|
|
708
734
|
}
|
|
709
735
|
response.cookies.set(stateCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
736
|
+
response.cookies.set(nonceCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
710
737
|
response.cookies.set(returnUrlCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
711
738
|
response.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, "/", secureCookies));
|
|
712
739
|
response.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, request.nextUrl.pathname, secureCookies));
|
|
713
740
|
return response;
|
|
714
|
-
}
|
|
741
|
+
}
|
|
715
742
|
}
|
|
716
743
|
|
|
717
744
|
// src/next/logout.ts
|
|
@@ -719,7 +746,7 @@ var import_server3 = require("next/server");
|
|
|
719
746
|
|
|
720
747
|
// src/core/discovery.ts
|
|
721
748
|
var DISCOVERY_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
722
|
-
var
|
|
749
|
+
var FETCH_TIMEOUT_MS2 = 5e3;
|
|
723
750
|
var cache = /* @__PURE__ */ new Map();
|
|
724
751
|
var inflight = /* @__PURE__ */ new Map();
|
|
725
752
|
function fetchDiscoveryCached(baseUrl) {
|
|
@@ -731,7 +758,7 @@ function fetchDiscoveryCached(baseUrl) {
|
|
|
731
758
|
if (existing) return existing;
|
|
732
759
|
const promise = (async () => {
|
|
733
760
|
const controller = new AbortController();
|
|
734
|
-
const timeoutId = setTimeout(() => controller.abort(),
|
|
761
|
+
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS2);
|
|
735
762
|
try {
|
|
736
763
|
const res = await fetch(`${baseUrl}/api/oauth/.well-known/openid-configuration`, {
|
|
737
764
|
signal: controller.signal
|
|
@@ -752,6 +779,23 @@ function fetchDiscoveryCached(baseUrl) {
|
|
|
752
779
|
}
|
|
753
780
|
|
|
754
781
|
// src/next/logout.ts
|
|
782
|
+
function buildLogoutConfirmHtml() {
|
|
783
|
+
return `<!doctype html>
|
|
784
|
+
<html lang="zh-CN">
|
|
785
|
+
<head>
|
|
786
|
+
<meta charset="utf-8">
|
|
787
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
788
|
+
<title>\u786E\u8BA4\u9000\u51FA\u767B\u5F55</title>
|
|
789
|
+
</head>
|
|
790
|
+
<body style="display:flex;align-items:center;justify-content:center;min-height:100vh;font-family:system-ui,sans-serif;">
|
|
791
|
+
<form method="post" style="text-align:center;">
|
|
792
|
+
<p>\u786E\u5B9A\u8981\u9000\u51FA\u767B\u5F55\u5417\uFF1F</p>
|
|
793
|
+
<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>
|
|
794
|
+
<p><a href="/" style="color:#2563eb;text-decoration:underline;">\u53D6\u6D88\u5E76\u8FD4\u56DE\u9996\u9875</a></p>
|
|
795
|
+
</form>
|
|
796
|
+
</body>
|
|
797
|
+
</html>`;
|
|
798
|
+
}
|
|
755
799
|
function generateRandomString2(length) {
|
|
756
800
|
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
757
801
|
const maxValid = Math.floor(256 / chars.length) * chars.length;
|
|
@@ -784,6 +828,7 @@ function createLogoutRouteHandler(config) {
|
|
|
784
828
|
const refreshTokenCookieName = pickName(config.refreshTokenCookieName, DEFAULT_REFRESH_TOKEN_COOKIE_NAME);
|
|
785
829
|
const idTokenCookieName = pickName(config.idTokenCookieName, DEFAULT_ID_TOKEN_COOKIE_NAME);
|
|
786
830
|
const stateCookieName = pickName(config.stateCookieName, DEFAULT_STATE_COOKIE_NAME);
|
|
831
|
+
const nonceCookieName = pickName(config.nonceCookieName, DEFAULT_NONCE_COOKIE_NAME);
|
|
787
832
|
const returnUrlCookieName = pickName(config.returnUrlCookieName, DEFAULT_RETURN_COOKIE_NAME);
|
|
788
833
|
const verifierCookieName = pickName(config.verifierCookieName, DEFAULT_VERIFIER_COOKIE_NAME);
|
|
789
834
|
const logoutStateCookieName = pickName(config.logoutStateCookieName, DEFAULT_LOGOUT_STATE_COOKIE_NAME);
|
|
@@ -804,6 +849,12 @@ function createLogoutRouteHandler(config) {
|
|
|
804
849
|
res.cookies.set(logoutStateCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
805
850
|
return res;
|
|
806
851
|
}
|
|
852
|
+
if (request.method === "GET") {
|
|
853
|
+
return new import_server3.NextResponse(buildLogoutConfirmHtml(), {
|
|
854
|
+
status: 200,
|
|
855
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
856
|
+
});
|
|
857
|
+
}
|
|
807
858
|
const refreshToken = request.cookies.get(refreshTokenCookieName)?.value;
|
|
808
859
|
const idTokenHint = request.cookies.get(idTokenCookieName)?.value;
|
|
809
860
|
if (refreshToken) {
|
|
@@ -832,6 +883,7 @@ function createLogoutRouteHandler(config) {
|
|
|
832
883
|
res.cookies.set(refreshTokenCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
833
884
|
res.cookies.set(idTokenCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
834
885
|
res.cookies.set(stateCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
886
|
+
res.cookies.set(nonceCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
835
887
|
res.cookies.set(returnUrlCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
836
888
|
res.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, "/", secureCookies));
|
|
837
889
|
res.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, callbackPath, secureCookies));
|
|
@@ -865,6 +917,7 @@ function createLogoutRouteHandler(config) {
|
|
|
865
917
|
DEFAULT_ACCESS_TOKEN_COOKIE_NAME,
|
|
866
918
|
DEFAULT_ID_TOKEN_COOKIE_NAME,
|
|
867
919
|
DEFAULT_LOGOUT_STATE_COOKIE_NAME,
|
|
920
|
+
DEFAULT_NONCE_COOKIE_NAME,
|
|
868
921
|
DEFAULT_REFRESH_TOKEN_COOKIE_NAME,
|
|
869
922
|
DEFAULT_RETURN_COOKIE_NAME,
|
|
870
923
|
DEFAULT_STATE_COOKIE_NAME,
|