The envoy.filters.http.oauth2 filter currently generates the state parameter by encoding the original request URL (scheme://host/path).
See filter.cc#L846-L847.
This URL is always derived from the request’s internal :scheme and Host/:authority.
When Envoy is deployed behind an external load balancer, the internal Envoy only sees its internal hostname.
As a result, the state sent to the IdP contains the wrong domain, and after successful login the redirect fails.
For the redirect_uri this problem can be solved, because the filter allows configuring it with formatters.
For example:
redirect_uri: "%REQ(x-forwarded-proto?:scheme)%://%REQ(x-forwarded-host?:authority)%/auth/callback"
This way operators can combine forwarded headers with internal values to build the correct external URI.
However, there is no equivalent flexibility for the state parameter. It is always built from Host, with no option to prefer X-Forwarded-Host or align it with the configured redirect_uri.
Schema:

Why this matters
-
In multi-LB setups the mismatch between redirect_uri and state breaks OAuth2 flows.
-
Operators are forced to add brittle Lua/Wasm filters to rewrite Host around the oauth2 filter just to trick it into producing the correct state.
-
A consistent configuration mechanism would allow both redirect_uri and state to be based on the same external domain.
Possible solutions
There are several ways this could be addressed, all with different trade-offs:
In all cases, there should also be a way to restrict allowed base domains (e.g. company.com, *.company.com) to avoid open redirect vulnerabilities.
-
Global flag
Add a simple option like:
use_forwarded_headers: true
allowed_domains:
- example.com
- "*.example.com"
When enabled, both redirect_uri and state would use X-Forwarded-Host / X-Forwarded-Proto (if present), falling back to :authority / :scheme.
Pro: very easy to configure, solves the most common case.
Con: less flexibility.
-
Сonfigurable state_uri with formatters
Allow operators to explicitly configure how the state URL should be built, similar to redirect_uri:
state_uri: "%REQ(x-forwarded-proto?:scheme)%://%REQ(x-forwarded-host?:authority)%"
allowed_domains:
- example.com
- "*.example.com"
Pro: maximum flexibility, consistent with redirect_uri.
Con: more complex, risk of misconfiguring redirect_uri and state differently.
-
Dedicated state_host_header / state_proto_header knobs
Add targeted options like:
state_host_header: x-forwarded-host # default = Host/:authority
state_proto_header: x-forwarded-proto # default = :scheme
allowed_domains:
- example.com
- "*.example.com"
Pro: explicit, minimal changes.
Con: adds more special-purpose fields to the filter config.
Any of these would solve the core problem: today redirect_uri can be fixed via formatters, but the state host and scheme are hardcoded to :authority/:scheme.
Benefits
-
Makes OAuth2 filter usable behind external LBs without hacks.
-
Provides consistency: both redirect_uri and state can reflect the same external domain.
-
Eliminates the need for custom Lua/Wasm filters that temporarily rewrite Host.
Alternatives today
- Insert Lua/Wasm filter before and after
oauth2 to rewrite Host/:authority.
This workaround functions but is fragile, adds latency, and complicates configuration.
The
envoy.filters.http.oauth2filter currently generates thestateparameter by encoding the original request URL (scheme://host/path).See filter.cc#L846-L847.
This URL is always derived from the request’s internal
:schemeandHost/:authority.When Envoy is deployed behind an external load balancer, the internal Envoy only sees its internal hostname.
As a result, the
statesent to the IdP contains the wrong domain, and after successful login the redirect fails.For the
redirect_urithis problem can be solved, because the filter allows configuring it with formatters.For example:
This way operators can combine forwarded headers with internal values to build the correct external URI.
However, there is no equivalent flexibility for the
stateparameter. It is always built fromHost, with no option to preferX-Forwarded-Hostor align it with the configuredredirect_uri.Schema:
Why this matters
In multi-LB setups the mismatch between
redirect_uriandstatebreaks OAuth2 flows.Operators are forced to add brittle Lua/Wasm filters to rewrite
Hostaround the oauth2 filter just to trick it into producing the correct state.A consistent configuration mechanism would allow both
redirect_uriandstateto be based on the same external domain.Possible solutions
There are several ways this could be addressed, all with different trade-offs:
In all cases, there should also be a way to restrict allowed base domains (e.g.
company.com,*.company.com) to avoid open redirect vulnerabilities.Global flag
Add a simple option like:
When enabled, both
redirect_uriandstatewould useX-Forwarded-Host/X-Forwarded-Proto(if present), falling back to:authority/:scheme.Pro: very easy to configure, solves the most common case.
Con: less flexibility.
Сonfigurable
state_uriwith formattersAllow operators to explicitly configure how the state URL should be built, similar to
redirect_uri:Pro: maximum flexibility, consistent with
redirect_uri.Con: more complex, risk of misconfiguring
redirect_uriandstatedifferently.Dedicated
state_host_header/state_proto_headerknobsAdd targeted options like:
Pro: explicit, minimal changes.
Con: adds more special-purpose fields to the filter config.
Any of these would solve the core problem: today
redirect_urican be fixed via formatters, but the statehostand scheme are hardcoded to:authority/:scheme.Benefits
Makes OAuth2 filter usable behind external LBs without hacks.
Provides consistency: both
redirect_uriand state can reflect the same external domain.Eliminates the need for custom Lua/Wasm filters that temporarily rewrite
Host.Alternatives today
oauth2to rewriteHost/:authority.This workaround functions but is fragile, adds latency, and complicates configuration.