@@ -51,15 +51,16 @@ public bool IsSupported(InputArguments input)
51
51
52
52
public async Task < ICredential > GetCredentialAsync ( InputArguments input )
53
53
{
54
+ // Compute the target URI
55
+ Uri targetUri = GetTargetUri ( input ) ;
56
+
54
57
// We should not allow unencrypted communication and should inform the user
55
- if ( StringComparer . OrdinalIgnoreCase . Equals ( input . Protocol , "http" ) )
58
+ if ( StringComparer . OrdinalIgnoreCase . Equals ( input . Protocol , "http" )
59
+ && ! IsBitbucketServer ( targetUri ) )
56
60
{
57
- throw new Exception ( "Unencrypted HTTP is not supported for Bitbucket. Ensure the repository remote URL is using HTTPS." ) ;
61
+ throw new Exception ( "Unencrypted HTTP is not supported for Bitbucket.org. Ensure the repository remote URL is using HTTPS." ) ;
58
62
}
59
63
60
- // Compute the target URI
61
- Uri targetUri = GetTargetUri ( input ) ;
62
-
63
64
// Try and get the username specified in the remote URL if any
64
65
string targetUriUser = targetUri . GetUserName ( ) ;
65
66
@@ -93,7 +94,7 @@ public async Task<ICredential> GetCredentialAsync(InputArguments input)
93
94
// or we have a freshly captured user/pass. Regardless, we must check if these credentials
94
95
// pass and two-factor requirement on the account.
95
96
_context . Trace . WriteLine ( "Checking if two-factor requirements for stored credentials..." ) ;
96
- bool requires2Fa = await RequiresTwoFactorAuthenticationAsync ( credential ) ;
97
+ bool requires2Fa = await RequiresTwoFactorAuthenticationAsync ( credential , targetUri ) ;
97
98
if ( ! requires2Fa )
98
99
{
99
100
_context . Trace . WriteLine ( "Two-factor requirement passed with stored credentials" ) ;
@@ -183,6 +184,19 @@ public Task StoreCredentialAsync(InputArguments input)
183
184
_context . CredentialStore . AddOrUpdate ( credentialKey , credential ) ;
184
185
_context . Trace . WriteLine ( "Credential was successfully stored." ) ;
185
186
187
+ Uri targetUri = GetTargetUri ( input ) ;
188
+ if ( IsBitbucketServer ( targetUri ) )
189
+ {
190
+ // BBS doesn't usually include the username in the urls which means they aren't included in the GET call,
191
+ // which means if we store only with the username the credentials are never found again ...
192
+ // This does have the potential to overwrite itself for different BbS accounts,
193
+ // but typically BbS doesn't encourage multiple user accounts
194
+ string bbsCredentialKey = GetBitbucketServerCredentialKey ( input ) ;
195
+ _context . Trace . WriteLine ( $ "Storing Bitbucket Server credential with key '{ bbsCredentialKey } '...") ;
196
+ _context . CredentialStore . AddOrUpdate ( bbsCredentialKey , credential ) ;
197
+ _context . Trace . WriteLine ( "Bitbucket Server Credential was successfully stored." ) ;
198
+ }
199
+
186
200
return Task . CompletedTask ;
187
201
}
188
202
@@ -220,8 +234,14 @@ private async Task<string> ResolveOAuthUserNameAsync(string accessToken)
220
234
throw new Exception ( $ "Failed to resolve username. HTTP: { result . StatusCode } ") ;
221
235
}
222
236
223
- private async Task < bool > RequiresTwoFactorAuthenticationAsync ( ICredential credentials )
237
+ private async Task < bool > RequiresTwoFactorAuthenticationAsync ( ICredential credentials , Uri targetUri )
224
238
{
239
+ if ( IsBitbucketServer ( targetUri ) )
240
+ {
241
+ // BBS does not support 2FA out of the box so neither does GCM
242
+ return false ;
243
+ }
244
+
225
245
RestApiResult < UserInfo > result = await _bitbucketApi . GetUserInformationAsync ( credentials . UserName , credentials . Password , false ) ;
226
246
switch ( result . StatusCode )
227
247
{
@@ -257,6 +277,21 @@ private string GetCredentialKey(InputArguments input)
257
277
return $ "git:{ url } ";
258
278
}
259
279
280
+ private string GetBitbucketServerCredentialKey ( InputArguments input )
281
+ {
282
+ // The credential (user/pass or an OAuth access token) key is the full target URI.
283
+ // If the full path is included (credential.useHttpPath = true) then respect that.
284
+ string url = GetBitbucketServerTargetUri ( input ) . AbsoluteUri ;
285
+
286
+ // Trim trailing slash
287
+ if ( url . EndsWith ( "/" ) )
288
+ {
289
+ url = url . Substring ( 0 , url . Length - 1 ) ;
290
+ }
291
+
292
+ return $ "git:{ url } ";
293
+ }
294
+
260
295
private string GetRefreshTokenKey ( InputArguments input )
261
296
{
262
297
Uri targetUri = GetTargetUri ( input ) ;
@@ -297,6 +332,23 @@ private static Uri GetTargetUri(InputArguments input)
297
332
return uri ;
298
333
}
299
334
335
+ private static Uri GetBitbucketServerTargetUri ( InputArguments input )
336
+ {
337
+ Uri uri = new UriBuilder
338
+ {
339
+ Scheme = input . Protocol ,
340
+ Host = input . Host ,
341
+ Path = input . Path
342
+ } . Uri ;
343
+
344
+ return uri ;
345
+ }
346
+
347
+ private bool IsBitbucketServer ( Uri targetUri )
348
+ {
349
+ return ! targetUri . Host . Equals ( BitbucketConstants . BitbucketBaseUrlHost ) ;
350
+ }
351
+
300
352
#endregion
301
353
302
354
public void Dispose ( )
0 commit comments