Skip to content

Refactor to allow HostProviders to override store and erase #49

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

Merged
merged 1 commit into from
Jun 17, 2019
Merged

Refactor to allow HostProviders to override store and erase #49

merged 1 commit into from
Jun 17, 2019

Conversation

mjcheetham
Copy link
Collaborator

Refactor the core commands and host provider types to permit providers to customise how they store and erase credentials. This would allow for providers that do not want to use the OS credential store for example.

This change is required to enable non-PAT-based authentication, such as delegating to ADAL/MSAL and using Azure access tokens directly (where we don't lookup, store, or erase from the system credential store).

@mjcheetham mjcheetham added enhancement New feature or request engineering Refactoring or build changes labels Jun 5, 2019
@mjcheetham mjcheetham requested review from jrbriggs, jamill and jeschu1 June 5, 2019 13:49
}

return url;
return $"git:{url}";
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The providers are now in direct control over how they store (or not) their credentials in the OS credential stores, meaning the common "git:" prefix is being pushed into the providers themselves. The fact they start with "git:" is now a convention, rather than something that is enforced.

/// <summary>
/// Represents a Git hosting provider where credentials can be stored and recalled in/from the Operating System's
/// secure credential store.
/// </summary>
public abstract class HostProvider : IHostProvider
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The base HostProvider class assumes that inheriting providers do want to use the ICredentialStore for the operating system, but providers can either override the various virtual methods, or directly implement IHostProvider instead.

Refactor the core commands and host provider types to permit providers
to customise how they store and erase credentials. This would allow for
providers that do not want to use the OS credential store for example.
@jamill
Copy link
Contributor

jamill commented Jun 6, 2019

I think the code changes look good.

I had more of a question around how other credential systems are going to work (e.g. ADAL/MSAL or Azure Access Tokens). How will the flow for these be different than with OS credential storage? How will the credential manager choose the appropriate HostProvider?

Will GitHub be able to use MSAL?

I assume that AzDevOps end points will need to be able to choose between ADAL/MSAL, OS provided storage, and Azure Access Tokens?

Will the ADAL/MSAL flavor only be able to “get” a credential, and not update / erase an entry?

@mjcheetham
Copy link
Collaborator Author

mjcheetham commented Jun 10, 2019

@jamill, perhaps an overview of the current flow, and the flow as proposed by this PR would be useful.

Command invocation

When Git invokes GCM it gives us a 'verb' and a dictionary of information as input. The available verbs are "get", "store", and "erase".
The main Application object loops through all available commands looking for one that understands the given Git verb, the executes it.

Each command ultimately inherits from CommandBase which defines two abstract methods: bool CanExecute(string[] args) and Task ExecuteAsync(ICommandContext ctx, string[] args).

Git Verb Responding command
get GetCommand
store StoreCommand
erase EraseCommand

The CanExecute method for each of the above commands just matches on the command line arguments supplied by Git, that is the verb name.

Current design

The ExecuteAsync method of each Git 'verb command' (that is to say, those that Git will invoke us with; does not include other commands such as 'help' or 'version') currently share the same initial logic:

  1. Determine the correct IHostProvider to service this request from the dictionary from standard input.
  2. Compute the credential key used to access stored credentials in the OS ICredentialStore.
  3. Invoke the abstract ExecuteInternalAsync method, which is verb specific.

Each of the verb commands inherit from the HostProviderCommandBase class which performs those 3 steps. The derived command implements the lookup/create/delete/store logic.

GetCommand does a check for an existing credential in the OS store, and returns if it found. Otherwise it calls the selected IHostProvider to CreateCredentialAsync.

// GetCommand
ICredential cred = credStore.Get(credKey);
if (cred is null)
{
    cred = await hostProvider.CreateCredentialAsync(...);
}

return cred;

StoreCommand and EraseCommand similarly just parse call the AddOrUpdate and Remove methods on the ICredentialStore using the computed credential key.

