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:
- Privacy concerns: Clients can read and log sensitive user data
- Token leakage: JWTs in browser storage expose claims permanently
- Revocation difficulties: JWTs remain valid until expiration
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:
- Token extraction: Extracts the Bearer token from the
Authorizationheader - Introspection request: Sends the token to your OAuth provider per RFC 7662
- JWT retrieval: The endpoint validates the token and returns a JWT
- Header replacement: Replaces the opaque token with the JWT
- 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:
- Keycloak: Supports token introspection with JWT responses
- Auth0: Implements RFC 7662 introspection
- Okta: Provides token introspection endpoints
- ForgeRock: Full RFC 7662 support
- Any RFC 7662-compliant provider: The protocol is standardized
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_moduleis 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_scopestakes 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:
internal: Prevents external accessproxy_pass_request_headers off: Stops header forwardingAccept: application/jwt: Requests a JWT responseAuthorization: Base64-encoded client credentials
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:
proxy_cache_methods POST: Enables caching for POST requestsproxy_cache_key $request_body: Uses the token as cache keyproxy_ignore_headers Set-Cookie: Prevents cache disabling
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:
- Token in
Authorization: Bearer <token>header - Correct OAuth server URL
- Valid Base64-encoded credentials
- Token not expired or revoked
Enable debug logging:
error_log /var/log/nginx/error.log debug;
502 Bad Gateway Errors
Common causes:
- Network connectivity to OAuth server
- SSL/TLS certificate issues
- Incorrect introspection path
Truncated JWT Errors
If logs show βbuffer is too small,β increase proxy_buffer_size.
Performance Tips
Minimize latency from introspection:
- Enable caching: Avoid repeated calls
- Size cache appropriately: Match your token volume
- Monitor cache hits: Track
$upstream_cache_status - Place OAuth server nearby: Reduce round-trip time
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

