gitproto: retry credential auth against the actual challenge URL

main

Commit

Soph3mo ago

The 401-retry path replayed against c.EndpointURL even when the 401 came from a cross-host redirect (e.g. github.com → replica.example). Go's http.Client strips Authorization on cross-host redirects (per shouldCopyHeaderOnRedirect), so the retry hit the challenger without auth, got 401 again, and we Reject'd the user's valid credentials — locking them out on the next sync, since Lookup would then return nothing.

Production flow that triggers this:

  1. GET origin/info/refs (anonymous) → 307 → challenger returns 401.
  2. res.Request.URL.Host == challenger; challengeURL keyed correctly.
  3. Lookup(challenger) returns the stored creds.
  4. Retry builds URL from c.EndpointURL = origin, attaches auth.
  5. http.Client follows 307 → strips Authorization → challenger 401.
  6. We hit the reject branch → Reject(challenger, valid-creds). Lost.

Fix:

  • tryHelperRetry captures res.Request.URL (the post-redirect URL the 401 actually came from) and passes it to the retry callback as an override target. The retry hits the challenger directly, no redirect to strip the header.

  • On a successful cross-host retry we also rewrite c.EndpointURL's scheme/host to the challenger via adoptChallengeHost. Otherwise follow-up ops on the same conn would redirect again, lose auth, and have their freshly-Approved creds Reject'd inside resolvePendingHelperCreds when the next 401 came through.

  • EnsureAuthForService had a related milder issue: Lookup ran against c.EndpointURL before the probe, but pendingHelperCreds.url was keyed on the post-probe challenge host. Cross-host: Lookup queried origin (probably no entry), then Approve/Reject targeted the challenger. Restructured to probe first, Lookup against challengeURL, then adopt the challenge host — same shape as the tryHelperRetry fix. Drops the pre-probe Lookup optimization (one fewer Lookup, one extra probe POST when the helper has no creds); necessary trade-off since a user may have creds for the challenger but not for the origin.

doInfoRefsRequest / doPostRPCRequest now accept an optional target *url.URL to support the override; nil keeps the previous "build from c.EndpointURL" behaviour for non-retry callers.

Regression tests:

  • TestRequestInfoRefs_OnUnauthorizedAfterCrossHostRedirectRetries- AgainstChallenger — drives the production scenario end-to-end and asserts (a) the retry RoundTrip lands on the challenger with a Basic auth header, (b) Approve fires keyed on the challenger, (c) no Reject, and (d) c.EndpointURL has adopted the challenger so follow-ups don't redirect.
  • TestEnsureAuthForService_CrossHostProbeLooksUpAndAdoptsChallenger — same idea on the EnsureAuthForService path.

cmd/git-sync test that expected exactly one git-receive-pack POST now expects two (auth-probe + real push) since the probe is unconditional; TestMain comment updated to reflect that.

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com Entire-Checkpoint: dcdc9414badf

Checkpoints

can you fix the conflict and rebase onto main?

Claude CodeOpus 4.7[1m]
View session
Checkpoint 1