Skip to content

Commit cf2a831

Browse files
isaacrowntreeclaude
andcommitted
fix: separate bot and user identity in auth storage
Bot and user tokens return different identities from auth.test — the bot returns the bot's user ID (e.g. U0AC6DLSHRV/slackbuzz) while the user token returns the human's (e.g. U018Y0Y9HSN/isaac). Previously both called StoreUserInfo which shared a single user_id/user_name, so whichever token was stored last would overwrite the other. Now bot identity is stored in separate bot_user_id/bot_user_name keys (keyring + auth.yml), and self-DM detection uses client.AuthUserID() which calls auth.test directly on the active token instead of reading potentially stale stored data. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 687877d commit cf2a831

7 files changed

Lines changed: 122 additions & 35 deletions

File tree

internal/api/client.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,13 @@ func NewClient(token string) *Client {
3737
func (c *Client) Token() string {
3838
return c.token
3939
}
40+
41+
// AuthUserID calls auth.test to get the user ID for this client's token.
42+
// Returns empty string on failure.
43+
func (c *Client) AuthUserID() string {
44+
resp, err := c.Slack.AuthTest()
45+
if err != nil {
46+
return ""
47+
}
48+
return resp.UserID
49+
}

internal/auth/keyring.go

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ import (
88
)
99

1010
const (
11-
serviceName = "slack-cli"
12-
botTokenKey = "bot_token"
13-
userTokenKey = "user_token"
14-
teamIDKey = "team_id"
15-
teamNameKey = "team_name"
16-
userIDKey = "user_id"
17-
userNameKey = "user_name"
11+
serviceName = "slack-cli"
12+
botTokenKey = "bot_token"
13+
userTokenKey = "user_token"
14+
teamIDKey = "team_id"
15+
teamNameKey = "team_name"
16+
userIDKey = "user_id"
17+
userNameKey = "user_name"
18+
botUserIDKey = "bot_user_id"
19+
botUserNameKey = "bot_user_name"
1820
)
1921

2022
// StoreBotToken saves a bot token (xoxb-) to the OS keyring, falling back to plaintext.
@@ -43,22 +45,19 @@ func StoreTeamInfo(teamID, teamName string) error {
4345
return ac.Save()
4446
}
4547

46-
// ResolveUserID returns the user's Slack ID, fetching it via auth.test if not
47-
// already stored (e.g. tokens saved before UserID tracking was added).
48-
// The result is cached to the keyring/file for future calls.
48+
// ResolveUserID returns the human user's Slack ID, fetching it via auth.test
49+
// on the user token if not already stored. Only uses the user token (not bot)
50+
// to ensure the returned ID is the human's, not the bot's.
4951
func ResolveUserID() (userID, userName string, err error) {
5052
userID, userName = GetUserInfo()
5153
if userID != "" {
5254
return userID, userName, nil
5355
}
5456

55-
// Try user token first, then bot token
57+
// Only use user token bot token would return the bot's identity
5658
token, tokenErr := GetUserToken()
5759
if tokenErr != nil || token == "" {
58-
token, tokenErr = GetBotToken()
59-
if tokenErr != nil || token == "" {
60-
return "", "", fmt.Errorf("no tokens available to resolve user ID")
61-
}
60+
return "", "", fmt.Errorf("no user token available to resolve user ID (bot token returns bot identity, not yours)")
6261
}
6362

6463
info, err := ValidateToken(token)
@@ -85,7 +84,7 @@ func GetUserToken() (string, error) {
8584
})
8685
}
8786

