-
Notifications
You must be signed in to change notification settings - Fork 4.9k
/
Copy pathAzureCliCredential.cs
210 lines (179 loc) · 10.3 KB
/
AzureCliCredential.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Text.Json;
using System.Text.RegularExpressions;
using Azure.Core;
using Azure.Core.Pipeline;
namespace Azure.Identity
{
/// <summary>
/// Enables authentication to Azure Active Directory using Azure CLI to obtain an access token.
/// </summary>
public class AzureCliCredential : TokenCredential
{
private readonly bool _allowMultiTenantAuthentication;
internal const string AzureCLINotInstalled = "Azure CLI not installed";
internal const string AzNotLogIn = "Please run 'az login' to set up account";
internal const string WinAzureCLIError = "'az' is not recognized";
internal const string AzureCliTimeoutError = "Azure CLI authentication timed out.";
internal const string AzureCliFailedError = "Azure CLI authentication failed due to an unknown error.";
internal const string InteractiveLoginRequired = "Azure CLI could not login. Interactive login is required.";
internal const string CLIInternalError = "CLIInternalError: The command failed with an unexpected error. Here is the traceback:";
private const int CliProcessTimeoutMs = 13000;
// 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.
private static readonly string DefaultPathWindows = $"{EnvironmentVariables.ProgramFilesX86}\\Microsoft SDKs\\Azure\\CLI2\\wbin;{EnvironmentVariables.ProgramFiles}\\Microsoft SDKs\\Azure\\CLI2\\wbin";
private static readonly string DefaultWorkingDirWindows = Environment.GetFolderPath(Environment.SpecialFolder.System);
private const string DefaultPathNonWindows = "/usr/bin:/usr/local/bin";
private const string DefaultWorkingDirNonWindows = "/bin/";
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.";
private static readonly string DefaultPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? DefaultPathWindows : DefaultPathNonWindows;
private static readonly string DefaultWorkingDir = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? DefaultWorkingDirWindows : DefaultWorkingDirNonWindows;
private static readonly Regex AzNotFoundPattern = new Regex("az:(.*)not found");
private readonly string _path;
private readonly CredentialPipeline _pipeline;
private readonly IProcessService _processService;
private readonly string _tenantId;
/// <summary>
/// Create an instance of CliCredential class.
/// </summary>
public AzureCliCredential()
: this(CredentialPipeline.GetInstance(null), default)
{ }
/// <summary>
/// Create an instance of CliCredential class.
/// </summary>
/// <param name="options"> The Azure Active Directory tenant (directory) Id of the service principal. </param>
public AzureCliCredential(AzureCliCredentialOptions options)
: this(CredentialPipeline.GetInstance(null), default, options)
{ }
internal AzureCliCredential(CredentialPipeline pipeline, IProcessService processService, AzureCliCredentialOptions options = null)
{
_pipeline = pipeline;
_path = !string.IsNullOrEmpty(EnvironmentVariables.Path) ? EnvironmentVariables.Path : DefaultPath;
_processService = processService ?? ProcessService.Default;
_allowMultiTenantAuthentication = options?.AllowMultiTenantAuthentication ?? false;
_tenantId = options?.TenantId;
}
/// <summary>
/// Obtains a access token from Azure CLI credential, using this access token to authenticate. This method called by Azure SDK clients.
/// </summary>
/// <param name="requestContext"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken = default)
{
return GetTokenImplAsync(false, requestContext, cancellationToken).EnsureCompleted();
}
/// <summary>
/// Obtains a access token from Azure CLI service, using the access token to authenticate. This method id called by Azure SDK clients.
/// </summary>
/// <param name="requestContext"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public override async ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken = default)
{
return await GetTokenImplAsync(true, requestContext, cancellationToken).ConfigureAwait(false);
}
private async ValueTask<AccessToken> GetTokenImplAsync(bool async, TokenRequestContext requestContext, CancellationToken cancellationToken)
{
using CredentialDiagnosticScope scope = _pipeline.StartGetTokenScope("AzureCliCredential.GetToken", requestContext);
try
{
AccessToken token = await RequestCliAccessTokenAsync(async, requestContext, cancellationToken).ConfigureAwait(false);
return scope.Succeeded(token);
}
catch (Exception e)
{
throw scope.FailWrapAndThrow(e);
}
}
private async ValueTask<AccessToken> RequestCliAccessTokenAsync(bool async, TokenRequestContext context, CancellationToken cancellationToken)
{
string resource = ScopeUtilities.ScopesToResource(context.Scopes);
string tenantId = TenantIdResolver.Resolve(_tenantId, context, _allowMultiTenantAuthentication);
ScopeUtilities.ValidateScope(resource);
GetFileNameAndArguments(resource, tenantId, out string fileName, out string argument);
ProcessStartInfo processStartInfo = GetAzureCliProcessStartInfo(fileName, argument);
using var processRunner = new ProcessRunner(_processService.Create(processStartInfo), TimeSpan.FromMilliseconds(CliProcessTimeoutMs), cancellationToken);
string output;
try
{
output = async ? await processRunner.RunAsync().ConfigureAwait(false) : processRunner.Run();
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
throw new AuthenticationFailedException(AzureCliTimeoutError);
}
catch (InvalidOperationException exception)
{
bool isWinError = exception.Message.StartsWith(WinAzureCLIError, StringComparison.CurrentCultureIgnoreCase);
bool isOtherOsError = AzNotFoundPattern.IsMatch(exception.Message);
if (isWinError || isOtherOsError)
{
throw new CredentialUnavailableException(AzureCLINotInstalled);
}
bool isLoginError = exception.Message.IndexOf("az login", StringComparison.OrdinalIgnoreCase) != -1 ||
exception.Message.IndexOf("az account set", StringComparison.OrdinalIgnoreCase) != -1;
if (isLoginError)
{
throw new CredentialUnavailableException(AzNotLogIn);
}
bool isRefreshTokenFailedError = exception.Message.IndexOf(AzureCliFailedError, StringComparison.OrdinalIgnoreCase) != -1 &&
exception.Message.IndexOf(RefreshTokeExpired, StringComparison.OrdinalIgnoreCase) != -1 ||
exception.Message.IndexOf("CLIInternalError", StringComparison.OrdinalIgnoreCase) != -1;
if (isRefreshTokenFailedError)
{
throw new CredentialUnavailableException(InteractiveLoginRequired);
}
throw new AuthenticationFailedException($"{AzureCliFailedError} {exception.Message}");
}
return DeserializeOutput(output);
}
private ProcessStartInfo GetAzureCliProcessStartInfo(string fileName, string argument) =>
new ProcessStartInfo
{
FileName = fileName,
Arguments = argument,
UseShellExecute = false,
ErrorDialog = false,
CreateNoWindow = true,
WorkingDirectory = DefaultWorkingDir,
Environment = { { "PATH", _path } }
};
private static void GetFileNameAndArguments(string resource, string tenantId, out string fileName, out string argument)
{
string command = tenantId switch
{
null => $"az account get-access-token --output json --resource {resource}",
_ => $"az account get-access-token --output json --resource {resource} -tenant {tenantId}"
};
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
fileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "cmd.exe");
argument = $"/c \"{command}\"";
}
else
{
fileName = "/bin/sh";
argument = $"-c \"{command}\"";
}
}
private static AccessToken DeserializeOutput(string output)
{
using JsonDocument document = JsonDocument.Parse(output);
JsonElement root = document.RootElement;
string accessToken = root.GetProperty("accessToken").GetString();
DateTimeOffset expiresOn = root.TryGetProperty("expiresIn", out JsonElement expiresIn)
? DateTimeOffset.UtcNow + TimeSpan.FromSeconds(expiresIn.GetInt64())
: DateTimeOffset.ParseExact(root.GetProperty("expiresOn").GetString(), "yyyy-MM-dd HH:mm:ss.ffffff", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeLocal);
return new AccessToken(accessToken, expiresOn);
}
}
}