
Stop Building Login Pages: How External Authorization Works
You’ve got a web app. It needs authentication. The reflex is to bolt on a login system — sessions, password hashing, MFA, the whole stack. But there’s a pattern that removes all of that from your application entirely.
External authorization puts an independent service in front of your app. It handles login flows, multi-factor authentication, and session management. Your app just receives the result: a set of HTTP headers telling it who the user is.
How the Flow Works
Every request to your application first passes through a reverse proxy — Caddy, Nginx, Traefik, or similar. Before forwarding anything to your backend, the proxy makes a side request to an authorization service: “Is this person authenticated?”
Two things can happen. If the user has a valid session, the auth service responds with 200 OK and includes identity headers — username, email, display name, group memberships. The proxy injects those headers into the original request and forwards it to your app. If there’s no valid session, the user gets redirected to a login portal where they authenticate (password, MFA, passkeys — whatever the auth service supports). After successful login, they’re sent back and the flow completes normally.
Your backend never sees unauthenticated requests. It never stores passwords. It never renders a login form. It just reads a few headers and knows exactly who’s knocking.
The Trusted Headers
The convention across most external auth systems looks like this:
| Header | What it carries |
|---|---|
Remote-User | Username or unique ID |
Remote-Email | Email address |
Remote-Name | Display name |
Remote-Groups | Comma-separated group list |
Some systems use slightly different naming (Authentik prefixes everything with X-Authentik-, for instance), but the concept is identical. Your app reads headers, extracts user info, and makes authorization decisions based on group membership or identity.
The Tools
Authelia is a lightweight, open-source auth server written in Go. Under 20 MB, under 30 MB RAM in use, and fast. It supports TOTP, WebAuthn, push notifications, and OpenID Connect. It’s designed specifically as a companion to reverse proxies and works out of the box with Caddy, Traefik, Nginx, and HAProxy. If you want something minimal that does the job well, this is it.
Authentik is the heavier alternative. It comes with a full admin UI, user management console, LDAP and SAML support, and social login providers. Think of it as a self-hosted identity platform rather than just an auth gateway. Great if you need to manage users across many services or need enterprise features. More resources required, more capabilities in return.
Vouch Proxy takes a different angle. Instead of being a full identity provider, it delegates authentication to existing OAuth/OIDC providers — Google, GitHub, Keycloak, or anything that speaks the protocol. It’s a thin bridge that translates OAuth sessions into trusted headers for your proxy. Useful when you already have an identity provider and just need the proxy integration layer.
Caddy deserves a mention on the proxy side specifically. Its forward_auth directive makes external authorization a first-class feature. You point it at your auth service, tell it which response headers to copy, and you’re done. No Lua scripts, no complex subrequest configuration. A few lines in the Caddyfile and the entire flow is wired up. Nginx and Traefik can do the same thing, but Caddy makes it noticeably easier.
What Your App Needs to Do
Almost nothing. That’s the appeal.
In Go, you write a middleware that reads the Remote-User header and rejects requests where it’s empty. You parse the groups header if you need role-based access. You store the user info in the request context. A dozen lines of code, no external dependencies.
In PHP, the headers arrive as $_SERVER variables (HTTP_REMOTE_USER, HTTP_REMOTE_EMAIL, etc.). You check if they’re present, extract the values, and carry on. In Laravel, this becomes a middleware class. In plain PHP, it’s a couple of functions.
There’s no token validation, no cryptographic verification, no library to install. The trust model is entirely network-based — which leads to the most important part.
The Security Contract
Trusted headers have no built-in integrity check. There’s no signature. No encryption. If someone can reach your application directly — bypassing the proxy — they can set Remote-User: admin and walk right in.
This means the security of the entire system depends on one thing: your app must be unreachable except through the proxy.
In Docker, this means not publishing your app’s ports. On bare metal, it means binding to 127.0.0.1 or a private network interface. If neither is possible, your app should validate that requests come from the proxy’s IP address and reject everything else.
The reverse proxy also has a job here. It must strip any Remote-* headers from the original client request before making the auth subrequest. Otherwise, a malicious client could send pre-set headers that survive the proxy chain. Caddy’s forward_auth handles this automatically. Nginx and Traefik need explicit configuration to do it right.
Trusted Headers vs. OpenID Connect
External auth systems typically support both approaches. Trusted headers are simpler — no libraries, no token lifecycle, no refresh logic. They work perfectly for apps behind a single proxy, internal tools, dashboards, and self-hosted services.
OpenID Connect is the right choice when your app lives across multiple domains, when it needs to make API calls on behalf of the user, or when you’re building something that third parties will integrate with. It’s more work to implement, but it carries its own proof of authenticity in every token.
The good news is that both Authelia and Authentik support OIDC alongside trusted headers. You can start with headers for simplicity and switch to OIDC later if the requirements grow — without changing your auth infrastructure.
For most self-hosted and internal scenarios, trusted headers are the pragmatic starting point. Simple to implement, simple to debug, and one less thing your application has to worry about.