Skip to content

Commit 75b0eb5

Browse files
longsizhuoclaude
andcommitted
feat(sso): 授权页禁止被嵌进 iframe;认 HoloCard staging 的 client
- /:locale/sso/* 加 Content-Security-Policy: frame-ancestors 'none' 和 X-Frame-Options: DENY, 防点击劫持骗已登录的访客点「继续」(实测只作用于授权页,其它页面响应头不变) - client 表加 holocard-staging(后端 INV-010 里单独的 client,secret 和回跳地址都和线上分开) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011CHv6oQ6cYWQvdBV6FFRhF
1 parent 9745511 commit 75b0eb5

3 files changed

Lines changed: 27 additions & 1 deletion

File tree

‎lib/sso.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
export const SSO_CLIENTS: Record<string, string> = { holocard: "HoloCard" };
1+
// 授权页上显示的站点名。和后端 sso.clients 表对应(后端还会再校验 client 和回跳地址)
2+
export const SSO_CLIENTS: Record<string, string> = {
3+
holocard: "HoloCard",
4+
"holocard-staging": "HoloCard(staging)",
5+
};
26

37
export const SSO_PENDING_KEY = "ih:sso:pending";
48
const PENDING_TTL_MS = 10 * 60 * 1000;

‎next.config.mjs‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,19 @@ const config = {
353353
// / NEXT_LOCALE 协商(307,单跳到 /zh 或 /en)才是对的。
354354
];
355355
},
356+
async headers() {
357+
return [
358+
{
359+
// IH 通行证授权页不许被别的网站嵌进 iframe:防点击劫持骗已登录的访客点「继续」,
360+
// 替攻击者发起的登录签出授权码(见 lib/sso.ts、后端 SECURITY.md 的 INV-010)
361+
source: "/:locale/sso/:path*",
362+
headers: [
363+
{ key: "Content-Security-Policy", value: "frame-ancestors 'none'" },
364+
{ key: "X-Frame-Options", value: "DENY" },
365+
],
366+
},
367+
];
368+
},
356369
async rewrites() {
357370
const backendUrl = process.env.BACKEND_URL ?? "http://localhost:8080";
358371
return [

‎tests/sso.test.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ describe("parseSsoRequest", () => {
3535
expect(parseSsoRequest({ ...VALID, extra: "x" })).toEqual(VALID);
3636
});
3737

38+
it("HoloCard 的 staging 是单独的 client,也认", () => {
39+
const staging = {
40+
...VALID,
41+
client_id: "holocard-staging",
42+
redirect_uri: "https://holocard.staging.longsizhuo.com/auth/callback",
43+
};
44+
expect(parseSsoRequest(staging)).toEqual(staging);
45+
});
46+
3847
it.each(Object.keys(VALID))("缺 %s 或为空 → 拒绝", (key) => {
3948
expect(parseSsoRequest({ ...VALID, [key]: undefined })).toBeNull();
4049
expect(parseSsoRequest({ ...VALID, [key]: "" })).toBeNull();

0 commit comments

Comments
 (0)