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

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

    style Auth fill:#e3f2fd
    style Authz fill:#e8f5e9
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

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)

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 
    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:

1
2
3
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4ifQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
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

    style Header fill:#e3f2fd
    style Payload fill:#fff3e0
    style Signature fill:#e8f5e9

Implementing JWT in Go

Installation

1
2
go get -u github.com/golang-jwt/jwt/v5
go get -u golang.org/x/crypto/bcrypt

JWT Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

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 style Client fill:#e3f2fd style Security fill:#e8f5e9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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

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
Authorization: Bearer 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 - Implementing role-based and attribute-based access control.

Scaling Go APIs: Pagination, Caching, and Rate Limiting Authorization Patterns: RBAC and ABAC in Go

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×