---
title: "JWT Authentication in Go"
url: "https://vanducng.dev/2024/04/19/JWT-Authentication-in-Go/"
date: "2024-04-19"
series: "go-backend"
categories: ["Backend"]
tags: ["go","security","api"]
---

# JWT Authentication in Go

Authentication verifies who users are, while authorization determines what they can access. This post covers implementing JWT (JSON Web Token) authentication in Go - the most common approach for stateless API authentication.

## Authentication vs Authorization

```mermaid
flowchart LR
    subgraph Auth["Authentication (AuthN)"]
        A1[Who are you?]
        A2[Username/Password]
        A3[Identity Verified]
    end

    subgraph Authz["Authorization (AuthZ)"]
        B1[What can you do?]
        B2[Check Permissions]
        B3[Access Granted/Denied]
    end

    Auth --> Authz

    classDef blueClass fill:#4A90E2,stroke:#333,stroke-width:2px,color:#fff
    classDef greenClass fill:#27AE60,stroke:#333,stroke-width:2px,color:#fff

    class Auth blueClass
    class Authz greenClass
```

| Aspect | Authentication | Authorization |
|--------|----------------|---------------|
| Question | "Who are you?" | "What can you access?" |
| Process | Verify identity | Check permissions |
| Methods | Passwords, tokens, biometrics | Roles, policies, ACLs |
| Happens | First | After authentication |

## Session vs Token Authentication

### Session-Based

```mermaid
sequenceDiagram
    participant C as Client
    participant S as Server
    participant DB as Session Store

    C->>S: Login (credentials)
    S->>DB: Create session
    DB-->>S: Session ID
    S-->>C: Set-Cookie: session_id

    Note over C,S: Subsequent requests
    C->>S: Request + Cookie
    S->>DB: Lookup session
    DB-->>S: Session data
    S-->>C: Response
```

### Token-Based (JWT)

```mermaid
sequenceDiagram
    participant C as Client
    participant S as Server

    C->>S: Login (credentials)
    S-->>C: JWT Token

    Note over C,S: Subsequent requests
    C->>S: Request + Authorization: Bearer <token>
    S->>S: Verify token signature
    S-->>C: Response
```

**JWT Advantages:**
- Stateless (no server-side storage)
- Scalable across multiple servers
- Contains user claims
- Works well with microservices

## JWT Structure

A JWT consists of three parts separated by dots:

```
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4ifQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
```

```mermaid
flowchart LR
    subgraph Header["Header (Base64)"]
        H[Algorithm + Type]
    end

    subgraph Payload["Payload (Base64)"]
        P[Claims - User Data]
    end

    subgraph Signature["Signature"]
        S[HMAC/RSA Signed]
    end

    Header --> Payload --> Signature

    classDef blueClass fill:#4A90E2,stroke:#333,stroke-width:2px,color:#fff
    classDef orangeClass fill:#F39C12,stroke:#333,stroke-width:2px,color:#fff
    classDef greenClass fill:#27AE60,stroke:#333,stroke-width:2px,color:#fff

    class Header blueClass
    class Payload orangeClass
    class Signature greenClass
```

## Implementing JWT in Go

### Installation

```bash
go get -u github.com/golang-jwt/jwt/v5
go get -u golang.org/x/crypto/bcrypt
```

### JWT Service