// StoreCommand
var cred = new Credential(input.UserName, input.Password);
credStore.AddOrUpdate(credKey, cred);

// EraseCommand
credStore.Remove(credKey);

Motivation for change

The assumption here is that all host providers are called for two things:

  1. Generate a new credential
  2. Compute a stable identifier for the credential (the key)

The GCM infrastructure is responsible for retrieving, storing and erasing these credentials using the provided keys.

In order to support scenarios such as using the AAD Access Token (AT) directly (rather than an Azure DevOps PAT), we need to delegate storage, lookup, and deletion to the ADAL/MSAL authentication library. It does not use the ICredentialStore but it's own cache. We don't want to store the AAD AT in the OS credential store.

Change overview

The primary goal in the PR is to remove the assumption above, and instead allow the host providers to do what they want want when called with "get", "store", or "erase".

The GetCommand, StoreCommand, and EraseCommand are now simplified to handle only the interactions with the standard input and output streams, and convert between text <--> objects for the selected IHostProvider; we delegate to the IHostProvider for all the logic.

// GetCommand
var cred = await provider.GetCredentialAsync(input);

output["protocol"] = input.Protocol;
output["host"]     = input.Host;
output["username"] = cred.UserName;
output["password"] = cred.Password;

// StoreCommand
provider.StoreCredentialAsync(input);

// EraseCommand
provider.EraseCredentialAsync(input);

The shared input parsing, and the selection of the correct IHostProvider remains in the HostProviderCommandBase abstract class that all the verb commands inherit from. The credential key computation responsibility has been pushed fully into the host providers.

Keeping it easy to implement a host provider

Since most of the current providers do want to use the OS credential store to lookup/store credentials, rather than make each provider have to reimplement the ICredentialStore.Get/Add/Remove logic, this has been pushed into the abstact HostProvider class. The Get|Store|EraseCredentialAsync methods have all been made virtual such that each provider can either:

  1. Get the shared OS credential store lookup/store functionality for free
  2. Hook any of the actions to perform any additional work (like clearing other provider specific caches), for example:
override Task EraseCredentialAsync(..)
{
    // Erase our special cache
    contosoOrgCache.EraseEntry(...);

    // Call the shared logic to erase the actual credential from the OS store
    base.EraseCredentialAsync();
}
  1. Completely override the shared logic by either:
    1. Overriding all the virtual methods, or
    2. Implement the IHostProvider interface directly
  2. Choose dynamically between 1, 2, or 3 at runtime by chosing to either call to the base or not.

To answer your question @Jamil:

Will GitHub be able to use MSAL?

The design of GCM Core is such that the authentication methods are more orthogonal to the host provider than GCM Windows.

If GitHub wished to provide a "sign in with Microsoft" option in the future, the GitHubHostProvider can simply consume the IMicrosoftAuthentication component and call GetAccessTokenAsync(..) passing the appropriate request options.

Such a feature (depending on how implemented) might look like this:

// GitHubHostProvider
override Task GetCredentialAsync(..)
{
    bool msAuthSupported = gitHubApi.CheckIfMicrosoftAuthIsSupported();
    bool userSelectedMsAuth = ShowSelectAuthTypePrompt();

    if (msAuthSupported && userSelectedMsAuth)
    {
        // Use existing MS auth flows
        IMicrosoftAuthentication msAuth = ..;
        var accessToken = msAuth.GetAccessToken(..);
        // ..
    }
    else
    {
        // Use existing GitHub auth flows
        IGitHubAuthentication ghAuth = ..;
        var cred = ghAuth.PromptForCredentials(..);
        // ..
    }
}

@jamill
Copy link
Contributor

jamill commented Jun 11, 2019

Thank you for the detailed comments!

@mjcheetham mjcheetham merged commit 5a13f18 into git-ecosystem:master Jun 17, 2019
@mjcheetham mjcheetham deleted the host-refactor branch June 17, 2019 10:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
engineering Refactoring or build changes enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants