-
Notifications
You must be signed in to change notification settings - Fork 4.9k
/
Copy pathAzureCliCredential.cs
179 lines (151 loc) · 8.27 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
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
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 const string AzureCLINotInstalled = "Azure CLI not installed";
private const string AzNotLogIn = "Please run 'az login' to set up account";
private const string WinAzureCLIError = "'az' is not recognized";
private const string AzureCliTimeoutError = "Azure CLI authentication timed out.";
private const string AzureCliFailedError = "Azure CLI authentication failed due to an unknown error.";
private const int CliProcessTimeoutMs = 10000;
// 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 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;
/// <summary>
/// Create an instance of CliCredential class.
/// </summary>
public AzureCliCredential()
: this(CredentialPipeline.GetInstance(null), default)
{ }
internal AzureCliCredential(CredentialPipeline pipeline, IProcessService processService)
{
_pipeline = pipeline;
_path = !string.IsNullOrEmpty(EnvironmentVariables.Path) ? EnvironmentVariables.Path : DefaultPath;
_processService = processService ?? ProcessService.Default;
}
/// <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.Scopes, cancellationToken).ConfigureAwait(false);
return scope.Succeeded(token);
}
catch (Exception e)
{
throw scope.FailWrapAndThrow(e);
}
}
private async ValueTask<AccessToken> RequestCliAccessTokenAsync(bool async, string[] scopes, CancellationToken cancellationToken)
{
string resource = ScopeUtilities.ScopesToResource(scopes);
ScopeUtilities.ValidateScope(resource);
GetFileNameAndArguments(resource, out string fileName, out string argument);
ProcessStartInfo processStartInfo = GetAzureCliProcessStartInfo(fileName, argument);
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);
}
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, out string fileName, out string argument)
{
string command = $"az account get-access-token --output json --resource {resource}";
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);
return new AccessToken(accessToken, expiresOn);
}
}
}