I am using the forward_auth directive to check user authentication state before using reverse_proxy on the main app. Depending on the user’s current state, our authentication server may return a Set-Cookie header to update or reset the user’s authentication related cookies.
In that case, we want to merge that Set-Cookie header with the one received from our reverse_proxy. I have this partially working using the Caddyfile below, however if the authentication server doesn’t return a Set-Cookie header, the end result is that Set-Cookie gets sets to the raw placeholder name (i.e: {http.reverse_proxy.header.Set-Cookie}).
2. Error messages and/or full log output:
No errors, and the logs aren’t super useful since Set-Cookie is redacted. When curling the request, I receive the following headers:
There might be a better way to handle this, but here’s a quick example I put together.
Caddyfile
{
http_port 8080
}
:8080 {
## Authenticate first
forward_auth http://127.0.0.1:8081 {
uri /oauth2/auth
copy_headers {
## Copy X-Access-Token from the response as-is
X-Access-Token
## Preserve Set-Cookie from the AUTH site as X-Access-Cookie request header
Set-Cookie>X-Access-Cookie
}
}
## If we got an X-Access-Cookie from the AUTH site, pass it along
@authCookie header X-Access-Cookie *
handle @authCookie {
header +Set-Cookie {header.X-Access-Cookie}
}
## Regular reverse proxy business
reverse_proxy http://127.0.0.1:8082
}
## Auth Site
:8081 {
header {
X-Access-Token TOKEN8081
Set-Cookie cookie8081=value8081
}
respond 200
}
## Main Site
:8082 {
header /gimmecookie* Set-Cookie cookie8082=value8082
respond "Main Site Alive!"
}
Test results:
## Auth site (8081) sets a cookie, Main site (8082) doesn’t
$ curl http://localhost:8080 -I
HTTP/1.1 200 OK
Content-Length: 16
Content-Type: text/plain; charset=utf-8
Date: Fri, 11 Apr 2025 19:44:57 GMT
Server: Caddy
Server: Caddy
Set-Cookie: cookie8081=value8081
## Both Auth site (8081) and Main site (8082) set cookies
$ curl http://localhost:8080/gimmecookie -I
HTTP/1.1 200 OK
Content-Length: 16
Content-Type: text/plain; charset=utf-8
Date: Fri, 11 Apr 2025 19:46:26 GMT
Server: Caddy
Server: Caddy
Set-Cookie: cookie8081=value8081
Set-Cookie: cookie8082=value8082
And one last thing - just to make sure no one’s spoofing your cookies: instead of using something like X-Access-Cookie as your temporary header name, go with something more random, like X-74A3A1D4-71AC-7C27-BCBB-8221E2930CA9.
It’s just for internal use anyway, never exposed to the client, so a random value helps avoid any accidental or intentional overlap.
Thank you for the responses, your approach seems to be working
I actually was typing up my concern around this as you messaged haha.
Would it be reasonable to use the request_header directive to strip the header on incoming requests before calling forward_auth? My thought is that way no one can pass in their own value so if its defined it could only come from the forward_auth call, and would mean we could use a more human-readable name.
This is sort of what I’m thinking:
:8080 {
request_header -X-Access-Cookie
## Authenticate first
forward_auth http://127.0.0.1:8081 {
uri /oauth2/auth
copy_headers {
## Copy X-Access-Token from the response as-is
X-Access-Token
## Preserve Set-Cookie from the AUTH site as X-Access-Cookie request header
Set-Cookie>X-Access-Cookie
}
}
## If we got an X-Access-Cookie from the AUTH site, pass it along
@authCookie header X-Access-Cookie *
handle @authCookie {
header +Set-Cookie {header.X-Access-Cookie}
}
## Regular reverse proxy business
reverse_proxy http://127.0.0.1:8082
}
You definitely can do that, but you’ll want to watch out for directive order in the Caddyfile - forward_auth has higher priority than request_header, so if you’re not careful, you might strip your own header before it gets passed along
To avoid that, wrap everything in a route block. That way, things are processed in the order you specify.
:8080 {
route {
request_header -X-Access-Cookie
## Authenticate first
forward_auth http://127.0.0.1:8081 {
uri /oauth2/auth
copy_headers {
## Copy X-Access-Token from the response as-is
X-Access-Token
## Preserve Set-Cookie from the AUTH site as X-Access-Cookie request header
Set-Cookie>X-Access-Cookie
}
}
## If we got an X-Access-Cookie from the AUTH site, pass it along
@authCookie header X-Access-Cookie *
header @authCookie +Set-Cookie {header.X-Access-Cookie}
}
## Regular reverse proxy business
reverse_proxy http://127.0.0.1:8082
}
That’s totally fine too. I’d still lean toward something a bit random, like X-Access-Cookie-wkQE5, just to avoid any accidental collisions. But if you’re sure X-Access-Cookie won’t be used anywhere else in your setup, it’s probably not a big deal. Up to you!
Since Set-Cookie isn’t a request header (it should never come from the client), we can just strip it right away and skip the whole renaming thing. And with route, everything stays nice and simple.
I’m wondering because the Caddyfile I would like to apply this to is kind of complex and I would need to do a fair amount of research before making the switch from handle->route.
This seems like it is working for me, and I think that makes sense based on the order of directives you linked; Just hoping for a sanity check haha.