git-remote-entire: ENTIRE_TOKEN env override for CI / workload identity · Entire
git-remote-entire: ENTIRE_TOKEN env override for CI / workload identity
657a7ed·
toothbrush·1mo ago·3 files·+176 added/-18 removed
Let ENTIRE_TOKEN=
When set, derive the home-region core URL from the token's URL-shaped aud
claim (login/sa-session JWTs carry aud=
tokens.ParseClaims already exposes Audience (normalised across string and array forms), so no helper port was needed.
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
Sessions
2f3a007d8690View transcript
Changes
3
cmd
entire/cli/auth
- Aenv_token.go+39
Aenv_token_test.go+92
git-remote-entire
Mmain.go+45/-18
1
2
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
35
36
37
38
39
package auth
import (
"fmt"
"net/url"
"strings"
"github.com/entireio/auth-go/tokens"
)
// EnvTokenVar is the environment variable that, when set, bypasses
// contexts.json and the keyring entirely: its value is used verbatim as the
// login JWT for repo-scoped token exchange. This is the CI / workload-identity
// path — a runner injects a short-lived login or sa-session JWT and clones
// without an interactive `entire login`.
const EnvTokenVar = "ENTIRE_TOKEN"
// CoreURLFromEnvToken derives the home-region core URL from an ENTIRE_TOKEN
// JWT's audience claim. Login and sa-session JWTs carry aud=<home-region URL>,
// which is what STS routing keys on — so we read aud, not iss (iss may be a
// regional core that can't mint the cross-region exchange).
//
// The aud claim is URL-shaped (scheme://host[/path]). It may be a single
// string or an array (RFC 7519 §4.1.3); ParseClaims normalises both to a
// slice. The first URL-shaped audience wins. A token with no URL-shaped aud
// is rejected with a clear error rather than silently falling back to context
// resolution, so a misconfigured CI token fails loudly.
func CoreURLFromEnvToken(rawToken string) (string, error) {
claims, err := tokens.ParseClaims(rawToken)
if err != nil {
return "", fmt.Errorf("parse %s claims: %w", EnvTokenVar, err)
}
for _, aud := range claims.Audience {
if u, err := url.Parse(aud); err == nil && (u.Scheme == "http" || u.Scheme == "https") && u.Host != "" {
return strings.TrimRight(aud, "/"), nil
}
}
return "", fmt.Errorf("%s must be a login or sa-session JWT whose aud is the home-region URL; found no URL-shaped audience claim", EnvTokenVar)
}
1
2
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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
package auth
import (
"encoding/base64"
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCoreURLFromEnvToken(t *testing.T) {
t.Parallel()
tests := []struct {
name string
aud any // nil omits the aud claim entirely
want string
wantErr bool
}{
{
name: "string aud",
aud: "https://core.us.entire.io",
want: "https://core.us.entire.io",
},
{
name: "string aud trailing slash trimmed",
aud: "https://core.us.entire.io/",
want: "https://core.us.entire.io",
},
{
name: "array aud picks first URL-shaped",
aud: []string{"entire-cli", "https://core.eu.entire.io"},
want: "https://core.eu.entire.io",
},
{
name: "opaque string aud rejected",
aud: "some-opaque-audience",
wantErr: true,
},
{
name: "array of opaque audiences rejected",
aud: []string{"aud-a", "aud-b"},
wantErr: true,
},
{
name: "missing aud rejected",
aud: nil,
wantErr: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
payload := map[string]any{"sub": "ci-runner"}
if tc.aud != nil {
payload["aud"] = tc.aud
}
raw, err := json.Marshal(payload)
require.NoError(t, err)
token := makeJWT(t, string(raw))
got, err := CoreURLFromEnvToken(token)
if tc.wantErr {
require.Error(t, err)
assert.Contains(t, err.Error(), EnvTokenVar)
return
}
require.NoError(t, err)
assert.Equal(t, tc.want, got)
})
}
}
func TestCoreURLFromEnvToken_MalformedToken(t *testing.T) {
t.Parallel()
_, err := CoreURLFromEnvToken("not-a-jwt")
require.Error(t, err)
assert.Contains(t, err.Error(), EnvTokenVar)
}
func TestCoreURLFromEnvToken_RejectsAlgNone(t *testing.T) {
t.Parallel()
// alg:none with a URL-shaped aud must still be rejected at the parse layer.
enc := base64.RawURLEncoding
token := enc.EncodeToString([]byte(`{"alg":"none"}`)) + "." +
enc.EncodeToString([]byte(`{"aud":"https://core.us.entire.io"}`)) + "."
_, err := CoreURLFromEnvToken(token)
require.Error(t, err)
assert.Contains(t, err.Error(), EnvTokenVar)
}
resolveCreds Functionality
// resolveCreds builds the repo-scoped token cache, choosing the auth source:
//
// - ENTIRE_TOKEN set: use the env JWT verbatim as the login token, deriving
// the core URL from its aud claim. Skips contexts.json and the keyring
// entirely — the CI / workload-identity path. A non-URL aud is a hard
// error, never a silent fallback to context resolution.
// - otherwise: resolve the login context for this cluster from contexts.json
// (migrating any pre-contexts.json login first) and exchange its stored
// login JWT.
func resolveCreds(ctx context.Context, parsedURL *url.URL, clusterBaseURL string, httpClient *http.Client) (*repocreds.Cache, error) {
if envToken := os.Getenv(auth.EnvTokenVar); envToken != "" {
coreURL, err := auth.CoreURLFromEnvToken(envToken)
if err != nil {
return nil, err //nolint:wrapcheck // CoreURLFromEnvToken already returns a user-facing, ENTIRE_TOKEN-prefixed error
}
debuglog.Printf("authenticating via %s; core=%s", auth.EnvTokenVar, coreURL)
return repocreds.New(coreURL, clusterBaseURL, func(context.Context) (string, error) {
return envToken, nil
}, httpClient), nil
}
// Bridge any pre-contexts.json login so the resolver can find it.
if _, err := auth.MigrateLegacyLoginContext(); err != nil {
debuglog.Printf("legacy login migration: %v", err)
}
// Resolve which login context authenticates this cluster: the cluster's
// cores are taken from the cluster_cores.json cache (or a live
// /.well-known fetch on miss/expiry), then the account is selected from
// local contexts — active context if eligible, else the sole eligible
// one, else an explicit-choice error.
cfgDir := contexts.DefaultConfigDir()
clusterCtx, err := clusterdiscovery.ResolveContextForCluster(ctx, cfgDir, discovery.DefaultCacheDir(), parsedURL.Host, httpClient, debuglog.Printf)
if err != nil {
return nil, err //nolint:wrapcheck // ResolveContextForCluster already returns a user-facing error; preserved verbatim for the "fatal: <msg>" surface
}
// Mint repo-scoped tokens by exchanging the context's login JWT at its
// core's /oauth/token, cached per (repo, action) for this invocation.
return repocreds.New(clusterCtx.CoreURL, clusterBaseURL, func(context.Context) (string, error) {
return auth.LoginTokenForContext(clusterCtx)
}, httpClient), nil
}
// gitActionFromRequest classifies a smart-HTTP request as "pull" or "push" // so the right repo-scoped token can be minted. Returns "" when the // endpoint isn't a recognised git smart-HTTP route.