jwt

package
v1.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 27, 2025 License: MIT Imports: 4 Imported by: 0

README

jwt Package

JWT token generation and validation for LaResto authentication.

Features

  • Access & Refresh Tokens: Dual-token authentication
  • Custom Claims: User ID, email, role support
  • Token Validation: Signature verification with expiration checking
  • Token Refresh: Generate new tokens using refresh token
  • Configurable TTL: Separate lifetimes for access/refresh tokens
  • Extract User ID: Get user ID without full validation (for logging)

Installation

import "github.com/LaRestoOU/laresto-go-common/pkg/jwt"

Quick Start

Create Token Manager
cfg := jwt.Config{
    AccessSecret:  os.Getenv("JWT_ACCESS_SECRET"),
    RefreshSecret: os.Getenv("JWT_REFRESH_SECRET"),
    Issuer:        "laresto-auth-service",
    AccessTTL:     15 * time.Minute,
    RefreshTTL:    7 * 24 * time.Hour,
}

tm, err := jwt.NewTokenManager(cfg)
if err != nil {
    log.Fatal("Failed to create token manager", err)
}
Generate Tokens
tokens, err := tm.GenerateTokenPair("user-123", "user@example.com", "admin")
if err != nil {
    return err
}

// Returns:
// tokens.AccessToken  - Use for API requests (15 min)
// tokens.RefreshToken - Store in client (7 days)
// tokens.ExpiresAt    - When access token expires
Validate Access Token
claims, err := tm.ValidateAccessToken(accessToken)
if err != nil {
    // Token invalid, expired, or tampered
    return errors.ErrUnauthorized
}

// Access user info
userID := claims.UserID
email := claims.Email
role := claims.Role
Refresh Tokens
newTokens, err := tm.RefreshTokens(refreshToken)
if err != nil {
    // Refresh token invalid or expired
    return errors.ErrUnauthorized
}

// Returns new access + refresh tokens

Configuration

type Config struct {
    // AccessSecret is the secret for access tokens (required)
    AccessSecret string

    // RefreshSecret is the secret for refresh tokens (required)
    RefreshSecret string

    // Issuer identifies the token issuer (required)
    Issuer string

    // AccessTTL is access token lifetime (default: 15 minutes)
    AccessTTL time.Duration

    // RefreshTTL is refresh token lifetime (default: 7 days)
    RefreshTTL time.Duration
}

Recommended values:

  • AccessTTL: 15 minutes (short-lived for security)
  • RefreshTTL: 7 days (convenient but revocable)
  • Secrets: 32+ character random strings

Token Structure

Claims
type Claims struct {
    UserID string `json:"user_id"`
    Email  string `json:"email"`
    Role   string `json:"role"`
    
    // Standard JWT claims
    jwt.RegisteredClaims
}
Token Payload (decoded)
{
  "user_id": "123",
  "email": "user@example.com",
  "role": "admin",
  "iss": "laresto-auth-service",
  "sub": "123",
  "exp": 1672531200,
  "iat": 1672530300,
  "nbf": 1672530300
}

Usage Patterns

