DEV Community

邱敬幃 Pardn Chiu
邱敬幃 Pardn Chiu

Posted on

JWT Auth (Golang)

cover

JWT Auth (Golang)

A JWT authentication package providing both Access Token and Refresh Token mechanisms, featuring fingerprint recognition, Redis storage, and automatic refresh functionality.

Node.js version can be found here

Three key features

  • Dual Token System: Access Token + Refresh ID, with automatic refresh
  • Device Fingerprinting: Generate unique fingerprints based on user agent, device ID, OS, and browser to prevent token abuse across different devices
  • Security Protection: Token revocation, version control, smart refresh, and concurrency protection with Redis lock mechanism

Flow

Click to show

flowchart TD
  Start([Request Start]) --> Auth{Has Access Token?}
  Auth -->|Yes| CheckRevoke[Check if Token is Revoked]
  Auth -->|No| HasRefresh{Has Refresh ID?}
  HasRefresh -->|No| Unauthorized[Return 401 Unauthorized]
  HasRefresh -->|Yes| ValidateRefresh[Validate Refresh ID]
  CheckRevoke --> IsRevoked{Token Revoked?}
  IsRevoked -->|Yes| Unauthorized
  IsRevoked -->|No| ParseToken[Parse Access Token]
  ParseToken --> TokenValid{Token Valid?}
  TokenValid -->|Yes| ValidateClaims[Validate Claims]
  TokenValid -->|No| IsExpired{Token Expired?}
  IsExpired -->|Yes| ParseExpiredToken[Parse Expired Token]
  IsExpired -->|No| InvalidToken[Return 400 Invalid Token]
  ParseExpiredToken --> ValidateExpiredClaims[Validate Expired Token Claims]
  ValidateExpiredClaims --> ExpiredClaimsValid{Refresh ID and Fingerprint Match?}
  ExpiredClaimsValid -->|No| InvalidClaims[Return 400 Invalid Claims]
  ExpiredClaimsValid -->|Yes| RefreshFlow[Enter Refresh Flow]
  ValidateClaims --> ClaimsValid{Claims Match?}
  ClaimsValid -->|No| InvalidClaims
  ClaimsValid -->|Yes| CheckJTI[Check JTI]
  CheckJTI --> JTIValid{JTI Valid?}
  JTIValid -->|No| Unauthorized
  JTIValid -->|Yes| Success[Return 200 Success]
  ValidateRefresh --> RefreshValid{Refresh ID Valid?}
  RefreshValid -->|No| Unauthorized
  RefreshValid -->|Yes| RefreshFlow
  RefreshFlow --> AcquireLock[Acquire Refresh Lock]
  AcquireLock --> LockSuccess{Lock Acquired?}
  LockSuccess -->|No| TooManyRequests[Return 429 Too Many Requests]
  LockSuccess -->|Yes| GetRefreshData[Get Refresh Data]
  GetRefreshData --> CheckTTL[Check TTL]
  CheckTTL --> NeedNewRefresh{Need New Refresh ID?}
  NeedNewRefresh -->|Yes| CreateNewRefresh[Create New Refresh ID]
  NeedNewRefresh -->|No| UpdateVersion[Update Version Number]
  CreateNewRefresh --> SetOldRefreshExpire[Set Old Refresh ID to Expire in 5 Seconds]
  SetOldRefreshExpire --> SetNewRefreshData[Set New Refresh Data]
  UpdateVersion --> SetNewRefreshData
  SetNewRefreshData --> CheckUserExists{User Exists Check}
  CheckUserExists -->|No| Unauthorized
  CheckUserExists -->|Yes| GenerateNewToken[Generate New Access Token]
  GenerateNewToken --> StoreJTI[Store New JTI]
  StoreJTI --> SetCookies[Set Cookies]
  SetCookies --> ReleaseLock[Release Lock]
  ReleaseLock --> RefreshSuccess[Return Refresh Success]
Enter fullscreen mode Exit fullscreen mode

Dependencies

How to use

Installation

go get github.com/pardnchiu/go-jwt-auth
Enter fullscreen mode Exit fullscreen mode

Initialization

package main

import (
  "log"
  "net/http"

  "github.com/gin-gonic/gin"
  jwtAuth "github.com/pardnchiu/go-jwt-auth"
)

func main() {
  // Minimal configuration - keys will be auto-generated
  config := jwtAuth.Config{
    Redis: jwtAuth.Redis{
      Host:     "localhost",
      Port:     6379,
      Password: "password",
      DB:       0,
    },
    CheckAuth: func(userData jwtAuth.Auth) (bool, error) {
      // Custom user validation logic
      return userData.ID != "", nil
    },
  }

  auth, err := jwtAuth.New(config)
  if err != nil {
    log.Fatal("Failed to initialize:", err)
  }
  defer auth.Close()

  r := gin.Default()

  // Login endpoint
  r.POST("/login", func(c *gin.Context) {
    // After validating login credentials...
    user := &jwtAuth.Auth{
      ID:    "user123",
      Name:  "John Doe",
      Email: "[email protected]",
      Scope: []string{"read", "write"},
    }

    result := auth.Create(c.Writer, c.Request, user)
    if !result.Success {
      c.JSON(result.StatusCode, gin.H{"error": result.Error})
      return
    }

    c.JSON(http.StatusOK, gin.H{
      "success": true,
      "token":   result.Token.Token,
      "user":    result.Data,
    })
  })

  // Protected routes
  protected := r.Group("/api")
  protected.Use(auth.GinMiddleware())
  {
    protected.GET("/profile", func(c *gin.Context) {
      user, _ := jwtAuth.GetAuthDataFromGinContext(c)
      c.JSON(http.StatusOK, gin.H{"user": user})
    })
  }

  // Logout endpoint
  r.POST("/logout", func(c *gin.Context) {
    result := auth.Revoke(c.Writer, c.Request)
    if !result.Success {
      c.JSON(result.StatusCode, gin.H{"error": result.Error})
      return
    }
    c.JSON(http.StatusOK, gin.H{"message": "Successfully logged out"})
  })

  r.Run(":8080")
}
Enter fullscreen mode Exit fullscreen mode

Configuration Details

type Config struct {
  Redis     Redis                    // Redis configuration (required)
  File      *File                    // File configuration for key management (optional)
  Log       *Log                     // Logging configuration (optional)
  Option    *Option                  // System parameters and token settings (optional)
  Cookie    *Cookie                  // Cookie security settings (optional)
  CheckAuth func(Auth) (bool, error) // User authentication validation function (optional)
}

type Redis struct {
  Host     string // Redis server host address (required)
  Port     int    // Redis server port number (required)
  Password string // Redis authentication password (optional, empty for no auth)
  DB       int    // Redis database index (required, typically 0-15)
}

type File struct {
  PrivateKeyPath string // Path to ECDSA private key file for JWT signing
  PublicKeyPath  string // Path to ECDSA public key file for JWT verification
}

type Log struct {
  Path      string // Log directory path (default: ./logs/jwtAuth)
  Stdout    bool   // Enable console output logging (default: false)
  MaxSize   int64  // Maximum log file size before rotation in bytes (default: 16MB)
  MaxBackup int    // Number of rotated log files to retain (default: 5)
  Type      string // Output format: "json" for slog standard, "text" for tree format (default: "text")
}

type Option struct {
  PrivateKey           string        // ECDSA private key content (auto-generated P-256 if not provided)
  PublicKey            string        // ECDSA public key content (auto-generated P-256 if not provided)
  AccessTokenExpires   time.Duration // Access token expiration duration (default: 15 minutes)
  RefreshIdExpires     time.Duration // Refresh ID expiration duration (default: 7 days)
  AccessTokenCookieKey string        // Access token cookie name (default: "access_token")
  RefreshIdCookieKey   string        // Refresh ID cookie name (default: "refresh_id")
  MaxVersion           int           // Maximum refresh token version count (default: 5)
  RefreshTTL           float64       // Refresh threshold as fraction of TTL (default: 0.5)
}

type Cookie struct {
  Domain   *string        // Cookie domain scope (nil for current domain)
  Path     *string        // Cookie path scope (default: "/")
  SameSite *http.SameSite // Cookie SameSite policy (default: Lax for CSRF protection)
  Secure   *bool          // Cookie secure flag for HTTPS only (default: false)
  HttpOnly *bool          // Cookie HttpOnly flag to prevent XSS (default: true)
}
Enter fullscreen mode Exit fullscreen mode

Supported Operations

Core Methods

// Create new authentication session
result := auth.Create(w, r, userData)

// Verify authentication status
result := auth.Verify(w, r)

// Revoke authentication (logout)
result := auth.Revoke(w, r)
Enter fullscreen mode Exit fullscreen mode

Middleware Usage

// Gin framework middleware
protected.Use(auth.GinMiddleware())

// Standard HTTP middleware
server := &http.Server{
  Handler: auth.HTTPMiddleware(handler),
}

// Get user data from context
user, exists := jwtAuth.GetAuthDataFromGinContext(c)
user, exists := jwtAuth.GetAuthDataFromHTTPRequest(r)
Enter fullscreen mode Exit fullscreen mode

Authentication Methods

// Multiple authentication methods supported:
// 1. Custom headers
r.Header.Set("X-Device-FP", fingerprint)
r.Header.Set("X-Refresh-ID", refreshID)
r.Header.Set("Authorization", "Bearer "+token)

// 2. Cookies (automatically managed)
// access_token, refresh_id cookies

// 3. Device fingerprinting (automatic)
// Based on user agent, device ID, OS, browser
Enter fullscreen mode Exit fullscreen mode

Core Features

Connection Management

  • New - Create new JWT auth instance
  auth, err := jwtAuth.New(config)
Enter fullscreen mode Exit fullscreen mode
  • Initialize Redis connection
  • Setup logging system
  • Auto-generate ECDSA keys if not provided
  • Validate configuration

    • Close - Close JWT auth instance
  err := auth.Close()
Enter fullscreen mode Exit fullscreen mode
  • Close Redis connection
  • Release system resources

Security Features

  • Device Fingerprinting - Generate unique fingerprints based on user agent, device ID, OS, browser, and device type
  fingerprint := auth.getFingerprint(w, r)
Enter fullscreen mode Exit fullscreen mode
  • Token Revocation - Add tokens to blacklist on logout
  result := auth.Revoke(w, r)
Enter fullscreen mode Exit fullscreen mode
  • Automatic Refresh - Smart token refresh based on expiration and version control
  // Automatically triggered during Verify() when needed
  result := auth.Verify(w, r)
Enter fullscreen mode Exit fullscreen mode

Authentication Flow

  • Create - Generate new authentication session
  result := auth.Create(w, r, userData)
Enter fullscreen mode Exit fullscreen mode
  • Generate access token and refresh ID
  • Set secure cookies
  • Store session data in Redis

    • Verify - Validate authentication status
  result := auth.Verify(w, r)
Enter fullscreen mode Exit fullscreen mode
  • Parse and validate JWT token
  • Check device fingerprint
  • Auto-refresh if needed
  • Return user data

    • Revoke - Terminate authentication session
  result := auth.Revoke(w, r)
Enter fullscreen mode Exit fullscreen mode
  • Clear cookies
  • Add token to blacklist
  • Update Redis records

Security Features

  • Device Fingerprinting: Generate unique fingerprints based on user agent, device ID, OS, browser, and device type with persistent tracking
  • Token Revocation: Add tokens to blacklist on logout
  • Automatic Expiration: Support TTL auto-cleanup for expired tokens
  • Version Control: Track refresh token versions to prevent replay attacks
  • Fingerprint Verification: Ensure tokens can only be used on the same device/browser
  • Auto Key Generation: Automatically generate secure ECDSA key pairs if not provided
  • Concurrency Protection: Redis lock mechanism prevents concurrent refresh conflicts

Error Handling

All methods return a JWTAuthResult structure:

type JWTAuthResult struct {
  StatusCode int          // HTTP status code
  Success    bool         // Whether operation succeeded
  Data       *Auth        // User data
  Token      *TokenResult // Token information
  Error      string       // Error message
  ErrorTag   string       // Error classification tag
}
Enter fullscreen mode Exit fullscreen mode

Error Tags

  • data_missing - Required data not provided
  • data_invalid - Invalid data format
  • unauthorized - Authentication failed
  • revoked - Token has been revoked
  • failed_to_update - Update operation failed
  • failed_to_create - Creation operation failed
  • failed_to_sign - Token signing failed
  • failed_to_store - Storage operation failed
  • failed_to_get - Retrieval operation failed

License

This source code project is licensed under the MIT License.


©️ 2025 邱敬幃 Pardn Chiu

Top comments (0)