I set up a simple docker-compose deployment with a nginx default.conf.template.
docker-compose.yml
services:
nginx:
build: ./nginx
ports:
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- ./nginx/default.conf.template:/etc/nginx/templates/default.conf.template
environment:
NGINX_HOST: localhost
NGINX_PORT: 80
BACKEND1_HOST: backend1
BACKEND1_PORT: 8081
BACKEND2_HOST: backend2
BACKEND2_PORT: 8082
BACKEND3_HOST: backend3
BACKEND3_PORT: 8083
INTROSPECTION_CLIENT_ID: api-gateway-client
INTROSPECTION_CLIENT_SECRET: Password1
INTROSPECTION_ENDPOINT: http://localhost:8443//oauth/v2/oauth-introspect
depends_on:
- backend1
- backend2
- backend3
backend1:
build:
context: ./backend1
ports:
- "8081:8081"
backend2:
build:
context: ./backend2
ports:
- "8082:8082"
backend3:
build:
context: ./backend3
ports:
- "8083:8083"
nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
# phantom module
load_module modules/ngx_curity_http_phantom_token_module.so;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/*.conf;
}
default.conf.template
proxy_cache_path cache levels=1:2 keys_zone=api_cache:10m max_size=10g inactive=60m use_temp_path=off;
server {
server_name ${NGINX_HOST};
listen ${NGINX_PORT};
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
location /service1/ {
proxy_pass http://${BACKEND1_HOST}:${BACKEND1_PORT};
phantom_token on;
phantom_token_client_credential ${INTROSPECTION_CLIENT_ID} ${INTROSPECTION_CLIENT_SECRET};
phantom_token_introspection_endpoint curity;
phantom_token_scopes phone;
}
location /service2/ {
proxy_pass http://${BACKEND2_HOST}:${BACKEND2_PORT};
phantom_token on;
phantom_token_client_credential ${INTROSPECTION_CLIENT_ID} ${INTROSPECTION_CLIENT_SECRET};
phantom_token_introspection_endpoint curity;
phantom_token_scopes phone;
}
location /service3/ {
proxy_pass http://${BACKEND3_HOST}:${BACKEND3_PORT}/hello;
phantom_token on;
phantom_token_client_credential ${INTROSPECTION_CLIENT_ID} ${INTROSPECTION_CLIENT_SECRET};
phantom_token_introspection_endpoint curity;
phantom_token_scopes read;
}
location curity {
proxy_pass ${INTROSPECTION_ENDPOINT};
proxy_cache_methods POST;
proxy_cache api_cache;
proxy_cache_key $request_body;
proxy_ignore_headers Set-Cookie;
}
}
I then request a token with:
curl -u 'api-client:Password1' -X POST http://localhost:8443/oauth/v2/oauth-token -d grant_type=client_credentials -d scope=phone
Notice that the scope phone is used.
Now I take the token from the oauth-token response and add it as authorization header to make a request to the service3:
curl -H "Authorization: Bearer _0XBPWQQ_16d5e094-c3b4-4a40-8302-ea19ed982860" http://localhost:80/service3/
What I was expecting was that the nginx phantom token module would reject my request to service3, because the scope "read" is not present in the token, but it does not. Instead the request is reverse proxied to the service3 and the backend handler is called.
According to the docs: https://github.com/curityio/nginx_phantom_token_module?tab=readme-ov-file#phantom_token_scopes
Am I right in assuming that the request should not be forwarded if the scope read is not included in the jwt ?
2
Answers
Many thanks Michal for pointing this out to me.
You are right regarding validating the parameters in the API.
I tested the flow with the lua plugin and openresty. Works like a charm.
Not in this plugin, no. This is the description from the docs:
The plugin uses the value from the
phantom_token_scopes
setting only when the client does not provide an access token at all. The value from this setting is used to populate the response’sWWW-Authenticate
header. Scopes are not validated in the gateway by this plugin.If you are able to switch to Lua plugins, you can use this version:
https://github.com/curityio/nginx-lua-phantom-token-plugin?tab=readme-ov-file#scope As you can see, the Lua plugin performs validation of the scope parameter in the gateway.
Remember that even if you validate the parameter in the gateway, your APIs should validate it again. The gateway validation should be treated as a fail-fast solution that limits unwanted traffic. Your APIs should not trust incoming traffic and validate all the parameters themselves, in case someone manages to circumvent your API gateway.