Skip to content

Commit 4e91e1d

Browse files
committed
Tell an agent's owner to disconnect it, not rotate its secret
auth logout and auth revoke on an agent profile said to rotate the client secret in Basecamp to end the agent's access. My > Agent has no rotate control (reconnecting is the rotation), and a rotation leaves the agent's tokens alive for their hour anyway. Disconnect is what ends its access, so that is what both now say. auth agent's help also stops calling every agent one an administrator created: a personal agent is created by its owner when they connect it.
1 parent 943dbb5 commit 4e91e1d

5 files changed

Lines changed: 31 additions & 6 deletions

File tree

‎internal/auth/agent_test.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,7 @@ func TestAgentRevokeIsRefusedAndKeepsTheCredential(t *testing.T) {
421421
err := m.RevokeStored(context.Background())
422422
require.Error(t, err)
423423
assert.Equal(t, output.CodeUsage, output.AsError(err).Code)
424-
assert.Contains(t, err.Error(), "rotate the client secret")
424+
assert.Contains(t, err.Error(), "disconnect the agent in Basecamp")
425425
assert.Empty(t, as.revokeCalls(), "a revocation was sent for an agent self-token")
426426

427427
creds, loadErr := m.store.Load(key)

‎internal/auth/revoke.go‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ const (
4545
// RevokeSkippedAgent: an agent self-token is minted on demand from a
4646
// client id and secret, so revoking one accomplishes nothing — the
4747
// same client mints another on the next command. Ending an agent's
48-
// access means rotating the client secret in Basecamp.
48+
// access means disconnecting it in Basecamp, which kills the client
49+
// secret and every token minted from it. (Basecamp has no separate
50+
// "rotate secret": reconnecting is the rotation.)
4951
RevokeSkippedAgent = "agent"
5052
)
5153

@@ -194,7 +196,7 @@ func (m *Manager) RevokeStored(ctx context.Context) error {
194196
"Forget the credential locally instead: basecamp auth logout")
195197
case RevokeSkippedAgent:
196198
return output.ErrUsageHint("An agent self-token is not worth revoking: the client that minted it can mint another",
197-
"Forget the credential locally instead (basecamp auth logout), and rotate the client secret in Basecamp to end the agent's access")
199+
"Forget the credential locally instead (basecamp auth logout), and disconnect the agent in Basecamp to end its access")
198200
}
199201
if err := m.Revoke(ctx, creds); err != nil {
200202
// Keep the failure's taxonomy — a transport failure or a 5xx

‎internal/commands/auth.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1607,7 +1607,7 @@ func describeLogout(done string, result *auth.LogoutResult) (summary string, fie
16071607
summary = done + " (forgot the imported token; it stays valid until revoked in Basecamp)"
16081608
case result.Skipped == auth.RevokeSkippedAgent:
16091609
fields["reason"] = result.Skipped
1610-
summary = done + " (forgot the agent credential; rotate the client secret in Basecamp to end its access)"
1610+
summary = done + " (forgot the agent credential; it stays connected until you disconnect the agent in Basecamp)"
16111611
case result.Err != nil:
16121612
fields["reason"] = result.Err.Error()
16131613
fields["remaining"] = result.Remaining

‎internal/commands/auth_agent.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ func newAuthAgentCmd() *cobra.Command {
1919
cmd := &cobra.Command{
2020
Use: "agent",
2121
Short: "Connect this computer to a Basecamp agent",
22-
Long: `Connect this computer to a Basecamp agent — an identity your Basecamp
23-
administrator created — so commands run as that agent.
22+
Long: `Connect this computer to a Basecamp agent — your own personal agent, or
23+
one your Basecamp administrator created — so commands run as that agent.
2424
2525
The connection hands this CLI the agent's own OAuth client, which mints the
2626
access tokens it spends. Nobody pastes a credential: you approve the

‎internal/commands/auth_logout_test.go‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,19 @@ func bc5LogoutCredentials(s *revocationServer) *auth.Credentials {
121121
}
122122
}
123123

124+
// agentLogoutCredentials is a connected agent's stored credential: its own
125+
// client id and secret, and a self-token minted from them.
126+
func agentLogoutCredentials(s *revocationServer) *auth.Credentials {
127+
return &auth.Credentials{
128+
AccessToken: "agent-at",
129+
OAuthType: "agent",
130+
ClientID: "bc-agent-1",
131+
ClientSecret: "agent-secret",
132+
TokenEndpoint: s.srv.URL + "/oauth/tokens",
133+
Scope: "full",
134+
}
135+
}
136+
124137
func decodeLogoutJSON(t *testing.T, buf *bytes.Buffer) map[string]any {
125138
t.Helper()
126139
var envelope struct {
@@ -181,6 +194,16 @@ func TestAuthLogoutHumanCopy(t *testing.T) {
181194
assert.False(t, app.Auth.IsAuthenticated())
182195
})
183196

197+
t.Run("agent", func(t *testing.T) {
198+
s := startRevocationServer(t)
199+
app, buf := newLogoutTestApp(t, s, output.FormatStyled, agentLogoutCredentials(s))
200+
require.NoError(t, runLogout(t, app))
201+
assert.Contains(t, buf.String(), "Logged out (forgot the agent credential; it stays connected until you disconnect the agent in Basecamp)")
202+
assert.NotContains(t, buf.String(), "rotate", "Basecamp has no rotate-secret control to send anyone to")
203+
assert.Empty(t, s.revoked(), "an agent self-token is minted on demand; revoking one ends nothing")
204+
assert.False(t, app.Auth.IsAuthenticated())
205+
})
206+
184207
t.Run("not logged in", func(t *testing.T) {
185208
s := startRevocationServer(t)
186209
app, buf := newLogoutTestApp(t, s, output.FormatStyled, nil)

0 commit comments

Comments
 (0)