golangJwtAuth

package module
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Jun 3, 2025 License: MIT Imports: 18 Imported by: 0

README

JWT Auth (Golang)

A JWT authentication package providing both Access Token and Refresh Token mechanisms, featuring fingerprint recognition, Redis storage, and automatic refresh functionality.
version Node.js can get here

version

Feature

  • Dual Token System
    • Access Token (short-term) + Refresh Token (long-term)
    • Automatic token refresh without requiring re-login
    • ES256 algorithm (Elliptic Curve Digital Signature)
  • Device Fingerprinting
    • Generates unique fingerprints based on User-Agent, Device ID, OS, and Browser
    • Prevents token misuse across different devices
    • Automatic device type detection (Desktop, Mobile, Tablet)
  • Token Revocation
    • Adds Access Token to blacklist upon logout
    • Redis TTL automatically cleans expired revocation records
    • Prevents reuse of logged-out tokens
  • Version Control Protection
    • Refresh Token version tracking
    • Auto-generates new Refresh ID after 5 refresh attempts
    • Prevents replay attacks
  • Smart Refresh Strategy
    • Auto-regenerates when Refresh Token has less than half lifetime remaining
    • 5-second grace period for old tokens to reduce concurrency issues
    • Minimizes database queries
  • Multiple Authentication Methods
    • Automatic cookie reading
    • Authorization Bearer Header
    • Custom Headers (X-Device-ID, X-Refresh-ID)
  • Flexible Configuration
    • Supports file paths or direct key content
    • Customizable Cookie names
    • Production/Development environment auto-switching

How to use

  • Installation
    go get github.com/pardnchiu/golang-jwt-auth
    
  • Initialize
    package main
    
    import (
      "log"
      "time"
    
      "github.com/pardnchiu/golang-jwt-auth"
    )
    
    func main() {
      config := &golangJwtAuth.Config{
        PrivateKeyPath:       "./keys/private.pem",
        PublicKeyPath:        "./keys/public.pem",
        AccessTokenExpires:   15 * time.Minute,
        RefreshIdExpires:     7 * 24 * time.Hour,
        IsProd:               false,
        Domain:               "pardn.io",
        Redis: golangJwtAuth.RedisConfig{
          Host: "localhost",
          Port: 6379,
        },
        CheckUserExists: func(user golangJwtAuth.AuthData) (bool, error) {
          return true, nil
        },
      }
    
      jwtAuth, err := golangJwtAuth.New(config)
      if err != nil {
        log.Fatal("failed to init:", err)
      }
      defer jwtAuth.Close()
    }
    
  • Create
    Flow
    flowchart TD
      CreateStart([Create Token Request]) --> ValidateAuthData{Validate User Data}
      ValidateAuthData -->|Invalid| CreateError[Return Error]
      ValidateAuthData -->|Valid| GenerateJTI[Generate JTI]
      GenerateJTI --> GenerateFingerprint[Generate Fingerprint]
      GenerateFingerprint --> CreateRefreshId[Create Refresh ID]
      CreateRefreshId --> CreateAccessToken[Create Access Token]
      CreateAccessToken --> StoreRedisData[Store Redis Data]
      StoreRedisData --> SetTokenCookies[Set Token Cookies]
      SetTokenCookies --> CreateSuccess[Creation Success]
    
    func loginHandler(jwtAuth *golangJwtAuth.JWTAuth) http.HandlerFunc {
      return func(w http.ResponseWriter, r *http.Request) {
        // after verifying user login info...
    
        userData := &golangJwtAuth.AuthData{
          ID:        "user123",
          Name:      "John Doe",
          Email:     "john@example.com",
          Thumbnail: "avatar.jpg",
          Role:      "user",
          Level:     1,
          Scope:     []string{"read", "write"},
        }
    
        tokenResult, err := jwtAuth.Create(r, w, userData)
        if err != nil {
          http.Error(w, err.Error(), http.StatusInternalServerError)
          return
        }
    
        // automatically set in cookies
        json.NewEncoder(w).Encode(map[string]interface{}{
          "success":    true,
          "token":      tokenResult.Token,
          "refresh_id": tokenResult.RefreshId,
        })
      }
    }
    
  • Verify
    Flow
    flowchart TD
      Start([Request Start]) --> Auth{Has Access Token?}
    
      Auth -->|Yes| CheckRevoke[Check Token Revocation]
      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 & 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]
    
      CreateNewRefresh --> SetOldRefreshExpire[Set Old Refresh ID to Expire in 5s]
      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]
    
    func protectedHandler(jwtAuth *golangJwtAuth.JWTAuth) http.HandlerFunc {
      return func(w http.ResponseWriter, r *http.Request) {
        result := jwtAuth.Verify(r, w)
    
        if !result.Success {
          w.WriteHeader(result.StatusCode)
          json.NewEncoder(w).Encode(map[string]string{
            "error": result.Error,
          })
          return
        }
    
        // Use the authenticated user data
        user := result.Data
        json.NewEncoder(w).Encode(map[string]interface{}{
          "message": "Protected resource accessed",
          "user":    user,
        })
      }
    }
    
  • Revoke
    Flow
    flowchart TD
      RevokeStart([Revoke Request]) --> ClearCookies[Clear Cookies]
      ClearCookies --> GetTokens[Get Token Info]
      GetTokens --> HasRefreshId{Has Refresh ID?}
      HasRefreshId -->|No| RevokeSuccess[Revocation Success]
      HasRefreshId -->|Yes| GetRefreshData2[Get Refresh Data from Redis]
      GetRefreshData2 --> RefreshExists{Refresh Data Exists?}
      RefreshExists -->|No| RevokeSuccess
      RefreshExists -->|Yes| SetOldRefreshExpire2[Set Refresh ID to Expire in 5s]
      SetOldRefreshExpire2 --> CheckAccessTTL[Check Access Token TTL]
      CheckAccessTTL --> TTLValid{TTL > 0?}
      TTLValid -->|No| RevokeSuccess
      TTLValid -->|Yes| AddToRevokeList[Add Access Token to Revoke List]
      AddToRevokeList --> RevokeSuccess
    
    func logoutHandler(jwtAuth *golangJwtAuth.JWTAuth) http.HandlerFunc {
      return func(w http.ResponseWriter, r *http.Request) {
        err := jwtAuth.Revoke(r, w)
        if err != nil {
          http.Error(w, err.Error(), http.StatusInternalServerError)
          return
        }
    
        json.NewEncoder(w).Encode(map[string]string{
          "message": "Successfully logged out",
        })
      }
    }
    
  • GinMiddleware
    package main
    
    import (
      "github.com/gin-gonic/gin"
      "github.com/pardnchiu/golang-jwt-auth"
    )
    
    func main() {
      // Initialize jwtAuth...
    
      r := gin.Default()
    
      // Apply as a global middleware
      r.Use(jwtAuth.GinMiddleware())
    
      // Or apply to specific route groups
      protected := r.Group("/api/protected")
      protected.Use(jwtAuth.GinMiddleware())
      {
        protected.GET("/profile", func(c *gin.Context) {
          // Get user data from Context
          user, exists := golangJwtAuth.GetAuthDataFromGinContext(c)
          if !exists {
            c.JSON(500, gin.H{"error": "Failed to get user data"})
            return
          }
    
          c.JSON(200, gin.H{
            "user": user,
          })
        })
      }
    
      r.Run(":8080")
    }
    
  • HTTPMiddleware
    package main
    
    import (
      "net/http"
      "github.com/pardnchiu/golang-jwt-auth"
    )
    
    func main() {
      // Initialize jwtAuth...
    
      mux := http.NewServeMux()
    
      // Protected route
      mux.HandleFunc("/api/profile", func(w http.ResponseWriter, r *http.Request) {
        // Get user data from Request Context
        user, exists := golangJwtAuth.GetAuthDataFromHTTPRequest(r)
        if !exists {
          http.Error(w, "Failed to get user data", http.StatusInternalServerError)
          return
        }
    
        json.NewEncoder(w).Encode(map[string]interface{}{
          "user": user,
        })
      })
    
      // Apply middleware
      server := &http.Server{
        Addr:    ":8080",
        Handler: jwtAuth.HTTPMiddleware(mux),
      }
    
      server.ListenAndServe()
    }
    

Configuration

Config Parameters
  • PrivateKeyPath / PrivateKey: private key file path or content
  • PublicKeyPath / PublicKey: public key file path or content
  • AccessTokenExpires: access token expire time
  • RefreshIdExpires: refresh id expire time
  • IsProd: is production or not (affects cookie setting)
  • Domain: cookie domain
  • Redis: redis connection
    • Host: redis host
    • Port: redis port
    • Password: redis password (optional)
    • Db: redis db (optional)
  • CheckUserExists: user existence check function
  • AccessTokenCookieKey: access token cookie name (default: 'access_token')
  • RefreshTokenCookieKey: refresh id cookie name (default: 'refresh_id')
  • MaxVersion: Version threshold (default: 5)
  • RefreshTTL: TTL threshold (default: 0.5)
  • LogPath: Custom log path (default: './logs/golangJWTAuth')
Supported Authentication Methods
  1. Cookie: Automatically reads token from cookie
  2. Authorization Header: Authorization: Bearer <token>
  3. Custom Headers:
    • X-Session-FP: Custom fingerprint
    • X-Refresh-ID: Custom Refresh ID
    • X-Device-ID: Device ID
Token Refresh Response

The new tokens are returned via:

  • HTTP Header: X-New-Access-Token
  • HTTP Header: X-New-Refresh-ID
  • Cookie auto-update

Security features

  • Fingerprint recognition: Generates a unique fingerprint based on User-Agent, Device-ID, OS, Browser, and Device type
  • Token revocation: Adds token to a blacklist on logout
  • Automatic expiration: Supports TTL to automatically clean up expired tokens
  • Version control: Tracks Refresh Token versions to prevent replay attacks
  • Fingerprint validation: Ensures tokens are used from the same device/browser

Error handling

All main methods return an AuthResult struct, including:

  • Success: Whether the operation succeeded
  • StatusCode: HTTP status code
  • Error: Error message
  • Data: User data (on success)

License

This source code project is licensed under the MIT license.

Creator

邱敬幃 Pardn Chiu


©️ 2025 邱敬幃 Pardn Chiu

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type AuthData

type AuthData struct {
	ID        string   `json:"id"`
	Name      string   `json:"name"`
	Email     string   `json:"email"`
	Thumbnail string   `json:"thumbnail,omitempty"`
	Scope     []string `json:"scope,omitempty"`
	Role      string   `json:"role,omitempty"`
	Level     int      `json:"level,omitempty"`
}

func GetAuthDataFromGinContext

func GetAuthDataFromGinContext(c *gin.Context) (*AuthData, bool)

func GetAuthDataFromHTTPRequest

func GetAuthDataFromHTTPRequest(r *http.Request) (*AuthData, bool)

type AuthResult

type AuthResult struct {
	Success    bool      `json:"success"`
	Data       *AuthData `json:"data,omitempty"`
	Error      string    `json:"error,omitempty"`
	StatusCode int       `json:"status_code"`
}

type Config

type Config struct {
	PrivateKeyPath       string                       `json:"private_key_path"`               // Path to private key file
	PublicKeyPath        string                       `json:"public_key_path"`                // Path to public key file
	PrivateKey           string                       `json:"private_key,omitempty"`          // Or directly provide private key content
	PublicKey            string                       `json:"public_key,omitempty"`           // Or directly provide public key content
	AccessTokenExpires   time.Duration                `json:"access_token_expires,omitempty"` // Default 15 minutes
	RefreshIdExpires     time.Duration                `json:"refresh_id_expires,omitempty"`   // Default 7 days
	IsProd               bool                         `json:"is_prod"`                        // Default false
	Domain               string                       `json:"domain,omitempty"`               // Default localhost
	Redis                RedisConfig                  `json:"redis"`
	CheckUserExists      func(AuthData) (bool, error) `json:"-"`
	AccessTokenCookieKey string                       `json:"access_token_cookie_key,omitempty"` // Default access_token
	RefreshIdCookieKey   string                       `json:"refresh_id_cookie_key,omitempty"`   // Default refresh_id
	MaxVersion           int                          `json:"max_version,omitempty"`             // Version threshold, default 5
	RefreshTTL           float64                      `json:"refresh_ttl,omitempty"`             // TTL threshold, default 0.5
	LogPath              string                       `json:"log_path,omitempty"`
	LogStdout            bool                         `json:"log_stdout,omitempty"` // Default true
	PrivateKeyPEM        *ecdsa.PrivateKey            `json:"-"`
	PublicKeyPEM         *ecdsa.PublicKey             `json:"-"`
}

type JWTAuth

type JWTAuth struct {
	Config  *Config
	Redis   *redis.Client
	Context context.Context
	Logger  *Logger
}

func New

func New(c *Config) (*JWTAuth, error)

func (*JWTAuth) Close

func (j *JWTAuth) Close() error

func (*JWTAuth) Create

func (j *JWTAuth) Create(r *http.Request, w http.ResponseWriter, u *AuthData) (*TokenResult, error)

func (*JWTAuth) GinMiddleware

func (j *JWTAuth) GinMiddleware() gin.HandlerFunc

func (*JWTAuth) HTTPMiddleware

func (j *JWTAuth) HTTPMiddleware(next http.Handler) http.Handler

func (*JWTAuth) Refresh

func (j *JWTAuth) Refresh(r *http.Request, w http.ResponseWriter, refreshId string, fp string) *AuthResult

func (*JWTAuth) Revoke

func (j *JWTAuth) Revoke(r *http.Request, w http.ResponseWriter) error

func (*JWTAuth) Verify

func (j *JWTAuth) Verify(r *http.Request, w http.ResponseWriter) *AuthResult

type Logger added in v0.4.0

type Logger struct {
	Stdout       bool
	DebugLogger  *log.Logger
	OutputLogger *log.Logger
	ErrorLogger  *log.Logger
	Path         string
	File         []*os.File

	MaxSize int64
	Closed  bool
	// contains filtered or unexported fields
}

func (*Logger) Close added in v0.5.0

func (l *Logger) Close() error

func (*Logger) Critical added in v0.5.0

func (l *Logger) Critical(messages ...string) error

func (*Logger) Debug added in v0.5.0

func (l *Logger) Debug(messages ...string)

func (*Logger) Error added in v0.5.0

func (l *Logger) Error(messages ...string) error

func (*Logger) Fatal added in v0.5.0

func (l *Logger) Fatal(messages ...string) error

func (*Logger) Flush added in v0.5.0

func (l *Logger) Flush() error

func (*Logger) Info added in v0.5.0

func (l *Logger) Info(messages ...string)

func (*Logger) Notice added in v0.5.0

func (l *Logger) Notice(messages ...string)

func (*Logger) Trace added in v0.5.0

func (l *Logger) Trace(messages ...string)

func (*Logger) Warning added in v0.5.0

func (l *Logger) Warning(messages ...string)

type LoggerConfig added in v0.5.0

type LoggerConfig struct {
	Path    string
	MaxSize int64

	Stdout bool
	// contains filtered or unexported fields
}

type RedisConfig

type RedisConfig struct {
	Host     string `json:"host"`
	Port     int    `json:"port"`
	Password string `json:"password,omitempty"`
	DB       int    `json:"db"`
}

type RefreshData

type RefreshData struct {
	Data        *AuthData `json:"data,omitempty"`
	Version     int       `json:"version"`
	Fingerprint string    `json:"fp"`
	EXP         int64     `json:"exp"`
	IAT         int64     `json:"iat"`
	JTI         string    `json:"jti"`
}

type RefreshID added in v0.5.0

type RefreshID struct {
	ID          string `json:"id"`
	Name        string `json:"name"`
	Email       string `json:"email"`
	Fingerprint string `json:"fp"`
	IAT         int64  `json:"iat"`
	JTI         string `json:"jti"`
}

type TokenResult

type TokenResult struct {
	Token     string `json:"token"`
	RefreshId string `json:"refresh_id"`
}

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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