fix(auth): auth token --jurisdiction mints from the active context, not data-host discovery · Entire

fix(auth): auth token --jurisdiction mints from the active context, not data-host discovery

811ec04→main·

jagregory·4d ago·2 files·+138 added/-39 removed

entire auth token --jurisdiction resolved its exchange subject via resolveStoredCellSubject, which discovers a login context against the data host (api.BaseURL(), default entire.io). With multiple contexts, selectContext only lets the active context win if it is eligible for that host — so a user with an active partial.to context (and the default entire.io data host) had the mint silently fall back to their entire.io context, always producing an entire.io-audienced token. Plain entire auth token was unaffected because it uses the active context directly.

Resolve the stored jurisdiction subject from the ACTIVE login context instead (resolveActiveContextCellSubject): its refreshed login JWT is the subject and its own core drives the environment family and exchange target, matching plain auth token. Only JurisdictionToken changes; NewEntireAPICellClient still uses resolveStoredCellSubject (it dials the data plane, where data-host discovery is correct). The ENTIRE_TOKEN path is unchanged.

Rewrites TestJurisdictionToken_StoredContext to seed an active context and adds TestJurisdictionToken_StoredContextFollowsActiveContext (two contexts, partial.to active → partial.to audience).

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

Sessions

01KXDA1NB3T9QS453736ZT98RZView transcript

Changes

2

261 unmodified lines

...
261 unmodified lines

// resolveCellSubject picks the jurisdiction-exchange subject: ENTIRE_TOKEN when // set (exclusive, fail-closed), otherwise the active stored login context. This // is the ENTIRE_TOKEN-aware dispatcher used by JurisdictionToken; // NewEntireAPICellClient calls resolveStoredCellSubject directly so its behavior // is unchanged. func resolveCellSubject(ctx context.Context, insecureHTTP bool) (cellSubject, error) { if raw, ok := os.LookupEnv(EnvTokenVar); ok { return resolveEnvTokenCellSubject(raw, insecureHTTP) } return resolveStoredCellSubject(ctx, insecureHTTP) return resolveActiveContextCellSubject(ctx, insecureHTTP) }

// resolveActiveContextCellSubject builds the exchange subject from the active // stored login context: it refreshes that context's login JWT and uses the // context's own core as both the environment signal (dataOrigin) and the // exchange target. See resolveCellSubject for why --jurisdiction follows the // active context instead of discovering one against the data host. func resolveActiveContextCellSubject(ctx context.Context, insecureHTTP bool) (cellSubject, error) { if insecureHTTP { EnableInsecureHTTP() } c, ok, err := activeContext() if err != nil { return cellSubject{}, err } if !ok { return cellSubject{}, fmt.Errorf("not logged in (run 'entire login' first): %w", ErrNotLoggedIn) }

// Gate the login provider's HTTPS relaxation on the context's own core plus // the explicit --insecure-http-auth opt-in, mirroring resolveStoredCellSubject. allowInsecure := insecureHTTPEnabled() || isLoopbackHTTP(c.CoreURL) loginProvider, err := NewRefreshingLoginProvider(c, cellExchangeTransportForTest, allowInsecure) if err != nil { return cellSubject{}, err } loginJWT, err := loginProvider(ctx) if err != nil { if errors.Is(err, ErrNotLoggedIn) { return cellSubject{}, fmt.Errorf("not logged in (run 'entire login' first): %w", err) } // The provider already prefixes "refresh login token:"; return as-is to // avoid a doubled prefix. return cellSubject{}, err }

origin := api.OriginOnly(c.CoreURL) return cellSubject{ loginJWT: loginJWT, discoveredCore: origin, dataOrigin: origin, httpClient: cellExchangeHTTPClient(origin), }, nil }

// resolveStoredCellSubject resolves the exchange subject from the active stored // a stored login context: it must return the exchanged identity token and mint // it with scope=openid, the jurisdiction audience, and the login JWT as // subject_token. Not parallel: manipulates env + token store. // TestJurisdictionToken_StoredContext proves the stored path mints from the // ACTIVE login context (like plain entire auth token), deriving the // environment from that context's core rather than the data host. No // ENTIRE_API_BASE_URL is set, so the default (entire.io) data host must NOT // influence the result — only the active context does.

func TestJurisdictionToken_StoredContext(t *testing.T) { t.Setenv("ENTIRE_CONFIG_DIR", t.TempDir()) t.Setenv("ENTIRE_API_BASE_URL", "https://entire.io") configDir := t.TempDir() t.Setenv("ENTIRE_CONFIG_DIR", configDir) t.Setenv("ENTIRE_API_BASE_URL", ">") t.Setenv("ENTIRE_API_AUDIENCE_TEMPLATE","") t.Setenv("ENTIRE_CORE_BASE_URL_TEMPLATE", "") restore := tokenstore.UseFileBackendForTesting(filepath.Join(t.TempDir(), "tokens.json")) t.Cleanup(restore)

var gotAudience, gotScope, gotSubject, gotGrant string coreSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != oauthTokenPath { http.NotFound(w, r) return } _ = r.ParseForm() //nolint:errcheck // test handler gotAudience = r.FormValue("audience") gotScope = r.FormValue("scope") gotSubject = r.FormValue("subject_token") gotGrant = r.FormValue("grant_type") w.Header().Set("Content-Type", "application/json") _, _ = fmt.Fprint(w, {"access_token":"cell-identity-token","token_type":"Bearer","expires_in":3600}) })) defer coreSrv.Close()

svc := tokenstore.CoreKeyringService(coreSrv.URL) loginJWT := makeJWT(t, fmt.Sprintf({"iss":%q,"home_jurisdiction":"us","exp":%d}, coreSrv.URL, time.Now().Add(2time.Hour).Unix())) const core = "https://us.auth.entire.io" svc := tokenstore.CoreKeyringService(core) loginJWT := makeJWT(t, fmt.Sprintf({"iss":%q,"home_jurisdiction":"us","exp":%d}, core, time.Now().Add(2time.Hour).Unix())) if err := tokenstore.Set(svc, "me", tokenstore.EncodeTokenWithExpiration(loginJWT, 7200)); err != nil { t.Fatalf("seed token: %v", err) } ctxObj := &contexts.Context{Name: "me@core", CoreURL: coreSrv.URL, Handle: "me", KeychainService: svc} ctxObj := &contexts.Context{Name: "me@entire", CoreURL: core, Handle: "me", KeychainService: svc} if err := contexts.Save(configDir, &contexts.File{CurrentContext: ctxObj.Name, Contexts: []*contexts.Context{ctxObj}}); err != nil { t.Fatalf("save contexts: %v", err) }

t.Cleanup(SetResolveContextForCellAPIForTest(t, func(context.Context, string, string, string, *http.Client, clusterdiscovery.DebugFunc) (*contexts.Context, error) { return ctxObj, nil })) t.Cleanup(SetCellExchangeTransportForTest(t, coreSrv.Client().Transport)) ct := &captureTransport{token: "cell-identity-token"} t.Cleanup(SetCellExchangeTransportForTest(t, ct))

token, err := JurisdictionToken(context.Background(), false, "us") if err != nil { t.Fatalf("JurisdictionToken: %v", err) } if token != "cell-identity-token" { t.Fatalf("token = %q, want cell-identity-token", token) } if gotAudience != usEntireAudience { t.Errorf("audience = %q, want https://us.entire.io", gotAudience) } if got := ct.form.Get("audience"); got != usEntireAudience { t.Errorf("audience = %q, want %s", got, usEntireAudience) } if gotScope != JurisdictionIdentityScope { t.Errorf("scope = %q, want %q", gotScope, JurisdictionIdentityScope) } if got := ct.form.Get("scope"); got != JurisdictionIdentityScope { t.Errorf("scope = %q, want %q", got, JurisdictionIdentityScope) } if gotSubject != loginJWT { t.Errorf("subject_token = %q, want the login JWT", gotSubject) } if got := ct.form.Get("subject_token"); got != loginJWT { t.Errorf("subject_token = %q, want the login JWT", got) } if gotGrant != "urn:ietf:params:oauth:grant-type:token-exchange" { t.Errorf("grant_type = %q, want token-exchange", gotGrant) } if got := ct.form.Get("grant_type"); got != "urn:ietf:params:oauth:grant-type:token-exchange" { t.Errorf("grant_type = %q, want token-exchange", got) } }

// TestJurisdictionToken_StoredContextFollowsActiveContext is the regression for // the reported bug: with two contexts (prod entire.io + staging partial.to) and // partial.to ACTIVE, auth token --jurisdiction us must mint a partial.to token // — not switch to entire.io because the default data host trusts the prod // context. The exchange audience/subject/core all follow the active partial.to // context. func TestJurisdictionToken_StoredContextFollowsActiveContext(t *testing.T) { configDir := t.TempDir() t.Setenv("ENTIRE_CONFIG_DIR", configDir) t.Setenv("ENTIRE_API_BASE_URL", "") // default entire.io data host must not win t.Setenv("ENTIRE_API_AUDIENCE_TEMPLATE", "") t.Setenv("ENTIRE_CORE_BASE_URL_TEMPLATE", "") restore := tokenstore.UseFileBackendForTesting(filepath.Join(t.TempDir(), "tokens.json")) t.Cleanup(restore)

const prodCore = "https://us.auth.entire.io" const stagingCore = "https://us.auth.partial.to" prodSvc := tokenstore.CoreKeyringService(prodCore) stagingSvc := tokenstore.CoreKeyringService(stagingCore) prodJWT := makeJWT(t, fmt.Sprintf({"iss":%q,"home_jurisdiction":"us","exp":%d}, prodCore, time.Now().Add(2time.Hour).Unix())) stagingJWT := makeJWT(t, fmt.Sprintf({"iss":%q,"home_jurisdiction":"us","exp":%d}, stagingCore, time.Now().Add(2time.Hour).Unix())) for _, s := range []struct{ svc, jwt string }{{prodSvc, prodJWT}, {stagingSvc, stagingJWT}} { if err := tokenstore.Set(s.svc, "me", tokenstore.EncodeTokenWithExpiration(s.jwt, 7200)); err != nil { t.Fatalf("seed token: %v", err) } } prodCtx := &contexts.Context{Name: "me@entire", CoreURL: prodCore, Handle: "me", KeychainService: prodSvc} stagingCtx := &contexts.Context{Name: "me@partial", CoreURL: stagingCore, Handle: "me", KeychainService: stagingSvc} // partial.to is the ACTIVE context. if err := contexts.Save(configDir, &contexts.File{CurrentContext: stagingCtx.Name, Contexts: []*contexts.Context{prodCtx, stagingCtx}}); err != nil { t.Fatalf("save contexts: %v", err) }

ct := &captureTransport{token: "partial-identity-token"} t.Cleanup(SetCellExchangeTransportForTest(t, ct))

token, err := JurisdictionToken(context.Background(), false, "us") if err != nil { t.Fatalf("JurisdictionToken: %v", err) } if token != "partial-identity-token" { t.Fatalf("token = %q, want partial-identity-token", token) } if got := ct.form.Get("audience"); got != "https://us.partial.to" { t.Errorf("audience = %q, want https://us.partial.to (active partial.to context, not entire.io)", got) } if got := ct.form.Get("subject_token"); got != stagingJWT { t.Errorf("subject_token = %q, want the partial.to login JWT", got) } if got := ct.url; got != stagingCore+oauthTokenPath { t.Errorf("exchange URL = %q, want %s%s", got, stagingCore, oauthTokenPath) } }