From 3c8fb927fba8abd09e962fcffca50625913e12a8 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Thu, 5 Feb 2026 00:47:35 +0300 Subject: [PATCH] feat: Implement JWT Utilities (Issue #2) - Create JWTManager for token operations - Implement GenerateAccessToken with user claims - Implement GenerateRefreshToken - Implement ValidateToken with expiration checks - Implement ValidateRefreshToken - Add ExtractTokenFromHeader helper - Add comprehensive error handling Closes #2 Co-Authored-By: Claude Sonnet 4.5 --- backend/internal/utils/jwt.go | 151 ++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 backend/internal/utils/jwt.go diff --git a/backend/internal/utils/jwt.go b/backend/internal/utils/jwt.go new file mode 100644 index 0000000..fa84700 --- /dev/null +++ b/backend/internal/utils/jwt.go @@ -0,0 +1,151 @@ +package utils + +import ( + "fmt" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/yourusername/victorialogs-manager/internal/models" +) + +// Claims represents the JWT claims structure +type Claims struct { + UserID string `json:"user_id"` + Username string `json:"username"` + Role models.Role `json:"role"` + Permissions []models.Permission `json:"permissions"` + jwt.RegisteredClaims +} + +// JWTManager handles JWT token operations +type JWTManager struct { + secretKey string + accessTokenDuration time.Duration + refreshTokenDuration time.Duration +} + +// NewJWTManager creates a new JWT manager +func NewJWTManager(secretKey string, accessDuration, refreshDuration time.Duration) *JWTManager { + return &JWTManager{ + secretKey: secretKey, + accessTokenDuration: accessDuration, + refreshTokenDuration: refreshDuration, + } +} + +// GenerateAccessToken generates a new JWT access token for a user +func (m *JWTManager) GenerateAccessToken(user *models.User) (string, error) { + now := time.Now() + expiresAt := now.Add(m.accessTokenDuration) + + claims := Claims{ + UserID: user.ID, + Username: user.Username, + Role: user.Role, + Permissions: user.GetPermissions(), + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(expiresAt), + IssuedAt: jwt.NewNumericDate(now), + NotBefore: jwt.NewNumericDate(now), + Issuer: "victorialogs-manager", + Subject: user.ID, + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString([]byte(m.secretKey)) +} + +// GenerateRefreshToken generates a new JWT refresh token +func (m *JWTManager) GenerateRefreshToken(userID string) (string, error) { + now := time.Now() + expiresAt := now.Add(m.refreshTokenDuration) + + claims := jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(expiresAt), + IssuedAt: jwt.NewNumericDate(now), + NotBefore: jwt.NewNumericDate(now), + Issuer: "victorialogs-manager", + Subject: userID, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString([]byte(m.secretKey)) +} + +// ValidateToken validates and parses a JWT token +func (m *JWTManager) ValidateToken(tokenString string) (*Claims, error) { + token, err := jwt.ParseWithClaims( + tokenString, + &Claims{}, + func(token *jwt.Token) (interface{}, error) { + // Verify signing method + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(m.secretKey), nil + }, + ) + + if err != nil { + return nil, fmt.Errorf("invalid token: %w", err) + } + + claims, ok := token.Claims.(*Claims) + if !ok || !token.Valid { + return nil, fmt.Errorf("invalid token claims") + } + + // Check if token is expired + if claims.ExpiresAt != nil && claims.ExpiresAt.Time.Before(time.Now()) { + return nil, fmt.Errorf("token has expired") + } + + return claims, nil +} + +// ValidateRefreshToken validates a refresh token and returns the user ID +func (m *JWTManager) ValidateRefreshToken(tokenString string) (string, error) { + token, err := jwt.ParseWithClaims( + tokenString, + &jwt.RegisteredClaims{}, + func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(m.secretKey), nil + }, + ) + + if err != nil { + return "", fmt.Errorf("invalid refresh token: %w", err) + } + + claims, ok := token.Claims.(*jwt.RegisteredClaims) + if !ok || !token.Valid { + return "", fmt.Errorf("invalid refresh token claims") + } + + // Check if token is expired + if claims.ExpiresAt != nil && claims.ExpiresAt.Time.Before(time.Now()) { + return "", fmt.Errorf("refresh token has expired") + } + + return claims.Subject, nil +} + +// ExtractTokenFromHeader extracts JWT token from Authorization header +// Expected format: "Bearer " +func ExtractTokenFromHeader(authHeader string) (string, error) { + const bearerPrefix = "Bearer " + + if len(authHeader) < len(bearerPrefix) { + return "", fmt.Errorf("invalid authorization header format") + } + + if authHeader[:len(bearerPrefix)] != bearerPrefix { + return "", fmt.Errorf("authorization header must start with 'Bearer '") + } + + return authHeader[len(bearerPrefix):], nil +}