Skip to content

Add RFC 8252 loopback-aware redirect URI matching to auth#56

Merged
ChiragAgg5k merged 2 commits into
mainfrom
feat/auth-redirect-uri-loopback-matching
Jul 9, 2026
Merged

Add RFC 8252 loopback-aware redirect URI matching to auth#56
ChiragAgg5k merged 2 commits into
mainfrom
feat/auth-redirect-uri-loopback-matching

Conversation

@ChiragAgg5k

Copy link
Copy Markdown
Member

What

Adds Utopia\Auth\OAuth2\RedirectUris, a value object over an OAuth2 client's registered redirect URIs with RFC 8252 §7.3 loopback-aware matching, plus tests and docs.

Why

Native and CLI OAuth2 clients (e.g. MCP clients such as Claude Code) bind an ephemeral loopback port per run and cannot register it ahead of time. They register once via dynamic client registration (with whatever port they held at the time), cache the client_id, and present a different port on every subsequent authentication. An authorization server doing exact redirect_uri string matching rejects every re-authentication after the first with invalid_redirect_uri — observed in production against fra.cloud.appwrite.io (client registered with http://localhost:3118/callback, later presented http://localhost:54155/callback).

RFC 8252 §7.3 requires authorization servers to allow any port for loopback redirect URIs for exactly this reason.

How

matches() does exact string comparison first (zero behavior change for non-loopback clients), then a loopback fallback: when both the presented and a registered URI are http URIs whose host is exactly localhost, 127.0.0.1, or [::1] with no userinfo or fragment, they compare on host, path, and query while ignoring the port.

Deliberately strict:

  • loopback hosts are an exact allowlist — localhost.evil.com never qualifies
  • host must still match host — localhost does not match 127.0.0.1
  • https and custom schemes stay exact-only
  • path is case-sensitive; empty path normalizes to /

from() tolerates malformed stored entries (they never match anything) since the registered list is existing data, not boundary input.

Consumers

Appwrite Cloud will replace the four inline exact-match checks (Authorize, PAR, Approve, Reject) with this matcher after the next utopia-php/auth release.

Testing

  • 28 new PHPUnit cases (RedirectUrisTest) covering exact matching, port variance across all three loopback hosts, host/scheme/path/query/fragment/userinfo strictness, and malformed-input robustness
  • bin/monorepo test auth — 193 tests, 440 assertions, green
  • bin/monorepo check auth — pint, phpstan, rector clean

Native and CLI OAuth2 clients (e.g. MCP clients) bind an ephemeral
loopback port per run and cannot register it ahead of time, so exact
redirect_uri matching rejects every re-authentication after the first.

RedirectUris wraps a client's registered redirect URIs and matches a
presented redirect_uri exactly, except that http loopback URIs
(localhost, 127.0.0.1, [::1]) match with any port per RFC 8252
Section 7.3. The loopback hosts are an exact allowlist, the host itself
must still match, and scheme, path, and query compare exactly.
@greptile-apps

greptile-apps Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds RedirectUris, a new value object that wraps an OAuth2 client's registered redirect URIs and implements RFC 8252 §7.3 loopback-aware port-insensitive matching, solving the problem where native/CLI clients bind an ephemeral port on each run and are rejected by strict string comparison.

  • Exact-match fallback preserved: non-loopback URIs (HTTPS, custom schemes, non-loopback hosts) continue to require exact string equality, so there is zero behavior change for web clients.
  • Loopback matching is deliberately strict: only http scheme; host must be exactly localhost, 127.0.0.1, or [::1]; path and query compare exactly; fragment or userinfo in either URI disables the loopback path and falls back to exact matching.
  • 28 PHPUnit cases cover port variance across all three loopback hosts, host/scheme/path/query/fragment/userinfo strictness, and malformed-input robustness.

Confidence Score: 5/5

Safe to merge — the change is entirely additive (new class, new tests, new docs) with no modifications to existing code paths.

The loopback allowlist rejects lookalikes and percent-encoded variants, the IPv6 [::1] constant matches what PHP's parse_url actually returns, and path/query/fragment/userinfo are all handled strictly. Test coverage is thorough across all security-relevant boundaries.

No files require special attention.

Important Files Changed

Filename Overview
packages/auth/src/Auth/OAuth2/RedirectUris.php New value object implementing RFC 8252 §7.3 loopback-aware redirect URI matching; logic is correct, IPv6 bracket handling aligns with PHP's parse_url behavior, and all edge cases (fragment, userinfo, scheme strictness, path normalisation) are correctly handled.
packages/auth/tests/Auth/OAuth2/RedirectUrisTest.php 28 data-provider test cases covering exact match, loopback port variance across all three loopback hosts, host/scheme/path/query/fragment/userinfo strictness, and malformed-input robustness; coverage is thorough.
packages/auth/docs/oauth2.md Documentation section added for redirect URI matching with accurate code examples and correct inline comments.

Reviews (1): Last reviewed commit: "(style): collapse empty constructor body" | Re-trigger Greptile

@ChiragAgg5k ChiragAgg5k merged commit 578f2b2 into main Jul 9, 2026
6 checks passed
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.

2 participants