Skip to content

Commit 658b03f

Browse files
authored
AzureCliCredential throws CredentialUnavailableException when interactive login is required (Azure#19245)
* AzureCliCredential throws CredentialUnavailableException when interactive login is required
1 parent 675631c commit 658b03f

File tree

3 files changed

+34
-28
lines changed

3 files changed

+34
-28
lines changed

sdk/identity/Azure.Identity/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Release History
22

3-
## 1.4.0-beta.4 (Unreleased)
3+
## 1.4.0-beta.4 (2021-03-09)
44

55
### Fixes and Improvements
66

sdk/identity/Azure.Identity/src/AzureCliCredential.cs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,20 @@ namespace Azure.Identity
2020
/// </summary>
2121
public class AzureCliCredential : TokenCredential
2222
{
23-
private const string AzureCLINotInstalled = "Azure CLI not installed";
24-
private const string AzNotLogIn = "Please run 'az login' to set up account";
23+
internal const string AzureCLINotInstalled = "Azure CLI not installed";
24+
internal const string AzNotLogIn = "Please run 'az login' to set up account";
2525
private const string WinAzureCLIError = "'az' is not recognized";
2626
private const string AzureCliTimeoutError = "Azure CLI authentication timed out.";
27-
private const string AzureCliFailedError = "Azure CLI authentication failed due to an unknown error.";
27+
internal const string AzureCliFailedError = "Azure CLI authentication failed due to an unknown error.";
28+
internal const string InteractiveLoginRequired = "Azure CLI could not login. Interactive login is required.";
2829
private const int CliProcessTimeoutMs = 13000;
2930

3031
// The default install paths are used to find Azure CLI if no path is specified. This is to prevent executing out of the current working directory.
3132
private static readonly string DefaultPathWindows = $"{EnvironmentVariables.ProgramFilesX86}\\Microsoft SDKs\\Azure\\CLI2\\wbin;{EnvironmentVariables.ProgramFiles}\\Microsoft SDKs\\Azure\\CLI2\\wbin";
3233
private static readonly string DefaultWorkingDirWindows = Environment.GetFolderPath(Environment.SpecialFolder.System);
3334
private const string DefaultPathNonWindows = "/usr/bin:/usr/local/bin";
3435
private const string DefaultWorkingDirNonWindows = "/bin/";
36+
private const string RefreshTokeExpired = "The provided authorization code or refresh token has expired due to inactivity. Send a new interactive authorization request for this user and resource.";
3537
private static readonly string DefaultPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? DefaultPathWindows : DefaultPathNonWindows;
3638
private static readonly string DefaultWorkingDir = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? DefaultWorkingDirWindows : DefaultWorkingDirNonWindows;
3739

@@ -130,6 +132,13 @@ private async ValueTask<AccessToken> RequestCliAccessTokenAsync(bool async, stri
130132
throw new CredentialUnavailableException(AzNotLogIn);
131133
}
132134

135+
bool isRefreshTokenFailedError = exception.Message.IndexOf(AzureCliFailedError, StringComparison.OrdinalIgnoreCase) != -1 && exception.Message.IndexOf(RefreshTokeExpired, StringComparison.OrdinalIgnoreCase) != -1;
136+
137+
if (isRefreshTokenFailedError)
138+
{
139+
throw new CredentialUnavailableException(InteractiveLoginRequired);
140+
}
141+
133142
throw new AuthenticationFailedException($"{AzureCliFailedError} {exception.Message}");
134143
}
135144

@@ -145,17 +154,20 @@ private ProcessStartInfo GetAzureCliProcessStartInfo(string fileName, string arg
145154
ErrorDialog = false,
146155
CreateNoWindow = true,
147156
WorkingDirectory = DefaultWorkingDir,
148-
Environment = {{"PATH", _path}}
157+
Environment = { { "PATH", _path } }
149158
};
150159

151160
private static void GetFileNameAndArguments(string resource, out string fileName, out string argument)
152161
{
153162
string command = $"az account get-access-token --output json --resource {resource}";
154163

155-
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
164+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
165+
{
156166
fileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "cmd.exe");
157167
argument = $"/c \"{command}\"";
158-
} else {
168+
}
169+
else
170+
{
159171
fileName = "/bin/sh";
160172
argument = $"-c \"{command}\"";
161173
}

sdk/identity/Azure.Identity/tests/AzureCliCredentialTests.cs

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -50,33 +50,27 @@ public void AuthenticateWithCliCredential_InvalidJsonOutput([Values("", "{}", "{
5050
Assert.ThrowsAsync<AuthenticationFailedException>(async () => await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default)));
5151
}
5252

53-
[Test]
54-
public void AuthenticateWithCliCredential_AzureCliNotInstalled([Values("'az' is not recognized", "az: command not found", "az: not found")] string errorMessage)
55-
{
56-
string expectedMessage = "Azure CLI not installed";
57-
var testProcess = new TestProcess { Error = errorMessage };
58-
AzureCliCredential credential = InstrumentClient(new AzureCliCredential(CredentialPipeline.GetInstance(null), new TestProcessService(testProcess)));
59-
var ex = Assert.ThrowsAsync<CredentialUnavailableException>(async () => await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default)));
60-
Assert.AreEqual(expectedMessage, ex.Message);
61-
}
62-
63-
[Test]
64-
public void AuthenticateWithCliCredential_AzNotLogIn()
53+
private const string RefreshTokenExpiredError = "Azure CLI authentication failed due to an unknown error. ERROR: Get Token request returned http error: 400 and server response: {\"error\":\"invalid_grant\",\"error_description\":\"AADSTS70008: The provided authorization code or refresh token has expired due to inactivity. Send a new interactive authorization request for this user and resource.";
54+
public static IEnumerable<object[]> AzureCliExceptionScenarios()
6555
{
66-
string expectedExMessage = $"Please run 'az login' to set up account";
67-
var testProcess = new TestProcess { Error = "Please run 'az login'" };
68-
AzureCliCredential credential = InstrumentClient(new AzureCliCredential(CredentialPipeline.GetInstance(null), new TestProcessService(testProcess)));
69-
var ex = Assert.ThrowsAsync<CredentialUnavailableException>(async () => await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default)));
70-
Assert.AreEqual(expectedExMessage, ex.Message);
56+
// params
57+
// az thrown Exception message, expected message, expected exception
58+
yield return new object[] { "'az' is not recognized", AzureCliCredential.AzureCLINotInstalled, typeof(CredentialUnavailableException) };
59+
yield return new object[] { "az: command not found", AzureCliCredential.AzureCLINotInstalled, typeof(CredentialUnavailableException) };
60+
yield return new object[] { "az: not found", AzureCliCredential.AzureCLINotInstalled, typeof(CredentialUnavailableException) };
61+
yield return new object[] { "Please run 'az login'", AzureCliCredential.AzNotLogIn, typeof(CredentialUnavailableException) };
62+
yield return new object[] { RefreshTokenExpiredError, AzureCliCredential.InteractiveLoginRequired, typeof(CredentialUnavailableException) };
63+
yield return new object[] { "random unknown exception", AzureCliCredential.AzureCliFailedError + " random unknown exception", typeof(AuthenticationFailedException) };
7164
}
7265

7366
[Test]
74-
public void AuthenticateWithCliCredential_AzureCliUnknownError()
67+
[TestCaseSource(nameof(AzureCliExceptionScenarios))]
68+
public void AuthenticateWithCliCredential_ExceptionScenarios(string errorMessage, string expectedMessage, Type exceptionType)
7569
{
76-
string mockResult = "mock-result";
77-
var testProcess = new TestProcess { Error = mockResult };
70+
var testProcess = new TestProcess { Error = errorMessage };
7871
AzureCliCredential credential = InstrumentClient(new AzureCliCredential(CredentialPipeline.GetInstance(null), new TestProcessService(testProcess)));
79-
Assert.ThrowsAsync<AuthenticationFailedException>(async () => await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default)));
72+
var ex = Assert.ThrowsAsync(exceptionType, async () => await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default)));
73+
Assert.AreEqual(expectedMessage, ex.Message);
8074
}
8175

8276
[Test]

0 commit comments

Comments
 (0)