# fix(auth): derive jurisdiction mint environment from the active context

`bfdc8b2`·

jagregory·5d ago·5 files·+293 added/-13 removed

`entire auth token --jurisdiction <slug>` (-j) mints a jurisdictional identity token, and the shared entire-api cell resolution templates the audience/exchange-core from an environment "family". Both derived that family from the data-API origin, which defaults to prod `entire.io` when `ENTIRE_API_BASE_URL` is unset — so a partial.to login minted a token audienced to `https://us.entire.io` (iss `https://us.auth.entire.io`), which a partial.to cell rejects. Plain `entire auth token` was already correct because it keys off the active login context's core_url.

Fix at the shared resolution point:

- `environmentFamily` no longer lets the *default* data origin mask the login environment. An explicit `ENTIRE_API_BASE_URL` still wins; the default falls through to the discovered login core (the core that issued/trusts the JWT being exchanged), so a partial.to login yields a partial.to family.
- New `auth.MintJurisdictionIdentityToken` exchanges the caller's login credential (active-context JWT or `ENTIRE_TOKEN`) for a jurisdictional identity token whose environment is taken from the context core, never a constant `entire.io` base.
- Wire it into `entire auth token` behind `--jurisdiction`/`-j`.

Before (active context us.auth.partial.to): aud=https://us.entire.io.
After: aud=https://us.partial.to (exchange at https://us.auth.partial.to). The entire.io context is unchanged.

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

## Sessions

879dcfce8ae4View transcript

## Changes

5

- MCLAUDE.md+7/-1

- cmd/entire/cli

- Mauth.go+22

- auth

- Mcell_data_api.go+82/-9

- Mcell_data_api_test.go+123/-3

- Mauth_token_test.go+59

```  
46 unmodified lines
47
48
49
50
50
51
52
53
54
55
56
57
58
59

46 unmodified lines

options, summary provider). Agent CRUD lives under `entire agent`.
- `auth`: `login`, `logout`, `status`, `contexts`, `use`, plus the hidden `token` (prints the active control-plane bearer to stdout for scripting/curl; honors `ENTIRE_TOKEN`, else the refreshed active-context login JWT). `logout` honors `ENTIRE_TOKEN`, else the refreshed active-context login JWT). With `--jurisdiction`/`-j <slug>` `token` instead exchanges that same credential for a jurisdictional identity token (scope=openid) audienced to the slug in the **active context's environment** — a partial.to login mints a partial.to jurisdiction token, not a hardwired entire.io one (see `auth.MintJurisdictionIdentityToken`; the environment is derived from the active context/env-token aud, never a constant `entire.io` base). `logout` takes `--everywhere` (revoke every session on the active core, not just the current one) and `--all-contexts` (log out of every saved login)
- `doctor`: bare runs the scan-and-fix flow, plus `trace`, `logs`, `bundle`
```

MCLAUDE.md+7/-1

```
137 unmodified lines

```  
46 unmodified lines

// login JWT, refreshed if it's near expiry — and prints nothing but the token
// (errors and the not-logged-in hint go to stderr) so command substitution
// stays clean.
//
// With --jurisdiction/-j it instead exchanges that same login credential for a
// jurisdictional identity token (scope=openid) audienced to the given
// jurisdiction in the ACTIVE CONTEXT'S environment — so a partial.to login mints
// a partial.to jurisdiction token, not a hardwired entire.io one. This is the
// credential a repo-hosting entire-api cell accepts (the control-plane bearer
// and the BFF api-access token are not accepted there).
func newAuthTokenCmd() *cobra.Command {
	var insecureHTTPAuth bool
	var jurisdiction string
	cmd := &cobra.Command{
		Use:    "token",
		Short:  "Print the active control-plane bearer token (for scripting)",
		...
	}
	addInsecureHTTPAuthFlag(cmd, &insecureHTTPAuth)
	cmd.Flags().StringVarP(&jurisdiction, "jurisdiction", "j", "", "Mint a jurisdictional identity token for this jurisdiction slug (e.g. \"us\"), audienced to the active context's environment, instead of the control-plane bearer")
	return cmd
}
```

```  
26 unmodified lines

```

// clustersAPIPath is entire-core's cluster catalog endpoint.
clustersAPIPath = "/api/v1/clusters"

// familyEntireIO / familyPartialTo are the registrable apexes the two Entire
// environments live under (prod / staging). Used to template jurisdiction
audience/core URLs from whichever environment the caller is logged into.
familyEntireIO  = "entire.io"
familyPartialTo = "partial.to"

// jurisdictionLabelPattern bounds a home_jurisdiction claim to a single DNS
217 unmodified lines

}

```

func environmentFamily(dataOrigin, discoveredCore string) string {
	if fam := entireDomainFamily(dataOrigin); fam != "" {
		return fam
	}
	if fam := entireDomainFamily(discoveredCore); fam != "" {
		return fam
	}
	return entireDomainFamily(dataOrigin)
}

// jurisdictionAudience returns the aud the entire-api cell for `jurisdiction`
151 unmodified lines

return strings.TrimRight(chosen.APIURL, "/"), nil
}

// MintJurisdictionIdentityToken exchanges subjectToken (the caller's login JWT,
// or ENTIRE_TOKEN) for a jurisdictional identity token (scope=openid) audienced
// to `jurisdiction` in the environment named by contextCoreURL.
//
// contextCoreURL is the active login context's core (or, under ENTIRE_TOKEN,
// the token's aud) — the same prod-vs-staging signal `entire auth token` keys off —
// so the mint follows the environment the caller is logged into rather than the
// data-API default (prod entire.io). It drives both the exchange core
// (https://<jurisdiction>.auth.<family>) and the audience
// (https://<jurisdiction>.<family>), honouring the ENTIRE_CORE_BASE_URL_TEMPLATE
// / ENTIRE_API_AUDIENCE_TEMPLATE overrides and loopback local-dev cores exactly
// as the entire-api cell path does.
//
// Returns the minted token plus the audience and exchange core it was minted
// against (surfaced for diagnostics and tests).
func MintJurisdictionIdentityToken(ctx context.Context, contextCoreURL, subjectToken, jurisdiction string, insecureHTTP bool) (token, audience, exchangeCore string, err error) {
	jurisdiction = strings.TrimSpace(jurisdiction)
	if jurisdiction == "" {
		return "", "", "", errors.New("jurisdiction must not be empty")
	}
	if !jurisdictionLabelPattern.MatchString(jurisdiction) {
		return "", "", "", fmt.Errorf("jurisdiction %q is not a valid label; refusing to route", jurisdiction)
	}
	if strings.TrimSpace(subjectToken) == "" {
		return "", "", "", errors.New("no login credential to exchange for a jurisdictional token")
	}
	if strings.TrimSpace(contextCoreURL) == "" {
		return "", "", "", errors.New("no login core to derive the jurisdiction environment from")
	}
	if insecureHTTP {
		EnableInsecureHTTP()
	}

// Pass contextCoreURL as both the data origin and the discovered core: the
	// mint's environment is the login context, never the data-API origin, so even
	// the ENTIRE_API_BASE_URL override branch inside environmentFamily resolves
	// from the context core (not a possibly-prod data URL a script may have set).
	audience = jurisdictionAudience(jurisdiction, contextCoreURL, contextCoreURL)
	exchangeCore = jurisdictionCoreURL(jurisdiction, contextCoreURL, contextCoreURL)
	if err := requireSafeExchangeURL("entire-core", exchangeCore); err != nil {
		return "", "", "", err
	}

httpClient := &http.Client{Timeout: cellDataAPITimeout, Transport: cellExchangeTransportForTest}
	token, err = exchangeJurisdictionToken(ctx, exchangeCore, subjectToken, audience, httpClient)
	if err != nil {
		return "", "", "", fmt.Errorf("exchange jurisdictional identity token: %w", err)
	}
	return token, audience, exchangeCore, nil
}

func exchangeJurisdictionToken(ctx context.Context, coreURL, loginJWT, audience string, httpClient *http.Client) (string, error) {
	if coreURL == "" {
		return "", errors.New("no entire-core URL configured for jurisdiction token exchange")
	}
	return "", nil
}
```

```  
2 unmodified lines

3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
30 unmodified lines
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109

2 unmodified lines

import (
	"bytes"
	"encoding/base64"
	"fmt"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"testing"

"github.com/entireio/cli/cmd/entire/cli/auth"
	"github.com/stretchr/testify/require"
)

// redirectTransport rewrites every request to point at target, so a mint that
// dials a templated prod host (https://us.auth.partial.to) is intercepted by a
// loopback httptest server. It records the last request it saw.
type redirectTransport struct {
	target *url.URL
}

func (rt redirectTransport) RoundTrip(r *http.Request) (*http.Response, error) {
	clone := r.Clone(r.Context())
	clone.URL.Scheme = rt.target.Scheme
	clone.URL.Host = rt.target.Host
	clone.Host = rt.target.Host
	return http.DefaultTransport.RoundTrip(clone)
}

// makeTestJWT builds an unsigned JWT with the given payload JSON. ParseClaims
// (used by ENTIRE_TOKEN resolution) reads the payload without verifying the
// signature, so an unsigned token is enough to exercise the resolution path.
30 unmodified lines

require.Empty(t, errOut.String())
})

t.Run("jurisdiction mints an identity token audienced to the context environment", func(t *testing.T) {
	// Neutralise any inherited overrides so the environment family is derived
	// purely from the context core (the env token's aud).
	t.Setenv("ENTIRE_API_BASE_URL", "")
	t.Setenv("ENTIRE_API_AUDIENCE_TEMPLATE", "")
	t.Setenv("ENTIRE_CORE_BASE_URL_TEMPLATE", "")

var gotAudience, gotSubject string
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_ = r.ParseForm() //nolint:errcheck // test handler
		gotAudience = r.FormValue("audience")
		gotSubject = r.FormValue("subject_token")
		w.Header().Set("Content-Type", "application/json")
		_, _ = fmt.Fprint(w, `{"access_token":"partial-juris-token","token_type":"Bearer","expires_in":3600}`)
	}))
	defer srv.Close()

target, err := url.Parse(srv.URL)
	require.NoError(t, err)
	t.Cleanup(auth.SetCellExchangeTransportForTest(t, redirectTransport{target: target}))

// ENTIRE_TOKEN whose aud is a partial.to core: the mint's environment must
	// follow that (partial.to), NOT a hardwired entire.io base.
	envTok := makeTestJWT(t, `{"sub":"ci","aud":"https://us.auth.partial.to"}`)
	t.Setenv("ENTIRE_TOKEN", envTok)

cmd := newAuthTokenCmd()
	cmd.SetArgs([]string{"--jurisdiction", "us"})
	var out, errOut bytes.Buffer
	cmd.SetOut(&out)
	cmd.SetErr(&errOut)
	require.NoError(t, cmd.ExecuteContext(t.Context()))

require.Equal(t, "partial-juris-token\n", out.String())
	require.Empty(t, errOut.String())
	require.Equal(t, "https://us.partial.to", gotAudience, "aud must follow the partial.to context, not entire.io")
	require.Equal(t, envTok, gotSubject)
})

t.Run("not logged in errors silently with a hint", func(t *testing.T) {
	// Isolated empty config so there's no active context to resolve.
	t.Setenv("ENTIRE_CONFIG_DIR", t.TempDir())
