feat(server): accept native-app redirect URIs for clients registered as native - #524
Merged
Merged
Conversation
…as native RFC 8252 gives native apps three redirects: a private-use URI scheme (§7.1), a claimed https URI (§7.2) and loopback http on any port (§7.3). FAPI 2.0 forbids only non-loopback http. Until now the server accepted https everywhere and loopback http under development assurance only, so a mobile wallet's com.example.app:/callback was refused at PAR whatever its registration said. - storage.RegisteredClientConfig.ApplicationType: ApplicationTypeWeb (the zero value, today's behaviour) or ApplicationTypeNative, after OIDC Registration's application_type; RFC 8252 §8.4 asks a server to record it. - NewRegisteredClient checks a native client's redirect URIs. Each must be a private-use scheme in reverse-domain form (with a ".", in scheme:/path form), loopback http to the IP literal 127.0.0.1 or [::1] (not "localhost", §8.3), or https. - A native client's loopback redirect URI matches on any port and exactly otherwise (§7.3, §8.4). The token request still has to name the port the authorization request used. - PAR accepts these for a native client in production too. A web client is unchanged: https, plus loopback http, exact, under development assurance only. - fapi.ParseRedirectURL and AllowPrivateUseScheme carry a redirect destination that has no host. ParseEndpointURL is unchanged. Automatically registered federation clients stay web: an RP's self-published application_type isn't acted on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
osanderson
added a commit
that referenced
this pull request
Oct 3, 2026
…irect policy #524 parsed every authorization response permissively at build time, on the grounds that the pushed authorization request had already checked redirect_uri against the client's type. BuildAuthorizationErrorRedirect never goes through one: it checked only that the URI was registered. So under production assurance a web client registered for a loopback or private-use URI got an error redirect there, which FAPI 2.0 forbids and v0.43.0 refused. It now applies parseRedirectURI as PAR does. PAR's refusal of a native app's redirect URI on a client registered as web now names ApplicationTypeNative in its cause, for the logs: the first mistake a wallet's integrator makes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Summary
A mobile wallet's redirect URI, such as
org.idfoundry.oid4vcgo.demowallet:/callback(a private-use URI scheme, RFC 8252 §7.1), was refused at PAR with "redirect_uri is not an acceptable redirect destination", whatever its registration said.Server.parseRedirectURIadmitted https, plus loopback http under development assurance only, andfapi.ParseEndpointURLcouldn't even represent a URI with no host.FAPI 2.0 forbids only non-loopback http redirect URIs; it allows loopback http for native clients and doesn't forbid private-use schemes. RFC 8252 §8.4 says the server MUST record the client type, match exactly except for the port on loopback, and SHOULD require reverse-domain schemes, at minimum rejecting one with no ".". This adds that, per client:
storage.RegisteredClientConfig.ApplicationType:ApplicationTypeWebis the zero value and today's behaviour.ApplicationTypeNativefollows OIDC Registration'sapplication_type.A native client's redirect URIs are checked at registration (
NewRegisteredClient). Each must be one of:scheme:/pathwith no authority (RFC 8252 §7.1);127.0.0.1or[::1], neverlocalhost(§8.3);Anything else is refused at registration, not at the first PAR.
Matching: a native client's loopback URI matches on any port, and exactly otherwise (§7.3, §8.4). Register
http://127.0.0.1/callback, then requesthttp://127.0.0.1:51004/callback. The token request still has to name the sameredirect_uri, port included (RFC 6749 §4.1.3).PAR accepts all three for a native client in production too. A web client is unchanged: https, plus exact-match loopback http under development assurance only.
fapi.ParseRedirectURLandfapi.AllowPrivateUseScheme()represent a redirect destination with no host.ParseEndpointURLdoesn't accept private-use schemes, even with the option.Federated clients stay web. An RP's self-published
application_typeisn't acted on, for the same reason its scopes aren't. The field's doc says so.Unchanged:
Docs.
ApplicationTypeNative's doc, GETTING_STARTED's client registration section, andfederation.RelyingPartyMetadata.ApplicationType. Additive (feat).Tests
fapi:TestParseRedirectURL:myapp:/cb(no "."), an opaque form, an authority, an empty or leading-hyphen label, a fragment, non-loopback http;ParseEndpointURLignores the option.FuzzParseRedirectURL: never panics, and with no options accepts only https (20-second run, clean).storage:TestNativeRedirectURIsAtRegistration: private-use,127.0.0.1,[::1], a port and https are accepted.myapp:,localhost,127.0.0.2, non-loopback http and an authority form are refused. So is an unknown application type.TestNativeLoopbackMatchesAnyPort: any port matches, for IPv4 and IPv6. A different path or query, port 0 or 70000,localhost,HTTP://, userinfo, or https does not. A web client's loopback URI doesn't match on another port.server:TestNativeClientPrivateUseRedirectUnderProduction: the response goes to the private-use URI withcode,stateandiss.TestNativeClientLoopbackAnyPortUnderProduction: the response goes to the requested port. The token exchange refuses the port-less registered URI and accepts the requested one.TestWebClientRefusedNativeRedirects: a web client getsinvalid_requestfor a private-use URI, loopback on any port, and loopback as registered, all under production.TestNativeClientPrivateUseRedirectWithJARM: a signed response (JARM) is sent to a private-use URI.localhostor any loopback IP;go test -raceacross fapi, storage, server, federation and client passes.go test ./cmd/... ./fapitest/...passes.🤖 Generated with Claude Code