Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
3d29f7c2
Commit
3d29f7c2
authored
Apr 22, 2026
by
IanShaw027
Browse files
fix(auth): invalidate access tokens on session revoke
parent
01a991f5
Changes
6
Hide whitespace changes
Inline
Side-by-side
backend/internal/handler/auth_handler.go
View file @
3d29f7c2
...
@@ -719,7 +719,7 @@ func (h *AuthHandler) RevokeAllSessions(c *gin.Context) {
...
@@ -719,7 +719,7 @@ func (h *AuthHandler) RevokeAllSessions(c *gin.Context) {
return
return
}
}
if
err
:=
h
.
authService
.
RevokeAllUser
Sessio
ns
(
c
.
Request
.
Context
(),
subject
.
UserID
);
err
!=
nil
{
if
err
:=
h
.
authService
.
RevokeAllUser
Toke
ns
(
c
.
Request
.
Context
(),
subject
.
UserID
);
err
!=
nil
{
slog
.
Error
(
"failed to revoke all sessions"
,
"user_id"
,
subject
.
UserID
,
"error"
,
err
)
slog
.
Error
(
"failed to revoke all sessions"
,
"user_id"
,
subject
.
UserID
,
"error"
,
err
)
response
.
InternalError
(
c
,
"Failed to revoke sessions"
)
response
.
InternalError
(
c
,
"Failed to revoke sessions"
)
return
return
...
...
backend/internal/handler/auth_session_revocation_test.go
0 → 100644
View file @
3d29f7c2
//go:build unit
package
handler
import
(
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/Wei-Shaw/sub2api/internal/config"
middleware2
"github.com/Wei-Shaw/sub2api/internal/server/middleware"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
func
TestAuthHandlerRevokeAllSessionsInvalidatesAccessTokens
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
repo
:=
&
userHandlerRepoStub
{
user
:
&
service
.
User
{
ID
:
29
,
Email
:
"session@example.com"
,
Username
:
"session-user"
,
Role
:
service
.
RoleUser
,
Status
:
service
.
StatusActive
,
TokenVersion
:
7
,
},
}
refreshTokenCache
:=
&
userHandlerRefreshTokenCacheStub
{}
cfg
:=
&
config
.
Config
{
JWT
:
config
.
JWTConfig
{
Secret
:
"test-secret"
,
ExpireHour
:
1
,
},
}
authService
:=
service
.
NewAuthService
(
nil
,
repo
,
nil
,
refreshTokenCache
,
cfg
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
)
handler
:=
&
AuthHandler
{
authService
:
authService
}
recorder
:=
httptest
.
NewRecorder
()
c
,
_
:=
gin
.
CreateTestContext
(
recorder
)
c
.
Request
=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/api/v1/auth/revoke-all-sessions"
,
nil
)
c
.
Set
(
string
(
middleware2
.
ContextKeyUser
),
middleware2
.
AuthSubject
{
UserID
:
29
})
handler
.
RevokeAllSessions
(
c
)
require
.
Equal
(
t
,
http
.
StatusOK
,
recorder
.
Code
)
require
.
Equal
(
t
,
[]
int64
{
29
},
refreshTokenCache
.
revokedUserIDs
)
require
.
Equal
(
t
,
int64
(
8
),
repo
.
user
.
TokenVersion
)
var
resp
struct
{
Code
int
`json:"code"`
Data
struct
{
Message
string
`json:"message"`
}
`json:"data"`
}
require
.
NoError
(
t
,
json
.
Unmarshal
(
recorder
.
Body
.
Bytes
(),
&
resp
))
require
.
Equal
(
t
,
0
,
resp
.
Code
)
require
.
Equal
(
t
,
"All sessions have been revoked. Please log in again."
,
resp
.
Data
.
Message
)
}
backend/internal/handler/auth_wechat_oauth_test.go
View file @
3d29f7c2
...
@@ -1346,18 +1346,6 @@ func newWeChatOAuthTestHandlerWithSettings(t *testing.T, invitationEnabled bool,
...
@@ -1346,18 +1346,6 @@ func newWeChatOAuthTestHandlerWithSettings(t *testing.T, invitationEnabled bool,
},
client
},
client
}
}
func
assertOAuthRedirectError
(
t
*
testing
.
T
,
location
string
,
errorCode
string
,
errorMessage
string
)
{
t
.
Helper
()
parsed
,
err
:=
url
.
Parse
(
location
)
require
.
NoError
(
t
,
err
)
fragment
,
err
:=
url
.
ParseQuery
(
parsed
.
Fragment
)
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
errorCode
,
fragment
.
Get
(
"error"
))
require
.
Equal
(
t
,
errorMessage
,
fragment
.
Get
(
"error_message"
))
}
type
wechatOAuthSettingRepoStub
struct
{
type
wechatOAuthSettingRepoStub
struct
{
values
map
[
string
]
string
values
map
[
string
]
string
}
}
...
...
backend/internal/handler/user_handler.go
View file @
3d29f7c2
...
@@ -259,7 +259,7 @@ func (h *UserHandler) UnbindIdentity(c *gin.Context) {
...
@@ -259,7 +259,7 @@ func (h *UserHandler) UnbindIdentity(c *gin.Context) {
return
return
}
}
if
h
.
authService
!=
nil
{
if
h
.
authService
!=
nil
{
if
err
:=
h
.
authService
.
RevokeAllUser
Sessio
ns
(
c
.
Request
.
Context
(),
subject
.
UserID
);
err
!=
nil
{
if
err
:=
h
.
authService
.
RevokeAllUser
Toke
ns
(
c
.
Request
.
Context
(),
subject
.
UserID
);
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
response
.
ErrorFrom
(
c
,
err
)
return
return
}
}
...
...
backend/internal/handler/user_handler_test.go
View file @
3d29f7c2
...
@@ -593,11 +593,12 @@ func TestUserHandlerUnbindIdentityRevokesAllUserSessionsWhenAuthServiceConfigure
...
@@ -593,11 +593,12 @@ func TestUserHandlerUnbindIdentityRevokesAllUserSessionsWhenAuthServiceConfigure
repo
:=
&
userHandlerRepoStub
{
repo
:=
&
userHandlerRepoStub
{
user
:
&
service
.
User
{
user
:
&
service
.
User
{
ID
:
23
,
ID
:
23
,
Email
:
"identity@example.com"
,
Email
:
"identity@example.com"
,
Username
:
"identity-user"
,
Username
:
"identity-user"
,
Role
:
service
.
RoleUser
,
Role
:
service
.
RoleUser
,
Status
:
service
.
StatusActive
,
Status
:
service
.
StatusActive
,
TokenVersion
:
4
,
},
},
identities
:
[]
service
.
UserAuthIdentityRecord
{
identities
:
[]
service
.
UserAuthIdentityRecord
{
{
{
...
@@ -632,6 +633,7 @@ func TestUserHandlerUnbindIdentityRevokesAllUserSessionsWhenAuthServiceConfigure
...
@@ -632,6 +633,7 @@ func TestUserHandlerUnbindIdentityRevokesAllUserSessionsWhenAuthServiceConfigure
require
.
Equal
(
t
,
http
.
StatusOK
,
recorder
.
Code
)
require
.
Equal
(
t
,
http
.
StatusOK
,
recorder
.
Code
)
require
.
Equal
(
t
,
[]
int64
{
23
},
refreshTokenCache
.
revokedUserIDs
)
require
.
Equal
(
t
,
[]
int64
{
23
},
refreshTokenCache
.
revokedUserIDs
)
require
.
Equal
(
t
,
int64
(
5
),
repo
.
user
.
TokenVersion
)
}
}
func
TestUserHandlerBindEmailIdentityRejectsWrongCurrentPasswordForBoundEmail
(
t
*
testing
.
T
)
{
func
TestUserHandlerBindEmailIdentityRejectsWrongCurrentPasswordForBoundEmail
(
t
*
testing
.
T
)
{
...
...
backend/internal/service/auth_service.go
View file @
3d29f7c2
...
@@ -1467,6 +1467,26 @@ func (s *AuthService) RevokeAllUserSessions(ctx context.Context, userID int64) e
...
@@ -1467,6 +1467,26 @@ func (s *AuthService) RevokeAllUserSessions(ctx context.Context, userID int64) e
return
s
.
refreshTokenCache
.
DeleteUserRefreshTokens
(
ctx
,
userID
)
return
s
.
refreshTokenCache
.
DeleteUserRefreshTokens
(
ctx
,
userID
)
}
}
// RevokeAllUserTokens invalidates both stateless access tokens and refresh sessions.
// Access/refresh token verification both depend on TokenVersion, so bumping it provides
// immediate revocation even if refresh-token cache cleanup later fails.
func
(
s
*
AuthService
)
RevokeAllUserTokens
(
ctx
context
.
Context
,
userID
int64
)
error
{
user
,
err
:=
s
.
userRepo
.
GetByID
(
ctx
,
userID
)
if
err
!=
nil
{
return
fmt
.
Errorf
(
"get user: %w"
,
err
)
}
user
.
TokenVersion
++
if
err
:=
s
.
userRepo
.
Update
(
ctx
,
user
);
err
!=
nil
{
return
fmt
.
Errorf
(
"update user: %w"
,
err
)
}
if
err
:=
s
.
RevokeAllUserSessions
(
ctx
,
userID
);
err
!=
nil
{
logger
.
LegacyPrintf
(
"service.auth"
,
"[Auth] Failed to revoke refresh sessions after token invalidation for user %d: %v"
,
userID
,
err
)
}
return
nil
}
// hashToken 计算Token的SHA256哈希
// hashToken 计算Token的SHA256哈希
func
hashToken
(
token
string
)
string
{
func
hashToken
(
token
string
)
string
{
hash
:=
sha256
.
Sum256
([]
byte
(
token
))
hash
:=
sha256
.
Sum256
([]
byte
(
token
))
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment