fix: implement token invalidation on password change

This commit is contained in:
Junming Chen
2025-12-29 17:18:17 -05:00
parent c01db6b180
commit 19d0ee130d
5 changed files with 32 additions and 6 deletions

View File

@@ -198,6 +198,7 @@ type userModel struct {
Concurrency int `gorm:"default:5;not null"`
Status string `gorm:"size:20;default:active;not null"`
AllowedGroups pq.Int64Array `gorm:"type:bigint[]"`
TokenVersion int64 `gorm:"default:0;not null"` // Incremented on password change
CreatedAt time.Time `gorm:"not null"`
UpdatedAt time.Time `gorm:"not null"`
DeletedAt gorm.DeletedAt `gorm:"index"`
@@ -221,6 +222,7 @@ func userModelToService(m *userModel) *service.User {
Concurrency: m.Concurrency,
Status: m.Status,
AllowedGroups: []int64(m.AllowedGroups),
TokenVersion: m.TokenVersion,
CreatedAt: m.CreatedAt,
UpdatedAt: m.UpdatedAt,
}
@@ -242,6 +244,7 @@ func userModelFromService(u *service.User) *userModel {
Concurrency: u.Concurrency,
Status: u.Status,
AllowedGroups: pq.Int64Array(u.AllowedGroups),
TokenVersion: u.TokenVersion,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
}
@@ -252,6 +255,7 @@ func applyUserModelToService(dst *service.User, src *userModel) {
return
}
dst.ID = src.ID
dst.TokenVersion = src.TokenVersion
dst.CreatedAt = src.CreatedAt
dst.UpdatedAt = src.UpdatedAt
}

View File

@@ -61,6 +61,13 @@ func jwtAuth(authService *service.AuthService, userService *service.UserService)
return
}
// Security: Validate TokenVersion to ensure token hasn't been invalidated
// This check ensures tokens issued before a password change are rejected
if claims.TokenVersion != user.TokenVersion {
AbortWithError(c, 401, "TOKEN_REVOKED", "Token has been revoked (password changed)")
return
}
c.Set(string(ContextKeyUser), AuthSubject{
UserID: user.ID,
Concurrency: user.Concurrency,

View File

@@ -20,6 +20,7 @@ var (
ErrEmailExists = infraerrors.Conflict("EMAIL_EXISTS", "email already exists")
ErrInvalidToken = infraerrors.Unauthorized("INVALID_TOKEN", "invalid token")
ErrTokenExpired = infraerrors.Unauthorized("TOKEN_EXPIRED", "token has expired")
ErrTokenRevoked = infraerrors.Unauthorized("TOKEN_REVOKED", "token has been revoked")
ErrEmailVerifyRequired = infraerrors.BadRequest("EMAIL_VERIFY_REQUIRED", "email verification is required")
ErrRegDisabled = infraerrors.Forbidden("REGISTRATION_DISABLED", "registration is currently disabled")
ErrServiceUnavailable = infraerrors.ServiceUnavailable("SERVICE_UNAVAILABLE", "service temporarily unavailable")
@@ -27,9 +28,10 @@ var (
// JWTClaims JWT载荷数据
type JWTClaims struct {
UserID int64 `json:"user_id"`
Email string `json:"email"`
Role string `json:"role"`
UserID int64 `json:"user_id"`
Email string `json:"email"`
Role string `json:"role"`
TokenVersion int64 `json:"token_version"` // Used to invalidate tokens on password change
jwt.RegisteredClaims
}
@@ -311,9 +313,10 @@ func (s *AuthService) GenerateToken(user *User) (string, error) {
expiresAt := now.Add(time.Duration(s.cfg.JWT.ExpireHour) * time.Hour)
claims := &JWTClaims{
UserID: user.ID,
Email: user.Email,
Role: user.Role,
UserID: user.ID,
Email: user.Email,
Role: user.Role,
TokenVersion: user.TokenVersion,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expiresAt),
IssuedAt: jwt.NewNumericDate(now),
@@ -368,6 +371,12 @@ func (s *AuthService) RefreshToken(ctx context.Context, oldTokenString string) (
return "", ErrUserNotActive
}
// Security: Check TokenVersion to prevent refreshing revoked tokens
// This ensures tokens issued before a password change cannot be refreshed
if claims.TokenVersion != user.TokenVersion {
return "", ErrTokenRevoked
}
// 生成新token
return s.GenerateToken(user)
}

View File

@@ -18,6 +18,7 @@ type User struct {
Concurrency int
Status string
AllowedGroups []int64
TokenVersion int64 // Incremented on password change to invalidate existing tokens
CreatedAt time.Time
UpdatedAt time.Time

View File

@@ -116,6 +116,7 @@ func (s *UserService) UpdateProfile(ctx context.Context, userID int64, req Updat
}
// ChangePassword 修改密码
// Security: Increments TokenVersion to invalidate all existing JWT tokens
func (s *UserService) ChangePassword(ctx context.Context, userID int64, req ChangePasswordRequest) error {
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
@@ -131,6 +132,10 @@ func (s *UserService) ChangePassword(ctx context.Context, userID int64, req Chan
return fmt.Errorf("set password: %w", err)
}
// Increment TokenVersion to invalidate all existing tokens
// This ensures that any tokens issued before the password change become invalid
user.TokenVersion++
if err := s.userRepo.Update(ctx, user); err != nil {
return fmt.Errorf("update user: %w", err)
}