fix(auth): loopback redirect URIs must match on any port (RFC 8252 §7.3) #2

Closed
opened 2026-08-25 02:02:14 +00:00 by jlxq0 · 0 comments
Owner

Symptom

Claude Code CLI cannot authenticate against https://caldav-mcp.kampong.social/mcp:

Dynamic Client Registration rejected (HTTP 400): unregistered redirect_uri

Cause

src/oauth_redirect.rs:23 requires exact string equality:

pub fn is_allowed_redirect_uri(allowed: &[String], uri: &str) -> bool {
    validate_redirect_uri(uri, "redirect_uri").is_ok()
        && allowed.iter().any(|allowed| allowed == uri)
}

The deployed allowlist carries http://localhost:8787/callback. Claude Code CLI does not use 8787. It picks a random free port per session and only falls back to a fixed one if the random draws all fail — the observed attempt used http://localhost:3118/callback. No static entry can match, so the CLI is permanently locked out. The 8787 entry was written assuming a fixed port and has presumably never matched.

RFC 8252 §7.3 is explicit that this is the server's bug, not the client's:

the authorization server MUST allow any port to be specified at the time of the request for loopback IP redirect URIs

validate_redirect_uri already implements the loopback carve-out for the scheme check (cleartext http permitted on loopback hosts only). The port half of the same rule is missing.

Fix

When an allowlist entry is a loopback http URI, compare scheme + host + path and ignore the port. Non-loopback entries keep exact matching — the port is a meaningful part of an https or private-scheme callback and loosening it there would be a real hole.

Tests to add:

  • http://localhost:8787/callback allowlisted accepts http://localhost:3118/callback
  • and rejects http://localhost:3118/other (path still matters)
  • and does not accept http://127.0.0.1:3118/callback unless 127.0.0.1 is separately listed (host still matters; RFC 8252 relaxes the port, not the host)
  • https://claude.ai/api/mcp/auth_callback still rejects https://claude.ai:8443/api/mcp/auth_callback

Siblings

oauth_redirect.rs is copy-pasted, not shared. The same defect is in carddav-mcp, jmap-mcp, typst-mcp and m365-mcp — verified identical filenames in each repo, and none of their deployed allowlists carry a loopback entry that a CLI could hit either (m365-mcp has no loopback entry at all). Fix here first, then port.

Workaround until this ships

Pin the client instead of the server: MCP_OAUTH_CALLBACK_PORT=8787 in the CLI's environment makes it reuse the already-allowlisted port.

## Symptom Claude Code CLI cannot authenticate against `https://caldav-mcp.kampong.social/mcp`: ``` Dynamic Client Registration rejected (HTTP 400): unregistered redirect_uri ``` ## Cause `src/oauth_redirect.rs:23` requires exact string equality: ```rust pub fn is_allowed_redirect_uri(allowed: &[String], uri: &str) -> bool { validate_redirect_uri(uri, "redirect_uri").is_ok() && allowed.iter().any(|allowed| allowed == uri) } ``` The deployed allowlist carries `http://localhost:8787/callback`. Claude Code CLI does not use 8787. It picks a **random free port** per session and only falls back to a fixed one if the random draws all fail — the observed attempt used `http://localhost:3118/callback`. No static entry can match, so the CLI is permanently locked out. The `8787` entry was written assuming a fixed port and has presumably never matched. RFC 8252 §7.3 is explicit that this is the server's bug, not the client's: > the authorization server MUST allow any port to be specified at the time of the request for loopback IP redirect URIs `validate_redirect_uri` already implements the loopback carve-out for the *scheme* check (cleartext `http` permitted on loopback hosts only). The port half of the same rule is missing. ## Fix When an allowlist entry is a loopback `http` URI, compare scheme + host + path and ignore the port. Non-loopback entries keep exact matching — the port is a meaningful part of an `https` or private-scheme callback and loosening it there would be a real hole. Tests to add: - `http://localhost:8787/callback` allowlisted accepts `http://localhost:3118/callback` - and rejects `http://localhost:3118/other` (path still matters) - and does not accept `http://127.0.0.1:3118/callback` unless `127.0.0.1` is separately listed (host still matters; RFC 8252 relaxes the port, not the host) - `https://claude.ai/api/mcp/auth_callback` still rejects `https://claude.ai:8443/api/mcp/auth_callback` ## Siblings `oauth_redirect.rs` is copy-pasted, not shared. The same defect is in `carddav-mcp`, `jmap-mcp`, `typst-mcp` and `m365-mcp` — verified identical filenames in each repo, and none of their deployed allowlists carry a loopback entry that a CLI could hit either (`m365-mcp` has no loopback entry at all). Fix here first, then port. ## Workaround until this ships Pin the client instead of the server: `MCP_OAUTH_CALLBACK_PORT=8787` in the CLI's environment makes it reuse the already-allowlisted port.
jlxq0 closed this issue 2026-08-25 02:19:00 +00:00
Sign in to join this conversation.
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#2
No description provided.