Repository: cli/cli (go)

Issue:
Account incorrectly reported active in `gh auth status`
### Describe the bug
Running `gh auth status` report account as active, but API calls use another account.

```sh
> gh --version
gh version 2.63.0 (1980-01-01)
https://github.com/cli/cli/releases/tag/v2.63.0
```
### Steps to reproduce the behavior

1. You have to two users on same hostname `github.com`, last active account is  `user1`.
2. Switch to another folder with custom `GH_CONFIG_DIR` (vid direnv), which a custom config which has one user `user2`
4. Run `gh auth status`
```sh
> gh auth status
github.com
  ✓ Logged in to github.com account user2 (keyring)
  - Active account: true
  - Git operations protocol: ssh
  - Token: gho_************************************
  - Token scopes: 'gist', 'read:org', 'repo', 'workflow'
```
6. Run `gh api /user | jq .name`  => prints `user1`, contradicting `auth status` output 

The expected behavior is to use the token for the given `host+user` of current repository without needing to `gh auth switch`, and report active account correctly.

### Context

Active token is fetched from keyring using only hostname
https://github.com/cli/cli/blob/5402e207ee89f2f3dc52779c3edde632485074cd/internal/config/config.go#L202-L218

Related to https://github.com/cli/cli/issues/9111#issuecomment-2558645932)



Pull request:
fix: get token for active user instead of blank if possible
Fixes #10136

I have been following this issue for a while but finally tried checking the code. It seemed pretty simple to allow the `GH_CONFIG_DIR` switching mechanism to work, and the change seems easy to reason about to me so tried sending a PR.

Note that when using `login`, we set the username-keyed token

https://github.com/cli/cli/blob/c50cdbd0c52cefc597e27c8ec4154127837e1846/internal/config/config.go#L345

so as far as I'm aware, there aren't really any gotchas, and in fact we could potentially remove the fallback to `""` - I left it for safety in case there are still some cases not covered by getting the token for the active user. If there are old versions that didn't populate the username-keyed value to keyring when logging in, then I guess they may rely on the fallback.

Patch:
diff --git a/internal/config/config.go b/internal/config/config.go
index 003a0ca17..47d9403b0 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -234,8 +234,18 @@ func (c *AuthConfig) ActiveToken(hostname string) (string, string) {
 	}
 	token, source := ghauth.TokenFromEnvOrConfig(hostname)
 	if token == "" {
+		var user string
 		var err error
-		token, err = c.TokenFromKeyring(hostname)
+		if user, err = c.ActiveUser(hostname); err == nil {
+			token, err = c.TokenFromKeyringForUser(hostname, user)
+		}
+		if err != nil {
+			// We should generally be able to find a token for the active user,
+			// but in some cases such as if the keyring was set up in a very old
+			// version of the CLI, it may only have a unkeyed token, so fallback
+			// to it.
+			token, err = c.TokenFromKeyring(hostname)
+		}
 		if err == nil {
 			source = "keyring"
 		}


Test patch:
diff --git a/internal/config/auth_config_test.go b/internal/config/auth_config_test.go
index 61245c650..ca5f7e584 100644
--- a/internal/config/auth_config_test.go
+++ b/internal/config/auth_config_test.go
@@ -769,6 +769,62 @@ func TestTokenWorksRightAfterMigration(t *testing.T) {
 	require.Equal(t, oauthTokenKey, source)
 }
 
+func TestTokenPrioritizesActiveUserToken(t *testing.T) {
+	// Given a keyring where the active slot contains the token from a previous user
+	authCfg := newTestAuthConfig(t)
+	require.NoError(t, keyring.Set(keyringServiceName("github.com"), "", "test-token"))
+	require.NoError(t, keyring.Set(keyringServiceName("github.com"), "test-user1", "test-token"))
+	require.NoError(t, keyring.Set(keyringServiceName("github.com"), "test-user2", "test-token2"))
+
+	// When no active user is set
+	authCfg.cfg.Remove([]string{hostsKey, "github.com", userKey})
+
+	// And get the token from the auth config
+	token, source := authCfg.ActiveToken("github.com")
+
+	// Then it returns the token from the keyring active slot
+	require.Equal(t, "keyring", source)
+	require.Equal(t, "test-token", token)
+
+	// When we set the active user to test-user1
+	authCfg.cfg.Set([]string{hostsKey, "github.com", userKey}, "test-user1")
+
+	// And get the token from the auth config
+	token, source = authCfg.ActiveToken("github.com")
+
+	// Then it returns the token from the active user entry in the keyring
+	require.Equal(t, "keyring", source)
+	require.Equal(t, "test-token", token)
+
+	// When we set the active user to test-user2
+	authCfg.cfg.Set([]string{hostsKey, "github.com", userKey}, "test-user2")
+
+	// And get the token from the auth config
+	token, source = authCfg.ActiveToken("github.com")
+
+	// Then it returns the token from the active user entry in the keyring
+	require.Equal(t, "keyring", source)
+	require.Equal(t, "test-token2", token)
+}
+
+func TestTokenWithActiveUserNotInKeyringFallsBackToBlank(t *testing.T) {
+	// Given a keyring that contains a token for a host
+	authCfg := newTestAuthConfig(t)
+	require.NoError(t, keyring.Set(keyringServiceName("github.com"), "", "test-token"))
+	require.NoError(t, keyring.Set(keyringServiceName("github.com"), "test-user1", "test-token1"))
+	require.NoError(t, keyring.Set(keyringServiceName("github.com"), "test-user2", "test-token2"))
+
+	// When we set the active user to test-user3
+	authCfg.cfg.Set([]string{hostsKey, "github.com", userKey}, "test-user3")
+
+	// And get the token from the auth config
+	token, source := authCfg.ActiveToken("github.com")
+
+	// Then it returns successfully with the fallback token
+	require.Equal(t, "keyring", source)
+	require.Equal(t, "test-token", token)
+}
+
 func TestLogoutRightAfterMigrationRemovesHost(t *testing.T) {
 	// Given we have logged in before migration
 	authCfg := newTestAuthConfig(t)
diff --git a/pkg/cmd/api/api_test.go b/pkg/cmd/api/api_test.go
index 321f7b7c0..bf4d51c4c 100644
--- a/pkg/cmd/api/api_test.go
+++ b/pkg/cmd/api/api_test.go
@@ -1358,7 +1358,12 @@ func Test_apiRun_cache(t *testing.T) {
 		Config: func() (gh.Config, error) {
 			return &ghmock.ConfigMock{
 				AuthenticationFunc: func() gh.AuthConfig {
-					return &config.AuthConfig{}
+					cfg := &config.AuthConfig{}
+					// Required because the http client tries to get the active token and otherwise
+					// this goes down to to go-gh config and panics. Pretty bad solution, it would
+					// be better if this were black box.
+					cfg.SetActiveToken("token", "stub")
+					return cfg
 				},
 				// Cached responses are stored in a tempdir that gets automatically cleaned up
 				CacheDirFunc: func() string {