fix(auth): accept any port on loopback redirect URIs (RFC 8252 §7.3) #4

Merged
jlxq0 merged 1 commit from oauth-loopback-port into main 2026-08-25 02:18:59 +00:00
Owner

RFC 8252 §7.3 requires the authorization server to allow any port on a loopback redirect URI. is_allowed_redirect_uri demanded exact string equality, so the deployed http://localhost:8787/callback entry could never match Claude Code CLI's randomly chosen loopback port and DCR rejected every attempt with unregistered redirect_uri.

Change

loopback_matches_ignoring_port compares scheme, host, path and query and ignores the port, and it fires only when the allowlist entry is a cleartext loopback http URI. Everything else keeps full string equality, so an https or private-use-scheme entry still pins its port: https://claude.ai/api/mcp/auth_callback does not admit https://claude.ai:8443/api/mcp/auth_callback.

Host is not relaxed either. localhost and 127.0.0.1 remain distinct, per RFC 8252, which relaxes the port alone.

Tests

Three new tests, and each was watched failing against a deliberately broken matcher:

Mutation Tests that went red
drop entry_host == uri_host loopback_entry_matches_any_port, loopback_relaxation_does_not_widen_hosts
drop entry.path() == uri.path() loopback_entry_matches_any_port
drop entry.query() == uri.query() loopback_entry_matches_any_port
relax port for every scheme non_loopback_entries_keep_exact_port_matching
revert to exact-only matching loopback_entry_matches_any_port, loopback_relaxation_does_not_widen_hosts

Gates

cargo fmt --all --check clean, cargo clippy --all-targets --all-features --locked -- -D warnings clean, cargo test --all-features --locked 99 passed / 0 failed, cargo audit exit 0, cargo deny check bans licenses sources bans ok / licenses ok / sources ok.

Codex reviewed the matcher read-only and confirmed no non-loopback URI, and no loopback URI with a different host, path or query, can reach acceptance through the new branch.

The same defect is in carddav-mcp, jmap-mcp, typst-mcp and m365-mcp; they are being fixed separately.

Closes #2

RFC 8252 §7.3 requires the authorization server to allow any port on a loopback redirect URI. `is_allowed_redirect_uri` demanded exact string equality, so the deployed `http://localhost:8787/callback` entry could never match Claude Code CLI's randomly chosen loopback port and DCR rejected every attempt with `unregistered redirect_uri`. ## Change `loopback_matches_ignoring_port` compares scheme, host, path and query and ignores the port, and it fires only when the **allowlist entry** is a cleartext loopback `http` URI. Everything else keeps full string equality, so an `https` or private-use-scheme entry still pins its port: `https://claude.ai/api/mcp/auth_callback` does not admit `https://claude.ai:8443/api/mcp/auth_callback`. Host is not relaxed either. `localhost` and `127.0.0.1` remain distinct, per RFC 8252, which relaxes the port alone. ## Tests Three new tests, and each was watched failing against a deliberately broken matcher: | Mutation | Tests that went red | |---|---| | drop `entry_host == uri_host` | `loopback_entry_matches_any_port`, `loopback_relaxation_does_not_widen_hosts` | | drop `entry.path() == uri.path()` | `loopback_entry_matches_any_port` | | drop `entry.query() == uri.query()` | `loopback_entry_matches_any_port` | | relax port for every scheme | `non_loopback_entries_keep_exact_port_matching` | | revert to exact-only matching | `loopback_entry_matches_any_port`, `loopback_relaxation_does_not_widen_hosts` | ## Gates `cargo fmt --all --check` clean, `cargo clippy --all-targets --all-features --locked -- -D warnings` clean, `cargo test --all-features --locked` 99 passed / 0 failed, `cargo audit` exit 0, `cargo deny check bans licenses sources` bans ok / licenses ok / sources ok. Codex reviewed the matcher read-only and confirmed no non-loopback URI, and no loopback URI with a different host, path or query, can reach acceptance through the new branch. The same defect is in `carddav-mcp`, `jmap-mcp`, `typst-mcp` and `m365-mcp`; they are being fixed separately. Closes #2
fix(auth): accept any port on loopback redirect URIs
All checks were successful
CI / cargo (pull_request) Successful in 1m13s
CI / docker (pull_request) Successful in 59s
33bd20d3a1
RFC 8252 §7.3 requires the authorization server to allow any port on a
loopback redirect, because a native or command-line client binds an
ephemeral local port per session. `is_allowed_redirect_uri` demanded exact
string equality, so the allowlisted `http://localhost:8787/callback` could
never match Claude Code CLI's randomly chosen port and DCR rejected it with
`unregistered redirect_uri`.

The relaxation covers the port and nothing else: scheme, host, path and
query must still match the allowlist entry exactly, and it applies only to
cleartext loopback entries. `https` and private-use-scheme entries keep full
string equality, where the port is part of the origin.

Closes #2
jlxq0 merged commit 762374e23c into main 2026-08-25 02:18:59 +00:00
jlxq0 deleted branch oauth-loopback-port 2026-08-25 02:19:00 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
jlxq0/caldav-mcp!4
No description provided.