From f45a5057c2a471b5938aa80e566ca5f286d47e90 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Mon, 20 Jun 2022 10:17:55 +0100 Subject: [PATCH 1/8] bb-test: update tests to await ICredential results --- .../BitbucketHostProviderTest.cs | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs b/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs index 4e5230c5e..40fe7a762 100644 --- a/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs +++ b/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs @@ -138,7 +138,7 @@ public void BitbucketHostProvider_IsSupported_HttpResponseMessage(string header, [Theory] [InlineData("https", DC_SERVER_HOST, "jsquire", "password")] [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password")] - public void BitbucketHostProvider_GetCredentialAsync_Succeeds_ForValidStoredBasicAuthAccount(string protocol, string host, string username,string password) + public async Task BitbucketHostProvider_GetCredentialAsync_Succeeds_ForValidStoredBasicAuthAccount(string protocol, string host, string username,string password) { InputArguments input = MockInput(protocol, host, username); @@ -149,7 +149,7 @@ public void BitbucketHostProvider_GetCredentialAsync_Succeeds_ForValidStoredBasi var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); - var credential = provider.GetCredentialAsync(input); + var credential = await provider.GetCredentialAsync(input); //verify bitbucket.org credentials were validated if (BITBUCKET_DOT_ORG_HOST.Equals(host)) @@ -172,7 +172,7 @@ public void BitbucketHostProvider_GetCredentialAsync_Succeeds_ForValidStoredBasi [Theory] // DC/Server does not currently support OAuth [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password")] - public void BitbucketHostProvider_GetCredentialAsync_Succeeds_ForValidStoredOAuthAccount(string protocol, string host, string username,string token) + public async Task BitbucketHostProvider_GetCredentialAsync_Succeeds_ForValidStoredOAuthAccount(string protocol, string host, string username,string token) { InputArguments input = MockInput(protocol, host, username); @@ -183,7 +183,7 @@ public void BitbucketHostProvider_GetCredentialAsync_Succeeds_ForValidStoredOAut var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); - var credential = provider.GetCredentialAsync(input); + var credential = await provider.GetCredentialAsync(input); //verify bitbucket.org credentials were validated VerifyValidateOAuthCredentialsRan(); @@ -200,7 +200,7 @@ public void BitbucketHostProvider_GetCredentialAsync_Succeeds_ForValidStoredOAut [InlineData("https", DC_SERVER_HOST, "jsquire", "password")] // cloud [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password")] - public void BitbucketHostProvider_GetCredentialAsync_Succeeds_ForFreshValidBasicAuthAccount(string protocol, string host, string username, string password) + public async Task BitbucketHostProvider_GetCredentialAsync_Succeeds_ForFreshValidBasicAuthAccount(string protocol, string host, string username, string password) { InputArguments input = MockInput(protocol, host, username); @@ -217,7 +217,7 @@ public void BitbucketHostProvider_GetCredentialAsync_Succeeds_ForFreshValidBasic var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); - var credential = provider.GetCredentialAsync(input); + var credential = await provider.GetCredentialAsync(input); VerifyBasicAuthFlowRan(password, true, input, credential); @@ -227,7 +227,7 @@ public void BitbucketHostProvider_GetCredentialAsync_Succeeds_ForFreshValidBasic [Theory] // DC/Server does not currently support OAuth [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", MOCK_ACCESS_TOKEN)] - public void BitbucketHostProvider_GetCredentialAsync_Succeeds_ForFreshValid2FAAcccount(string protocol, string host, string username, string password) + public async Task BitbucketHostProvider_GetCredentialAsync_Succeeds_ForFreshValid2FAAcccount(string protocol, string host, string username, string password) { var input = MockInput(protocol, host, username); @@ -242,7 +242,7 @@ public void BitbucketHostProvider_GetCredentialAsync_Succeeds_ForFreshValid2FAAc var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); - var credential = provider.GetCredentialAsync(input); + var credential = await provider.GetCredentialAsync(input); VerifyOAuthFlowRan(password, false, true, input, credential, null); @@ -254,7 +254,7 @@ public void BitbucketHostProvider_GetCredentialAsync_Succeeds_ForFreshValid2FAAc [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password", "basic")] [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password", "oauth")] // Basic Auth works - public void BitbucketHostProvider_GetCredentialAsync_ForcedAuthMode_IsRespected(string protocol, string host, string username, string password, + public async Task BitbucketHostProvider_GetCredentialAsync_ForcedAuthMode_IsRespected(string protocol, string host, string username, string password, string preconfiguredAuthModes) { var input = MockInput(protocol, host, username); @@ -271,7 +271,7 @@ public void BitbucketHostProvider_GetCredentialAsync_ForcedAuthMode_IsRespected( var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); - var credential = provider.GetCredentialAsync(input); + var credential = await provider.GetCredentialAsync(input); Assert.NotNull(credential); @@ -301,7 +301,7 @@ public void BitbucketHostProvider_GetCredentialAsync_ForcedAuthMode_IsRespected( [InlineData("https", DC_SERVER_HOST, "jsquire", "password", "1")] [InlineData("https", DC_SERVER_HOST, "jsquire", "password", "true")] [InlineData("https", DC_SERVER_HOST, "jsquire", "password", null)] - public void BitbucketHostProvider_GetCredentialAsync_AlwaysRefreshCredentials_IsRespected(string protocol, string host, string username, string password, + public async Task BitbucketHostProvider_GetCredentialAsync_AlwaysRefreshCredentials_IsRespected(string protocol, string host, string username, string password, string alwaysRefreshCredentials) { var input = MockInput(protocol, host, username); @@ -319,7 +319,7 @@ public void BitbucketHostProvider_GetCredentialAsync_AlwaysRefreshCredentials_Is var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); - var credential = provider.GetCredentialAsync(input); + var credential = await provider.GetCredentialAsync(input); var alwaysRefreshCredentialsBool = "1".Equals(alwaysRefreshCredentials) || "on".Equals(alwaysRefreshCredentials) @@ -450,7 +450,7 @@ private static InputArguments MockInput(string protocol, string host, string use }); } - private void VerifyBasicAuthFlowRan(string password, bool expected, InputArguments input, Task credential) + private void VerifyBasicAuthFlowRan(string password, bool expected, InputArguments input, ICredential credential) { Assert.Equal(expected, credential != null); @@ -459,7 +459,7 @@ private void VerifyBasicAuthFlowRan(string password, bool expected, InputArgumen bitbucketAuthentication.Verify(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny()), Times.Once); } - private void VerifyInteractiveBasicAuthFlowRan(string password, InputArguments input, Task credential) + private void VerifyInteractiveBasicAuthFlowRan(string password, InputArguments input, ICredential credential) { var remoteUri = input.GetRemoteUri(); @@ -485,14 +485,14 @@ private void VerifyBasicAuthFlowNeverRan(string password, InputArguments input, } } - private void VerifyInteractiveBasicAuthFlowNeverRan(string password, InputArguments input, Task credential) + private void VerifyInteractiveBasicAuthFlowNeverRan(string password, InputArguments input, ICredential credential) { var remoteUri = input.GetRemoteUri(); bitbucketAuthentication.Verify(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny()), Times.Never); } - private void VerifyOAuthFlowRan(string password, bool storedAccount, bool expected, InputArguments input, Task credential, + private void VerifyOAuthFlowRan(string password, bool storedAccount, bool expected, InputArguments input, ICredential credential, string preconfiguredAuthModes) { Assert.Equal(expected, credential != null); @@ -517,7 +517,7 @@ private void VerifyOAuthFlowRan(string password, bool storedAccount, bool expect } } - private void VerifyInteractiveOAuthFlowRan(string password, InputArguments input, System.Threading.Tasks.Task credential) + private void VerifyInteractiveOAuthFlowRan(string password, InputArguments input, ICredential credential) { var remoteUri = input.GetRemoteUri(); @@ -526,7 +526,7 @@ private void VerifyInteractiveOAuthFlowRan(string password, InputArguments input } - private void VerifyOAuthFlowDidNotRun(string password, bool expected, InputArguments input, System.Threading.Tasks.Task credential) + private void VerifyOAuthFlowDidNotRun(string password, bool expected, InputArguments input, ICredential credential) { Assert.Equal(expected, credential != null); @@ -542,7 +542,7 @@ private void VerifyOAuthFlowDidNotRun(string password, bool expected, InputArgum bitbucketApi.Verify(m => m.GetUserInformationAsync(null, MOCK_ACCESS_TOKEN, true), Times.Never); } - private void VerifyInteractiveOAuthFlowNeverRan(InputArguments input, System.Threading.Tasks.Task credential) + private void VerifyInteractiveOAuthFlowNeverRan(InputArguments input, ICredential credential) { var remoteUri = input.GetRemoteUri(); From 2babadc616cd6b7782f1a6f83012a521bb670952 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Mon, 20 Jun 2022 14:01:32 +0100 Subject: [PATCH 2/8] bb-test: inline 2FA mocking test helpers Inline some 2FA mock setup helper methods that differ only by a single boolean argument. --- .../BitbucketHostProviderTest.cs | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs b/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs index 40fe7a762..f2acd0da8 100644 --- a/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs +++ b/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs @@ -145,7 +145,7 @@ public async Task BitbucketHostProvider_GetCredentialAsync_Succeeds_ForValidStor var context = new TestCommandContext(); MockStoredAccount(context, input, password); - MockRemoteBasicAuthAccountIsValidNo2FA(bitbucketApi, input, password); + MockRemoteBasicAuthAccountIsValid(bitbucketApi, input, password, twoFactor: false); var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); @@ -213,7 +213,7 @@ public async Task BitbucketHostProvider_GetCredentialAsync_Succeeds_ForFreshVali MockRemoteOAuthAccountIsValid(bitbucketApi, input, password, true); } - MockRemoteBasicAuthAccountIsValidNo2FA(bitbucketApi, input, password); + MockRemoteBasicAuthAccountIsValid(bitbucketApi, input, password, twoFactor: false); var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); @@ -236,7 +236,7 @@ public async Task BitbucketHostProvider_GetCredentialAsync_Succeeds_ForFreshVali // user is prompted for basic auth credentials MockUserEntersValidBasicCredentials(bitbucketAuthentication, input, password); // basic auth credentials are valid but 2FA is ON - MockRemoteBasicAuthAccountIsValidRequires2FA(bitbucketApi, input, password); + MockRemoteBasicAuthAccountIsValid(bitbucketApi, input, password, twoFactor: true); MockRemoteOAuthAccountIsValid(bitbucketApi, input, password, true); MockRemoteValidRefreshToken(); @@ -266,7 +266,7 @@ public async Task BitbucketHostProvider_GetCredentialAsync_ForcedAuthMode_IsResp } MockUserEntersValidBasicCredentials(bitbucketAuthentication, input, password); - MockRemoteBasicAuthAccountIsValidRequires2FA(bitbucketApi, input, password); + MockRemoteBasicAuthAccountIsValid(bitbucketApi, input, password, twoFactor: true); bitbucketAuthentication.Setup(m => m.ShowOAuthRequiredPromptAsync()).ReturnsAsync(true); var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); @@ -315,7 +315,7 @@ public async Task BitbucketHostProvider_GetCredentialAsync_AlwaysRefreshCredenti MockStoredAccount(context, input, password); MockUserEntersValidBasicCredentials(bitbucketAuthentication, input, password); MockRemoteOAuthAccountIsValid(bitbucketApi, input, password, true); - MockRemoteBasicAuthAccountIsValidNo2FA(bitbucketApi, input, password); + MockRemoteBasicAuthAccountIsValid(bitbucketApi, input, password, twoFactor: false); var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); @@ -390,7 +390,7 @@ public async Task BitbucketHostProvider_GetCredentialAsync_ValidateTargetUriAsyn else { MockUserEntersValidBasicCredentials(bitbucketAuthentication, input, password); - MockRemoteBasicAuthAccountIsValidRequires2FA(bitbucketApi, input, password); + MockRemoteBasicAuthAccountIsValid(bitbucketApi, input, password, twoFactor: true); MockRemoteValidRefreshToken(); bitbucketAuthentication.Setup(m => m.ShowOAuthRequiredPromptAsync()).ReturnsAsync(true); bitbucketAuthentication.Setup(m => m.CreateOAuthCredentialsAsync(It.IsAny())).ReturnsAsync(new OAuth2TokenResult(MOCK_ACCESS_TOKEN, "access_token")); @@ -615,25 +615,15 @@ private static void MockUserDoesNotEntersValidBasicCredentials(Mock bitbucketApi, InputArguments input, string password, bool twoFAEnabled) + private static void MockRemoteBasicAuthAccountIsValid(Mock bitbucketApi, InputArguments input, string password, bool twoFactor) { - var userInfo = new UserInfo() { IsTwoFactorAuthenticationEnabled = twoFAEnabled }; + var userInfo = new UserInfo() { IsTwoFactorAuthenticationEnabled = twoFactor }; // Basic bitbucketApi.Setup(x => x.GetUserInformationAsync(input.UserName, password, false)) .ReturnsAsync(new RestApiResult(System.Net.HttpStatusCode.OK, userInfo)); } - private static void MockRemoteBasicAuthAccountIsValidRequires2FA(Mock bitbucketApi, InputArguments input, string password) - { - MockRemoteBasicAuthAccountIsValid(bitbucketApi, input, password, true); - } - - private static void MockRemoteBasicAuthAccountIsValidNo2FA(Mock bitbucketApi, InputArguments input, string password) - { - MockRemoteBasicAuthAccountIsValid(bitbucketApi, input, password, false); - } - private static void MockRemoteBasicAuthAccountIsInvalid(Mock bitbucketApi, InputArguments input, string password) { var userInfo = new UserInfo(); From 81eccb90bf9952b754488b1f74c088f915f77e5c Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Mon, 20 Jun 2022 14:04:48 +0100 Subject: [PATCH 3/8] bb-test: remove unused test helpers --- .../BitbucketHostProviderTest.cs | 46 ------------------- 1 file changed, 46 deletions(-) diff --git a/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs b/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs index f2acd0da8..280ecb505 100644 --- a/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs +++ b/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs @@ -568,40 +568,17 @@ private void VerifyValidateBasicAuthCredentialsRan() bitbucketApi.Verify(m => m.GetUserInformationAsync(It.IsAny(), It.IsAny(), false), Times.Once); } - private void VerifyValidateOAuthCredentialsNeverRan() - { - // never check username/password works - bitbucketApi.Verify(m => m.GetUserInformationAsync(null, It.IsAny(), false), Times.Never); - } - private void VerifyValidateOAuthCredentialsRan() { // check username/password works bitbucketApi.Verify(m => m.GetUserInformationAsync(null, It.IsAny(), true), Times.Once); } - private void MockStoredOAuthAccount(TestCommandContext context, InputArguments input) - { - // refresh token - context.CredentialStore.Add("https://bitbucket.org/refresh_token", new TestCredential(input.Host, input.UserName, MOCK_REFRESH_TOKEN)); - // auth token - context.CredentialStore.Add("https://bitbucket.org", new TestCredential(input.Host, input.UserName, MOCK_ACCESS_TOKEN)); - } - private void MockRemoteValidRefreshToken() { bitbucketAuthentication.Setup(m => m.RefreshOAuthCredentialsAsync(MOCK_REFRESH_TOKEN)).ReturnsAsync(new OAuth2TokenResult(MOCK_ACCESS_TOKEN, "access_token")); } - private static void MockInvalidRemoteBasicAccount(Mock bitbucketApi, Mock bitbucketAuthentication) - { - bitbucketAuthentication.Setup(m => m.GetCredentialsAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new CredentialsPromptResult(AuthenticationModes.Basic, null)); - - bitbucketApi.Setup(x => x.GetUserInformationAsync(It.IsAny(), It.IsAny(), false)) - .ReturnsAsync(new RestApiResult(System.Net.HttpStatusCode.Unauthorized)); - - } private static void MockUserEntersValidBasicCredentials(Mock bitbucketAuthentication, InputArguments input, string password) { var remoteUri = input.GetRemoteUri(); @@ -609,12 +586,6 @@ private static void MockUserEntersValidBasicCredentials(Mock bitbucketAuthentication) - { - bitbucketAuthentication.Setup(m => m.GetCredentialsAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new CredentialsPromptResult(AuthenticationModes.Basic, null)); - } - private static void MockRemoteBasicAuthAccountIsValid(Mock bitbucketApi, InputArguments input, string password, bool twoFactor) { var userInfo = new UserInfo() { IsTwoFactorAuthenticationEnabled = twoFactor }; @@ -624,15 +595,6 @@ private static void MockRemoteBasicAuthAccountIsValid(Mock bi } - private static void MockRemoteBasicAuthAccountIsInvalid(Mock bitbucketApi, InputArguments input, string password) - { - var userInfo = new UserInfo(); - // Basic - bitbucketApi.Setup(x => x.GetUserInformationAsync(input.UserName, password, false)) - .ReturnsAsync(new RestApiResult(System.Net.HttpStatusCode.Forbidden, userInfo)); - - } - private static void MockRemoteOAuthAccountIsValid(Mock bitbucketApi, InputArguments input, string password, bool twoFAEnabled) { var userInfo = new UserInfo() { IsTwoFactorAuthenticationEnabled = twoFAEnabled }; @@ -648,14 +610,6 @@ private static void MockStoredAccount(TestCommandContext context, InputArguments context.CredentialStore.Add(remoteUrl, new TestCredential(input.Host, input.UserName, password)); } - private static void MockValidStoredOAuthUser(TestCommandContext context, Mock bitbucketApi) - { - var userInfo = new UserInfo() { IsTwoFactorAuthenticationEnabled = false }; - bitbucketApi.Setup(x => x.GetUserInformationAsync("jsquire", "password1", false)) - .ReturnsAsync(new RestApiResult(System.Net.HttpStatusCode.OK, userInfo)); - context.CredentialStore.Add("https://bitbucket.org", new TestCredential("https://bitbucket.org", "jsquire", "password1")); - } - #endregion } } From 8e143e066759926d016c998b8f534818329d0121 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Mon, 20 Jun 2022 14:11:58 +0100 Subject: [PATCH 4/8] bb-test: rename test/mock helper methods Rename some of the test/mock helper methods to be a little easier to grok/be shorter. --- .../BitbucketHostProviderTest.cs | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs b/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs index 280ecb505..8dbb76fed 100644 --- a/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs +++ b/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs @@ -145,7 +145,7 @@ public async Task BitbucketHostProvider_GetCredentialAsync_Succeeds_ForValidStor var context = new TestCommandContext(); MockStoredAccount(context, input, password); - MockRemoteBasicAuthAccountIsValid(bitbucketApi, input, password, twoFactor: false); + MockRemoteBasicValid(bitbucketApi, input, password, twoFactor: false); var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); @@ -163,7 +163,7 @@ public async Task BitbucketHostProvider_GetCredentialAsync_Succeeds_ForValidStor } // Stored credentials so don't ask for more - VerifyInteractiveBasicAuthFlowNeverRan(password, input, credential); + VerifyInteractiveAuthNeverRan(password, input, credential); // Valid Basic Auth credentials so don't run Oauth VerifyInteractiveOAuthFlowNeverRan(input, credential); @@ -179,7 +179,7 @@ public async Task BitbucketHostProvider_GetCredentialAsync_Succeeds_ForValidStor var context = new TestCommandContext(); MockStoredAccount(context, input, token); - MockRemoteOAuthAccountIsValid(bitbucketApi, input, token, false); + MockRemoteAccessTokenValid(bitbucketApi, input, token, false); var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); @@ -189,7 +189,7 @@ public async Task BitbucketHostProvider_GetCredentialAsync_Succeeds_ForValidStor VerifyValidateOAuthCredentialsRan(); // Stored credentials so don't ask for more - VerifyInteractiveBasicAuthFlowNeverRan(token, input, credential); + VerifyInteractiveAuthNeverRan(token, input, credential); // Valid Basic Auth credentials so don't run Oauth VerifyInteractiveOAuthFlowNeverRan(input, credential); @@ -206,14 +206,14 @@ public async Task BitbucketHostProvider_GetCredentialAsync_Succeeds_ForFreshVali var context = new TestCommandContext(); - MockUserEntersValidBasicCredentials(bitbucketAuthentication, input, password); + MockPromptBasic(bitbucketAuthentication, input, password); if (BITBUCKET_DOT_ORG_HOST.Equals(host)) { - MockRemoteOAuthAccountIsValid(bitbucketApi, input, password, true); + MockRemoteAccessTokenValid(bitbucketApi, input, password, true); } - MockRemoteBasicAuthAccountIsValid(bitbucketApi, input, password, twoFactor: false); + MockRemoteBasicValid(bitbucketApi, input, password, twoFactor: false); var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); @@ -234,10 +234,10 @@ public async Task BitbucketHostProvider_GetCredentialAsync_Succeeds_ForFreshVali var context = new TestCommandContext(); // user is prompted for basic auth credentials - MockUserEntersValidBasicCredentials(bitbucketAuthentication, input, password); + MockPromptBasic(bitbucketAuthentication, input, password); // basic auth credentials are valid but 2FA is ON - MockRemoteBasicAuthAccountIsValid(bitbucketApi, input, password, twoFactor: true); - MockRemoteOAuthAccountIsValid(bitbucketApi, input, password, true); + MockRemoteBasicValid(bitbucketApi, input, password, twoFactor: true); + MockRemoteAccessTokenValid(bitbucketApi, input, password, true); MockRemoteValidRefreshToken(); var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); @@ -265,8 +265,8 @@ public async Task BitbucketHostProvider_GetCredentialAsync_ForcedAuthMode_IsResp context.Environment.Variables.Add(BitbucketConstants.EnvironmentVariables.AuthenticationModes, preconfiguredAuthModes); } - MockUserEntersValidBasicCredentials(bitbucketAuthentication, input, password); - MockRemoteBasicAuthAccountIsValid(bitbucketApi, input, password, twoFactor: true); + MockPromptBasic(bitbucketAuthentication, input, password); + MockRemoteBasicValid(bitbucketApi, input, password, twoFactor: true); bitbucketAuthentication.Setup(m => m.ShowOAuthRequiredPromptAsync()).ReturnsAsync(true); var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); @@ -283,7 +283,7 @@ public async Task BitbucketHostProvider_GetCredentialAsync_ForcedAuthMode_IsResp if (preconfiguredAuthModes.Contains("oauth")) { - VerifyInteractiveBasicAuthFlowNeverRan(password, input, credential); + VerifyInteractiveAuthNeverRan(password, input, credential); VerifyInteractiveOAuthFlowRan(password, input, credential); } } @@ -313,9 +313,9 @@ public async Task BitbucketHostProvider_GetCredentialAsync_AlwaysRefreshCredenti } MockStoredAccount(context, input, password); - MockUserEntersValidBasicCredentials(bitbucketAuthentication, input, password); - MockRemoteOAuthAccountIsValid(bitbucketApi, input, password, true); - MockRemoteBasicAuthAccountIsValid(bitbucketApi, input, password, twoFactor: false); + MockPromptBasic(bitbucketAuthentication, input, password); + MockRemoteAccessTokenValid(bitbucketApi, input, password, true); + MockRemoteBasicValid(bitbucketApi, input, password, twoFactor: false); var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); @@ -389,8 +389,8 @@ public async Task BitbucketHostProvider_GetCredentialAsync_ValidateTargetUriAsyn } else { - MockUserEntersValidBasicCredentials(bitbucketAuthentication, input, password); - MockRemoteBasicAuthAccountIsValid(bitbucketApi, input, password, twoFactor: true); + MockPromptBasic(bitbucketAuthentication, input, password); + MockRemoteBasicValid(bitbucketApi, input, password, twoFactor: true); MockRemoteValidRefreshToken(); bitbucketAuthentication.Setup(m => m.ShowOAuthRequiredPromptAsync()).ReturnsAsync(true); bitbucketAuthentication.Setup(m => m.CreateOAuthCredentialsAsync(It.IsAny())).ReturnsAsync(new OAuth2TokenResult(MOCK_ACCESS_TOKEN, "access_token")); @@ -485,7 +485,7 @@ private void VerifyBasicAuthFlowNeverRan(string password, InputArguments input, } } - private void VerifyInteractiveBasicAuthFlowNeverRan(string password, InputArguments input, ICredential credential) + private void VerifyInteractiveAuthNeverRan(string password, InputArguments input, ICredential credential) { var remoteUri = input.GetRemoteUri(); @@ -579,14 +579,14 @@ private void MockRemoteValidRefreshToken() bitbucketAuthentication.Setup(m => m.RefreshOAuthCredentialsAsync(MOCK_REFRESH_TOKEN)).ReturnsAsync(new OAuth2TokenResult(MOCK_ACCESS_TOKEN, "access_token")); } - private static void MockUserEntersValidBasicCredentials(Mock bitbucketAuthentication, InputArguments input, string password) + private static void MockPromptBasic(Mock bitbucketAuthentication, InputArguments input, string password) { var remoteUri = input.GetRemoteUri(); bitbucketAuthentication.Setup(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny())) .ReturnsAsync(new CredentialsPromptResult(AuthenticationModes.Basic, new TestCredential(input.Host, input.UserName, password))); } - private static void MockRemoteBasicAuthAccountIsValid(Mock bitbucketApi, InputArguments input, string password, bool twoFactor) + private static void MockRemoteBasicValid(Mock bitbucketApi, InputArguments input, string password, bool twoFactor) { var userInfo = new UserInfo() { IsTwoFactorAuthenticationEnabled = twoFactor }; // Basic @@ -595,7 +595,7 @@ private static void MockRemoteBasicAuthAccountIsValid(Mock bi } - private static void MockRemoteOAuthAccountIsValid(Mock bitbucketApi, InputArguments input, string password, bool twoFAEnabled) + private static void MockRemoteAccessTokenValid(Mock bitbucketApi, InputArguments input, string password, bool twoFAEnabled) { var userInfo = new UserInfo() { IsTwoFactorAuthenticationEnabled = twoFAEnabled }; // OAuth From bf068e1ded25347959ea6ee2dfc1e7d1f398344d Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Mon, 20 Jun 2022 10:04:32 +0100 Subject: [PATCH 5/8] bitbucket: remove separate OAuth prompt/dialog Remove the separate "OAuth Required" UI prompt in favour of just always showing the "Credentials" prompt, where users can select the OAuth auth mode directly anyway. --- .../BitbucketAuthenticationTest.cs | 15 - .../BitbucketHostProviderTest.cs | 459 +++++++++--------- .../BitbucketAuthentication.cs | 47 +- .../BitbucketHostProvider.cs | 69 +-- 4 files changed, 252 insertions(+), 338 deletions(-) diff --git a/src/shared/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs b/src/shared/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs index 0606c8111..9fac31e93 100644 --- a/src/shared/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs +++ b/src/shared/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs @@ -108,21 +108,6 @@ public async Task BitbucketAuthentication_GetCredentialsAsync_All_NoDesktopSessi Assert.Equal(password, result.Credential.Password); } - [Fact] - public async Task BitbucketAuthentication_ShowOAuthRequiredPromptAsync_SucceedsAfterUserInput() - { - var context = new TestCommandContext(); - context.Terminal.Prompts["Press enter to continue..."] = " "; - - var bitbucketAuthentication = new BitbucketAuthentication(context); - - var result = await bitbucketAuthentication.ShowOAuthRequiredPromptAsync(); - - Assert.True(result); - Assert.Equal($"Your account has two-factor authentication enabled.{Environment.NewLine}" + - $"To continue you must complete authentication in your web browser.{Environment.NewLine}", context.Terminal.Messages[0].Item1); - } - [Fact] public async Task BitbucketAuthentication_GetCredentialsAsync_AllModes_NoUser_BBCloud_HelperCmdLine() { diff --git a/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs b/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs index 8dbb76fed..f658c4029 100644 --- a/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs +++ b/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs @@ -15,6 +15,8 @@ public class BitbucketHostProviderTest #region Tests private const string MOCK_ACCESS_TOKEN = "at-0987654321"; + private const string MOCK_ACCESS_TOKEN_ALT = "at-onetwothreefour-1234"; + private const string MOCK_EXPIRED_ACCESS_TOKEN = "at-1234567890-expired"; private const string MOCK_REFRESH_TOKEN = "rt-1234567809"; private const string BITBUCKET_DOT_ORG_HOST = "bitbucket.org"; private const string DC_SERVER_HOST = "example.com"; @@ -138,136 +140,189 @@ public void BitbucketHostProvider_IsSupported_HttpResponseMessage(string header, [Theory] [InlineData("https", DC_SERVER_HOST, "jsquire", "password")] [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password")] - public async Task BitbucketHostProvider_GetCredentialAsync_Succeeds_ForValidStoredBasicAuthAccount(string protocol, string host, string username,string password) + public async Task BitbucketHostProvider_GetCredentialAsync_Valid_Stored_Basic( + string protocol, string host, string username, string password) { InputArguments input = MockInput(protocol, host, username); var context = new TestCommandContext(); MockStoredAccount(context, input, password); - MockRemoteBasicValid(bitbucketApi, input, password, twoFactor: false); + MockRemoteBasicValid(input, password); var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); var credential = await provider.GetCredentialAsync(input); - //verify bitbucket.org credentials were validated + Assert.Equal(username, credential.Account); + Assert.Equal(password, credential.Password); + + // Verify bitbucket.org credentials were validated if (BITBUCKET_DOT_ORG_HOST.Equals(host)) { - VerifyValidateBasicAuthCredentialsRan(); + VerifyValidateBasicAuthCredentialsRan(input, password); } else { - //verify DC/Server credentials were not validated + // Verify DC/Server credentials were not validated VerifyValidateBasicAuthCredentialsNeverRan(); } // Stored credentials so don't ask for more - VerifyInteractiveAuthNeverRan(password, input, credential); - - // Valid Basic Auth credentials so don't run Oauth - VerifyInteractiveOAuthFlowNeverRan(input, credential); + VerifyInteractiveAuthNeverRan(); } [Theory] // DC/Server does not currently support OAuth [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password")] - public async Task BitbucketHostProvider_GetCredentialAsync_Succeeds_ForValidStoredOAuthAccount(string protocol, string host, string username,string token) + public async Task BitbucketHostProvider_GetCredentialAsync_Valid_Stored_OAuth( + string protocol, string host, string username, string token) { InputArguments input = MockInput(protocol, host, username); var context = new TestCommandContext(); MockStoredAccount(context, input, token); - MockRemoteAccessTokenValid(bitbucketApi, input, token, false); + MockRemoteAccessTokenValid(input, token); var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); var credential = await provider.GetCredentialAsync(input); - //verify bitbucket.org credentials were validated - VerifyValidateOAuthCredentialsRan(); + Assert.Equal(username, credential.Account); + Assert.Equal(token, credential.Password); - // Stored credentials so don't ask for more - VerifyInteractiveAuthNeverRan(token, input, credential); + // Verify bitbucket.org credentials were validated + VerifyValidateAccessTokenRan(input, token); - // Valid Basic Auth credentials so don't run Oauth - VerifyInteractiveOAuthFlowNeverRan(input, credential); + // Stored credentials so don't ask for more + VerifyInteractiveAuthNeverRan(); } [Theory] // DC [InlineData("https", DC_SERVER_HOST, "jsquire", "password")] - // cloud + // Cloud [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password")] - public async Task BitbucketHostProvider_GetCredentialAsync_Succeeds_ForFreshValidBasicAuthAccount(string protocol, string host, string username, string password) + public async Task BitbucketHostProvider_GetCredentialAsync_Valid_New_Basic( + string protocol, string host, string username, string password) { InputArguments input = MockInput(protocol, host, username); var context = new TestCommandContext(); - MockPromptBasic(bitbucketAuthentication, input, password); + MockPromptBasic(input, password); + MockRemoteBasicValid(input, password); - if (BITBUCKET_DOT_ORG_HOST.Equals(host)) - { - MockRemoteAccessTokenValid(bitbucketApi, input, password, true); - } + var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); + + var credential = await provider.GetCredentialAsync(input); + + Assert.Equal(username, credential.Account); + Assert.Equal(password, credential.Password); + + VerifyInteractiveAuthRan(input); + } + + [Theory] + // DC/Server does not currently support OAuth + [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", MOCK_REFRESH_TOKEN, MOCK_ACCESS_TOKEN)] + public async Task BitbucketHostProvider_GetCredentialAsync_Valid_New_OAuth( + string protocol, string host, string username, string refreshToken, string accessToken) + { + InputArguments input = MockInput(protocol, host, username); + + var context = new TestCommandContext(); - MockRemoteBasicValid(bitbucketApi, input, password, twoFactor: false); + MockPromptOAuth(input); + MockRemoteOAuthTokenCreate(input, accessToken, refreshToken); + MockRemoteAccessTokenValid(input, accessToken); var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); var credential = await provider.GetCredentialAsync(input); - VerifyBasicAuthFlowRan(password, true, input, credential); + Assert.Equal(username, credential.Account); + Assert.Equal(accessToken, credential.Password); - VerifyOAuthFlowDidNotRun(password, true, input, credential); + VerifyInteractiveAuthRan(input); + VerifyOAuthFlowRan(input, accessToken); + VerifyValidateAccessTokenRan(input, accessToken); + VerifyOAuthRefreshTokenStored(context, input, refreshToken); } [Theory] // DC/Server does not currently support OAuth - [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", MOCK_ACCESS_TOKEN)] - public async Task BitbucketHostProvider_GetCredentialAsync_Succeeds_ForFreshValid2FAAcccount(string protocol, string host, string username, string password) + [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", MOCK_REFRESH_TOKEN, MOCK_ACCESS_TOKEN)] + public async Task BitbucketHostProvider_GetCredentialAsync_MissingAT_OAuth_Refresh( + string protocol, string host, string username, string refreshToken, string accessToken) { var input = MockInput(protocol, host, username); var context = new TestCommandContext(); - // user is prompted for basic auth credentials - MockPromptBasic(bitbucketAuthentication, input, password); - // basic auth credentials are valid but 2FA is ON - MockRemoteBasicValid(bitbucketApi, input, password, twoFactor: true); - MockRemoteAccessTokenValid(bitbucketApi, input, password, true); - MockRemoteValidRefreshToken(); + // AT has does not exist, but RT is still valid + MockStoredRefreshToken(context, input, refreshToken); + MockRemoteAccessTokenValid(input, accessToken); + MockRemoteRefreshTokenValid(refreshToken, accessToken); var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); var credential = await provider.GetCredentialAsync(input); - VerifyOAuthFlowRan(password, false, true, input, credential, null); + Assert.Equal(username, credential.Account); + Assert.Equal(accessToken, credential.Password); - VerifyBasicAuthFlowNeverRan(password, input, false, null); + VerifyValidateAccessTokenRan(input, accessToken); + VerifyOAuthRefreshRan(refreshToken); + VerifyInteractiveAuthNeverRan(); } [Theory] - // cloud - [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password", "basic")] - [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password", "oauth")] - // Basic Auth works - public async Task BitbucketHostProvider_GetCredentialAsync_ForcedAuthMode_IsRespected(string protocol, string host, string username, string password, - string preconfiguredAuthModes) + // DC/Server does not currently support OAuth + [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", MOCK_REFRESH_TOKEN, MOCK_EXPIRED_ACCESS_TOKEN, MOCK_ACCESS_TOKEN)] + public async Task BitbucketHostProvider_GetCredentialAsync_ExpiredAT_OAuth_Refresh( + string protocol, string host, string username, string refreshToken, string expiredAccessToken, string accessToken) { var input = MockInput(protocol, host, username); var context = new TestCommandContext(); - if (preconfiguredAuthModes != null) - { - context.Environment.Variables.Add(BitbucketConstants.EnvironmentVariables.AuthenticationModes, preconfiguredAuthModes); - } - MockPromptBasic(bitbucketAuthentication, input, password); - MockRemoteBasicValid(bitbucketApi, input, password, twoFactor: true); - bitbucketAuthentication.Setup(m => m.ShowOAuthRequiredPromptAsync()).ReturnsAsync(true); + // AT exists but has expired, but RT is still valid + MockStoredAccount(context, input, expiredAccessToken); + MockRemoteAccessTokenExpired(input, expiredAccessToken); + + MockStoredRefreshToken(context, input, refreshToken); + MockRemoteAccessTokenValid(input, accessToken); + MockRemoteRefreshTokenValid(refreshToken, accessToken); + + var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); + + var credential = await provider.GetCredentialAsync(input); + + Assert.Equal(username, credential.Account); + Assert.Equal(accessToken, credential.Password); + + VerifyValidateAccessTokenRan(input, accessToken); + VerifyOAuthRefreshRan(refreshToken); + VerifyInteractiveAuthNeverRan(); + } + + [Theory] + // Cloud + [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", MOCK_REFRESH_TOKEN, MOCK_ACCESS_TOKEN)] + public async Task BitbucketHostProvider_GetCredentialAsync_PreconfiguredMode_OAuth_ValidRT_IsRespected( + string protocol, string host, string username, string refreshToken, string accessToken) + { + var input = MockInput(protocol, host, username); + + var context = new TestCommandContext(); + context.Environment.Variables.Add(BitbucketConstants.EnvironmentVariables.AuthenticationModes, "oauth"); + + // We have a stored RT so we can just use that without any prompts + MockStoredRefreshToken(context, input, refreshToken); + MockRemoteAccessTokenValid(input, accessToken); + MockRemoteRefreshTokenValid(refreshToken, accessToken); var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); @@ -275,73 +330,72 @@ public async Task BitbucketHostProvider_GetCredentialAsync_ForcedAuthMode_IsResp Assert.NotNull(credential); - if (preconfiguredAuthModes.Contains("basic")) - { - VerifyInteractiveBasicAuthFlowRan(password, input, credential); - VerifyInteractiveOAuthFlowNeverRan(input, credential); - } + VerifyInteractiveAuthNeverRan(); + VerifyOAuthRefreshRan(refreshToken); + } - if (preconfiguredAuthModes.Contains("oauth")) - { - VerifyInteractiveAuthNeverRan(password, input, credential); - VerifyInteractiveOAuthFlowRan(password, input, credential); - } + [Theory] + // DC/Server does not currently support OAuth + [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", MOCK_ACCESS_TOKEN, MOCK_ACCESS_TOKEN_ALT, MOCK_REFRESH_TOKEN)] + public async Task BitbucketHostProvider_GetCredentialAsync_AlwaysRefreshCredentials_OAuth_IsRespected( + string protocol, string host, string username, string storedToken, string newToken, string refreshToken) + { + var input = MockInput(protocol, host, username); + + var context = new TestCommandContext(); + context.Environment.Variables.Add( + BitbucketConstants.EnvironmentVariables.AlwaysRefreshCredentials, bool.TrueString); + + // User has stored access token that we shouldn't use - RT should be used to mint new AT + MockStoredAccount(context, input, storedToken); + MockStoredRefreshToken(context, input, refreshToken); + MockRemoteAccessTokenValid(input, newToken); + MockRemoteRefreshTokenValid(refreshToken, newToken); + + var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); + + var credential = await provider.GetCredentialAsync(input); + + Assert.Equal(username, credential.Account); + Assert.Equal(newToken, credential.Password); + + VerifyInteractiveAuthNeverRan(); + VerifyOAuthRefreshRan(refreshToken); } [Theory] - // cloud - [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password", "false")] - [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password", "0")] - [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password", "true")] - [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password", "1")] - [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password", null)] + // Cloud + [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "old-password", "new-password")] // DC - [InlineData("https", DC_SERVER_HOST, "jsquire", "password", "false")] - [InlineData("https", DC_SERVER_HOST, "jsquire", "password", "0")] - [InlineData("https", DC_SERVER_HOST, "jsquire", "password", "1")] - [InlineData("https", DC_SERVER_HOST, "jsquire", "password", "true")] - [InlineData("https", DC_SERVER_HOST, "jsquire", "password", null)] - public async Task BitbucketHostProvider_GetCredentialAsync_AlwaysRefreshCredentials_IsRespected(string protocol, string host, string username, string password, - string alwaysRefreshCredentials) + [InlineData("https", DC_SERVER_HOST, "jsquire", "old-password", "new-password")] + public async Task BitbucketHostProvider_GetCredentialAsync_AlwaysRefreshCredentials_Basic_IsRespected( + string protocol, string host, string username, string storedPassword, string freshPassword) { var input = MockInput(protocol, host, username); var context = new TestCommandContext(); - if (alwaysRefreshCredentials != null) - { - context.Environment.Variables.Add(BitbucketConstants.EnvironmentVariables.AlwaysRefreshCredentials, alwaysRefreshCredentials); - } + context.Environment.Variables.Add( + BitbucketConstants.EnvironmentVariables.AlwaysRefreshCredentials, bool.TrueString); - MockStoredAccount(context, input, password); - MockPromptBasic(bitbucketAuthentication, input, password); - MockRemoteAccessTokenValid(bitbucketApi, input, password, true); - MockRemoteBasicValid(bitbucketApi, input, password, twoFactor: false); + // User has stored password that we shouldn't use + MockStoredAccount(context, input, storedPassword); + MockPromptBasic(input, freshPassword); var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); var credential = await provider.GetCredentialAsync(input); - var alwaysRefreshCredentialsBool = "1".Equals(alwaysRefreshCredentials) - || "on".Equals(alwaysRefreshCredentials) - || "true".Equals(alwaysRefreshCredentials) ? true : false; + Assert.Equal(username, credential.Account); + Assert.Equal(freshPassword, credential.Password); - if (alwaysRefreshCredentialsBool) - { - VerifyBasicAuthFlowRan(password, true, input, credential); - } - else - { - VerifyBasicAuthFlowNeverRan(password, input, true, null); - } - - VerifyOAuthFlowDidNotRun(password, true, input, credential); + VerifyInteractiveAuthRan(input); } [Theory] // DC - supports Basic [InlineData("https://example.com", "basic", AuthenticationModes.Basic)] [InlineData("https://example.com", "oauth", AuthenticationModes.Basic)] - // cloud - supports Basic, OAuth + // Cloud - supports Basic, OAuth [InlineData("https://bitbucket.org", "oauth", AuthenticationModes.OAuth)] [InlineData("https://bitbucket.org", "basic", AuthenticationModes.Basic)] [InlineData("https://bitbucket.org", "NOT-A-REAL-VALUE", BitbucketConstants.DotOrgAuthenticationModes)] @@ -354,7 +408,7 @@ public void BitbucketHostProvider_GetSupportedAuthenticationModes(string uriStri { var targetUri = new Uri(uriString); - var context = new TestCommandContext { }; + var context = new TestCommandContext(); if (bitbucketAuthModes != null) { context.Environment.Variables.Add(BitbucketConstants.EnvironmentVariables.AuthenticationModes, bitbucketAuthModes); @@ -367,40 +421,6 @@ public void BitbucketHostProvider_GetSupportedAuthenticationModes(string uriStri Assert.Equal(expectedModes, actualModes); } - [Theory] - // DC - [InlineData("https", DC_SERVER_HOST, "jsquire", "password")] - [InlineData("http", DC_SERVER_HOST, "jsquire", "password")] - // cloud - [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password")] - [InlineData("http", BITBUCKET_DOT_ORG_HOST, "jsquire", "password")] - public async Task BitbucketHostProvider_GetCredentialAsync_ValidateTargetUriAsync(string protocol, string host, string username, string password) - { - var input = MockInput(protocol, host, username); - - var context = new TestCommandContext(); - - var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); - - if (protocol.ToLower().Equals("http") && host.ToLower().Equals(BITBUCKET_DOT_ORG_HOST)) - { - // only fail for http://bitbucket.org - await Assert.ThrowsAsync(async () => await provider.GetCredentialAsync(input)); - } - else - { - MockPromptBasic(bitbucketAuthentication, input, password); - MockRemoteBasicValid(bitbucketApi, input, password, twoFactor: true); - MockRemoteValidRefreshToken(); - bitbucketAuthentication.Setup(m => m.ShowOAuthRequiredPromptAsync()).ReturnsAsync(true); - bitbucketAuthentication.Setup(m => m.CreateOAuthCredentialsAsync(It.IsAny())).ReturnsAsync(new OAuth2TokenResult(MOCK_ACCESS_TOKEN, "access_token")); - var userInfo = new UserInfo() { IsTwoFactorAuthenticationEnabled = false }; - bitbucketApi.Setup(x => x.GetUserInformationAsync(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(new RestApiResult(System.Net.HttpStatusCode.OK, userInfo)); - - var credential = await provider.GetCredentialAsync(input); - } - } - [Theory] [InlineData("https", DC_SERVER_HOST, "jsquire")] public async Task BitbucketHostProvider_StoreCredentialAsync(string protocol, string host, string username) @@ -440,6 +460,7 @@ public async Task BitbucketHostProvider_EraseCredentialAsync(string protocol, st #endregion #region Test helpers + private static InputArguments MockInput(string protocol, string host, string username) { return new InputArguments(new Dictionary @@ -450,156 +471,102 @@ private static InputArguments MockInput(string protocol, string host, string use }); } - private void VerifyBasicAuthFlowRan(string password, bool expected, InputArguments input, ICredential credential) + private void VerifyOAuthFlowRan(InputArguments input, string token) { - Assert.Equal(expected, credential != null); - var remoteUri = input.GetRemoteUri(); - bitbucketAuthentication.Verify(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny()), Times.Once); - } - - private void VerifyInteractiveBasicAuthFlowRan(string password, InputArguments input, ICredential credential) - { - var remoteUri = input.GetRemoteUri(); + // use refresh token to get new access token and refresh token + bitbucketAuthentication.Verify(m => m.CreateOAuthCredentialsAsync(remoteUri), Times.Once); - // verify users was prompted for username/password credentials - bitbucketAuthentication.Verify(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny()), Times.Once); + // Check access token works/resolve username + bitbucketApi.Verify(m => m.GetUserInformationAsync(null, token, true), Times.Once); } - private void VerifyBasicAuthFlowNeverRan(string password, InputArguments input, bool storedAccount, - string preconfiguredAuthModes) - { - var remoteUri = input.GetRemoteUri(); - - if (!storedAccount && - (preconfiguredAuthModes == null || preconfiguredAuthModes.Contains("basic")) ) - { - // never prompt the user for basic credentials - bitbucketAuthentication.Verify(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny()), Times.Once); - } - else - { - // never prompt the user for basic credentials - bitbucketAuthentication.Verify(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny()), Times.Never); - } - } - - private void VerifyInteractiveAuthNeverRan(string password, InputArguments input, ICredential credential) - { - var remoteUri = input.GetRemoteUri(); - - bitbucketAuthentication.Verify(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny()), Times.Never); - } - - private void VerifyOAuthFlowRan(string password, bool storedAccount, bool expected, InputArguments input, ICredential credential, - string preconfiguredAuthModes) + private void VerifyValidateBasicAuthCredentialsNeverRan() { - Assert.Equal(expected, credential != null); - - var remoteUri = input.GetRemoteUri(); - - if (storedAccount) - { - // use refresh token to get new access token and refresh token - bitbucketAuthentication.Verify(m => m.RefreshOAuthCredentialsAsync(MOCK_REFRESH_TOKEN), Times.Once); - - // check access token works - bitbucketApi.Verify(m => m.GetUserInformationAsync(null, MOCK_ACCESS_TOKEN, true), Times.Once); - } - else - { - if (preconfiguredAuthModes == null || preconfiguredAuthModes.Contains("basic")) - { - // prompt user for basic auth, if basic auth is not excluded - bitbucketAuthentication.Verify(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny()), Times.Once); - } - } + // Never check username/password works + bitbucketApi.Verify(m => m.GetUserInformationAsync(It.IsAny(), It.IsAny(), false), Times.Never); } - private void VerifyInteractiveOAuthFlowRan(string password, InputArguments input, ICredential credential) + private void VerifyValidateBasicAuthCredentialsRan(InputArguments input, string password) { - var remoteUri = input.GetRemoteUri(); - - // Basic Auth 403-ed so push user through OAuth flow - bitbucketAuthentication.Verify(m => m.ShowOAuthRequiredPromptAsync(), Times.Once); - + // Check username/password works + bitbucketApi.Verify(m => m.GetUserInformationAsync(input.UserName, password, false), Times.Once); } - private void VerifyOAuthFlowDidNotRun(string password, bool expected, InputArguments input, ICredential credential) + private void VerifyValidateAccessTokenRan(InputArguments input, string token) { - Assert.Equal(expected, credential != null); - - var remoteUri = input.GetRemoteUri(); - - // never prompt user through OAuth flow - bitbucketAuthentication.Verify(m => m.ShowOAuthRequiredPromptAsync(), Times.Never); - - // Never try to refresh Access Token - bitbucketAuthentication.Verify(m => m.RefreshOAuthCredentialsAsync(It.IsAny()), Times.Never); - - // never check access token works - bitbucketApi.Verify(m => m.GetUserInformationAsync(null, MOCK_ACCESS_TOKEN, true), Times.Never); + // Check tokens works + bitbucketApi.Verify(m => m.GetUserInformationAsync(null, token, true), Times.Once); } - private void VerifyInteractiveOAuthFlowNeverRan(InputArguments input, ICredential credential) + private void VerifyInteractiveAuthRan(InputArguments input) { var remoteUri = input.GetRemoteUri(); - // never prompt user through OAuth flow - bitbucketAuthentication.Verify(m => m.ShowOAuthRequiredPromptAsync(), Times.Never); - - // Never try to refresh Access Token - bitbucketAuthentication.Verify(m => m.RefreshOAuthCredentialsAsync(It.IsAny()), Times.Never); - - // never check access token works - bitbucketApi.Verify(m => m.GetUserInformationAsync(null, MOCK_ACCESS_TOKEN, true), Times.Never); + bitbucketAuthentication.Verify(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny()), Times.Once); } - private void VerifyValidateBasicAuthCredentialsNeverRan() + private void VerifyInteractiveAuthNeverRan() { - // never check username/password works - bitbucketApi.Verify(m => m.GetUserInformationAsync(It.IsAny(), It.IsAny(), false), Times.Never); + bitbucketAuthentication.Verify(m => m.GetCredentialsAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } - private void VerifyValidateBasicAuthCredentialsRan() + private void VerifyOAuthRefreshRan(string refreshToken) { - // check username/password works - bitbucketApi.Verify(m => m.GetUserInformationAsync(It.IsAny(), It.IsAny(), false), Times.Once); + // Check refresh was called + bitbucketAuthentication.Verify(m => m.RefreshOAuthCredentialsAsync(refreshToken), Times.Once); } - private void VerifyValidateOAuthCredentialsRan() + private void MockRemoteRefreshTokenValid(string refreshToken, string accessToken) { - // check username/password works - bitbucketApi.Verify(m => m.GetUserInformationAsync(null, It.IsAny(), true), Times.Once); + bitbucketAuthentication.Setup(m => m.RefreshOAuthCredentialsAsync(refreshToken)).ReturnsAsync(new OAuth2TokenResult(accessToken, "access_token")); } - private void MockRemoteValidRefreshToken() + private void MockPromptBasic(InputArguments input, string password) { - bitbucketAuthentication.Setup(m => m.RefreshOAuthCredentialsAsync(MOCK_REFRESH_TOKEN)).ReturnsAsync(new OAuth2TokenResult(MOCK_ACCESS_TOKEN, "access_token")); + var remoteUri = input.GetRemoteUri(); + bitbucketAuthentication.Setup(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny())) + .ReturnsAsync(new CredentialsPromptResult(AuthenticationModes.Basic, new TestCredential(input.Host, input.UserName, password))); } - private static void MockPromptBasic(Mock bitbucketAuthentication, InputArguments input, string password) + private void MockPromptOAuth(InputArguments input) { var remoteUri = input.GetRemoteUri(); bitbucketAuthentication.Setup(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny())) - .ReturnsAsync(new CredentialsPromptResult(AuthenticationModes.Basic, new TestCredential(input.Host, input.UserName, password))); + .ReturnsAsync(new CredentialsPromptResult(AuthenticationModes.OAuth)); } - private static void MockRemoteBasicValid(Mock bitbucketApi, InputArguments input, string password, bool twoFactor) + private void MockRemoteBasicValid(InputArguments input, string password, bool twoFactor = true) { - var userInfo = new UserInfo() { IsTwoFactorAuthenticationEnabled = twoFactor }; + var userInfo = new UserInfo + { + UserName = input.UserName, + IsTwoFactorAuthenticationEnabled = twoFactor + }; + // Basic bitbucketApi.Setup(x => x.GetUserInformationAsync(input.UserName, password, false)) .ReturnsAsync(new RestApiResult(System.Net.HttpStatusCode.OK, userInfo)); + } + private void MockRemoteAccessTokenExpired(InputArguments input, string token) + { + // OAuth + bitbucketApi.Setup(x => x.GetUserInformationAsync(null, token, true)) + .ReturnsAsync(new RestApiResult(System.Net.HttpStatusCode.Unauthorized)); } - private static void MockRemoteAccessTokenValid(Mock bitbucketApi, InputArguments input, string password, bool twoFAEnabled) + private void MockRemoteAccessTokenValid(InputArguments input, string token, bool twoFactor = true) { - var userInfo = new UserInfo() { IsTwoFactorAuthenticationEnabled = twoFAEnabled }; + var userInfo = new UserInfo + { + UserName = input.UserName, + IsTwoFactorAuthenticationEnabled = twoFactor + }; + // OAuth - bitbucketApi.Setup(x => x.GetUserInformationAsync(null, password, true)) + bitbucketApi.Setup(x => x.GetUserInformationAsync(null, token, true)) .ReturnsAsync(new RestApiResult(System.Net.HttpStatusCode.OK, userInfo)); } @@ -610,6 +577,30 @@ private static void MockStoredAccount(TestCommandContext context, InputArguments context.CredentialStore.Add(remoteUrl, new TestCredential(input.Host, input.UserName, password)); } + private static void MockStoredRefreshToken(TestCommandContext context, InputArguments input, string token) + { + var remoteUri = input.GetRemoteUri(); + var refreshService = BitbucketHostProvider.GetRefreshTokenServiceName(remoteUri); + context.CredentialStore.Add(refreshService, new TestCredential(refreshService, input.UserName, token)); + } + + private void MockRemoteOAuthTokenCreate(InputArguments input, string accessToken, string refreshToken) + { + var remoteUri = input.GetRemoteUri(); + bitbucketAuthentication.Setup(x => x.CreateOAuthCredentialsAsync(remoteUri)) + .ReturnsAsync(new OAuth2TokenResult(accessToken, "access_token") { RefreshToken = refreshToken }); + } + + private void VerifyOAuthRefreshTokenStored(TestCommandContext context, InputArguments input, string refreshToken) + { + var remoteUri = input.GetRemoteUri(); + string refreshService = BitbucketHostProvider.GetRefreshTokenServiceName(remoteUri); + bool result = context.CredentialStore.TryGet(refreshService, input.UserName, out var credential); + + Assert.True(result); + Assert.Equal(refreshToken, credential.Password); + } + #endregion } } diff --git a/src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs b/src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs index 9d3ff0d54..15d9b10fa 100644 --- a/src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs +++ b/src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs @@ -23,7 +23,6 @@ public enum AuthenticationModes public interface IBitbucketAuthentication : IDisposable { Task GetCredentialsAsync(Uri targetUri, string userName, AuthenticationModes modes); - Task ShowOAuthRequiredPromptAsync(); Task CreateOAuthCredentialsAsync(Uri targetUri); Task RefreshOAuthCredentialsAsync(string refreshToken); } @@ -62,8 +61,6 @@ public class BitbucketAuthentication : AuthenticationBase, IBitbucketAuthenticat public BitbucketAuthentication(ICommandContext context) : base(context) { } - #region IBitbucketAuthentication - public async Task GetCredentialsAsync(Uri targetUri, string userName, AuthenticationModes modes) { ThrowIfUserInteractionDisabled(); @@ -104,6 +101,11 @@ public async Task GetCredentialsAsync(Uri targetUri, st cmdArgs.AppendFormat(" --username {0}", QuoteCmdArg(userName)); } + if ((modes & AuthenticationModes.Basic) != 0) + { + cmdArgs.Append(" --show-basic"); + } + if ((modes & AuthenticationModes.OAuth) != 0) { cmdArgs.Append(" --show-oauth"); @@ -187,35 +189,6 @@ public async Task GetCredentialsAsync(Uri targetUri, st } } - public async Task ShowOAuthRequiredPromptAsync() - { - ThrowIfUserInteractionDisabled(); - - // Shell out to the UI helper and show the Bitbucket prompt - if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession && - TryFindHelperExecutablePath(out string helperPath)) - { - IDictionary output = await InvokeHelperAsync(helperPath, "oauth"); - - if (output.TryGetValue("continue", out string continueStr) && continueStr.IsTruthy()) - { - return true; - } - - return false; - } - else - { - ThrowIfTerminalPromptsDisabled(); - - Context.Terminal.WriteLine($"Your account has two-factor authentication enabled.{Environment.NewLine}" + - $"To continue you must complete authentication in your web browser.{Environment.NewLine}"); - - var _ = Context.Terminal.Prompt("Press enter to continue..."); - return true; - } - } - public async Task CreateOAuthCredentialsAsync(Uri targetUri) { ThrowIfUserInteractionDisabled(); @@ -241,10 +214,6 @@ public async Task RefreshOAuthCredentialsAsync(string refresh return await oauthClient.GetTokenByRefreshTokenAsync(refreshToken, CancellationToken.None); } - #endregion - - #region Private Methods - protected internal virtual bool TryFindHelperExecutablePath(out string path) { return TryFindHelperExecutablePath( @@ -257,15 +226,9 @@ protected internal virtual bool TryFindHelperExecutablePath(out string path) private HttpClient _httpClient; private HttpClient HttpClient => _httpClient ??= Context.HttpClientFactory.CreateClient(); - #endregion - - #region IDisposable - public void Dispose() { _httpClient?.Dispose(); } - - #endregion } } diff --git a/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs b/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs index 724498a25..0db55bd0d 100644 --- a/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs +++ b/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs @@ -135,64 +135,39 @@ private async Task GetRefreshedCredentials(Uri remoteUri, string us if (refreshToken is null) { - _context.Trace.WriteLine($"No stored refresh token found"); + _context.Trace.WriteLine("No stored refresh token found"); // There is no refresh token either because this is a non-2FA enabled account (where OAuth is not // required), or because we previously erased the RT. - bool skipOAuthPrompt = false; + _context.Trace.WriteLine("Prompt for credentials..."); - if (SupportsBasicAuth(authModes)) + var result = await _bitbucketAuth.GetCredentialsAsync(remoteUri, userName, authModes); + if (result is null || result.AuthenticationMode == AuthenticationModes.None) { - _context.Trace.WriteLine("Prompt for credentials..."); - - // We don't have any credentials to use at all! Start with the assumption of no 2FA requirement - // and capture username and password via an interactive prompt. - var result = await _bitbucketAuth.GetCredentialsAsync(remoteUri, userName, authModes); - if (result is null || result.AuthenticationMode == AuthenticationModes.None) - { - _context.Trace.WriteLine("User cancelled credential prompt"); - throw new Exception("User cancelled credential prompt."); - } - - switch (result.AuthenticationMode) - { - case AuthenticationModes.Basic: - // Return the valid credential - return result.Credential; - - case AuthenticationModes.OAuth: - // If the user wants to use OAuth fall through to interactive auth - // and avoid the OAuth "continue" confirmation prompt - skipOAuthPrompt = true; - break; - - default: - throw new ArgumentOutOfRangeException( - $"Unexpected {nameof(AuthenticationModes)} returned from prompt"); - } - - // Fall through to the start of the interactive OAuth authentication flow + _context.Trace.WriteLine("User cancelled credential prompt"); + throw new Exception("User cancelled credential prompt."); } - if (SupportsOAuth(authModes) && !skipOAuthPrompt) + switch (result.AuthenticationMode) { - _context.Trace.WriteLine("Two-factor authentication is required - prompting for auth via OAuth..."); - _context.Trace.WriteLine("Prompt for OAuth..."); - - // Show the 2FA/OAuth authentication required prompt - bool @continue = await _bitbucketAuth.ShowOAuthRequiredPromptAsync(); - if (!@continue) - { - _context.Trace.WriteLine("User cancelled OAuth prompt"); - throw new Exception("User cancelled OAuth authentication."); - } - - // Fall through to the start of the interactive OAuth authentication flow + case AuthenticationModes.Basic: + // Return the valid credential + return result.Credential; + + case AuthenticationModes.OAuth: + // If the user wants to use OAuth fall through to interactive auth + break; + + default: + throw new ArgumentOutOfRangeException( + $"Unexpected {nameof(AuthenticationModes)} returned from prompt"); } + + // Fall through to the start of the interactive OAuth authentication flow } else { - _context.Trace.WriteLineSecrets($"Found stored refresh token: {{0}}", new object[] { refreshToken }); + _context.Trace.WriteLineSecrets("Found stored refresh token: {0}", new object[] { refreshToken }); try { @@ -420,7 +395,7 @@ private static string GetServiceName(Uri remoteUri) return remoteUri.WithoutUserInfo().AbsoluteUri.TrimEnd('/'); } - private static string GetRefreshTokenServiceName(Uri remoteUri) + internal /* for testing */ static string GetRefreshTokenServiceName(Uri remoteUri) { Uri baseUri = remoteUri.WithoutUserInfo(); From d4fcfb7deb76dee17e946e206457111ef426a581 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Fri, 17 Jun 2022 18:22:11 +0100 Subject: [PATCH 6/8] bitbucket: update shared UI helper code for new UI model Update shared UI helper code for Bitbucket to remove the separate OAuth prompt/command, and allow basic authentication (username/password) options to be hidden based on a command-line option. --- .../BitbucketAuthenticationTest.cs | 6 +- .../BitbucketHostProviderTest.cs | 2 +- .../Commands/CredentialsCommand.cs | 49 ++++++++----- .../Commands/OAuthCommand.cs | 46 ------------ .../ViewModels/CredentialsViewModel.cs | 23 ++++-- .../ViewModels/OAuthViewModel.cs | 72 ------------------- .../BitbucketAuthentication.cs | 2 +- 7 files changed, 51 insertions(+), 149 deletions(-) delete mode 100644 src/shared/Atlassian.Bitbucket.UI/Commands/OAuthCommand.cs delete mode 100644 src/shared/Atlassian.Bitbucket.UI/ViewModels/OAuthViewModel.cs diff --git a/src/shared/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs b/src/shared/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs index 9fac31e93..aace44ad7 100644 --- a/src/shared/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs +++ b/src/shared/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs @@ -122,7 +122,7 @@ public async Task BitbucketAuthentication_GetCredentialsAsync_AllModes_NoUser_BB ["password"] = expectedPassword }; - string expectedArgs = $"userpass --show-oauth"; + string expectedArgs = $"prompt --show-basic --show-oauth"; var context = new TestCommandContext(); context.SessionManager.IsDesktopSession = true; // Enable OAuth and UI helper selection @@ -158,7 +158,7 @@ public async Task BitbucketAuthentication_GetCredentialsAsync_BasicOnly_User_BBC ["password"] = expectedPassword }; - string expectedArgs = $"userpass --username {expectedUserName}"; + string expectedArgs = $"prompt --username {expectedUserName} --show-basic"; var context = new TestCommandContext(); context.SessionManager.IsDesktopSession = true; // Enable UI helper selection @@ -194,7 +194,7 @@ public async Task BitbucketAuthentication_GetCredentialsAsync_AllModes_NoUser_BB ["password"] = expectedPassword }; - string expectedArgs = $"userpass --url {targetUri} --show-oauth"; + string expectedArgs = $"prompt --url {targetUri} --show-basic --show-oauth"; var context = new TestCommandContext(); context.SessionManager.IsDesktopSession = true; // Enable OAuth and UI helper selection diff --git a/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs b/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs index f658c4029..cf8216afc 100644 --- a/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs +++ b/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs @@ -475,7 +475,7 @@ private void VerifyOAuthFlowRan(InputArguments input, string token) { var remoteUri = input.GetRemoteUri(); - // use refresh token to get new access token and refresh token + // Get new access token and refresh token bitbucketAuthentication.Verify(m => m.CreateOAuthCredentialsAsync(remoteUri), Times.Once); // Check access token works/resolve username diff --git a/src/shared/Atlassian.Bitbucket.UI/Commands/CredentialsCommand.cs b/src/shared/Atlassian.Bitbucket.UI/Commands/CredentialsCommand.cs index 009625335..a17bdf5f3 100644 --- a/src/shared/Atlassian.Bitbucket.UI/Commands/CredentialsCommand.cs +++ b/src/shared/Atlassian.Bitbucket.UI/Commands/CredentialsCommand.cs @@ -13,7 +13,7 @@ namespace Atlassian.Bitbucket.UI.Commands public abstract class CredentialsCommand : HelperCommand { protected CredentialsCommand(ICommandContext context) - : base(context, "userpass", "Show authentication prompt.") + : base(context, "prompt", "Show authentication prompt.") { AddOption( new Option("--url", "Bitbucket Server or Data Center URL") @@ -27,40 +27,51 @@ protected CredentialsCommand(ICommandContext context) new Option("--show-oauth", "Show OAuth option.") ); - Handler = CommandHandler.Create(ExecuteAsync); + AddOption( + new Option("--show-basic", "Show username/password option.") + ); + + Handler = CommandHandler.Create(ExecuteAsync); } - private async Task ExecuteAsync(Uri url, string userName, bool showOAuth) + private async Task ExecuteAsync(Uri url, string userName, bool showOAuth, bool showBasic) { var viewModel = new CredentialsViewModel(Context.Environment) { Url = url, UserName = userName, - ShowOAuth = showOAuth + ShowOAuth = showOAuth, + ShowBasic = showBasic }; await ShowAsync(viewModel, CancellationToken.None); - if (!viewModel.WindowResult) + if (!viewModel.WindowResult || viewModel.SelectedMode == AuthenticationModes.None) { throw new Exception("User cancelled dialog."); } - if (viewModel.UseOAuth) + switch (viewModel.SelectedMode) { - WriteResult(new Dictionary - { - ["mode"] = "oauth" - }); - } - else - { - WriteResult(new Dictionary - { - ["mode"] = "basic", - ["username"] = viewModel.UserName, - ["password"] = viewModel.Password, - }); + case AuthenticationModes.OAuth: + WriteResult(new Dictionary + { + ["mode"] = "oauth" + }); + break; + + case AuthenticationModes.Basic: + WriteResult(new Dictionary + { + ["mode"] = "basic", + ["username"] = viewModel.UserName, + ["password"] = viewModel.Password, + }); + break; + + default: + throw new ArgumentOutOfRangeException(nameof(AuthenticationModes), + "Unknown authentication mode", viewModel.SelectedMode.ToString()); } return 0; diff --git a/src/shared/Atlassian.Bitbucket.UI/Commands/OAuthCommand.cs b/src/shared/Atlassian.Bitbucket.UI/Commands/OAuthCommand.cs deleted file mode 100644 index 6203e1c81..000000000 --- a/src/shared/Atlassian.Bitbucket.UI/Commands/OAuthCommand.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Collections.Generic; -using System.CommandLine; -using System.CommandLine.Invocation; -using System.Threading; -using System.Threading.Tasks; -using Atlassian.Bitbucket.UI.ViewModels; -using GitCredentialManager; -using GitCredentialManager.UI; - -namespace Atlassian.Bitbucket.UI.Commands -{ - public abstract class OAuthCommand : HelperCommand - { - protected OAuthCommand(ICommandContext context) - : base(context, "oauth", "Show OAuth required prompt.") - { - AddOption( - new Option("--url", "Bitbucket Server or Data Center URL") - ); - - Handler = CommandHandler.Create(ExecuteAsync); - } - - private async Task ExecuteAsync() - { - var viewModel = new OAuthViewModel(Context.Environment); - - await ShowAsync(viewModel, CancellationToken.None); - - if (!viewModel.WindowResult) - { - throw new Exception("User cancelled dialog."); - } - - WriteResult(new Dictionary - { - ["continue"] = "true" - }); - - return 0; - } - - protected abstract Task ShowAsync(OAuthViewModel viewModel, CancellationToken ct); - } -} diff --git a/src/shared/Atlassian.Bitbucket.UI/ViewModels/CredentialsViewModel.cs b/src/shared/Atlassian.Bitbucket.UI/ViewModels/CredentialsViewModel.cs index 38475e9be..d4e8e51c9 100644 --- a/src/shared/Atlassian.Bitbucket.UI/ViewModels/CredentialsViewModel.cs +++ b/src/shared/Atlassian.Bitbucket.UI/ViewModels/CredentialsViewModel.cs @@ -15,6 +15,7 @@ public class CredentialsViewModel : WindowViewModel private string _userName; private string _password; private bool _showOAuth; + private bool _showBasic; public CredentialsViewModel() { @@ -28,7 +29,7 @@ public CredentialsViewModel(IEnvironment environment) _environment = environment; Title = "Connect to Bitbucket"; - LoginCommand = new RelayCommand(Accept, CanLogin); + LoginCommand = new RelayCommand(AcceptBasic, CanLogin); CancelCommand = new RelayCommand(Cancel); OAuthCommand = new RelayCommand(AcceptOAuth, CanAcceptOAuth); ForgotPasswordCommand = new RelayCommand(ForgotPassword); @@ -53,9 +54,15 @@ private bool CanLogin() return !string.IsNullOrWhiteSpace(UserName) && !string.IsNullOrWhiteSpace(Password); } + private void AcceptBasic() + { + SelectedMode = AuthenticationModes.Basic; + Accept(); + } + private void AcceptOAuth() { - UseOAuth = true; + SelectedMode = AuthenticationModes.OAuth; Accept(); } @@ -101,7 +108,7 @@ public string Password } /// - /// Show the direct-to-OAuth button. + /// Show the OAuth option. /// public bool ShowOAuth { @@ -110,14 +117,16 @@ public bool ShowOAuth } /// - /// User indicated a preference to use OAuth authentication over username/password. + /// Show the basic authentication options. /// - public bool UseOAuth + public bool ShowBasic { - get; - private set; + get => _showBasic; + set => SetAndRaisePropertyChanged(ref _showBasic, value); } + public AuthenticationModes SelectedMode { get; private set; } + /// /// Start the process to validate the username/password /// diff --git a/src/shared/Atlassian.Bitbucket.UI/ViewModels/OAuthViewModel.cs b/src/shared/Atlassian.Bitbucket.UI/ViewModels/OAuthViewModel.cs deleted file mode 100644 index a9376a3a2..000000000 --- a/src/shared/Atlassian.Bitbucket.UI/ViewModels/OAuthViewModel.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Windows.Input; -using GitCredentialManager; -using GitCredentialManager.UI; -using GitCredentialManager.UI.ViewModels; - -namespace Atlassian.Bitbucket.UI.ViewModels -{ - public class OAuthViewModel : WindowViewModel - { - private readonly IEnvironment _environment; - - public OAuthViewModel() - { - // Constructor the XAML designer - } - - public OAuthViewModel(IEnvironment environment) - { - EnsureArgument.NotNull(environment, nameof(environment)); - - _environment = environment; - - Title = "OAuth authentication required"; - OkCommand = new RelayCommand(Accept); - CancelCommand = new RelayCommand(Cancel); - LearnMoreCommand = new RelayCommand(LearnMore); - ForgotPasswordCommand = new RelayCommand(ForgotPassword); - SignUpCommand = new RelayCommand(SignUp); - } - - private void LearnMore() - { - // 2FA is not supported on Server/DC so this prompt will never be seen outside of Bitbucket Cloud - BrowserUtils.OpenDefaultBrowser(_environment, BitbucketConstants.HelpUrls.TwoFactor); - } - - private void ForgotPassword() - { - BrowserUtils.OpenDefaultBrowser(_environment, BitbucketConstants.HelpUrls.PasswordReset); - } - - private void SignUp() - { - BrowserUtils.OpenDefaultBrowser(_environment, BitbucketConstants.HelpUrls.SignUp); - } - - /// - /// Provides a link to Bitbucket OAuth documentation - /// - public ICommand LearnMoreCommand { get; } - - /// - /// Hyperlink to the Bitbucket forgotten password process. - /// - public ICommand ForgotPasswordCommand { get; } - - /// - /// Hyperlink to the Bitbucket sign up process. - /// - public ICommand SignUpCommand { get; } - - /// - /// Run the OAuth dance. - /// - public ICommand OkCommand { get; } - - /// - /// Cancel the authentication attempt. - /// - public ICommand CancelCommand { get; } - } -} diff --git a/src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs b/src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs index 15d9b10fa..14e7b3fb1 100644 --- a/src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs +++ b/src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs @@ -90,7 +90,7 @@ public async Task GetCredentialsAsync(Uri targetUri, st if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession && TryFindHelperExecutablePath(out string helperPath)) { - var cmdArgs = new StringBuilder("userpass"); + var cmdArgs = new StringBuilder("prompt"); if (!BitbucketHostProvider.IsBitbucketOrg(targetUri)) { cmdArgs.AppendFormat(" --url {0}", QuoteCmdArg(targetUri.ToString())); From 6ceab40eec2d87513000ed4e563a6069a7b28c88 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 16 Jun 2022 19:07:12 +0100 Subject: [PATCH 7/8] bb-avnui: update Avalonia-based Bitbucket UI Update the Avalonia-based UI for Bitbucket authentication to use a tabbed interface, showing each authentication mode in a separate tab. The new "credentials" prompt can offer and serve all authentication modes in a simpler way that pushes users who can to authenticate using a more secure method (where we don't capture the user/pass). --- .../Commands/OAuthCommandImpl.cs | 19 ----- .../Controls/TesterWindow.axaml | 24 +++++- .../Controls/TesterWindow.axaml.cs | 19 ++--- .../Program.cs | 1 - .../Views/CredentialsView.axaml | 79 +++++++++++++------ .../Views/CredentialsView.axaml.cs | 21 ++++- .../Views/OAuthView.axaml | 42 ---------- .../Views/OAuthView.axaml.cs | 29 ------- .../Converters/BoolConvertersEx.cs | 3 + 9 files changed, 104 insertions(+), 133 deletions(-) delete mode 100644 src/shared/Atlassian.Bitbucket.UI.Avalonia/Commands/OAuthCommandImpl.cs delete mode 100644 src/shared/Atlassian.Bitbucket.UI.Avalonia/Views/OAuthView.axaml delete mode 100644 src/shared/Atlassian.Bitbucket.UI.Avalonia/Views/OAuthView.axaml.cs diff --git a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Commands/OAuthCommandImpl.cs b/src/shared/Atlassian.Bitbucket.UI.Avalonia/Commands/OAuthCommandImpl.cs deleted file mode 100644 index f3ee88591..000000000 --- a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Commands/OAuthCommandImpl.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using Atlassian.Bitbucket.UI.ViewModels; -using Atlassian.Bitbucket.UI.Views; -using GitCredentialManager; -using GitCredentialManager.UI; - -namespace Atlassian.Bitbucket.UI.Commands -{ - public class OAuthCommandImpl : OAuthCommand - { - public OAuthCommandImpl(CommandContext context) : base(context) { } - - protected override Task ShowAsync(OAuthViewModel viewModel, CancellationToken ct) - { - return AvaloniaUi.ShowViewAsync(viewModel, GetParentHandle(), ct); - } - } -} diff --git a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Controls/TesterWindow.axaml b/src/shared/Atlassian.Bitbucket.UI.Avalonia/Controls/TesterWindow.axaml index 86b010696..fca8daf48 100644 --- a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Controls/TesterWindow.axaml +++ b/src/shared/Atlassian.Bitbucket.UI.Avalonia/Controls/TesterWindow.axaml @@ -6,8 +6,24 @@ x:Class="Atlassian.Bitbucket.UI.Controls.TesterWindow" Title="Bitbucket Authentication Dialog Tester" Height="240" Width="420" CanResize="False"> - -