Proxy Authentication and Redirect Flow#
internal/controller/proxy_controller.go is the single entry point for all reverse-proxy authentication checks. The route GET/ANY /api/auth/:proxy accepts a proxy identifier (traefik, caddy, envoy, nginx) and delegates to proxyHandler, which:
- Resolves a
ProxyContext(host, proto, path, auth module type) from forwarded headers - Runs the policy engine rule chain
- Returns
200(allow),302(browser redirect), or401/403+x-tinyauth-location(non-browser)
Request Context Extraction by Proxy Type#
getProxyContext determines which auth module(s) to try based on the proxy type :
| Proxy | Auth Module(s) | Headers Used |
|---|---|---|
| Traefik / Caddy | ForwardAuth | x-forwarded-host, x-forwarded-uri, x-forwarded-proto |
| Nginx | AuthRequest β ForwardAuth | x-original-url β fallback to forwarded headers |
| Envoy | ExtAuthz β ForwardAuth | x-forwarded-proto + Host + path query param β fallback |
Modules are tried in order; the first to succeed wins .
Path Normalization Before ACL Evaluation#
After resolving the context, getProxyContext sanitizes the path before it is passed to the policy engine:
- Parses
ctx.Pathwithurl.Parse - Rejects paths that contain a host component or don't start with
/ - Strips query parameters β only
upath.Pathis used - Normalizes with
path.Clean
This normalization was added in PR #1055 to close ACL bypass vectors via dot segments (/foo/../admin), percent-encoding (/%61dmin), and query-string tricks (e.g., x-forwarded-uri: /public?next=/admin). An earlier fix in PR #1044 had introduced getRequestPath() per-module to partially address path parsing from x-forwarded-uri, but the comprehensive sanitization landed in PR #1055.
Security implication: Without normalization, a crafted x-forwarded-uri could trick a path allow rule into granting access to protected resources. Malformed URIs now return 400.
ACL Rule Chain and Browser Detection#
proxyHandler runs rules in strict sequence; each short-circuits on a definitive outcome (see Policy Engine for full detail):
RuleIPBypassedβ client IP in bypass list β200, skip authRuleAuthEnabledβ path matchespath.allow(and not blocked) β200, skip authRuleIPAllowedβ IP blocked or not in allow list β403/unauthorizedRuleUserAllowed/RuleOAuthGroup/RuleLDAPGroupβ user/group checks β403or continue- Unauthenticated β
401redirect to/login
PR #1044 reworked RuleAuthEnabled's path matching to use matchPathRule(): plain strings are treated as prefix matches, patterns wrapped in /β¦/ are treated as regexes, and invalid patterns default to EffectDeny (preventing silent bypass).
Browser vs. non-browser response is decided by useBrowserResponse: Nginx always gets 401/403 JSON + x-tinyauth-location header (regardless of User-Agent), while Traefik/Caddy/Envoy check the User-Agent against Chrome|Gecko|AppleWebKit|Opera|Edge .
Forward-Auth Redirect Flow#
When a request is unauthenticated and a browser response is appropriate, the proxy controller builds a login redirect :
redirect_uri = proto://host/path (reconstructed from ProxyContext)
β 302 to {APP_URL}/login?redirect_uri=...&login_for=app
The login_for=app parameter tells the OAuth/login callback to route the post-login redirect to /continue?redirect_uri=... rather than an OIDC flow. On the /continue page, the frontend validates the redirect_uri via isRedirectSafe() and sends the user to the original URL.
login_for is optional since v5.1.2: older NGINX setups (e.g., Swag/LinuxServer configs) that didn't pass login_for were broken in v5.1.0β5.1.1 because the frontend required it to determine the post-login destination. The fix made login_for optional β if absent but redirect_uri is present, the frontend defaults to /continue .
TOTP continuation: After a successful TOTP submission, the frontend must navigate to /continue?redirect_uri=... to complete the flow. A v5.1.0β5.1.1 bug caused the frontend to navigate to /logout instead, affecting all users with 2FA enabled on NGINX setups. Fixed in v5.1.2-beta.3 .
OIDC Callback Redirect URI Construction#
In oidc_controller.go, authorizeComplete() constructs the final redirect URI:
- Parsing
authorizeReq.RedirectURIwithurl.Parse - Calling
cu.Query()to extract existing query parameters - Setting/overwriting
codeand (if non-empty)state - Re-encoding via
cu.RawQuery = q.Encode()
This preserves any pre-existing params in the redirect_uri. Before PR #1003, the code used fmt.Sprintf("%s?%s", ...), which always appended a ? regardless of whether params already existed β causing malformed URLs like ?provider=oidc?code=... for apps such as Hister that require a query param in their callback URL . The same pattern was applied to authorizeError() to preserve callback query params on error responses.
Redirect Safety Validation#
isRedirectSafe() in oauth_controller.go validates any redirect_uri before the frontend executes it. It applies two phases:
- Exact host match β validates the redirect URI host against
APP_URLvia a domain validator (port-aware). Returnstrueif it passes. - Subdomain match (if
ENABLE_SUBDOMAINS=true) β checks whether the redirect hostname ends with.<cookieDomain>or exactly equalscookieDomain.
A failure at both phases causes the frontend to navigate to /logout instead of the intended redirect_uri .
v5.1.0β5.1.1 regression: The Phase 2 check required the redirect hostname to end with ".<cookieDomain>". The root domain itself (e.g., domain.com) never satisfies this when cookieDomain is also domain.com, so protecting a root domain with Tinyauth on a subdomain broke post-login redirect. Fixed in v5.1.2-beta.1 by also allowing an exact match against cookieDomain .