Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for NGINX By James Joyner IV · · 11 min read

AI-Assisted NGINX auth_request for SSO and Forward Auth

Protect any backend with NGINX auth_request and an external auth service: the subrequest flow, passing identity headers, handling 401 vs 403, and validating it actually blocks.

  • #nginx
  • #ai
  • #auth_request
  • #sso

The cleanest way I’ve found to bolt authentication onto a backend that has none is NGINX’s auth_request module. You point it at a small auth service, and before every request to the protected app, NGINX makes an internal subrequest to that service and only proceeds if it returns a 2xx. It’s the mechanism behind oauth2-proxy, Authelia, and most “forward auth” setups. I lean on an AI assistant to draft the config because the subrequest flow has a few non-obvious moving parts — but I verify the block actually blocks, because a misconfigured auth_request fails open in a way that’s easy to miss.

This guide walks through the pattern end to end, with AI doing the drafting and me doing the validating.

How auth_request works

The flow is worth internalizing before you touch config, because it explains every directive:

  1. A request arrives for a protected location.
  2. NGINX pauses it and fires an internal subrequest to your auth endpoint.
  3. If the auth service returns 2xx, the original request proceeds to the backend.
  4. If it returns 401 or 403, NGINX rejects the original request with that status.
  5. Any other status (500, timeout) is treated as an error.

The auth service can be anything that speaks HTTP and reads the incoming cookies or headers — a session validator, an oauth2-proxy, a JWT checker. NGINX doesn’t care how it decides; it only cares about the status code.

Drafting the config with AI

I start with a prompt that describes the flow rather than asking for “an auth config,” because the latter gets you HTTP basic auth instead:

Draft an NGINX config using the auth_request module. Protected app is on 127.0.0.1:8080. The auth service is on 127.0.0.1:4180 at /auth/verify and returns 2xx for allowed, 401 for unauthenticated. Pass the auth service’s response headers (X-User, X-Email) on to the backend. Comment the subrequest flow. Output only config.

Here’s the result after I read it line by line:

server {
    listen 443 ssl;
    server_name app.example.com;

    ssl_certificate     /etc/ssl/certs/app.example.com.pem;
    ssl_certificate_key /etc/ssl/private/app.example.com.key;

    # The internal auth subrequest target — not reachable directly
    location = /auth/verify {
        internal;
        proxy_pass http://127.0.0.1:4180/auth/verify;

        # The auth service doesn't need the request body
        proxy_pass_request_body off;
        proxy_set_header Content-Length "";

        # Forward what the auth service needs to make a decision
        proxy_set_header Host             $host;
        proxy_set_header X-Original-URI   $request_uri;
        proxy_set_header X-Original-Method $request_method;
        proxy_set_header Cookie           $http_cookie;
    }

    location / {
        # Run the subrequest before serving this location
        auth_request /auth/verify;

        # Capture identity headers from the auth response...
        auth_request_set $user  $upstream_http_x_user;
        auth_request_set $email $upstream_http_x_email;

        # ...and pass them to the real backend
        proxy_set_header X-User  $user;
        proxy_set_header X-Email $email;

        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host             $host;
        proxy_set_header X-Real-IP        $remote_addr;
        proxy_set_header X-Forwarded-For  $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Three details an AI draft gets wrong often enough to check every time:

  • internal; on the auth location is essential. Without it, anyone can hit /auth/verify directly, and worse, it can become a bypass surface. Confirm it’s there.
  • auth_request_set is the only way to pull identity out of the auth response and hand it to the backend. NGINX doesn’t forward those headers automatically. If your app shows “anonymous” for every logged-in user, this is why.
  • proxy_pass_request_body off stops NGINX from streaming the (possibly large) request body to the auth service, which doesn’t need it. Leaving it on can stall big POSTs.

Handling 401 versus 403 for humans

A raw auth_request rejection returns a bare 401, which is fine for an API but useless for a browser — the user just sees a blank page. For human-facing apps you usually want to redirect unauthenticated users to a login page. The pattern is an error_page that catches the 401:

location / {
    auth_request /auth/verify;
    error_page 401 = @login_redirect;
    # ... rest as above
}

location @login_redirect {
    # Send them to the SSO login, preserving where they wanted to go
    return 302 https://auth.example.com/login?rd=$scheme://$host$request_uri;
}

The distinction matters: 401 means “you’re not authenticated” (redirect to login), while 403 means “you’re authenticated but not allowed” (show a forbidden page, don’t loop them through login). When I ask AI to handle this, I’m explicit about which status means what, because it tends to collapse both into a single redirect that traps forbidden users in an infinite login loop.

Validate, then prove it blocks

Same gate as always:

sudo nginx -t
sudo nginx -s reload

But nginx -t only checks syntax. The dangerous failure here is failing open — a config that lets everyone through because the auth subrequest is misrouted. So I test both paths explicitly:

# Without a valid session: must be rejected (401 or a redirect)
curl -I https://app.example.com/

# With a valid session cookie: must reach the backend (200)
curl -I -H "Cookie: session=VALID_TOKEN" https://app.example.com/

If the unauthenticated request returns 200, the gate is open and you have a real security hole — not a syntax error NGINX would have caught. That’s the whole reason a human stays in the loop.

Where AI fits

The AI drafted the subrequest plumbing, remembered proxy_pass_request_body off, and explained auth_request_set when I asked why my identity headers were empty. What it did not do reliably was get the internal; directive right on the first pass, or distinguish 401-redirect from 403-forbid without me spelling it out — and both of those are the difference between a working gate and a broken one. Draft with AI, but you verify that the unauthenticated request actually gets turned away.

For more in this vein, the AI for NGINX category has the related patterns, the NGINX config security audit prompt is a good second pass over any auth config you generate, and the prompt library covers the reverse-proxy headers this builds on. Let AI draft the subrequest, validate with nginx -t, and confirm the lock holds before you ship it.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.