v2 🎉
Highly flexible forward auth service for use with an oauth endpoint and a reverse proxy (e.g. traefik).
forward-auth can be configured in two ways, values are applied in following priority (low to high):
environment variables < query params
Note that LISTEN_HOST, LISTEN_PORT, APP_KEY, COOKIE_NAME, COOKIE_AGE, CALLBACK_PORT, and CALLBACK_URL can only be set via environment variables, not query params.
The following options are available:
| Environment Variable | Description | Required | Default |
|---|---|---|---|
LISTEN_HOST |
Host to bind | 0.0.0.0 |
|
LISTEN_PORT |
Port to bind | 8080 |
|
APP_KEY |
Key for cookie signing (min. 32 characters) | ✔ | |
COOKIE_NAME |
Name of the session cookie | __auth |
|
COOKIE_AGE |
Max age of cookie in seconds | 604800 (7 days) |
|
COOKIE_INSECURE |
Allow cookies over insecure (HTTP) connections. Set to true for development, should be false in production |
false |
|
REDIRECT_CODE |
HTTP status code to return when redirectingbecause | 302 |
|
DISCOVERY_URL |
OpenID Connect Discovery URL, used to auto-configure AUTHORIZE_URL, TOKEN_URL, and USERINFO_URL |
||
AUTHORIZE_URL |
OAuth Authorization Request URL (spec) | ✔* | |
TOKEN_URL |
OAuth Access Token Endpoint | ✔* | |
USERINFO_URL |
OpenID Connect UserInfo endpoint, must include sub field |
✔* | |
CLIENT_ID |
OAuth Client ID | ✔ | |
CLIENT_SECRET |
OAuth Client Secret | ✔ | |
ALLOWED_USERS |
Comma-separated list of allowed sub values; empty = anyone |
[] |
|
SCOPES |
Comma-separated OAuth scopes | id |
|
CALLBACK_PORT |
Port for the centralized callback server (see Centralized Callback); disabled when unset | ||
CALLBACK_URL |
Public URL of the centralized callback endpoint, e.g. https://auth-cb.example.com/callback (required when CALLBACK_PORT set) |
||
CALLBACK_CENTRALISED |
Enable/disable centralized callback mode (true/false). Can be overridden per-request via query param (?callback_centralised=false) |
true |
|
LOG_LEVEL |
Log level (DEBUG, INFO, WARN, ERROR, NONE) |
INFO |
* You can either provide individual URLs (AUTHORIZE_URL, TOKEN_URL, USERINFO_URL) OR use DISCOVERY_URL to automatically fetch them from an OpenID Connect provider's discovery document.
When a client is authenticated, forward-auth passes X-Auth-User with the sub and X-Auth-Info with the JSON-encoded USERINFO_URL response. These can be forwarded to your application via the reverse proxy (see examples below).
[!WARNING] > Security Note on
X-Forwarded-*Headers This service relies onX-Forwarded-Host,X-Forwarded-Proto, andX-Forwarded-Uriheaders to construct redirect URLs and identify the original request. It is critical that your reverse proxy is configured to overwrite these headers and not trust the values provided by incoming client requests. Failure to do so can lead to open redirect vulnerabilities. The provided examples for Traefik and Nginx demonstrate a secure configuration.
If your identity provider supports OpenID Connect, you can use DISCOVERY_URL instead of manually configuring the individual endpoint URLs.
The service will automatically fetch the standard endpoints from the provider's discovery document available at /.well-known/openid-configuration.
For example, with Google:
DISCOVERY_URL=https://accounts.google.com
This will automatically configure AUTHORIZE_URL, TOKEN_URL, and USERINFO_URL based on the discovery document.
If you provide any of these URLs explicitly, they will override the values from the discovery document.
By default, the OAuth redirect_uri is derived from the service being accessed (e.g. https://app-a.example.com/_auth/callback), which means every protected service needs its own callback URL registered with the OAuth provider.
Setting CALLBACK_PORT and CALLBACK_URL enables a centralized callback server on a separate port, so you only need to register a single callback URL with your OAuth provider — regardless of how many services are protected.
CALLBACK_PORT=8081
CALLBACK_URL=https://auth-callback.example.com/callback
The secondary server listens on CALLBACK_PORT and exposes a single GET /callback endpoint. It must be reachable at CALLBACK_URL by the browser (i.e. exposed via your reverse proxy or directly).
- An unauthenticated user visits a protected service
- forward-auth redirects to the OAuth provider with
redirect_uriset toCALLBACK_URL - After login, the OAuth provider redirects the browser to
CALLBACK_URL?code=...&state=... - The centralized callback server exchanges the code, fetches user info, and redirects the browser back to the originating service's
/_auth/callback?handoff=<signed-token> - The reverse proxy forwards that to the primary forward-auth, which verifies the signed token, sets the session cookie on the correct domain, and redirects to the original destination
The session cookie is always set on the originating service's domain. CSRF protection is preserved: a signed nonce ties the handoff token to the session cookie that was set before the OAuth redirect.
Note
Per-request config overrides passed as query parameters (e.g. ?allowed_users=alice) are supported — they are evaluated at the primary server, not the callback server. The callback server uses only environment-level configuration.
Expose the callback server alongside the primary auth server and add it as the single registered redirect URI in your OAuth provider:
services:
forward_auth:
image: ghcr.io/mkuhlmann/forward-auth
environment:
- APP_KEY=CHANGE_ME
- DISCOVERY_URL=https://idp.example.com
- CLIENT_ID=clientid
- CLIENT_SECRET=verysecret
- CALLBACK_PORT=8081
- CALLBACK_URL=https://auth-callback.example.com/callback
labels:
# Primary auth endpoint (internal, called by forward-auth middleware)
- 'traefik.http.routers.forward_auth.rule=PathPrefix(`/auth`)'
- 'traefik.http.routers.forward_auth.service=forward_auth'
- 'traefik.http.services.forward_auth.loadbalancer.server.port=8080'
# Centralized callback (public, called by the OAuth provider redirect)
- 'traefik.http.routers.callback.rule=Host(`auth-callback.example.com`)'
- 'traefik.http.routers.callback.service=callback'
- 'traefik.http.services.callback.loadbalancer.server.port=8081'Register https://auth-callback.example.com/callback as the single allowed redirect URI in your OAuth provider. All protected services will share it.
Example docker-compose.yml
version: '3.5'
services:
traefik:
image: traefik:v3
restart: always
command:
- '--providers.docker=true'
- '--providers.docker.exposedbydefault=false'
- '--entrypoints.web.address=:80'
ports:
- 80:80
volumes:
- /var/run/docker.sock:/var/run/docker.sock
forward_auth:
image: ghcr.io/mkuhlmann/forward-auth
restart: unless-stopped
environment:
- APP_KEY=CHANGE_ME
# Either use DISCOVERY_URL
- DISCOVERY_URL=https://example.com
# OR specify individual endpoints
# - AUTHORIZE_URL=https://example.com/oauth/authorize
# - TOKEN_URL=https://example.com/oauth/token
# - USERINFO_URL=https://example.com/oauth/userinfo
- CLIENT_ID=clientid
- CLIENT_SECRET=verysecret
nginx:
image: nginx:mainline-alpine
networks:
- proxy
labels:
- 'traefik.enable=true'
- 'traefik.http.services.nginx.loadbalancer.server.port=80'
- 'traefik.http.routers.nginx.entrypoints=web'
- 'traefik.http.routers.nginx.rule=Host(`private.example.com`)'
- 'traefik.http.middlewares.forward_auth.forwardauth.address=http://forward_auth:8080/auth?allowed_users=ALLOWED_USER_SUB'
- 'traefik.http.middlewares.forward_auth.forwardauth.authResponseHeaders=X-Auth-User,X-Auth-Info'Example nginx config, be sure to set REDIRECT_CODE=403!
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name secret.example.com;
location = /auth {
internal;
proxy_pass http://forward_auth:8080;
proxy_intercept_errors on;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Uri $request_uri;
proxy_pass_request_headers on;
proxy_set_header Content-Length "";
}
location @auth_redirect {
add_header Set-Cookie $auth_cookie;
return 302 $auth_location;
}
location / {
auth_request /auth;
auth_request_set $auth_location $upstream_http_location;
auth_request_set $auth_cookie $upstream_http_set_cookie;
add_header Set-Cookie $auth_cookie;
error_page 403 = @auth_redirect;
error_page 401 = /no_auth;
auth_request_set $auth_user $upstream_http_x_auth_user;
auth_request_set $auth_info $upstream_http_x_auth_info;
proxy_set_header X-Auth-User $auth_user;
proxy_set_header X-Auth-Info $auth_info;
proxy_buffering off;
proxy_pass http://upstream;
proxy_set_header Host $host;
proxy_redirect http:// https://;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
location = /noauth {
internal;
add_header Content-Type text/plain;
return 200 'unauthenticated';
}
}
Pull request are very welcome!