So, this is _super cool_. We were just missing two small pieces plus a bug fix t…o make this pattern possible.
Recently, we found out that there's demand for integrating Caddy with Authelia https://github.com/authelia/authelia/issues/1241, so that Authelia can be used for acting as an auth gateway for apps served by Caddy. The way it's typically done with other proxies is with a built-in feature called [ForwardAuth in Traefik](https://doc.traefik.io/traefik/middlewares/http/forwardauth/) and [auth_request in Nginx](https://nginx.org/en/docs/http/ngx_http_auth_request_module.html). TL;DR, an HTTP request is made to Authelia, and Authelia either responds with a `200` if the request authorized :+1: or with a `401` or redirect if auth is required :-1:
So that got me thinking, `reverse_proxy` can make requests (obviously) and we've recently implemented a quite flexible `handle_response` feature that makes is quite simple to interact with the proxy response and even ignore the response body if we need. So what if we just use `reverse_proxy` to perform ForwardAuth-like functionality? That means we don't need a plugin, and it would work basically out-of-the-box with Caddy.
The key bits that were missing though, is that we need a way to tell the proxy "don't use the request body" and "always make GET requests" so that the request body isn't consumed so it can be actually used by a later HTTP handler. That's pretty easy, so I added `no_body` and `override_method` subdirectives to do this. We'd also need a way let `reverse_proxy` not be a terminal HTTP handler... but... turns out, `handle_response` was already implemented to work that way!
Then I started testing it a bit. Turns out that `handle_response` had a small bug, it would pass the _cloned_ request to subsequent routes; that's bad, because then `header_up` manipulations would become permanent and apply to subsequent requests, and if we used `no_body`, we wouldn't have access to the body, etc. So I rewired things so that the _original_ request is passed through subsequent routes. Fixed!
So here's how I tested this:
```nginx
{
debug
}
:8881 {
log
route {
# pre-check request
reverse_proxy :8882 {
# setup, don't want to consume the body
override_method GET
no_body
# just to prove that the later handlers
# _don't_ see this header (see :8883)
header_up Bar bar-header
# handle the response, set a header on
# the original request so subsequent
# handlers can pass it through
handle_response {
request_header Foo foo-header
}
}
# the "actual" handling, e.g. your app
reverse_proxy :8883
}
}
# pre-check, just responding with a 200 response
:8882 {
log
respond "We're good to go!"
}
# your app
:8883 {
log
respond "{header.Foo} {header.Bar}
{http.request.body}"
}
```
And then making a request like this; a `POST` to prove that `:8882` indeed sees a `GET`, and `:8883` sees the original `POST`, and the body:
```bash
$ curl -v http://localhost:8881 -H "Content-Type: application/json" -d '{"productId": 123456, "quantity": 100}'
* Trying 127.0.0.1:8881...
* Connected to localhost (127.0.0.1) port 8881 (#0)
> POST / HTTP/1.1
> Host: localhost:8881
> User-Agent: curl/7.74.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 38
>
* upload completely sent off: 38 out of 38 bytes
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Length: 51
< Date: Thu, 28 Apr 2022 03:37:48 GMT
< Server: Caddy
< Server: Caddy
< Content-Type: text/plain; charset=utf-8
<
foo-header
{"productId": 123456, "quantity": 100}
* Connection #0 to host localhost left intact
```
And the logs from this:
```
2022/04/28 04:22:21.924 DEBUG http.handlers.reverse_proxy selected upstream {"dial": ":8882", "total_upstreams": 1}
2022/04/28 04:22:21.925 INFO http.log.access handled request {"request": {"remote_ip": "127.0.0.1", "remote_port": "39378", "proto": "HTTP/1.1", "method": "GET", "host": "localhost:8881", "uri": "/", "headers": {"Accept": ["*/*"], "Bar": ["bar-header"], "X-Forwarded-For": ["127.0.0.1"], "X-Forwarded-Proto": ["http"], "User-Agent": ["curl/7.74.0"], "Content-Type": ["application/json"], "X-Forwarded-Host": ["localhost"], "Accept-Encoding": ["gzip"]}}, "user_id": "", "duration": 0.000050308, "size": 17, "status": 200, "resp_headers": {"Server": ["Caddy"], "Content-Type": []}}
2022/04/28 04:22:21.925 DEBUG http.handlers.reverse_proxy upstream roundtrip {"upstream": ":8882", "duration": 0.000680542, "request": {"remote_ip": "127.0.0.1", "remote_port": "41778", "proto": "HTTP/1.1", "method": "GET", "host": "localhost:8881", "uri": "/", "headers": {"Content-Length": ["38"], "Content-Type": ["application/json"], "X-Forwarded-For": ["127.0.0.1"], "X-Forwarded-Proto": ["http"], "X-Forwarded-Host": ["localhost"], "User-Agent": ["curl/7.74.0"], "Accept": ["*/*"], "Bar": ["bar-header"]}}, "headers": {"Server": ["Caddy"], "Date": ["Thu, 28 Apr 2022 04:22:21 GMT"], "Content-Length": ["17"]}, "status": 200}
2022/04/28 04:22:21.925 DEBUG http.handlers.reverse_proxy handling response {"handler": 0}
2022/04/28 04:22:21.925 DEBUG http.handlers.reverse_proxy selected upstream {"dial": ":8883", "total_upstreams": 1}
2022/04/28 04:22:21.925 INFO http.log.access handled request {"request": {"remote_ip": "127.0.0.1", "remote_port": "35452", "proto": "HTTP/1.1", "method": "POST", "host": "localhost:8881", "uri": "/", "headers": {"Content-Type": ["application/json"], "Foo": ["foo-header"], "X-Forwarded-For": ["127.0.0.1"], "Accept-Encoding": ["gzip"], "User-Agent": ["curl/7.74.0"], "Content-Length": ["38"], "Accept": ["*/*"], "X-Forwarded-Host": ["localhost"], "X-Forwarded-Proto": ["http"]}}, "user_id": "", "duration": 0.00009696, "size": 50, "status": 200, "resp_headers": {"Server": ["Caddy"], "Content-Type": []}}
2022/04/28 04:22:21.925 DEBUG http.handlers.reverse_proxy upstream roundtrip {"upstream": ":8883", "duration": 0.000544203, "request": {"remote_ip": "127.0.0.1", "remote_port": "41778", "proto": "HTTP/1.1", "method": "POST", "host": "localhost:8881", "uri": "/", "headers": {"X-Forwarded-Host": ["localhost"], "User-Agent": ["curl/7.74.0"], "Accept": ["*/*"], "Content-Type": ["application/json"], "Content-Length": ["38"], "Foo": ["foo-header"], "X-Forwarded-For": ["127.0.0.1"], "X-Forwarded-Proto": ["http"]}}, "headers": {"Server": ["Caddy"], "Date": ["Thu, 28 Apr 2022 04:22:21 GMT"], "Content-Length": ["50"]}, "status": 200}
2022/04/28 04:22:21.926 INFO http.log.access handled request {"request": {"remote_ip": "127.0.0.1", "remote_port": "41778", "proto": "HTTP/1.1", "method": "POST", "host": "localhost:8881", "uri": "/", "headers": {"Content-Type": ["application/json"], "Content-Length": ["38"], "User-Agent": ["curl/7.74.0"], "Accept": ["*/*"]}}, "user_id": "", "duration": 0.001739929, "size": 50, "status": 200, "resp_headers": {"Server": ["Caddy", "Caddy", "Caddy"], "Date": ["Thu, 28 Apr 2022 04:22:21 GMT", "Thu, 28 Apr 2022 04:22:21 GMT"], "Content-Length": ["50"]}}
```
So, the logs in order:
- Selected `:8882` as the upstream for the pre-check
- The `:8882` server `log`s the request, notice it's a `GET` here and there's no `Content-Length` header
- The `:8882` proxy logs its roundtrip
- The `handle_response` is selected and runs, then continues the handling chain
- Selected `:8883` as the upstream for the "actual" handling of the request
- The `:8883` server `log`s the request, notice it's a `POST` here and `Content-Length` is `38` (my dumb little JSON payload)
- The `:8883` proxy logs its roundtrip
- The `:8881` server finally `log`s the request and the `50` byte response (the `Foo` header value and echoed request body)
I just noticed as I write this though that the last log line has `Server: Caddy` _three_ times :thinking: there might be a bug with the response writer, I'll need to look into this more closely to see what's going on, but interestingly the response in `curl` only has _two_ (which is correct, i.e. `:8881` and `:8883` ultimately should be the only ones manipulating the response).
So with all this out of the way, this means that ForwardAuth can be done purely with `reverse_proxy`. But obviously this is _pretty_ verbose and has a lot of boilerplate, so we'll probably provide a `forward_auth` Caddyfile directive, similarly to `php_fastcgi` which is a shortcut/sugar over `reverse_proxy` to make it nicer to use, with good defaults.