```go
package auth

import (
    "errors"
    "time"

    "github.com/golang-jwt/jwt/v5"
)

var (
    ErrInvalidToken = errors.New("invalid token")
    ErrExpiredToken = errors.New("token expired")
)

type JWTService struct {
    secretKey     []byte
    accessExpiry  time.Duration
    refreshExpiry time.Duration
}

type Claims struct {
    UserID   uint   `json:"user_id"`
    Username string `json:"username"`
    Role     string `json:"role"`
    jwt.RegisteredClaims
}

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

func NewJWTService(secret string, accessExpiry, refreshExpiry time.Duration) *JWTService {
    return &JWTService{
        secretKey:     []byte(secret),
        accessExpiry:  accessExpiry,
        refreshExpiry: refreshExpiry,
    }
}

func (s *JWTService) GenerateTokenPair(userID uint, username, role string) (*TokenPair, error) {
    // Access token
    accessClaims := Claims{
        UserID:   userID,
        Username: username,
        Role:     role,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(s.accessExpiry)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            Subject:   fmt.Sprintf("%d", userID),
        },
    }

    accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
    accessString, err := accessToken.SignedString(s.secretKey)
    if err != nil {
        return nil, err
    }

    // Refresh token (longer expiry, minimal claims)
    refreshClaims := jwt.RegisteredClaims{
        ExpiresAt: jwt.NewNumericDate(time.Now().Add(s.refreshExpiry)),
        IssuedAt:  jwt.NewNumericDate(time.Now()),
        Subject:   fmt.Sprintf("%d", userID),
    }

    refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
    refreshString, err := refreshToken.SignedString(s.secretKey)
    if err != nil {
        return nil, err
    }

    return &TokenPair{
        AccessToken:  accessString,
        RefreshToken: refreshString,
        ExpiresAt:    accessClaims.ExpiresAt.Unix(),
    }, nil
}

func (s *JWTService) 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, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
        }
        return s.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 (s *JWTService) RefreshAccessToken(refreshTokenString string) (*TokenPair, error) {
    // Validate refresh token
    token, err := jwt.Parse(refreshTokenString, func(token *jwt.Token) (interface{}, error) {
        return s.secretKey, nil
    })

    if err != nil || !token.Valid {
        return nil, ErrInvalidToken
    }

    claims, ok := token.Claims.(jwt.MapClaims)
    if !ok {
        return nil, ErrInvalidToken
    }

    // Get user ID from refresh token
    userID, _ := strconv.ParseUint(claims["sub"].(string), 10, 32)

    // Fetch fresh user data from database (recommended)
    // user, _ := userRepo.FindByID(uint(userID))

    // Generate new token pair
    return s.GenerateTokenPair(uint(userID), "", "") // Add user data
}
```

### Password Hashing

```go
package auth

import (
    "golang.org/x/crypto/bcrypt"
)

func HashPassword(password string) (string, error) {
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    return string(bytes), err
}

func CheckPassword(password, hash string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
    return err == nil
}
```

## Authentication Handlers

### Registration

```go
type RegisterRequest struct {
    Username string `json:"username" binding:"required,min=3,max=50"`
    Email    string `json:"email" binding:"required,email"`
    Password string `json:"password" binding:"required,min=8"`
}

func (h *AuthHandler) Register(c *gin.Context) {
    var req RegisterRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    // Check if user exists
    if h.userRepo.ExistsByEmail(req.Email) {
        c.JSON(http.StatusConflict, gin.H{"error": "Email already registered"})
        return
    }

    // Hash password
    hashedPassword, err := auth.HashPassword(req.Password)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to process password"})
        return
    }

    // Create user
    user := &models.User{
        Username: req.Username,
        Email:    req.Email,
        Password: hashedPassword,
        Role:     "user",
    }

    if err := h.userRepo.Create(user); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
        return
    }

    // Generate tokens
    tokens, err := h.jwtService.GenerateTokenPair(user.ID, user.Username, user.Role)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate tokens"})
        return
    }

    c.JSON(http.StatusCreated, gin.H{
        "message": "Registration successful",
        "user": gin.H{
            "id":       user.ID,
            "username": user.Username,
            "email":    user.Email,
        },
        "tokens": tokens,
    })
}
```

### Login

```go
type LoginRequest struct {
    Email    string `json:"email" binding:"required,email"`
    Password string `json:"password" binding:"required"`
}

func (h *AuthHandler) Login(c *gin.Context) {
    var req LoginRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    // Find user
    user, err := h.userRepo.FindByEmail(req.Email)
    if err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
        return
    }

    // Verify password
    if !auth.CheckPassword(req.Password, user.Password) {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
        return
    }

    // Check if user is active
    if !user.IsActive {
        c.JSON(http.StatusForbidden, gin.H{"error": "Account is deactivated"})
        return
    }

    // Generate tokens
    tokens, err := h.jwtService.GenerateTokenPair(user.ID, user.Username, user.Role)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate tokens"})
        return
    }

    // Update last login
    h.userRepo.UpdateLastLogin(user.ID)

    c.JSON(http.StatusOK, gin.H{
        "message": "Login successful",
        "user": gin.H{
            "id":       user.ID,
            "username": user.Username,
            "email":    user.Email,
            "role":     user.Role,
        },
        "tokens": tokens,
    })
}
```

### Token Refresh

