From aac0bc3b87ca34eb739ee50bfcf4e12168bf1179 Mon Sep 17 00:00:00 2001 From: Zach Renner Date: Wed, 14 Nov 2018 12:41:00 -0800 Subject: [PATCH] Fix main provider flow to try other bearer token providers if exchanging for a session token fails. Add exception handling to continue trying other bearer token providers if one throws. --- Build.props | 2 +- .../Vsts/BearerTokenProviderTests.cs | 303 ------------------ .../Vsts/VstsCredentialProviderTests.cs | 106 +++++- .../Vsts/AdalTokenCacheUtils.cs | 32 ++ .../Vsts/AdalTokenProvider.cs | 19 +- .../Vsts/BearerTokenProvider.cs | 141 -------- .../Vsts/BearerTokenProviders.cs | 144 +++++++++ .../Vsts/BearerTokenProvidersFactory.cs | 34 ++ .../Vsts/IAdalTokenProviderFactory.cs | 23 +- .../Vsts/IBearerTokenProvider.cs | 28 -- .../Vsts/IBearerTokenProvidersFactory.cs | 13 + .../Vsts/IVstsSessionTokenClient.cs | 15 + ...VstsSessionTokenFromBearerTokenProvider.cs | 18 ++ .../Vsts/VstsAdalTokenProviderFactory.cs | 32 ++ .../Vsts/VstsCredentialProvider.cs | 123 +++---- .../Vsts/VstsSessionTokenClient.cs | 6 +- ...VstsSessionTokenFromBearerTokenProvider.cs | 64 ++++ CredentialProvider.Microsoft/Program.cs | 11 +- .../Resources.Designer.cs | 115 +++---- CredentialProvider.Microsoft/Resources.resx | 52 ++- CredentialProvider.Microsoft/Util/EnvUtil.cs | 6 + README.md | 5 + 22 files changed, 616 insertions(+), 676 deletions(-) delete mode 100644 CredentialProvider.Microsoft.Tests/CredentialProviders/Vsts/BearerTokenProviderTests.cs create mode 100644 CredentialProvider.Microsoft/CredentialProviders/Vsts/AdalTokenCacheUtils.cs delete mode 100644 CredentialProvider.Microsoft/CredentialProviders/Vsts/BearerTokenProvider.cs create mode 100644 CredentialProvider.Microsoft/CredentialProviders/Vsts/BearerTokenProviders.cs create mode 100644 CredentialProvider.Microsoft/CredentialProviders/Vsts/BearerTokenProvidersFactory.cs delete mode 100644 CredentialProvider.Microsoft/CredentialProviders/Vsts/IBearerTokenProvider.cs create mode 100644 CredentialProvider.Microsoft/CredentialProviders/Vsts/IBearerTokenProvidersFactory.cs create mode 100644 CredentialProvider.Microsoft/CredentialProviders/Vsts/IVstsSessionTokenClient.cs create mode 100644 CredentialProvider.Microsoft/CredentialProviders/Vsts/IVstsSessionTokenFromBearerTokenProvider.cs create mode 100644 CredentialProvider.Microsoft/CredentialProviders/Vsts/VstsAdalTokenProviderFactory.cs create mode 100644 CredentialProvider.Microsoft/CredentialProviders/Vsts/VstsSessionTokenFromBearerTokenProvider.cs diff --git a/Build.props b/Build.props index 4097b3bd..f5c3034b 100644 --- a/Build.props +++ b/Build.props @@ -1,6 +1,6 @@ - 0.1.7 + 0.1.8 \ No newline at end of file diff --git a/CredentialProvider.Microsoft.Tests/CredentialProviders/Vsts/BearerTokenProviderTests.cs b/CredentialProvider.Microsoft.Tests/CredentialProviders/Vsts/BearerTokenProviderTests.cs deleted file mode 100644 index ba47448c..00000000 --- a/CredentialProvider.Microsoft.Tests/CredentialProviders/Vsts/BearerTokenProviderTests.cs +++ /dev/null @@ -1,303 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// -// Licensed under the MIT license. - -using System; -using System.Threading; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.IdentityModel.Clients.ActiveDirectory; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using NuGet.Protocol.Plugins; -using NuGetCredentialProvider.CredentialProviders.Vsts; -using NuGetCredentialProvider.Logging; - -namespace CredentialProvider.Microsoft.Tests.CredentialProviders.Vsts -{ - [TestClass] - public class AdalVssCredentialProviderTests - { - private readonly CancellationToken cancellationToken = default(CancellationToken); - private readonly Uri testAuthority = new Uri("https://example.aad.authority.com"); - - private Mock mockLogger; - private Mock mockAdalTokenProviderFactory; - private Mock mockAdalTokenProvider; - private Mock mockAuthUtil; - - private BearerTokenProvider bearerTokenProvider; - - [TestInitialize] - public void TestInitialize() - { - mockLogger = new Mock(); - - mockAdalTokenProvider = new Mock(); - - mockAdalTokenProviderFactory = new Mock(); - mockAdalTokenProviderFactory - .Setup(x => x.Get(It.IsAny())) - .Returns(mockAdalTokenProvider.Object); - - mockAuthUtil = new Mock(); - mockAuthUtil - .Setup(x => x.GetAadAuthorityUriAsync(It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(testAuthority)); - - bearerTokenProvider = new BearerTokenProvider(mockLogger.Object, mockAdalTokenProviderFactory.Object, mockAuthUtil.Object); - } - - [TestMethod] - public async Task Get_WithoutCachedToken_CallsWindowsIntegratedFlow() - { - var source = new Uri("https://example.com/index.json"); - var isRetry = false; - var isNonInteractive = false; - var canShowDialog = true; - - var adalToken = "TestADALToken"; - MockCachedToken(null); - MockWindowsIntegratedToken(adalToken); - - var bearerToken = await bearerTokenProvider.GetAsync(source, isRetry, isNonInteractive, canShowDialog, cancellationToken); - bearerToken.Token.Should().Be(adalToken); - - mockAdalTokenProvider - .Verify(x => x.AcquireTokenSilentlyAsync(It.IsAny()), Times.Once); - mockAdalTokenProvider - .Verify(x => x.AcquireTokenWithWindowsIntegratedAuth(It.IsAny()), Times.Once); - mockAdalTokenProvider - .Verify(x => x.AcquireTokenWithUI(It.IsAny()), Times.Never); - mockAdalTokenProvider - .Verify(x => x.AcquireTokenWithDeviceFlowAsync(It.IsAny>(), It.IsAny()), Times.Never); - - VerifyAuthority(source); - } - - [TestMethod] - public async Task Get_WithoutCachedTokenAndWindowsIntegratedFails_CallsUIFlow() - { - var source = new Uri("https://example.com/index.json"); - var isRetry = false; - var isNonInteractive = false; - var canShowDialog = true; - - var adalToken = "TestADALToken"; - MockCachedToken(null); - MockWindowsIntegratedToken(null); - MockUIToken(adalToken); - - var bearerToken = await bearerTokenProvider.GetAsync(source, isRetry, isNonInteractive, canShowDialog, cancellationToken); - bearerToken.Token.Should().Be(adalToken); - - mockAdalTokenProvider - .Verify(x => x.AcquireTokenSilentlyAsync(It.IsAny()), Times.Once); - mockAdalTokenProvider - .Verify(x => x.AcquireTokenWithWindowsIntegratedAuth(It.IsAny()), Times.Once); - mockAdalTokenProvider - .Verify(x => x.AcquireTokenWithUI(It.IsAny()), Times.Once); - mockAdalTokenProvider - .Verify(x => x.AcquireTokenWithDeviceFlowAsync(It.IsAny>(), It.IsAny()), Times.Never); - - VerifyAuthority(source); - } - - [TestMethod] - public async Task Get_WithoutCachedTokenAndWindowsIntegratedFailedAndUIFlowCanceled_CallsDeviceCodeFlow() - { - var source = new Uri("https://example.com/index.json"); - var isRetry = false; - var isNonInteractive = false; - var canShowDialog = true; - - var adalToken = "TestADALToken"; - MockCachedToken(null); - MockWindowsIntegratedToken(null); - MockUIToken(null); - MockDeviceFlowToken(adalToken); - - var bearerTokenResult = await bearerTokenProvider.GetAsync(source, isRetry, isNonInteractive, canShowDialog, cancellationToken); - bearerTokenResult.Token.Should().Be(adalToken); - - mockAdalTokenProvider - .Verify(x => x.AcquireTokenSilentlyAsync(It.IsAny()), Times.Once); - mockAdalTokenProvider - .Verify(x => x.AcquireTokenWithWindowsIntegratedAuth(It.IsAny()), Times.Once); - mockAdalTokenProvider - .Verify(x => x.AcquireTokenWithUI(It.IsAny()), Times.Once); - mockAdalTokenProvider - .Verify(x => x.AcquireTokenWithDeviceFlowAsync(It.IsAny>(), It.IsAny()), Times.Once); - - VerifyAuthority(source); - } - - [TestMethod] - public async Task Get_WithCachedToken_DoesNotCallAnyFlows() - { - var source = new Uri("https://example.com/index.json"); - var isRetry = false; - var isNonInteractive = false; - var canShowDialog = false; - - var adalToken = "TestADALToken"; - MockCachedToken(adalToken); - - var bearerTokenResult = await bearerTokenProvider.GetAsync(source, isRetry, isNonInteractive, canShowDialog, cancellationToken); - bearerTokenResult.Token.Should().Be(adalToken); - - mockAdalTokenProvider - .Verify(x => x.AcquireTokenSilentlyAsync(It.IsAny()), Times.Once); - mockAdalTokenProvider - .Verify(x => x.AcquireTokenWithWindowsIntegratedAuth(It.IsAny()), Times.Never); - mockAdalTokenProvider - .Verify(x => x.AcquireTokenWithUI(It.IsAny()), Times.Never); - mockAdalTokenProvider - .Verify(x => x.AcquireTokenWithDeviceFlowAsync(It.IsAny>(), It.IsAny()), Times.Never); - - VerifyAuthority(source); - } - - [TestMethod] - public async Task Get_IsRetry_DoesNotQueryCache() - { - var source = new Uri("https://example.com/index.json"); - var isRetry = true; - var isNonInteractive = false; - var canShowDialog = true; - - var adalToken = "TestADALToken"; - MockCachedToken("OldCachedToken"); - MockWindowsIntegratedToken(null); - MockUIToken(adalToken); - - var bearerTokenResult = await bearerTokenProvider.GetAsync(source, isRetry, isNonInteractive, canShowDialog, cancellationToken); - bearerTokenResult.Token.Should().Be(adalToken); - - mockAdalTokenProvider - .Verify(x => x.AcquireTokenSilentlyAsync(It.IsAny()), Times.Never); - mockAdalTokenProvider - .Verify(x => x.AcquireTokenWithWindowsIntegratedAuth(It.IsAny()), Times.Once); - mockAdalTokenProvider - .Verify(x => x.AcquireTokenWithUI(It.IsAny()), Times.Once); - mockAdalTokenProvider - .Verify(x => x.AcquireTokenWithDeviceFlowAsync(It.IsAny>(), It.IsAny()), Times.Never); - - VerifyAuthority(source); - } - - [TestMethod] - public async Task Get_WithoutCachedTokenAndIsNonInteractive_DoesNotCallInteractiveFlows() - { - var source = new Uri("https://example.com/index.json"); - var isRetry = false; - var isNonInteractive = true; - var canShowDialog = false; - - MockCachedToken(null); - - var bearerTokenResult = await bearerTokenProvider.GetAsync(source, isRetry, isNonInteractive, canShowDialog, cancellationToken); - bearerTokenResult.Should().BeNull(); - - mockAdalTokenProvider - .Verify(x => x.AcquireTokenSilentlyAsync(It.IsAny()), Times.Once); - mockAdalTokenProvider - .Verify(x => x.AcquireTokenWithWindowsIntegratedAuth(It.IsAny()), Times.Once); - mockAdalTokenProvider - .Verify(x => x.AcquireTokenWithUI(It.IsAny()), Times.Never); - mockAdalTokenProvider - .Verify(x => x.AcquireTokenWithDeviceFlowAsync(It.IsAny>(), It.IsAny()), Times.Never); - - VerifyAuthority(source); - } - - [TestMethod] - public async Task Get_WithCachedTokenAndIsNonInteractive_DoesNotCallAnyFlows() - { - var source = new Uri("https://example.com/index.json"); - var isRetry = false; - var canShowDialog = false; - var isNonInteractive = true; - - var adalToken = "TestADALToken"; - MockCachedToken(adalToken); - - var bearerTokenResult = await bearerTokenProvider.GetAsync(source, isRetry, isNonInteractive, canShowDialog, cancellationToken); - bearerTokenResult.Token.Should().Be(adalToken); - - mockAdalTokenProvider - .Verify(x => x.AcquireTokenSilentlyAsync(It.IsAny()), Times.Once); - mockAdalTokenProvider - .Verify(x => x.AcquireTokenWithWindowsIntegratedAuth(It.IsAny()), Times.Never); - mockAdalTokenProvider - .Verify(x => x.AcquireTokenWithUI(It.IsAny()), Times.Never); - mockAdalTokenProvider - .Verify(x => x.AcquireTokenWithDeviceFlowAsync(It.IsAny>(), It.IsAny()), Times.Never); - - VerifyAuthority(source); - } - - [TestMethod] - public async Task Get_IsRetryIsNonInteractive_ShouldWarn() - { - var source = new Uri("https://example.com/index.json"); - var isRetry = true; - var isNonInteractive = true; - var canShowDialog = false; - - var bearerTokenResult = await bearerTokenProvider.GetAsync(source, isRetry, isNonInteractive, canShowDialog, cancellationToken); - bearerTokenResult.Should().BeNull(); - - mockAdalTokenProvider - .Verify(x => x.AcquireTokenSilentlyAsync(It.IsAny()), Times.Never); - mockAdalTokenProvider - .Verify(x => x.AcquireTokenWithUI(It.IsAny()), Times.Never); - mockAdalTokenProvider - .Verify(x => x.AcquireTokenWithDeviceFlowAsync(It.IsAny>(), It.IsAny()), Times.Never); - - mockLogger - .Verify(x => x.Log(NuGet.Common.LogLevel.Warning, It.IsAny())); - - VerifyAuthority(source); - } - - private void MockCachedToken(string token) - { - mockAdalTokenProvider - .Setup(x => x.AcquireTokenSilentlyAsync(It.IsAny())) - .Returns(Task.FromResult(new AdalToken("Bearer", token))); - } - - private void MockWindowsIntegratedToken(string token) - { - mockAdalTokenProvider - .Setup(x => x.AcquireTokenWithWindowsIntegratedAuth(It.IsAny())) - .Returns(Task.FromResult(new AdalToken("Bearer", token))); - } - - private void MockDeviceFlowToken(string token) - { - mockAdalTokenProvider - .Setup(x => x.AcquireTokenWithDeviceFlowAsync(It.IsAny>(), It.IsAny())) - .Returns(Task.FromResult(new AdalToken("Bearer", token))); - } - - private void MockUIToken(string token) - { - mockAdalTokenProvider - .Setup(x => x.AcquireTokenWithUI(It.IsAny())) - .Returns(Task.FromResult(new AdalToken("Bearer", token))); - } - - private void VerifyAuthority(Uri uri) - { - // Verify we're getting the correct authority for the request - mockAuthUtil - .Verify(x => x.GetAadAuthorityUriAsync(uri, It.IsAny())); - - // Verify we're getting the correct adal token provider for the request - mockAdalTokenProviderFactory - .Verify(x => x.Get(testAuthority.ToString())); - } - } -} diff --git a/CredentialProvider.Microsoft.Tests/CredentialProviders/Vsts/VstsCredentialProviderTests.cs b/CredentialProvider.Microsoft.Tests/CredentialProviders/Vsts/VstsCredentialProviderTests.cs index 5619e78c..662a7d0f 100644 --- a/CredentialProvider.Microsoft.Tests/CredentialProviders/Vsts/VstsCredentialProviderTests.cs +++ b/CredentialProvider.Microsoft.Tests/CredentialProviders/Vsts/VstsCredentialProviderTests.cs @@ -9,6 +9,7 @@ using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; +using NuGet.Protocol.Plugins; using NuGetCredentialProvider.CredentialProviders.Vsts; using NuGetCredentialProvider.Logging; using NuGetCredentialProvider.Util; @@ -18,27 +19,46 @@ namespace CredentialProvider.Microsoft.Tests.CredentialProviders.Vsts [TestClass] public class VstsCredentialProviderTests { + private readonly Uri testUri = new Uri("https://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json"); private readonly Uri testAuthority = new Uri("https://example.aad.authority.com"); private Mock mockLogger; - private Mock mockBearerTokenProvider; + private Mock mockBearerTokenProvider1 = new Mock(); + private Mock mockBearerTokenProvider2 = new Mock(); + private Mock mockBearerTokenProvidersFactory; + private Mock mockVstsSessionTokenFromBearerTokenProvider; private Mock mockAuthUtil; private VstsCredentialProvider vstsCredentialProvider; + [TestInitialize] public void TestInitialize() { mockLogger = new Mock(); - mockBearerTokenProvider = new Mock(); + mockBearerTokenProvider1 = new Mock(); + mockBearerTokenProvider1.Setup(x => x.ShouldRun(It.IsAny(), It.IsAny(), It.IsAny())).Returns(true); + mockBearerTokenProvider1.Setup(x => x.GetTokenAsync(It.IsAny(), It.IsAny())).ReturnsAsync((string)null); + mockBearerTokenProvider2 = new Mock(); + mockBearerTokenProvider2.Setup(x => x.ShouldRun(It.IsAny(), It.IsAny(), It.IsAny())).Returns(true); + mockBearerTokenProvider2.Setup(x => x.GetTokenAsync(It.IsAny(), It.IsAny())).ReturnsAsync((string)null); + mockBearerTokenProvidersFactory = new Mock(); + mockBearerTokenProvidersFactory.Setup(x => x.Get(It.IsAny())).Returns(new[] { mockBearerTokenProvider1.Object, mockBearerTokenProvider2.Object }); + + mockVstsSessionTokenFromBearerTokenProvider = new Mock(); + mockVstsSessionTokenFromBearerTokenProvider.Setup(x => x.GetAzureDevOpsSessionTokenFromBearerToken(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); mockAuthUtil = new Mock(); mockAuthUtil .Setup(x => x.GetAadAuthorityUriAsync(It.IsAny(), It.IsAny())) .Returns(Task.FromResult(testAuthority)); - vstsCredentialProvider = new VstsCredentialProvider(mockLogger.Object, mockAuthUtil.Object, mockBearerTokenProvider.Object); + vstsCredentialProvider = new VstsCredentialProvider( + mockLogger.Object, + mockAuthUtil.Object, + mockBearerTokenProvidersFactory.Object, + mockVstsSessionTokenFromBearerTokenProvider.Object); } [TestMethod] @@ -85,5 +105,85 @@ public async Task CanProvideCredentials_ReturnsTrueForOverridenSources() mockAuthUtil .Verify(x => x.IsVstsUriAsync(It.IsAny()), Times.Never, "because we shouldn't probe for known sources"); } + + [TestMethod] + public async Task HandleRequestAsync_DoesNotRunBearerTokenProviderWhenShouldRunFalse() + { + mockBearerTokenProvider1.Setup(x => x.ShouldRun(It.IsAny(), It.IsAny(), It.IsAny())).Returns(false); + await vstsCredentialProvider.HandleRequestAsync(new GetAuthenticationCredentialsRequest(testUri, false, false, false), CancellationToken.None); + mockBearerTokenProvider1.Verify(x => x.GetTokenAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task HandleRequestAsync_RunsBearerTokenProviderWhenShouldRunTrue() + { + mockBearerTokenProvider1.Setup(x => x.ShouldRun(It.IsAny(), It.IsAny(), It.IsAny())).Returns(true); + await vstsCredentialProvider.HandleRequestAsync(new GetAuthenticationCredentialsRequest(testUri, false, false, false), CancellationToken.None); + mockBearerTokenProvider1.Verify(x => x.GetTokenAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [TestMethod] + public async Task HandleRequestAsync_RunsNextBearerTokenProviderOnException() + { + mockBearerTokenProvider1.Setup(x => x.GetTokenAsync(It.IsAny(), It.IsAny())).ThrowsAsync(new Exception("bad")); + await vstsCredentialProvider.HandleRequestAsync(new GetAuthenticationCredentialsRequest(testUri, false, false, false), CancellationToken.None); + mockBearerTokenProvider2.Verify(x => x.GetTokenAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [TestMethod] + public async Task HandleRequestAsync_RunsNextBearerTokenProviderOnReturnNull() + { + mockBearerTokenProvider1.Setup(x => x.GetTokenAsync(It.IsAny(), It.IsAny())).ReturnsAsync((string)null); + await vstsCredentialProvider.HandleRequestAsync(new GetAuthenticationCredentialsRequest(testUri, false, false, false), CancellationToken.None); + mockBearerTokenProvider2.Verify(x => x.GetTokenAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [TestMethod] + public async Task HandleRequestAsync_ExchangesBearerTokenForSessionTokenAndReturnsToken() + { + mockBearerTokenProvider1.Setup(x => x.GetTokenAsync(It.IsAny(), It.IsAny())).ReturnsAsync("aadtoken"); + mockVstsSessionTokenFromBearerTokenProvider.Setup(x => x.GetAzureDevOpsSessionTokenFromBearerToken(It.IsAny(), "aadtoken", It.IsAny(), It.IsAny())).ReturnsAsync("sessiontoken"); + + var response = await vstsCredentialProvider.HandleRequestAsync(new GetAuthenticationCredentialsRequest(testUri, false, false, false), CancellationToken.None); + + mockBearerTokenProvider1.Verify(x => x.GetTokenAsync(It.IsAny(), It.IsAny()), Times.Once); + mockBearerTokenProvider2.Verify(x => x.GetTokenAsync(It.IsAny(), It.IsAny()), Times.Never); + mockVstsSessionTokenFromBearerTokenProvider.Verify(x => x.GetAzureDevOpsSessionTokenFromBearerToken(It.IsAny(), "aadtoken", It.IsAny(), It.IsAny()), Times.Once); + response.Password.Should().Be("sessiontoken"); + } + + [TestMethod] + public async Task HandleRequestAsync_TriesNextBearerTokenProviderWhenExchangeBearerTokenForSessionTokenFails() + { + mockBearerTokenProvider1.Setup(x => x.GetTokenAsync(It.IsAny(), It.IsAny())).ReturnsAsync("aadtoken1"); + mockBearerTokenProvider2.Setup(x => x.GetTokenAsync(It.IsAny(), It.IsAny())).ReturnsAsync("aadtoken2"); + mockVstsSessionTokenFromBearerTokenProvider.Setup(x => x.GetAzureDevOpsSessionTokenFromBearerToken(It.IsAny(), "aadtoken1", It.IsAny(), It.IsAny())).ReturnsAsync((string)null); + mockVstsSessionTokenFromBearerTokenProvider.Setup(x => x.GetAzureDevOpsSessionTokenFromBearerToken(It.IsAny(), "aadtoken2", It.IsAny(), It.IsAny())).ReturnsAsync("sessiontoken"); + + var response = await vstsCredentialProvider.HandleRequestAsync(new GetAuthenticationCredentialsRequest(testUri, false, false, false), CancellationToken.None); + + mockBearerTokenProvider1.Verify(x => x.GetTokenAsync(It.IsAny(), It.IsAny()), Times.Once); + mockBearerTokenProvider2.Verify(x => x.GetTokenAsync(It.IsAny(), It.IsAny()), Times.Once); + mockVstsSessionTokenFromBearerTokenProvider.Verify(x => x.GetAzureDevOpsSessionTokenFromBearerToken(It.IsAny(), "aadtoken1", It.IsAny(), It.IsAny()), Times.Once); + mockVstsSessionTokenFromBearerTokenProvider.Verify(x => x.GetAzureDevOpsSessionTokenFromBearerToken(It.IsAny(), "aadtoken2", It.IsAny(), It.IsAny()), Times.Once); + response.Password.Should().Be("sessiontoken"); + } + + [TestMethod] + public async Task HandleRequestAsync_ReturnsNullWhenAllBearerTokensBad() + { + mockBearerTokenProvider1.Setup(x => x.GetTokenAsync(It.IsAny(), It.IsAny())).ReturnsAsync("aadtoken1"); + mockBearerTokenProvider2.Setup(x => x.GetTokenAsync(It.IsAny(), It.IsAny())).ReturnsAsync("aadtoken2"); + mockVstsSessionTokenFromBearerTokenProvider.Setup(x => x.GetAzureDevOpsSessionTokenFromBearerToken(It.IsAny(), "aadtoken1", It.IsAny(), It.IsAny())).ReturnsAsync((string)null); + mockVstsSessionTokenFromBearerTokenProvider.Setup(x => x.GetAzureDevOpsSessionTokenFromBearerToken(It.IsAny(), "aadtoken2", It.IsAny(), It.IsAny())).ReturnsAsync((string)null); + + var response = await vstsCredentialProvider.HandleRequestAsync(new GetAuthenticationCredentialsRequest(testUri, false, false, false), CancellationToken.None); + + mockBearerTokenProvider1.Verify(x => x.GetTokenAsync(It.IsAny(), It.IsAny()), Times.Once); + mockBearerTokenProvider2.Verify(x => x.GetTokenAsync(It.IsAny(), It.IsAny()), Times.Once); + mockVstsSessionTokenFromBearerTokenProvider.Verify(x => x.GetAzureDevOpsSessionTokenFromBearerToken(It.IsAny(), "aadtoken1", It.IsAny(), It.IsAny()), Times.Once); + mockVstsSessionTokenFromBearerTokenProvider.Verify(x => x.GetAzureDevOpsSessionTokenFromBearerToken(It.IsAny(), "aadtoken2", It.IsAny(), It.IsAny()), Times.Once); + response.Should().BeNull(); + } } } diff --git a/CredentialProvider.Microsoft/CredentialProviders/Vsts/AdalTokenCacheUtils.cs b/CredentialProvider.Microsoft/CredentialProviders/Vsts/AdalTokenCacheUtils.cs new file mode 100644 index 00000000..08e461dd --- /dev/null +++ b/CredentialProvider.Microsoft/CredentialProviders/Vsts/AdalTokenCacheUtils.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft. All rights reserved. +// +// Licensed under the MIT license. + +using System.Runtime.InteropServices; +using Microsoft.IdentityModel.Clients.ActiveDirectory; +using NuGetCredentialProvider.Logging; +using NuGetCredentialProvider.Util; + +namespace NuGetCredentialProvider.CredentialProviders.Vsts +{ + public static class AdalTokenCacheUtils + { + public static TokenCache GetAdalTokenCache(ILogger logger) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + logger.Verbose(Resources.DPAPIUnavailableNonWindows); + return TokenCache.DefaultShared; + } + + if (!EnvUtil.AdalFileCacheEnabled()) + { + logger.Verbose(Resources.AdalFileCacheDisabled); + return TokenCache.DefaultShared; + } + + logger.Verbose(string.Format(Resources.AdalFileCacheLocation, EnvUtil.AdalTokenCacheLocation)); + return new AdalFileCache(EnvUtil.AdalTokenCacheLocation); + } + } +} \ No newline at end of file diff --git a/CredentialProvider.Microsoft/CredentialProviders/Vsts/AdalTokenProvider.cs b/CredentialProvider.Microsoft/CredentialProviders/Vsts/AdalTokenProvider.cs index 80a5c83d..76270a5f 100644 --- a/CredentialProvider.Microsoft/CredentialProviders/Vsts/AdalTokenProvider.cs +++ b/CredentialProvider.Microsoft/CredentialProviders/Vsts/AdalTokenProvider.cs @@ -3,7 +3,6 @@ // Licensed under the MIT license. using System; -using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using Microsoft.IdentityModel.Clients.ActiveDirectory; @@ -14,20 +13,26 @@ namespace NuGetCredentialProvider.CredentialProviders.Vsts public class AdalTokenProvider : IAdalTokenProvider { private const string NativeClientRedirect = "urn:ietf:wg:oauth:2.0:oob"; + private readonly string authority; private readonly string resource; private readonly string clientId; - - private AuthenticationContext authenticationContext; + private readonly TokenCache tokenCache; internal AdalTokenProvider(string authority, string resource, string clientId, TokenCache tokenCache) { + this.authority = authority; this.resource = resource; this.clientId = clientId; - this.authenticationContext = new AuthenticationContext(authority, tokenCache); + this.tokenCache = tokenCache; + + // authenticationContext is re-created on each call since the authority can be unexpectedly mutated by another call. + // e.g. AcquireTokenWithWindowsIntegratedAuth could set it to a specific AAD authority preventing a future AcquireTokenWithDeviceFlowAsync from working for a MSA account. } public async Task AcquireTokenWithDeviceFlowAsync(Func deviceCodeHandler, CancellationToken cancellationToken) { + var authenticationContext = new AuthenticationContext(authority, tokenCache); + var deviceCode = await authenticationContext.AcquireDeviceCodeAsync(resource, clientId); cancellationToken.ThrowIfCancellationRequested(); @@ -45,6 +50,8 @@ public async Task AcquireTokenWithDeviceFlowAsync(Func AcquireTokenSilentlyAsync(CancellationToken cancellationToken) { + var authenticationContext = new AuthenticationContext(authority, tokenCache); + try { var result = await authenticationContext.AcquireTokenSilentAsync(resource, clientId); @@ -60,6 +67,8 @@ public async Task AcquireTokenSilentlyAsync(CancellationToken cancel public async Task AcquireTokenWithUI(CancellationToken cancellationToken) { + var authenticationContext = new AuthenticationContext(authority, tokenCache); + var parameters = #if NETFRAMEWORK new PlatformParameters(PromptBehavior.Always); @@ -87,6 +96,8 @@ public async Task AcquireTokenWithUI(CancellationToken cancellationT public async Task AcquireTokenWithWindowsIntegratedAuth(CancellationToken cancellationToken) { + var authenticationContext = new AuthenticationContext(authority, tokenCache); + try { string upn = WindowsIntegratedAuthUtils.GetUserPrincipalName(); diff --git a/CredentialProvider.Microsoft/CredentialProviders/Vsts/BearerTokenProvider.cs b/CredentialProvider.Microsoft/CredentialProviders/Vsts/BearerTokenProvider.cs deleted file mode 100644 index 9ed27a31..00000000 --- a/CredentialProvider.Microsoft/CredentialProviders/Vsts/BearerTokenProvider.cs +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// -// Licensed under the MIT license. - -using System; -using System.Runtime.InteropServices; -using System.Security.Claims; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.IdentityModel.Clients.ActiveDirectory; -using NuGetCredentialProvider.Logging; -using NuGetCredentialProvider.Util; - -namespace NuGetCredentialProvider.CredentialProviders.Vsts -{ - public class BearerTokenProvider : IBearerTokenProvider - { - private const string Resource = "499b84ac-1321-427f-aa17-267ca6975798"; - private const string ClientId = "872cd9fa-d31f-45e0-9eab-6e460a02d1f1"; - - private readonly ILogger logger; - private readonly IAdalTokenProviderFactory adalTokenProviderFactory; - private readonly IAuthUtil authUtil; - - public BearerTokenProvider(ILogger logger) - : this(logger, new AdalTokenProviderFactory(Resource, ClientId, GetTokenCache(logger)), new AuthUtil(logger)) - { - } - - public BearerTokenProvider(ILogger logger, IAdalTokenProviderFactory adalTokenProviderFactory, IAuthUtil authUtil) - { - this.logger = logger; - this.adalTokenProviderFactory = adalTokenProviderFactory; - this.authUtil = authUtil; - } - - public async Task GetAsync(Uri uri, bool isRetry, bool isNonInteractive, bool canShowDialog, CancellationToken cancellationToken) - { - var authority = await authUtil.GetAadAuthorityUriAsync(uri, cancellationToken); - logger.Verbose(string.Format(Resources.AdalUsingAuthority, authority)); - - var adalTokenProvider = adalTokenProviderFactory.Get(authority.ToString()); - cancellationToken.ThrowIfCancellationRequested(); - - IAdalToken adalToken; - - // Try to acquire token silently - if (!isRetry) - { - adalToken = await adalTokenProvider.AcquireTokenSilentlyAsync(cancellationToken); - if (adalToken?.AccessToken != null) - { - logger.Verbose(Resources.AdalAcquireTokenSilentSuccess); - return new BearerTokenResult(adalToken.AccessToken, obtainedInteractively: false); - } - else - { - logger.Verbose(Resources.AdalAcquireTokenSilentFailed); - } - } - - // Try Windows Integrated Auth if supported - if (WindowsIntegratedAuthUtils.SupportsWindowsIntegratedAuth()) - { - adalToken = await adalTokenProvider.AcquireTokenWithWindowsIntegratedAuth(cancellationToken); - if (adalToken?.AccessToken != null) - { - logger.Verbose(Resources.AdalAcquireTokenWIASuccess); - return new BearerTokenResult(adalToken.AccessToken, obtainedInteractively: false); - } - else - { - logger.Verbose(Resources.AdalAcquireTokenWIAFailed); - } - } - - // Interactive flows if allowed - if (!isNonInteractive) - { -#if NETFRAMEWORK - if (canShowDialog && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - // Try UI prompt - adalToken = await adalTokenProvider.AcquireTokenWithUI(cancellationToken); - - if (adalToken?.AccessToken != null) - { - return new BearerTokenResult(adalToken.AccessToken, obtainedInteractively: true); - } - } -#endif - - // Try device flow - adalToken = await adalTokenProvider.AcquireTokenWithDeviceFlowAsync( - (DeviceCodeResult deviceCodeResult) => - { - logger.Minimal(string.Format(Resources.AdalDeviceFlowRequestedResource, uri.ToString())); - logger.Minimal(string.Format(Resources.AdalDeviceFlowMessage, deviceCodeResult.VerificationUrl, deviceCodeResult.UserCode)); - - return Task.CompletedTask; - }, - cancellationToken); - - if (adalToken?.AccessToken != null) - { - logger.Verbose(Resources.AdalAcquireTokenDeviceFlowSuccess); - return new BearerTokenResult(adalToken.AccessToken, obtainedInteractively: true); - } - else - { - logger.Verbose(Resources.AdalAcquireTokenDeviceFlowFailed); - } - } - else if (isRetry) - { - logger.Warning(Resources.CannotRetryWithNonInteractiveFlag); - } - - logger.Verbose(string.Format(Resources.AdalTokenNotFound, uri.ToString())); - return null; - } - - private static TokenCache GetTokenCache(ILogger logger) - { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - logger.Verbose(Resources.DPAPIUnavailableNonWindows); - return TokenCache.DefaultShared; - } - - if (!EnvUtil.AdalFileCacheEnabled()) - { - logger.Verbose(Resources.AdalFileCacheDisabled); - return TokenCache.DefaultShared; - } - - logger.Verbose(string.Format(Resources.AdalFileCacheLocation, EnvUtil.AdalTokenCacheLocation)); - return new AdalFileCache(EnvUtil.AdalTokenCacheLocation); - } - } -} diff --git a/CredentialProvider.Microsoft/CredentialProviders/Vsts/BearerTokenProviders.cs b/CredentialProvider.Microsoft/CredentialProviders/Vsts/BearerTokenProviders.cs new file mode 100644 index 00000000..b3f550d2 --- /dev/null +++ b/CredentialProvider.Microsoft/CredentialProviders/Vsts/BearerTokenProviders.cs @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft. All rights reserved. +// +// Licensed under the MIT license. + +using System; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Clients.ActiveDirectory; +using NuGetCredentialProvider.Logging; +using NuGetCredentialProvider.Util; + +namespace NuGetCredentialProvider.CredentialProviders.Vsts +{ + /// + /// Acquire an AAD token via the ADAL cache + /// + public class AdalCacheBearerTokenProvider : IBearerTokenProvider + { + private readonly IAdalTokenProvider adalTokenProvider; + + public AdalCacheBearerTokenProvider(IAdalTokenProvider adalTokenProvider) + { + this.adalTokenProvider = adalTokenProvider; + } + + public bool Interactive { get; } = false; + public string Name { get; } = "ADAL Cache"; + + public async Task GetTokenAsync(Uri uri, CancellationToken cancellationToken) + { + return (await adalTokenProvider.AcquireTokenSilentlyAsync(cancellationToken))?.AccessToken; + } + + public bool ShouldRun(bool isRetry, bool isNonInteractive, bool canShowDialog) + { + return !isRetry; + } + } + + /// + /// Acquire an AAD token via Windows Integrated Authentication + /// + public class WindowsIntegratedAuthBearerTokenProvider : IBearerTokenProvider + { + private readonly IAdalTokenProvider adalTokenProvider; + + public WindowsIntegratedAuthBearerTokenProvider(IAdalTokenProvider adalTokenProvider) + { + this.adalTokenProvider = adalTokenProvider; + } + + public bool Interactive { get; } = false; + public string Name { get; } = "ADAL Windows Integrated Authentication"; + + public async Task GetTokenAsync(Uri uri, CancellationToken cancellationToken) + { + return (await adalTokenProvider.AcquireTokenWithWindowsIntegratedAuth(cancellationToken))?.AccessToken; + } + + public bool ShouldRun(bool isRetry, bool isNonInteractive, bool canShowDialog) + { + return WindowsIntegratedAuthUtils.SupportsWindowsIntegratedAuth() && EnvUtil.WindowsIntegratedAuthenticationEnabled(); + } + } + + /// + /// Acquire an AAD token by showing a sign-in UI + /// + public class UserInterfaceBearerTokenProvider : IBearerTokenProvider + { + private readonly IAdalTokenProvider adalTokenProvider; + + public UserInterfaceBearerTokenProvider(IAdalTokenProvider adalTokenProvider) + { + this.adalTokenProvider = adalTokenProvider; + } + + public bool Interactive { get; } = true; + public string Name { get; } = "ADAL UI"; + + public async Task GetTokenAsync(Uri uri, CancellationToken cancellationToken) + { + return (await adalTokenProvider.AcquireTokenWithUI(cancellationToken))?.AccessToken; + } + + public bool ShouldRun(bool isRetry, bool isNonInteractive, bool canShowDialog) + { + return !isNonInteractive && canShowDialog && RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + } + } + + /// + /// Acquire an AAD token by presenting a "device code" and waiting for the user to sign in with it in a browser + /// + public class DeviceCodeFlowBearerTokenProvider : IBearerTokenProvider + { + private readonly IAdalTokenProvider adalTokenProvider; + private readonly ILogger logger; + + public DeviceCodeFlowBearerTokenProvider( + IAdalTokenProvider adalTokenProvider, + ILogger logger) + { + this.adalTokenProvider = adalTokenProvider; + this.logger = logger; + } + + public bool Interactive { get; } = true; + public string Name { get; } = "ADAL Device Code"; + + public async Task GetTokenAsync(Uri uri, CancellationToken cancellationToken) + { + return (await adalTokenProvider.AcquireTokenWithDeviceFlowAsync( + (DeviceCodeResult deviceCodeResult) => + { + logger.Minimal(string.Format(Resources.AdalDeviceFlowRequestedResource, uri.ToString())); + logger.Minimal(string.Format(Resources.AdalDeviceFlowMessage, deviceCodeResult.VerificationUrl, deviceCodeResult.UserCode)); + + return Task.CompletedTask; + }, + cancellationToken))?.AccessToken; + } + + public bool ShouldRun(bool isRetry, bool isNonInteractive, bool canShowDialog) + { + return !isNonInteractive; + } + } + + /// + /// A mechanism to obtain a bearer (e.g. AAD) token which can later be exchanged for an Azure DevOps session or personal access token. + /// + public interface IBearerTokenProvider + { + string Name { get; } + + bool Interactive { get; } + + bool ShouldRun(bool isRetry, bool isNonInteractive, bool canShowDialog); + + Task GetTokenAsync(Uri uri, CancellationToken cancellationToken); + } +} diff --git a/CredentialProvider.Microsoft/CredentialProviders/Vsts/BearerTokenProvidersFactory.cs b/CredentialProvider.Microsoft/CredentialProviders/Vsts/BearerTokenProvidersFactory.cs new file mode 100644 index 00000000..912d5560 --- /dev/null +++ b/CredentialProvider.Microsoft/CredentialProviders/Vsts/BearerTokenProvidersFactory.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All rights reserved. +// +// Licensed under the MIT license. + +using System.Collections.Generic; +using NuGetCredentialProvider.Logging; + +namespace NuGetCredentialProvider.CredentialProviders.Vsts +{ + public class BearerTokenProvidersFactory : IBearerTokenProvidersFactory + { + private readonly ILogger logger; + private readonly IAdalTokenProviderFactory adalTokenProviderFactory; + + public BearerTokenProvidersFactory(ILogger logger, IAdalTokenProviderFactory adalTokenProviderFactory) + { + this.adalTokenProviderFactory = adalTokenProviderFactory; + this.logger = logger; + } + + public IEnumerable Get(string authority) + { + IAdalTokenProvider adalTokenProvider = adalTokenProviderFactory.Get(authority); + return new IBearerTokenProvider[] + { + // Order here is important - providers (potentially) run in this order. + new AdalCacheBearerTokenProvider(adalTokenProvider), + new WindowsIntegratedAuthBearerTokenProvider(adalTokenProvider), + new UserInterfaceBearerTokenProvider(adalTokenProvider), + new DeviceCodeFlowBearerTokenProvider(adalTokenProvider, logger) + }; + } + } +} \ No newline at end of file diff --git a/CredentialProvider.Microsoft/CredentialProviders/Vsts/IAdalTokenProviderFactory.cs b/CredentialProvider.Microsoft/CredentialProviders/Vsts/IAdalTokenProviderFactory.cs index 529fd2a7..0cdd26bd 100644 --- a/CredentialProvider.Microsoft/CredentialProviders/Vsts/IAdalTokenProviderFactory.cs +++ b/CredentialProvider.Microsoft/CredentialProviders/Vsts/IAdalTokenProviderFactory.cs @@ -2,31 +2,10 @@ // // Licensed under the MIT license. -using Microsoft.IdentityModel.Clients.ActiveDirectory; - namespace NuGetCredentialProvider.CredentialProviders.Vsts { public interface IAdalTokenProviderFactory { IAdalTokenProvider Get(string authority); } - - public class AdalTokenProviderFactory : IAdalTokenProviderFactory - { - private readonly string resource; - private readonly string clientId; - private readonly TokenCache tokenCache; - - public AdalTokenProviderFactory(string resource, string clientId, TokenCache tokenCache) - { - this.resource = resource; - this.clientId = clientId; - this.tokenCache = tokenCache; - } - - public IAdalTokenProvider Get(string authority) - { - return new AdalTokenProvider(authority, resource, clientId, tokenCache); - } - } -} +} \ No newline at end of file diff --git a/CredentialProvider.Microsoft/CredentialProviders/Vsts/IBearerTokenProvider.cs b/CredentialProvider.Microsoft/CredentialProviders/Vsts/IBearerTokenProvider.cs deleted file mode 100644 index 2e747f8f..00000000 --- a/CredentialProvider.Microsoft/CredentialProviders/Vsts/IBearerTokenProvider.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// -// Licensed under the MIT license. - -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace NuGetCredentialProvider.CredentialProviders.Vsts -{ - public interface IBearerTokenProvider - { - Task GetAsync(Uri uri, bool isRetry, bool isNonInteractive, bool canShowDialog, CancellationToken cancellationToken); - } - - public class BearerTokenResult - { - public BearerTokenResult(string token, bool obtainedInteractively) - { - Token = token; - ObtainedInteractively = obtainedInteractively; - } - - public string Token { get; } - - public bool ObtainedInteractively { get; } - } -} diff --git a/CredentialProvider.Microsoft/CredentialProviders/Vsts/IBearerTokenProvidersFactory.cs b/CredentialProvider.Microsoft/CredentialProviders/Vsts/IBearerTokenProvidersFactory.cs new file mode 100644 index 00000000..be42fb51 --- /dev/null +++ b/CredentialProvider.Microsoft/CredentialProviders/Vsts/IBearerTokenProvidersFactory.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft. All rights reserved. +// +// Licensed under the MIT license. + +using System.Collections.Generic; + +namespace NuGetCredentialProvider.CredentialProviders.Vsts +{ + public interface IBearerTokenProvidersFactory + { + IEnumerable Get(string authority); + } +} \ No newline at end of file diff --git a/CredentialProvider.Microsoft/CredentialProviders/Vsts/IVstsSessionTokenClient.cs b/CredentialProvider.Microsoft/CredentialProviders/Vsts/IVstsSessionTokenClient.cs new file mode 100644 index 00000000..86778728 --- /dev/null +++ b/CredentialProvider.Microsoft/CredentialProviders/Vsts/IVstsSessionTokenClient.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. +// +// Licensed under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace NuGetCredentialProvider.CredentialProviders.Vsts +{ + public interface IVstsSessionTokenClient + { + Task CreateSessionTokenAsync(VstsTokenType tokenType, DateTime validTo, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/CredentialProvider.Microsoft/CredentialProviders/Vsts/IVstsSessionTokenFromBearerTokenProvider.cs b/CredentialProvider.Microsoft/CredentialProviders/Vsts/IVstsSessionTokenFromBearerTokenProvider.cs new file mode 100644 index 00000000..0c0752a2 --- /dev/null +++ b/CredentialProvider.Microsoft/CredentialProviders/Vsts/IVstsSessionTokenFromBearerTokenProvider.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft. All rights reserved. +// +// Licensed under the MIT license. +using System.Threading; +using System.Threading.Tasks; +using NuGet.Protocol.Plugins; + +namespace NuGetCredentialProvider.CredentialProviders.Vsts +{ + public interface IAzureDevOpsSessionTokenFromBearerTokenProvider + { + Task GetAzureDevOpsSessionTokenFromBearerToken( + GetAuthenticationCredentialsRequest request, + string bearerToken, + bool bearerTokenObtainedInteractively, + CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/CredentialProvider.Microsoft/CredentialProviders/Vsts/VstsAdalTokenProviderFactory.cs b/CredentialProvider.Microsoft/CredentialProviders/Vsts/VstsAdalTokenProviderFactory.cs new file mode 100644 index 00000000..93d1afc4 --- /dev/null +++ b/CredentialProvider.Microsoft/CredentialProviders/Vsts/VstsAdalTokenProviderFactory.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft. All rights reserved. +// +// Licensed under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Clients.ActiveDirectory; +using NuGet.Protocol.Plugins; +using NuGetCredentialProvider.Logging; +using NuGetCredentialProvider.Util; + +namespace NuGetCredentialProvider.CredentialProviders.Vsts +{ + public class VstsAdalTokenProviderFactory : IAdalTokenProviderFactory + { + private const string Resource = "499b84ac-1321-427f-aa17-267ca6975798"; + private const string ClientId = "872cd9fa-d31f-45e0-9eab-6e460a02d1f1"; + + private readonly TokenCache tokenCache; + + public VstsAdalTokenProviderFactory(TokenCache tokenCache) + { + this.tokenCache = tokenCache; + } + + public IAdalTokenProvider Get(string authority) + { + return new AdalTokenProvider(authority, Resource, ClientId, tokenCache); + } + } +} diff --git a/CredentialProvider.Microsoft/CredentialProviders/Vsts/VstsCredentialProvider.cs b/CredentialProvider.Microsoft/CredentialProviders/Vsts/VstsCredentialProvider.cs index 4e6fb4f2..51850c5b 100644 --- a/CredentialProvider.Microsoft/CredentialProviders/Vsts/VstsCredentialProvider.cs +++ b/CredentialProvider.Microsoft/CredentialProviders/Vsts/VstsCredentialProvider.cs @@ -16,25 +16,21 @@ namespace NuGetCredentialProvider.CredentialProviders.Vsts public sealed class VstsCredentialProvider : CredentialProviderBase { private const string Username = "VssSessionToken"; - private const string TokenScope = "vso.packaging_write vso.drop_write"; - private const double DefaultSessionTimeHours = 4; - private const double DefaultPersonalAccessTimeHours = 2160; // 90 days private readonly IAuthUtil authUtil; - private readonly IBearerTokenProvider bearerTokenProvider; - - public VstsCredentialProvider(ILogger logger) - : base(logger) - { - this.authUtil = new AuthUtil(logger); - this.bearerTokenProvider = new BearerTokenProvider(logger); - } - - public VstsCredentialProvider(ILogger logger, IAuthUtil authUtil, IBearerTokenProvider bearerTokenProvider) + private readonly IBearerTokenProvidersFactory bearerTokenProvidersFactory; + private readonly IAzureDevOpsSessionTokenFromBearerTokenProvider vstsSessionTokenProvider; + + public VstsCredentialProvider( + ILogger logger, + IAuthUtil authUtil, + IBearerTokenProvidersFactory bearerTokenProvidersFactory, + IAzureDevOpsSessionTokenFromBearerTokenProvider vstsSessionTokenProvider) : base(logger) { this.authUtil = authUtil; - this.bearerTokenProvider = bearerTokenProvider; + this.bearerTokenProvidersFactory = bearerTokenProvidersFactory; + this.vstsSessionTokenProvider = vstsSessionTokenProvider; } protected override string LoggingName => nameof(VstsCredentialProvider); @@ -55,82 +51,67 @@ public override async Task CanProvideCredentialsAsync(Uri uri) public override async Task HandleRequestAsync(GetAuthenticationCredentialsRequest request, CancellationToken cancellationToken) { - // If this is a retry, let's try without sending a retry request to the underlying bearerTokenProvider - if (request.IsRetry) + Uri authority = await authUtil.GetAadAuthorityUriAsync(request.Uri, cancellationToken); + Verbose(string.Format(Resources.AdalUsingAuthority, authority)); + + IEnumerable bearerTokenProviders = bearerTokenProvidersFactory.Get(authority.ToString()); + cancellationToken.ThrowIfCancellationRequested(); + + // Try each bearer token provider (e.g. ADAL cache, ADAL WIA, ADAL UI, ADAL DeviceCode) in order. + // Only consider it successful if the bearer token can be exchanged for an Azure DevOps token. + foreach (IBearerTokenProvider bearerTokenProvider in bearerTokenProviders) { - var responseWithNoRetry = await HandleRequestAsync(GetNonRetryRequest(request), cancellationToken); - if (responseWithNoRetry?.ResponseCode == MessageResponseCode.Success) + bool shouldRun = bearerTokenProvider.ShouldRun(request.IsRetry, request.IsNonInteractive, request.CanShowDialog); + if (!shouldRun) { - return responseWithNoRetry; + Verbose(string.Format(Resources.NotRunningBearerTokenProvider, bearerTokenProvider.Name)); + continue; } - } - try - { - BearerTokenResult bearerTokenResult = await bearerTokenProvider.GetAsync(request.Uri, isRetry: false, request.IsNonInteractive, request.CanShowDialog, cancellationToken); - if (bearerTokenResult == null || string.IsNullOrWhiteSpace(bearerTokenResult.Token)) + Verbose(string.Format(Resources.AttemptingToAcquireBearerTokenUsingProvider, bearerTokenProvider.Name)); + + string bearerToken = null; + try { - return new GetAuthenticationCredentialsResponse( - username: null, - password: null, - message: Resources.BearerTokenFailed, - authenticationTypes: null, - responseCode: MessageResponseCode.Error); + bearerToken = await bearerTokenProvider.GetTokenAsync(request.Uri, cancellationToken); + } + catch (Exception ex) + { + Verbose(string.Format(Resources.BearerTokenProviderException, bearerTokenProvider.Name, ex)); + continue; } - // Allow the user to choose their token type - // If they don't and interactive auth was required, then prefer a PAT so we can safely default to a much longer validity period - VstsTokenType tokenType = EnvUtil.GetVstsTokenType() ?? - (bearerTokenResult.ObtainedInteractively - ? VstsTokenType.Compact - : VstsTokenType.SelfDescribing); - - // Allow the user to override the validity period - TimeSpan? preferredTokenTime = EnvUtil.GetSessionTimeFromEnvironment(Logger); - TimeSpan sessionTimeSpan; - if (tokenType == VstsTokenType.Compact) + if (string.IsNullOrWhiteSpace(bearerToken)) { - // Allow Personal Access Tokens to be as long as SPS will grant, since they're easily revokable - sessionTimeSpan = preferredTokenTime ?? TimeSpan.FromHours(DefaultPersonalAccessTimeHours); + Verbose(string.Format(Resources.BearerTokenProviderReturnedNull, bearerTokenProvider.Name)); + continue; } - else + + Info(string.Format(Resources.AcquireBearerTokenSuccess, bearerTokenProvider.Name)); + Info(Resources.ExchangingBearerTokenForSessionToken); + try { - // But limit self-describing session tokens to a strict 24 hours, since they're harder to revoke - sessionTimeSpan = preferredTokenTime ?? TimeSpan.FromHours(DefaultSessionTimeHours); - if (sessionTimeSpan >= TimeSpan.FromHours(24)) + string sessionToken = await vstsSessionTokenProvider.GetAzureDevOpsSessionTokenFromBearerToken(request, bearerToken, bearerTokenProvider.Interactive, cancellationToken); + + if (!string.IsNullOrWhiteSpace(sessionToken)) { - sessionTimeSpan = TimeSpan.FromHours(24); + Verbose(string.Format(Resources.VSTSSessionTokenCreated, request.Uri.ToString())); + return new GetAuthenticationCredentialsResponse( + Username, + sessionToken, + message: null, + authenticationTypes: new List() { "Basic" }, + responseCode: MessageResponseCode.Success); } } - - DateTime endTime = DateTime.UtcNow + sessionTimeSpan; - Verbose(string.Format(Resources.VSTSSessionTokenValidity, tokenType.ToString(), sessionTimeSpan.ToString(), endTime.ToUniversalTime().ToString())); - var sessionTokenClient = new VstsSessionTokenClient(request.Uri, bearerTokenResult.Token, authUtil); - var sessionToken = await sessionTokenClient.CreateSessionTokenAsync(tokenType, endTime, cancellationToken); - - if (!string.IsNullOrWhiteSpace(sessionToken)) + catch (Exception e) { - Verbose(string.Format(Resources.VSTSSessionTokenCreated, request.Uri.ToString())); - return new GetAuthenticationCredentialsResponse( - Username, - sessionToken, - message: null, - authenticationTypes: new List() { "Basic" }, - responseCode: MessageResponseCode.Success); + Verbose(string.Format(Resources.VSTSCreateSessionException, request.Uri, e.Message, e.StackTrace)); } } - catch (Exception e) - { - Verbose(string.Format(Resources.VSTSCreateSessionException, request.Uri, e.Message, e.StackTrace)); - } Verbose(string.Format(Resources.VSTSCredentialsNotFound, request.Uri.ToString())); return null; } - - private GetAuthenticationCredentialsRequest GetNonRetryRequest(GetAuthenticationCredentialsRequest request) - { - return new GetAuthenticationCredentialsRequest(request.Uri, isRetry: false, request.IsNonInteractive, request.CanShowDialog); - } } } \ No newline at end of file diff --git a/CredentialProvider.Microsoft/CredentialProviders/Vsts/VstsSessionTokenClient.cs b/CredentialProvider.Microsoft/CredentialProviders/Vsts/VstsSessionTokenClient.cs index f9d70a1b..200fe110 100644 --- a/CredentialProvider.Microsoft/CredentialProviders/Vsts/VstsSessionTokenClient.cs +++ b/CredentialProvider.Microsoft/CredentialProviders/Vsts/VstsSessionTokenClient.cs @@ -12,8 +12,10 @@ namespace NuGetCredentialProvider.CredentialProviders.Vsts { - public class VstsSessionTokenClient + public class VstsSessionTokenClient : IVstsSessionTokenClient { + private const string TokenScope = "vso.packaging_write vso.drop_write"; + private readonly Uri vstsUri; private readonly string bearerToken; private readonly IAuthUtil authUtil; @@ -48,7 +50,7 @@ public async Task CreateSessionTokenAsync(VstsTokenType tokenType, DateT new VstsSessionToken() { DisplayName = "Azure DevOps Artifacts Credential Provider", - Scope = "vso.packaging_write", + Scope = TokenScope, ValidTo = validTo }), Encoding.UTF8, diff --git a/CredentialProvider.Microsoft/CredentialProviders/Vsts/VstsSessionTokenFromBearerTokenProvider.cs b/CredentialProvider.Microsoft/CredentialProviders/Vsts/VstsSessionTokenFromBearerTokenProvider.cs new file mode 100644 index 00000000..f10f71a7 --- /dev/null +++ b/CredentialProvider.Microsoft/CredentialProviders/Vsts/VstsSessionTokenFromBearerTokenProvider.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft. All rights reserved. +// +// Licensed under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using NuGet.Protocol.Plugins; +using NuGetCredentialProvider.Logging; +using NuGetCredentialProvider.Util; + +namespace NuGetCredentialProvider.CredentialProviders.Vsts +{ + public class VstsSessionTokenFromBearerTokenProvider : IAzureDevOpsSessionTokenFromBearerTokenProvider + { + private const double DefaultSessionTimeHours = 4; + private const double DefaultPersonalAccessTimeHours = 2160; // 90 days + private readonly IAuthUtil authUtil; + private readonly ILogger logger; + + public VstsSessionTokenFromBearerTokenProvider(IAuthUtil authUtil, ILogger logger) + { + this.authUtil = authUtil; + this.logger = logger; + } + + public async Task GetAzureDevOpsSessionTokenFromBearerToken( + GetAuthenticationCredentialsRequest request, + string bearerToken, + bool bearerTokenObtainedInteractively, + CancellationToken cancellationToken) + { + // Allow the user to choose their token type + // If they don't and interactive auth was required, then prefer a PAT so we can safely default to a much longer validity period + VstsTokenType tokenType = EnvUtil.GetVstsTokenType() ?? + (bearerTokenObtainedInteractively + ? VstsTokenType.Compact + : VstsTokenType.SelfDescribing); + + // Allow the user to override the validity period + TimeSpan? preferredTokenTime = EnvUtil.GetSessionTimeFromEnvironment(logger); + TimeSpan sessionTimeSpan; + if (tokenType == VstsTokenType.Compact) + { + // Allow Personal Access Tokens to be as long as SPS will grant, since they're easily revokable + sessionTimeSpan = preferredTokenTime ?? TimeSpan.FromHours(DefaultPersonalAccessTimeHours); + } + else + { + // But limit self-describing session tokens to a strict 24 hours, since they're harder to revoke + sessionTimeSpan = preferredTokenTime ?? TimeSpan.FromHours(DefaultSessionTimeHours); + if (sessionTimeSpan >= TimeSpan.FromHours(24)) + { + sessionTimeSpan = TimeSpan.FromHours(24); + } + } + + DateTime endTime = DateTime.UtcNow + sessionTimeSpan; + logger.Verbose(string.Format(Resources.VSTSSessionTokenValidity, tokenType.ToString(), sessionTimeSpan.ToString(), endTime.ToUniversalTime().ToString())); + VstsSessionTokenClient sessionTokenClient = new VstsSessionTokenClient(request.Uri, bearerToken, authUtil); + return await sessionTokenClient.CreateSessionTokenAsync(tokenType, endTime, cancellationToken); + } + } +} diff --git a/CredentialProvider.Microsoft/Program.cs b/CredentialProvider.Microsoft/Program.cs index b6e117a9..758fc8bd 100644 --- a/CredentialProvider.Microsoft/Program.cs +++ b/CredentialProvider.Microsoft/Program.cs @@ -72,11 +72,17 @@ public static async Task Main(string[] args) tokenSource.Cancel(); }; + var authUtil = new AuthUtil(multiLogger); + var adalTokenCache = AdalTokenCacheUtils.GetAdalTokenCache(multiLogger); + var adalTokenProviderFactory = new VstsAdalTokenProviderFactory(adalTokenCache); + var bearerTokenProvidersFactory = new BearerTokenProvidersFactory(multiLogger, adalTokenProviderFactory); + var vstsSessionTokenProvider = new VstsSessionTokenFromBearerTokenProvider(authUtil, multiLogger); + List credentialProviders = new List { new VstsBuildTaskServiceEndpointCredentialProvider(multiLogger), new VstsBuildTaskCredentialProvider(multiLogger), - new VstsCredentialProvider(multiLogger), + new VstsCredentialProvider(multiLogger, authUtil, bearerTokenProvidersFactory, vstsSessionTokenProvider), }; try @@ -112,7 +118,8 @@ public static async Task Main(string[] args) EnvUtil.BuildTaskAccessToken, EnvUtil.BuildTaskExternalEndpoints, EnvUtil.AdalTokenCacheLocation, - EnvUtil.SessionTokenCacheLocation + EnvUtil.SessionTokenCacheLocation, + EnvUtil.WindowsIntegratedAuthenticationEnvVar )); return 0; } diff --git a/CredentialProvider.Microsoft/Resources.Designer.cs b/CredentialProvider.Microsoft/Resources.Designer.cs index 9417a20a..f6446d1d 100644 --- a/CredentialProvider.Microsoft/Resources.Designer.cs +++ b/CredentialProvider.Microsoft/Resources.Designer.cs @@ -79,65 +79,20 @@ internal static string AADAuthorityOverrideFound { } /// - /// Looks up a localized string similar to Failed to acquire session token: {0}. - /// - internal static string AcquireSessionTokenFailed { - get { - return ResourceManager.GetString("AcquireSessionTokenFailed", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Failed to acquire token using DeviceFlow. - /// - internal static string AdalAcquireTokenDeviceFlowFailed { - get { - return ResourceManager.GetString("AdalAcquireTokenDeviceFlowFailed", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Acquired token using DeviceFlow. - /// - internal static string AdalAcquireTokenDeviceFlowSuccess { - get { - return ResourceManager.GetString("AdalAcquireTokenDeviceFlowSuccess", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Failed to acquire token from cache. - /// - internal static string AdalAcquireTokenSilentFailed { - get { - return ResourceManager.GetString("AdalAcquireTokenSilentFailed", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Acquired token from ADAL cache. - /// - internal static string AdalAcquireTokenSilentSuccess { - get { - return ResourceManager.GetString("AdalAcquireTokenSilentSuccess", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Failed to acquire token using Windows Integrated Authentication. + /// Looks up a localized string similar to Acquired bearer token using '{0}'. /// - internal static string AdalAcquireTokenWIAFailed { + internal static string AcquireBearerTokenSuccess { get { - return ResourceManager.GetString("AdalAcquireTokenWIAFailed", resourceCulture); + return ResourceManager.GetString("AcquireBearerTokenSuccess", resourceCulture); } } /// - /// Looks up a localized string similar to Acquired token using Windows Integrated Authentication. + /// Looks up a localized string similar to Failed to acquire session token: {0}. /// - internal static string AdalAcquireTokenWIASuccess { + internal static string AcquireSessionTokenFailed { get { - return ResourceManager.GetString("AdalAcquireTokenWIASuccess", resourceCulture); + return ResourceManager.GetString("AcquireSessionTokenFailed", resourceCulture); } } @@ -178,29 +133,38 @@ internal static string AdalFileCacheLocation { } /// - /// Looks up a localized string similar to Could not find ADAL token for {0}. + /// Looks up a localized string similar to Using AAD authority: {0}. /// - internal static string AdalTokenNotFound { + internal static string AdalUsingAuthority { get { - return ResourceManager.GetString("AdalTokenNotFound", resourceCulture); + return ResourceManager.GetString("AdalUsingAuthority", resourceCulture); } } /// - /// Looks up a localized string similar to Using AAD authority: {0}. + /// Looks up a localized string similar to Attempting to acquire bearer token using provider '{0}'. /// - internal static string AdalUsingAuthority { + internal static string AttemptingToAcquireBearerTokenUsingProvider { get { - return ResourceManager.GetString("AdalUsingAuthority", resourceCulture); + return ResourceManager.GetString("AttemptingToAcquireBearerTokenUsingProvider", resourceCulture); } } /// - /// Looks up a localized string similar to Failed to acquire bearer token for VSTSSessionTokenClient. + /// Looks up a localized string similar to Bearer token provider '{0}' failed with exception:\n{1}. /// - internal static string BearerTokenFailed { + internal static string BearerTokenProviderException { get { - return ResourceManager.GetString("BearerTokenFailed", resourceCulture); + return ResourceManager.GetString("BearerTokenProviderException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Bearer token provider '{0}' didn't acquire a token. + /// + internal static string BearerTokenProviderReturnedNull { + get { + return ResourceManager.GetString("BearerTokenProviderReturnedNull", resourceCulture); } } @@ -276,15 +240,6 @@ internal static string CachingSessionToken { } } - /// - /// Looks up a localized string similar to Cannot retry with -IsNonInteractive flag. - /// - internal static string CannotRetryWithNonInteractiveFlag { - get { - return ResourceManager.GetString("CannotRetryWithNonInteractiveFlag", resourceCulture); - } - } - /// /// Looks up a localized string similar to Command-line v{0}: {1}. /// @@ -390,6 +345,15 @@ internal static string EnvironmentVariableHelp { } } + /// + /// Looks up a localized string similar to Attempting to exchange the bearer token for an Azure DevOps session token.. + /// + internal static string ExchangingBearerTokenForSessionToken { + get { + return ResourceManager.GetString("ExchangingBearerTokenForSessionToken", resourceCulture); + } + } + /// /// Looks up a localized string similar to Faulted on message: {0}. /// @@ -480,6 +444,15 @@ internal static string NoEndpointsFound { } } + /// + /// Looks up a localized string similar to Not running bearer token provider '{0}'. + /// + internal static string NotRunningBearerTokenProvider { + get { + return ResourceManager.GetString("NotRunningBearerTokenProvider", resourceCulture); + } + } + /// /// Looks up a localized string similar to Parsing json. /// @@ -625,7 +598,7 @@ internal static string VstsBuildTaskExternalCredentialCredentialProviderError { } /// - /// Looks up a localized string similar to Exception trying to generate VSTS/TFS token: {0}, message: {1}, stack: {2}. + /// Looks up a localized string similar to Exception trying to generate Azure DevOps token: {0}, message: {1}, stack: {2}. /// internal static string VSTSCreateSessionException { get { @@ -634,7 +607,7 @@ internal static string VSTSCreateSessionException { } /// - /// Looks up a localized string similar to Could not find credentials for {0}. + /// Looks up a localized string similar to Could not obtain credentials for {0}. /// internal static string VSTSCredentialsNotFound { get { diff --git a/CredentialProvider.Microsoft/Resources.resx b/CredentialProvider.Microsoft/Resources.resx index e6e163ed..875cd736 100644 --- a/CredentialProvider.Microsoft/Resources.resx +++ b/CredentialProvider.Microsoft/Resources.resx @@ -126,18 +126,6 @@ Failed to acquire session token: {0} - - Failed to acquire token using DeviceFlow - - - Acquired token using DeviceFlow - - - Failed to acquire token from cache - - - Acquired token from ADAL cache - To sign in, use a web browser to open the page {0} and enter the code {1} to authenticate. @@ -150,15 +138,9 @@ ADAL FileCache location: {0} - - Could not find ADAL token for {0} - Using AAD authority: {0} - - Failed to acquire bearer token for VSTSSessionTokenClient - This credential provider must be run under the Team Build tasks for NuGet @@ -180,9 +162,6 @@ Caching SessionToken for {0} - - Cannot retry with -IsNonInteractive flag - Command-line v{0}: {1} @@ -268,10 +247,10 @@ Username - Exception trying to generate VSTS/TFS token: {0}, message: {1}, stack: {2} + Exception trying to generate Azure DevOps token: {0}, message: {1}, stack: {2} - Could not find credentials for {0} + Could not obtain credentials for {0} Found SessionToken for {0} @@ -355,7 +334,12 @@ Cache Location {11} Session Token Cache - {12} + {12} + +Windows Integrated Authentication + {13} + Boolean to enable/disable using silent Windows Integrated Authentication + to authenticate as the logged-in user. Enabled by default. Failed to parse credentials @@ -381,10 +365,22 @@ Cache Location This credential provider must be run under the Team Build tasks for NuGet with external endpoint credentials - - Failed to acquire token using Windows Integrated Authentication + + Acquired bearer token using '{0}' + + + Attempting to acquire bearer token using provider '{0}' + + + Bearer token provider '{0}' failed with exception:\n{1} + + + Bearer token provider '{0}' didn't acquire a token + + + Attempting to exchange the bearer token for an Azure DevOps session token. - - Acquired token using Windows Integrated Authentication + + Not running bearer token provider '{0}' \ No newline at end of file diff --git a/CredentialProvider.Microsoft/Util/EnvUtil.cs b/CredentialProvider.Microsoft/Util/EnvUtil.cs index 0a9aee66..a92f07b7 100644 --- a/CredentialProvider.Microsoft/Util/EnvUtil.cs +++ b/CredentialProvider.Microsoft/Util/EnvUtil.cs @@ -15,6 +15,7 @@ public static class EnvUtil { public const string LogPathEnvVar = "NUGET_CREDENTIALPROVIDER_LOG_PATH"; public const string SessionTokenCacheEnvVar = "NUGET_CREDENTIALPROVIDER_SESSIONTOKENCACHE_ENABLED"; + public const string WindowsIntegratedAuthenticationEnvVar = "NUGET_CREDENTIALPROVIDER_WINDOWSINTEGRATEDAUTHENTICATION_ENABLED"; public const string AuthorityEnvVar = "NUGET_CREDENTIALPROVIDER_ADAL_AUTHORITY"; public const string AdalFileCacheEnvVar = "NUGET_CREDENTIALPROVIDER_ADAL_FILECACHE_ENABLED"; @@ -83,6 +84,11 @@ public static bool SessionTokenCacheEnabled() return GetEnabledFromEnvironment(SessionTokenCacheEnvVar); } + public static bool WindowsIntegratedAuthenticationEnabled() + { + return GetEnabledFromEnvironment(WindowsIntegratedAuthenticationEnvVar); + } + public static TimeSpan? GetSessionTimeFromEnvironment(ILogger logger) { var minutes = Environment.GetEnvironmentVariable(SessionTimeEnvVar); diff --git a/README.md b/README.md index 83999d79..cbb16288 100644 --- a/README.md +++ b/README.md @@ -205,6 +205,11 @@ Cache Location Session Token Cache C:\Users\someuser\AppData\Local\MicrosoftCredentialProvider\SessionTokenCache.dat + +Windows Integrated Authentication + NUGET_CREDENTIALPROVIDER_WINDOWSINTEGRATEDAUTHENTICATION_ENABLED + Boolean to enable/disable using silent Windows Integrated Authentication + to authenticate as the logged-in user. Enabled by default. ``` ## Contribute