Disclaimer, this article was AI generated

Overview

This guide provides detailed instructions for implementing a secure authentication system using NGINX with JSON Web Tokens (JWT). You’ll learn how to install all necessary dependencies, compile NGINX with the nginx-auth-jwt module, and deploy two complementary Go microservices: one for generating and issuing JWTs and another that demonstrates how NGINX validates these tokens in a real-world scenario.

By the end of this tutorial, you’ll have a robust authentication system that can be integrated into your existing microservices architecture.


1. System Dependencies

First, let’s install all the required packages to build NGINX with the JWT module:

sudo apt update
sudo apt install -y build-essential libpcre3 libpcre3-dev zlib1g zlib1g-dev libssl-dev git curl wget

These packages provide the essential build tools and libraries required for compiling NGINX from source, including PCRE for regular expression support, zlib for compression, and OpenSSL for TLS/SSL capabilities.


2. Build NGINX with the JWT Module

Clone Repositories

We’ll start by cloning the nginx-auth-jwt module repository:

git clone https://github.com/kjdev/nginx-auth-jwt.git
cd nginx-auth-jwt

Download NGINX Source

Next, we need to download the NGINX source code. We’ll use version 1.28.0, but you can adjust this to your preferred version:

NGINX_VERSION=1.28.0
wget http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz
tar -zxf nginx-${NGINX_VERSION}.tar.gz
cd nginx-${NGINX_VERSION}

Configure and Build NGINX

Now we’ll configure and build the JWT module as a dynamic module for NGINX:

./configure --add-dynamic-module=../ --with-compat
make modules
sudo cp objs/ngx_http_auth_jwt_module.so /etc/nginx/modules/

This compiles only the JWT module without rebuilding the entire NGINX server, making it compatible with your existing NGINX installation.


3. Configure NGINX to Load JWT Module

Modify /etc/nginx/nginx.conf

Open your main NGINX configuration file and add the following directive at the top, before the events block:

load_module modules/ngx_http_auth_jwt_module.so;

This instructs NGINX to load our newly compiled JWT authentication module when it starts.


4. Sample NGINX Config: /etc/nginx/conf.d/microservice.example.com.conf

Create a new server configuration file with the following content:

server {
    listen 80;
    server_name microservice.example.com;

    # Auth service - unprotected endpoint for obtaining JWTs
    location /auth/ {
        proxy_pass http://localhost:8081;
    }

    # Protected endpoints requiring valid JWT
    location / {
        auth_jwt "secured";
        auth_jwt_key_file /etc/nginx/jwt_key.jwk;

        # Pass JWT claims as headers to backend services
        proxy_set_header X-JWT-Sub $jwt_sub;
        proxy_set_header X-JWT-Email $jwt_email;
        proxy_set_header X-JWT-Role $jwt_role;

        proxy_pass http://localhost:8080;
    }
}

This configuration sets up two distinct locations:

  • /auth/ routes requests to our authentication service without requiring a JWT
  • All other routes (/) require a valid JWT token and will forward authenticated user information to the backend

5. Go Services

JWT Auth Service (auth_service.go)

This service handles user registration and JWT token generation:

package main

import (
    "encoding/csv"
    "encoding/json"
    "fmt"
    "net/http"
    "os"
    "time"
    "github.com/golang-jwt/jwt/v5"
)

var jwtSecret = []byte(os.Getenv("JWT_SECRET"))

func main() {
    http.HandleFunc("/auth/register", register)
    http.HandleFunc("/auth/login", login)
    fmt.Println("Auth service listening on :8081")
    http.ListenAndServe(":8081", nil)
}

type User struct {
    Username string
    Password string
}

func register(w http.ResponseWriter, r *http.Request) {
    var u User
    json.NewDecoder(r.Body).Decode(&u)
    
    // For a production system, you should validate input and hash passwords
    file, _ := os.OpenFile("users.csv", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    writer := csv.NewWriter(file)
    writer.Write([]string{u.Username, u.Password})
    writer.Flush()
    w.WriteHeader(http.StatusCreated)
}

func login(w http.ResponseWriter, r *http.Request) {
    var u User
    json.NewDecoder(r.Body).Decode(&u)
    
    // Check credentials against our simple CSV database
    file, _ := os.Open("users.csv")
    reader := csv.NewReader(file)
    records, _ := reader.ReadAll()
    
    for _, record := range records {
        if record[0] == u.Username && record[1] == u.Password {
            // Create a JWT with user claims
            token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
                "sub": u.Username,
                "email": u.Username + "@example.com",
                "role": "user",
                "exp": time.Now().Add(time.Hour * 1).Unix(),
            })
            
            // Sign the token with our secret
            tokenString, _ := token.SignedString(jwtSecret)
            json.NewEncoder(w).Encode(map[string]string{"token": tokenString})
            return
        }
    }
    
    http.Error(w, "Invalid credentials", http.StatusUnauthorized)
}

JWT Reader Service (jwt_reader.go)

This simple service demonstrates how to access the JWT claims that NGINX extracts and forwards:

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello! JWT Claims:\n")
        fmt.Fprintf(w, "Sub: %s\n", r.Header.Get("X-JWT-Sub"))
        fmt.Fprintf(w, "Email: %s\n", r.Header.Get("X-JWT-Email"))
        fmt.Fprintf(w, "Role: %s\n", r.Header.Get("X-JWT-Role"))
    })
    fmt.Println("Reader service listening on :8080")
    http.ListenAndServe(":8080", nil)
}

This service simply reads the JWT claims that NGINX extracts and passes as headers, demonstrating how your backend services can utilize user information without having to parse or validate the JWT themselves.


6. JWT Key File

For production use, you’ll need to create a proper JWK (JSON Web Key) file:

# Generate a JWK file from your secret
echo -n '{"keys":[{"k":"'$(echo -n $JWT_SECRET | base64 | tr -d '=' | tr '/+' '_-')'","kty":"oct"}]}' > /etc/nginx/jwt_key.jwk

Ensure the file has appropriate permissions:

sudo chmod 640 /etc/nginx/jwt_key.jwk
sudo chown nginx:nginx /etc/nginx/jwt_key.jwk

7. Run the Services

To get everything up and running:

# Set environment variable for JWT secret
export JWT_SECRET=your_jwt_secret_here

# Start auth service
go run auth_service.go

# In another terminal
go run jwt_reader.go

# Restart NGINX to apply configuration changes
sudo systemctl restart nginx

8. Test the Setup

Now it’s time to test our authentication system:

  1. Register a user:
curl -X POST http://microservice.example.com/auth/register \
    -H "Content-Type: application/json" \
    -d '{"username": "testuser", "password": "testpass"}'
  1. Login to get JWT:
curl -X POST http://microservice.example.com/auth/login \
    -H "Content-Type: application/json" \
    -d '{"username": "testuser", "password": "testpass"}'

You should receive a response containing your JWT token:

{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}
  1. Access protected resource:
curl http://microservice.example.com/ \
    -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

If everything is configured correctly, you should see the claims from your JWT displayed.

  1. Test unauthorized access:
curl http://microservice.example.com/

This should return a 401 Unauthorized error since no JWT was provided.


9. Security Considerations

For production deployments, consider these additional security measures:

  1. Use HTTPS: Always serve your APIs over HTTPS in production
  2. Password Hashing: Implement proper password hashing (e.g., bcrypt) instead of storing plaintext passwords
  3. Database: Replace the CSV file with a proper database
  4. Token Expiration: Implement token refresh mechanisms for handling expired tokens
  5. Rate Limiting: Add rate limiting to prevent brute force attacks

Conclusion

You’ve now successfully set up a secure authentication system using NGINX with JWT validation. This architecture offloads the authentication concerns to NGINX, allowing your microservices to focus on their core functionality while still having access to user identity information.

This pattern is particularly useful in microservice architectures where multiple services need consistent authentication without duplicating validation logic across each service.

Feel free to adapt this setup to your specific requirements and security policies.