package jwt import ( "errors" "time" "github.com/golang-jwt/jwt/v5" ) var ( ErrInvalidToken = errors.New("invalid token") ErrExpiredToken = errors.New("token has expired") ) type Claims struct { UserID int32 `json:"user_id"` UserType string `json:"user_type"` // "user" or "admin" Username string `json:"username,omitempty"` jwt.RegisteredClaims } type JWTManager struct { secretKey []byte accessTokenTTL time.Duration refreshTokenTTL time.Duration } func NewJWTManager(secret string, accessTokenTTL, refreshTokenTTL time.Duration) *JWTManager { return &JWTManager{ secretKey: []byte(secret), accessTokenTTL: accessTokenTTL, refreshTokenTTL: refreshTokenTTL, } } func (m *JWTManager) GenerateAccessToken(userID int32, userType string, username string) (string, error) { now := time.Now() claims := &Claims{ UserID: userID, UserType: userType, Username: username, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(now.Add(m.accessTokenTTL)), IssuedAt: jwt.NewNumericDate(now), NotBefore: jwt.NewNumericDate(now), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString(m.secretKey) } func (m *JWTManager) GenerateRefreshToken(userID int32, userType string) (string, error) { now := time.Now() claims := &Claims{ UserID: userID, UserType: userType, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(now.Add(m.refreshTokenTTL)), IssuedAt: jwt.NewNumericDate(now), NotBefore: jwt.NewNumericDate(now), Subject: "refresh", }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString(m.secretKey) } func (m *JWTManager) ValidateToken(tokenString string) (*Claims, error) { token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, ErrInvalidToken } return m.secretKey, nil }) if err != nil { if errors.Is(err, jwt.ErrTokenExpired) { return nil, ErrExpiredToken } return nil, ErrInvalidToken } claims, ok := token.Claims.(*Claims) if !ok || !token.Valid { return nil, ErrInvalidToken } return claims, nil } func (m *JWTManager) RefreshAccessToken(refreshToken string) (string, error) { claims, err := m.ValidateToken(refreshToken) if err != nil { return "", err } // Only refresh tokens with subject "refresh" can be used if claims.Subject != "refresh" { return "", ErrInvalidToken } return m.GenerateAccessToken(claims.UserID, claims.UserType, claims.Username) }