NGINX subrequest-Authentication: Symfony and Cache-Control

Gaylord Aulke
3 min readJul 6, 2020

--

Today i wanted to write a authentication script that works with the NGINX http_auth_request_module.

Concept: NGINX is a proxy in front of the REST endpoints. The Auth sub request endpoint is called for every request, before the actual backend gets called. It has to fetch information from the request (Session Cookie, JWT, whatever) and check user authentication. Then it returns things like user-id, account-id etc. in response headers. NGINX can then forward this information to the individual backend services by adding them to the request. In our case as additional headers. The services can then rely on information like user-id, roles, group membership etc. in the header of every request.

Neat idea, but there is one problem: When the auth request is called as a subrequest before any of the backend requests are invoked, this can produce a lot of overhead. In our case up to 12 auth requests for one page load.

Fortunately, NGINX can cache the sub request responses. And we can manipulate the cache key. So we use PHP’s session cookie as a cache key and let NGINX cache for 1 minute. This way, the auth sub request will only be made once per minute per logged-in user. All i have to do is tell NGINX to prepare a cache with a configuration like:

proxy_cache_path cache/  keys_zone=auth_cache:1m;

And then in my auth-proxy configuration (for the sub request) i just add the corresponding cache config like:

proxy_cache auth_cache;proxy_cache_valid 200 204 1m;proxy_cache_key "$cookie_PHPSESSID";

(complete config see below)

So the auth request just needs to get the current user from the session, then respond with headers for user_id etc. and also return a Cache-Control header allowing for 1 minute caching of the auth response.

And exactly that did not work out. Even after setting Cache-Control data into the Response Object as described here, Symfony always sent Cache-Control: max-age 0, private, must-revalidate .

It took me some time to find why: Symfony assumes that once you used the session in a request (in our case to determine the current user), the data generated by the request are user specific. You do not want anyone to cache user specific results. This is why the AbstractSessionListener class hooks into the kernel.response event and overwrites your Cache-Control settings.

In our case we have the session cookie as a cache key, so we can do some caching, but Symfony does not know that. From the source code, i could read what needs to be done here:

* In addition, if the session has been started it overrides the Cache-Control
* header in such a way that all caching is disabled in that case.
* If you have a scenario where caching responses with session information in
* them makes sense, you can disable this behaviour by setting the header
* AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER on the response.

So, adding this fake response header will prevent Symfony from overwriting your Cache-Control Response headers (which usually is not a good idea, mind you. We can do this here, because we control the cache-key of the calling party).

Putting it all together, the NGINX config now looks like this:

The Login and Logout functions can be implemented with the standard symfony security package (form login as described here).

The important part here was to include login and auth pathes into one firewall and to have a ACL for the auth route:

The final component was the actual Authentication-Action in the Controller. As mentioned above, this needs to include Cache-Control response headers and in order to actually return them, some AbstractSessionListener Magic:

Now our Authentication Proxy can provide stateless authentication information to a set of backend services. The services are independent on the authentication mechansim and things should be quite simple, secure and fast.

--

--