Login Flow
func (s *AuthService) Login(ctx context.Context, email, password string) (*jwt.TokenPair, error) {
    // Verify credentials
    user, err := s.db.FindUserByEmail(ctx, email)
    if err != nil {
        return nil, errors.ErrUnauthorized
    }
    
    if !bcrypt.CompareHashAndPassword(user.PasswordHash, []byte(password)) {
        return nil, errors.ErrUnauthorized
    }
    
    // Generate tokens
    tokens, err := s.tokenManager.GenerateTokenPair(
        user.ID,
        user.Email,
        user.Role,
    )
    if err != nil {
        return nil, err
    }
    
    // Store refresh token in Redis
    s.cache.Set(ctx, 
        fmt.Sprintf("refresh:%s", user.ID), 
        tokens.RefreshToken,
        7*24*time.Hour,
    )
    
    return tokens, nil
}
Protected Endpoint
func (h *Handler) GetProfile(c *gin.Context) {
    // Get token from header
    authHeader := c.GetHeader("Authorization")
    if authHeader == "" {
        c.JSON(401, gin.H{"error": "Missing authorization header"})
        return
    }
    
    // Extract token (format: "Bearer <token>")
    parts := strings.Split(authHeader, " ")
    if len(parts) != 2 || parts[0] != "Bearer" {
        c.JSON(401, gin.H{"error": "Invalid authorization format"})
        return
    }
    
    // Validate token
    claims, err := h.tokenManager.ValidateAccessToken(parts[1])
    if err != nil {
        c.JSON(401, gin.H{"error": "Invalid or expired token"})
        return
    }
    
    // Use claims
    user, err := h.userService.GetUser(c.Request.Context(), claims.UserID)
    if err != nil {
        c.JSON(500, gin.H{"error": "Internal error"})
        return
    }
    
    c.JSON(200, user)
}
Token Refresh Flow
func (s *AuthService) RefreshTokens(ctx context.Context, refreshToken string) (*jwt.TokenPair, error) {
    // Validate refresh token
    claims, err := s.tokenManager.ValidateRefreshToken(refreshToken)
    if err != nil {
        return nil, errors.ErrUnauthorized
    }
    
    // Check if refresh token exists in Redis (not revoked)
    storedToken, err := s.cache.Get(ctx, 
        fmt.Sprintf("refresh:%s", claims.UserID),
        &tokenData,
    )
    if err != nil || storedToken != refreshToken {
        return nil, errors.ErrUnauthorized
    }
    
    // Generate new tokens
    newTokens, err := s.tokenManager.RefreshTokens(refreshToken)
    if err != nil {
        return nil, err
    }
    
    // Update Redis with new refresh token
    s.cache.Set(ctx,
        fmt.Sprintf("refresh:%s", claims.UserID),
        newTokens.RefreshToken,
        7*24*time.Hour,
    )
    
    return newTokens, nil
}
Logout (Token Revocation)
func (s *AuthService) Logout(ctx context.Context, userID string) error {
    // Delete refresh token from Redis
    return s.cache.Delete(ctx, fmt.Sprintf("refresh:%s", userID))
}
Middleware
func AuthMiddleware(tm *jwt.TokenManager) gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"})
            return
        }
        
        parts := strings.Split(authHeader, " ")
        if len(parts) != 2 || parts[0] != "Bearer" {
            c.AbortWithStatusJSON(401, gin.H{"error": "Invalid token format"})
            return
        }
        
        claims, err := tm.ValidateAccessToken(parts[1])
        if err != nil {
            c.AbortWithStatusJSON(401, gin.H{"error": "Invalid token"})
            return
        }
        
        // Store claims in context
        c.Set("user_id", claims.UserID)
        c.Set("email", claims.Email)
        c.Set("role", claims.Role)
        
        c.Next()
    }
}

// Usage
router.Use(AuthMiddleware(tokenManager))
router.GET("/profile", GetProfile)
Role-Based Authorization
func RequireRole(role string) gin.HandlerFunc {
    return func(c *gin.Context) {
        userRole, exists := c.Get("role")
        if !exists {
            c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"})
            return
        }
        
        if userRole != role {
            c.AbortWithStatusJSON(403, gin.H{"error": "Forbidden"})
            return
        }
        
        c.Next()
    }
}

// Usage
router.GET("/admin/users", RequireRole("admin"), ListUsers)

Token Lifecycle

1. Login
   ↓
   Generate access (15m) + refresh (7d) tokens
   ↓
   Store refresh token in Redis
   ↓
   Return both tokens to client

2. API Request
   ↓
   Client sends access token in header
   ↓
   Server validates access token
   ↓
   Process request

3. Access Token Expires (after 15m)
   ↓
   Client sends refresh token to /refresh
   ↓
   Server validates refresh token
   ↓
   Generate new access + refresh tokens
   ↓
   Update Redis
   ↓
   Return new tokens

4. Logout
   ↓
   Delete refresh token from Redis
   ↓
   Access token expires naturally (15m)

Security Best Practices

DO ✅
// Use environment variables for secrets
cfg := jwt.Config{
    AccessSecret:  os.Getenv("JWT_ACCESS_SECRET"),
    RefreshSecret: os.Getenv("JWT_REFRESH_SECRET"),
}

// Use different secrets for access and refresh
AccessSecret:  "access-secret-XYZ..."
RefreshSecret: "refresh-secret-ABC..." // Different!

// Store refresh tokens server-side (Redis)
cache.Set(ctx, fmt.Sprintf("refresh:%s", userID), refreshToken, ttl)

// Use short access token TTL
AccessTTL: 15 * time.Minute

// Validate tokens on every protected request
claims, err := tm.ValidateAccessToken(token)

// Use HTTPS for token transmission
// Tokens sent over HTTP can be intercepted!
DON'T ❌
// Don't hardcode secrets
AccessSecret: "my-secret" // BAD!

// Don't use same secret for both
AccessSecret:  "same-secret"
RefreshSecret: "same-secret" // BAD!

