Skip to content

Commit

Permalink
Issue-267 Add fingerprint header, to autodetect Bitbucket DC instances.
Browse files Browse the repository at this point in the history
Copied and refreshed the Bitbucket development doc from the Git Credential Manager for Windows project.
  • Loading branch information
mminns committed Jun 18, 2021
1 parent dd0752d commit d48fb28
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 2 deletions.
80 changes: 80 additions & 0 deletions docs/bitbucket-development.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Bitbucket Authentication, 2FA and OAuth

By default for authenticating against private Git repositories Bitbucket supports SSH and username/password Basic Auth over HTTPS.
Username/password Basic Auth over HTTPS is also available for REST API access.
Additionally Bitbucket supports App-specific passwords which can be used via Basic Auth as username/app-specific-password.

To enhance security Bitbucket offers optional Two-Factor Authentication (2FA). When 2FA is enabled username/password Basic Auth access to the REST APIs and to Git repositories is suspended.
At that point users are left with the choice of username/apps-specific-password Basic Auth for REST APIs and Git interactions, OAuth for REST APIs and Git/Hg interactions or SSH for Git/HG interactions and one of the previous choices for REST APIs.
SSH and REST API access are beyond the scope of this document.
Read about [Bitbucket's 2FA implementation](https://confluence.atlassian.com/bitbucket/two-step-verification-777023203.html).

App-specific passwords are not particularly user friendly as once created Bitbucket hides their value, even from the owner.
They are intended for use within application that talk to Bitbucket where application can remember and use the app-specific-password.
[Additional information](https://confluence.atlassian.com/display/BITBUCKET/App+passwords).

OAuth is the intended authentication method for user interactions with HTTPS remote URL for Git repositories when 2FA is active.
Essentially once a client application has an OAuth access token it can be used in place of a user's password.
Read more about information [Bitbucket's OAuth implementation](https://confluence.atlassian.com/bitbucket/oauth-on-bitbucket-cloud-238027431.html).

Bitbucket's OAuth implementation follows the standard specifications for OAuth 2.0, which is out of scope for this document.
However it implements a comparatively rare part of OAuth 2.0 Refresh Tokens.
Bitbucket's Access Token's expire after 1 hour if not revoked, as opposed to GitHub's that expire after 1 year.
When GitHub's Access Tokens expire the user must anticipate in the standard OAuth authentication flow to get a new Access Token. Since this occurs, in theory, once per year this is not too onerous.
Since Bitbucket's Access Tokens expire every hour it is too much to expect a user to go through the OAuth authentication flow every hour.
Bitbucket implements refresh Tokens.
Refresh Tokens are issued to the client application at the same time as Access Tokens.
They can only be used to request a new Access Token, and then only if they have not been revoked.
As such the support for Bitbucket and the use of its OAuth in the Git Credentials Manager differs significantly from how VSTS and GitHub are implemented.
This is explained in more detail below.

## Multiple User Accounts

Unlike the GitHub implementation within the Git Credential Manager, the Bitbucket implementation stores 'secrets', passwords, app-specific passwords, or OAuth tokens, with usernames in the [Windows Credential Manager](https://msdn.microsoft.com/en-us/library/windows/desktop/aa374792(v=vs.85).aspx) vault.

Depending on the circumstances this means either saving an explicit username in to the Windows Credential Manager/Vault or including the username in the URL used as the identifying key of entries in the Windows Credential Manager vault, i.e. using a key such as `git:https://mminns@bitbucket.org/` rather than `git:https://bitbucket.org`.
This means that the Bitbucket implementation in the GCM can support multiple accounts, and usernames, for a single user against Bitbucket, e.g. a personal account and a work account.

## Authentication User Experience

When the GCM is triggered by Git, the GCM will check the `host` parameter passed to it.
If it contains `bitbucket.org` it will trigger the Bitbucket related processes.

### Basic Authentication

If the GCM needs to prompt the user for credentials they will always be shown an initial dialog where they can enter a username and password. If the `username` parameter was passed into the GCM it is used to pre-populate the username field, although it can be overridden.
When username and password credentials are submitted the GCM will use them to attempt to retrieve a token, for Basic Authentication this token is in effect the password the user just entered.
The GCM retrieves this `token` by checking the password can be used to successfully retrieve the User profile via the Bitbucket REST API.

If the username and password credentials sent as Basic Authentication credentials works, then the password is identified as the token. The credentials, the username and the password/token, are then stored and the values returned to Git.

If the request for the User profile via the REST API fails with a 401 return code it indicates the username/password combination is invalid, nothing is stored and nothing is returned to Git.

However if the request fails with a 403 (Forbidden) return code, this indicates that the username and password are valid but 2FA is enabled on the Bitbucket Account.
When this occurs the user it prompted to complete the OAuth authentication process.

### OAuth

OAuth authentication prompts the User with a new dialog where they can trigger OAuth authentication.
This involves opening a browser request to `_https://bitbucket.org/site/oauth2/authorize?response_type=code&client_id={consumerkey}&state=authenticated&scope={scopes}&redirect_uri=http://localhost:34106/_`.
This will trigger a flow on Bitbucket where the user must login, potentially including a 2FA prompt, and authorize the GCM to access Bitbucket with the specified scopes.
The GCM will spawn a temporary, local webserver, listening on port 34106, to handle the OAuth redirect/callback.
Assuming the user successfully logins into Bitbucket and authorizes the GCM this callback will include the Access and Refresh Tokens.

The Access and Refresh Tokens will be stored against the username and the username/Access Token credentials returned to Git.

# On-Premise Bitbucket

On-premise Bitbucket, more correctly known as Bitbucket Server or Bitbucket DC, has a number of differences compared to the cloud instance of Bitbucket, https://bitbucket.org.

As far as GCMC is concerned the main difference it doesn't support OAuth so only Basic Authentication is available.

It is possible to test with Bitbucket Server by running it locally using the following command from the Atlassian SDK:

❯ atlas-run-standalone --product bitbucket

See https://developer.atlassian.com/server/framework/atlassian-sdk/atlas-run-standalone/.

This will download and run a standalone instance of Bitbucket Server which can be accessed using the credentials `admin`/`admin` at

https://localhost:7990/bitbucket
60 changes: 60 additions & 0 deletions src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using Microsoft.Git.CredentialManager;
using Microsoft.Git.CredentialManager.Tests.Objects;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Xunit;

namespace Atlassian.Bitbucket.Tests
{
public class BitbucketHostProviderTest
{
[Theory]
// We report that we support unencrypted HTTP here so that we can fail and
// show a helpful error message in the call to `GenerateCredentialAsync` instead.
[InlineData("http", "bitbucket.org", true)]
[InlineData("ssh", "bitbucket.org", false)]
[InlineData("https", "bitbucket.org", true)]
[InlineData("https", "api.bitbucket.org", true)] // Currently does support sub domains.

[InlineData("https", "bitbucket.ogg", false)] // No support of phony similar tld.
[InlineData("https", "bitbucket.com", false)] // No support of wrong tld.
[InlineData("https", "example.com", false)] // No support of non bitbucket domains.

[InlineData("http", "bitbucket.my-company-server.com", false)] // Currently no support for named on-premise instances
[InlineData("https", "my-company-server.com", false)]
[InlineData("https", "bitbucket.my.company.server.com", false)]
[InlineData("https", "api.bitbucket.my-company-server.com", false)]
[InlineData("https", "BITBUCKET.My-Company-Server.Com", false)]
public void BitbucketHostProvider_IsSupported(string protocol, string host, bool expected)
{
var input = new InputArguments(new Dictionary<string, string>
{
["protocol"] = protocol,
["host"] = host,
});

var provider = new BitbucketHostProvider(new TestCommandContext());
Assert.Equal(expected, provider.IsSupported(input));
}

[Theory]
[InlineData("X-AREQUESTID", "123456789", true)] // only the specific header is acceptable
[InlineData("X-REQUESTID", "123456789", false)]
[InlineData(null, null, false)]
public void BitbucketHostProvider_IsSupported_HttpResponseMessage(string header, string value, bool expected)
{
var input = new HttpResponseMessage();
if (header != null)
{
input.Headers.Add(header, value);
}

var provider = new BitbucketHostProvider(new TestCommandContext());
Assert.Equal(expected, provider.IsSupported(input));
}
}
}
8 changes: 6 additions & 2 deletions src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,12 @@ public bool IsSupported(HttpResponseMessage response)
return false;
}

// TODO: identify Bitbucket on-prem instances from the HTTP response
return false;
// Identify Bitbucket on-prem instances from the HTTP response using the Atlassian specific header X-AREQUESTID
var supported = response.Headers.Contains("X-AREQUESTID");

_context.Trace.WriteLine($"Host is{(supported ? null : "n't")} supported as Bitbucket");

return supported;
}

public async Task<ICredential> GetCredentialAsync(InputArguments input)
Expand Down

0 comments on commit d48fb28

Please sign in to comment.