Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

github: add multi-account prompt and login/logout commands #1267

Merged
merged 13 commits into from
Jun 1, 2023

Conversation

mjcheetham
Copy link
Collaborator

Add multi-account support for the GitHub host provider.

Previously it was possible to use multiple accounts with GitHub, but this required prior knowledge about including the username in the remote URL (e.g. https://mona_alt@github.com/mona/test), and without this the incorrect account may be selected, and subsequently erased due to insufficient permissions.

This PR adds a few different features and prompts to improve discoverability and experience using multiple GitHub accounts with GCM:

  1. Add login, logout and list commands for the GitHub provider
  2. Add UI and TTY prompts to select between GitHub user accounts
  3. Abridged documentation and links to quickly discover the 'user-in-remote-URL' functionality
  4. (bonus!) Ability to suppress GUI prompts over text-based ones via the command-line using --no-ui

One thing that is absent compared to the Azure Repos provider's multi-account support is the lack of account 'binding'. This is due to lack of knowledge, by default, of the full remote URL from Git to be able to discern between different repositories or organisations on the same host.

Command-line usage:

% git-credential-manager github --help    
Description:
  Commands for interacting with the GitHub host provider

Usage:
  git-credential-manager github [command] [options]

Options:
  --no-ui         Do not use graphical user interface prompts
  -?, -h, --help  Show help and usage information

Commands:
  list              List all known GitHub accounts.
  login             Add a GitHub account.
  logout <account>  Remove a GitHub account.
% git-credential-manager github login --help
Description:
  Add a GitHub account.

Usage:
  git-credential-manager github login [options]

Options:
  --url <url>             URL of the GitHub instance to target, otherwise use GitHub.com
  --username <username>   User name to authenticate with
  --device                Use device flow to authenticate
  --browser, --web        Use a web browser to authenticate
  --pat, --token <token>  Use personal access token to authenticate
  --no-ui                 Do not use graphical user interface prompts
  -?, -h, --help          Show help and usage information
% git-credential-manager github logout --help 
Description:
  Remove a GitHub account.

Usage:
  git-credential-manager github logout <account> [options]

Arguments:
  <account>  Account to remove

Options:
  --url <url>     URL of the GitHub instance to target, otherwise use GitHub.com
  --no-ui         Do not use graphical user interface prompts
  -?, -h, --help  Show help and usage information
% git-credential-manager github list --help  
Description:
  List all known GitHub accounts.

Usage:
  git-credential-manager github list [options]

Options:
  --url <url>     URL of the GitHub instance to target, otherwise use GitHub.com
  --no-ui         Do not use graphical user interface prompts
  -?, -h, --help  Show help and usage information

Select account UI prompts:

Avalonia Windows legacy helper (WPF)
image image

Add ability to suppress the GUI from the command-line argument
`--no-ui`. We achieve this by setting the process enviroment variable
that already exists and is used to control GUI prompts.
@mjcheetham mjcheetham marked this pull request as ready for review May 22, 2023 21:39
Copy link
Contributor

@ldennington ldennington left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome work! 🚀 I did notice something in my testing that may or may not be by design. Running the below forced a prompt, even though I had GitHub creds for ldennington in my KeyChain.

Users/ldennington/repos/git-credential-manager/out/shared/Git-Credential-Manager/bin/Debug/net6.0/git-credential-manager github login --username ldennington

I also had to consent:

Screenshot 2023-05-23 at 1 54 01 PM

I then tried Device code and the same thing happened:

Users/ldennington/repos/git-credential-manager/out/shared/Git-Credential-Manager/bin/Debug/net6.0/git-credential-manager github login --username ldennington
--device

Screenshot 2023-05-23 at 1 55 36 PM

If this is by design, carry on! But wanted to call it out just in case.

/// <param name="command">Command to add options to.</param>
/// <param name="arity">Specify the required arity of the options.</param>
/// <param name="options">Set of options to add.</param>
public static void AddOptionSet(this Command command, OptionArity arity, params Option[] options)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very interesting!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, mutually exclusive options or option 'set' isn't natively in System.CommandLine, so here's a custom validator to add that functionality!

src/shared/Core/CommandExtensions.cs Outdated Show resolved Hide resolved
@@ -35,7 +37,12 @@ public SecretServiceCollection(string @namespace)

#region ICredentialStore

public unsafe ICredential Get(string service, string account)
public ICredential Get(string service, string account)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good separation.

//
var loginCmd = new Command("login", "Add a GitHub account.");
var userNameOpt = new Option<string>("--username", "User name to authenticate with");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit

Suggested change
var userNameOpt = new Option<string>("--username", "User name to authenticate with");
var userNameOpt = new Option<string>("--username", "Username to authenticate with");

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Elsewhere in GCM public facing strings, we've used "user name" as two words rather than "username".

if (token is not null)
{
// Resolve the GitHub user handle if the user didn't supply one
if (string.IsNullOrEmpty(userName))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this ever be empty since we have the above userName null check?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. We're doing userName = userName ?? url.GetUserName() and so userName could still be null or empty if there was no --username option passed and no user in the remote URL.

git-credential-manager github login --token $TOKEN is a complete valid command, and --username is not required.

@@ -56,9 +56,15 @@ protected AuthenticationBase(ICommandContext context)
// Kill the process upon a cancellation request
ct.Register(() => process.Kill());

if (!(standardInput is null))
// Write the standard input to the process if we have any to write
if (standardInput is not null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice cleanup.

src/shared/GitHub/UI/Views/SelectAccountView.axaml.cs Outdated Show resolved Hide resolved
@@ -86,4 +95,28 @@ You can use the `github [list | login | logout]` commands to manage your GitHub
accounts. These commands are documented in the [command-line usage][cli-usage]
or by running `git-credential-manager github --help`.

## TL;DR: Tell GCM to remember which account to use
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great explanation!

@mjcheetham
Copy link
Collaborator Author

@ldennington:

I did notice something in my testing that may or may not be by design. Running the below forced a prompt, even though I had GitHub creds for ldennington in my KeyChain.

This is expected, I guess. We don't check for existing credentials during github login. The idea was/is to add a new login, or replace an existing credential for the same account.

The GitHub CLI takes the opposite approach, and say "you're already logged in, login again?":

% gh auth login
? What account do you want to log into? GitHub.com
? You're already logged into github.com. Do you want to re-authenticate? (y/N) 

I guess we could modify our login command here to do the following, if you think this makes more sense?

  • login: show the login prompt - we don't know if the user is going to pick a different account from what may/may-not already exist.
  • login --username <username>: check for an existing credential for <username> and if it already exists, then bail saying "credential for user already exists" or similar
  • login --username <username> --force or --add: explicitly add/replace an already existing credential.. the user may wish to renew the token used for some reason

Thoughts?

@ldennington
Copy link
Contributor

Thoughts?

I like this approach - it aligns better with what I expected to happen when I ran the command.

Add an extension method for adding multiple options to a `Command`,
optionally setting a validator to enforce the arity of their usage.
This allows us to create mutually exclusive option sets.
Add the ability to pull all the accounts from a credential store.
Change GitHubHostProvider to implement IHostProvider directly rather
than derive from the abstract HostProvider class. Inline the previously
super-class implementations to preserve behaviour.
Refactor the `GenerateCredentialAsync` method to take a remote URI and
optional user name hint, rather than requiring an instance of
`InputArguments` from Git.
Add a set of basic provider commands to add, remove and list GitHub
accounts. Users can explicitly login to GitHub.com or any GHES instance,
or remove an account.
Add a few options to the `login` command to customise how auth happens,
without needing to respond to an interactive prompt.

 --username <username>
    Specify the user name to use; useful for login hints in the web

 --browser/--web
    Select the web browser OAuth flow

 --device
    Select the device code flow

 --pat/--token <token>
    Use the specified personal access token. The user name is resolved
    if not provided with the --username option.
@mjcheetham
Copy link
Collaborator Author

I like this approach - it aligns better with what I expected to happen when I ran the command.

Sure! I can work on this.

If we find a credential already exists for the same user prior to
authenticating, require the user explicitly pass --force to make us
re-authenticate the same account.
Add a new prompt to allow the user to select between GitHub accounts, or
using a new account.
Check for multiple existing accounts, and if there is more than one
account with credentials in the store AND no user name hint provided in
the remote URL, then show the new account selection prompt.
@mjcheetham
Copy link
Collaborator Author

mjcheetham commented May 23, 2023

Added! cc @ldennington

 Description:
   Add a GitHub account.

 Usage:
   git-credential-manager github login [options]

 Options:
   --url <url>             URL of the GitHub instance to target, otherwise use GitHub.com
   --username <username>   User name to authenticate with
   --device                Use device flow to authenticate
   --browser, --web        Use a web browser to authenticate
   --pat, --token <token>  Use personal access token to authenticate
+  --force                 Force re-authentication even if a credential already exists for the account
   --no-ui                 Do not use graphical user interface prompts
   -?, -h, --help          Show help and usage information
% git-credential-manager github login --username mjcheetham
Account 'mjcheetham' already has credentials for https://github.com; use --force to re-authenticate

@ldennington
Copy link
Contributor

Added! cc @ldennington

Awesome! Took a look at the corresponding changes and they all look good to me.

Add the ability to double-click to select an account and 'continue' for
the account selection Avalonia window.
@mjcheetham
Copy link
Collaborator Author

@ldennington just pushed one more commit (b94ade0) on top to allow double-clicking the account from the list, to accept and continue... so you don't have to select the account from the list and then press the 'Continue' button.

@mjcheetham mjcheetham merged commit e34dba7 into git-ecosystem:main Jun 1, 2023
7 checks passed
@mjcheetham mjcheetham deleted the gh-multiaccounts branch June 1, 2023 16:59
@mjcheetham mjcheetham mentioned this pull request Jun 26, 2023
mjcheetham added a commit that referenced this pull request Jun 26, 2023
**Changes:**

- Use in-proc methods for getting OS version number (#1240, #1264)
- Update System.CommandLine (#1265)
- Suppress GUI from command-line argument (#1267)
- Add github (login|logout|list) commands (#1267)
- cURL Cookie file support (#1251)
- Update target framework on Mac/Linux to .NET 7 (#1274, #1282)
- Replace JSON.NET with System.Text.Json (#1274)
- Preserve exact redirect URI formatting in OAuth requests (#1281)
- Use IP localhost redirect for GitHub (#1286)
- Use WWW-Authenticate headers from Git for Azure Repos authority
(#1288)
- Better GitHub Enterprise Managed User (EMU) account support (#1190)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants