11"use server" ;
22
33import type { ApiResponse } from "@repo/shared-types/api/common" ;
4+ import { createHash } from "crypto" ;
45import { cookies , headers } from "next/headers" ;
56
67import { logAuditEvent } from "@/lib/server/audit" ;
78import { verifyToken } from "@/lib/server/captcha" ;
9+ import { getClientIP , getClientUserAgent } from "@/lib/server/get-client-info" ;
810import {
911 type AccessTokenPayload ,
1012 jwtTokenSign ,
@@ -27,6 +29,39 @@ import {
2729
2830const 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