```go
type RefreshRequest struct {
    RefreshToken string `json:"refresh_token" binding:"required"`
}

func (h *AuthHandler) Refresh(c *gin.Context) {
    var req RefreshRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    tokens, err := h.jwtService.RefreshAccessToken(req.RefreshToken)
    if err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid refresh token"})
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "tokens": tokens,
    })
}
```

## Authentication Middleware

```go
func AuthMiddleware(jwtService *auth.JWTService) gin.HandlerFunc {
    return func(c *gin.Context) {
        // Get token from header
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"})
            c.Abort()
            return
        }

        // Check Bearer format
        parts := strings.Split(authHeader, " ")
        if len(parts) != 2 || parts[0] != "Bearer" {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid authorization format"})
            c.Abort()
            return
        }

        // Validate token
        claims, err := jwtService.ValidateToken(parts[1])
        if err != nil {
            if errors.Is(err, auth.ErrExpiredToken) {
                c.JSON(http.StatusUnauthorized, gin.H{"error": "Token expired"})
            } else {
                c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
            }
            c.Abort()
            return
        }

        // Set user info in context
        c.Set("user_id", claims.UserID)
        c.Set("username", claims.Username)
        c.Set("role", claims.Role)

        c.Next()
    }
}
```

### Using Middleware

```go
func SetupRoutes(router *gin.Engine, jwtService *auth.JWTService) {
    // Public routes
    public := router.Group("/api/v1")
    {
        public.POST("/register", authHandler.Register)
        public.POST("/login", authHandler.Login)
        public.POST("/refresh", authHandler.Refresh)
    }

    // Protected routes
    protected := router.Group("/api/v1")
    protected.Use(AuthMiddleware(jwtService))
    {
        protected.GET("/profile", userHandler.GetProfile)
        protected.PUT("/profile", userHandler.UpdateProfile)
        protected.GET("/orders", orderHandler.ListOrders)
    }
}
```

## Token Storage Best Practices

```mermaid
flowchart TD
    subgraph Client["Client Storage"]
        AT[Access Token] --> M1[Memory/State]
        RT[Refresh Token] --> M2[HttpOnly Cookie]
    end

    subgraph Security["Security Measures"]
        S1[Short Access Expiry
15-60 minutes]
        S2[Long Refresh Expiry
7-30 days]
        S3[HttpOnly + Secure
+ SameSite]
    end

    classDef blueClass fill:#4A90E2,stroke:#333,stroke-width:2px,color:#fff
    classDef greenClass fill:#27AE60,stroke:#333,stroke-width:2px,color:#fff

    class Client blueClass
    class Security greenClass
```

### Secure Cookie for Refresh Token

```go
func (h *AuthHandler) Login(c *gin.Context) {
    // ... authentication logic ...

    // Set refresh token as HttpOnly cookie
    c.SetCookie(
        "refresh_token",
        tokens.RefreshToken,
        int(7*24*time.Hour.Seconds()), // 7 days
        "/api/v1/refresh",
        "",    // domain
        true,  // secure (HTTPS only)
        true,  // httpOnly
    )

    // Return only access token in body
    c.JSON(http.StatusOK, gin.H{
        "access_token": tokens.AccessToken,
        "expires_at":   tokens.ExpiresAt,
    })
}
```

## Complete Auth Flow

```mermaid
sequenceDiagram
    participant C as Client
    participant S as Server
    participant DB as Database

    Note over C,S: Registration
    C->>S: POST /register
    S->>DB: Create user
    S-->>C: 201 + Tokens

    Note over C,S: Login
    C->>S: POST /login
    S->>DB: Verify credentials
    S-->>C: 200 + Tokens

    Note over C,S: Protected Request
    C->>S: GET /profile with Bearer token
    S->>S: Validate JWT
    S->>DB: Fetch user data
    S-->>C: 200 + Profile

    Note over C,S: Token Refresh
    C->>S: POST /refresh
    S->>S: Validate refresh token
    S-->>C: 200 + New tokens
```

## Summary

| Component | Purpose |
|-----------|---------|
| **JWT** | Stateless authentication token |
| **Access Token** | Short-lived, contains claims |
| **Refresh Token** | Long-lived, used to get new access tokens |
| **bcrypt** | Secure password hashing |
| **Middleware** | Validates token on protected routes |

---

*Next post: [Authorization Patterns: RBAC and ABAC in Go](https://vanducng.dev/2024/04/26/Authorization-Patterns-RBAC-ABAC-Go/) - Implementing role-based and attribute-based access control.*
