Site icon GetPageSpeed

Phantom Token NGINX Module: Secure API Gateway Auth

NGINX Phantom Token Module: Secure API Gateway Authentication

Phantom Token NGINX Module: Secure API Gateway Authentication

The Phantom Token NGINX module implements a powerful security pattern. It protects your APIs by keeping sensitive JWT claims hidden from clients. This module performs token introspection at your NGINX gateway. It exchanges opaque access tokens for full JWTs before forwarding to backends.

What Problem Does Phantom Token NGINX Solve?

The Phantom Token NGINX approach addresses a key security challenge. How do you pass identity data to backends without exposing claims to clients?

Traditional OAuth flows give clients a JWT with user identity and permissions. However, this approach has significant drawbacks:

The Phantom Token pattern solves these issues elegantly. Clients receive an opaque token (a random string). Meanwhile, backends receive a full JWT with all claims. The Phantom Token NGINX module performs this exchange at the gateway.

How the Module Works

When a client sends a request with an opaque token, the module intercepts it. Then it performs these steps:

  1. Token extraction: Extracts the Bearer token from the Authorization header
  2. Introspection request: Sends the token to your OAuth provider per RFC 7662
  3. JWT retrieval: The endpoint validates the token and returns a JWT
  4. Header replacement: Replaces the opaque token with the JWT
  5. Caching: Stores valid responses to avoid repeated calls

As a result, clients stay simple while backends get rich identity data.

Does It Work Without Curity Identity Server?

Yes! While Curity developed this module, it is not vendor-locked. The module uses RFC 7662 Token Introspection. Therefore, it works with many providers:

The key requirement is Accept: application/jwt header support. This tells the endpoint to return a JWT directly. Most modern OAuth servers support this.

Installation

RHEL, CentOS, AlmaLinux, Rocky Linux

First, install the GetPageSpeed repository and the module:

sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-phantom-token

Then, enable the module at the top of /etc/nginx/nginx.conf:

load_module modules/ngx_curity_http_phantom_token_module.so;

Debian and Ubuntu

First, set up the GetPageSpeed APT repository. Then install:

sudo apt-get update
sudo apt-get install nginx-module-phantom-token

On Debian/Ubuntu, the package handles module loading. No load_module is needed.

For details, see the RPM module page or APT module page.

Configuration Directives

The module provides five directives:

phantom_token

Enables or disables the module.

Property Value
Syntax phantom_token on \| off
Default off
Context http, server, location

phantom_token_introspection_endpoint

Specifies the introspection subrequest location. Required when enabled.

Property Value
Syntax phantom_token_introspection_endpoint location_name
Default β€”
Context location

phantom_token_realm

Sets the realm name for 401 WWW-Authenticate headers.

Property Value
Syntax phantom_token_realm "realm_name"
Default api
Context location

phantom_token_scopes

Defines required scopes as a space-separated string.

Property Value
Syntax phantom_token_scopes "scope1 scope2"
Default β€”
Context location

phantom_token_scope

Alternative array-style syntax. Use multiple directives for each scope.

Property Value
Syntax phantom_token_scope "scope_name"
Default β€”
Context location

If both are configured, phantom_token_scopes takes precedence.

Basic Configuration

Here is a minimal Phantom Token NGINX configuration:

load_module modules/ngx_curity_http_phantom_token_module.so;

events {
    worker_connections 1024;
}

http {
    server {
        listen 80;

        # Protected API endpoint
        location /api {
            phantom_token on;
            phantom_token_introspection_endpoint introspect;

            proxy_pass http://127.0.0.1:8080;
        }

        # Introspection subrequest location
        location introspect {
            internal;
            proxy_pass_request_headers off;
            proxy_set_header Accept "application/jwt";
            proxy_set_header Content-Type "application/x-www-form-urlencoded";
            proxy_set_header Authorization "Basic Y2xpZW50X2lkOmNsaWVudF9zZWNyZXQ=";
            proxy_pass http://127.0.0.1:8443/oauth/introspect;
        }
    }
}

The introspection location needs these settings:

Generate Base64 credentials with:

echo -n "client_id:client_secret" | base64

Production Configuration with Caching

For production, enable caching. This reduces introspection calls significantly:

load_module modules/ngx_curity_http_phantom_token_module.so;

events {
    worker_connections 1024;
}

http {
    # Cache zone for phantom tokens
    proxy_cache_path /var/cache/nginx/phantom_token 
                     levels=1:2 
                     keys_zone=phantom_cache:10m 
                     max_size=100m 
                     inactive=60m 
                     use_temp_path=off;

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

        location /api {
            phantom_token on;
            phantom_token_introspection_endpoint introspect;
            phantom_token_realm "api";
            phantom_token_scopes "read write";

            proxy_pass http://127.0.0.1:8080;
        }

        location introspect {
            internal;
            proxy_pass_request_headers off;
            proxy_set_header Accept "application/jwt";
            proxy_set_header Content-Type "application/x-www-form-urlencoded";
            proxy_set_header Authorization "Basic Y2xpZW50X2lkOmNsaWVudF9zZWNyZXQ=";

            # Caching configuration
            proxy_cache phantom_cache;
            proxy_cache_methods POST;
            proxy_cache_key $request_body;
            proxy_ignore_headers Set-Cookie;

            proxy_pass http://127.0.0.1:8443/oauth/introspect;
        }
    }
}

Each caching setting explained:

Keycloak Configuration Example

Configure Phantom Token NGINX with Keycloak:

