Using forward_auth and writing my own authenticator in PHP

1. Caddy version (caddy version):

v2.5.1 h1:bAWwslD1jNeCzDa+jDCNwb8M3UJ2tPa8UZFFzPVmGKs=

2. How I run Caddy:

a. System environment:

Arch Linux
Linux 5.18.7-arch1-1 #1 SMP PREEMPT_DYNAMIC Sat, 25 Jun 2022 20:22:01 +0000 x86_64 GNU/Linux

PHP 8.1.7 fpm

b. Command:

cd /srv/authtest ; ./caddy run

c. Service/unit/compose file:

N/A

d. My complete Caddyfile or JSON config:

{
	debug
	http_port 1081 # Doesn't matter, we're not using it here.
}

# The site that I want to authenticate for.

localhost:8080 {
	forward_auth 127.0.0.1:8081 {
		uri /auth.php
	}
	root * def
	file_server
}

# My authentication service, that should only be accessible from
# the local host.

http://127.0.0.1:8081 {
	bind 127.0.0.1
	root * auth
	file_server
	php_fastcgi unix//var/run/php-fpm/php-fpm.sock
}

# vim: ts=8 sw=8 noexpandtab

3. The problem I’m having:

I’m looking to write my own authenticator in PHP, leveraging the new forward_auth directive.

To begin with, the auth.php file is just:

<?php
http_response_code(401);
echo "<h1>Auth site</h1>";

So when I access https://localhost:8080/ I expect a 401 response with the one line <h1>Auth site</h1> in the body. Instead I get a 200 response and no body.

Am I missing something with how forward_auth is suppose to work?

All files (sans Caddy executable) (1351 bytes): authtest.zip

See logs below.

4. Error messages and/or full log output:

Output from curl -v https://localhost:8080/ is:

*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
* ALPN: offers h2
* ALPN: offers http/1.1
*  CAfile: /etc/ssl/certs/ca-certificates.crt
*  CApath: none
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256
* ALPN: server accepted h2
* Server certificate:
*  subject: [NONE]
*  start date: Jun 27 05:38:36 2022 GMT
*  expire date: Jun 27 17:38:36 2022 GMT
*  subjectAltName: host "localhost" matched cert's "localhost"
*  issuer: CN=Caddy Local Authority - ECC Intermediate
*  SSL certificate verify ok.
* Using HTTP2, server supports multiplexing
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* h2h3 [:method: GET]
* h2h3 [:path: /]
* h2h3 [:scheme: https]
* h2h3 [:authority: localhost:8080]
* h2h3 [user-agent: curl/7.83.1]
* h2h3 [accept: */*]
* Using Stream ID: 1 (easy handle 0x562e32f17fa0)
> GET / HTTP/2
> Host: localhost:8080
> user-agent: curl/7.83.1
> accept: */*
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
< HTTP/2 200
< date: Mon, 27 Jun 2022 06:36:23 GMT
< date: Mon, 27 Jun 2022 06:36:23 GMT
< server: Caddy
< server: Caddy
< server: Caddy
< content-length: 0
<
* Connection #0 to host localhost left intact

Corresponding caddy service log output is:

2022/06/27 06:35:41.206 INFO    using adjacent Caddyfile
2022/06/27 06:35:41.208 INFO    admin   admin endpoint started  {"address": "tcp/localhost:2019", "enforce_origin": false, "origins": ["//localhost:2019", "//[::1]:2019", "//127.0.0.1:2019"]}
2022/06/27 06:35:41.208 INFO    http    enabling automatic HTTP->HTTPS redirects        {"server_name": "srv1"}
2022/06/27 06:35:41.208 INFO    tls.cache.maintenance   started background certificate maintenance      {"cache": "0xc00040e930"}
2022/06/27 06:35:41.209 DEBUG   http    starting server loop    {"address": "[::]:1081", "http3": false, "tls": false}
2022/06/27 06:35:41.209 DEBUG   http    starting server loop    {"address": "127.0.0.1:8081", "http3": false, "tls": false}
2022/06/27 06:35:41.209 DEBUG   http    starting server loop    {"address": "[::]:8080", "http3": false, "tls": true}
2022/06/27 06:35:41.209 INFO    http    enabling automatic TLS certificate management   {"domains": ["localhost"]}
2022/06/27 06:35:41.210 WARN    tls     stapling OCSP   {"error": "no OCSP stapling for [localhost]: no OCSP server specified in certificate", "identifiers": ["localhost"]}
2022/06/27 06:35:41.210 DEBUG   tls.cache       added certificate to cache      {"subjects": ["localhost"], "expiration": "2022/06/27 17:38:36.000", "managed": true, "issuer_key": "local", "hash": "21f5de718760485841efeb9e35a21d367ba5ed086427e58c81f77756fb5b5a39", "cache_size": 1, "cache_capacity": 10000}
2022/06/27 06:35:41.210 INFO    tls     cleaning storage unit   {"description": "FileStorage:/home/evmcl/.local/share/caddy"}
2022/06/27 06:35:41.210 INFO    tls     finished cleaning storage units
2022/06/27 06:35:41.228 INFO    pki.ca.local    root certificate is already trusted by system   {"path": "storage:pki/authorities/local/root.crt"}
2022/06/27 06:35:41.230 INFO    autosaved config (load with --resume flag)      {"file": "/home/evmcl/.config/caddy/autosave.json"}
2022/06/27 06:35:41.230 INFO    serving initial configuration
2022/06/27 06:36:23.827 DEBUG   tls.handshake   choosing certificate    {"identifier": "localhost", "num_choices": 1}
2022/06/27 06:36:23.827 DEBUG   tls.handshake   default certificate selection results   {"identifier": "localhost", "subjects": ["localhost"], "managed": true, "issuer_key": "local", "hash": "21f5de718760485841efeb9e35a21d367ba5ed086427e58c81f77756fb5b5a39"}
2022/06/27 06:36:23.827 DEBUG   tls.handshake   matched certificate in cache    {"subjects": ["localhost"], "managed": true, "expiration": "2022/06/27 17:38:36.000", "hash": "21f5de718760485841efeb9e35a21d367ba5ed086427e58c81f77756fb5b5a39"}
2022/06/27 06:36:23.829 DEBUG   http.handlers.reverse_proxy     selected upstream       {"dial": "127.0.0.1:8081", "total_upstreams": 1}
2022/06/27 06:36:23.829 DEBUG   http.handlers.reverse_proxy     upstream roundtrip      {"upstream": "127.0.0.1:8081", "duration": 0.000334876, "request": {"remote_ip": "127.0.0.1", "remote_port": "33440", "proto": "HTTP/2.0", "method": "GET", "host": "localhost:8080", "uri": "/auth.php", "headers": {"Accept": ["*/*"], "X-Forwarded-For": ["127.0.0.1"], "X-Forwarded-Proto": ["https"], "X-Forwarded-Host": ["localhost:8080"], "X-Forwarded-Method": ["GET"], "X-Forwarded-Uri": ["/"], "User-Agent": ["curl/7.83.1"]}, "tls": {"resumed": false, "version": 772, "cipher_suite": 4865, "proto": "h2", "server_name": "localhost"}}, "headers": {"Content-Length": ["0"], "Server": ["Caddy"], "Date": ["Mon, 27 Jun 2022 06:36:23 GMT"]}, "status": 200}
2022/06/27 06:36:23.829 DEBUG   http.handlers.reverse_proxy     handling response       {"handler": 1}

5. What I already tried:

I have confirmed that my auth.php end-point works with curl -v http://127.0.0.1:8081/auth.php:

*   Trying 127.0.0.1:8081...
* Connected to 127.0.0.1 (127.0.0.1) port 8081 (#0)
> GET /auth.php HTTP/1.1
> Host: 127.0.0.1:8081
> User-Agent: curl/7.83.1
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 401 Unauthorized
< Content-Type: text/html; charset=UTF-8
< Server: Caddy
< Status: 401 Unauthorized
< X-Powered-By: PHP/8.1.7
< Date: Mon, 27 Jun 2022 06:38:33 GMT
< Content-Length: 18
<
* Connection #0 to host 127.0.0.1 left intact
<h1>Auth site</h1>

And if I comment out the forward_auth directive, I do get back the simple one-line contents of the index.html file in the def site. curl -v https://localhost:8080/ with no forward_auth:

*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
* ALPN: offers h2
* ALPN: offers http/1.1
*  CAfile: /etc/ssl/certs/ca-certificates.crt
*  CApath: none
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256
* ALPN: server accepted h2
* Server certificate:
*  subject: [NONE]
*  start date: Jun 27 05:38:36 2022 GMT
*  expire date: Jun 27 17:38:36 2022 GMT
*  subjectAltName: host "localhost" matched cert's "localhost"
*  issuer: CN=Caddy Local Authority - ECC Intermediate
*  SSL certificate verify ok.
* Using HTTP2, server supports multiplexing
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* h2h3 [:method: GET]
* h2h3 [:path: /]
* h2h3 [:scheme: https]
* h2h3 [:authority: localhost:8080]
* h2h3 [user-agent: curl/7.83.1]
* h2h3 [accept: */*]
* Using Stream ID: 1 (easy handle 0x5558902c1fa0)
> GET / HTTP/2
> Host: localhost:8080
> user-agent: curl/7.83.1
> accept: */*
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
< HTTP/2 200
< accept-ranges: bytes
< content-type: text/html; charset=utf-8
< etag: "re4huwm"
< last-modified: Mon, 27 Jun 2022 06:25:44 GMT
< server: Caddy
< content-length: 22
< date: Mon, 27 Jun 2022 06:41:13 GMT
<
<h1>Default site</h1>
* Connection #0 to host localhost left intact

6. Links to relevant resources:

We had some significant bugs with forward_auth in v2.5.1 that weren’t obvious during our initial round of testing/integration. You might be hitting these.

They were fixed in reverseproxy: Fix double headers in response handlers by francislavoie · Pull Request #4847 · caddyserver/caddy · GitHub. You can build from the master branch (or from commit reverseproxy: Fix double headers in response handlers (#4847) · caddyserver/caddy@98468af · GitHub) to get the fixes, and try again. It might resolve your issue.

1 Like

Building off the master branch didn’t make much difference.

v2.5.2-0.20220622225346-10f85558ead1 h1:lpdCTdHIBKppLrsaUeiGKrk4VrMqTPKnyqOlD3LBMIQ=

The only change I could see was in Caddy’s debug logs, the third http.handlers.reverse_proxy message: “handling response {"handler": 1}” was not issued. Just the following:

2022/06/28 00:26:09.865 DEBUG   http.handlers.reverse_proxy     selected upstream       {"dial": "127.0.0.1:8081", "total_upstreams": 1}
2022/06/28 00:26:09.865 DEBUG   http.handlers.reverse_proxy     upstream roundtrip      {"upstream": "127.0.0.1:8081", "duration": 0.0001514, "request": {"remote_ip": "127.0.0.1", "remote_port": "57690", "proto": "HTTP/2.0", "method": "GET", "host": "localhost:8080", "uri": "/auth.php", "headers": {"X-Forwarded-For": ["127.0.0.1"], "X-Forwarded-Proto": ["https"], "X-Forwarded-Host": ["localhost:8080"], "X-Forwarded-Method": ["GET"], "X-Forwarded-Uri": ["/"], "User-Agent": ["curl/7.83.1"], "Accept": ["*/*"]}, "tls": {"resumed": false, "version": 772, "cipher_suite": 4865, "proto": "h2", "server_name": "localhost"}}, "headers": {"Server": ["Caddy"], "Date": ["Tue, 28 Jun 2022 00:26:09 GMT"], "Content-Length": ["0"]}, "status": 200}

Ohh I know what it is. Your original request has localhost in it, which is passed through via the Host header to the proxy as-is, but you used 127.0.0.1 as a host matcher for your auth site block. A site block is basically a host request matcher, and it doesn’t match because of this mismatch.

Instead, just use http://:8081, omit the hostname. You’re using bind anyways, so nothing else should be able to reach it.

Do that, or use header Host 127.0.0.1 or header_up Host {upstream_hostport} inside forward_auth to make sure the right Host header is used.

1 Like

That’s gotten me a little further. Updated Caddyfile to the following:

{
	debug
	http_port 1081 # Doesn't matter, we're not using it here.
}

# The site that I want to authenticate for.

localhost:8080 {
	forward_auth 127.0.0.1:8081 {
		uri /auth.php
	}
	root * def
	file_server
}

# My authentication service, that should only be accessible from
# the local host.

http://:8081 {
	bind 127.0.0.1
	root * auth
	file_server
	php_fastcgi unix//var/run/php-fpm/php-fpm.sock
}

# vim: ts=8 sw=8 noexpandtab

And curl -v https://localhost:8080/ returns a 401 with the “<h1>Auth site</h1>” as expected.

However if I change me auth.php to return HTTP 200 instead of 401, then the above curl call returns 200 as expected, but still with “<h1>Auth site</h1>”. Would have expected it to complete the call and return the contents of def/index.html. Instead, it just seems to return the results of the forward auth call, even though it returned a 2xx response.

Could you show a bit more detail? What’s in the logs? Debug logs are very useful to see exactly the flow of the request, through both site blocks. Please add the log directive in both as well to add in access logs.

It’s possible that forward_auth doesn’t behave properly unless you configure copy_headers, that might be an oversight. Try adding that in with a dummy header for testing.

That seems to have fixed it. Everything is behaving as expected now. Works with v2.5.1 as well as master.

For the record, my final Caddyfile was:

{
	debug
	http_port 1081 # Doesn't matter, we're not using it here.
}

# The site that I want to authenticate for.

localhost:8080 {
	forward_auth 127.0.0.1:8081 {
		uri /auth.php
		copy_headers Remote-User
	}
	root * def
	file_server
	log {
		output file def.log
		format console
	}
}

# My authentication service, that should only be accessible from
# the local host.

http://:8081 {
	bind 127.0.0.1
	root * auth
	file_server
	php_fastcgi unix//var/run/php-fpm/php-fpm.sock
	log {
		output file auth.log
		format console
	}
}

# vim: ts=8 sw=8 noexpandtab

Thanks for the help.

E.

Cool, I’ll try to replicate that problem and make a fix for it.

I definitely recommend you use the master branch though for now, even if it “seems” to work, because there’s bugs with the non-2xx branch of execution, where it would write all response headers twice. Not good. Some browsers like Safari broke in weird ways.

1 Like

Alright here’s the fix:

1 Like

This topic was automatically closed after 30 days. New replies are no longer allowed.