diff --git a/sdk/identity/Azure.Identity/CHANGELOG.md b/sdk/identity/Azure.Identity/CHANGELOG.md index 55e2685d91094..0fc681dd62952 100644 --- a/sdk/identity/Azure.Identity/CHANGELOG.md +++ b/sdk/identity/Azure.Identity/CHANGELOG.md @@ -9,6 +9,7 @@ ### Bugs Fixed ### Other Changes +- All developer credentials in the `DefaultAzureCredential` credential chain will fall through to the next credential in the chain on any failure. Previously, some exceptions would throw `AuthenticationFailedException`, which stops further progress in the chain. ## 1.9.0 (2023-05-09) diff --git a/sdk/identity/Azure.Identity/src/CredentialDiagnosticScope.cs b/sdk/identity/Azure.Identity/src/CredentialDiagnosticScope.cs index 85f28af4454e5..04681d17fa036 100644 --- a/sdk/identity/Azure.Identity/src/CredentialDiagnosticScope.cs +++ b/sdk/identity/Azure.Identity/src/CredentialDiagnosticScope.cs @@ -36,7 +36,7 @@ public AccessToken Succeeded(AccessToken token) return token; } - public Exception FailWrapAndThrow(Exception ex, string additionalMessage = null) + public Exception FailWrapAndThrow(Exception ex, string additionalMessage = null, bool isCredentialUnavailable = false) { var wrapped = TryWrapException(ref ex, additionalMessage); RegisterFailed(ex); @@ -55,7 +55,7 @@ private void RegisterFailed(Exception ex) _scopeHandler.Fail(_name, _scope, ex); } - private bool TryWrapException(ref Exception exception, string additionalMessageText = null) + private bool TryWrapException(ref Exception exception, string additionalMessageText = null, bool isCredentialUnavailable = false) { if (exception is OperationCanceledException || exception is AuthenticationFailedException) { @@ -76,7 +76,9 @@ private bool TryWrapException(ref Exception exception, string additionalMessageT { exceptionMessage = exceptionMessage + $"\n{additionalMessageText}"; } - exception = new AuthenticationFailedException(exceptionMessage, exception); + exception = isCredentialUnavailable ? + new CredentialUnavailableException(exceptionMessage, exception) : + new AuthenticationFailedException(exceptionMessage, exception); return true; } diff --git a/sdk/identity/Azure.Identity/src/Credentials/AzureCliCredential.cs b/sdk/identity/Azure.Identity/src/Credentials/AzureCliCredential.cs index 84aeaa1d2bad6..4b4d1ff6a6ac2 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/AzureCliCredential.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/AzureCliCredential.cs @@ -132,7 +132,7 @@ private async ValueTask RequestCliAccessTokenAsync(bool async, Toke } catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { - throw new AuthenticationFailedException(AzureCliTimeoutError); + throw new CredentialUnavailableException(AzureCliTimeoutError); } catch (InvalidOperationException exception) { @@ -163,7 +163,7 @@ private async ValueTask RequestCliAccessTokenAsync(bool async, Toke throw new CredentialUnavailableException(InteractiveLoginRequired); } - throw new AuthenticationFailedException($"{AzureCliFailedError} {Troubleshoot} {exception.Message}"); + throw new CredentialUnavailableException($"{AzureCliFailedError} {Troubleshoot} {exception.Message}"); } AccessToken token = DeserializeOutput(output); diff --git a/sdk/identity/Azure.Identity/src/Credentials/AzureDeveloperCliCredential.cs b/sdk/identity/Azure.Identity/src/Credentials/AzureDeveloperCliCredential.cs index 6f715b49d5464..f3172ffdbf88b 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/AzureDeveloperCliCredential.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/AzureDeveloperCliCredential.cs @@ -123,7 +123,7 @@ private async ValueTask RequestCliAccessTokenAsync(bool async, Toke } catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { - throw new AuthenticationFailedException(AzdCliTimeoutError); + throw new CredentialUnavailableException(AzdCliTimeoutError); } catch (InvalidOperationException exception) { @@ -153,7 +153,7 @@ private async ValueTask RequestCliAccessTokenAsync(bool async, Toke throw new CredentialUnavailableException(InteractiveLoginRequired); } - throw new AuthenticationFailedException($"{AzdCliFailedError} {Troubleshoot} {exception.Message}"); + throw new CredentialUnavailableException($"{AzdCliFailedError} {Troubleshoot} {exception.Message}"); } AccessToken token = DeserializeOutput(output); diff --git a/sdk/identity/Azure.Identity/src/Credentials/AzurePowerShellCredential.cs b/sdk/identity/Azure.Identity/src/Credentials/AzurePowerShellCredential.cs index 5f11ac8d4fda2..7b0bf862822b2 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/AzurePowerShellCredential.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/AzurePowerShellCredential.cs @@ -124,12 +124,12 @@ private async ValueTask GetTokenImplAsync(bool async, TokenRequestC } catch (Exception e) { - throw scope.FailWrapAndThrow(e); + throw scope.FailWrapAndThrow(e, isCredentialUnavailable: true); } } catch (Exception e) { - throw scope.FailWrapAndThrow(e); + throw scope.FailWrapAndThrow(e, isCredentialUnavailable: true); } } @@ -162,7 +162,7 @@ private async ValueTask RequestAzurePowerShellAccessTokenAsync(bool catch (InvalidOperationException exception) { CheckForErrors(exception.Message); - throw new AuthenticationFailedException($"{AzurePowerShellFailedError} {exception.Message}"); + throw new CredentialUnavailableException($"{AzurePowerShellFailedError} {exception.Message}"); } return DeserializeOutput(output); } diff --git a/sdk/identity/Azure.Identity/src/Credentials/VisualStudioCredential.cs b/sdk/identity/Azure.Identity/src/Credentials/VisualStudioCredential.cs index 914b19df907fc..78df884eea78c 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/VisualStudioCredential.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/VisualStudioCredential.cs @@ -102,10 +102,14 @@ private async ValueTask GetTokenImplAsync(TokenRequestContext reque return scope.Succeeded(accessToken); } - catch (Exception e) + catch (CredentialUnavailableException e) { throw scope.FailWrapAndThrow(e); } + catch (Exception e) + { + throw scope.FailWrapAndThrow(e, isCredentialUnavailable: true); + } } private static string GetTokenProviderPath() diff --git a/sdk/identity/Azure.Identity/tests/AzureCliCredentialTests.cs b/sdk/identity/Azure.Identity/tests/AzureCliCredentialTests.cs index e1bfd6c33c58d..a2cc9215bbc18 100644 --- a/sdk/identity/Azure.Identity/tests/AzureCliCredentialTests.cs +++ b/sdk/identity/Azure.Identity/tests/AzureCliCredentialTests.cs @@ -103,8 +103,8 @@ public static IEnumerable AzureCliExceptionScenarios() yield return new object[] { AzureCliCredential.AzNotLogIn, AzureCliCredential.AzNotLogIn, typeof(CredentialUnavailableException) }; yield return new object[] { RefreshTokenExpiredError, AzureCliCredential.InteractiveLoginRequired, typeof(CredentialUnavailableException) }; yield return new object[] { AzureCliCredential.CLIInternalError, AzureCliCredential.InteractiveLoginRequired, typeof(CredentialUnavailableException) }; - yield return new object[] { "random unknown exception", AzureCliCredential.AzureCliFailedError + " " + AzureCliCredential.Troubleshoot + " random unknown exception", typeof(AuthenticationFailedException) }; - yield return new object[] { "AADSTS12345: Some AAD error. To re-authenticate, please run: az login", AzureCliCredential.AzureCliFailedError + " " + AzureCliCredential.Troubleshoot + " AADSTS12345: Some AAD error. To re-authenticate, please run: az login", typeof(AuthenticationFailedException) }; + yield return new object[] { "random unknown exception", AzureCliCredential.AzureCliFailedError + " " + AzureCliCredential.Troubleshoot + " random unknown exception", typeof(CredentialUnavailableException) }; + yield return new object[] { "AADSTS12345: Some AAD error. To re-authenticate, please run: az login", AzureCliCredential.AzureCliFailedError + " " + AzureCliCredential.Troubleshoot + " AADSTS12345: Some AAD error. To re-authenticate, please run: az login", typeof(CredentialUnavailableException) }; } [Test] @@ -135,7 +135,7 @@ public void ConfigureCliProcessTimeout_ProcessTimeout() new AzureCliCredential(CredentialPipeline.GetInstance(null), new TestProcessService(testProcess), new AzureCliCredentialOptions() { ProcessTimeout = TimeSpan.Zero })); - var ex = Assert.ThrowsAsync(async () => await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default))); + var ex = Assert.ThrowsAsync(async () => await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default))); Assert.AreEqual(AzureCliCredential.AzureCliTimeoutError, ex.Message); } } diff --git a/sdk/identity/Azure.Identity/tests/AzureDeveloperCliCredentialTests.cs b/sdk/identity/Azure.Identity/tests/AzureDeveloperCliCredentialTests.cs index c83ae30d2d605..2584545f94a28 100644 --- a/sdk/identity/Azure.Identity/tests/AzureDeveloperCliCredentialTests.cs +++ b/sdk/identity/Azure.Identity/tests/AzureDeveloperCliCredentialTests.cs @@ -90,8 +90,8 @@ public static IEnumerable AzureDeveloperCliExceptionScenarios() yield return new object[] { AzureDeveloperCliCredential.AzdNotLogIn, AzureDeveloperCliCredential.AzdNotLogIn, typeof(CredentialUnavailableException) }; yield return new object[] { RefreshTokenExpiredError, AzureDeveloperCliCredential.InteractiveLoginRequired, typeof(CredentialUnavailableException) }; yield return new object[] { AzureDeveloperCliCredential.AzdCLIInternalError, AzureDeveloperCliCredential.InteractiveLoginRequired, typeof(CredentialUnavailableException) }; - yield return new object[] { "random unknown exception", AzureDeveloperCliCredential.AzdCliFailedError + " " + AzureDeveloperCliCredential.Troubleshoot + " random unknown exception", typeof(AuthenticationFailedException) }; - yield return new object[] { "AADSTS12345: Some AAD error. To re-authenticate, please run: azd auth login", AzureDeveloperCliCredential.AzdCliFailedError + " " + AzureDeveloperCliCredential.Troubleshoot + " AADSTS12345: Some AAD error. To re-authenticate, please run: azd auth login", typeof(AuthenticationFailedException) }; + yield return new object[] { "random unknown exception", AzureDeveloperCliCredential.AzdCliFailedError + " " + AzureDeveloperCliCredential.Troubleshoot + " random unknown exception", typeof(CredentialUnavailableException) }; + yield return new object[] { "AADSTS12345: Some AAD error. To re-authenticate, please run: azd auth login", AzureDeveloperCliCredential.AzdCliFailedError + " " + AzureDeveloperCliCredential.Troubleshoot + " AADSTS12345: Some AAD error. To re-authenticate, please run: azd auth login", typeof(CredentialUnavailableException) }; } [Test] @@ -122,7 +122,7 @@ public void ConfigureCliProcessTimeout_ProcessTimeout() new AzureDeveloperCliCredential(CredentialPipeline.GetInstance(null), new TestProcessService(testProcess), new AzureDeveloperCliCredentialOptions() { ProcessTimeout = TimeSpan.Zero })); - var ex = Assert.ThrowsAsync(async () => await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default))); + var ex = Assert.ThrowsAsync(async () => await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default))); Assert.AreEqual(AzureDeveloperCliCredential.AzdCliTimeoutError, ex.Message); } } diff --git a/sdk/identity/Azure.Identity/tests/AzurePowerShellCredentialsTests.cs b/sdk/identity/Azure.Identity/tests/AzurePowerShellCredentialsTests.cs index ffd15df63ef6e..0688b6e61aa56 100644 --- a/sdk/identity/Azure.Identity/tests/AzurePowerShellCredentialsTests.cs +++ b/sdk/identity/Azure.Identity/tests/AzurePowerShellCredentialsTests.cs @@ -92,10 +92,11 @@ private static IEnumerable ErrorScenarios() yield return new object[] { "Get-AzAccessToken: Run Connect-AzAccount to login.", AzurePowerShellCredential.AzurePowerShellNotLogInError, typeof(CredentialUnavailableException) }; yield return new object[] { "No accounts were found in the cache", AzurePowerShellCredential.AzurePowerShellNotLogInError, typeof(CredentialUnavailableException) }; yield return new object[] { "cannot retrieve access token", AzurePowerShellCredential.AzurePowerShellNotLogInError, typeof(CredentialUnavailableException) }; + yield return new object[] { "Some random exception", AzurePowerShellCredential.AzurePowerShellFailedError + " Some random exception", typeof(CredentialUnavailableException) }; yield return new object[] { "AADSTS500011: The resource principal named was not found in the tenant named", AzurePowerShellCredential.AzurePowerShellFailedError + " AADSTS500011: The resource principal named was not found in the tenant named", - typeof(AuthenticationFailedException) }; + typeof(CredentialUnavailableException) }; } [Test] @@ -220,7 +221,7 @@ public void AuthenticateWithAzurePowerShellCredential_AzurePowerShellUnknownErro var testProcess = new TestProcess { Error = mockResult }; AzurePowerShellCredential credential = InstrumentClient( new AzurePowerShellCredential(new AzurePowerShellCredentialOptions(), CredentialPipeline.GetInstance(null), new TestProcessService(testProcess))); - Assert.ThrowsAsync(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/VisualStudioCredentialTests.cs b/sdk/identity/Azure.Identity/tests/VisualStudioCredentialTests.cs index 0d8f9bc12a531..8bf32d2c96dad 100644 --- a/sdk/identity/Azure.Identity/tests/VisualStudioCredentialTests.cs +++ b/sdk/identity/Azure.Identity/tests/VisualStudioCredentialTests.cs @@ -279,5 +279,15 @@ public void ConfigureVisualStudioProcessTimeout_ProcessTimeout() var ex = Assert.ThrowsAsync(async () => await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default), CancellationToken.None)); Assert.True(ex.Message.Contains("has failed to get access token in 0 seconds.")); } + + [Test] + public void GenericException_throws_CredentialUnavailableException() + { + var testProcess = new TestProcess() { ExceptionOnStartHandler = p => throw new Exception("Test exception") }; + 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)); + } } }