load_module modules/ngx_curity_http_phantom_token_module.so;

events {
    worker_connections 1024;
}

http {
    proxy_cache_path /var/cache/nginx/phantom_token 
                     levels=1:2 
                     keys_zone=phantom_cache:10m 
                     max_size=100m 
                     inactive=60m;

    server {
        listen 80;

        location /api {
            phantom_token on;
            phantom_token_introspection_endpoint keycloak_introspect;
            phantom_token_realm "my-realm";
            phantom_token_scope "openid";
            phantom_token_scope "profile";
            phantom_token_scope "email";

            proxy_pass http://127.0.0.1:8080;
        }

        location keycloak_introspect {
            internal;
            proxy_pass_request_headers off;
            proxy_set_header Accept "application/jwt";
            proxy_set_header Content-Type "application/x-www-form-urlencoded";
            proxy_set_header Authorization "Basic a2V5Y2xvYWstY2xpZW50OmtleWNsb2FrLXNlY3JldA==";

            proxy_cache phantom_cache;
            proxy_cache_methods POST;
            proxy_cache_key $request_body;
            proxy_ignore_headers Set-Cookie;

            # Keycloak introspection endpoint
            proxy_pass http://127.0.0.1:8180/realms/my-realm/protocol/openid-connect/token/introspect;
        }
    }
}

Note: Keycloak before 25.0 may not fully support JWT responses. See issue #29841.

Handling Large JWTs

For JWTs with many claims, increase buffer sizes:

location introspect {
    internal;
    proxy_pass_request_headers off;
    proxy_set_header Accept "application/jwt";
    proxy_set_header Content-Type "application/x-www-form-urlencoded";
    proxy_set_header Authorization "Basic Y2xpZW50X2lkOmNsaWVudF9zZWNyZXQ=";

    # Larger buffers for big JWTs
    proxy_buffer_size 16k;
    proxy_buffers 4 16k;
    proxy_ignore_headers Set-Cookie;

    proxy_pass http://127.0.0.1:8443/oauth/introspect;
}

The default 4KB buffer may be too small for extensive claims.

Security Best Practices

Protect Introspection Credentials

Store credentials in separate files:

include /etc/nginx/secrets/introspection-auth.conf;

In /etc/nginx/secrets/introspection-auth.conf:

# chmod 600 this file
set $introspection_auth "Basic Y2xpZW50X2lkOmNsaWVudF9zZWNyZXQ=";

Reference it in your configuration:

location introspect {
    internal;
    proxy_set_header Authorization $introspection_auth;
    # ... rest of config
}

Use HTTPS for Introspection

Always use TLS with your OAuth provider:

location introspect {
    internal;
    proxy_ssl_verify on;
    proxy_ssl_trusted_certificate /etc/pki/tls/certs/ca-bundle.crt;
    proxy_pass https://127.0.0.1:8443/oauth/introspect;
}

Limit Cache Duration

Set appropriate cache timeouts:

proxy_cache_valid 200 5m;  # Cache valid responses 5 minutes
proxy_cache_valid any 0;   # Don't cache errors

Comparison with JWT Module

Phantom Token NGINX differs from NGINX JWT Authentication:

Feature Phantom Token JWT Module
Token type Opaque tokens JWTs directly
Validation Introspection endpoint Local signature
Revocation Immediate Requires short expiry
Privacy Clients never see claims Clients have full JWT
Latency Higher (network call) Lower (local)

Choose Phantom Token NGINX when client privacy matters. Also use it for immediate token revocation.

Use Cases

Microservices Architecture

In microservices, the Phantom Token NGINX module acts as your security gateway. All services behind NGINX receive validated JWTs. This eliminates duplicated authentication logic across services. This approach is especially valuable for protecting self-hosted AI services like Ollama or vLLM behind an NGINX reverse proxy, which often lack built-in authentication.

Single Page Applications

SPAs often store tokens in browser storage. With opaque tokens, leaked tokens reveal nothing. Attackers cannot decode user claims from intercepted tokens.

Mobile Applications

Mobile apps face similar risks to SPAs. The Phantom Token approach protects user data even if device storage is compromised.

Testing Your Configuration

Verify your setup with curl:

# Get an opaque token from your OAuth provider
TOKEN="your-opaque-access-token"

# Test the protected endpoint
curl -H "Authorization: Bearer $TOKEN" http://localhost/api/resource

If configured correctly, your backend receives a JWT.

Troubleshooting

401 Unauthorized Responses

Check these items:

  1. Token in Authorization: Bearer <token> header
  2. Correct OAuth server URL
  3. Valid Base64-encoded credentials
  4. Token not expired or revoked

Enable debug logging:

error_log /var/log/nginx/error.log debug;

502 Bad Gateway Errors

Common causes:

  1. Network connectivity to OAuth server
  2. SSL/TLS certificate issues
  3. Incorrect introspection path

Truncated JWT Errors

If logs show β€œbuffer is too small,” increase proxy_buffer_size.

Performance Tips

Minimize latency from introspection:

Log cache performance:

log_format phantom '$remote_addr $request cache:$upstream_cache_status time:$upstream_response_time';

Conclusion

The Phantom Token NGINX module provides robust API gateway security. By implementing this pattern, you protect sensitive user data. Additionally, you maintain OAuth 2.0 compatibility. The module works with any RFC 7662 provider.

For more options, see our guides on JWT authentication, TOTP two-factor auth, and LDAP authentication.

Resources:
– GitHub Repository
– RFC 7662: Token Introspection
– Phantom Token Pattern

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