@nihplod/sso-sdk 1.3.0 → 1.4.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 +52 -9
- package/dist/index.d.mts +26 -6
- package/dist/index.d.ts +26 -6
- package/dist/index.js +118 -10
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +118 -10
- package/dist/index.mjs.map +1 -1
- package/dist/next/index.d.mts +81 -4
- package/dist/next/index.d.ts +81 -4
- package/dist/next/index.js +287 -23
- package/dist/next/index.js.map +1 -1
- package/dist/next/index.mjs +285 -23
- package/dist/next/index.mjs.map +1 -1
- package/dist/react/index.d.mts +26 -6
- package/dist/react/index.d.ts +26 -6
- package/dist/react/index.js +176 -16
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +176 -16
- 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 */
|
|
@@ -129,6 +131,8 @@ interface CallbackRouteConfig {
|
|
|
129
131
|
idTokenCookieName?: string;
|
|
130
132
|
/** State Cookie 名称,默认 __Host-nihplod_sso_state */
|
|
131
133
|
stateCookieName?: string;
|
|
134
|
+
/** OIDC Nonce Cookie 名称,默认 __Host-nihplod_sso_nonce(须与 createSsoMiddleware 一致) */
|
|
135
|
+
nonceCookieName?: string;
|
|
132
136
|
/** Return URL Cookie 名称,默认 __Host-nihplod_sso_return */
|
|
133
137
|
returnUrlCookieName?: string;
|
|
134
138
|
/** PKCE Verifier Cookie 名称,默认 __Secure-nihplod_sso_verifier */
|
|
@@ -155,7 +159,9 @@ declare function createCallbackRouteHandler(config: CallbackRouteConfig): (reque
|
|
|
155
159
|
* 在 /api/auth/logout 路由中处理本地登出 + RP-Initiated Logout:
|
|
156
160
|
* 1. 从 cookie 读取 refresh_token 并调用 revocation_endpoint 撤销
|
|
157
161
|
* 2. 清除所有 SSO cookie
|
|
158
|
-
* 3.
|
|
162
|
+
* 3. 按退出范围(defaultScope / 表单 global 字段)决定:
|
|
163
|
+
* - "local"(默认):仅退出本站,重定向回本站首页
|
|
164
|
+
* - "global":重定向到 SSO 中心 end-session,全局退出所有 NIHPLOD 平台
|
|
159
165
|
*
|
|
160
166
|
* 用法 (src/app/api/auth/logout/route.ts):
|
|
161
167
|
* ```ts
|
|
@@ -169,7 +175,7 @@ declare function createCallbackRouteHandler(config: CallbackRouteConfig): (reque
|
|
|
169
175
|
* postLogoutRedirectUri: "https://myapp.com/",
|
|
170
176
|
* });
|
|
171
177
|
*
|
|
172
|
-
* // 推荐使用 POST 触发登出(防登出 CSRF
|
|
178
|
+
* // 推荐使用 POST 触发登出(防登出 CSRF);GET 不执行登出,仅返回确认页
|
|
173
179
|
* export const GET = handler;
|
|
174
180
|
* export const POST = handler;
|
|
175
181
|
* ```
|
|
@@ -189,7 +195,18 @@ interface LogoutRouteConfig {
|
|
|
189
195
|
clientSecret?: string;
|
|
190
196
|
/** 登出后跳转回子项目的地址,默认取 redirectUri 的 origin */
|
|
191
197
|
postLogoutRedirectUri?: string;
|
|
192
|
-
/**
|
|
198
|
+
/**
|
|
199
|
+
* 默认退出范围(默认 "local"):
|
|
200
|
+
* - "local":仅退出本站(撤销 refresh_token + 清本地 cookie),不跳转 SSO 中心;
|
|
201
|
+
* - "global":同时跳转 SSO 中心 end-session 全局退出(RP-Initiated Logout)。
|
|
202
|
+
* 用户可在 GET 确认页勾选"同时退出所有 NIHPLOD 平台"后通过表单字段
|
|
203
|
+
* global=1 覆盖默认值;POST 表单携带 global 字段时以表单为准。
|
|
204
|
+
*/
|
|
205
|
+
defaultScope?: "local" | "global";
|
|
206
|
+
/**
|
|
207
|
+
* @deprecated 请改用 defaultScope(true → "global",false → "local")。
|
|
208
|
+
* 仅为兼容保留;与 defaultScope 同时传入时以 defaultScope 为准。
|
|
209
|
+
*/
|
|
193
210
|
redirectToSso?: boolean;
|
|
194
211
|
/** Access Token Cookie 名称 */
|
|
195
212
|
accessTokenCookieName?: string;
|
|
@@ -199,6 +216,8 @@ interface LogoutRouteConfig {
|
|
|
199
216
|
idTokenCookieName?: string;
|
|
200
217
|
/** State Cookie 名称 */
|
|
201
218
|
stateCookieName?: string;
|
|
219
|
+
/** OIDC Nonce Cookie 名称,默认 __Host-nihplod_sso_nonce(须与 middleware/callback 一致) */
|
|
220
|
+
nonceCookieName?: string;
|
|
202
221
|
/** Return URL Cookie 名称 */
|
|
203
222
|
returnUrlCookieName?: string;
|
|
204
223
|
/** PKCE Verifier Cookie 名称 */
|
|
@@ -224,6 +243,63 @@ interface LogoutRouteConfig {
|
|
|
224
243
|
}
|
|
225
244
|
declare function createLogoutRouteHandler(config: LogoutRouteConfig): (request: NextRequest) => Promise<NextResponse<unknown>>;
|
|
226
245
|
|
|
246
|
+
/** 验证通过的 logout_token 负载(仅暴露子站清会话所需的标识) */
|
|
247
|
+
interface LogoutTokenPayload {
|
|
248
|
+
sub?: string;
|
|
249
|
+
sid?: string;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* App Router Backchannel Logout 接收端 Route Handler
|
|
254
|
+
*
|
|
255
|
+
* 接收 SSO 中心在用户全局登出 / 撤销授权时推送的 logout_token
|
|
256
|
+
* (OIDC Back-Channel Logout 1.0),验证通过后清除本站 SSO cookie 并
|
|
257
|
+
* 调用 onLogout 钩子(子站在钩子里清除自己的本地会话,如数据库 session)。
|
|
258
|
+
*
|
|
259
|
+
* 前置条件:在 SSO 中心管理后台为本 client 注册 backchannelLogoutUri
|
|
260
|
+
* 指向本路由的公网地址(必须 HTTPS)。
|
|
261
|
+
*
|
|
262
|
+
* 用法 (src/app/api/auth/backchannel-logout/route.ts):
|
|
263
|
+
* ```ts
|
|
264
|
+
* import { createBackchannelLogoutRouteHandler } from "@nihplod/sso-sdk/next";
|
|
265
|
+
*
|
|
266
|
+
* export const POST = createBackchannelLogoutRouteHandler({
|
|
267
|
+
* clientId: "my-app",
|
|
268
|
+
* ssoBaseUrl: "https://nihplod.cn",
|
|
269
|
+
* onLogout: async ({ sub, sid }) => {
|
|
270
|
+
* // 清除子站本地会话(按 sub 或 sid 定位)
|
|
271
|
+
* },
|
|
272
|
+
* });
|
|
273
|
+
* ```
|
|
274
|
+
*/
|
|
275
|
+
|
|
276
|
+
interface BackchannelLogoutRouteConfig {
|
|
277
|
+
/** OAuth Client ID(logout_token 的 aud 必须等于它) */
|
|
278
|
+
clientId: string;
|
|
279
|
+
/** SSO 中心地址(Discovery / JWKS 基准) */
|
|
280
|
+
ssoBaseUrl: string;
|
|
281
|
+
/**
|
|
282
|
+
* 登出通知钩子:logout_token 验证通过后调用,
|
|
283
|
+
* 子站在此处按 sub / sid 清除自己的本地会话(数据库 session 等)。
|
|
284
|
+
* 抛错时返回 500,让 IdP 重投。
|
|
285
|
+
*/
|
|
286
|
+
onLogout?: (payload: LogoutTokenPayload, request: NextRequest) => void | Promise<void>;
|
|
287
|
+
/** Access Token Cookie 名称 */
|
|
288
|
+
accessTokenCookieName?: string;
|
|
289
|
+
/** Refresh Token Cookie 名称 */
|
|
290
|
+
refreshTokenCookieName?: string;
|
|
291
|
+
/** ID Token Cookie 名称,默认 __Host-nihplod_sso_id */
|
|
292
|
+
idTokenCookieName?: string;
|
|
293
|
+
/**
|
|
294
|
+
* 本地 HTTP 开发模式(默认 false)。关闭 Cookie 的 Secure 属性并去除
|
|
295
|
+
* __Host-/__Secure- 前缀;必须与 middleware / callback / logout 的配置保持一致。
|
|
296
|
+
* 生产严禁启用——生产环境(NODE_ENV=production 且 ssoBaseUrl 为 https)下
|
|
297
|
+
* 该开关会被强制忽略并告警。
|
|
298
|
+
*/
|
|
299
|
+
insecureLocalDev?: boolean;
|
|
300
|
+
}
|
|
301
|
+
declare function createBackchannelLogoutRouteHandler(config: BackchannelLogoutRouteConfig): (request: NextRequest) => Promise<NextResponse<unknown>>;
|
|
302
|
+
|
|
227
303
|
/**
|
|
228
304
|
* Next.js 集成默认 Cookie 名称
|
|
229
305
|
*
|
|
@@ -238,6 +314,7 @@ declare const DEFAULT_ACCESS_TOKEN_COOKIE_NAME = "__Host-nihplod_sso_at";
|
|
|
238
314
|
declare const DEFAULT_REFRESH_TOKEN_COOKIE_NAME = "__Host-nihplod_sso_rt";
|
|
239
315
|
declare const DEFAULT_ID_TOKEN_COOKIE_NAME = "__Host-nihplod_sso_id";
|
|
240
316
|
declare const DEFAULT_STATE_COOKIE_NAME = "__Host-nihplod_sso_state";
|
|
317
|
+
declare const DEFAULT_NONCE_COOKIE_NAME = "__Host-nihplod_sso_nonce";
|
|
241
318
|
declare const DEFAULT_RETURN_COOKIE_NAME = "__Host-nihplod_sso_return";
|
|
242
319
|
declare const DEFAULT_VERIFIER_COOKIE_NAME = "__Secure-nihplod_sso_verifier";
|
|
243
320
|
declare const DEFAULT_LOGOUT_STATE_COOKIE_NAME = "__Host-nihplod_sso_logout_state";
|
|
@@ -264,4 +341,4 @@ declare function getSecureCookieOptions(maxAge?: number, path?: string, secure?:
|
|
|
264
341
|
maxAge?: number;
|
|
265
342
|
};
|
|
266
343
|
|
|
267
|
-
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 };
|
|
344
|
+
export { type BackchannelLogoutRouteConfig, 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, createBackchannelLogoutRouteHandler, createCallbackRouteHandler, createLogoutRouteHandler, createSsoMiddleware, getHostCookieOptions, getSecureCookieOptions, toInsecureCookieName };
|
package/dist/next/index.js
CHANGED
|
@@ -23,10 +23,12 @@ __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,
|
|
29
30
|
DEFAULT_VERIFIER_COOKIE_NAME: () => DEFAULT_VERIFIER_COOKIE_NAME,
|
|
31
|
+
createBackchannelLogoutRouteHandler: () => createBackchannelLogoutRouteHandler,
|
|
30
32
|
createCallbackRouteHandler: () => createCallbackRouteHandler,
|
|
31
33
|
createLogoutRouteHandler: () => createLogoutRouteHandler,
|
|
32
34
|
createSsoMiddleware: () => createSsoMiddleware,
|
|
@@ -44,6 +46,7 @@ var DEFAULT_ACCESS_TOKEN_COOKIE_NAME = "__Host-nihplod_sso_at";
|
|
|
44
46
|
var DEFAULT_REFRESH_TOKEN_COOKIE_NAME = "__Host-nihplod_sso_rt";
|
|
45
47
|
var DEFAULT_ID_TOKEN_COOKIE_NAME = "__Host-nihplod_sso_id";
|
|
46
48
|
var DEFAULT_STATE_COOKIE_NAME = "__Host-nihplod_sso_state";
|
|
49
|
+
var DEFAULT_NONCE_COOKIE_NAME = "__Host-nihplod_sso_nonce";
|
|
47
50
|
var DEFAULT_RETURN_COOKIE_NAME = "__Host-nihplod_sso_return";
|
|
48
51
|
var DEFAULT_VERIFIER_COOKIE_NAME = "__Secure-nihplod_sso_verifier";
|
|
49
52
|
var DEFAULT_LOGOUT_STATE_COOKIE_NAME = "__Host-nihplod_sso_logout_state";
|
|
@@ -199,21 +202,20 @@ function createSsoMiddleware(config) {
|
|
|
199
202
|
const secureCookies = !insecureLocalDev;
|
|
200
203
|
const accessTokenCookieName = insecureLocalDev ? toInsecureCookieName(config.accessTokenCookieName ?? DEFAULT_ACCESS_TOKEN_COOKIE_NAME) : config.accessTokenCookieName ?? DEFAULT_ACCESS_TOKEN_COOKIE_NAME;
|
|
201
204
|
const stateCookieName = insecureLocalDev ? toInsecureCookieName(config.stateCookieName ?? DEFAULT_STATE_COOKIE_NAME) : config.stateCookieName ?? DEFAULT_STATE_COOKIE_NAME;
|
|
205
|
+
const nonceCookieName = insecureLocalDev ? toInsecureCookieName(config.nonceCookieName ?? DEFAULT_NONCE_COOKIE_NAME) : config.nonceCookieName ?? DEFAULT_NONCE_COOKIE_NAME;
|
|
202
206
|
const returnUrlCookieName = insecureLocalDev ? toInsecureCookieName(config.returnUrlCookieName ?? DEFAULT_RETURN_COOKIE_NAME) : config.returnUrlCookieName ?? DEFAULT_RETURN_COOKIE_NAME;
|
|
203
207
|
const verifierCookieName = insecureLocalDev ? toInsecureCookieName(config.verifierCookieName ?? DEFAULT_VERIFIER_COOKIE_NAME) : config.verifierCookieName ?? DEFAULT_VERIFIER_COOKIE_NAME;
|
|
204
208
|
const normalizedBase = ssoBaseUrl.replace(/\/+$/, "");
|
|
205
209
|
const normalizedServerBase = (config.serverBaseUrl ?? ssoBaseUrl).replace(/\/+$/, "");
|
|
206
|
-
if (
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
);
|
|
216
|
-
}
|
|
210
|
+
if (!validateSsoCookie) {
|
|
211
|
+
console.warn(
|
|
212
|
+
"[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"
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
if (!clientSecret) {
|
|
216
|
+
console.warn(
|
|
217
|
+
"[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"
|
|
218
|
+
);
|
|
217
219
|
}
|
|
218
220
|
return async function ssoMiddleware(request) {
|
|
219
221
|
const { pathname } = request.nextUrl;
|
|
@@ -256,6 +258,7 @@ function createSsoMiddleware(config) {
|
|
|
256
258
|
}
|
|
257
259
|
}
|
|
258
260
|
const state = generateRandomString(32);
|
|
261
|
+
const nonce = generateRandomString(43);
|
|
259
262
|
const verifier = generateRandomString(64);
|
|
260
263
|
const challenge = await computeCodeChallenge(verifier);
|
|
261
264
|
const authorizeParams = new URLSearchParams();
|
|
@@ -264,12 +267,14 @@ function createSsoMiddleware(config) {
|
|
|
264
267
|
authorizeParams.set("redirect_uri", redirectUri);
|
|
265
268
|
authorizeParams.set("scope", scopes);
|
|
266
269
|
authorizeParams.set("state", state);
|
|
270
|
+
authorizeParams.set("nonce", nonce);
|
|
267
271
|
authorizeParams.set("code_challenge", challenge);
|
|
268
272
|
authorizeParams.set("code_challenge_method", "S256");
|
|
269
273
|
const loginUrl = new URL("/api/oauth/authorize", normalizedBase);
|
|
270
274
|
loginUrl.search = authorizeParams.toString();
|
|
271
275
|
const response = import_server.NextResponse.redirect(loginUrl);
|
|
272
276
|
response.cookies.set(stateCookieName, state, getHostCookieOptions(600, secureCookies));
|
|
277
|
+
response.cookies.set(nonceCookieName, nonce, getHostCookieOptions(600, secureCookies));
|
|
273
278
|
response.cookies.set(verifierCookieName, verifier, getSecureCookieOptions(600, callbackPath, secureCookies));
|
|
274
279
|
const safeReturnUrl = (request.nextUrl.pathname + request.nextUrl.search).slice(0, 2048);
|
|
275
280
|
response.cookies.set(returnUrlCookieName, safeReturnUrl, getHostCookieOptions(600, secureCookies));
|
|
@@ -358,18 +363,21 @@ function normalizeIssuer(url) {
|
|
|
358
363
|
var cachedJwks = null;
|
|
359
364
|
var cachedDiscovery = null;
|
|
360
365
|
var JWKS_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
366
|
+
var FETCH_TIMEOUT_MS = 1e4;
|
|
361
367
|
async function fetchDiscoveryDoc(baseUrl) {
|
|
362
368
|
const now = Date.now();
|
|
363
369
|
if (cachedDiscovery && cachedDiscovery.baseUrl === baseUrl && now - cachedDiscovery.fetchedAt < JWKS_CACHE_TTL_MS) {
|
|
364
370
|
return cachedDiscovery.doc;
|
|
365
371
|
}
|
|
366
372
|
try {
|
|
367
|
-
const res = await fetch(`${baseUrl}/api/oauth/.well-known/openid-configuration
|
|
368
|
-
|
|
373
|
+
const res = await fetch(`${baseUrl}/api/oauth/.well-known/openid-configuration`, {
|
|
374
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
375
|
+
});
|
|
376
|
+
if (!res.ok) return null;
|
|
377
|
+
const doc = await res.json();
|
|
369
378
|
cachedDiscovery = { baseUrl, doc, fetchedAt: now };
|
|
370
379
|
return doc;
|
|
371
380
|
} catch {
|
|
372
|
-
cachedDiscovery = { baseUrl, doc: null, fetchedAt: now };
|
|
373
381
|
return null;
|
|
374
382
|
}
|
|
375
383
|
}
|
|
@@ -381,7 +389,10 @@ async function fetchJwks(baseUrl, options = {}) {
|
|
|
381
389
|
const discovery = await fetchDiscoveryDoc(baseUrl);
|
|
382
390
|
const jwksUri = discovery?.jwks_uri || `${baseUrl}/api/oauth/jwks`;
|
|
383
391
|
try {
|
|
384
|
-
const res = await fetch(jwksUri,
|
|
392
|
+
const res = await fetch(jwksUri, {
|
|
393
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
394
|
+
...options.forceRefresh ? { cache: "no-cache" } : {}
|
|
395
|
+
});
|
|
385
396
|
if (!res.ok) return null;
|
|
386
397
|
const jwks = await res.json();
|
|
387
398
|
cachedJwks = { baseUrl, jwks, fetchedAt: now };
|
|
@@ -428,7 +439,7 @@ async function computeAtHash(accessToken) {
|
|
|
428
439
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
429
440
|
}
|
|
430
441
|
async function validateIdToken(idToken, accessToken, expectedIssuer, expectedClientId, options = {}) {
|
|
431
|
-
const { rejectHs256WhenRs256Available = true } = options;
|
|
442
|
+
const { rejectHs256WhenRs256Available = true, expectedNonce } = options;
|
|
432
443
|
const header = decodeJwtHeader(idToken);
|
|
433
444
|
if (!header) {
|
|
434
445
|
throw new SsoError("id_token_invalid", "ID Token \u683C\u5F0F\u9519\u8BEF");
|
|
@@ -518,6 +529,12 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
|
|
|
518
529
|
throw new SsoError("id_token_at_hash_mismatch", "ID Token at_hash \u4E0D\u5339\u914D");
|
|
519
530
|
}
|
|
520
531
|
}
|
|
532
|
+
if (expectedNonce !== void 0) {
|
|
533
|
+
const tokenNonce = typeof payload.nonce === "string" ? payload.nonce : "";
|
|
534
|
+
if (!tokenNonce || !timingSafeEqualString(expectedNonce, tokenNonce)) {
|
|
535
|
+
throw new SsoError("id_token_nonce_mismatch", "ID Token nonce \u4E0D\u5339\u914D");
|
|
536
|
+
}
|
|
537
|
+
}
|
|
521
538
|
return { sub: payload.sub };
|
|
522
539
|
}
|
|
523
540
|
|
|
@@ -538,11 +555,19 @@ function createCallbackRouteHandler(config) {
|
|
|
538
555
|
const refreshTokenCookieName = pickName(config.refreshTokenCookieName, DEFAULT_REFRESH_TOKEN_COOKIE_NAME);
|
|
539
556
|
const idTokenCookieName = pickName(config.idTokenCookieName, DEFAULT_ID_TOKEN_COOKIE_NAME);
|
|
540
557
|
const stateCookieName = pickName(config.stateCookieName, DEFAULT_STATE_COOKIE_NAME);
|
|
558
|
+
const nonceCookieName = pickName(config.nonceCookieName, DEFAULT_NONCE_COOKIE_NAME);
|
|
541
559
|
const returnUrlCookieName = pickName(config.returnUrlCookieName, DEFAULT_RETURN_COOKIE_NAME);
|
|
542
560
|
const verifierCookieName = pickName(config.verifierCookieName, DEFAULT_VERIFIER_COOKIE_NAME);
|
|
543
561
|
const normalizedBase = ssoBaseUrl.replace(/\/+$/, "");
|
|
544
562
|
const normalizedServerBase = (config.serverBaseUrl ?? ssoBaseUrl).replace(/\/+$/, "");
|
|
545
563
|
return async function GET(request) {
|
|
564
|
+
const response = await handleCallback(request);
|
|
565
|
+
if (response.status >= 400) {
|
|
566
|
+
response.cookies.set(nonceCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
567
|
+
}
|
|
568
|
+
return response;
|
|
569
|
+
};
|
|
570
|
+
async function handleCallback(request) {
|
|
546
571
|
const { searchParams } = request.nextUrl;
|
|
547
572
|
const error = searchParams.get("error");
|
|
548
573
|
if (error) {
|
|
@@ -573,7 +598,7 @@ function createCallbackRouteHandler(config) {
|
|
|
573
598
|
{ status: 400 }
|
|
574
599
|
);
|
|
575
600
|
}
|
|
576
|
-
if (returnedState
|
|
601
|
+
if (!timingSafeEqualString(returnedState ?? "", savedState)) {
|
|
577
602
|
return import_server2.NextResponse.json(
|
|
578
603
|
{
|
|
579
604
|
error: "invalid_request",
|
|
@@ -582,6 +607,7 @@ function createCallbackRouteHandler(config) {
|
|
|
582
607
|
{ status: 400 }
|
|
583
608
|
);
|
|
584
609
|
}
|
|
610
|
+
const expectedNonce = request.cookies.get(nonceCookieName)?.value;
|
|
585
611
|
const verifier = request.cookies.get(verifierCookieName)?.value;
|
|
586
612
|
if (!verifier) {
|
|
587
613
|
return import_server2.NextResponse.json(
|
|
@@ -663,7 +689,8 @@ function createCallbackRouteHandler(config) {
|
|
|
663
689
|
tokenData.id_token,
|
|
664
690
|
tokenData.access_token,
|
|
665
691
|
normalizedBase,
|
|
666
|
-
clientId
|
|
692
|
+
clientId,
|
|
693
|
+
{ expectedNonce }
|
|
667
694
|
);
|
|
668
695
|
} catch (err) {
|
|
669
696
|
return import_server2.NextResponse.json(
|
|
@@ -707,11 +734,12 @@ function createCallbackRouteHandler(config) {
|
|
|
707
734
|
}
|
|
708
735
|
}
|
|
709
736
|
response.cookies.set(stateCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
737
|
+
response.cookies.set(nonceCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
710
738
|
response.cookies.set(returnUrlCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
711
739
|
response.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, "/", secureCookies));
|
|
712
740
|
response.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, request.nextUrl.pathname, secureCookies));
|
|
713
741
|
return response;
|
|
714
|
-
}
|
|
742
|
+
}
|
|
715
743
|
}
|
|
716
744
|
|
|
717
745
|
// src/next/logout.ts
|
|
@@ -719,7 +747,7 @@ var import_server3 = require("next/server");
|
|
|
719
747
|
|
|
720
748
|
// src/core/discovery.ts
|
|
721
749
|
var DISCOVERY_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
722
|
-
var
|
|
750
|
+
var FETCH_TIMEOUT_MS2 = 5e3;
|
|
723
751
|
var cache = /* @__PURE__ */ new Map();
|
|
724
752
|
var inflight = /* @__PURE__ */ new Map();
|
|
725
753
|
function fetchDiscoveryCached(baseUrl) {
|
|
@@ -731,7 +759,7 @@ function fetchDiscoveryCached(baseUrl) {
|
|
|
731
759
|
if (existing) return existing;
|
|
732
760
|
const promise = (async () => {
|
|
733
761
|
const controller = new AbortController();
|
|
734
|
-
const timeoutId = setTimeout(() => controller.abort(),
|
|
762
|
+
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS2);
|
|
735
763
|
try {
|
|
736
764
|
const res = await fetch(`${baseUrl}/api/oauth/.well-known/openid-configuration`, {
|
|
737
765
|
signal: controller.signal
|
|
@@ -752,6 +780,26 @@ function fetchDiscoveryCached(baseUrl) {
|
|
|
752
780
|
}
|
|
753
781
|
|
|
754
782
|
// src/next/logout.ts
|
|
783
|
+
function buildLogoutConfirmHtml() {
|
|
784
|
+
return `<!doctype html>
|
|
785
|
+
<html lang="zh-CN">
|
|
786
|
+
<head>
|
|
787
|
+
<meta charset="utf-8">
|
|
788
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
789
|
+
<title>\u786E\u8BA4\u9000\u51FA\u767B\u5F55</title>
|
|
790
|
+
</head>
|
|
791
|
+
<body style="display:flex;align-items:center;justify-content:center;min-height:100vh;font-family:system-ui,sans-serif;">
|
|
792
|
+
<form method="post" style="text-align:center;">
|
|
793
|
+
<p>\u786E\u5B9A\u8981\u9000\u51FA\u767B\u5F55\u5417\uFF1F</p>
|
|
794
|
+
<label style="display:flex;align-items:center;justify-content:center;gap:6px;margin:12px 0;font-size:14px;color:#374151;cursor:pointer;">
|
|
795
|
+
<input type="checkbox" name="global" value="1">\u540C\u65F6\u9000\u51FA\u6240\u6709 NIHPLOD \u5E73\u53F0
|
|
796
|
+
</label>
|
|
797
|
+
<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>
|
|
798
|
+
<p><a href="/" style="color:#2563eb;text-decoration:underline;">\u53D6\u6D88\u5E76\u8FD4\u56DE\u9996\u9875</a></p>
|
|
799
|
+
</form>
|
|
800
|
+
</body>
|
|
801
|
+
</html>`;
|
|
802
|
+
}
|
|
755
803
|
function generateRandomString2(length) {
|
|
756
804
|
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
757
805
|
const maxValid = Math.floor(256 / chars.length) * chars.length;
|
|
@@ -773,10 +821,18 @@ function createLogoutRouteHandler(config) {
|
|
|
773
821
|
redirectUri,
|
|
774
822
|
clientSecret,
|
|
775
823
|
postLogoutRedirectUri = new URL(redirectUri).origin + "/",
|
|
776
|
-
redirectToSso = true,
|
|
777
824
|
callbackPath = "/api/auth/callback",
|
|
778
825
|
insecureLocalDev: insecureLocalDevOpt = false
|
|
779
826
|
} = config;
|
|
827
|
+
let defaultScope = config.defaultScope ?? "local";
|
|
828
|
+
if (config.redirectToSso !== void 0) {
|
|
829
|
+
console.warn(
|
|
830
|
+
'[SSO SDK] redirectToSso \u5DF2\u5F03\u7528\uFF0C\u8BF7\u6539\u7528 defaultScope\uFF08redirectToSso: true \u2192 defaultScope: "global"\uFF0Cfalse \u2192 "local"\uFF09\u3002\u6CE8\u610F\uFF1A\u9ED8\u8BA4\u9000\u51FA\u8303\u56F4\u5DF2\u53D8\u66F4\u4E3A "local"\uFF08\u4EC5\u9000\u51FA\u672C\u7AD9\uFF0C\u4E0D\u8DF3\u8F6C SSO \u4E2D\u5FC3\uFF09\u3002'
|
|
831
|
+
);
|
|
832
|
+
if (config.defaultScope === void 0) {
|
|
833
|
+
defaultScope = config.redirectToSso ? "global" : "local";
|
|
834
|
+
}
|
|
835
|
+
}
|
|
780
836
|
const insecureLocalDev = resolveInsecureLocalDev(insecureLocalDevOpt, ssoBaseUrl);
|
|
781
837
|
const secureCookies = !insecureLocalDev;
|
|
782
838
|
const pickName = (explicit, fallback) => insecureLocalDev ? toInsecureCookieName(explicit ?? fallback) : explicit ?? fallback;
|
|
@@ -784,6 +840,7 @@ function createLogoutRouteHandler(config) {
|
|
|
784
840
|
const refreshTokenCookieName = pickName(config.refreshTokenCookieName, DEFAULT_REFRESH_TOKEN_COOKIE_NAME);
|
|
785
841
|
const idTokenCookieName = pickName(config.idTokenCookieName, DEFAULT_ID_TOKEN_COOKIE_NAME);
|
|
786
842
|
const stateCookieName = pickName(config.stateCookieName, DEFAULT_STATE_COOKIE_NAME);
|
|
843
|
+
const nonceCookieName = pickName(config.nonceCookieName, DEFAULT_NONCE_COOKIE_NAME);
|
|
787
844
|
const returnUrlCookieName = pickName(config.returnUrlCookieName, DEFAULT_RETURN_COOKIE_NAME);
|
|
788
845
|
const verifierCookieName = pickName(config.verifierCookieName, DEFAULT_VERIFIER_COOKIE_NAME);
|
|
789
846
|
const logoutStateCookieName = pickName(config.logoutStateCookieName, DEFAULT_LOGOUT_STATE_COOKIE_NAME);
|
|
@@ -804,6 +861,18 @@ function createLogoutRouteHandler(config) {
|
|
|
804
861
|
res.cookies.set(logoutStateCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
805
862
|
return res;
|
|
806
863
|
}
|
|
864
|
+
if (request.method === "GET") {
|
|
865
|
+
return new import_server3.NextResponse(buildLogoutConfirmHtml(), {
|
|
866
|
+
status: 200,
|
|
867
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
let formGlobal = null;
|
|
871
|
+
try {
|
|
872
|
+
formGlobal = new URLSearchParams(await request.text()).get("global");
|
|
873
|
+
} catch {
|
|
874
|
+
}
|
|
875
|
+
const effectiveScope = formGlobal !== null ? formGlobal === "1" || formGlobal === "true" ? "global" : "local" : defaultScope;
|
|
807
876
|
const refreshToken = request.cookies.get(refreshTokenCookieName)?.value;
|
|
808
877
|
const idTokenHint = request.cookies.get(idTokenCookieName)?.value;
|
|
809
878
|
if (refreshToken) {
|
|
@@ -832,12 +901,13 @@ function createLogoutRouteHandler(config) {
|
|
|
832
901
|
res.cookies.set(refreshTokenCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
833
902
|
res.cookies.set(idTokenCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
834
903
|
res.cookies.set(stateCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
904
|
+
res.cookies.set(nonceCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
835
905
|
res.cookies.set(returnUrlCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
836
906
|
res.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, "/", secureCookies));
|
|
837
907
|
res.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, callbackPath, secureCookies));
|
|
838
908
|
return res;
|
|
839
909
|
};
|
|
840
|
-
if (
|
|
910
|
+
if (effectiveScope === "global") {
|
|
841
911
|
const discovery = await fetchDiscoveryCached(normalizedServerBase);
|
|
842
912
|
const endSessionEndpoint = discovery?.end_session_endpoint || `${normalizedBase}/api/oauth/end-session`;
|
|
843
913
|
const logoutUrl = new URL(endSessionEndpoint);
|
|
@@ -860,15 +930,209 @@ function createLogoutRouteHandler(config) {
|
|
|
860
930
|
);
|
|
861
931
|
};
|
|
862
932
|
}
|
|
933
|
+
|
|
934
|
+
// src/next/backchannel-logout.ts
|
|
935
|
+
var import_server4 = require("next/server");
|
|
936
|
+
|
|
937
|
+
// src/core/logout-token.ts
|
|
938
|
+
var BACKCHANNEL_LOGOUT_EVENT = "http://schemas.openid.net/event/backchannel-logout";
|
|
939
|
+
var JTI_CACHE_CAPACITY = 1e3;
|
|
940
|
+
var seenJti = /* @__PURE__ */ new Map();
|
|
941
|
+
function recordJti(jti, expiresAtMs) {
|
|
942
|
+
const now = Date.now();
|
|
943
|
+
const existing = seenJti.get(jti);
|
|
944
|
+
if (existing !== void 0 && existing > now) return false;
|
|
945
|
+
for (const [key, exp] of seenJti) {
|
|
946
|
+
if (exp <= now) seenJti.delete(key);
|
|
947
|
+
}
|
|
948
|
+
while (seenJti.size >= JTI_CACHE_CAPACITY) {
|
|
949
|
+
const oldest = seenJti.keys().next().value;
|
|
950
|
+
if (oldest === void 0) break;
|
|
951
|
+
seenJti.delete(oldest);
|
|
952
|
+
}
|
|
953
|
+
seenJti.set(jti, expiresAtMs);
|
|
954
|
+
return true;
|
|
955
|
+
}
|
|
956
|
+
async function verifyLogoutToken(logoutToken, ssoBaseUrl, clientId) {
|
|
957
|
+
const baseUrl = ssoBaseUrl.replace(/\/+$/, "");
|
|
958
|
+
const header = decodeJwtHeader(logoutToken);
|
|
959
|
+
if (!header) {
|
|
960
|
+
throw new SsoError("logout_token_invalid", "Logout Token \u683C\u5F0F\u9519\u8BEF");
|
|
961
|
+
}
|
|
962
|
+
if (header.alg !== "RS256") {
|
|
963
|
+
throw new SsoError(
|
|
964
|
+
"logout_token_unsupported_alg",
|
|
965
|
+
`\u4E0D\u652F\u6301\u7684 Logout Token \u7B7E\u540D\u7B97\u6CD5: ${String(header.alg)}`
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
const discovery = await fetchDiscoveryDoc(baseUrl);
|
|
969
|
+
const normalizedIssuer = (discovery?.issuer || baseUrl).replace(/\/+$/, "");
|
|
970
|
+
const jwks = await fetchJwks(baseUrl);
|
|
971
|
+
if (!jwks) {
|
|
972
|
+
throw new SsoError(
|
|
973
|
+
"logout_token_invalid_signature",
|
|
974
|
+
"\u65E0\u6CD5\u83B7\u53D6 JWKS \u9A8C\u8BC1 Logout Token \u7B7E\u540D"
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
const kid = typeof header.kid === "string" ? header.kid : void 0;
|
|
978
|
+
const matchCandidates = (set) => set.keys.filter(
|
|
979
|
+
(k) => k.kty === "RSA" && k.alg === "RS256" && k.use === "sig" && (kid ? k.kid === kid : true)
|
|
980
|
+
);
|
|
981
|
+
const verifyAny = async (keys) => {
|
|
982
|
+
for (const key of keys) {
|
|
983
|
+
if (await verifyRs256Signature(logoutToken, key)) return true;
|
|
984
|
+
}
|
|
985
|
+
return false;
|
|
986
|
+
};
|
|
987
|
+
let candidates = matchCandidates(jwks);
|
|
988
|
+
let validSig = candidates.length > 0 ? await verifyAny(candidates) : false;
|
|
989
|
+
if (candidates.length === 0 || !validSig) {
|
|
990
|
+
const freshJwks = await fetchJwks(baseUrl, { forceRefresh: true });
|
|
991
|
+
if (freshJwks) {
|
|
992
|
+
candidates = matchCandidates(freshJwks);
|
|
993
|
+
validSig = candidates.length > 0 ? await verifyAny(candidates) : false;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
if (candidates.length === 0) {
|
|
997
|
+
throw new SsoError(
|
|
998
|
+
"logout_token_invalid_signature",
|
|
999
|
+
"JWKS \u4E2D\u672A\u627E\u5230\u5339\u914D\u7684 RS256 \u516C\u94A5"
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
if (!validSig) {
|
|
1003
|
+
throw new SsoError(
|
|
1004
|
+
"logout_token_invalid_signature",
|
|
1005
|
+
"Logout Token \u7B7E\u540D\u9A8C\u8BC1\u5931\u8D25"
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
const payload = decodeJwtPayload(logoutToken);
|
|
1009
|
+
if (!payload) {
|
|
1010
|
+
throw new SsoError("logout_token_invalid", "Logout Token payload \u89E3\u6790\u5931\u8D25");
|
|
1011
|
+
}
|
|
1012
|
+
const tokenIssuer = typeof payload.iss === "string" ? payload.iss.replace(/\/+$/, "") : "";
|
|
1013
|
+
if (tokenIssuer !== normalizedIssuer) {
|
|
1014
|
+
throw new SsoError(
|
|
1015
|
+
"logout_token_issuer_mismatch",
|
|
1016
|
+
"Logout Token issuer \u4E0D\u5339\u914D"
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
const aud = payload.aud;
|
|
1020
|
+
const audList = Array.isArray(aud) ? aud : typeof aud === "string" ? [aud] : [];
|
|
1021
|
+
if (!audList.includes(clientId)) {
|
|
1022
|
+
throw new SsoError(
|
|
1023
|
+
"logout_token_audience_mismatch",
|
|
1024
|
+
"Logout Token audience \u4E0D\u5339\u914D"
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
if (typeof payload.exp !== "number") {
|
|
1028
|
+
throw new SsoError("logout_token_invalid", "Logout Token \u7F3A\u5C11 exp \u58F0\u660E");
|
|
1029
|
+
}
|
|
1030
|
+
if (Date.now() >= payload.exp * 1e3 + 6e4) {
|
|
1031
|
+
throw new SsoError("logout_token_expired", "Logout Token \u5DF2\u8FC7\u671F");
|
|
1032
|
+
}
|
|
1033
|
+
const events = payload.events;
|
|
1034
|
+
if (!events || typeof events !== "object" || !(BACKCHANNEL_LOGOUT_EVENT in events)) {
|
|
1035
|
+
throw new SsoError(
|
|
1036
|
+
"logout_token_invalid",
|
|
1037
|
+
"Logout Token \u7F3A\u5C11 backchannel-logout events \u58F0\u660E"
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
const sub = typeof payload.sub === "string" && payload.sub ? payload.sub : void 0;
|
|
1041
|
+
const sid = typeof payload.sid === "string" && payload.sid ? payload.sid : void 0;
|
|
1042
|
+
if (!sub && !sid) {
|
|
1043
|
+
throw new SsoError(
|
|
1044
|
+
"logout_token_invalid",
|
|
1045
|
+
"Logout Token \u5FC5\u987B\u5305\u542B sub \u6216 sid \u81F3\u5C11\u5176\u4E00"
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
const jti = typeof payload.jti === "string" ? payload.jti : "";
|
|
1049
|
+
if (!jti) {
|
|
1050
|
+
throw new SsoError("logout_token_invalid", "Logout Token \u7F3A\u5C11 jti \u58F0\u660E");
|
|
1051
|
+
}
|
|
1052
|
+
if (!recordJti(jti, payload.exp * 1e3 + 6e4)) {
|
|
1053
|
+
throw new SsoError("logout_token_replay", "Logout Token jti \u91CD\u653E");
|
|
1054
|
+
}
|
|
1055
|
+
return { sub, sid };
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// src/next/backchannel-logout.ts
|
|
1059
|
+
function createBackchannelLogoutRouteHandler(config) {
|
|
1060
|
+
const { clientId, ssoBaseUrl, onLogout } = config;
|
|
1061
|
+
const insecureLocalDev = resolveInsecureLocalDev(
|
|
1062
|
+
config.insecureLocalDev ?? false,
|
|
1063
|
+
ssoBaseUrl
|
|
1064
|
+
);
|
|
1065
|
+
const secureCookies = !insecureLocalDev;
|
|
1066
|
+
const pickName = (explicit, fallback) => insecureLocalDev ? toInsecureCookieName(explicit ?? fallback) : explicit ?? fallback;
|
|
1067
|
+
const accessTokenCookieName = pickName(
|
|
1068
|
+
config.accessTokenCookieName,
|
|
1069
|
+
DEFAULT_ACCESS_TOKEN_COOKIE_NAME
|
|
1070
|
+
);
|
|
1071
|
+
const refreshTokenCookieName = pickName(
|
|
1072
|
+
config.refreshTokenCookieName,
|
|
1073
|
+
DEFAULT_REFRESH_TOKEN_COOKIE_NAME
|
|
1074
|
+
);
|
|
1075
|
+
const idTokenCookieName = pickName(
|
|
1076
|
+
config.idTokenCookieName,
|
|
1077
|
+
DEFAULT_ID_TOKEN_COOKIE_NAME
|
|
1078
|
+
);
|
|
1079
|
+
return async function handler(request) {
|
|
1080
|
+
if (request.method !== "POST") {
|
|
1081
|
+
return import_server4.NextResponse.json(
|
|
1082
|
+
{ error: "method_not_allowed", error_description: "\u4EC5\u63A5\u53D7 POST \u8BF7\u6C42" },
|
|
1083
|
+
{ status: 405 }
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
const logoutToken = new URLSearchParams(await request.text()).get(
|
|
1087
|
+
"logout_token"
|
|
1088
|
+
);
|
|
1089
|
+
if (!logoutToken) {
|
|
1090
|
+
return import_server4.NextResponse.json(
|
|
1091
|
+
{ error: "invalid_request", error_description: "\u7F3A\u5C11 logout_token" },
|
|
1092
|
+
{ status: 400 }
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
let payload;
|
|
1096
|
+
try {
|
|
1097
|
+
payload = await verifyLogoutToken(logoutToken, ssoBaseUrl, clientId);
|
|
1098
|
+
} catch (err) {
|
|
1099
|
+
const code = err instanceof SsoError ? err.code : "logout_token_invalid";
|
|
1100
|
+
const description = err instanceof SsoError ? err.description : "Logout Token \u9A8C\u8BC1\u5931\u8D25";
|
|
1101
|
+
return import_server4.NextResponse.json(
|
|
1102
|
+
{ error: code, error_description: description },
|
|
1103
|
+
{ status: 400 }
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
try {
|
|
1107
|
+
await onLogout?.(payload, request);
|
|
1108
|
+
} catch (err) {
|
|
1109
|
+
console.error(
|
|
1110
|
+
"[SSO SDK] backchannel logout onLogout \u94A9\u5B50\u6267\u884C\u5931\u8D25:",
|
|
1111
|
+
err
|
|
1112
|
+
);
|
|
1113
|
+
return import_server4.NextResponse.json(
|
|
1114
|
+
{ error: "server_error", error_description: "onLogout \u5904\u7406\u5931\u8D25" },
|
|
1115
|
+
{ status: 500 }
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
const res = new import_server4.NextResponse(null, { status: 200 });
|
|
1119
|
+
res.cookies.set(accessTokenCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
1120
|
+
res.cookies.set(refreshTokenCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
1121
|
+
res.cookies.set(idTokenCookieName, "", getHostCookieOptions(0, secureCookies));
|
|
1122
|
+
return res;
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
863
1125
|
// Annotate the CommonJS export names for ESM import in node:
|
|
864
1126
|
0 && (module.exports = {
|
|
865
1127
|
DEFAULT_ACCESS_TOKEN_COOKIE_NAME,
|
|
866
1128
|
DEFAULT_ID_TOKEN_COOKIE_NAME,
|
|
867
1129
|
DEFAULT_LOGOUT_STATE_COOKIE_NAME,
|
|
1130
|
+
DEFAULT_NONCE_COOKIE_NAME,
|
|
868
1131
|
DEFAULT_REFRESH_TOKEN_COOKIE_NAME,
|
|
869
1132
|
DEFAULT_RETURN_COOKIE_NAME,
|
|
870
1133
|
DEFAULT_STATE_COOKIE_NAME,
|
|
871
1134
|
DEFAULT_VERIFIER_COOKIE_NAME,
|
|
1135
|
+
createBackchannelLogoutRouteHandler,
|
|
872
1136
|
createCallbackRouteHandler,
|
|
873
1137
|
createLogoutRouteHandler,
|
|
874
1138
|
createSsoMiddleware,
|