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
cmd/entire/cli/auth
Mcell_data_api.go+57/-6
Mcell_data_api_test.go+81/-33
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) } }