/device confirmation page posts to absolute root path instead of respecting listen.public_url subpath, breaking deployments behind a path-based reverse proxy
Environment:
- Gateway server version: 2.1.267 (
claude gateway --config gateway.yaml) - Deployment: behind a reverse proxy (Azure API Management) that forwards
https://gateway.example.com/claudeauth1/*to the gateway's own root (https://gateway-internal.example.com:8443/*), i.e. the gateway itself has no knowledge of the/claudeauth1prefix -- it's stripped by the proxy before the request reaches the gateway. gateway.yaml:
``yaml``
listen:
public_url: https://gateway.example.com/claudeauth1
What's wrong:
The OIDC discovery endpoint correctly respects listen.public_url's path component:
$ curl -s https://gateway.example.com/claudeauth1/.well-known/oauth-authorization-server
{"issuer":"https://gateway.example.com/claudeauth1","device_authorization_endpoint":"https://gateway.example.com/claudeauth1/oauth/device_authorization","token_endpoint":"https://gateway.example.com/claudeauth1/oauth/token","grant_types_supported":["urn:ietf:params:oauth:grant-type:device_code","refresh_token"],"response_types_supported":[],"token_endpoint_auth_methods_supported":["none"],"scopes_supported":["openid","profile","email"],"gateway_protocol_version":1}
However, the /device confirmation page (served at GET https://gateway.example.com/claudeauth1/device?user_code=..., part of the device-code flow) renders an HTML <form> whose action attribute is a root-relative path (/device) rather than either a purely relative path (device) or one built from the configured public_url. When a user clicks "This matches my device -- continue", the browser resolves that root-relative action against the current origin, producing POST https://gateway.example.com/device -- dropping the /claudeauth1 prefix entirely. Behind a path-based reverse proxy that only routes /claudeauth1/*, this request has nowhere to go and 404s, so the device-code sign-in flow cannot be completed.
Steps to reproduce:
- Deploy the gateway behind a reverse proxy that maps an external path prefix (e.g.
/claudeauth1) to the gateway's own root, withlisten.public_urlset to include that prefix. - Start a device-code flow:
POST {public_url}/oauth/device_authorization. - Open the returned
verification_uri_completein a browser. The confirmation page loads correctly. - Click "This matches my device -- continue".
Expected: The browser submits to {public_url}/device (i.e. https://gateway.example.com/claudeauth1/device), matching the reverse proxy's routing, and the sign-in completes.
Actual: The browser submits to https://gateway.example.com/device (no prefix). Confirmed directly:
$ curl -s -i -X POST "https://gateway.example.com/device" -d "user_code=..."
HTTP/1.1 404 Resource Not Found
Why this looks like a bug rather than a proxy misconfiguration: the gateway demonstrably has the information needed to build a correctly-prefixed URL here (it already does so for the discovery document, from the same listen.public_url config value) -- the /device page's form action just doesn't use it.
Workaround attempted: we looked at adding a reverse-proxy route for the bare /device path at the proxy's root, but our proxy's root path is already occupied by an unrelated API on the same shared instance, so this isn't viable for us. A relative (action="device") or public_url-based absolute form action would fix this without requiring any proxy-side workaround.