I have a working design to validate API KEYs in Nginx/njs. Let me show you my final solution and then ask a couple of questions related to issues I ran into along the way (I’d love to learn why these other options didn’t work).
This is my nginx.conf:
js_include validate-api-keys.js;
server {
# We try to always use port 9000 (by convention) for our application entrypoints.
listen 9000;
server_name localhost;
location = /health {
# Always pass the health check to the reverse-proxied server
proxy_pass http://localhost:8080;
}
location / {
# Validate that the api-key header matches the API_KEY_CURRENT or API_KEY_PREVIOUS env var
js_content validate_api_key;
# Q1: the following commented config didn't work (why was it ALWAYS doing the proxy_pass?):
# My goal was: if the above js code didn't already return a 401 response, pass the request to the reversed proxied app
# proxy_pass http://localhost:8080;
}
# This location is internal (only nginx itself can call this location)
location @app-backend {
proxy_pass http://localhost:8080;
}
# Removed this, the subrequest in the JS didn't work:
# location /internalProxyPassToBackend {
# internal;
# proxy_pass http://localhost:8080;
# }
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
This is my validate-api-keys.js (please ignore the "UNCOMMENT TO DEBUG" comments, it’s just a trick to debug: I monitor the returned response headers to see where the code goes, what the values are, etc.):
function validate_api_key(r) {
// UNCOMMENT TO DEBUG: r.headersOut['validate_api_key-start'] = 'Log at START of validate_api_key'
var requestApiKey = r.headersIn["api-key"];
// UNCOMMENT TO DEBUG: r.headersOut['validate_api_key-input-key'] = 'validate_api_key received tpg-api-key header=' + requestApiKey
// Validating API KEY
if (requestApiKey !== "<API_KEY_CURRENT>" && requestApiKey !== "<API_KEY_PREVIOUS>") {
// Return 401 Unauthorized (Access Denied) if key is invalid (doesn't match CURRENT and PREVIOUS key)
// UNCOMMENT TO DEBUG: r.headersOut['validate_api_key-401'] = 'validate_api_key returning 401'
r.return(401, "Access Denied");
} else {
// Send the request to the backend (proxy_pass)
// UNCOMMENT TO DEBUG: r.headersOut['validate_api_key-200'] = 'validate_api_key returning 200'
r.internalRedirect('@app-backend');
// This didn't work (didn't pass the Method.POST):
// r.subrequest('/internalProxyPassToBackend', r.variables)
// .then(reply => r.return(reply.status, reply.responseBody));
// This didn't work (didn't pass the Method.POST):
//r.subrequest('/internalProxyPassToBackend', r.variables,
// function(reply) {
// r.return(reply.status, reply.responseBody);
// return;
// }
// );
}
// UNCOMMENT TO DEBUG: r.headersOut['validate_api_key-end'] = 'Log at END of validate_api_key'
}
Q1: in my nginx.conf, when I had both my js_content and proxy_pass in the same location context, the proxy_pass would also trigger, no matter if my javascript (in js_content) tried to return 401. It would always do the proxy_pass! Why is that? I feel like this has the same idea/root cause as "IFs are evil in location blocks" ?
Q2: in my JavaScript, as you can see, I finally resorted to do an r.internalRedirect (works great!) BUT I first hit my nose on a bunch of walls: what’s wrong with my commented out code? Why isn’t "r.subrequest" passing the Method (in my case POST)? My backend would always complain that it didn’t support "GET" because, obviously, my code wasn’t passing the Method=POST along. Does anyone know how to make that commented-out code work (pass ALL of the arguments of the initial request, Method, etc to the backend)?
Thanks for helping me figure out what was wrong with my initial path!
2
Answers
in nginx configuration you need to specify proxy method
quite old but also was there quite recently.
In your script pass the original request method and body to a subrequest like below
and voilla.
best