Proxy-Authorization header (tested)
Proxy-Authorization carries your credentials for a proxy, not for the website. A proxy that wants a login answers 407 Proxy Authentication Required with a Proxy-Authenticate header, and the client retries with Proxy-Authorization. We built a small proxy that requires a password, sent requests through it with curl and Python, and logged what reached the proxy and what reached the website behind it.
1. The three pieces
| Header / status | Sent by | Example |
|---|---|---|
407 Proxy Authentication Required | Proxy | Status code: "log in to the proxy first" |
Proxy-Authenticate | Proxy, on the 407 | Basic realm="test-proxy" |
Proxy-Authorization | Client | Basic dXNlcjpwQHNz |
They mirror 401, WWW-Authenticate and Authorization, which are for the website itself (see the Authorization header guide). The value is a scheme plus credentials. For Basic it is base64 of user:password: dXNlcjpwQHNz decodes to user:p@ss. Base64 is not encryption, so anyone who can read the request can read the password.
2. What a 407 looks like (measured)
A request through our proxy with no credentials:
$ curl -si -x http://127.0.0.1:8991 http://127.0.0.1:8992/
HTTP/1.1 407 Proxy Authentication Required
Proxy-Authenticate: Basic realm="test-proxy"
...
A wrong password (user:bad) also got 407; the proxy logged Proxy-Authorization: Basic dXNlcjpiYWQ=.
3. What curl sends (measured)
Both forms worked and sent the same header to the proxy:
curl -x 'http://user:p%40ss@127.0.0.1:8991' http://127.0.0.1:8992/
curl -x http://127.0.0.1:8991 --proxy-user 'user:p@ss' http://127.0.0.1:8992/
Proxy log for each:
GET http://127.0.0.1:8992/
proxy-authorization: Basic dXNlcjpwQHNz
proxy-connection: Keep-Alive
| Test | Result |
|---|---|
| When curl sends it | On the first request, before any 407. Only one request reached the proxy. |
--proxy-anyauth | Different: the first request went out with no Proxy-Authorization, got 407, then curl retried with it (two requests in the proxy log, final status 200). |
Password with @ written raw in -x | Failed before connecting: curl: (5) Unsupported proxy syntax ... Bad hostname. |
Same password written p%40ss | 200. curl decoded %40 to @ before encoding. |
--proxy-user 'user:p@ss' | 200. No URL encoding needed here. |
Use --proxy-user when the password has @ : / # % or other URL-special characters.
4. Python requests (measured)
import requests
requests.get("http://127.0.0.1:8992/",
proxies={"http": "http://user:p%40ss@127.0.0.1:8991"})
requests 2.32.3 also sent Proxy-Authorization: Basic dXNlcjpwQHNz on the first request (one request in the log, status 200). As with curl, we percent-encoded the @ in the proxy URL.
5. Does the website see it? (measured)
Not if the proxy strips it, which our proxy did. The origin on 8992 logged:
{"host":"127.0.0.1:8992","user-agent":"curl/8.14.1","accept":"*/*","connection":"keep-alive"}
No proxy-authorization and no proxy-connection. Our proxy has one line that deletes the header before forwarding. From the RFC, not measured: RFC 9110 says a proxy should not forward Proxy-Authorization if it used those credentials itself, but a chain of proxies can pass the header on so each proxy can authenticate in turn. If you write your own proxy, delete the header yourself; don't rely on something else to do it.
6. HTTPS goes through CONNECT
For an https:// URL the client first sends CONNECT host:443 to the proxy, then runs TLS inside the tunnel. Proxy-Authorization goes on the CONNECT request, in plain text to the proxy. The website's own headers are inside TLS, so the proxy can't read or strip them. We measured the first half: curl's CONNECT carried the header.
CONNECT 127.0.0.1:8992
proxy-authorization: Basic dXNlcjpwQHNz
(Our test proxy rejected every CONNECT, so we did not measure a full HTTPS fetch.) From the docs, not measured: requests puts the credentials from an https proxy entry on the CONNECT, and a proxy URL that starts http:// means the CONNECT and its Basic password go over the network unencrypted. Use an https:// proxy URL if the proxy supports it.
7. Fixing a 407
- Read
Proxy-Authenticate: it names the scheme the proxy wants (Basic, Digest, NTLM, Negotiate). - Percent-encode special characters in a proxy URL, or use
--proxy-user. - For a non-Basic scheme in curl, add
--proxy-anyauth(or--proxy-ntlm/--proxy-digest) so it waits for the 407 and picks the scheme. - Check the
http_proxy/https_proxyenvironment variables. curl and requests read them, and a stale one without credentials gives a 407 even though your code never set a proxy. - A 407 is from the proxy, not the website. Changing the
Authorizationheader won't help.
Proxies also add headers of their own. See the Via header guide.
Frequently asked questions
What is the difference between Authorization and Proxy-Authorization?
Authorization is for the website (answered with 401 and WWW-Authenticate). Proxy-Authorization is for the proxy in between (answered with 407 and Proxy-Authenticate). A request can carry both.
Does curl send Proxy-Authorization before getting a 407?
Yes for Basic, the default. In our test curl 8.14.1 sent it on the first request. With --proxy-anyauth it first sent the request with no credentials, got 407, then retried.
Why does curl say 'Unsupported proxy syntax' with my password?
The password contains a character like @ that breaks the URL. Write it percent-encoded (@ becomes %40) or pass it with --proxy-user 'user:pass'.
Does the website receive my proxy password?
It shouldn't. Our test proxy deleted Proxy-Authorization and the origin never saw it. For HTTPS the header only goes on the CONNECT request to the proxy, never inside the tunnel.
Is Basic proxy auth secure?
Only over an encrypted link. The value is base64, which anyone can decode. With an http:// proxy URL it goes across the network in plain text, even when the site itself is HTTPS.
Written with AI assistance. Measured on 2026-10-02 with curl 8.14.1, Python requests 2.32.3 and Node.js 24.21.0: a small Node forward proxy on 127.0.0.1:8991 that requires Basic proxy auth (user <code>user</code>, password <code>p@ss</code>) and forwards to a Node origin on 127.0.0.1:8992 that logs every header it receives. PlainHeaders is our extension; curl, Python and Node.js are free.