fix(auth): loopback redirect URIs must match on any port (RFC 8252 §7.3) #2
Labels
No labels
deferred
waiting-on-julian
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
jlxq0/typst-mcp#2
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Symptom
Claude Code CLI cannot complete Dynamic Client Registration:
Observed against
caldav-mcpand diagnosed there;src/oauth_redirect.rsis copy-pasted across all five Rust MCP servers, so this repo has the same defect.Cause
is_allowed_redirect_uri(src/oauth_redirect.rs:23) requires exact string equality against the allowlist. The allowlist carrieshttp://localhost:8787/callback. Claude Code CLI does not use 8787: it binds a random free port per session (the observed attempt usedhttp://localhost:3118/callback) and only consults a fixed port if every random draw fails.MCP_OAUTH_CALLBACK_PORToverrides it, but nothing sets that by default. No static entry can match, so native loopback clients are permanently locked out.RFC 8252 §7.3:
validate_redirect_urialready implements the loopback carve-out for the scheme check — cleartexthttpis permitted on loopback hosts only. The port half of the same rule was never written.Fix
When an allowlist entry is a loopback
httpURI, compare scheme + host + path and ignore the port. Non-loopback entries keep exact matching: the port is a meaningful part of anhttpsor private-scheme callback and relaxing it there would be a real hole.Tests:
http://localhost:8787/callbackallowlisted acceptshttp://localhost:3118/callbackhttp://localhost:3118/other— path still mattershttp://127.0.0.1:3118/callbackunless127.0.0.1is listed separately — RFC 8252 relaxes the port, not the hosthttps://claude.ai/api/mcp/auth_callbackstill rejectshttps://claude.ai:8443/api/mcp/auth_callbackBefore trusting the new tests, break the matcher and watch them go red.
Scope
Sibling issue with the full diagnosis: jlxq0/caldav-mcp#2
Also affected: carddav-mcp, jmap-mcp, m365-mcp, caldav-mcp.
No deployment manifest change is required — the existing allowlists already carry a loopback entry, which starts matching once the rule is right.
Recording the production verification, because this was closed on a merged PR and a green
gate rather than on the deployed system answering about itself.
Checked against
https://typst-mcp.hanso.groupon 2026-08-26, driven at the allowlist thedeployment actually enforces (read off the container env with
kubectl -n typst-mcp get deploy typst-mcp -o jsonpath=..., not off an in-repo copy), whoseonly loopback entries are
http://localhost:8787/callbackandhttp://localhost:8787/oauth/callback.Each row is a real
GET /authorizewithresponse_type=code, the DCR client id, an S256challenge and
scope=render; onlyredirect_urivaries.http://localhost:8787/callback(exact allowlist entry)http://localhost:54321/callbackhttp://localhost:54321/oauth/callbackhttp://127.0.0.1:54321/callbackunregistered redirect_urihttp://localhost:54321/wrongunregistered redirect_urihttps://localhost:54321/callbackunregistered redirect_urihttp://evil.example.com:54321/callbackunregistered redirect_uriThe port is relaxed and nothing else is:
localhoststill does not match127.0.0.1,/callbackstill does not match/oauth/callback,httpsdoes not match anhttpentry,and a non-loopback host is refused outright.
The four refusals are the point. Three 303s alone would also be produced by a matcher that
accepts everything, so they cannot distinguish a working fix from a hole. Watching it refuse
the near-misses is what makes this evidence rather than a green tick.
Live version at the time of the check:
/healthreported0.2.1, running container digestsha256:0b3da4c968bd91f470fd74f2225682da3b922845d6fa2dc91bb844f5bad216cb, matching what thev0.2.1tag build pushed.