diff --git a/sdk/identity/Azure.Identity/CHANGELOG.md b/sdk/identity/Azure.Identity/CHANGELOG.md index 3e1ba120dada2..8840612328120 100644 --- a/sdk/identity/Azure.Identity/CHANGELOG.md +++ b/sdk/identity/Azure.Identity/CHANGELOG.md @@ -1,16 +1,25 @@ # Release History -## 1.3.0-preview.1 (Unreleased) +## 1.3.0-beta.1 (2020-09-11) ### New Features - Restoring Application Authentication APIs from 1.2.0-preview.6 +- Added support for App Service Managed Identity API version `2019-08-01` ([#13687](https://github.com/Azure/azure-sdk-for-net/issues/13687)) - Added `IncludeX5CClaimHeader` to `ClientCertificateCredentialOptions` to enable subject name / issuer authentication with the `ClientCertificateCredential`. - Added `RedirectUri` to `InteractiveBrowserCredentialOptions` to enable authentication with user specified application with a custom redirect url. - Added `IdentityModelFactory` to enable constructing models from the Azure.Identity library for mocking. +- Unify exception handling between `DefaultAzureCredential` and `ChainedTokenCredential` ([#14408](https://github.com/Azure/azure-sdk-for-net/issues/14408)) ### Fixes and improvements +- Updated `MsalPublicClient` and `MsalConfidentialClient` to respect `CancellationToken` during initialization ([#13201](https://github.com/Azure/azure-sdk-for-net/issues/13201)) +- Fixed `VisualStudioCodeCredential` crashes on macOS (Issue [#14362](https://github.com/Azure/azure-sdk-for-net/issues/14362)) - Fixed issue with non GUID Client Ids (Issue [#14585](https://github.com/Azure/azure-sdk-for-net/issues/14585)) - Update `VisualStudioCredential` and `VisualStudioCodeCredential` to throw `CredentialUnavailableException` for ADFS tenant (Issue [#14639](https://github.com/Azure/azure-sdk-for-net/issues/14639)) +## 1.2.3 (2020-09-11) + +### Fixes and improvements +- Fixed issue with `DefaultAzureCredential` incorrectly catching `AuthenticationFailedException` (Issue [#14974](https://github.com/Azure/azure-sdk-for-net/issues/14974)) +- Fixed issue with `DefaultAzureCredential` throwing exceptions during concurrent calls (Issue [#15013](https://github.com/Azure/azure-sdk-for-net/issues/15013)) ## 1.2.2 (2020-08-20) diff --git a/sdk/identity/Azure.Identity/src/AuthenticationFailedException.cs b/sdk/identity/Azure.Identity/src/AuthenticationFailedException.cs index ff11b0eb132b6..1e46658948675 100644 --- a/sdk/identity/Azure.Identity/src/AuthenticationFailedException.cs +++ b/sdk/identity/Azure.Identity/src/AuthenticationFailedException.cs @@ -31,28 +31,5 @@ public AuthenticationFailedException(string message, Exception innerException) : base(message, innerException) { } - - internal static AuthenticationFailedException CreateAggregateException(string message, IList exceptions) - { - // Build the credential unavailable message, this code is only reachable if all credentials throw AuthenticationFailedException - StringBuilder errorMsg = new StringBuilder(message); - - bool allCredentialUnavailableException = true; - foreach (var exception in exceptions) - { - allCredentialUnavailableException &= exception is CredentialUnavailableException; - errorMsg.Append(Environment.NewLine).Append("- ").Append(exception.Message); - } - - var innerException = exceptions.Count == 1 - ? exceptions[0] - : new AggregateException("Multiple exceptions were encountered while attempting to authenticate.", exceptions); - - // If all credentials have thrown CredentialUnavailableException, throw CredentialUnavailableException, - // otherwise throw AuthenticationFailedException - return allCredentialUnavailableException - ? new CredentialUnavailableException(errorMsg.ToString(), innerException) - : new AuthenticationFailedException(errorMsg.ToString(), innerException); - } } } diff --git a/sdk/identity/Azure.Identity/src/Azure.Identity.csproj b/sdk/identity/Azure.Identity/src/Azure.Identity.csproj index c34249f38254a..88478f603d284 100644 --- a/sdk/identity/Azure.Identity/src/Azure.Identity.csproj +++ b/sdk/identity/Azure.Identity/src/Azure.Identity.csproj @@ -2,7 +2,7 @@ This is the implementation of the Azure SDK Client Library for Azure Identity Microsoft Azure.Identity Component - 1.3.0-preview.1 + 1.3.0-beta.1 1.2.2 Microsoft Azure Identity;$(PackageCommonTags) $(RequiredTargetFrameworks) diff --git a/sdk/identity/Azure.Identity/src/ChainedTokenCredential.cs b/sdk/identity/Azure.Identity/src/ChainedTokenCredential.cs index 8d694878cd865..d3df121fc6a24 100644 --- a/sdk/identity/Azure.Identity/src/ChainedTokenCredential.cs +++ b/sdk/identity/Azure.Identity/src/ChainedTokenCredential.cs @@ -18,7 +18,7 @@ public class ChainedTokenCredential : TokenCredential { private const string AggregateAllUnavailableErrorMessage = "The ChainedTokenCredential failed to retrieve a token from the included credentials."; - private const string AggregateCredentialFailedErrorMessage = "The ChainedTokenCredential failed due to an unhandled exception: "; + private const string AuthenticationFailedErrorMessage = "The ChainedTokenCredential failed due to an unhandled exception: "; private readonly TokenCredential[] _sources; @@ -77,7 +77,7 @@ private async ValueTask GetTokenImplAsync(bool async, TokenRequestC var groupScopeHandler = new ScopeGroupHandler(default); try { - List exceptions = new List(); + List exceptions = new List(); foreach (TokenCredential source in _sources) { try @@ -88,18 +88,17 @@ private async ValueTask GetTokenImplAsync(bool async, TokenRequestC groupScopeHandler.Dispose(default, default); return token; } - catch (AuthenticationFailedException e) + catch (CredentialUnavailableException e) { exceptions.Add(e); } - catch (Exception e) when (!(e is OperationCanceledException)) + catch (Exception e) when (!cancellationToken.IsCancellationRequested) { - exceptions.Add(e); - throw AuthenticationFailedException.CreateAggregateException(AggregateCredentialFailedErrorMessage + e.Message, exceptions); + throw new AuthenticationFailedException(AuthenticationFailedErrorMessage + e.Message, e); } } - throw AuthenticationFailedException.CreateAggregateException(AggregateAllUnavailableErrorMessage, exceptions); + throw CredentialUnavailableException.CreateAggregateException(AggregateAllUnavailableErrorMessage, exceptions); } catch (Exception exception) { diff --git a/sdk/identity/Azure.Identity/src/CredentialUnavailableException.cs b/sdk/identity/Azure.Identity/src/CredentialUnavailableException.cs index 5e24bec50be3f..f3d0d8d764eb0 100644 --- a/sdk/identity/Azure.Identity/src/CredentialUnavailableException.cs +++ b/sdk/identity/Azure.Identity/src/CredentialUnavailableException.cs @@ -2,6 +2,8 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; +using System.Text; using Azure.Core; namespace Azure.Identity @@ -31,5 +33,24 @@ public CredentialUnavailableException(string message, Exception innerException) : base(message, innerException) { } + + internal static CredentialUnavailableException CreateAggregateException(string message, IList exceptions) + { + if (exceptions.Count == 1) + { + return exceptions[0]; + } + + // Build the credential unavailable message, this code is only reachable if all credentials throw AuthenticationFailedException + StringBuilder errorMsg = new StringBuilder(message); + + foreach (var exception in exceptions) + { + errorMsg.Append(Environment.NewLine).Append("- ").Append(exception.Message); + } + + var innerException = new AggregateException("Multiple exceptions were encountered while attempting to authenticate.", exceptions); + return new CredentialUnavailableException(errorMsg.ToString(), innerException); + } } } diff --git a/sdk/identity/Azure.Identity/src/DefaultAzureCredential.cs b/sdk/identity/Azure.Identity/src/DefaultAzureCredential.cs index 3137d61a04835..19e3438c7beb5 100644 --- a/sdk/identity/Azure.Identity/src/DefaultAzureCredential.cs +++ b/sdk/identity/Azure.Identity/src/DefaultAzureCredential.cs @@ -143,7 +143,7 @@ private static async ValueTask GetTokenFromCredentialAsync(TokenCre private static async ValueTask<(AccessToken, TokenCredential)> GetTokenFromSourcesAsync(TokenCredential[] sources, TokenRequestContext requestContext, bool async, CancellationToken cancellationToken) { - List exceptions = new List(); + List exceptions = new List(); for (var i = 0; i < sources.Length && sources[i] != null; i++) { @@ -155,18 +155,13 @@ private static async ValueTask GetTokenFromCredentialAsync(TokenCre return (token, sources[i]); } - catch (AuthenticationFailedException e) + catch (CredentialUnavailableException e) { exceptions.Add(e); } - catch (Exception e) when (!(e is OperationCanceledException)) - { - exceptions.Add(e); - throw AuthenticationFailedException.CreateAggregateException(UnhandledExceptionMessage + e.Message, exceptions); - } } - throw AuthenticationFailedException.CreateAggregateException(DefaultExceptionMessage, exceptions); + throw CredentialUnavailableException.CreateAggregateException(DefaultExceptionMessage, exceptions); } private static TokenCredential[] GetDefaultAzureCredentialChain(DefaultAzureCredentialFactory factory, DefaultAzureCredentialOptions options) diff --git a/sdk/identity/Azure.Identity/src/VisualStudioCodeCredential.cs b/sdk/identity/Azure.Identity/src/VisualStudioCodeCredential.cs index ef29cb99b82e5..bc30aca0a76f4 100644 --- a/sdk/identity/Azure.Identity/src/VisualStudioCodeCredential.cs +++ b/sdk/identity/Azure.Identity/src/VisualStudioCodeCredential.cs @@ -69,12 +69,7 @@ private async ValueTask GetTokenImplAsync(TokenRequestContext reque } var cloudInstance = GetAzureCloudInstance(environmentName); - var storedCredentials = _vscAdapter.GetCredentials(CredentialsSection, environmentName); - - if (!IsRefreshTokenString(storedCredentials)) - { - throw new CredentialUnavailableException("Need to re-authenticate user in VSCode Azure Account."); - } + string storedCredentials = GetStoredCredentials(environmentName); var result = await _client.AcquireTokenByRefreshToken(requestContext.Scopes, storedCredentials, cloudInstance, tenant, async, cancellationToken).ConfigureAwait(false); return scope.Succeeded(new AccessToken(result.AccessToken, result.ExpiresOn)); @@ -89,6 +84,24 @@ private async ValueTask GetTokenImplAsync(TokenRequestContext reque } } + private string GetStoredCredentials(string environmentName) + { + try + { + var storedCredentials = _vscAdapter.GetCredentials(CredentialsSection, environmentName); + if (!IsRefreshTokenString(storedCredentials)) + { + throw new CredentialUnavailableException("Need to re-authenticate user in VSCode Azure Account."); + } + + return storedCredentials; + } + catch (InvalidOperationException ex) + { + throw new CredentialUnavailableException("Stored credentials not found. Need to authenticate user in VSCode Azure Account.", ex); + } + } + private static bool IsRefreshTokenString(string str) { for (var index = 0; index < str.Length; index++) diff --git a/sdk/identity/Azure.Identity/src/VisualStudioCredential.cs b/sdk/identity/Azure.Identity/src/VisualStudioCredential.cs index d34a6e1df598a..c8831cd2dfff4 100644 --- a/sdk/identity/Azure.Identity/src/VisualStudioCredential.cs +++ b/sdk/identity/Azure.Identity/src/VisualStudioCredential.cs @@ -125,9 +125,9 @@ private async Task RunProcessesAsync(List process { exceptions.Add(new CredentialUnavailableException($"Process \"{processStartInfo.FileName}\" has non-json output: {output}.", exception)); } - catch (Exception exception) + catch (Exception exception) when (!(exception is OperationCanceledException)) { - exceptions.Add(exception); + exceptions.Add(new CredentialUnavailableException($"Process \"{processStartInfo.FileName}\" has failed with unexpected error: {exception.Message}.", exception)); } } @@ -192,24 +192,35 @@ private VisualStudioTokenProvider[] GetTokenProviders(string tokenProviderPath) { var content = GetTokenProviderContent(tokenProviderPath); - using JsonDocument document = JsonDocument.Parse(content); + try + { + using JsonDocument document = JsonDocument.Parse(content); - JsonElement providersElement = document.RootElement.GetProperty("TokenProviders"); + JsonElement providersElement = document.RootElement.GetProperty("TokenProviders"); - var providers = new VisualStudioTokenProvider[providersElement.GetArrayLength()]; - for (int i = 0; i < providers.Length; i++) - { - JsonElement providerElement = providersElement[i]; + var providers = new VisualStudioTokenProvider[providersElement.GetArrayLength()]; + for (int i = 0; i < providers.Length; i++) + { + JsonElement providerElement = providersElement[i]; - var path = providerElement.GetProperty("Path").GetString(); - var preference = providerElement.GetProperty("Preference").GetInt32(); - var arguments = GetStringArrayPropertyValue(providerElement, "Arguments"); + var path = providerElement.GetProperty("Path").GetString(); + var preference = providerElement.GetProperty("Preference").GetInt32(); + var arguments = GetStringArrayPropertyValue(providerElement, "Arguments"); - providers[i] = new VisualStudioTokenProvider(path, arguments, preference); - } + providers[i] = new VisualStudioTokenProvider(path, arguments, preference); + } - Array.Sort(providers); - return providers; + Array.Sort(providers); + return providers; + } + catch (JsonException exception) + { + throw new CredentialUnavailableException($"File found at \"{tokenProviderPath}\" isn't a valid JSON file", exception); + } + catch (Exception exception) + { + throw new CredentialUnavailableException($"JSON file found at \"{tokenProviderPath}\" has invalid schema.", exception); + } } private string GetTokenProviderContent(string tokenProviderPath) diff --git a/sdk/identity/Azure.Identity/tests/AzureCliCredentialTests.cs b/sdk/identity/Azure.Identity/tests/AzureCliCredentialTests.cs index a0b5ba2972139..54d469f9bfdc2 100644 --- a/sdk/identity/Azure.Identity/tests/AzureCliCredentialTests.cs +++ b/sdk/identity/Azure.Identity/tests/AzureCliCredentialTests.cs @@ -47,7 +47,7 @@ public void AuthenticateWithCliCredential_InvalidJsonOutput([Values("", "{}", "{ { var testProcess = new TestProcess { Output = jsonContent }; AzureCliCredential credential = InstrumentClient(new AzureCliCredential(CredentialPipeline.GetInstance(null), new TestProcessService(testProcess))); - Assert.CatchAsync(async () => await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default))); + Assert.ThrowsAsync(async () => await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default))); } [Test] diff --git a/sdk/identity/Azure.Identity/tests/ChainedTokenCredentialLiveTests.cs b/sdk/identity/Azure.Identity/tests/ChainedTokenCredentialLiveTests.cs index 6974a4639be40..8b3f2aa96c542 100644 --- a/sdk/identity/Azure.Identity/tests/ChainedTokenCredentialLiveTests.cs +++ b/sdk/identity/Azure.Identity/tests/ChainedTokenCredentialLiveTests.cs @@ -57,7 +57,7 @@ public async Task ChainedTokenCredential_UseVisualStudioCredential() } [Test] - [RunOnlyOnPlatforms(Windows = true, OSX = true, ContainerNames = new[] { "ubuntu_netcore2_keyring" })] + [RunOnlyOnPlatforms(Windows = true, OSX = true, ContainerNames = new[] { "ubuntu_netcore_keyring" })] public async Task ChainedTokenCredential_UseVisualStudioCodeCredential() { var cloudName = Guid.NewGuid().ToString(); @@ -89,7 +89,7 @@ public async Task ChainedTokenCredential_UseVisualStudioCodeCredential() } [Test] - [RunOnlyOnPlatforms(Windows = true, OSX = true, ContainerNames = new[] { "ubuntu_netcore2_keyring" })] + [RunOnlyOnPlatforms(Windows = true, OSX = true, ContainerNames = new[] { "ubuntu_netcore_keyring" })] public async Task ChainedTokenCredential_UseVisualStudioCodeCredential_ParallelCalls() { var cloudName = Guid.NewGuid().ToString(); @@ -209,7 +209,35 @@ public void ChainedTokenCredential_AllCredentialsHaveFailed_CredentialUnavailabl } [Test] - public void ChainedTokenCredential_AllCredentialsHaveFailed_AuthenticationFailedException() + [NonParallelizable] + public void ChainedTokenCredential_AllCredentialsHaveFailed_FirstAuthenticationFailedException() + { + using var endpoint = new TestEnvVar("MSI_ENDPOINT", "abc"); + + var vscAdapter = new TestVscAdapter(ExpectedServiceName, "Azure", null); + var fileSystem = new TestFileSystemService(); + var processService = new TestProcessService(new TestProcess {Error = "Error"}); + + var miCredential = new ManagedIdentityCredential(EnvironmentVariables.ClientId); + var vsCredential = new VisualStudioCredential(default, default, fileSystem, processService); + var vscCredential = new VisualStudioCodeCredential(new VisualStudioCodeCredentialOptions { TenantId = TestEnvironment.TestTenantId }, default, default, fileSystem, vscAdapter); + var azureCliCredential = new AzureCliCredential(CredentialPipeline.GetInstance(null), processService); + + var credential = InstrumentClient(new ChainedTokenCredential(miCredential, vsCredential, vscCredential, azureCliCredential)); + + List scopes; + using (ClientDiagnosticListener diagnosticListener = new ClientDiagnosticListener(s => s.StartsWith("Azure.Identity"))) + { + Assert.CatchAsync(async () => await credential.GetTokenAsync(new TokenRequestContext(new[] {"https://vault.azure.net/.default"}), CancellationToken.None)); + scopes = diagnosticListener.Scopes; + } + + Assert.AreEqual(1, scopes.Count); + Assert.AreEqual($"{nameof(ManagedIdentityCredential)}.{nameof(ManagedIdentityCredential.GetToken)}", scopes[0].Name); + } + + [Test] + public void ChainedTokenCredential_AllCredentialsHaveFailed_LastAuthenticationFailedException() { var vscAdapter = new TestVscAdapter(ExpectedServiceName, "Azure", null); var fileSystem = new TestFileSystemService(); diff --git a/sdk/identity/Azure.Identity/tests/DefaultAzureCredentialLiveTests.cs b/sdk/identity/Azure.Identity/tests/DefaultAzureCredentialLiveTests.cs index eeca8c1e170b9..1b06db90d9b63 100644 --- a/sdk/identity/Azure.Identity/tests/DefaultAzureCredentialLiveTests.cs +++ b/sdk/identity/Azure.Identity/tests/DefaultAzureCredentialLiveTests.cs @@ -44,7 +44,7 @@ public async Task DefaultAzureCredential_UseVisualStudioCredential() var (expectedToken, expectedExpiresOn, processOutput) = CredentialTestHelpers.CreateTokenForVisualStudio(); var testProcess = new TestProcess { Output = processOutput }; - var factory = new TestDefaultAzureCredentialFactory(options, fileSystem, new TestProcessService(testProcess), default); + var factory = new TestDefaultAzureCredentialFactory(options, fileSystem, new TestProcessService(testProcess), default) { ManagedIdentitySourceFactory = () => default }; var credential = InstrumentClient(new DefaultAzureCredential(factory, options)); AccessToken token; @@ -65,7 +65,7 @@ public async Task DefaultAzureCredential_UseVisualStudioCredential() } [Test] - [RunOnlyOnPlatforms(Windows = true, OSX = true, ContainerNames = new[] { "ubuntu_netcore2_keyring" })] + [RunOnlyOnPlatforms(Windows = true, OSX = true, ContainerNames = new[] { "ubuntu_netcore_keyring" })] public async Task DefaultAzureCredential_UseVisualStudioCodeCredential() { var options = Recording.InstrumentClientOptions(new DefaultAzureCredentialOptions @@ -80,7 +80,7 @@ public async Task DefaultAzureCredential_UseVisualStudioCodeCredential() var fileSystem = CredentialTestHelpers.CreateFileSystemForVisualStudioCode(TestEnvironment, cloudName); var process = new TestProcess { Error = "Error" }; - var factory = new TestDefaultAzureCredentialFactory(options, fileSystem, new TestProcessService(process), default); + var factory = new TestDefaultAzureCredentialFactory(options, fileSystem, new TestProcessService(process), default) { ManagedIdentitySourceFactory = () => default }; var credential = InstrumentClient(new DefaultAzureCredential(factory, options)); AccessToken token; @@ -101,7 +101,7 @@ public async Task DefaultAzureCredential_UseVisualStudioCodeCredential() } [Test] - [RunOnlyOnPlatforms(Windows = true, OSX = true, ContainerNames = new[] { "ubuntu_netcore2_keyring" })] + [RunOnlyOnPlatforms(Windows = true, OSX = true, ContainerNames = new[] { "ubuntu_netcore_keyring" })] public async Task DefaultAzureCredential_UseVisualStudioCodeCredential_ParallelCalls() { var options = Recording.InstrumentClientOptions(new DefaultAzureCredentialOptions @@ -116,7 +116,7 @@ public async Task DefaultAzureCredential_UseVisualStudioCodeCredential_ParallelC var fileSystem = CredentialTestHelpers.CreateFileSystemForVisualStudioCode(TestEnvironment, cloudName); var processService = new TestProcessService { CreateHandler = psi => new TestProcess { Error = "Error" }}; - var factory = new TestDefaultAzureCredentialFactory(options, fileSystem, processService, default); + var factory = new TestDefaultAzureCredentialFactory(options, fileSystem, processService, default) { ManagedIdentitySourceFactory = () => default }; var credential = InstrumentClient(new DefaultAzureCredential(factory, options)); var tasks = new List>(); @@ -152,7 +152,7 @@ public async Task DefaultAzureCredential_UseAzureCliCredential() var vscAdapter = new TestVscAdapter(ExpectedServiceName, "Azure", null); var fileSystem = CredentialTestHelpers.CreateFileSystemForVisualStudioCode(TestEnvironment); - var factory = new TestDefaultAzureCredentialFactory(options, fileSystem, new TestProcessService(testProcess), vscAdapter); + var factory = new TestDefaultAzureCredentialFactory(options, fileSystem, new TestProcessService(testProcess), vscAdapter) { ManagedIdentitySourceFactory = () => default }; var credential = InstrumentClient(new DefaultAzureCredential(factory, options)); AccessToken token; @@ -188,7 +188,7 @@ public async Task DefaultAzureCredential_UseAzureCliCredential_ParallelCalls() var vscAdapter = new TestVscAdapter(ExpectedServiceName, "Azure", null); var fileSystem = CredentialTestHelpers.CreateFileSystemForVisualStudioCode(TestEnvironment); - var factory = new TestDefaultAzureCredentialFactory(options, fileSystem, processService, vscAdapter); + var factory = new TestDefaultAzureCredentialFactory(options, fileSystem, processService, vscAdapter) { ManagedIdentitySourceFactory = () => default }; var credential = InstrumentClient(new DefaultAzureCredential(factory, options)); var tasks = new List>(); @@ -218,7 +218,7 @@ public void DefaultAzureCredential_AllCredentialsHaveFailed_CredentialUnavailabl }); var vscAdapter = new TestVscAdapter(ExpectedServiceName, "Azure", "{}"); - var factory = new TestDefaultAzureCredentialFactory(options, new TestFileSystemService(), new TestProcessService(new TestProcess { Error = "'az' is not recognized" }), vscAdapter); + var factory = new TestDefaultAzureCredentialFactory(options, new TestFileSystemService(), new TestProcessService(new TestProcess { Error = "'az' is not recognized" }), vscAdapter) { ManagedIdentitySourceFactory = () => default }; var credential = InstrumentClient(new DefaultAzureCredential(factory, options)); List scopes; @@ -237,7 +237,7 @@ public void DefaultAzureCredential_AllCredentialsHaveFailed_CredentialUnavailabl } [Test] - public void DefaultAzureCredential_AllCredentialsHaveFailed_AuthenticationFailedException() + public void DefaultAzureCredential_AllCredentialsHaveFailed_FirstAuthenticationFailedException() { var options = Recording.InstrumentClientOptions(new DefaultAzureCredentialOptions { @@ -247,7 +247,34 @@ public void DefaultAzureCredential_AllCredentialsHaveFailed_AuthenticationFailed }); var vscAdapter = new TestVscAdapter(ExpectedServiceName, "Azure", null); - var factory = new TestDefaultAzureCredentialFactory(options, new TestFileSystemService(), new TestProcessService(new TestProcess { Error = "Error" }), vscAdapter); + var factory = new TestDefaultAzureCredentialFactory(options, new TestFileSystemService(), new TestProcessService(new TestProcess { Error = "Error" }), vscAdapter) { ManagedIdentitySourceFactory = () => throw new InvalidOperationException() }; + var credential = InstrumentClient(new DefaultAzureCredential(factory, options)); + + List scopes; + + using (ClientDiagnosticListener diagnosticListener = new ClientDiagnosticListener(s => s.StartsWith("Azure.Identity"))) + { + Assert.CatchAsync(async () => await credential.GetTokenAsync(new TokenRequestContext(new[] {"https://vault.azure.net/.default"}), CancellationToken.None)); + scopes = diagnosticListener.Scopes; + } + + Assert.AreEqual(2, scopes.Count); + Assert.AreEqual($"{nameof(DefaultAzureCredential)}.{nameof(DefaultAzureCredential.GetToken)}", scopes[0].Name); + Assert.AreEqual($"{nameof(ManagedIdentityCredential)}.{nameof(ManagedIdentityCredential.GetToken)}", scopes[1].Name); + } + + [Test] + public void DefaultAzureCredential_AllCredentialsHaveFailed_LastAuthenticationFailedException() + { + var options = Recording.InstrumentClientOptions(new DefaultAzureCredentialOptions + { + ExcludeEnvironmentCredential = true, + ExcludeInteractiveBrowserCredential = true, + ExcludeSharedTokenCacheCredential = true, + }); + + var vscAdapter = new TestVscAdapter(ExpectedServiceName, "Azure", null); + var factory = new TestDefaultAzureCredentialFactory(options, new TestFileSystemService(), new TestProcessService(new TestProcess { Error = "Error" }), vscAdapter) { ManagedIdentitySourceFactory = () => default }; var credential = InstrumentClient(new DefaultAzureCredential(factory, options)); List scopes; diff --git a/sdk/identity/Azure.Identity/tests/ManagedIdentityCredentialImdsLiveTests.cs b/sdk/identity/Azure.Identity/tests/ManagedIdentityCredentialImdsLiveTests.cs index 49731f8b225fd..386d2c26ce10d 100644 --- a/sdk/identity/Azure.Identity/tests/ManagedIdentityCredentialImdsLiveTests.cs +++ b/sdk/identity/Azure.Identity/tests/ManagedIdentityCredentialImdsLiveTests.cs @@ -78,7 +78,7 @@ private ManagedIdentityCredential CreateManagedIdentityCredential(string clientI // if we're in playback mode we need to mock the ImdsAvailable call since we won't be able to open a connection var client = (Mode == RecordedTestMode.Playback) - ? new MockManagedIdentityClient(pipeline, clientId) { AuthRequestBuilderFactory = () => new ImdsManagedIdentitySource(pipeline.HttpPipeline, clientId) } + ? new MockManagedIdentityClient(pipeline, clientId) { ManagedIdentitySourceFactory = () => new ImdsManagedIdentitySource(pipeline.HttpPipeline, clientId) } : new ManagedIdentityClient(pipeline, clientId); var cred = new ManagedIdentityCredential(pipeline, client); diff --git a/sdk/identity/Azure.Identity/tests/ManagedIdentityCredentialTests.cs b/sdk/identity/Azure.Identity/tests/ManagedIdentityCredentialTests.cs index 31765c45df7ad..5338f0c07d6bc 100644 --- a/sdk/identity/Azure.Identity/tests/ManagedIdentityCredentialTests.cs +++ b/sdk/identity/Azure.Identity/tests/ManagedIdentityCredentialTests.cs @@ -39,7 +39,7 @@ public async Task VerifyImdsRequestMockAsync() var pipeline = CredentialPipeline.GetInstance(options); - var client = new MockManagedIdentityClient(pipeline, "mock-client-id") { AuthRequestBuilderFactory = () => new ImdsManagedIdentitySource(pipeline.HttpPipeline, "mock-client-id") }; + var client = new MockManagedIdentityClient(pipeline, "mock-client-id") { ManagedIdentitySourceFactory = () => new ImdsManagedIdentitySource(pipeline.HttpPipeline, "mock-client-id") }; ManagedIdentityCredential credential = InstrumentClient(new ManagedIdentityCredential(pipeline, client)); @@ -80,7 +80,7 @@ public async Task VerifyImdsRequestWithClientIdMockAsync() var pipeline = CredentialPipeline.GetInstance(options); - var client = new MockManagedIdentityClient(pipeline, "mock-client-id") { AuthRequestBuilderFactory = () => new ImdsManagedIdentitySource(pipeline.HttpPipeline, "mock-client-id") }; + var client = new MockManagedIdentityClient(pipeline, "mock-client-id") { ManagedIdentitySourceFactory = () => new ImdsManagedIdentitySource(pipeline.HttpPipeline, "mock-client-id") }; ManagedIdentityCredential credential = InstrumentClient(new ManagedIdentityCredential(pipeline, client)); @@ -342,7 +342,7 @@ public async Task VerifyCloudShellMsiRequestWithClientIdMockAsync() [Test] public async Task VerifyMsiUnavailableCredentialException() { - var mockClient = new MockManagedIdentityClient { AuthRequestBuilderFactory = () => default }; + var mockClient = new MockManagedIdentityClient { ManagedIdentitySourceFactory = () => default }; var credential = InstrumentClient(new ManagedIdentityCredential(CredentialPipeline.GetInstance(null), mockClient)); @@ -356,7 +356,7 @@ public async Task VerifyMsiUnavailableCredentialException() [Test] public async Task VerifyClientGetMsiTypeThrows() { - var mockClient = new MockManagedIdentityClient { AuthRequestBuilderFactory = () => throw new MockClientException("message") }; + var mockClient = new MockManagedIdentityClient { ManagedIdentitySourceFactory = () => throw new MockClientException("message") }; var credential = InstrumentClient(new ManagedIdentityCredential(CredentialPipeline.GetInstance(null), mockClient)); @@ -370,7 +370,7 @@ public async Task VerifyClientGetMsiTypeThrows() [Test] public async Task VerifyClientAuthenticateThrows() { - var mockClient = new MockManagedIdentityClient { AuthRequestBuilderFactory = () => new ImdsManagedIdentitySource(default, default), TokenFactory = () => throw new MockClientException("message") }; + var mockClient = new MockManagedIdentityClient { ManagedIdentitySourceFactory = () => new ImdsManagedIdentitySource(default, default), TokenFactory = () => throw new MockClientException("message") }; var credential = InstrumentClient(new ManagedIdentityCredential(CredentialPipeline.GetInstance(null), mockClient)); diff --git a/sdk/identity/Azure.Identity/tests/Mock/MockManagedIdentityClient.cs b/sdk/identity/Azure.Identity/tests/Mock/MockManagedIdentityClient.cs index 0410ba8057930..6a11fc7a2f764 100644 --- a/sdk/identity/Azure.Identity/tests/Mock/MockManagedIdentityClient.cs +++ b/sdk/identity/Azure.Identity/tests/Mock/MockManagedIdentityClient.cs @@ -27,7 +27,7 @@ public MockManagedIdentityClient(CredentialPipeline pipeline, string clientId) { } - public Func AuthRequestBuilderFactory { get; set; } + public Func ManagedIdentitySourceFactory { get; set; } public Func TokenFactory { get; set; } @@ -35,6 +35,6 @@ public override ValueTask AuthenticateAsync(bool async, string[] sc => TokenFactory != null ? new ValueTask(TokenFactory()) : base.AuthenticateAsync(async, scopes, cancellationToken); private protected override ValueTask GetManagedIdentitySourceAsync(bool async, CancellationToken cancellationToken) - => AuthRequestBuilderFactory != null ? new ValueTask(AuthRequestBuilderFactory()) : base.GetManagedIdentitySourceAsync(async, cancellationToken); + => ManagedIdentitySourceFactory != null ? new ValueTask(ManagedIdentitySourceFactory()) : base.GetManagedIdentitySourceAsync(async, cancellationToken); } } diff --git a/sdk/identity/Azure.Identity/tests/Mock/TestDefaultAzureCredentialFactory.cs b/sdk/identity/Azure.Identity/tests/Mock/TestDefaultAzureCredentialFactory.cs index 36f48506379af..d51bab3983a7c 100644 --- a/sdk/identity/Azure.Identity/tests/Mock/TestDefaultAzureCredentialFactory.cs +++ b/sdk/identity/Azure.Identity/tests/Mock/TestDefaultAzureCredentialFactory.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using Azure.Core; namespace Azure.Identity.Tests.Mock @@ -19,11 +20,13 @@ public TestDefaultAzureCredentialFactory(TokenCredentialOptions options, IFileSy _vscAdapter = vscAdapter; } + public Func ManagedIdentitySourceFactory { get; set; } + public override TokenCredential CreateEnvironmentCredential() => new EnvironmentCredential(Pipeline); public override TokenCredential CreateManagedIdentityCredential(string clientId) - => new ManagedIdentityCredential(Pipeline, new ManagedIdentityClient(Pipeline, clientId)); + => new ManagedIdentityCredential(Pipeline, CreateManagedIdentityClient(clientId)); public override TokenCredential CreateSharedTokenCacheCredential(string tenantId, string username) => new SharedTokenCacheCredential(tenantId, username, default, Pipeline); @@ -39,5 +42,10 @@ public override TokenCredential CreateVisualStudioCredential(string tenantId) public override TokenCredential CreateVisualStudioCodeCredential(string tenantId) => new VisualStudioCodeCredential(new VisualStudioCodeCredentialOptions { TenantId = tenantId }, Pipeline, default, _fileSystem, _vscAdapter); + + private ManagedIdentityClient CreateManagedIdentityClient(string clientId) + => ManagedIdentitySourceFactory != default + ? new MockManagedIdentityClient(Pipeline, clientId) { ManagedIdentitySourceFactory = ManagedIdentitySourceFactory } + : new ManagedIdentityClient(Pipeline, clientId); } } diff --git a/sdk/identity/Azure.Identity/tests/SessionRecords/ChainedTokenCredentialLiveTests/ChainedTokenCredential_AllCredentialsHaveFailed_AuthenticationFailedException.json b/sdk/identity/Azure.Identity/tests/SessionRecords/ChainedTokenCredentialLiveTests/ChainedTokenCredential_AllCredentialsHaveFailed_FirstAuthenticationFailedException.json similarity index 100% rename from sdk/identity/Azure.Identity/tests/SessionRecords/ChainedTokenCredentialLiveTests/ChainedTokenCredential_AllCredentialsHaveFailed_AuthenticationFailedException.json rename to sdk/identity/Azure.Identity/tests/SessionRecords/ChainedTokenCredentialLiveTests/ChainedTokenCredential_AllCredentialsHaveFailed_FirstAuthenticationFailedException.json diff --git a/sdk/identity/Azure.Identity/tests/SessionRecords/ChainedTokenCredentialLiveTests/ChainedTokenCredential_AllCredentialsHaveFailed_AuthenticationFailedExceptionAsync.json b/sdk/identity/Azure.Identity/tests/SessionRecords/ChainedTokenCredentialLiveTests/ChainedTokenCredential_AllCredentialsHaveFailed_FirstAuthenticationFailedExceptionAsync.json similarity index 100% rename from sdk/identity/Azure.Identity/tests/SessionRecords/ChainedTokenCredentialLiveTests/ChainedTokenCredential_AllCredentialsHaveFailed_AuthenticationFailedExceptionAsync.json rename to sdk/identity/Azure.Identity/tests/SessionRecords/ChainedTokenCredentialLiveTests/ChainedTokenCredential_AllCredentialsHaveFailed_FirstAuthenticationFailedExceptionAsync.json diff --git a/sdk/identity/Azure.Identity/tests/SessionRecords/ChainedTokenCredentialLiveTests/ChainedTokenCredential_AllCredentialsHaveFailed_LastAuthenticationFailedException.json b/sdk/identity/Azure.Identity/tests/SessionRecords/ChainedTokenCredentialLiveTests/ChainedTokenCredential_AllCredentialsHaveFailed_LastAuthenticationFailedException.json new file mode 100644 index 0000000000000..f144ab1b86887 --- /dev/null +++ b/sdk/identity/Azure.Identity/tests/SessionRecords/ChainedTokenCredentialLiveTests/ChainedTokenCredential_AllCredentialsHaveFailed_LastAuthenticationFailedException.json @@ -0,0 +1,7 @@ +{ + "Entries": [], + "Variables": { + "AZURE_IDENTITY_TEST_TENANTID": "c54fac88-3dd3-461f-a7c4-8a368e0340b3", + "TENANT_ID": null + } +} \ No newline at end of file diff --git a/sdk/identity/Azure.Identity/tests/SessionRecords/ChainedTokenCredentialLiveTests/ChainedTokenCredential_AllCredentialsHaveFailed_LastAuthenticationFailedExceptionAsync.json b/sdk/identity/Azure.Identity/tests/SessionRecords/ChainedTokenCredentialLiveTests/ChainedTokenCredential_AllCredentialsHaveFailed_LastAuthenticationFailedExceptionAsync.json new file mode 100644 index 0000000000000..f144ab1b86887 --- /dev/null +++ b/sdk/identity/Azure.Identity/tests/SessionRecords/ChainedTokenCredentialLiveTests/ChainedTokenCredential_AllCredentialsHaveFailed_LastAuthenticationFailedExceptionAsync.json @@ -0,0 +1,7 @@ +{ + "Entries": [], + "Variables": { + "AZURE_IDENTITY_TEST_TENANTID": "c54fac88-3dd3-461f-a7c4-8a368e0340b3", + "TENANT_ID": null + } +} \ No newline at end of file diff --git a/sdk/identity/Azure.Identity/tests/SessionRecords/DefaultAzureCredentialLiveTests/DefaultAzureCredential_AllCredentialsHaveFailed_FirstAuthenticationFailedException.json b/sdk/identity/Azure.Identity/tests/SessionRecords/DefaultAzureCredentialLiveTests/DefaultAzureCredential_AllCredentialsHaveFailed_FirstAuthenticationFailedException.json new file mode 100644 index 0000000000000..73dc9938e4e82 --- /dev/null +++ b/sdk/identity/Azure.Identity/tests/SessionRecords/DefaultAzureCredentialLiveTests/DefaultAzureCredential_AllCredentialsHaveFailed_FirstAuthenticationFailedException.json @@ -0,0 +1,6 @@ +{ + "Entries": [], + "Variables": { + "RandomSeed": "1827883678" + } +} \ No newline at end of file diff --git a/sdk/identity/Azure.Identity/tests/SessionRecords/DefaultAzureCredentialLiveTests/DefaultAzureCredential_AllCredentialsHaveFailed_FirstAuthenticationFailedExceptionAsync.json b/sdk/identity/Azure.Identity/tests/SessionRecords/DefaultAzureCredentialLiveTests/DefaultAzureCredential_AllCredentialsHaveFailed_FirstAuthenticationFailedExceptionAsync.json new file mode 100644 index 0000000000000..82d73aeeb670e --- /dev/null +++ b/sdk/identity/Azure.Identity/tests/SessionRecords/DefaultAzureCredentialLiveTests/DefaultAzureCredential_AllCredentialsHaveFailed_FirstAuthenticationFailedExceptionAsync.json @@ -0,0 +1,6 @@ +{ + "Entries": [], + "Variables": { + "RandomSeed": "130164589" + } +} \ No newline at end of file diff --git a/sdk/identity/Azure.Identity/tests/SessionRecords/DefaultAzureCredentialLiveTests/DefaultAzureCredential_AllCredentialsHaveFailed_AuthenticationFailedException.json b/sdk/identity/Azure.Identity/tests/SessionRecords/DefaultAzureCredentialLiveTests/DefaultAzureCredential_AllCredentialsHaveFailed_LastAuthenticationFailedException.json similarity index 100% rename from sdk/identity/Azure.Identity/tests/SessionRecords/DefaultAzureCredentialLiveTests/DefaultAzureCredential_AllCredentialsHaveFailed_AuthenticationFailedException.json rename to sdk/identity/Azure.Identity/tests/SessionRecords/DefaultAzureCredentialLiveTests/DefaultAzureCredential_AllCredentialsHaveFailed_LastAuthenticationFailedException.json diff --git a/sdk/identity/Azure.Identity/tests/SessionRecords/DefaultAzureCredentialLiveTests/DefaultAzureCredential_AllCredentialsHaveFailed_AuthenticationFailedExceptionAsync.json b/sdk/identity/Azure.Identity/tests/SessionRecords/DefaultAzureCredentialLiveTests/DefaultAzureCredential_AllCredentialsHaveFailed_LastAuthenticationFailedExceptionAsync.json similarity index 100% rename from sdk/identity/Azure.Identity/tests/SessionRecords/DefaultAzureCredentialLiveTests/DefaultAzureCredential_AllCredentialsHaveFailed_AuthenticationFailedExceptionAsync.json rename to sdk/identity/Azure.Identity/tests/SessionRecords/DefaultAzureCredentialLiveTests/DefaultAzureCredential_AllCredentialsHaveFailed_LastAuthenticationFailedExceptionAsync.json diff --git a/sdk/identity/Azure.Identity/tests/VisualStudioCodeCredentialLiveTests.cs b/sdk/identity/Azure.Identity/tests/VisualStudioCodeCredentialLiveTests.cs index 0b1cf131af72d..2ddd1d37c39e7 100644 --- a/sdk/identity/Azure.Identity/tests/VisualStudioCodeCredentialLiveTests.cs +++ b/sdk/identity/Azure.Identity/tests/VisualStudioCodeCredentialLiveTests.cs @@ -27,7 +27,7 @@ public VisualStudioCodeCredentialLiveTests(bool isAsync) : base(isAsync) } [Test] - [RunOnlyOnPlatforms(Windows = true, OSX = true, ContainerNames = new[] { "ubuntu_netcore2_keyring" })] + [RunOnlyOnPlatforms(Windows = true, OSX = true, ContainerNames = new[] { "ubuntu_netcore_keyring" })] public async Task AuthenticateWithVscCredential() { var cloudName = Guid.NewGuid().ToString(); @@ -82,7 +82,7 @@ public async Task AuthenticateWithVscCredential_EmptySettingsFile() } [Test] - [RunOnlyOnPlatforms(Windows = true, OSX = true, ContainerNames = new[] { "ubuntu_netcore2_keyring" })] + [RunOnlyOnPlatforms(Windows = true, OSX = true, ContainerNames = new[] { "ubuntu_netcore_keyring" })] public async Task AuthenticateWithVscCredential_TenantInSettings() { var cloudName = Guid.NewGuid().ToString(); @@ -98,7 +98,7 @@ public async Task AuthenticateWithVscCredential_TenantInSettings() } [Test] - [RunOnlyOnPlatforms(Windows = true, OSX = true, ContainerNames = new[] { "ubuntu_netcore2_keyring" })] + [RunOnlyOnPlatforms(Windows = true, OSX = true, ContainerNames = new[] { "ubuntu_netcore_keyring" })] public void AuthenticateWithVscCredential_NoVscInstalled() { var cloudName = Guid.NewGuid().ToString(); @@ -120,7 +120,7 @@ public void AuthenticateWithVscCredential_NoRefreshToken() var options = Recording.InstrumentClientOptions(new VisualStudioCodeCredentialOptions { TenantId = tenantId }); VisualStudioCodeCredential credential = InstrumentClient(new VisualStudioCodeCredential(options, default, default, fileSystem, vscAdapter)); - Assert.CatchAsync(async () => await credential.GetTokenAsync(new TokenRequestContext(new[] {"https://vault.azure.net/.default"}), CancellationToken.None)); + Assert.ThrowsAsync(async () => await credential.GetTokenAsync(new TokenRequestContext(new[] {"https://vault.azure.net/.default"}), CancellationToken.None)); } [Test] diff --git a/sdk/identity/Azure.Identity/tests/VisualStudioCredentialTests.cs b/sdk/identity/Azure.Identity/tests/VisualStudioCredentialTests.cs index 24604e707a199..f41ddf8b6bc7b 100644 --- a/sdk/identity/Azure.Identity/tests/VisualStudioCredentialTests.cs +++ b/sdk/identity/Azure.Identity/tests/VisualStudioCredentialTests.cs @@ -141,12 +141,22 @@ public void AuthenticateWithVsCredential_NoDirectoryFound() [Test] public void AuthenticateWithVsCredential_BrokenJsonFileFound() + { + var (_, _, processOutput) = CredentialTestHelpers.CreateTokenForVisualStudio(); + var testProcess = new TestProcess { Output = processOutput }; + var fileSystem = new TestFileSystemService { ReadAllHandler = p => "{\"Some\": " }; + var credential = InstrumentClient(new VisualStudioCredential(default, default, fileSystem, new TestProcessService(testProcess))); + Assert.ThrowsAsync(async () => await credential.GetTokenAsync(new TokenRequestContext(new[]{"https://vault.azure.net/"}), CancellationToken.None)); + } + + [Test] + public void AuthenticateWithVsCredential_IncorrectJsonFileFound() { var (_, _, processOutput) = CredentialTestHelpers.CreateTokenForVisualStudio(); var testProcess = new TestProcess { Output = processOutput }; var fileSystem = new TestFileSystemService { ReadAllHandler = p => "{\"Some\": false}" }; var credential = InstrumentClient(new VisualStudioCredential(default, default, fileSystem, new TestProcessService(testProcess))); - Assert.ThrowsAsync(async () => await credential.GetTokenAsync(new TokenRequestContext(new[]{"https://vault.azure.net/"}), CancellationToken.None)); + Assert.ThrowsAsync(async () => await credential.GetTokenAsync(new TokenRequestContext(new[]{"https://vault.azure.net/"}), CancellationToken.None)); } [Test] @@ -155,7 +165,7 @@ public void AuthenticateWithVsCredential_ProcessFailed() var testProcess = new TestProcess { Error = "Some error" }; var fileSystem = CredentialTestHelpers.CreateFileSystemForVisualStudio(); var credential = InstrumentClient(new VisualStudioCredential(default, default, fileSystem, new TestProcessService(testProcess))); - Assert.ThrowsAsync(async () => await credential.GetTokenAsync(new TokenRequestContext(new[]{"https://vault.azure.net/"}), CancellationToken.None)); + Assert.ThrowsAsync(async () => await credential.GetTokenAsync(new TokenRequestContext(new[]{"https://vault.azure.net/"}), CancellationToken.None)); } [Test]