Skip to content

Web browser and PAT support for GitLab #591

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 6 commits into from
Jan 19, 2022

Conversation

hickford
Copy link
Contributor

@hickford hickford commented Dec 30, 2021

GitLab support similar to the current GitHub support:

Select an authentication method for 'https://gitlab.com/':
  1. Web browser (default)
  2. Personal access token
  3. Username/password
option (enter for default): 

Fixes #589 and https://gitlab.com/gitlab-org/gitlab/-/issues/225215


Progress so far:

  • Web browser works
  • Personal access token works
  • Username and password works
  • OAuth configuration for public GitLab instances:
  • Documentation for configuring other GitLab instances
  • Recognises GitLab remotes with domain prefix gitlab. and links to documentation

Caveats:

@hickford hickford force-pushed the gitlab branch 4 times, most recently from 8db9d63 to b64f860 Compare December 31, 2021 15:03
@hickford hickford changed the title Gitlab support Web browser and PAT support for GitLab Dec 31, 2021
@hickford hickford force-pushed the gitlab branch 5 times, most recently from a70bff5 to a8bb5e3 Compare December 31, 2021 23:57
@hickford hickford marked this pull request as ready for review December 31, 2021 23:57
@hickford hickford force-pushed the gitlab branch 8 times, most recently from 7354d28 to 4076146 Compare January 5, 2022 22:59
@hickford
Copy link
Contributor Author

hickford commented Jan 5, 2022

NB. The OAuth apps on https://gitlab.com and https://gitlab.freedesktop.org are owned my account on each system. Ideally they'd be owned by a common Git Credential Manager group, but I don't think cross-instance groups are possible, and creating independent groups on each system seems more than trouble than its worth.

@hickford hickford force-pushed the gitlab branch 3 times, most recently from dc84d33 to e51a07d Compare January 5, 2022 23:12
@hickford hickford mentioned this pull request Jan 5, 2022
Copy link
Collaborator

@mjcheetham mjcheetham left a comment

Choose a reason for hiding this comment

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

First let me say I am excited to see such a contribution from the community! 🎉
This is the first new host provider we've had in a long time (since Bitbucket)
and we're always happy to see such engagement in GCM.

I have left a few comments about style, typos, etc, but overall it looks good.

There are however two main changes we'd need to see before we could take this:

1. Remove hardcoded public instances other than gitlab.com

As I mentioned in another review comment, we should not hardcode public
instances outside of the host-maintained offering (gitlab.com, github.com, etc).
Doing so opens the project and maintainers up to a lot of risk and burden.

Instances come and go over time. Some may construe support of some instances as
being an endorsement of that entity. The potentially unbounded number of
requests to add a new instance is not scalable. It's not fair to decide to take
some instances over others.

You have already added support to add custom instances by config. I would
suggest we try and leverage that for all cases. Setting the client ID/secret and
redirect URI scoped to a hostname is possible. e.g.,:

[credential "gitlab.example.com"]
    provider = gitlab
    gitLabDevClientId = 123456
    gitLabDevClientSecret = abcdef
    gitLabDevRedirectUri = http://127.0.0.1
[credential "another-example.com/gitlab"]
    provider = gitlab
    gitLabDevClientId = 890123
    gitLabDevClientSecret = xyzwsfs
    gitLabDevRedirectUri = http://127.0.0.1:56800/callback

We can also leverage the ICommandProvider interface to provide a custom
command for GitLab, to manage this config in a easier way. For example imagine
the following command-line interface:

git credential-manager-core gitlab list
git credential-manager-core gitlab add <url> <id> <secret> <redirect>
git credential-manager-core gitlab remove <url>

add would set the appropriate config as above, making it easier for users to
add their preferred instances.

Ideally engage GitLab to have a GCM OAuth app deployed as a default app in new
instances, and/or have some public metadata discovery for such IDs if not the
same across instances. GitHub Enterprise Server and GitHub AE all have a GCM
OAuth app deployed with well-known settings.


2. Implement support for OAuth refresh tokens

This is an important feature to support if we're going to support OAuth with
GitLab (given that expiring tokens exist).

The general idea here is to check for a RT if no AT is found (or is not valid), and try and use
that first before prompting for the whole interactive auth dance again.

This is the same as the Bitbucket host provider, which I outline here:

public async Task<ICredential> GetCredentialAsync(InputArguments input)
{
    Uri remoteUri = input.GetRemoteUri();
    string service = GetServiceName(remoteUri);
    string account = input.UserName;

    // Check for an existing user/pass, PAT, or OAuth token
    var credential = Context.CredentialStore.Get(service, account);

    // Validate the token/credential is valid
    bool valid = await ValidateCredentialAsync(remoteUri, credential);

    // Do we have a valid token to return?
    if (valid)
    {
        return credential;
    }

    // Something like: https://gitlab.com/refresh_token
    string refreshService = GetRefreshServiceName(remoteUri);
        
    // Check for an OAuth refresh token
    var refreshCredential = Context.CredentialStore.Get(refreshService, account);

    // Try and use the RT to mint a new AT
    if (refreshCredential != null)
    {
        try
        {
            var newCred = await RefreshOAuthTokenAsync(remoteUri, refreshCredential.Password);
            if (newCred != null)
            {
                return newCred;
            }
        }
        catch (Exception ex)
        {
            Context.Trace.WriteLine("Failed to use refresh token");
            Context.Trace.WriteException(ex);
        }
    }

    // Fall through to interactive auth

    AuthenticationModes modes = GetSupportedAuthenticationModes(remoteUri);
    AuthenticationPromptResult result = await _auth.GetAuthenticationAsync(targetUri, account, modes);
    switch (result.AuthenticationMode)
    {
        case AuthenticationModes.Browser:
            return await CreateOAuthTokenAsync(remoteUri, account);
        ...
    }
}