88-
// StoreUserInfo saves the authenticated user's Slack ID and username.
87+
// StoreUserInfo saves the human user's Slack ID and username (from user token auth.test).
8988
func StoreUserInfo(userID, userName string) error {
9089
_ = keyring.Set(serviceName, userIDKey, userID)
9190
_ = keyring.Set(serviceName, userNameKey, userName)
@@ -97,7 +96,19 @@ func StoreUserInfo(userID, userName string) error {
9796
return ac.Save()
9897
}
9998

100-
// GetUserInfo retrieves the stored user ID and username.
99+
// StoreBotUserInfo saves the bot's Slack user ID and username (from bot token auth.test).
100+
func StoreBotUserInfo(userID, userName string) error {
101+
_ = keyring.Set(serviceName, botUserIDKey, userID)
102+
_ = keyring.Set(serviceName, botUserNameKey, userName)
103+
104+
// Also persist to file as fallback
105+
ac, _ := config.LoadAuth()
106+
ac.BotUserID = userID
107+
ac.BotUserName = userName
108+
return ac.Save()
109+
}
110+
111+
// GetUserInfo retrieves the stored human user ID and username.
101112
func GetUserInfo() (userID, userName string) {
102113
userID, _ = keyring.Get(serviceName, userIDKey)
103114
userName, _ = keyring.Get(serviceName, userNameKey)
@@ -112,6 +123,21 @@ func GetUserInfo() (userID, userName string) {
112123
return ac.UserID, ac.UserName
113124
}
114125

126+
// GetBotUserInfo retrieves the stored bot user ID and username.
127+
func GetBotUserInfo() (userID, userName string) {
128+
userID, _ = keyring.Get(serviceName, botUserIDKey)
129+
userName, _ = keyring.Get(serviceName, botUserNameKey)
130+
if userID != "" {
131+
return
132+
}
133+
134+
ac, err := config.LoadAuth()
135+
if err != nil {
136+
return "", ""
137+
}
138+
return ac.BotUserID, ac.BotUserName
139+
}
140+
115141
// GetTeamInfo retrieves the stored team ID and name.
116142
func GetTeamInfo() (teamID, teamName string) {
117143
teamID, _ = keyring.Get(serviceName, teamIDKey)
@@ -135,6 +161,8 @@ func ClearTokens() error {
135161
_ = keyring.Delete(serviceName, teamNameKey)
136162
_ = keyring.Delete(serviceName, userIDKey)
137163
_ = keyring.Delete(serviceName, userNameKey)
164+
_ = keyring.Delete(serviceName, botUserIDKey)
165+
_ = keyring.Delete(serviceName, botUserNameKey)
138166

139167
ac := &config.AuthConfig{}
140168
_ = ac.Clear()

internal/auth/keyring_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,49 @@ func TestStoreAndGetUserInfo(t *testing.T) {
190190
}
191191
}
192192

193+
func TestStoreAndGetBotUserInfo(t *testing.T) {
194+
keyring.MockInit()
195+
setConfigDir(t)
196+
197+
if err := StoreBotUserInfo("U_BOT_123", "mybot"); err != nil {
198+
t.Fatalf("StoreBotUserInfo() error: %v", err)
199+
}
200+
201+
botUserID, botUserName := GetBotUserInfo()
202+
if botUserID != "U_BOT_123" {
203+
t.Errorf("GetBotUserInfo() userID = %q, want %q", botUserID, "U_BOT_123")
204+
}
205+
if botUserName != "mybot" {
206+
t.Errorf("GetBotUserInfo() userName = %q, want %q", botUserName, "mybot")
207+
}
208+
}
209+
210+
func TestBotAndUserInfoSeparate(t *testing.T) {
211+
keyring.MockInit()
212+
setConfigDir(t)
213+
214+
// Store bot info
215+
if err := StoreBotUserInfo("U_BOT", "bot-name"); err != nil {
216+
t.Fatalf("StoreBotUserInfo() error: %v", err)
217+
}
218+
219+
// Store user info — should NOT overwrite bot info
220+
if err := StoreUserInfo("U_HUMAN", "human-name"); err != nil {
221+
t.Fatalf("StoreUserInfo() error: %v", err)
222+
}
223+
224+
// Verify both are stored independently
225+
botID, botName := GetBotUserInfo()
226+
userID, userName := GetUserInfo()
227+
228+
if botID != "U_BOT" || botName != "bot-name" {
229+
t.Errorf("GetBotUserInfo() = (%q, %q), want (%q, %q)", botID, botName, "U_BOT", "bot-name")
230+
}
231+
if userID != "U_HUMAN" || userName != "human-name" {
232+
t.Errorf("GetUserInfo() = (%q, %q), want (%q, %q)", userID, userName, "U_HUMAN", "human-name")
233+
}
234+
}
235+
193236
func TestClearTokensFileFallback(t *testing.T) {
194237
keyring.MockInitWithError(fmt.Errorf("keyring unavailable"))
195238
setConfigDir(t)

internal/config/auth_config.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ type AuthConfig struct {
1414
ConfigToken string `yaml:"config_token,omitempty"` // App configuration token for manifest API
1515
TeamID string `yaml:"team_id,omitempty"`
1616
TeamName string `yaml:"team_name,omitempty"`
17-
UserID string `yaml:"user_id,omitempty"`
18-
UserName string `yaml:"user_name,omitempty"`
17+
UserID string `yaml:"user_id,omitempty"` // Human user ID (from user token)
18+
UserName string `yaml:"user_name,omitempty"` // Human username (from user token)
19+
BotUserID string `yaml:"bot_user_id,omitempty"` // Bot user ID (from bot token)
20+
BotUserName string `yaml:"bot_user_name,omitempty"` // Bot username (from bot token)
1921
}
2022

2123
// AuthFile returns the path to the auth config file.
@@ -64,5 +66,7 @@ func (a *AuthConfig) Clear() error {
6466
a.TeamName = ""
6567
a.UserID = ""
6668
a.UserName = ""
69+
a.BotUserID = ""
70+
a.BotUserName = ""
6771
return os.Remove(AuthFile())
6872
}

pkg/cmd/app/create.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ func storeDetectedToken(ios *iostreams.IOStreams, cs *iostreams.ColorScheme, tok
270270
fmt.Fprintf(ios.ErrOut, "Warning: failed to store bot token: %v\n", storeErr)
271271
} else {
272272
_ = auth.StoreTeamInfo(info.TeamID, info.Team)
273-
_ = auth.StoreUserInfo(info.UserID, info.User)
273+
_ = auth.StoreBotUserInfo(info.UserID, info.User)
274274
fmt.Fprintf(ios.Out, "%s Bot token saved (%s on %s)\n\n", cs.Green("✓"), cs.Bold(info.User), cs.Bold(info.Team))
275275
storedBot = true
276276
}

pkg/cmd/auth/login.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,13 @@ func loginRun(opts *loginOptions) error {
124124
}
125125
}
126126

127-
// Store team and user info
127+
// Store team info and token-specific user info
128128
_ = auth.StoreTeamInfo(info.TeamID, info.Team)
129-
_ = auth.StoreUserInfo(info.UserID, info.User)
129+
if tokenType == "bot" {
130+
_ = auth.StoreBotUserInfo(info.UserID, info.User)
131+
} else {
132+
_ = auth.StoreUserInfo(info.UserID, info.User)
133+
}
130134

131135
fmt.Fprintf(ios.Out, "%s Logged in as %s (%s token) — team: %s\n",
132136
cs.Green("✓"),

pkg/cmd/message/send.go

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"github.com/slack-go/slack"
1010
"github.com/spf13/cobra"
1111
"github.com/triptechtravel/slackbuzz-cli/internal/api"
12-
"github.com/triptechtravel/slackbuzz-cli/internal/auth"
1312
"github.com/triptechtravel/slackbuzz-cli/pkg/cmdutil"
1413
)
1514

@@ -121,18 +120,17 @@ func sendRun(opts *sendOptions) error {
121120
}
122121

123122
// Self-DM: if sending a DM to yourself, switch to bot so you get a notification.
124-
// The bot must re-open its own DM channel since the user's channel ID won't work.
125-
if api.LooksLikeUser(opts.channel) && !opts.asBot {
126-
if selfID, _, _ := auth.ResolveUserID(); selfID != "" {
127-
targetID := resolveTargetUserID(resolver, opts.channel)
128-
if targetID == selfID {
129-
if botClient, botErr := opts.factory.BotClient(); botErr == nil {
130-
botResolver := api.NewResolver(botClient.Slack)
131-
if botChannelID, botErr := botResolver.ResolveDM(opts.channel); botErr == nil {
132-
client = botClient
133-
channelID = botChannelID
134-
fmt.Fprintf(ios.ErrOut, "%s Sending to yourself — using bot so you get a notification\n", cs.Blue("→"))
135-
}
123+
// The bot opens its own DM channel with you so the message appears from the bot.
124+
if api.LooksLikeUser(opts.channel) && !opts.asBot && !agentMode {
125+
targetID := resolveTargetUserID(resolver, opts.channel)
126+
selfID := client.AuthUserID()
127+
if targetID != "" && selfID != "" && targetID == selfID {
128+
if botClient, botErr := opts.factory.BotClient(); botErr == nil {
129+
botResolver := api.NewResolver(botClient.Slack)
130+
if botChannelID, botErr := botResolver.ResolveDM(opts.channel); botErr == nil {
131+
client = botClient
132+
channelID = botChannelID
133+
fmt.Fprintf(ios.ErrOut, "%s Sending to yourself — using bot so you get a notification\n", cs.Blue("→"))
136134
}
137135
}
138136
}

0 commit comments

Comments
 (0)