Skip to content

Allow gh auth git-credential get to get a token for non-active user #9111

Description

@emiksk

Describe the feature or problem you’d like to solve

Currently gh auth git-credential get can get a token for only active account in specified host.

e.g.

$ gh auth status
github.com
  ✓ Logged in to github.com account emiksk (keyring)
  - Active account: true
  - Git operations protocol: https
  - Token: gho_************************************
  - Token scopes: 'gist', 'read:org', 'repo', 'workflow'

  ✓ Logged in to github.com account non-emiksk (keyring)
  - Active account: false
  - Git operations protocol: https
  - Token: gho_************************************
  - Token scopes: 'gist', 'read:org', 'repo', 'workflow'

$ gh auth git-credential get
protocol=https
host=github.com

protocol=https
host=github.com
username=emiksk
password=gho_************************************

$ gh auth git-credential get
protocol=https
host=github.com
username=non-emiksk


$

I think that gh auth git-credential get should be able to get a token regardless of whether user is active or not if the specific username is given.

Proposed solution

This enhancement will allow developers to control credentials by remote url (such as https://[email protected]/org/repo and https://[email protected]/org/repo) instead of using gh auth switch.

Additional context

This behavior is caused by using ActiveToken() method in helper.go even if a username is given.
It would be better to use the TokenForUser() method when username is given, as in token.go.

However, TokenForUser() method ignores environment variables such as GITHUB_TOKEN unlike ActiveToken() method.

func (c *AuthConfig) TokenForUser(hostname, user string) (string, string, error) {
if token, err := keyring.Get(keyringServiceName(hostname), user); err == nil {
return token, "keyring", nil
}
if token, err := c.cfg.Get([]string{hostsKey, hostname, usersKey, user, oauthTokenKey}); err == nil {
return token, "oauth_token", nil
}
return "", "default", fmt.Errorf("no token found for '%s'", user)
}

token, source := ghAuth.TokenFromEnvOrConfig(hostname)

Therefore, if the logic of token.go and helper.go are simply made common, a behavior is changed when an environment variable exists and username is explicitly given.
Since this behavior also exists in the test case, it needs to consider whether the behavior should be changed or whether different logic should be implemented instead of sharing the same logic.

Activity

  1. williammartin commented on May 22, 2024

    @williammartin
    Member

    Ahhh yes thank you for this excellent write up and investigation! This is something that we discussed and decided to leave out of the original multi-account MVP to keep the scope small. I'm glad you opened this because a similar thing was mentioned over here: #8875 (comment)

    Can you talk a little bit more about how you would like to use this with regards remote URLs? Do you clone and configure your repositories outside of gh using git directly? Would it be interesting if we found a way in gh to make this kind of thing configurable as well? For example, perhaps we could offer configuration of remotes with a username during repo clone, repo fork etc. I'm not sure exactly what this would look like, but I think it's worth thinking holistically about the issue.

  2. added
    more-info-neededMore info needed from user/contributor
    gh-authrelating to the gh auth command
    and removed on May 22, 2024
  3. emiksk commented on May 23, 2024

    @emiksk
    Author

    @williammartin
    Thank you for your reply!

    For example, perhaps we could offer configuration of remotes with a username during repo clone, repo fork etc.

    It’s the interesting feature. I didn’t have thought of it, yet I might use it if it’s offered.

    Imagining myself using remote URLs to switch credentials, I would probably use a combination of includeIf and insteadOf in gitconfig to automatically switch remote URLs according to working directories.

    For example, adding the following includeIf to gitconfig:

    [includeIf "/path/to/repositories_for_emiksk]
      path = /path/to/gitconfig_for_emiksk
    

    Then, adding the following insteadOf to /path/to/gitconfig_for_emiksk, remote URLs will automatically be changed to https://[email protected] from https://github.com when I work in the git directory.

    [url "https://[email protected]"]
      insteadOf = https://github.com
    

    I believe this is a nice way to automatically switch credentials for accounts for each directory.
    Of course, there may be times when I want to switch between multiple credentials in one git directory.

    It's the poor English writing, but I hope you get the message.

  4. added and removed
    more-info-neededMore info needed from user/contributor
    on Jul 9, 2024
  5. LukeDerkzen commented on Dec 19, 2024

    @LukeDerkzen

    Would love for this to be picked up at some point if possible.

  6. med8bra commented on Dec 22, 2024

    @med8bra

    The current behavior for fetching token is a bit unexpected, as even if gh auth status reports that the account is active, it doesn't necessarily mean the token is active.

    In my case I use GH_CONFIG_DIR with direnv to customize config per a tree of repos

    With a work config setup like

    # ./dev/work/gh/hosts.yaml: 
    github.com:
        git_protocol: https
        users:
           work-username:
        user: work-username

    and a personal setup like

    # ~/.config/gh/hosts.yaml: 
    github.com:
        git_protocol: ssl
        users:
           dev-username:
        user: dev-username

    running gh auth status in a work repo, works as expected

    > gh auth status
    github.com
      ✓ Logged in to github.com account work-username (keyring)
      - Active account: true
      - Git operations protocol: https
      - Token: gho_************************************
      - Token scopes: 'gist', 'read:org', 'repo', 'workflow'

    But, doing any API is using the last "active account", which doesn't match gh auth status, and requires to gh auth switch

    > gh api /user | jq -r .name
    dev-username

    After a quick look into how token is manged, it seems only hostname is used to fetch the active token from the keyring

    // ActiveToken will retrieve the active auth token for the given hostname,
    // searching environment variables, plain text config, and
    // lastly encrypted storage.
    func (c *AuthConfig) ActiveToken(hostname string) (string, string) {
    if c.tokenOverride != nil {
    return c.tokenOverride(hostname)
    }
    token, source := ghauth.TokenFromEnvOrConfig(hostname)
    if token == "" {
    var err error
    token, err = c.TokenFromKeyring(hostname)
    if err == nil {
    source = "keyring"
    }
    }
    return token, source
    }

  7. williammartin commented on Dec 23, 2024

    @williammartin
    Member

    Thanks for chiming in. This is definitely a bug. As you've correctly pinpointed, when a token is stored in the keyring as "active", it's only referenced by host. Unfortunately, we can't change that the keyring is referenced only by name for the active account, because it would be a breaking change (this approach was chosen long ago, and long before multiple accounts were even supported). However, maybe what we could do is first look for the token in the keyring that is referenced by host and username (this is how we store the tokens to swap them in when auth switch is called), and only then fall back to the current mechanism.

    Would you mind creating a new issue to track that bug @med8bra? Also, I'd love to know your reason in that issue for maintaining two hosts files? It looks like maybe the only difference is in the git protocol, and that you like the swapping to occur via direnv?

  8. added
    coreThis issue is not accepting PRs from outside contributors
    and removed on Jul 14, 2025
  9. galamdring commented on Apr 10, 2026

    @galamdring

    I came at this problem from a different direction, looking to make it automatic to use my non-personal account for any repo in the non-personal org, and my personal account for everything else. I implemented it in a PR, that was of course automatically closed, but wanted to add it here as a reference as well.
    I set it up so that you can add a mapping of owner -> user, and then any repo with a configured owner will use the mapped user via the factory created http client. If there is not an owner->user match, it will use the globally active one. This allows me to leave it as my personal account in general, but ensures that when I need to interact with a non-personal repo, I'm using the correct credential.

    The solutions suggested here are good solutions if the repos are segregated by directory easily. That wasn't the case for me, so the owner path seemed the most straightforward.

    PR: #13130
    Root Cause Analysis: #12885

    Similar Issues:
    #326
    #12145
    #11938

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    coreThis issue is not accepting PRs from outside contributorsenhancementa request to improve CLIgh-authrelating to the gh auth command

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions