fix(apiserver): revoke the token on logout (bug-1946)

Cause: Logout only deleted tokens/<user>/token, a file that token
validation (ValidateHandler / GetUsernameFromToken) never reads, so a
logged-out token stayed valid until its expiry (24 h by default).

Fix: an in-memory revocation set (controllers/token_revocation.go) keyed
by the token's jti (generateJWT now sets a random uuid jti, so two logons
in the same second give different tokens) or, for tokens without a jti,
by SHA-256 of the token; each entry is kept until the token's expiry and
expired entries are purged on every revocation. Logout revokes the token;
the shared ParseTokenClaims (used by ValidateHandler, RequirePermission
and handlers via GetUsernameFromToken) rejects revoked tokens. A missing
token file no longer fails logout. Scope: per process, not persisted and
not shared between apiservers.

Tests: TestLogout_RevokesToken (middleware), TestTokenRevocations_*,
TestIntegration_PostLogoutTokenRejected (replaces the design-gap test).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-10-10 08:40:36 +00:00
parent 19a1a6f023
commit 4ce4f0b798
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
6 changed files with 256 additions and 34 deletions

View file

@ -3,7 +3,6 @@ package middleware
import (
"apiserver/common"
"apiserver/controllers"
"apiserver/models"
logger "apiserver/utils"
"bytes"
"encoding/json"
@ -13,7 +12,6 @@ import (
"strings"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v4"
"go.uber.org/zap"
)
@ -98,20 +96,14 @@ func validateHeader() gin.HandlerFunc {
}
}
// isValidToken verifies the token (signature, HS256 only, expiry, not
// revoked by logout) and its IP binding.
func isValidToken(tokenString string, IP string) (bool, error) {
token, err := jwt.ParseWithClaims(tokenString, &models.JWTClaims{},
controllers.JWTKeyFunc, controllers.JWTValidMethods())
claims, err := controllers.ParseTokenClaims(tokenString)
if err != nil {
return false, err
}
if claims, ok := token.Claims.(*models.JWTClaims); ok && token.Valid {
if claims.IP == IP {
return true, nil
}
}
return false, nil
return claims.IP == IP, nil
}
func validateRequestBody(ctx *gin.Context) (bool, error) {