Site icon GetPageSpeed

NGINX TeslaGov JWT Module: Claim Forwarding

NGINX TeslaGov JWT Module: Authentication with Claim Forwarding

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:

  1. Extracts the token from the Authorization: Bearer header or a named cookie
  2. Decodes and verifies the JWT signature using a configured HMAC secret or RSA/ECDSA public key via the libjwt library
  3. Validates expiration by checking the exp claim against the current server time
  4. Optionally validates that the sub (subject) claim is present
  5. Extracts claims into NGINX variables, request headers (forwarded to upstream), or response headers (sent to the client)
  6. 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_module directive 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 alg header 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 as auth_jwt_extract_request_claims sub. The proxy_set_header directive 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.

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:

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:

  1. Verify the claim names match exactly (case-sensitive)
  2. Only string claims are supported. Numeric, boolean, array, or object claims are not extracted
  3. Do not use proxy_set_header for the same JWT-* headers — it overrides the module’s extracted values with empty strings
  4. 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.

D

Danila Vershinin

Founder & Lead Engineer

NGINX configuration and optimizationLinux system administrationWeb performance engineering

10+ years NGINX experience • Maintainer of GetPageSpeed RPM repository • Contributor to open-source NGINX modules

Exit mobile version