auth: factor shared per-context manager + reauth-error mapping · Entire
auth: factor shared per-context manager + reauth-error mapping
8af3492→main·
toothbrush·1mo ago·1 file·+70 added/-59 removed
NewRefreshingLoginProvider and NewRefreshingResourceProvider shared ~30 near-identical lines (validation, tokenmanager.New, the reauth error switch). Extract newContextTokenManager + contextReauthError; the two providers now differ only in Refresh() vs Token(req) and the residual error wording.
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
Sessions
6f9136693ab0View transcript
Changes
1
cmd/entire/cli/auth
Mrefresh.go+70/-59
103 unmodified lines
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
15 unmodified lines
179
180
181
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
182
183
144
184
185
146
147
148
149
150
186
187
153
154
155
156
157
158
188
189
190
191
192
193
194
7 unmodified lines
202
203
204
172
173
174
175
176
177
205
206
207
208
209
210
211
212
213
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
214
215
200
216
217
202
203
204
218
219
220
208
209
210
211
212
213
221
222
223
224
225
226
227
103 unmodified lines
return nil
}// newContextTokenManager builds the per-context auth-go tokenmanager that both
// NewRefreshingLoginProvider and NewRefreshingResourceProvider sit on. Keying
// Issuer on c.CoreURL is the whole point: store reads, the refresh grant, and
// the STS exchange all target that context's core (the bug the singleton
// manager — pinned to AuthBaseURL — has when the active context lives on a
// different core).
//
// STSPath is set unconditionally even for the login-only provider: Refresh()
// never reaches the exchange path, so an unused STSPath is harmless, and a
// single config keeps the two providers from drifting.
//
// transport carries the caller's TLS configuration; allowInsecureHTTP permits
// an http:// core/resource for loopback/dev.
func newContextTokenManager(c *contexts.Context, transport http.RoundTripper, allowInsecureHTTP bool) (*tokenmanager.Manager, error) {
if c == nil {
return nil, errors.New("nil context")
}
if c.KeychainService == "" || c.Handle == "" {
return nil, fmt.Errorf("context %q has no keychain slot", c.Name)
}
mgr, err := tokenmanager.New(tokenmanager.Config{
Issuer: strings.TrimRight(c.CoreURL, "/"),
ClientID: CurrentProvider().ClientID,
STSPath: CurrentProvider().STSPath,
RefreshPath: CurrentProvider().TokenPath,
Store: contextTokenStore{service: c.KeychainService, handle: c.Handle},
Transport: transport,
AllowInsecureHTTP: allowInsecureHTTP,
UserAgent: CurrentProvider().ClientID,
})
if err != nil {
return nil, fmt.Errorf("init token manager for context %q: %w", c.Name, err)
}
return mgr, nil
}
// contextReauthError maps the two re-auth sentinels a per-context manager can
// return into a single friendly message that names the context and its core
// (so a multi-core user logs back into the right one — matching
// clusterdiscovery.RenderLoginHint's idiom). Returns nil when err is neither
// sentinel, leaving the caller to wrap the residual error in its own terms
// (refresh vs exchange).
func contextReauthError(c *contexts.Context, err error) error {
coreURL := strings.TrimRight(c.CoreURL, "/")
relogin := fmt.Sprintf("ENTIRE_AUTH_BASE_URL=%s entire login", coreURL)
switch {
case errors.Is(err, tokenmanager.ErrReauthRequired):
return fmt.Errorf("login session for %q (%s) expired; run `%s` to re-authenticate", c.Name, coreURL, relogin)
case errors.Is(err, tokenmanager.ErrNotLoggedIn):
return fmt.Errorf("no usable login for %q (%s); run `%s`", c.Name, coreURL, relogin)
}
return nil
}
// NewRefreshingLoginProvider returns a login-JWT provider (the shape
// repocreds wants) for context c that transparently re-mints an expired
// login JWT from the stored refresh token.
15 unmodified lines
// transport carries the caller's TLS configuration; allowInsecureHTTP
// permits an http:// core for loopback/dev.
func NewRefreshingLoginProvider(c *contexts.Context, transport http.RoundTripper, allowInsecureHTTP bool) (func(context.Context) (string, error), error) {
if c == nil {
return nil, errors.New("nil context")
}
if c.KeychainService == "" || c.Handle == "" {
return nil, fmt.Errorf("context %q has no keychain slot", c.Name)
}
mgr, err := tokenmanager.New(tokenmanager.Config{
Issuer: strings.TrimRight(c.CoreURL, "/"),
ClientID: CurrentProvider().ClientID,
RefreshPath: CurrentProvider().TokenPath,
Store: contextTokenStore{service: c.KeychainService, handle: c.Handle},
Transport: transport,
AllowInsecureHTTP: allowInsecureHTTP,
UserAgent: CurrentProvider().ClientID,
})
mgr, err := newContextTokenManager(c, transport, allowInsecureHTTP)
if err != nil {
return nil, fmt.Errorf("init token manager for context %q: %w", c.Name, err)
return nil, err
}
name := c.Name
coreURL := strings.TrimRight(c.CoreURL, "/")
// Name the core in the re-login hint so a multi-core user logs back
// into the right one; matches clusterdiscovery.RenderLoginHint's idiom.
relogin := fmt.Sprintf("ENTIRE_AUTH_BASE_URL=%s entire login", coreURL)
return func(ctx context.Context) (string, error) {
tok, err := mgr.Refresh(ctx)
switch {
case errors.Is(err, tokenmanager.ErrReauthRequired):
return "", fmt.Errorf("login session for %q (%s) expired; run `%s` to re-authenticate", name, coreURL, relogin)
case errors.Is(err, tokenmanager.ErrNotLoggedIn):
return "", fmt.Errorf("no usable login for %q (%s); run `%s`", name, coreURL, relogin)
case err != nil:
if mapped := contextReauthError(c, err); mapped != nil {
return "", mapped
}
if err != nil {
return "", fmt.Errorf("refresh login token: %w", err)
}
return tok, nil
}
return nil
}
// control plane / cluster cases, where the host is the core), this performs
// the token exchange the data API requires.
//
// Both the silent login-JWT re-mint and the exchange run through a
// tokenmanager keyed on c.CoreURL as Issuer, so store reads, refresh, and the
// STS endpoint all target the right core — the same fix the control-plane
// per-context provider applies, extended with an exchange step. resourceOrigin
// must already be origin-only (no path); audience is passed verbatim as the
// RFC 8693 audience param. Exchanged tokens are cached in-process by the
// Both the silent login-JWT re-mint and the exchange run through the shared
// per-context tokenmanager (newContextTokenManager). resourceOrigin must
// already be origin-only (no path); audience is passed verbatim as the RFC
// 8693 audience param. Exchanged tokens are cached in-process by the
// tokenmanager for the life of this process.
//
// transport carries the caller's TLS configuration; allowInsecureHTTP permits
// an http:// core/resource for loopback/dev.
func NewRefreshingResourceProvider(c *contexts.Context, resourceOrigin, audience string, transport http.RoundTripper, allowInsecureHTTP bool) (func(context.Context) (string, error), error) {
if c == nil {
return nil, errors.New("nil context")
}
if c.KeychainService == "" || c.Handle == "" {
return nil, fmt.Errorf("context %q has no keychain slot", c.Name)
}
mgr, err := tokenmanager.New(tokenmanager.Config{
Issuer: strings.TrimRight(c.CoreURL, "/"),
ClientID: CurrentProvider().ClientID,
STSPath: CurrentProvider().STSPath,
RefreshPath: CurrentProvider().TokenPath,
Store: contextTokenStore{service: c.KeychainService, handle: c.Handle},
Transport: transport,
AllowInsecureHTTP: allowInsecureHTTP,
UserAgent: CurrentProvider().ClientID,
})
mgr, err := newContextTokenManager(c, transport, allowInsecureHTTP)
if err != nil {
return nil, fmt.Errorf("init token manager for context %q: %w", c.Name, err)
return nil, err
}
name := c.Name
coreURL := strings.TrimRight(c.CoreURL, "/")
relogin := fmt.Sprintf("ENTIRE_AUTH_BASE_URL=%s entire login", coreURL)
req := tokenmanager.TokenRequest{Resource: resourceOrigin, Audience: audience}
return func(ctx context.Context) (string, error) {
tok, err := mgr.Token(ctx, req)
switch {
case errors.Is(err, tokenmanager.ErrReauthRequired):
return "", fmt.Errorf("login session for %q (%s) expired; run `%s` to re-authenticate", name, coreURL, relogin)
case errors.Is(err, tokenmanager.ErrNotLoggedIn):
return "", fmt.Errorf("no usable login for %q (%s); run `%s`", name, coreURL, relogin)
case err != nil:
if mapped := contextReauthError(c, err); mapped != nil {
return "", mapped
}
if err != nil {
return "", fmt.Errorf("exchange token for %s: %w", resourceOrigin, err)
}
return tok, nil
}
}