NGINX subrequest-Authentication: Symfony and Cache-Control

Today I wanted to write a authentication script that works with the NGINX http_auth_request_module.  docs.nginx.com/nginx/admin-guide/security-controls/configuring-subrequest-authentication/

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.

Detail Solution

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 solution for NGINX

Check upgrade.md for your project


# ddev default (PHP project type) config

  # If you want to take over this file and customize it, remove the line above
  # and ddev will respect it and won't overwrite the file.
  # See https://ddev.readthedocs.io/en/stable/users/extend/customization-extendibility/#providing-custom-nginx-configuration

  server {
  listen 80 default_server;
  listen 443 ssl default_server;

  root /var/www/html/public;

  ssl_certificate /etc/ssl/certs/master.crt;
  ssl_certificate_key /etc/ssl/certs/master.key;

  include /etc/nginx/monitoring.conf;

  index index.php index.htm index.html;

  # Disable sendfile as per https://docs.vagrantup.com/v2/synced-folders/virtualbox.html
  sendfile off;
  error_log /dev/stdout info;
  access_log /var/log/nginx/access.log;

  location / {
  absolute_redirect off;
  try_files $uri $uri/ /index.php?$query_string;

  location @rewrite {
  # For D7 and above:
  # Clean URLs are handled in drupal_environment_initialize().
  rewrite ^ /index.php;

  # pass the PHP scripts to FastCGI server listening on socket
  location ~ \.php$ {
  try_files $uri =404;
  fastcgi_split_path_info ^(.+\.php)(/.+)$;
  fastcgi_pass unix:/run/php-fpm.sock;
  fastcgi_buffers 16 16k;
  fastcgi_buffer_size 32k;
  fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
  fastcgi_param SCRIPT_NAME $fastcgi_script_name;
  fastcgi_index index.php;
  include fastcgi_params;
  fastcgi_intercept_errors off;
  # fastcgi_read_timeout should match max_execution_time in php.ini
  fastcgi_read_timeout 10m;
  fastcgi_param SERVER_NAME $host;
  fastcgi_param HTTPS $fcgi_https;

  # Expire rules for static content

  # Media: images, icons, video, audio, HTC
  location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ {
  expires 1M;
  access_log off;
  add_header Cache-Control "public";

  # Prevent clients from accessing hidden files (starting with a dot)
  # This is particularly important if you store .htpasswd files in the site hierarchy
  # Access to `/.well-known/` is allowed.
  # https://www.mnot.net/blog/2010/04/07/well-known
  # https://tools.ietf.org/html/rfc5785
  location ~* /\.(?!well-known\/) {
  deny all;

  # Prevent clients from accessing to backup/config/source files
  location ~* (?:\.(?:bak|conf|dist|fla|in[ci]|log|psd|sh|sql|sw[op])|~)$ {
  deny all;

  include /etc/nginx/common.d/*.conf;
  include /mnt/ddev_config/nginx/*.conf;