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:
- Register a user:
curl -X POST http://microservice.example.com/auth/register \
-H "Content-Type: application/json" \
-d '{"username": "testuser", "password": "testpass"}'
- 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..."}
- 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.
- 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:
- Use HTTPS: Always serve your APIs over HTTPS in production
- Password Hashing: Implement proper password hashing (e.g., bcrypt) instead of storing plaintext passwords
- Database: Replace the CSV file with a proper database
- Token Expiration: Implement token refresh mechanisms for handling expired tokens
- 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.