Skip to content

fix(coreapi): handle cross-jurisdiction routing for control-plane commands - #1457

Merged
toothbrush merged 8 commits into
mainfrom
fix/coreapi-cross-juris-transport
Jun 18, 2026
Merged

fix(coreapi): handle cross-jurisdiction routing for control-plane commands#1457
toothbrush merged 8 commits into
mainfrom
fix/coreapi-cross-juris-transport

Conversation

@toothbrush

@toothbrush toothbrush commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

https://entire.io/gh/entireio/cli/trails/599

What

Control-plane commands (mirror create/delete/collaborators, repo, grant, …) didn't work cross-region. They pinned to the home core, sent the bare login JWT, and had no 421-follow or token exchange. Operating on a resource whose home jurisdiction is another region failed: the home core's 421 surfaced as a generic decode error, and even reaching the foreign core a home-region login JWT can't verify there (local-only JWKS → bare invalid token 401, with no cross_juris_token_required hint).

Fix

Wrap the coreapi client's transport with a cross-juris RoundTripper (WithClient):

  • Follows a 421's home_core_url, gated by the responding core's /.well-known/entire-federation manifest.
  • After the redirect, a bare 401 triggers a proactive RFC 8693 exchange at <home>/oauth/token — the home core can't verify a foreign-region JWT's signature so it never emits the hint, but its /oauth/token federates the sibling JWKS and mints a token it accepts.
  • Also handles the structured cross_juris_token_required hint.
  • Per-origin token cache; HTTPS / same-origin / federation trust gates so the login JWT never leaks off-origin.

Reuses the existing httputil.PostOAuthToken primitive. Inert for same-jurisdiction calls.

Test

Run a control-plane command against a resource in another region (e.g. entire mirror collaborators ... for a US-jurisdiction cluster as an EU user) — it now follows the 421 and exchanges automatically instead of erroring.

Mirrors the server-side + entire-core-CLI fix in entirehq/entiredb (no server change needed; the 421 home_core_url, federation manifest, and sibling JWKS federation are already in place).

🤖 Generated with Claude Code


Note

High Risk
Changes authentication transport for all control-plane API calls: redirects, token exchange, and federation trust directly affect whether login JWTs are sent and where; mistakes could break cross-region ops or weaken JWT exfiltration protections.

Overview
Control-plane coreapi clients now use a cross-jurisdiction HTTP transport so mirror/repo/grant-style commands work when a resource’s home region differs from the user’s login region.

The transport follows 421 responses using home_core_url, but only if the redirect host appears in the responding core’s /.well-known/entire-federation peer list. After redirect, it handles cross_juris_token_required 401s and the bare 401 case (foreign login JWT fails local JWKS) by RFC 8693 token exchange at same-origin /oauth/token, always using the original login JWT as subject_token, caching exchanged tokens per origin (~4m TTL), and retrying with buffered request bodies.

Trust gates block off-origin exchange URLs and non-HTTPS targets (except loopback for tests) so server-supplied URLs cannot exfiltrate the login JWT. Same-jurisdiction traffic is unchanged. New() and NewWithBearer() both opt in via WithClient(newCrossJurisHTTPClient()); tests cover federation rejection, off-origin hints, 421→exchange, and body replay.

Reviewed by Cursor Bugbot for commit 6a23ba4. Configure here.

…mands

Control-plane commands (mirror create/delete/collaborators, repo, grant,
…) pinned to the home core, sent the bare login JWT, and had no 421
follow or token exchange — so any operation on a resource whose home
jurisdiction is another region failed: the home core returns 421, which
surfaced as a generic decode error, and even reaching the foreign core a
home-region login JWT can't verify there (local-only JWKS → bare 401).

Wrap the coreapi client's transport with a cross-juris RoundTripper:
- follows a 421's home_core_url (gated by the responding core's
  /.well-known/entire-federation manifest);
