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
5f649d5·
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).
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
httpClient *http.Client
// 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.
// resolveCellSubject picks the jurisdiction-exchange subject for
// JurisdictionToken (the `entire auth token --jurisdiction` scripting helper):
// ENTIRE_TOKEN when set (exclusive, fail-closed), otherwise the ACTIVE stored
// login context.
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(2*time.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(2*time.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("token = %q, want cell-identity-token", token)
}
if gotAudience != usEntireAudience {
t.Errorf("audience = %q, want https://us.entire.io", gotAudience)
}
}
// 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"))
defer 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(2*time.Hour).Unix()))
stagingJWT := makeJWT(t, fmt.Sprintf(`{"iss":%%q,"home_jurisdiction":"us","exp":%d}`, stagingCore, time.Now().Add(2*time.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)
}
}