refactor(auth): extract shared token validation into auth package

Move JWT parsing (GetUserIDFromToken) and API token validation
(ValidateAPITokenString) into pkg/modules/auth so both HTTP middleware
and WebSocket auth use the same logic. This ensures consistent token
validity checks including expiry and user status (disabled/locked).

The HTTP API token middleware now delegates to the shared function,
removing duplicated lookup/expiry logic.
This commit is contained in:
kolaente 2026-04-02 18:18:20 +02:00 committed by kolaente
parent 0139e9a2ab
commit 55ea5bd966
2 changed files with 59 additions and 8 deletions

View File

@ -192,6 +192,62 @@ func GetAuthFromClaims(c *echo.Context) (a web.Auth, err error) {
return nil, echo.NewHTTPError(http.StatusBadRequest, "Invalid JWT token.")
}
// ValidateAPITokenString looks up an API token by its raw string, checks expiry,
// and returns the token and its owner. This is the shared validation logic used
// by both the HTTP middleware and WebSocket auth.
func ValidateAPITokenString(tokenString string) (*models.APIToken, *user.User, error) {
s := db.NewSession()
defer s.Close()
token, err := models.GetTokenFromTokenString(s, tokenString)
if err != nil {
return nil, nil, err
}
if time.Now().After(token.ExpiresAt) {
return nil, nil, fmt.Errorf("API token %d expired on %s", token.ID, token.ExpiresAt.String())
}
u, err := user.GetUserByID(s, token.OwnerID)
if err != nil {
if user.IsErrUserStatusError(err) {
return nil, nil, fmt.Errorf("API token %d owner account is disabled or locked", token.ID)
}
return nil, nil, err
}
return token, u, nil
}
// GetUserIDFromToken parses a raw JWT token string and returns the user ID.
// Only regular user tokens are accepted (not link shares).
// Returns 0 and an error if the token is invalid.
func GetUserIDFromToken(tokenString string) (int64, error) {
token, err := jwt.Parse(tokenString, func(_ *jwt.Token) (any, error) {
return []byte(config.ServiceSecret.GetString()), nil
})
if err != nil {
return 0, err
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok || !token.Valid {
return 0, jwt.ErrTokenInvalidClaims
}
typ, ok := claims["type"].(float64)
if !ok || int(typ) != AuthTypeUser {
return 0, jwt.ErrTokenInvalidClaims
}
userIDFloat, ok := claims["id"].(float64)
if !ok {
return 0, jwt.ErrTokenInvalidClaims
}
return int64(userIDFloat), nil
}
func CreateUserWithRandomUsername(s *xorm.Session, uu *user.User) (u *user.User, err error) {
// Check if we actually have a preferred username and generate a random one right away if we don't
for {

View File

@ -21,9 +21,9 @@ import (
"strings"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/modules/auth"
"code.vikunja.io/api/pkg/web"
echojwt "github.com/labstack/echo-jwt/v5"
@ -72,14 +72,9 @@ func SetupTokenMiddleware() echo.MiddlewareFunc {
}
func checkAPITokenAndPutItInContext(tokenHeaderValue string, c *echo.Context) error {
s := db.NewSession()
defer s.Close()
token, u, err := models.ValidateTokenAndGetOwner(s, strings.TrimPrefix(tokenHeaderValue, "Bearer "))
token, u, err := auth.ValidateAPITokenString(strings.TrimPrefix(tokenHeaderValue, "Bearer "))
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Internal Server Error").Wrap(err)
}
if token == nil || u == nil {
log.Debugf("[auth] API token validation failed: %v", err)
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}