- after the redirect, a bare 401 (the home core can't verify a
  foreign-region JWT's signature, so it emits no cross_juris hint)
  triggers a proactive RFC 8693 exchange at <home>/oauth/token, where
  the target core federates the sibling JWKS and mints a token it
  accepts;
- also handles the structured cross_juris_token_required hint;
- per-origin token cache, HTTPS/same-origin/federation trust gates.

Reuses the existing httputil.PostOAuthToken exchange primitive.
Copilot AI review requested due to automatic review settings June 18, 2026 05:23
@toothbrush
toothbrush requested a review from a team as a code owner June 18, 2026 05:23
Comment thread internal/coreapi/cross_juris_transport.go

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the internal/coreapi (control-plane) HTTP client to support cross-jurisdiction routing by automatically following 421 home_core_url responses (with federation trust gating) and performing RFC 8693 token exchange when required, so control-plane commands work across regions.

Changes:

  • Added a cross-jurisdiction http.RoundTripper that follows 421 redirects (validated via /.well-known/entire-federation) and performs token exchange on structured 401 hints or bare 401s after a 421.
  • Wired the generated Core API client to use the new cross-juris HTTP client via WithClient(...) in both New() and NewWithBearer(...).
  • Added unit tests covering redirect following, exchange behavior, and trust-gating rules.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
internal/coreapi/cross_juris_transport.go Implements the cross-jurisdiction transport (421 follow + 401-triggered token exchange) and per-origin caching/trust gates.
internal/coreapi/cross_juris_transport_test.go Adds tests for redirect/exchange flows and security gates (federation + same-origin validation).
internal/coreapi/client.go Wraps the generated Core API client with the new cross-jurisdiction HTTP client.

Comment thread internal/coreapi/cross_juris_transport.go
Comment thread internal/coreapi/cross_juris_transport_test.go Outdated
Comment thread internal/coreapi/cross_juris_transport.go
Comment thread internal/coreapi/cross_juris_transport_test.go Outdated
Comment thread internal/coreapi/cross_juris_transport_test.go Outdated
NewWithBearer's doc claimed "no per-request resolution or STS exchange",
which the cross-juris transport now contradicts (it exchanges on a 421).
Update New/NewWithBearer to note the 421-follow + token exchange, and
drop the bare core/api/middleware.go / core/coreapi/mirrors.go path
references (entire-core server files, not present in this repo) in favor
of naming entire-core directly.
- goconst: extract the repeated "/oauth/token" literal to a const
  (nolint G101 — it's a URL path, not a credential).
- gosec G704: annotate the federation fetch; origin is a core the CLI
  already dials and the path is a fixed well-known endpoint, not SSRF.
- -race: capture Authorization headers recorded in httptest handler
  goroutines through a mutex-guarded recorder (HTTP completion isn't a
  happens-before edge; matches the channel pattern in client_test.go).
@toothbrush

Copy link
Copy Markdown
Contributor Author

Ooh hangon i have concerns about this, i'll make it DRAFT again.

@toothbrush
toothbrush marked this pull request as draft June 18, 2026 05:56
@toothbrush
toothbrush requested a review from Copilot June 18, 2026 06:18
@toothbrush

Copy link
Copy Markdown
Contributor Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

1 issue from previous review remains unresolved.

Fix All in Cursor

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 6a23ba4. Configure here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 11 comments.

Comment thread internal/coreapi/cross_juris_transport.go
Comment thread internal/coreapi/cross_juris_transport_test.go
Comment thread internal/coreapi/cross_juris_transport_test.go
Comment thread internal/coreapi/cross_juris_transport_test.go
Comment thread internal/coreapi/cross_juris_transport_test.go
Comment thread internal/coreapi/cross_juris_transport_test.go
Comment thread internal/coreapi/cross_juris_transport_test.go
Comment thread internal/coreapi/cross_juris_transport_test.go
Comment thread internal/coreapi/cross_juris_transport_test.go
Comment thread internal/coreapi/cross_juris_transport_test.go
toothbrush and others added 2 commits June 18, 2026 15:57
All tests own their httptest servers/transports with no process-global
state, so they conform to the repo's t.Parallel() convention.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 41b4654948eb
Match the entiredb staging bed's transport one-for-one:
- drainAndRestoreBody uses io.LimitReader and restores resp.Body even on
  read error (drops the nil-ResponseWriter MaxBytesReader idiom).
- port the four tests the original CLI port dropped: TokenCacheReuses-
  Exchanged, NoInfiniteLoopOn421Chain, ExchangeFailurePropagates401, and
  ExchangeAfter401Then421UsesOriginalSubjectToken (the chained-exchange
  subject_token regression).

goconst/intrange satisfied (hoisted Bearer literals, range-over-int).
@toothbrush
toothbrush marked this pull request as ready for review June 18, 2026 07:07
toothbrush and others added 3 commits June 18, 2026 17:18
exchangeSubjectToken strips oauthTokenPath and PostOAuthToken re-appends
it, but validateExchangeURL only gated scheme and host. A hint whose path
wasn't /oauth/token would make the strip a no-op and POST to a
server-chosen path. Validate the path so the round-trip is honest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: ea9e8dae6384
storeToken always used the fixed 4m cachedTokenTTL and discarded the
expires_in PostOAuthToken returns. A server token shorter than 4m could
be served stale, producing a 401 the cache assumed impossible. Thread
expires_in through and cache for min(expires_in - buffer, cachedTokenTTL),
declining to cache lifetimes shorter than the buffer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 1edf9ab1ef2c
The struct doc claimed the cache TTL "matches the foreign-session token's
lifetime (5 minutes)" while cachedTokenTTL is 4m. Describe the actual
expires_in-honoring, buffered behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: c6abc0aac791
@toothbrush
toothbrush merged commit 847b496 into main Jun 18, 2026
9 checks passed
@toothbrush
toothbrush deleted the fix/coreapi-cross-juris-transport branch June 18, 2026 08:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants