| | 1 | | // Copyright (c) Microsoft Corporation. All rights reserved. |
| | 2 | | // Licensed under the MIT License. |
| | 3 | |
|
| | 4 | | using System; |
| | 5 | | using System.Collections.Generic; |
| | 6 | | using System.Diagnostics; |
| | 7 | | using System.Globalization; |
| | 8 | | using System.IO; |
| | 9 | | using System.Runtime.InteropServices; |
| | 10 | | using System.Text; |
| | 11 | | using System.Threading; |
| | 12 | | using System.Threading.Tasks; |
| | 13 | | using System.Text.Json; |
| | 14 | | using System.Text.RegularExpressions; |
| | 15 | | using Azure.Core; |
| | 16 | | using Azure.Core.Pipeline; |
| | 17 | |
|
| | 18 | | namespace Azure.Identity |
| | 19 | | { |
| | 20 | | /// <summary> |
| | 21 | | /// Enables authentication to Azure Active Directory using Azure CLI to obtain an access token. |
| | 22 | | /// </summary> |
| | 23 | | public class AzureCliCredential : TokenCredential |
| | 24 | | { |
| | 25 | | private const string AzureCLINotInstalled = "Azure CLI not installed"; |
| | 26 | | private const string AzNotLogIn = "Please run 'az login' to set up account"; |
| | 27 | | private const string WinAzureCLIError = "'az' is not recognized"; |
| | 28 | | private const string AzureCliTimeoutError = "Azure CLI authentication timed out."; |
| | 29 | | private const string AzureCliFailedError = "Azure CLI authentication failed due to an unknown error."; |
| | 30 | | private const int CliProcessTimeoutMs = 10000; |
| | 31 | |
|
| | 32 | | // The default install paths are used to find Azure CLI if no path is specified. This is to prevent executing ou |
| 2 | 33 | | private static readonly string DefaultPathWindows = $"{EnvironmentVariables.ProgramFilesX86}\\Microsoft SDKs\\Az |
| 2 | 34 | | private static readonly string DefaultWorkingDirWindows = Environment.GetFolderPath(Environment.SpecialFolder.Sy |
| | 35 | | private const string DefaultPathNonWindows = "/usr/bin:/usr/local/bin"; |
| | 36 | | private const string DefaultWorkingDirNonWindows = "/bin/"; |
| 2 | 37 | | private static readonly string DefaultPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? DefaultPathWi |
| 2 | 38 | | private static readonly string DefaultWorkingDir = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Default |
| | 39 | |
|
| 2 | 40 | | private static readonly Regex AzNotFoundPattern = new Regex("az:(.*)not found"); |
| | 41 | |
|
| | 42 | | private readonly string _path; |
| | 43 | |
|
| | 44 | | private readonly CredentialPipeline _pipeline; |
| | 45 | | private readonly IProcessService _processService; |
| | 46 | |
|
| | 47 | | /// <summary> |
| | 48 | | /// Create an instance of CliCredential class. |
| | 49 | | /// </summary> |
| | 50 | | public AzureCliCredential() |
| 52 | 51 | | : this(CredentialPipeline.GetInstance(null), default) |
| 52 | 52 | | { } |
| | 53 | |
|
| 138 | 54 | | internal AzureCliCredential(CredentialPipeline pipeline, IProcessService processService) |
| | 55 | | { |
| 138 | 56 | | _pipeline = pipeline; |
| 138 | 57 | | _path = !string.IsNullOrEmpty(EnvironmentVariables.Path) ? EnvironmentVariables.Path : DefaultPath; |
| 138 | 58 | | _processService = processService ?? ProcessService.Default; |
| 138 | 59 | | } |
| | 60 | |
|
| | 61 | | /// <summary> |
| | 62 | | /// Obtains a access token from Azure CLI credential, using this access token to authenticate. This method calle |
| | 63 | | /// </summary> |
| | 64 | | /// <param name="requestContext"></param> |
| | 65 | | /// <param name="cancellationToken"></param> |
| | 66 | | /// <returns></returns> |
| | 67 | | public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken = d |
| | 68 | | { |
| 52 | 69 | | return GetTokenImplAsync(false, requestContext, cancellationToken).EnsureCompleted(); |
| | 70 | | } |
| | 71 | |
|
| | 72 | | /// <summary> |
| | 73 | | /// Obtains a access token from Azure CLI service, using the access token to authenticate. This method id called |
| | 74 | | /// </summary> |
| | 75 | | /// <param name="requestContext"></param> |
| | 76 | | /// <param name="cancellationToken"></param> |
| | 77 | | /// <returns></returns> |
| | 78 | | public override async ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken |
| | 79 | | { |
| 52 | 80 | | return await GetTokenImplAsync(true, requestContext, cancellationToken).ConfigureAwait(false); |
| 26 | 81 | | } |
| | 82 | |
|
| | 83 | | private async ValueTask<AccessToken> GetTokenImplAsync(bool async, TokenRequestContext requestContext, Cancellat |
| | 84 | | { |
| 104 | 85 | | using CredentialDiagnosticScope scope = _pipeline.StartGetTokenScope("AzureCliCredential.GetToken", requestC |
| | 86 | |
|
| | 87 | | try |
| | 88 | | { |
| 104 | 89 | | AccessToken token = await RequestCliAccessTokenAsync(async, requestContext.Scopes, cancellationToken).Co |
| 50 | 90 | | return scope.Succeeded(token); |
| | 91 | | } |
| 54 | 92 | | catch (Exception e) |
| | 93 | | { |
| 54 | 94 | | throw scope.FailWrapAndThrow(e); |
| | 95 | | } |
| 50 | 96 | | } |
| | 97 | |
|
| | 98 | | private async ValueTask<AccessToken> RequestCliAccessTokenAsync(bool async, string[] scopes, CancellationToken c |
| | 99 | | { |
| 104 | 100 | | string resource = ScopeUtilities.ScopesToResource(scopes); |
| | 101 | |
|
| 104 | 102 | | ScopeUtilities.ValidateScope(resource); |
| | 103 | |
|
| 104 | 104 | | GetFileNameAndArguments(resource, out string fileName, out string argument); |
| 104 | 105 | | ProcessStartInfo processStartInfo = GetAzureCliProcessStartInfo(fileName, argument); |
| 104 | 106 | | var processRunner = new ProcessRunner(_processService.Create(processStartInfo), TimeSpan.FromMilliseconds(Cl |
| | 107 | |
|
| | 108 | | string output; |
| | 109 | | try |
| | 110 | | { |
| 104 | 111 | | output = async ? await processRunner.RunAsync().ConfigureAwait(false) : processRunner.Run(); |
| 70 | 112 | | } |
| 6 | 113 | | catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) |
| | 114 | | { |
| 2 | 115 | | throw new AuthenticationFailedException(AzureCliTimeoutError); |
| | 116 | | } |
| 28 | 117 | | catch (InvalidOperationException exception) |
| | 118 | | { |
| 28 | 119 | | bool isWinError = exception.Message.StartsWith(WinAzureCLIError, StringComparison.CurrentCultureIgnoreCa |
| | 120 | |
|
| 28 | 121 | | bool isOtherOsError = AzNotFoundPattern.IsMatch(exception.Message); |
| | 122 | |
|
| 28 | 123 | | if (isWinError || isOtherOsError) |
| | 124 | | { |
| 16 | 125 | | throw new CredentialUnavailableException(AzureCLINotInstalled); |
| | 126 | | } |
| | 127 | |
|
| 12 | 128 | | bool isLoginError = exception.Message.IndexOf("az login", StringComparison.OrdinalIgnoreCase) != -1 || e |
| | 129 | |
|
| 12 | 130 | | if (isLoginError) |
| | 131 | | { |
| 4 | 132 | | throw new CredentialUnavailableException(AzNotLogIn); |
| | 133 | | } |
| | 134 | |
|
| 8 | 135 | | throw new AuthenticationFailedException($"{AzureCliFailedError} {exception.Message}"); |
| | 136 | | } |
| | 137 | |
|
| 70 | 138 | | return DeserializeOutput(output); |
| 50 | 139 | | } |
| | 140 | |
|
| | 141 | | private ProcessStartInfo GetAzureCliProcessStartInfo(string fileName, string argument) => |
| 104 | 142 | | new ProcessStartInfo |
| 104 | 143 | | { |
| 104 | 144 | | FileName = fileName, |
| 104 | 145 | | Arguments = argument, |
| 104 | 146 | | UseShellExecute = false, |
| 104 | 147 | | ErrorDialog = false, |
| 104 | 148 | | CreateNoWindow = true, |
| 104 | 149 | | WorkingDirectory = DefaultWorkingDir, |
| 104 | 150 | | Environment = {{"PATH", _path}} |
| 104 | 151 | | }; |
| | 152 | |
|
| | 153 | | private static void GetFileNameAndArguments(string resource, out string fileName, out string argument) |
| | 154 | | { |
| 104 | 155 | | string command = $"az account get-access-token --output json --resource {resource}"; |
| | 156 | |
|
| 104 | 157 | | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { |
| 104 | 158 | | fileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "cmd.exe"); |
| 104 | 159 | | argument = $"/c \"{command}\""; |
| | 160 | | } else { |
| 0 | 161 | | fileName = "/bin/sh"; |
| 0 | 162 | | argument = $"-c \"{command}\""; |
| | 163 | | } |
| 0 | 164 | | } |
| | 165 | |
|
| | 166 | | private static AccessToken DeserializeOutput(string output) |
| | 167 | | { |
| 70 | 168 | | using JsonDocument document = JsonDocument.Parse(output); |
| | 169 | |
|
| 66 | 170 | | JsonElement root = document.RootElement; |
| 66 | 171 | | string accessToken = root.GetProperty("accessToken").GetString(); |
| 54 | 172 | | DateTimeOffset expiresOn = root.TryGetProperty("expiresIn", out JsonElement expiresIn) |
| 54 | 173 | | ? DateTimeOffset.UtcNow + TimeSpan.FromSeconds(expiresIn.GetInt64()) |
| 54 | 174 | | : DateTimeOffset.ParseExact(root.GetProperty("expiresOn").GetString(), "yyyy-MM-dd HH:mm:ss.ffffff", Cul |
| | 175 | |
|
| 50 | 176 | | return new AccessToken(accessToken, expiresOn); |
| 50 | 177 | | } |
| | 178 | | } |
| | 179 | | } |