Skip to content

Commit 8b585a2

Browse files
authored
Merge pull request #373 from mminns/issue-267-auto-detect-bitbucket-dc
Issue-267 Add fingerprint header, to autodetect Bitbucket DC instances.
2 parents 1e69646 + 702ddb3 commit 8b585a2

File tree

3 files changed

+146
-2
lines changed

3 files changed

+146
-2
lines changed

docs/bitbucket-development.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Bitbucket Authentication, 2FA and OAuth
2+
3+
By default for authenticating against private Git repositories Bitbucket supports SSH and username/password Basic Auth over HTTPS.
4+
Username/password Basic Auth over HTTPS is also available for REST API access.
5+
Additionally Bitbucket supports App-specific passwords which can be used via Basic Auth as username/app-specific-password.
6+
7+
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.
8+
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.
9+
SSH and REST API access are beyond the scope of this document.
10+
Read about [Bitbucket's 2FA implementation](https://confluence.atlassian.com/bitbucket/two-step-verification-777023203.html).
11+
12+
App-specific passwords are not particularly user friendly as once created Bitbucket hides their value, even from the owner.
13+
They are intended for use within application that talk to Bitbucket where application can remember and use the app-specific-password.
14+
[Additional information](https://confluence.atlassian.com/display/BITBUCKET/App+passwords).
15+
16+
OAuth is the intended authentication method for user interactions with HTTPS remote URL for Git repositories when 2FA is active.
17+
Essentially once a client application has an OAuth access token it can be used in place of a user's password.
18+
Read more about information [Bitbucket's OAuth implementation](https://confluence.atlassian.com/bitbucket/oauth-on-bitbucket-cloud-238027431.html).
19+
20+
Bitbucket's OAuth implementation follows the standard specifications for OAuth 2.0, which is out of scope for this document.
21+
However it implements a comparatively rare part of OAuth 2.0 Refresh Tokens.
22+
Bitbucket's Access Token's expire after 1 hour if not revoked, as opposed to GitHub's that expire after 1 year.
23+
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.
24+
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.
25+
Bitbucket implements refresh Tokens.
26+
Refresh Tokens are issued to the client application at the same time as Access Tokens.
27+
They can only be used to request a new Access Token, and then only if they have not been revoked.
28+
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.
29+
This is explained in more detail below.
30+
31+
## Multiple User Accounts
32+
33+
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.
34+
35+
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://[email protected]/` rather than `git:https://bitbucket.org`.
36+
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.
37+
38+
## Authentication User Experience
39+
40+
When the GCM is triggered by Git, the GCM will check the `host` parameter passed to it.
41+
If it contains `bitbucket.org` it will trigger the Bitbucket related processes.
42+
43+
### Basic Authentication
44+
45+
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.
46+
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.
47+
The GCM retrieves this `token` by checking the password can be used to successfully retrieve the User profile via the Bitbucket REST API.
48+
49+
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.
50+
51+
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.
52+
53+
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.
54+
When this occurs the user it prompted to complete the OAuth authentication process.
55+
56+
### OAuth
57+
58+
OAuth authentication prompts the User with a new dialog where they can trigger OAuth authentication.
59+
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/_`.
60+
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.
61+
The GCM will spawn a temporary, local webserver, listening on port 34106, to handle the OAuth redirect/callback.
62+
Assuming the user successfully logins into Bitbucket and authorizes the GCM this callback will include the Access and Refresh Tokens.
63+
64+
The Access and Refresh Tokens will be stored against the username and the username/Access Token credentials returned to Git.
65+
66+
# On-Premise Bitbucket
67+
68+
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.
69+
70+
As far as GCMC is concerned the main difference it doesn't support OAuth so only Basic Authentication is available.
71+
72+
It is possible to test with Bitbucket Server by running it locally using the following command from the Atlassian SDK:
73+
74+
❯ atlas-run-standalone --product bitbucket
75+
76+
See https://developer.atlassian.com/server/framework/atlassian-sdk/atlas-run-standalone/.
77+
78+
This will download and run a standalone instance of Bitbucket Server which can be accessed using the credentials `admin`/`admin` at
79+
80+
https://localhost:7990/bitbucket
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using Microsoft.Git.CredentialManager;
2+
using Microsoft.Git.CredentialManager.Tests.Objects;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Net.Http;
7+
using System.Text;
8+
using System.Threading.Tasks;
9+
using Xunit;
10+
11+
namespace Atlassian.Bitbucket.Tests
12+
{
13+
public class BitbucketHostProviderTest
14+
{
15+
[Theory]
16+
// We report that we support unencrypted HTTP here so that we can fail and
17+
// show a helpful error message in the call to `GenerateCredentialAsync` instead.
18+
[InlineData("http", "bitbucket.org", true)]
19+
[InlineData("ssh", "bitbucket.org", false)]
20+
[InlineData("https", "bitbucket.org", true)]
21+
[InlineData("https", "api.bitbucket.org", true)] // Currently does support sub domains.
22+
23+
[InlineData("https", "bitbucket.ogg", false)] // No support of phony similar tld.
24+
[InlineData("https", "bitbucket.com", false)] // No support of wrong tld.
25+
[InlineData("https", "example.com", false)] // No support of non bitbucket domains.
26+
27+
[InlineData("http", "bitbucket.my-company-server.com", false)] // Currently no support for named on-premise instances
28+
[InlineData("https", "my-company-server.com", false)]
29+
[InlineData("https", "bitbucket.my.company.server.com", false)]
30+
[InlineData("https", "api.bitbucket.my-company-server.com", false)]
31+
[InlineData("https", "BITBUCKET.My-Company-Server.Com", false)]
32+
public void BitbucketHostProvider_IsSupported(string protocol, string host, bool expected)
33+
{
34+
var input = new InputArguments(new Dictionary<string, string>
35+
{
36+
["protocol"] = protocol,
37+
["host"] = host,
38+
});
39+
40+
var provider = new BitbucketHostProvider(new TestCommandContext());
41+
Assert.Equal(expected, provider.IsSupported(input));
42+
}
43+
44+
[Theory]
45+
[InlineData("X-AREQUESTID", "123456789", true)] // only the specific header is acceptable
46+
[InlineData("X-REQUESTID", "123456789", false)]
47+
[InlineData(null, null, false)]
48+
public void BitbucketHostProvider_IsSupported_HttpResponseMessage(string header, string value, bool expected)
49+
{
50+
var input = new HttpResponseMessage();
51+
if (header != null)
52+
{
53+
input.Headers.Add(header, value);
54+
}
55+
56+
var provider = new BitbucketHostProvider(new TestCommandContext());
57+
Assert.Equal(expected, provider.IsSupported(input));
58+
}
59+
}
60+
}

src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,12 @@ public bool IsSupported(HttpResponseMessage response)
6666
return false;
6767
}
6868

69-
// TODO: identify Bitbucket on-prem instances from the HTTP response
70-
return false;
69+
// Identify Bitbucket on-prem instances from the HTTP response using the Atlassian specific header X-AREQUESTID
70+
var supported = response.Headers.Contains("X-AREQUESTID");
71+
72+
_context.Trace.WriteLine($"Host is{(supported ? null : "n't")} supported as Bitbucket");
73+
74+
return supported;
7175
}
7276

7377
public async Task<ICredential> GetCredentialAsync(InputArguments input)

0 commit comments

Comments
 (0)