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

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