- Serve multiple domains (RP IDs) from one instance: host-based dispatch, per-domain credentials and sessions, domains managed at runtime in the admin UI — previously one RP per instance - Cross-domain sign-in via Related Origin Requests: per-domain related-origins list with a served .well-known/webauthn document - Explicit per-domain origin lists with shell-glob wildcards (**. for apex + any subdomain depth, *. for one level), editable in the admin UI with validation and self-lockout guards - Per-domain auth hosts: the account/admin UI can live on a different host per domain, no longer confined to subdomains of a single RP - CLI: 'paskia init <rp-id [rp-name]' initializes or adds a domain to an existing database; 'paskia migrate' converts legacy databases BREAKING CHANGES (v2.0): - Database schema: config is now per-domain and credentials/sessions carry an rp_id — existing databases must be converted with 'paskia migrate' - Origins are now explicit: main implicitly allowed every subdomain of the RP; configure '**.' origins to reproduce that behavior - CLI: the flat '--rp-id/--rp-name/--origin/--auth/--save' flags are replaced by the 'init' and 'migrate' subcommandsReviewed-on: #4
3.6 KiB
Forward-Auth Proxy Guides
/auth/api/forward · Trusted headers
These guides show how to protect a backend application with Paskia using the forward-auth (also called "external authentication") mechanism. The reverse proxy asks Paskia whether a request is allowed before forwarding it to the protected service.
For details about the endpoint the proxy calls, see /auth/api/forward. For the headers your backend receives on successful requests, see Trusted Headers.
Available guides
- Caddy — fully supported with ready-to-use snippets (
auth/setupandauth/require). - Nginx — using the
auth_requestmodule. - Traefik — using the
ForwardAuthmiddleware. - Apache APISIX — using the
forward-authplugin. - Envoy — using the
ext_authzHTTP filter. - HAProxy — using a Lua auth request.
Common requirements
No matter which proxy you use, the auth subrequest must:
- Be sent to
GET /auth/api/forwardon the Paskia backend. By default Paskia listens onlocalhost:4401; set theAUTH_UPSTREAMenvironment variable in our Caddy snippets, or point your proxy at wherever Paskia is running. - Include the query parameters Paskia needs for access control:
perm— required permission scope, repeatable (e.g.perm=myapp:login). See perm argument.max_age— how recently the user must have authenticated (e.g.max_age=5min). See max_age argument.public=1— optional; allow public access (anonymous visitors and users missingpermpass through, marked with aRemote-Publicheader instead of a 401/403). See public access.
- Forward these request headers from the original client request:
Host— the site the user is visiting.Cookie— the session cookie, normally__Host-paskia.X-Forwarded-Method— the original HTTP method (e.g.GET,POST).X-Forwarded-Uri— the original path and query string (e.g./reports?foo=bar).Accept— decides whether a 401/403 response should be HTML (browser) or JSON (API/fetch).
- Strip hop-by-hop headers (
Connection,Upgrade,Transfer-Encoding,Keep-Alive,Proxy-Connection,TE) from the auth subrequest. The auth check is a plain HTTP request and must not carry WebSocket/body framing headers. - On a
204 No Contentresponse, copy theRemote-*response headers to the request that is forwarded to the protected backend. The headers are the whole point of the auth check. Withpublic=1, also copyRemote-Public— it marks whether the request isauthenticated,forbiddenoranonymous, and the backend must check it. - On a 401/403 response, send Paskia's response back to the client without contacting the protected backend. (With
public=1these only occur for reauth requirements.) - Also proxy the
/auth/path prefix to Paskia so the login/profile UI, API endpoints, and WebSockets are reachable. Paskia's WebSocket endpoints needUpgradeandConnectionheaders passed through for that path.
Backend usage
After the proxy forwards the request, your backend can read the trusted headers. For example, in Python/FastAPI:
user_id = request.headers.get("Remote-User")
org_id = request.headers.get("Remote-Org")
permissions = request.headers.get("Remote-Groups", "").split(",")
Only trust headers that come from the proxy; never trust Remote-* headers that arrive directly from the internet. Your proxy configuration should strip any client-supplied Remote-* headers before the auth check.