@nihplod/sso-sdk 1.2.1 → 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 +445 -441
- package/dist/index.d.mts +15 -4
- package/dist/index.d.ts +15 -4
- package/dist/index.js +62 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +62 -11
- package/dist/index.mjs.map +1 -1
- package/dist/next/index.d.mts +27 -2
- package/dist/next/index.d.ts +27 -2
- package/dist/next/index.js +118 -41
- package/dist/next/index.js.map +1 -1
- package/dist/next/index.mjs +117 -41
- package/dist/next/index.mjs.map +1 -1
- package/dist/react/index.d.mts +15 -4
- package/dist/react/index.d.ts +15 -4
- package/dist/react/index.js +112 -17
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +112 -17
- package/dist/react/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/next/index.d.ts
CHANGED
|
@@ -57,6 +57,8 @@ interface SsoMiddlewareConfig {
|
|
|
57
57
|
accessTokenCookieName?: string;
|
|
58
58
|
/** State Cookie 名称,默认 __Host-nihplod_sso_state */
|
|
59
59
|
stateCookieName?: string;
|
|
60
|
+
/** OIDC Nonce Cookie 名称,默认 __Host-nihplod_sso_nonce */
|
|
61
|
+
nonceCookieName?: string;
|
|
60
62
|
/** Return URL Cookie 名称,默认 __Host-nihplod_sso_return */
|
|
61
63
|
returnUrlCookieName?: string;
|
|
62
64
|
/** PKCE Verifier Cookie 名称,默认 __Secure-nihplod_sso_verifier */
|
|
@@ -73,6 +75,12 @@ interface SsoMiddlewareConfig {
|
|
|
73
75
|
* middleware / callback / logout 三处配置需保持一致。
|
|
74
76
|
*/
|
|
75
77
|
insecureLocalDev?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* 服务端到服务端调用的内网地址(可选,如 http://127.0.0.1:3000)。
|
|
80
|
+
* 仅用于 introspect 等服务器间请求;浏览器跳转(authorize)仍使用
|
|
81
|
+
* ssoBaseUrl 公网地址。适用于子站与 SSO 中心同机/同内网部署。
|
|
82
|
+
*/
|
|
83
|
+
serverBaseUrl?: string;
|
|
76
84
|
}
|
|
77
85
|
declare function createSsoMiddleware(config: SsoMiddlewareConfig): (request: NextRequest) => Promise<NextResponse<unknown>>;
|
|
78
86
|
|
|
@@ -123,6 +131,8 @@ interface CallbackRouteConfig {
|
|
|
123
131
|
idTokenCookieName?: string;
|
|
124
132
|
/** State Cookie 名称,默认 __Host-nihplod_sso_state */
|
|
125
133
|
stateCookieName?: string;
|
|
134
|
+
/** OIDC Nonce Cookie 名称,默认 __Host-nihplod_sso_nonce(须与 createSsoMiddleware 一致) */
|
|
135
|
+
nonceCookieName?: string;
|
|
126
136
|
/** Return URL Cookie 名称,默认 __Host-nihplod_sso_return */
|
|
127
137
|
returnUrlCookieName?: string;
|
|
128
138
|
/** PKCE Verifier Cookie 名称,默认 __Secure-nihplod_sso_verifier */
|
|
@@ -135,6 +145,11 @@ interface CallbackRouteConfig {
|
|
|
135
145
|
* 强制忽略并告警。
|
|
136
146
|
*/
|
|
137
147
|
insecureLocalDev?: boolean;
|
|
148
|
+
/**
|
|
149
|
+
* 服务端到服务端调用的内网地址(可选,如 http://127.0.0.1:3000)。
|
|
150
|
+
* 仅用于 token 交换等服务器间请求;浏览器跳转仍使用 ssoBaseUrl 公网地址。
|
|
151
|
+
*/
|
|
152
|
+
serverBaseUrl?: string;
|
|
138
153
|
}
|
|
139
154
|
declare function createCallbackRouteHandler(config: CallbackRouteConfig): (request: NextRequest) => Promise<NextResponse<unknown>>;
|
|
140
155
|
|
|
@@ -158,7 +173,7 @@ declare function createCallbackRouteHandler(config: CallbackRouteConfig): (reque
|
|
|
158
173
|
* postLogoutRedirectUri: "https://myapp.com/",
|
|
159
174
|
* });
|
|
160
175
|
*
|
|
161
|
-
* // 推荐使用 POST 触发登出(防登出 CSRF
|
|
176
|
+
* // 推荐使用 POST 触发登出(防登出 CSRF);GET 不执行登出,仅返回确认页
|
|
162
177
|
* export const GET = handler;
|
|
163
178
|
* export const POST = handler;
|
|
164
179
|
* ```
|
|
@@ -188,6 +203,8 @@ interface LogoutRouteConfig {
|
|
|
188
203
|
idTokenCookieName?: string;
|
|
189
204
|
/** State Cookie 名称 */
|
|
190
205
|
stateCookieName?: string;
|
|
206
|
+
/** OIDC Nonce Cookie 名称,默认 __Host-nihplod_sso_nonce(须与 middleware/callback 一致) */
|
|
207
|
+
nonceCookieName?: string;
|
|
191
208
|
/** Return URL Cookie 名称 */
|
|
192
209
|
returnUrlCookieName?: string;
|
|
193
210
|
/** PKCE Verifier Cookie 名称 */
|
|
@@ -196,6 +213,13 @@ interface LogoutRouteConfig {
|
|
|
196
213
|
callbackPath?: string;
|
|
197
214
|
/** Logout State Cookie 名称(RP-Initiated Logout CSRF 防护),默认 __Host-nihplod_sso_logout_state */
|
|
198
215
|
logoutStateCookieName?: string;
|
|
216
|
+
/**
|
|
217
|
+
* 服务端到服务端调用的内网地址(可选,如 http://127.0.0.1:3000)。
|
|
218
|
+
* 仅用于 discovery / revocation 等服务器间请求;浏览器跳转
|
|
219
|
+
* (end-session 等)始终使用 ssoBaseUrl 公网地址。
|
|
220
|
+
* 适用于子站与 SSO 中心同机/同内网部署:避免经公网代理回源的延迟。
|
|
221
|
+
*/
|
|
222
|
+
serverBaseUrl?: string;
|
|
199
223
|
/**
|
|
200
224
|
* 本地 HTTP 开发模式(默认 false)。关闭 Cookie 的 Secure 属性并去除
|
|
201
225
|
* __Host-/__Secure- 前缀;必须与 middleware / callback 的配置保持一致,
|
|
@@ -220,6 +244,7 @@ declare const DEFAULT_ACCESS_TOKEN_COOKIE_NAME = "__Host-nihplod_sso_at";
|
|
|
220
244
|
declare const DEFAULT_REFRESH_TOKEN_COOKIE_NAME = "__Host-nihplod_sso_rt";
|
|
221
245
|
declare const DEFAULT_ID_TOKEN_COOKIE_NAME = "__Host-nihplod_sso_id";
|
|
222
246
|
declare const DEFAULT_STATE_COOKIE_NAME = "__Host-nihplod_sso_state";
|
|
247
|
+
declare const DEFAULT_NONCE_COOKIE_NAME = "__Host-nihplod_sso_nonce";
|
|
223
248
|
declare const DEFAULT_RETURN_COOKIE_NAME = "__Host-nihplod_sso_return";
|
|
224
249
|
declare const DEFAULT_VERIFIER_COOKIE_NAME = "__Secure-nihplod_sso_verifier";
|
|
225
250
|
declare const DEFAULT_LOGOUT_STATE_COOKIE_NAME = "__Host-nihplod_sso_logout_state";
|
|
@@ -246,4 +271,4 @@ declare function getSecureCookieOptions(maxAge?: number, path?: string, secure?:
|
|
|
246
271
|
maxAge?: number;
|
|
247
272
|
};
|
|
248
273
|
|
|
249
|
-
export { type CallbackRouteConfig, DEFAULT_ACCESS_TOKEN_COOKIE_NAME, DEFAULT_ID_TOKEN_COOKIE_NAME, DEFAULT_LOGOUT_STATE_COOKIE_NAME, DEFAULT_REFRESH_TOKEN_COOKIE_NAME, DEFAULT_RETURN_COOKIE_NAME, DEFAULT_STATE_COOKIE_NAME, DEFAULT_VERIFIER_COOKIE_NAME, type LogoutRouteConfig, type SsoMiddlewareConfig, createCallbackRouteHandler, createLogoutRouteHandler, createSsoMiddleware, getHostCookieOptions, getSecureCookieOptions, toInsecureCookieName };
|
|
274
|
+
export { type CallbackRouteConfig, DEFAULT_ACCESS_TOKEN_COOKIE_NAME, DEFAULT_ID_TOKEN_COOKIE_NAME, DEFAULT_LOGOUT_STATE_COOKIE_NAME, DEFAULT_NONCE_COOKIE_NAME, DEFAULT_REFRESH_TOKEN_COOKIE_NAME, DEFAULT_RETURN_COOKIE_NAME, DEFAULT_STATE_COOKIE_NAME, DEFAULT_VERIFIER_COOKIE_NAME, type LogoutRouteConfig, type SsoMiddlewareConfig, createCallbackRouteHandler, createLogoutRouteHandler, createSsoMiddleware, getHostCookieOptions, getSecureCookieOptions, toInsecureCookieName };
|
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,20 +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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
208
|
+
const normalizedServerBase = (config.serverBaseUrl ?? ssoBaseUrl).replace(/\/+$/, "");
|
|
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
|
+
);
|
|
216
218
|
}
|
|
217
219
|
return async function ssoMiddleware(request) {
|
|
218
220
|
const { pathname } = request.nextUrl;
|
|
@@ -231,7 +233,7 @@ function createSsoMiddleware(config) {
|
|
|
231
233
|
if (validateSsoCookie) {
|
|
232
234
|
const tokenActive = await introspectAccessToken(
|
|
233
235
|
ssoSession.value,
|
|
234
|
-
|
|
236
|
+
normalizedServerBase,
|
|
235
237
|
clientId,
|
|
236
238
|
clientSecret
|
|
237
239
|
);
|
|
@@ -246,7 +248,7 @@ function createSsoMiddleware(config) {
|
|
|
246
248
|
if (accessTokenCookie?.value) {
|
|
247
249
|
const tokenActive = await introspectAccessToken(
|
|
248
250
|
accessTokenCookie.value,
|
|
249
|
-
|
|
251
|
+
normalizedServerBase,
|
|
250
252
|
clientId,
|
|
251
253
|
clientSecret
|
|
252
254
|
);
|
|
@@ -255,6 +257,7 @@ function createSsoMiddleware(config) {
|
|
|
255
257
|
}
|
|
256
258
|
}
|
|
257
259
|
const state = generateRandomString(32);
|
|
260
|
+
const nonce = generateRandomString(43);
|
|
258
261
|
const verifier = generateRandomString(64);
|
|
259
262
|
const challenge = await computeCodeChallenge(verifier);
|
|
260
263
|
const authorizeParams = new URLSearchParams();
|
|
@@ -263,12 +266,14 @@ function createSsoMiddleware(config) {
|
|
|
263
266
|
authorizeParams.set("redirect_uri", redirectUri);
|
|
264
267
|
authorizeParams.set("scope", scopes);
|
|
265
268
|
authorizeParams.set("state", state);
|
|
269
|
+
authorizeParams.set("nonce", nonce);
|
|
266
270
|
authorizeParams.set("code_challenge", challenge);
|
|
267
271
|
authorizeParams.set("code_challenge_method", "S256");
|
|
268
272
|
const loginUrl = new URL("/api/oauth/authorize", normalizedBase);
|
|
269
273
|
loginUrl.search = authorizeParams.toString();
|
|
270
274
|
const response = import_server.NextResponse.redirect(loginUrl);
|
|
271
275
|
response.cookies.set(stateCookieName, state, getHostCookieOptions(600, secureCookies));
|
|
276
|
+
response.cookies.set(nonceCookieName, nonce, getHostCookieOptions(600, secureCookies));
|
|
272
277
|
response.cookies.set(verifierCookieName, verifier, getSecureCookieOptions(600, callbackPath, secureCookies));
|
|
273
278
|
const safeReturnUrl = (request.nextUrl.pathname + request.nextUrl.search).slice(0, 2048);
|
|
274
279
|
response.cookies.set(returnUrlCookieName, safeReturnUrl, getHostCookieOptions(600, secureCookies));
|
|
@@ -357,18 +362,21 @@ function normalizeIssuer(url) {
|
|
|
357
362
|
var cachedJwks = null;
|
|
358
363
|
var cachedDiscovery = null;
|
|
359
364
|
var JWKS_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
365
|
+
var FETCH_TIMEOUT_MS = 1e4;
|
|
360
366
|
async function fetchDiscoveryDoc(baseUrl) {
|
|
361
367
|
const now = Date.now();
|
|
362
368
|
if (cachedDiscovery && cachedDiscovery.baseUrl === baseUrl && now - cachedDiscovery.fetchedAt < JWKS_CACHE_TTL_MS) {
|
|
363
369
|
return cachedDiscovery.doc;
|
|
364
370
|
}
|
|
365
371
|
try {
|
|
366
|
-
const res = await fetch(`${baseUrl}/api/oauth/.well-known/openid-configuration
|
|
367
|
-
|
|
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();
|
|
368
377
|
cachedDiscovery = { baseUrl, doc, fetchedAt: now };
|
|
369
378
|
return doc;
|
|
370
379
|
} catch {
|
|
371
|
-
cachedDiscovery = { baseUrl, doc: null, fetchedAt: now };
|
|
372
380
|
return null;
|
|
373
381
|
}
|
|
374
382
|
}
|
|
@@ -380,7 +388,10 @@ async function fetchJwks(baseUrl, options = {}) {
|
|
|
380
388
|
const discovery = await fetchDiscoveryDoc(baseUrl);
|
|
381
389
|
const jwksUri = discovery?.jwks_uri || `${baseUrl}/api/oauth/jwks`;
|
|
382
390
|
try {
|
|
383
|
-
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
|
+
});
|
|
384
395
|
if (!res.ok) return null;
|
|
385
396
|
const jwks = await res.json();
|
|
386
397
|
cachedJwks = { baseUrl, jwks, fetchedAt: now };
|
|
@@ -427,7 +438,7 @@ async function computeAtHash(accessToken) {
|
|
|
427
438
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
428
439
|
}
|
|
429
440
|
async function validateIdToken(idToken, accessToken, expectedIssuer, expectedClientId, options = {}) {
|
|
430
|
-
const { rejectHs256WhenRs256Available = true } = options;
|
|
441
|
+
const { rejectHs256WhenRs256Available = true, expectedNonce } = options;
|
|
431
442
|
const header = decodeJwtHeader(idToken);
|
|
432
443
|
if (!header) {
|
|
433
444
|
throw new SsoError("id_token_invalid", "ID Token \u683C\u5F0F\u9519\u8BEF");
|
|
@@ -517,6 +528,12 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
|
|
|
517
528
|
throw new SsoError("id_token_at_hash_mismatch", "ID Token at_hash \u4E0D\u5339\u914D");
|
|
518
529
|
}
|
|
519
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
|
+
}
|
|
520
537
|
return { sub: payload.sub };
|
|
521
538
|
}
|
|
522
539
|
|
|
@@ -537,10 +554,19 @@ function createCallbackRouteHandler(config) {
|
|
|
537
554
|
const refreshTokenCookieName = pickName(config.refreshTokenCookieName, DEFAULT_REFRESH_TOKEN_COOKIE_NAME);
|
|
538
555
|
const idTokenCookieName = pickName(config.idTokenCookieName, DEFAULT_ID_TOKEN_COOKIE_NAME);
|
|
539
556
|
const stateCookieName = pickName(config.stateCookieName, DEFAULT_STATE_COOKIE_NAME);
|
|
557
|
+
const nonceCookieName = pickName(config.nonceCookieName, DEFAULT_NONCE_COOKIE_NAME);
|
|
540
558
|
const returnUrlCookieName = pickName(config.returnUrlCookieName, DEFAULT_RETURN_COOKIE_NAME);
|
|
541
559
|
const verifierCookieName = pickName(config.verifierCookieName, DEFAULT_VERIFIER_COOKIE_NAME);
|
|
542
560
|
const normalizedBase = ssoBaseUrl.replace(/\/+$/, "");
|
|
561
|
+
const normalizedServerBase = (config.serverBaseUrl ?? ssoBaseUrl).replace(/\/+$/, "");
|
|
543
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) {
|
|
544
570
|
const { searchParams } = request.nextUrl;
|
|
545
571
|
const error = searchParams.get("error");
|
|
546
572
|
if (error) {
|
|
@@ -571,7 +597,7 @@ function createCallbackRouteHandler(config) {
|
|
|
571
597
|
{ status: 400 }
|
|
572
598
|
);
|
|
573
599
|
}
|
|
574
|
-
if (returnedState
|
|
600
|
+
if (!timingSafeEqualString(returnedState ?? "", savedState)) {
|
|
575
601
|
return import_server2.NextResponse.json(
|
|
576
602
|
{
|
|
577
603
|
error: "invalid_request",
|
|
@@ -580,6 +606,7 @@ function createCallbackRouteHandler(config) {
|
|
|
580
606
|
{ status: 400 }
|
|
581
607
|
);
|
|
582
608
|
}
|
|
609
|
+
const expectedNonce = request.cookies.get(nonceCookieName)?.value;
|
|
583
610
|
const verifier = request.cookies.get(verifierCookieName)?.value;
|
|
584
611
|
if (!verifier) {
|
|
585
612
|
return import_server2.NextResponse.json(
|
|
@@ -590,7 +617,7 @@ function createCallbackRouteHandler(config) {
|
|
|
590
617
|
{ status: 400 }
|
|
591
618
|
);
|
|
592
619
|
}
|
|
593
|
-
const tokenEndpoint = `${
|
|
620
|
+
const tokenEndpoint = `${normalizedServerBase}/api/oauth/token`;
|
|
594
621
|
const body = new URLSearchParams();
|
|
595
622
|
body.set("grant_type", "authorization_code");
|
|
596
623
|
body.set("code", code);
|
|
@@ -661,7 +688,8 @@ function createCallbackRouteHandler(config) {
|
|
|
661
688
|
tokenData.id_token,
|
|
662
689
|
tokenData.access_token,
|
|
663
690
|
normalizedBase,
|
|
664
|
-
clientId
|
|
691
|
+
clientId,
|
|
692
|
+
{ expectedNonce }
|
|
665
693
|
);
|
|
666
694
|
} catch (err) {
|
|
667
695
|
return import_server2.NextResponse.json(
|
|
@@ -705,30 +733,68 @@ function createCallbackRouteHandler(config) {
|
|
|
705
733
|
}
|
|
706
734
|
}
|
|
707
735
|
response.cookies.set(stateCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
736
|
+
response.cookies.set(nonceCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
708
737
|
response.cookies.set(returnUrlCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
709
738
|
response.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, "/", secureCookies));
|
|
710
739
|
response.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, request.nextUrl.pathname, secureCookies));
|
|
711
740
|
return response;
|
|
712
|
-
}
|
|
741
|
+
}
|
|
713
742
|
}
|
|
714
743
|
|
|
715
744
|
// src/next/logout.ts
|
|
716
745
|
var import_server3 = require("next/server");
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
return
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
746
|
+
|
|
747
|
+
// src/core/discovery.ts
|
|
748
|
+
var DISCOVERY_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
749
|
+
var FETCH_TIMEOUT_MS2 = 5e3;
|
|
750
|
+
var cache = /* @__PURE__ */ new Map();
|
|
751
|
+
var inflight = /* @__PURE__ */ new Map();
|
|
752
|
+
function fetchDiscoveryCached(baseUrl) {
|
|
753
|
+
const hit = cache.get(baseUrl);
|
|
754
|
+
if (hit && hit.expiresAt > Date.now()) {
|
|
755
|
+
return Promise.resolve(hit.doc);
|
|
756
|
+
}
|
|
757
|
+
const existing = inflight.get(baseUrl);
|
|
758
|
+
if (existing) return existing;
|
|
759
|
+
const promise = (async () => {
|
|
760
|
+
const controller = new AbortController();
|
|
761
|
+
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS2);
|
|
762
|
+
try {
|
|
763
|
+
const res = await fetch(`${baseUrl}/api/oauth/.well-known/openid-configuration`, {
|
|
764
|
+
signal: controller.signal
|
|
765
|
+
});
|
|
766
|
+
if (!res.ok) return null;
|
|
767
|
+
const doc = await res.json();
|
|
768
|
+
cache.set(baseUrl, { doc, expiresAt: Date.now() + DISCOVERY_CACHE_TTL_MS });
|
|
769
|
+
return doc;
|
|
770
|
+
} catch {
|
|
771
|
+
return null;
|
|
772
|
+
} finally {
|
|
773
|
+
clearTimeout(timeoutId);
|
|
774
|
+
inflight.delete(baseUrl);
|
|
775
|
+
}
|
|
776
|
+
})();
|
|
777
|
+
inflight.set(baseUrl, promise);
|
|
778
|
+
return promise;
|
|
779
|
+
}
|
|
780
|
+
|
|
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>`;
|
|
732
798
|
}
|
|
733
799
|
function generateRandomString2(length) {
|
|
734
800
|
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
@@ -762,10 +828,12 @@ function createLogoutRouteHandler(config) {
|
|
|
762
828
|
const refreshTokenCookieName = pickName(config.refreshTokenCookieName, DEFAULT_REFRESH_TOKEN_COOKIE_NAME);
|
|
763
829
|
const idTokenCookieName = pickName(config.idTokenCookieName, DEFAULT_ID_TOKEN_COOKIE_NAME);
|
|
764
830
|
const stateCookieName = pickName(config.stateCookieName, DEFAULT_STATE_COOKIE_NAME);
|
|
831
|
+
const nonceCookieName = pickName(config.nonceCookieName, DEFAULT_NONCE_COOKIE_NAME);
|
|
765
832
|
const returnUrlCookieName = pickName(config.returnUrlCookieName, DEFAULT_RETURN_COOKIE_NAME);
|
|
766
833
|
const verifierCookieName = pickName(config.verifierCookieName, DEFAULT_VERIFIER_COOKIE_NAME);
|
|
767
834
|
const logoutStateCookieName = pickName(config.logoutStateCookieName, DEFAULT_LOGOUT_STATE_COOKIE_NAME);
|
|
768
835
|
const normalizedBase = ssoBaseUrl.replace(/\/+$/, "");
|
|
836
|
+
const normalizedServerBase = (config.serverBaseUrl ?? ssoBaseUrl).replace(/\/+$/, "");
|
|
769
837
|
const callbackOrigin = new URL(redirectUri).origin;
|
|
770
838
|
return async function handler(request) {
|
|
771
839
|
const returnedState = request.nextUrl.searchParams.get("state");
|
|
@@ -781,12 +849,18 @@ function createLogoutRouteHandler(config) {
|
|
|
781
849
|
res.cookies.set(logoutStateCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
782
850
|
return res;
|
|
783
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
|
+
}
|
|
784
858
|
const refreshToken = request.cookies.get(refreshTokenCookieName)?.value;
|
|
785
859
|
const idTokenHint = request.cookies.get(idTokenCookieName)?.value;
|
|
786
860
|
if (refreshToken) {
|
|
787
861
|
try {
|
|
788
|
-
const discovery = await
|
|
789
|
-
const revokeUrl = discovery?.revocation_endpoint || `${normalizedBase}/api/oauth/revoke`;
|
|
862
|
+
const discovery = config.serverBaseUrl ? null : await fetchDiscoveryCached(normalizedServerBase);
|
|
863
|
+
const revokeUrl = config.serverBaseUrl ? `${normalizedServerBase}/api/oauth/revoke` : discovery?.revocation_endpoint || `${normalizedBase}/api/oauth/revoke`;
|
|
790
864
|
const body = new URLSearchParams({
|
|
791
865
|
token: refreshToken,
|
|
792
866
|
token_type_hint: "refresh_token",
|
|
@@ -798,7 +872,8 @@ function createLogoutRouteHandler(config) {
|
|
|
798
872
|
await fetch(revokeUrl, {
|
|
799
873
|
method: "POST",
|
|
800
874
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
801
|
-
body: body.toString()
|
|
875
|
+
body: body.toString(),
|
|
876
|
+
signal: AbortSignal.timeout(3e3)
|
|
802
877
|
});
|
|
803
878
|
} catch {
|
|
804
879
|
}
|
|
@@ -808,13 +883,14 @@ function createLogoutRouteHandler(config) {
|
|
|
808
883
|
res.cookies.set(refreshTokenCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
809
884
|
res.cookies.set(idTokenCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
810
885
|
res.cookies.set(stateCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
886
|
+
res.cookies.set(nonceCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
811
887
|
res.cookies.set(returnUrlCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
812
888
|
res.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, "/", secureCookies));
|
|
813
889
|
res.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, callbackPath, secureCookies));
|
|
814
890
|
return res;
|
|
815
891
|
};
|
|
816
892
|
if (redirectToSso) {
|
|
817
|
-
const discovery = await
|
|
893
|
+
const discovery = await fetchDiscoveryCached(normalizedServerBase);
|
|
818
894
|
const endSessionEndpoint = discovery?.end_session_endpoint || `${normalizedBase}/api/oauth/end-session`;
|
|
819
895
|
const logoutUrl = new URL(endSessionEndpoint);
|
|
820
896
|
logoutUrl.searchParams.set("client_id", clientId);
|
|
@@ -841,6 +917,7 @@ function createLogoutRouteHandler(config) {
|
|
|
841
917
|
DEFAULT_ACCESS_TOKEN_COOKIE_NAME,
|
|
842
918
|
DEFAULT_ID_TOKEN_COOKIE_NAME,
|
|
843
919
|
DEFAULT_LOGOUT_STATE_COOKIE_NAME,
|
|
920
|
+
DEFAULT_NONCE_COOKIE_NAME,
|
|
844
921
|
DEFAULT_REFRESH_TOKEN_COOKIE_NAME,
|
|
845
922
|
DEFAULT_RETURN_COOKIE_NAME,
|
|
846
923
|
DEFAULT_STATE_COOKIE_NAME,
|