# 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)
	}
}
