diff --git a/src/shared/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs b/src/shared/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs index e65674825..e8da8d79c 100644 --- a/src/shared/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs +++ b/src/shared/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs @@ -239,17 +239,14 @@ public async Task AzureReposProvider_GetCredentialAsync_JwtMode_CachedAuthority_ [Fact] public async Task AzureReposProvider_GetCredentialAsync_JwtMode_CachedAuthority_DevAzureUrlOrgName_ReturnsCredential() { - var input = new InputArguments(new Dictionary { ["protocol"] = "https", ["host"] = "dev.azure.com", - ["path"] = "org/project/_git/repo", ["username"] = "org" }); var expectedOrgUri = new Uri("https://dev.azure.com/org"); - var remoteUri = new Uri("https://dev.azure.com/org/project/_git/repo"); var authorityUrl = "https://login.microsoftonline.com/common"; var expectedClientId = AzureDevOpsConstants.AadClientId; var expectedRedirectUri = AzureDevOpsConstants.AadRedirectUri; @@ -385,7 +382,6 @@ public async Task AzureReposProvider_GetCredentialAsync_JwtMode_CachedAuthority_ [Fact] public async Task AzureReposProvider_GetCredentialAsync_JwtMode_NoCachedAuthority_NoUser_ReturnsCredential() { - var input = new InputArguments(new Dictionary { ["protocol"] = "https", @@ -433,9 +429,52 @@ public async Task AzureReposProvider_GetCredentialAsync_JwtMode_NoCachedAuthorit } [Fact] - public async Task AzureReposProvider_GetCredentialAsync_PatMode_NoExistingPat_GeneratesCredential() + public async Task AzureReposProvider_GetCredentialAsync_PatMode_OrgInUserName_NoExistingPat_GeneratesCredential() { + var input = new InputArguments(new Dictionary + { + ["protocol"] = "https", + ["host"] = "dev.azure.com", + ["username"] = "org" + }); + + var expectedOrgUri = new Uri("https://dev.azure.com/org"); + var authorityUrl = "https://login.microsoftonline.com/common"; + var expectedClientId = AzureDevOpsConstants.AadClientId; + var expectedRedirectUri = AzureDevOpsConstants.AadRedirectUri; + var expectedScopes = AzureDevOpsConstants.AzureDevOpsDefaultScopes; + var accessToken = "ACCESS-TOKEN"; + var personalAccessToken = "PERSONAL-ACCESS-TOKEN"; + var account = "john.doe"; + var authResult = CreateAuthResult(account, accessToken); + + var context = new TestCommandContext(); + + var azDevOpsMock = new Mock(MockBehavior.Strict); + azDevOpsMock.Setup(x => x.GetAuthorityAsync(expectedOrgUri)).ReturnsAsync(authorityUrl); + azDevOpsMock.Setup(x => x.CreatePersonalAccessTokenAsync(expectedOrgUri, accessToken, It.IsAny>())) + .ReturnsAsync(personalAccessToken); + + var msAuthMock = new Mock(MockBehavior.Strict); + msAuthMock.Setup(x => x.GetTokenForUserAsync(authorityUrl, expectedClientId, expectedRedirectUri, expectedScopes, null, true)) + .ReturnsAsync(authResult); + + var authorityCacheMock = new Mock(MockBehavior.Strict); + + var userMgrMock = new Mock(MockBehavior.Strict); + var provider = new AzureReposHostProvider(context, azDevOpsMock.Object, msAuthMock.Object, authorityCacheMock.Object, userMgrMock.Object); + + ICredential credential = await provider.GetCredentialAsync(input); + + Assert.NotNull(credential); + Assert.Equal(account, credential.Account); + Assert.Equal(personalAccessToken, credential.Password); + } + + [Fact] + public async Task AzureReposProvider_GetCredentialAsync_PatMode_NoExistingPat_GeneratesCredential() + { var input = new InputArguments(new Dictionary { ["protocol"] = "https", diff --git a/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs b/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs index 941b2bd53..55b1449d7 100644 --- a/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs +++ b/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs @@ -92,8 +92,8 @@ public async Task GetCredentialAsync(InputArguments input) if (UsePersonalAccessTokens()) { - Uri remoteUri = input.GetRemoteUri(); - string service = GetServiceName(remoteUri); + Uri remoteWithUserUri = input.GetRemoteUri(includeUser: true); + string service = GetServiceName(remoteWithUserUri); string account = GetAccountNameForCredentialQuery(input); _context.Trace.WriteLine($"Looking for existing credential in store with service={service} account={account}..."); @@ -219,8 +219,8 @@ private async Task GeneratePersonalAccessTokenAsync(InputArguments "Unencrypted HTTP is not supported for Azure Repos. Ensure the repository remote URL is using HTTPS."); } - Uri remoteUri = input.GetRemoteUri(); - Uri orgUri = UriHelpers.CreateOrganizationUri(remoteUri, out _); + Uri remoteUserUri = input.GetRemoteUri(includeUser: true); + Uri orgUri = UriHelpers.CreateOrganizationUri(remoteUserUri, out _); // Determine the MS authentication authority for this organization _context.Trace.WriteLine("Determining Microsoft Authentication Authority..."); @@ -257,17 +257,17 @@ private async Task GeneratePersonalAccessTokenAsync(InputArguments private async Task GetAzureAccessTokenAsync(InputArguments input) { - Uri remoteUri = input.GetRemoteUri(); + Uri remoteWithUserUri = input.GetRemoteUri(includeUser: true); string userName = input.UserName; // We should not allow unencrypted communication and should inform the user - if (StringComparer.OrdinalIgnoreCase.Equals(remoteUri.Scheme, "http")) + if (StringComparer.OrdinalIgnoreCase.Equals(remoteWithUserUri.Scheme, "http")) { throw new Trace2Exception(_context.Trace2, "Unencrypted HTTP is not supported for Azure Repos. Ensure the repository remote URL is using HTTPS."); } - Uri orgUri = UriHelpers.CreateOrganizationUri(remoteUri, out string orgName); + Uri orgUri = UriHelpers.CreateOrganizationUri(remoteWithUserUri, out string orgName); _context.Trace.WriteLine($"Determining Microsoft Authentication authority for Azure DevOps organization '{orgName}'..."); if (TryGetAuthorityFromHeaders(input.WwwAuth, out string authAuthority)) @@ -306,8 +306,8 @@ private async Task GetAzureAccessTokenAsync(Inpu // var icmp = StringComparer.OrdinalIgnoreCase; if (!string.IsNullOrWhiteSpace(userName) && - (UriHelpers.IsVisualStudioComHost(remoteUri.Host) || - (UriHelpers.IsAzureDevOpsHost(remoteUri.Host) && !icmp.Equals(orgName, userName)))) + (UriHelpers.IsVisualStudioComHost(remoteWithUserUri.Host) || + (UriHelpers.IsAzureDevOpsHost(remoteWithUserUri.Host) && !icmp.Equals(orgName, userName)))) { _context.Trace.WriteLine("Using username as specified in remote."); } @@ -422,7 +422,7 @@ private static string GetServiceName(Uri remoteUri) { // If we're given the full path for an older *.visualstudio.com-style URL then we should // respect that in the service name. - return remoteUri.AbsoluteUri.TrimEnd('/'); + return remoteUri.WithoutUserInfo().AbsoluteUri.TrimEnd('/'); } throw new InvalidOperationException("Host is not Azure DevOps.");