2026年11月12日

2026年11月12日

Nginx auth_request: Protect WordPress Admin and APIs with External Auth

This article is a localized edition of the Japanese original. Read the Japanese original

The Nginx auth_request directive sends a subrequest to another URL before processing a request, and decides whether to allow access based on that subrequest's response code. The authentication itself is done by an external auth server (your own service, oauth2-proxy and so on); Nginx only decides whether to let the request through based on the result.

This article covers checking for the module, building locations that put only the WordPress admin area behind authentication, passing user information returned by the auth server to PHP or an API, and handling authentication failures, following the official nginx.org documentation (as of October 2026).

How auth_request works

When a request reaches a location with auth_request, Nginx sends a subrequest to the given URI and handles the response code as follows.

Subrequest response What happens to the original request
2xx Access is allowed and the request is processed normally
401 Denied with 401 (the WWW-Authenticate header returned by the auth server is also passed to the client)
403 Denied with 403
Anything else (auth server down, 5xx, 302, etc.) Treated as an error, resulting in 500

Even if the auth server answers with a 302 to a login page, Nginx treats it as an error. The redirect to the login page is done on the Nginx side with error_page, not by the auth server (see below).

Check that the module is built in

ngx_http_auth_request_module is not built by default when you compile from source; it is enabled with --with-http_auth_request_module. The official packages from nginx.org and the distribution packages for Ubuntu, Debian, RHEL-family systems and others usually include it. You do not need the commercial NGINX Plus.

Check whether your Nginx includes it with this command:

nginx -V 2>&1 | tr ' ' '\n' | grep auth_request
# if --with-http_auth_request_module is shown, you can use it

If nothing is shown, switch to the packages from the official nginx.org repository or rebuild with that option.

What the auth server must do

Prepare an auth server that responds like this:

  • Checks the cookie or Authorization header used for authentication and returns 2xx (200, 204, etc.) if it is valid
  • Returns 401 if the user is not logged in, and 403 if the user is logged in but lacks permission
  • Optionally returns the user name and similar values in response headers such as X-Auth-User

Besides writing your own, oauth2-proxy is existing software that supports this use. Its /oauth2/auth endpoint only returns 202 or 401, and its official documentation describes calling it from Nginx auth_request.

The examples below assume the auth server runs on 127.0.0.1:8081 and decides at /auth. Before configuring Nginx, check the auth server's responses on their own.

# no cookie -> check that 401 is returned
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8081/auth

# with a valid session cookie -> check that 2xx is returned (match the cookie name and value to your auth server)
curl -s -o /dev/null -w '%{http_code}\n' -b 'session=VALID_SESSION_ID' http://127.0.0.1:8081/auth

Basic setup: an internal-only auth location

Make the subrequest target an internal location that cannot be called directly from outside.

location = /_auth {
    internal;                              # direct access from outside returns 404
    proxy_pass http://127.0.0.1:8081/auth;
    proxy_pass_request_body off;           # do not send the body to the auth server
    proxy_set_header Content-Length "";
    proxy_set_header X-Original-URI $request_uri;
    proxy_set_header X-Original-Method $request_method;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_connect_timeout 2s;              # do not wait long when the auth server does not respond
    proxy_read_timeout 5s;
}
  • proxy_pass_request_body off with Content-Length "" is the combination from the official example; it keeps large bodies such as uploads from also being sent to the auth server.
  • The subrequest carries the original request's headers, so you do not need to forward Cookie or Authorization again with proxy_set_header.
  • The auth server does not receive the original URL, so if you want per-URL decisions, add a header yourself, such as X-Original-URI.

Put only the WordPress admin area behind authentication

For a WordPress setup where "anyone can view public pages, and only the admin area and login page go through authentication", you need to watch the location priority.

Nginx gives regular-expression locations (location ~ \.php$) priority over prefix locations (location /wp-admin/). So even if you write location /wp-admin/ { auth_request ...; }, requests for PHP files such as /wp-admin/index.php are handled by the \.php$ location and are not authenticated. Conversely, putting auth_request in the \.php$ location applies authentication to every PHP file, including the front page, which means the whole site.

To avoid this, use a prefix location with ^~ so it takes priority over regular expressions, and nest the PHP location inside it.

server {
    listen 443 ssl;
    http2 on;
    server_name example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    root /var/www/html;
    index index.php;

    # subrequest to the auth server (internal only)
    location = /_auth {
        internal;
        proxy_pass http://127.0.0.1:8081/auth;
        proxy_pass_request_body off;
        proxy_set_header Content-Length "";
        proxy_set_header X-Original-URI $request_uri;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_connect_timeout 2s;
        proxy_read_timeout 5s;
    }

    # public pages (no authentication)
    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    # admin area: ^~ gives it priority over regex locations
    location ^~ /wp-admin/ {
        auth_request /_auth;
        auth_request_set $auth_user $upstream_http_x_auth_user;
        error_page 401 = @login_required;

        location ~ \.php$ {
            try_files $uri =404;
            include fastcgi_params;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            fastcgi_param NGINX_AUTH_USER $auth_user;  # in PHP: $_SERVER['NGINX_AUTH_USER']
            fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        }
    }

    # admin-ajax.php is also called from forms on public pages, so exclude it from authentication
    location = /wp-admin/admin-ajax.php {
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }

    # login page
    location = /wp-login.php {
        auth_request /_auth;
        error_page 401 = @login_required;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }

    # other PHP (no authentication)
    location ~ \.php$ {
        try_files $uri =404;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }

    # unauthenticated (401): send to the auth server's login page
    location @login_required {
        return 302 https://auth.example.com/login?rd=$scheme://$host$request_uri;
    }
}

Key points of this configuration:

  • Exact-match (=) locations take priority over ^~, so you can exclude just /wp-admin/admin-ajax.php from authentication.
  • auth_request is inherited from the parent location by nested locations. To turn it off inside a nested location, write auth_request off;.
  • The PHP-FPM socket path differs by environment (for PHP 8.3 on Ubuntu 24.04 it is /run/php/php8.3-fpm.sock). Check with ls /run/php/.
  • The listen 443 ssl http2; form was deprecated in Nginx 1.25.1 in favor of a separate http2 on;. Older Nginx versions do not support http2 on;, so check your version with nginx -v.
  • The = in error_page 401 = @login_required; replaces the response code with the result of the named location (302 here). 403 is left with the default error page.

You still need to log in to WordPress itself with this setup. Think of auth_request as an extra gate in front of the WordPress login page.

Pass the authentication result to PHP

Values the auth server returns in response headers can be stored in variables with auth_request_set. The response header X-Auth-User is readable as the variable $upstream_http_x_auth_user.

The example above passes it to PHP with fastcgi_param NGINX_AUTH_USER $auth_user;. The name does not start with HTTP_ because Nginx passes the client's request headers to PHP under names starting with HTTP_. With a name such as HTTP_X_AUTH_USER, it would be hard to tell it apart from a client sending its own X-Auth-User header.

<?php
// read the authenticated user name passed by Nginx in code that runs in wp-admin
$auth_user = isset( $_SERVER['NGINX_AUTH_USER'] ) ? sanitize_text_field( wp_unslash( $_SERVER['NGINX_AUTH_USER'] ) ) : '';
if ( '' !== $auth_user ) {
	error_log( 'auth_request user: ' . $auth_user );
}

Protect an API with tokens and return JSON errors

This example puts Nginx in front of a backend separate from WordPress (an API server) and has the auth server validate an Authorization: Bearer token.

upstream token_auth_service {
    server 127.0.0.1:3001;
    keepalive 10;
}

upstream api_backend {
    server 127.0.0.1:9000;
}

server {
    listen 443 ssl;
    http2 on;
    server_name api.example.com;

    ssl_certificate     /etc/letsencrypt/live/api.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;

    location = /_token_auth {
        internal;
        proxy_pass http://token_auth_service/validate;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_pass_request_body off;
        proxy_set_header Content-Length "";
        proxy_set_header X-Original-URI $request_uri;
    }

    location /api/ {
        auth_request /_token_auth;
        auth_request_set $api_user_id $upstream_http_x_user_id;

        proxy_pass http://api_backend;
        proxy_set_header X-User-ID $api_user_id;

        error_page 401 = @api_unauthorized;
    }

    location @api_unauthorized {
        default_type application/json;
        return 401 '{"error":"unauthorized","message":"valid token required"}';
    }
}
  • To reuse connections to the auth server with keepalive, you need proxy_http_version 1.1; and proxy_set_header Connection "";.
  • A header set with proxy_set_header X-User-ID is passed to the backend with the configured value, even if the client sends a header with the same name. However, if the backend can be reached directly without going through Nginx, the header can be spoofed, so make the backend listen on an address unreachable from outside, such as 127.0.0.1.

A caveat when protecting part of the REST API

You can protect part of the WordPress REST API (for example /wp-json/wp/v2/users) with a location, but the WordPress REST API can also be called with a query string, as in /?rest_route=/wp/v2/users. Nginx locations do not look at the query string, so path-only protection can be bypassed. If you want to stop the user list from being public, also control it on the WordPress side, for example with the rest_endpoints filter.

Testing and logging

After changing the configuration, check the syntax and then reload.

sudo nginx -t && sudo systemctl reload nginx

# admin area without authentication -> 302 (to the login page)
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' https://example.com/wp-admin/

# public page -> 200
curl -s -o /dev/null -w '%{http_code}\n' https://example.com/

# admin-ajax.php is excluded from authentication (if WordPress returns 400 or similar, Nginx let it through)
curl -s -o /dev/null -w '%{http_code}\n' https://example.com/wp-admin/admin-ajax.php

Logging the authentication result in the access log lets you trace which requests were denied and why. Put log_format in the http block.

# inside the http block
log_format auth_log '$remote_addr [$time_local] "$request" $status auth=$auth_status user=$auth_user';

# inside a server or location block
auth_request_set $auth_status $upstream_status;
access_log /var/log/nginx/auth.log auth_log;

When the auth server is down, the subrequest fails, 500 is returned, and a connection error is written to error_log. Every request goes through the auth server, so its response time directly affects how fast your site loads.

Summary

  • auth_request allows the request when the subrequest returns 2xx, denies it on 401 or 403, and returns 500 for anything else.
  • Check for the module with nginx -V. The official packages and major distribution packages usually include it; NGINX Plus is not required.
  • To protect only the WordPress admin area, nest the PHP location inside location ^~ /wp-admin/. Applying auth_request to all of \.php$ puts the whole site behind authentication.
  • Receive the result with auth_request_set and pass it to PHP under a name that does not start with HTTP_.
  • Path-only protection can be bypassed, for example with ?rest_route=, so combine it with controls on the WordPress side.

The basic configuration for running WordPress on Nginx is explained in Configuring Nginx for WordPress.

Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)

お気軽にご相談ください

お見積りへ お問い合わせへ
▲