Summary
In production, MetaMCP's OAuth Dynamic Client Registration (DCR) rejects loopback redirect URIs commonly used by native/public clients. This prevents clients such as Pi Adapter from registering a local callback even though RFC 8252 defines loopback redirects as an appropriate native-app pattern.
Reproduction
Using the public /oauth/register endpoint, submit a generic DCR request such as:
POST /oauth/register
Content-Type: application/json
{
"redirect_uris": ["http://localhost:19876/callback"]
}
The example callback is based on Pi Adapter's documented/use-in-the-wild dynamic callback pattern. This is a generic reproduction only; no private deployment is required.
Actual behavior
With NODE_ENV=production, registration returns 400 invalid_redirect_uri with an error like:
Invalid redirect URI: http://localhost:19876/callback. Must use secure scheme and valid format.
Expected behavior
Registration should accept a narrowly constrained loopback HTTP redirect for native/public clients, while continuing to reject other insecure or private-network redirect URIs. RFC 8252 §7.3 explicitly covers loopback IP literal redirect URIs and requires authorization servers to allow any port; loopback IP literals are the normative form. localhost is included here as a common compatibility case used by clients such as Pi Adapter, not as a claim that RFC 8252 requires it.
Root cause
The production-only rejection comes from the redirect validation path:
apps/backend/src/routers/oauth/registration.ts calls validateRedirectUri() and maps failure to 400 invalid_redirect_uri.
apps/backend/src/routers/oauth/utils.ts rejects non-HTTPS schemes in production and also rejects localhost/loopback/private addresses, so the native-app loopback exception is never available.
Suggested secure fix
- In production, allow
http://127.0.0.1:<ephemeral-port>/... and http://[::1]:<ephemeral-port>/....
- Optionally allow
http://localhost:<ephemeral-port>/... for compatibility with common native clients.
- Continue rejecting other HTTP hosts and private addresses; require HTTPS for public redirect URIs.
- Preserve PKCE,
state, and exact matching against the redirect URI registered for the client.
- Add positive and negative tests covering IPv4 loopback, IPv6 loopback, optional
localhost, arbitrary ports, public HTTP rejection, and private-address rejection.
Acceptance tests
- Production DCR accepts the constrained loopback cases above, including arbitrary callback ports.
- Production DCR continues to reject non-loopback HTTP and private-network redirect URIs.
- Public HTTPS redirect URI behavior remains unchanged.
- Authorization-code redirect validation still requires an exact registered URI, with PKCE and
state protections intact.
Related but different
The following public reports address MetaMCP-generated OAuth/DCR service URLs becoming localhost behind a reverse proxy. They do not modify validateRedirectUri() and are not this compatibility defect:
References
Summary
In production, MetaMCP's OAuth Dynamic Client Registration (DCR) rejects loopback redirect URIs commonly used by native/public clients. This prevents clients such as Pi Adapter from registering a local callback even though RFC 8252 defines loopback redirects as an appropriate native-app pattern.
Reproduction
Using the public
/oauth/registerendpoint, submit a generic DCR request such as:{ "redirect_uris": ["http://localhost:19876/callback"] }The example callback is based on Pi Adapter's documented/use-in-the-wild dynamic callback pattern. This is a generic reproduction only; no private deployment is required.
Actual behavior
With
NODE_ENV=production, registration returns400 invalid_redirect_uriwith an error like:Expected behavior
Registration should accept a narrowly constrained loopback HTTP redirect for native/public clients, while continuing to reject other insecure or private-network redirect URIs. RFC 8252 §7.3 explicitly covers loopback IP literal redirect URIs and requires authorization servers to allow any port; loopback IP literals are the normative form.
localhostis included here as a common compatibility case used by clients such as Pi Adapter, not as a claim that RFC 8252 requires it.Root cause
The production-only rejection comes from the redirect validation path:
apps/backend/src/routers/oauth/registration.tscallsvalidateRedirectUri()and maps failure to400 invalid_redirect_uri.apps/backend/src/routers/oauth/utils.tsrejects non-HTTPS schemes in production and also rejects localhost/loopback/private addresses, so the native-app loopback exception is never available.Suggested secure fix
http://127.0.0.1:<ephemeral-port>/...andhttp://[::1]:<ephemeral-port>/....http://localhost:<ephemeral-port>/...for compatibility with common native clients.state, and exact matching against the redirect URI registered for the client.localhost, arbitrary ports, public HTTP rejection, and private-address rejection.Acceptance tests
stateprotections intact.Related but different
The following public reports address MetaMCP-generated OAuth/DCR service URLs becoming
localhostbehind a reverse proxy. They do not modifyvalidateRedirectUri()and are not this compatibility defect:References