// Don't store tokens in localStorage (XSS vulnerable)
// Use httpOnly cookies or secure storage

// Don't use long access token TTL
AccessTTL: 24 * time.Hour // TOO LONG!

// Don't skip validation
// claims := extractClaims(token) // BAD! No validation

// Don't trust expired tokens
// Validation fails automatically

// Don't expose secrets in logs
log.Info("Token", "secret", secret) // BAD!

Token Storage (Client-Side)

Mobile App (iOS):

// Access token: Memory (15 min TTL)
var accessToken: String?

// Refresh token: Keychain (7 day TTL)
KeychainWrapper.standard.set(refreshToken, forKey: "refresh_token")

Web App:

// Access token: Memory or sessionStorage
sessionStorage.setItem('access_token', token);

// Refresh token: httpOnly cookie (recommended)
// Set by server: Set-Cookie: refresh_token=...; HttpOnly; Secure; SameSite=Strict

Testing

func TestAuth(t *testing.T) {
    cfg := jwt.Config{
        AccessSecret:  "test-access-secret",
        RefreshSecret: "test-refresh-secret",
        Issuer:        "test-issuer",
        AccessTTL:     15 * time.Minute,
        RefreshTTL:    7 * 24 * time.Hour,
    }
    
    tm, _ := jwt.NewTokenManager(cfg)
    
    // Generate tokens
    tokens, err := tm.GenerateTokenPair("user-123", "user@example.com", "user")
    require.NoError(t, err)
    
    // Validate access token
    claims, err := tm.ValidateAccessToken(tokens.AccessToken)
    require.NoError(t, err)
    assert.Equal(t, "user-123", claims.UserID)
    
    // Refresh tokens
    newTokens, err := tm.RefreshTokens(tokens.RefreshToken)
    require.NoError(t, err)
    assert.NotEqual(t, tokens.AccessToken, newTokens.AccessToken)
}

Troubleshooting

"Token is expired"

  • Access token expired (15 min) → Use refresh token
  • Refresh token expired (7 days) → User must login again

"Invalid signature"

  • Token was tampered with
  • Using wrong secret to validate
  • Token from different environment (dev vs prod)

"Token used before issued"

  • Clock skew between servers
  • Token not yet valid (nbf claim)

License

MIT License - see LICENSE file for details

Documentation

Overview

Package jwt provides JWT token generation and validation for LaResto authentication.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Claims

type Claims struct {
	UserID string `json:"user_id"`
	Email  string `json:"email"`
	Role   string `json:"role"`
	jwt.RegisteredClaims
}

Claims represents JWT claims.

type Config

type Config struct {
	// AccessSecret is the secret key for access tokens
	AccessSecret string

	// RefreshSecret is the secret key for refresh tokens
	RefreshSecret string

	// Issuer is the token issuer (e.g., "laresto-auth-service")
	Issuer string

	// AccessTTL is the access token lifetime (default: 15 minutes)
	AccessTTL time.Duration

	// RefreshTTL is the refresh token lifetime (default: 7 days)
	RefreshTTL time.Duration
}

Config holds JWT configuration.

type TokenManager

type TokenManager struct {
	// contains filtered or unexported fields
}

TokenManager handles JWT token operations.

func NewTokenManager

func NewTokenManager(cfg Config) (*TokenManager, error)

NewTokenManager creates a new token manager.

func (*TokenManager) ExtractUserID

func (tm *TokenManager) ExtractUserID(tokenString string) (string, error)

ExtractUserID extracts the user ID from a token without full validation. Use this for logging/metrics, not authorization.

func (*TokenManager) GenerateTokenPair

func (tm *TokenManager) GenerateTokenPair(userID, email, role string) (*TokenPair, error)

GenerateTokenPair generates access and refresh tokens.

func (*TokenManager) RefreshTokens

func (tm *TokenManager) RefreshTokens(refreshToken string) (*TokenPair, error)

RefreshTokens generates a new token pair using a valid refresh token.

func (*TokenManager) ValidateAccessToken

func (tm *TokenManager) ValidateAccessToken(tokenString string) (*Claims, error)

ValidateAccessToken validates an access token and returns claims.

func (*TokenManager) ValidateRefreshToken

func (tm *TokenManager) ValidateRefreshToken(tokenString string) (*Claims, error)

ValidateRefreshToken validates a refresh token and returns claims.

type TokenPair

type TokenPair struct {
	AccessToken  string    `json:"access_token"`
	RefreshToken string    `json:"refresh_token"`
	ExpiresAt    time.Time `json:"expires_at"`
}

TokenPair holds access and refresh tokens.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL