/device confirmation page posts to absolute root path instead of respecting listen.public_url subpath, breaking deployments behind a path-based reverse proxy

Status Open
Reported on v2.1.267
Maintainer reply None cached
Activity 0 comments · opened Sep 22, 2026

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 /claudeauth1 prefix -- 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:

  1. Deploy the gateway behind a reverse proxy that maps an external path prefix (e.g. /claudeauth1) to the gateway's own root, with listen.public_url set to include that prefix.
  2. Start a device-code flow: POST {public_url}/oauth/device_authorization.
  3. Open the returned verification_uri_complete in a browser. The confirmation page loads correctly.
  4. 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.

View original on GitHub ↗