private async Task<bool> ValidateCredentialAsync(Uri remoteUri, ICredential credential)
{
    if (credential is null) return false;

    // TODO: Make a HTTP request using the credential to validate the token
    // is still valid.

    // If we fail to validate, we should assume it is valid.
    return true;
}

private async Task<ICredential> CreateOAuthTokenAsync(Uri remoteUri, string userName)
{
    OAuth2TokenResult result = await _auth.GetOAuthTokenViaBrowserAsync(targetUri);

    // Resolve the username for this new access token
    GitLabUserInfo userInfo = await _api.GetUserInfoAsync(targetUri, result.AccessToken);

    // Store the refresh token if we have been given one
    if (!string.IsNullOrWhiteSpace(result.RefreshToken))
    {
        string refreshServiceName = GetRefreshServiceName(remoteUri);
        _context.CredentialStore.AddOrUpdate(refreshServiceName, userInfo.UserName, result.RefreshToken);
    }

    return new GitCredential("oauth2", result.AccessToken);
}

private async Task<ICredential> RefreshOAuthTokenAsync(Uri remoteUri, string refreshToken, string refreshService)
{
    // Use refresh token to mint a new access token
    try
    {
        OAuth2TokenResult refreshResult = await _auth.GetOAuthTokenViaRefreshTokenAsync(targetUri, refreshToken);

        // Resolve the username for this refresh token
        GitLabUserInfo newUserInfo = await _api.GetUserInfoAsync(targetUri, refreshResult.AccessToken);

        // Store the refresh token if we have been given a new one
        if (!string.IsNullOrWhiteSpace(refreshResult.RefreshToken))
        {
            _context.CredentialStore.AddOrUpdate(refreshService, newUserInfo.UserName, refreshResult.RefreshToken);
        }

        return new GitCredential("oauth2", refreshResult.AccessToken);
    }
    catch (OAuth2Exception)
    {
        // Failed to use refresh token
        return null;
    }
}

I realise this a big wall of text and comments on your work.

Let me say again that we do appreciate your efforts here to bring GitLab to GCM,
and comments here are to ensure that support for a new host provider covers all
the cases in a way that's sustainable for the project.

New host providers contributions are held to a high standard, and it's great
stuff to see that you've clearly put in a lot of high quality work so far!

Thank you ❤️

Comment on lines 170 to 160
// username oauth2 https://gitlab.com/gitlab-org/gitlab/-/issues/349461
return new GitCredential("oauth2", result.AccessToken);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is a shame 😢 .. but something we may just have to do until (if?) they fix this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So this could cause a problem if you push to two repos on the same host with different users? Could you give an example?

@mjcheetham mjcheetham added the enhancement New feature or request label Jan 7, 2022
@mjcheetham
Copy link
Collaborator

I should also add that we've taken the liberty of creating a Git Credential Manager group on gitlab.com with an OAuth application registered as below:

Application ID: 172b9f227872b5dde33f4d9b1db06a6a5515ae79508e7a00c973c85ce490671e
Secret: 7da92770d1447508601e4ba026bc5eb655c8268e818cd609889cc9bae2023f39
Callback URL: http://127.0.0.1:56789
Confidential: No
Scopes: read_user, write_repository

Feel free to update your PR to use these values for the gitlab.com instance. This should work, but please let us know if there are problems.

@hickford hickford marked this pull request as draft January 14, 2022 17:10
@hickford hickford force-pushed the gitlab branch 2 times, most recently from 7e79dec to 13370ca Compare January 17, 2022 20:55
Copy link
Collaborator

@mjcheetham mjcheetham left a comment

Choose a reason for hiding this comment

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

Hey @hickford, thanks for the updates to the PR! Overall this is looking much better.

There are two bugs (one crashing, one behavioural) however that'll need fixing before it can be merged. I've called them out in the review comments. Once these are fixed this PR can be merged 😃

@hickford hickford force-pushed the gitlab branch 2 times, most recently from 3bcbaa4 to a9b3a9a Compare January 19, 2022 08:11
@hickford hickford marked this pull request as ready for review January 19, 2022 08:13
Co-authored-by: Matthew John Cheetham <[email protected]>
Copy link
Collaborator

@mjcheetham mjcheetham left a comment

Choose a reason for hiding this comment

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

Thanks @hickford for the quick turn around there. This looks good to merge right now. Thanks again for the contribution! 🎉

@mjcheetham mjcheetham merged commit 00b84a0 into git-ecosystem:main Jan 19, 2022
@muttebe
Copy link

muttebe commented Feb 7, 2022

Many thanks!

Is there already a plan, when this extension is released to "GCM for Windows"?

@ldennington
Copy link
Contributor

Hi @muttebe -

There are no plans to port this to Git Credenital Manager for Windows, as I believe that project has been deprecated in favor of GCM.

@muttebe
Copy link

muttebe commented Feb 8, 2022

Hi @ldennington!

Thank you for your fast reply! I fully agree with you!

Actually i am intrested in how often the GCM is released and escpecially when it is released next time?
Sorry for the missunderstanding! I was not aware that there are different code basis!

@LeonardSchmitt67
Copy link

Hi!
Do you know when a release with those changes will be available for windows ?
Thanks!

@ldennington
Copy link
Contributor

Apologies for the delay here @LeonardSchmitt67. We don't have a set cadence for GCM releases. However, since we ship with Git for Windows, we keep that release cadence in mind and try to get new bits out ~1 week ahead of that. The next release cycle for Git for Windows begins on April 4th, so we're targeting sometime around March 28th for our next release.

@LeonardSchmitt67
Copy link

Thanks a lot! @ldennington

@Emyim2

This comment was marked as off-topic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

GitLab support
6 participants