You have five microservices behind NGINX. Each one receives API requests carrying a JWT token. Each one independently parses that token, verifies the signature, checks the expiration, and extracts the user’s identity before it can do any real work. You are maintaining five copies of the same JWT validation logic, five sets of key configurations, and five potential points of failure. When you rotate a signing key, you update five services. When a vulnerability is found in a JWT library, you patch five dependencies. This is the reality of application-level JWT authentication in a microservice architecture — and the NGINX TeslaGov JWT module eliminates it entirely.
This module validates JWT tokens once at the NGINX edge and forwards the verified claims as plain HTTP headers to your upstream services. Your backends receive trusted headers like JWT-sub and JWT-role — no token parsing, no key management, no JWT libraries needed.
The NGINX TeslaGov JWT module goes beyond basic token validation. It supports cookie-based authentication for single-page applications, automatic login page redirects for browser-based flows, and flexible claim extraction to NGINX variables, request headers, and response headers. For system administrators managing API gateways or microservice architectures, it provides a complete JWT authentication layer directly in NGINX.
How the NGINX TeslaGov JWT Module Works
The module registers an access phase handler in NGINX’s request processing pipeline. When a request arrives at a protected location, the module:
- Extracts the token from the
Authorization: Bearerheader or a named cookie - Decodes and verifies the JWT signature using a configured HMAC secret or RSA/ECDSA public key via the libjwt library
- Validates expiration by checking the
expclaim against the current server time - Optionally validates that the
sub(subject) claim is present - Extracts claims into NGINX variables, request headers (forwarded to upstream), or response headers (sent to the client)
- Allows or rejects the request — returning 401 Unauthorized, or redirecting to a login page
Because validation happens in NGINX’s access phase, rejected requests never reach your backend. This reduces load on upstream services and centralizes your authentication logic in one place. For a comparison with another JWT approach, see our guide on NGINX JWT authentication which covers the lighter-weight nginx-module-jwt.
Installation
RHEL, CentOS, AlmaLinux, Rocky Linux, Fedora
sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-teslagov-jwt
Then load the module in /etc/nginx/nginx.conf by adding this directive at the top level, before any http block:
load_module modules/ngx_http_auth_jwt_module.so;
For more details, see the module’s RPM page.
Debian and Ubuntu
First, set up the GetPageSpeed APT repository, then install:
sudo apt-get update
sudo apt-get install nginx-module-teslagov-jwt
On Debian/Ubuntu, the package handles module loading automatically. No
load_moduledirective is needed.
For more details, see the module’s APT page.
Configuration Directives
The NGINX TeslaGov JWT module provides 12 configuration directives. All directives are valid in http, server, and location contexts, and child contexts inherit values from their parents.
auth_jwt_enabled
Enables or disables JWT authentication for the current context.
Syntax: auth_jwt_enabled on | off | $variable;
Default: disabled (when no key is configured)
# Static enable
auth_jwt_enabled on;
# Dynamic enable via variable
auth_jwt_enabled $jwt_enabled;
When set to a variable, the module evaluates it at request time. If the variable resolves to off, authentication is skipped. This allows conditional authentication — for example, disabling JWT checks for health check endpoints while protecting everything else.
auth_jwt_key
Specifies the key used to verify JWT signatures.
Syntax: auth_jwt_key <value>;
Default: none
For HMAC algorithms (HS256, HS384, HS512), provide the key in hexadecimal format:
auth_jwt_key 48c80a1b5e69a9519a14d0a58bc0be29ab3b89e71543c72b7fd8e48e7baa1a2c;
For RSA or ECDSA algorithms, provide the PEM-encoded public key inline:
auth_jwt_key "-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----";
Generate a secure 256-bit HMAC key with:
openssl rand -hex 32
auth_jwt_algorithm
Specifies the expected JWT signing algorithm.
Syntax: auth_jwt_algorithm HS256 | HS384 | HS512 | RS256 | RS384 | RS512 | ES256 | ES384 | ES512;
Default: HS256
auth_jwt_algorithm RS256;
Supported algorithm families:
| Family | Algorithms | Key Type |
|---|---|---|
| HMAC | HS256, HS384, HS512 | Shared secret (hex) |
| RSA | RS256, RS384, RS512 | RSA public key (PEM) |
| ECDSA | ES256, ES384, ES512 | EC public key (PEM) |
Important: This directive controls how the key is processed for signature verification, but the actual algorithm used for verification is determined by the JWT token’s
algheader field. When using HMAC keys, ensure your token-issuing service uses the same algorithm you intend.
auth_jwt_location
Specifies where to find the JWT token in the request.
Syntax: auth_jwt_location HEADER=<name> | COOKIE=<name>;
Default: HEADER=Authorization
When set to a header, the module expects the token in the Authorization: Bearer <token> format. When set to a cookie, the module reads the token directly from the named cookie value.
# Read from Authorization header (default)
auth_jwt_location HEADER=Authorization;
# Read from a cookie named "auth_token"
auth_jwt_location COOKIE=auth_token;
Cookie-based authentication is particularly useful for single-page applications where storing tokens in cookies with HttpOnly and Secure flags provides better protection against XSS attacks than localStorage.
auth_jwt_redirect
Enables redirection to a login page when authentication fails.
Syntax: auth_jwt_redirect on | off;
Default: off
When enabled, failed authentication results in a 302 redirect to the URL specified by auth_jwt_loginurl. When disabled, the module returns 401 Unauthorized.
auth_jwt_loginurl
Specifies the login page URL for authentication redirects.
Syntax: auth_jwt_loginurl <url>;
Default: none
auth_jwt_redirect on;
auth_jwt_loginurl https://login.example.com/auth;
For GET requests, the module appends a return_url query parameter containing the original request URL, so the login page can redirect back after successful authentication:
https://login.example.com/auth?return_url=http://example.com/protected/page
auth_jwt_validate_sub
Requires the JWT to contain a sub (subject) claim.
Syntax: auth_jwt_validate_sub on | off;
Default: off
auth_jwt_validate_sub on;
When enabled, tokens without a sub claim are rejected with a 401 response. This is useful when your authorization logic depends on identifying the token holder.
auth_jwt_use_keyfile and auth_jwt_keyfile_path
Load the verification key from a file instead of specifying it inline.
Syntax: auth_jwt_use_keyfile on | off;
Default: off
Syntax: auth_jwt_keyfile_path <path>;
Default: none
auth_jwt_use_keyfile on;
auth_jwt_keyfile_path /etc/nginx/keys/jwt-public.pem;
This is the recommended approach for RSA and ECDSA keys, as it keeps large PEM keys out of the NGINX configuration file and simplifies key rotation. The file is read once at configuration load time.
auth_jwt_extract_var_claims
Extracts JWT claims as NGINX variables.
Syntax: auth_jwt_extract_var_claims <claim> [<claim> ...];
Default: none
auth_jwt_extract_var_claims sub role department;
Each specified claim becomes available as an NGINX variable named $jwt_claim_<name>. For example, the sub claim becomes $jwt_claim_sub. You can use these variables in add_header, proxy_set_header, map, if, log formats, and anywhere else NGINX variables are accepted.
Note: Only string claims are supported. Non-string claim types (numbers, booleans, arrays, objects) are not extracted.
auth_jwt_extract_request_claims
Extracts JWT claims as request headers forwarded to upstream services.
Syntax: auth_jwt_extract_request_claims <claim> [<claim> ...];
Default: none
auth_jwt_extract_request_claims sub role;
Each specified claim is added as a request header with a JWT- prefix. For example, the sub claim becomes the header JWT-sub. These headers are then available to upstream services, and within NGINX as $http_jwt_sub.
This is the key feature for API gateway architectures — your backend receives verified identity information as plain HTTP headers without needing to parse or validate the JWT itself.
Important: Do not use
proxy_set_header JWT-sub ""in the same location asauth_jwt_extract_request_claims sub. Theproxy_set_headerdirective overrides the module’s extracted claims, and the backend will receive empty headers instead of the claim values.
auth_jwt_extract_response_claims
Extracts JWT claims as response headers sent to the client.
Syntax: auth_jwt_extract_response_claims <claim> [<claim> ...];
Default: none
auth_jwt_extract_response_claims sub;
Each specified claim is added to the response with a JWT- prefix. Within NGINX, these are accessible as $sent_http_jwt_<name>.
Basic Configuration: Protecting an API
The simplest use case is protecting API endpoints with HMAC-based JWT authentication.
First, generate a secure key:
openssl rand -hex 32
Then configure NGINX:
server {
listen 80;
server_name api.example.com;
root /var/www/api;
# Public health check
location /health {
access_log off;
return 200 "OK\n";
}
# Protected API
location /api/ {
auth_jwt_enabled on;
auth_jwt_key 48c80a1b5e69a9519a14d0a58bc0be29ab3b89e71543c72b7fd8e48e7baa1a2c;
auth_jwt_algorithm HS256;
}
}
Requests without a valid JWT receive a 401 response. Requests with a valid Authorization: Bearer <token> header are allowed through.
Cookie-Based Authentication for SPAs
Single-page applications often store JWT tokens in cookies rather than in JavaScript-accessible storage for better security. The module supports reading tokens directly from cookies and redirecting unauthenticated users to a login page.
server {
listen 80;
server_name app.example.com;
root /var/www/app;
location / {
auth_jwt_enabled on;
auth_jwt_key 48c80a1b5e69a9519a14d0a58bc0be29ab3b89e71543c72b7fd8e48e7baa1a2c;
auth_jwt_algorithm HS256;
auth_jwt_location COOKIE=auth_token;
auth_jwt_redirect on;
auth_jwt_loginurl https://app.example.com/login;
}
# Login page is public
location /login {
auth_jwt_enabled off;
}
}
When an unauthenticated user visits any page, they are redirected to:
https://app.example.com/login?return_url=http://app.example.com/original-page
Your login page can read the return_url parameter and redirect back after successful authentication.
API Gateway with Claim Forwarding
The most powerful use case is forwarding verified JWT claims to upstream services. This eliminates redundant JWT validation in every microservice.
upstream backend_api {
server 127.0.0.1:8080;
}
server {
listen 80;
server_name gateway.example.com;
location /api/ {
auth_jwt_enabled on;
auth_jwt_key 48c80a1b5e69a9519a14d0a58bc0be29ab3b89e71543c72b7fd8e48e7baa1a2c;
auth_jwt_algorithm HS256;
# Forward verified claims to upstream
auth_jwt_extract_request_claims sub role department;
proxy_pass http://backend_api;
}
}
Your backend service receives these additional request headers:
JWT-sub: user123
JWT-role: admin
JWT-department: engineering
The backend can trust these headers because NGINX has already verified the token’s signature and expiration. Therefore, the backend no longer needs JWT libraries or key management — it reads identity from plain headers.
Security note: When using claim forwarding, ensure that clients cannot bypass NGINX and connect directly to the backend. Otherwise, an attacker could forge
JWT-*headers. Use firewall rules or network policies to restrict backend access to NGINX only.
RSA Key Configuration
For environments where you cannot share a symmetric key (for example, when a separate identity provider issues tokens), use RSA keys. The identity provider signs tokens with its private key, and NGINX verifies them with the corresponding public key.
server {
listen 80;
server_name api.example.com;
location /api/ {
auth_jwt_enabled on;
auth_jwt_use_keyfile on;
auth_jwt_keyfile_path /etc/nginx/keys/jwt-public.pem;
auth_jwt_algorithm RS256;
auth_jwt_validate_sub on;
}
}
Set the correct file permissions on the key:
sudo chmod 644 /etc/nginx/keys/jwt-public.pem
sudo chown root:root /etc/nginx/keys/jwt-public.pem
Since this is a public key, it does not need restrictive permissions. However, never place a private key on the NGINX server — only the token issuer needs it.
Dynamic Authentication with Variables
You can conditionally enable or disable JWT authentication using NGINX variables. This is useful when certain paths (health checks, metrics, static assets) should remain public while everything else requires authentication.
map $uri $jwt_auth {
/health off;
/metrics off;
default on;
}
server {
listen 80;
server_name api.example.com;
root /var/www/api;
location / {
auth_jwt_enabled $jwt_auth;
auth_jwt_key 48c80a1b5e69a9519a14d0a58bc0be29ab3b89e71543c72b7fd8e48e7baa1a2c;
auth_jwt_algorithm HS256;
}
}
With this configuration, requests to /health and /metrics bypass JWT authentication, while all other paths require a valid token.
Using Extracted Claims in NGINX Logic
Extracted variable claims can drive routing, logging, and access control decisions within NGINX itself.
Logging Authenticated Users
log_format jwt_log '$remote_addr - $jwt_claim_sub [$time_local] '
'"$request" $status $body_bytes_sent '
'"$jwt_claim_role"';
server {
listen 80;
server_name api.example.com;
root /var/www/api;
location /api/ {
auth_jwt_enabled on;
auth_jwt_key 48c80a1b5e69a9519a14d0a58bc0be29ab3b89e71543c72b7fd8e48e7baa1a2c;
auth_jwt_algorithm HS256;
auth_jwt_extract_var_claims sub role;
access_log /var/log/nginx/api_access.log jwt_log;
}
}
This produces log entries like:
192.168.1.100 - user123 [28/Mar/2026:10:30:00 +0000] "GET /api/data HTTP/1.1" 200 1234 "admin"
Testing Your Configuration
Generate Test Tokens with Python
Install the PyJWT library:
pip install PyJWT
Create a token generator script:
import jwt
import time
# Use the same key as in your NGINX config (raw bytes from hex)
secret = bytes.fromhex("48c80a1b5e69a9519a14d0a58bc0be29ab3b89e71543c72b7fd8e48e7baa1a2c")
token = jwt.encode(
{
"sub": "user123",
"role": "admin",
"department": "engineering",
"exp": int(time.time()) + 3600
},
secret,
algorithm="HS256"
)
print(token)
Verify with curl
Test a protected endpoint:
# Should return 401
curl -I http://localhost/api/
# Should return 200
TOKEN=$(python3 generate_token.py)
curl -I -H "Authorization: Bearer $TOKEN" http://localhost/api/
Test cookie-based authentication:
curl -I --cookie "auth_token=$TOKEN" http://localhost/app/
Test claim forwarding (check response headers from upstream):
curl -D- -H "Authorization: Bearer $TOKEN" http://localhost/gateway/api/
Performance Considerations
JWT validation at the NGINX level is lightweight. The cryptographic verification is handled by the libjwt library, which uses OpenSSL under the hood. However, consider these points:
- HMAC vs. RSA: HMAC verification (HS256) is significantly faster than RSA (RS256). For high-throughput APIs, HMAC is preferred when you can securely distribute the shared secret.
- Token size: Large JWTs with many claims increase header processing time. Keep tokens lean — include only the claims your services actually need.
- Claim extraction overhead: Each
auth_jwt_extract_*directive adds minimal processing per request. Extracting 3-5 claims has negligible impact. However, avoid extracting dozens of claims if you only need a few. - Invalid tokens are cheap to reject: Invalid tokens fail fast at the signature verification step, before any claim processing happens. This means that malicious or expired tokens add minimal load.
Security Best Practices
Protect Your Keys
For HMAC keys, use at least 256 bits (32 bytes, or 64 hex characters). Generate keys with a cryptographically secure random source:
openssl rand -hex 32
For RSA keys, use at least 2048-bit key pairs. Store private keys only on the token-issuing service, never on the NGINX server.
Always Set Token Expiration
The module checks the exp claim automatically. Ensure your token issuer always sets short-lived expiration times (15 minutes to 1 hour for access tokens). Tokens without an exp claim are treated as non-expiring.
Restrict Backend Access
When forwarding claims via auth_jwt_extract_request_claims, lock down your backends so they only accept connections from NGINX. An attacker who reaches the backend directly could forge JWT-* headers:
# On the backend server, only allow connections from NGINX
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.0.0.1" port port="8080" protocol="tcp" accept'
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" port port="8080" protocol="tcp" reject'
sudo firewall-cmd --reload
Validate the Subject Claim
Enable auth_jwt_validate_sub on when your authorization logic depends on identifying the token holder. This rejects tokens that lack a sub claim, which may indicate a misconfigured token issuer.
Use HTTPS in Production
JWT tokens in headers or cookies are transmitted in plaintext over HTTP. Always use TLS in production to prevent token interception.
Troubleshooting
401 Unauthorized on All Requests
Check the NGINX error log for specific messages:
tail -f /var/log/nginx/error.log
Common error messages and solutions:
| Error Message | Cause | Solution |
|---|---|---|
failed to find a JWT |
No token in the request | Check auth_jwt_location matches how you send the token |
failed to parse JWT |
Token is malformed or signature verification failed | Verify the key matches the one used to sign the token |
the JWT has expired |
Token’s exp claim is in the past |
Generate a new token with a future expiration |
the JWT does not contain a subject |
auth_jwt_validate_sub on but token has no sub claim |
Add a sub claim to your tokens |
Module Not Loading
If NGINX fails to start after adding load_module:
nginx -t
If you see unknown directive "auth_jwt_enabled", the module is not loaded. Verify the module file exists:
ls /usr/lib64/nginx/modules/ngx_http_auth_jwt_module.so
Claims Not Appearing in Upstream Headers
If auth_jwt_extract_request_claims is configured but your backend doesn’t see the headers:
- Verify the claim names match exactly (case-sensitive)
- Only string claims are supported. Numeric, boolean, array, or object claims are not extracted
- Do not use
proxy_set_headerfor the sameJWT-*headers — it overrides the module’s extracted values with empty strings - Test with curl to confirm the headers appear in the response when using
auth_jwt_extract_response_claims
return Directive Bypasses Authentication
NGINX’s return directive executes in the rewrite phase, which runs before the access phase where JWT validation occurs. If you have:
location /api/ {
auth_jwt_enabled on;
auth_jwt_key ...;
return 200 "OK"; # This bypasses JWT validation!
}
The return statement short-circuits request processing before the module can validate the token. Instead, serve actual files or proxy to an upstream service.
Comparison with nginx-module-jwt
GetPageSpeed also packages nginx-module-jwt, a different JWT module by a different author. Here is how they compare:
| Feature | nginx-module-teslagov-jwt | nginx-module-jwt |
|---|---|---|
| Claim forwarding to upstream | Yes (auth_jwt_extract_request_claims) |
Manual via proxy_set_header |
| Response claim headers | Yes (auth_jwt_extract_response_claims) |
No |
| Cookie-based auth | Yes (COOKIE=name) |
Yes ($cookie_name variable) |
| Login redirect | Yes (auth_jwt_redirect + auth_jwt_loginurl) |
Via error_page + named locations |
| Dynamic enable/disable | Yes (variable support in auth_jwt_enabled) |
Yes ($variable in auth_jwt) |
| Custom authorization rules | No | Yes (auth_jwt_require) |
| Key encodings | Hex, PEM inline, PEM file | Hex, Base64, UTF-8, PEM file |
| Subject validation | Yes (auth_jwt_validate_sub) |
No |
Choose the NGINX TeslaGov JWT module when you need built-in claim forwarding to upstream services and login page redirects. Choose nginx-module-jwt when you need flexible authorization rules via auth_jwt_require or non-hex key encodings.
Conclusion
The NGINX TeslaGov JWT module brings enterprise-grade JWT authentication to NGINX with built-in claim extraction and forwarding. By validating tokens at the edge and passing verified identity as HTTP headers, it simplifies your microservice architecture and eliminates redundant authentication logic in backend services.
For source code and issue tracking, visit the GitHub repository.

