Skip to content

fix(server): native-redirect review fixes before v0.44.0 - #526

Merged
osanderson merged 4 commits into
mainfrom
fix/native-redirect-review
Oct 3, 2026
Merged

osanderson merged 4 commits into
mainfrom
fix/native-redirect-review

Conversation

@osanderson

Copy link
Copy Markdown
Collaborator

Summary

These are the fixes from the security and DevX review of #524 (native-app redirect URIs), before v0.44.0. There are four commits, for release-please.

fix(server): BuildAuthorizationErrorRedirect skipped the redirect policy (Low regression, both reviews found it).

  • Cause: feat(server): accept native-app redirect URIs for clients registered as native #524 parsed every authorization response permissively when building it, assuming PAR had already held redirect_uri to the client's type. BuildAuthorizationErrorRedirect never goes through PAR; it only checked that the URI was registered.
  • Effect: under production assurance, a web client registered for http://127.0.0.1/callback, http://localhost/callback or a private-use URI got an error redirect there, carrying error and state. FAPI 2.0 forbids that, and v0.43.0 refused it.
  • Severity: it isn't an open redirect, since the destination is always one of the client's own registered URIs.
  • Fix: the method now applies parseRedirectURI(client, …), as PAR does.

PAR's refusal of a native URI on a client registered as web now names ApplicationTypeNative in its cause (for logs, never the response). That's the first mistake a wallet integrator makes.

fix: stricter native URI forms and clearer messages.

  • Empty authority: private-use URIs with one (com.example.app:///cb, :////host/cb) are refused. The error shows the single-slash form RFC 8252 §7.1 writes: com.example.app:/callback, not com.example.app://callback.
  • Strict loopback literal: a native loopback URI must be exactly 127.0.0.1 or ::1; net.IP.Equal also accepted the IPv4-mapped ::ffff:127.0.0.1.
  • Clearer host message: ParseIssuerURL and ParseEndpointURL now say a hostless URI "must have a host" instead of the misleading "must be absolute".

feat(storage): ApplicationType.String/IsValid and ParseApplicationType for the "web"/"native" wire values, matching ClientAuthMethod and SenderConstrain.

docs: several docs said loopback http redirects are development-only. That's the web client's rule, so I updated the native exceptions in:

  • server/assurance.go;
  • AllowLoopbackHTTP, URLOption, AllowPrivateUseScheme, ApplicationTypeNative and HasRedirectURI;
  • ARCHITECTURE and GETTING_STARTED.

GETTING_STARTED now shows the single-slash form and corrects "a web client can use neither" (it can use loopback under development). README lists native apps and RFC 8252.

Deferred: per-authorization redirect URIs in the FAPIgo client, for a desktop wallet that binds a new port each flow. One *client.Client per listener works today.

Tests

  • TestBuildAuthorizationErrorRedirectHoldsTheRedirectPolicy (production): web clients with loopback, localhost or private-use URIs are refused; native clients with private-use or loopback URIs are accepted.
  • TestPARNamesTheNativeRegistration: the cause names ApplicationTypeNative for a native URI on a web client, and doesn't for a URI no registration admits.
  • TestParseRedirectURL and TestParseRedirectURLSaysWhatToChange:
    • :/// and :////host are refused;
    • the two-slash error shows com.example.app:/path;
    • the no-host error doesn't say "must be absolute".
  • Storage:
    • TestNativeRedirectURIsAtRegistration gains the [::ffff:127.0.0.1] refusal.
    • TestApplicationTypeWireValues round-trips "web" and "native" and refuses "", "Native" and "mobile".
  • Mutation checks:
    • unchecking the error redirect fails a test;
    • dropping the native hint fails a test;
    • allowing an empty authority fails a test;
    • allowing IPv4-mapped loopback fails a test.
  • Other checks:
    • go test -race across fapi, storage, server, federation and client passes.
    • go test ./cmd/... ./fapitest/... passes.
    • Every demo's tests pass.
    • FuzzParseRedirectURL (15 seconds) is clean.
    • golangci-lint is clean.

🤖 Generated with Claude Code

osanderson and others added 4 commits October 3, 2026 09:13
ApplicationType gains the helpers ClientAuthMethod and SenderConstrain
already have, mapping OIDC Registration's application_type wire values
("web", "native"), for deployments that read client registrations
from configuration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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>
- A private-use redirect URI with an empty authority
  (com.example.app:///cb, or :////host/cb) passed as scheme:/path. It's
  now refused, with a message that shows the single-slash form RFC 8252
  §7.1 writes (com.example.app:/callback, not com.example.app://callback).
- A native client's loopback redirect URI must be the literal 127.0.0.1
  or ::1. net.IP.Equal also took the IPv4-mapped ::ffff:127.0.0.1.
- A URI with no host is refused by ParseIssuerURL and ParseEndpointURL
  as having no host, rather than as not absolute.
- Docs: AllowLoopbackHTTP, URLOption, AllowPrivateUseScheme,
  ApplicationTypeNative and HasRedirectURI now describe the native
  exceptions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
server/assurance.go, ARCHITECTURE, GETTING_STARTED and README said
loopback http redirect URIs were development-only; that's a web client's
rule, and a native client's are accepted in production. GETTING_STARTED
shows the single-slash private-use form, and README lists RFC 8252.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@sonarqubecloud

sonarqubecloud Bot commented Oct 3, 2026

Copy link
Copy Markdown

@osanderson
osanderson merged commit 5e35f8c into main Oct 3, 2026
17 checks passed
@osanderson
osanderson deleted the fix/native-redirect-review branch October 3, 2026 01:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant