Skip to content

Commit 35a462d

Browse files
CopilotRavelloH
andauthored
opti: 改进 reauth 安全性
* Initial plan * fix(auth): bind reauth token to client context Co-authored-by: RavelloH <68409330+RavelloH@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: RavelloH <68409330+RavelloH@users.noreply.github.com>
1 parent 0de6880 commit 35a462d

File tree

1 file changed

+62
-6
lines changed

1 file changed

+62
-6
lines changed

apps/web/src/actions/reauth.ts

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
"use server";
22

33
import type { ApiResponse } from "@repo/shared-types/api/common";
4+
import { createHash } from "crypto";
45
import { cookies, headers } from "next/headers";
56

67
import { logAuditEvent } from "@/lib/server/audit";
78
import { verifyToken } from "@/lib/server/captcha";
9+
import { getClientIP, getClientUserAgent } from "@/lib/server/get-client-info";
810
import {
911
type AccessTokenPayload,
1012
jwtTokenSign,
@@ -27,6 +29,39 @@ import {
2729

2830
const REAUTH_TOKEN_EXPIRY = 600; // 10 分钟
2931

32+
type ReauthTokenPayload = {
33+
uid: number;
34+
type?: string;
35+
exp: number;
36+
ipHash?: string;
37+
userAgentHash?: string;
38+
};
39+
40+
function hashTokenBindingValue(value: string): string | undefined {
41+
const normalizedValue = value.trim().toLowerCase();
42+
if (!normalizedValue || normalizedValue === "unknown") {
43+
return undefined;
44+
}
45+
return createHash("sha256")
46+
.update(normalizedValue)
47+
.digest("hex")
48+
.slice(0, 32);
49+
}
50+
51+
async function getReauthTokenBindings(): Promise<{
52+
ipHash?: string;
53+
userAgentHash?: string;
54+
}> {
55+
const [ip, userAgent] = await Promise.all([
56+
getClientIP(),
57+
getClientUserAgent(),
58+
]);
59+
return {
60+
ipHash: hashTokenBindingValue(ip),
61+
userAgentHash: hashTokenBindingValue(userAgent),
62+
};
63+
}
64+
3065
/**
3166
* 检查是否有有效的 REAUTH_TOKEN,并绑定当前登录用户
3267
*/
@@ -40,11 +75,7 @@ export async function checkReauthToken(expectedUid?: number): Promise<boolean> {
4075
}
4176

4277
// 验证 token 是否有效
43-
const decoded = jwtTokenVerify<{
44-
uid: number;
45-
type?: string;
46-
exp: number;
47-
}>(reauthToken);
78+
const decoded = jwtTokenVerify<ReauthTokenPayload>(reauthToken);
4879
if (!decoded) {
4980
return false;
5081
}
@@ -76,9 +107,25 @@ export async function checkReauthToken(expectedUid?: number): Promise<boolean> {
76107
const isMatched = decoded.uid === verifyUid;
77108
if (!isMatched) {
78109
cookieStore.delete("REAUTH_TOKEN");
110+
return false;
79111
}
80112

81-
return isMatched;
113+
if (decoded.ipHash || decoded.userAgentHash) {
114+
const currentBindings = await getReauthTokenBindings();
115+
if (decoded.ipHash && decoded.ipHash !== currentBindings.ipHash) {
116+
cookieStore.delete("REAUTH_TOKEN");
117+
return false;
118+
}
119+
if (
120+
decoded.userAgentHash &&
121+
decoded.userAgentHash !== currentBindings.userAgentHash
122+
) {
123+
cookieStore.delete("REAUTH_TOKEN");
124+
return false;
125+
}
126+
}
127+
128+
return true;
82129
} catch (error) {
83130
console.error("Check reauth token error:", error);
84131
return false;
@@ -319,13 +366,16 @@ export async function verifyPasswordForReauth({
319366

320367
// 如果没有启用 TOTP,直接生成 REAUTH_TOKEN
321368

369+
const bindings = await getReauthTokenBindings();
370+
322371
// 生成 REAUTH_TOKEN
323372
const expiredAtUnix = Math.floor(Date.now() / 1000) + REAUTH_TOKEN_EXPIRY;
324373
const reauthToken = jwtTokenSign({
325374
inner: {
326375
uid: user.uid,
327376
type: "reauth",
328377
exp: expiredAtUnix,
378+
...bindings,
329379
},
330380
expired: `${REAUTH_TOKEN_EXPIRY}s`,
331381
});
@@ -522,13 +572,16 @@ export async function verifyTotpForReauth({
522572
// 清除 TOTP Token
523573
cookieStore.delete("TOTP_TOKEN");
524574

575+
const bindings = await getReauthTokenBindings();
576+
525577
// 生成 REAUTH_TOKEN
526578
const expiredAtUnix = Math.floor(Date.now() / 1000) + REAUTH_TOKEN_EXPIRY;
527579
const reauthToken = jwtTokenSign({
528580
inner: {
529581
uid: user.uid,
530582
type: "reauth",
531583
exp: expiredAtUnix,
584+
...bindings,
532585
},
533586
expired: `${REAUTH_TOKEN_EXPIRY}s`,
534587
});
@@ -670,13 +723,16 @@ export async function verifySSOForReauth({
670723
}) as unknown as ApiResponse<null>;
671724
}
672725

726+
const bindings = await getReauthTokenBindings();
727+
673728
// 生成 REAUTH_TOKEN
674729
const expiredAtUnix = Math.floor(Date.now() / 1000) + REAUTH_TOKEN_EXPIRY;
675730
const reauthToken = jwtTokenSign({
676731
inner: {
677732
uid: user.uid,
678733
type: "reauth",
679734
exp: expiredAtUnix,
735+
...bindings,
680736
},
681737
expired: `${REAUTH_TOKEN_EXPIRY}s`,
682738
});

0 commit comments

Comments
 (0)