Serve /oauth/authorize as a 302 instead of a 200 + JS redirect - #55
Merged
Conversation
The authorize hop to the II connect link was a 200 HTML page whose inline script did location.replace(...). Replace it with a real HTTP 302 (Location), so there's one fewer interposition point and it works with JS disabled. The reason it was JS (js_redirect's doc: "a Location redirect drops the fragment in some clients") does not affect the real path: modern browsers preserve a fragment present in a Location header (RFC 7231 §7.1.2), and the fragment never goes on the wire, so beta II reads #callback/state/ registration_key from location.hash exactly as before. The clients that would drop it are non-browser redirect-followers, which cannot complete an interactive II sign-in anyway. - authorize now returns redirect_302(&ii_url) (still sets the connect cookie); redirect_302 also sets Referrer-Policy: no-referrer (the authorize query carries only non-secret OAuth params, so that is tidiness). Its doc is generalized to cover both top-level hops. - Remove the now-unused js_redirect helper (js_escape stays; it is still used by the finishing page and the pinned page's redeem URL). Verified: authorize returns 302 with the fragment intact in Location plus the cookie, and a headless-browser run confirms the full fragment survives the 302 into location.hash. Tests green (111). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T9N8USDfNK5yzznPGg7Ym
There was a problem hiding this comment.
Pull request overview
This PR replaces the /oauth/authorize “JS-driven redirect page” with a real HTTP 302 Found redirect to the Internet Identity /mcp URL (including its fragment), improving robustness and working with JavaScript disabled.
Changes:
- Switch
/oauth/authorizefromjs_redirect(&ii_url)(200 + inline JS) toredirect_302(&ii_url)(302 +Location). - Generalize and expand the
redirect_302doc comment to cover both redirect hops and ensureReferrer-Policy: no-referreris set. - Remove the now-unused
js_redirecthelper (keepingjs_escapefor other pages).
Comments suppressed due to low confidence (1)
src/auth.rs:503
- The authorize redirect behavior changed from an HTML + JS hop to an HTTP 302, but there’s still no test coverage asserting the response shape (status + Location + Referrer-Policy + Set-Cookie). Since this file already has unit tests, adding a small test around the redirect helper (and/or the authorize handler) would prevent regressions (e.g., accidentally reintroducing a 200 page or dropping the Referrer-Policy header).
// Redirect the consenting browser to the II connect link with a real HTTP
// 302 (`Location`). The link's params ride in the URL fragment
// (`#callback=…&state=…®istration_key=…`); modern browsers preserve a
// fragment present in a `Location` header (RFC 7231 §7.1.2), and the fragment
// never goes on the wire, so beta II's frontend still reads it from
// `location.hash` exactly as before, with one fewer interposition point than
// a script-driven hop, and it works with JS disabled. `redirect_302` also
// sets `Referrer-Policy: no-referrer` (the authorize query carries only
// non-secret OAuth params, so that is tidiness, not a leak fix).
let mut resp = redirect_302(&ii_url);
resp.headers_mut().insert(
axum::http::header::SET_COOKIE,
axum::http::HeaderValue::from_str(&set_cookie).expect("valid cookie"),
);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Update the Location/fragment citation from RFC 7231 §7.1.2 (obsolete) to RFC 9110 §10.2.2 in both spots (the authorize comment and the redirect_302 doc). - Add a regression test for the authorize redirect shape (the suggested coverage): asserts /oauth/authorize returns 302 with the II link in Location (fragment params callback/state/registration_key intact), Referrer-Policy: no-referrer, and the mcp_connect binding cookie — so a regression to a 200 + JS page or a dropped header is caught. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T9N8USDfNK5yzznPGg7Ym
- Compare the Referrer-Policy header via .to_str() rather than relying on HeaderValue's PartialEq<&str> (the original compiled and passed via std's blanket `PartialEq<&B> for &A`, but the explicit form is clearer). - Assert the cookie using the CONNECT_COOKIE constant instead of the hardcoded "mcp_connect", so a future rename can't silently pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T9N8USDfNK5yzznPGg7Ym
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The authorize hop to the Internet Identity connect link was served as a
200HTML page whose inline script didlocation.replace(...). This makes it a real HTTP 302 (Location) instead: one fewer place a redirect can be interfered with, and it works with JavaScript disabled.Why the old code used JS, and why the 302 is safe
js_redirect's doc said it used a script hop so "aLocationredirect drops the fragment in some clients" — the II link carries its params in the URL fragment (#callback=…&state=…®istration_key=…). That concern does not affect the real path:Locationheader (RFC 7231 §7.1.2); the fragment never goes on the wire, so beta II's frontend reads it fromlocation.hashexactly as before.beta.id.aito consume).Changes
authorizenow returnsredirect_302(&ii_url)(still setting themcp_connectcookie on that response).redirect_302also setsReferrer-Policy: no-referrer— the authorize query carries only non-secret OAuth params (client_id,redirect_uri, PKCE challenge,state), so that's tidiness, not a leak fix. Its doc comment is generalized to cover both top-level hops (out to II, and back to the client).js_redirecthelper.js_escapestays (still used by the finishing page and the pinned page's redeem URL).Testing
cargo build --locked --all-targets+cargo test --locked --all-targetsgreen (111 tests). No test asserted the authorize response shape, so none needed changing.GET /oauth/authorizenow returns302 FoundwithLocation: https://beta.id.ai/mcp#callback=…&state=…®istration_key=…(fragment intact), plus the connect cookie andReferrer-Policy: no-referrer.Locationcarries a fragment confirmed the full fragment (callback,state,registration_key) survives intolocation.hash— i.e. beta II will read it unchanged.Note (as discussed): this is a general robustness nicety and does not address a blocked-popup failure; it just removes the script-driven interposition on the authorize hop.
🤖 Generated with Claude Code
https://claude.ai/code/session_014T9N8USDfNK5yzznPGg7Ym
Generated by Claude Code