< Summary

Class:Azure.Identity.AzureCliCredential
Assembly:Azure.Identity
File(s):C:\Git\azure-sdk-for-net\sdk\identity\Azure.Identity\src\AzureCliCredential.cs
Covered lines:63
Uncovered lines:3
Coverable lines:66
Total lines:179
Line coverage:95.4% (63 of 66)
Covered branches:16
Total branches:20
Branch coverage:80% (16 of 20)

Metrics

MethodCyclomatic complexity Line coverage Branch coverage
.cctor()-100%50%
.ctor()-100%100%
.ctor(...)-100%75%
GetToken(...)-100%100%
GetTokenAsync()-100%100%
GetTokenImplAsync()-100%100%
RequestCliAccessTokenAsync()-100%100%
GetAzureCliProcessStartInfo(...)-100%100%
GetFileNameAndArguments(...)-57.14%50%
DeserializeOutput(...)-100%100%

File(s)

C:\Git\azure-sdk-for-net\sdk\identity\Azure.Identity\src\AzureCliCredential.cs

#LineLine coverage
 1// Copyright (c) Microsoft Corporation. All rights reserved.
 2// Licensed under the MIT License.
 3
 4using System;
 5using System.Collections.Generic;
 6using System.Diagnostics;
 7using System.Globalization;
 8using System.IO;
 9using System.Runtime.InteropServices;
 10using System.Text;
 11using System.Threading;
 12using System.Threading.Tasks;
 13using System.Text.Json;
 14using System.Text.RegularExpressions;
 15using Azure.Core;
 16using Azure.Core.Pipeline;
 17
 18namespace 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
 233        private static readonly string DefaultPathWindows = $"{EnvironmentVariables.ProgramFilesX86}\\Microsoft SDKs\\Az
 234        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/";
 237        private static readonly string DefaultPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? DefaultPathWi
 238        private static readonly string DefaultWorkingDir = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Default
 39
 240        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()
 5251            : this(CredentialPipeline.GetInstance(null), default)
 5252        { }
 53
 13854        internal AzureCliCredential(CredentialPipeline pipeline, IProcessService processService)
 55        {
 13856            _pipeline = pipeline;
 13857            _path = !string.IsNullOrEmpty(EnvironmentVariables.Path) ? EnvironmentVariables.Path : DefaultPath;
 13858            _processService = processService ?? ProcessService.Default;
 13859        }
 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        {
 5269            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        {
 5280            return await GetTokenImplAsync(true, requestContext, cancellationToken).ConfigureAwait(false);
 2681        }
 82
 83        private async ValueTask<AccessToken> GetTokenImplAsync(bool async, TokenRequestContext requestContext, Cancellat
 84        {
 10485            using CredentialDiagnosticScope scope = _pipeline.StartGetTokenScope("AzureCliCredential.GetToken", requestC
 86
 87            try
 88            {
 10489                AccessToken token = await RequestCliAccessTokenAsync(async, requestContext.Scopes, cancellationToken).Co
 5090                return scope.Succeeded(token);
 91            }
 5492            catch (Exception e)
 93            {
 5494                throw scope.FailWrapAndThrow(e);
 95            }
 5096        }
 97
 98        private async ValueTask<AccessToken> RequestCliAccessTokenAsync(bool async, string[] scopes, CancellationToken c
 99        {
 104100            string resource = ScopeUtilities.ScopesToResource(scopes);
 101
 104102            ScopeUtilities.ValidateScope(resource);
 103
 104104            GetFileNameAndArguments(resource, out string fileName, out string argument);
 104105            ProcessStartInfo processStartInfo = GetAzureCliProcessStartInfo(fileName, argument);
 104106            var processRunner = new ProcessRunner(_processService.Create(processStartInfo), TimeSpan.FromMilliseconds(Cl
 107
 108            string output;
 109            try
 110            {
 104111                output = async ? await processRunner.RunAsync().ConfigureAwait(false) : processRunner.Run();
 70112            }
 6113            catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
 114            {
 2115                throw new AuthenticationFailedException(AzureCliTimeoutError);
 116            }
 28117            catch (InvalidOperationException exception)
 118            {
 28119                bool isWinError = exception.Message.StartsWith(WinAzureCLIError, StringComparison.CurrentCultureIgnoreCa
 120
 28121                bool isOtherOsError = AzNotFoundPattern.IsMatch(exception.Message);
 122
 28123                if (isWinError || isOtherOsError)
 124                {
 16125                    throw new CredentialUnavailableException(AzureCLINotInstalled);
 126                }
 127
 12128                bool isLoginError = exception.Message.IndexOf("az login", StringComparison.OrdinalIgnoreCase) != -1 || e
 129
 12130                if (isLoginError)
 131                {
 4132                    throw new CredentialUnavailableException(AzNotLogIn);
 133                }
 134
 8135                throw new AuthenticationFailedException($"{AzureCliFailedError} {exception.Message}");
 136            }
 137
 70138            return DeserializeOutput(output);
 50139        }
 140
 141        private ProcessStartInfo GetAzureCliProcessStartInfo(string fileName, string argument) =>
 104142            new ProcessStartInfo
 104143            {
 104144                FileName = fileName,
 104145                Arguments = argument,
 104146                UseShellExecute = false,
 104147                ErrorDialog = false,
 104148                CreateNoWindow = true,
 104149                WorkingDirectory = DefaultWorkingDir,
 104150                Environment = {{"PATH", _path}}
 104151            };
 152
 153        private static void GetFileNameAndArguments(string resource, out string fileName, out string argument)
 154        {
 104155            string command = $"az account get-access-token --output json --resource {resource}";
 156
 104157            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
 104158                fileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "cmd.exe");
 104159                argument = $"/c \"{command}\"";
 160            } else {
 0161                fileName = "/bin/sh";
 0162                argument = $"-c \"{command}\"";
 163            }
 0164        }
 165
 166        private static AccessToken DeserializeOutput(string output)
 167        {
 70168            using JsonDocument document = JsonDocument.Parse(output);
 169
 66170            JsonElement root = document.RootElement;
 66171            string accessToken = root.GetProperty("accessToken").GetString();
 54172            DateTimeOffset expiresOn = root.TryGetProperty("expiresIn", out JsonElement expiresIn)
 54173                ? DateTimeOffset.UtcNow + TimeSpan.FromSeconds(expiresIn.GetInt64())
 54174                : DateTimeOffset.ParseExact(root.GetProperty("expiresOn").GetString(), "yyyy-MM-dd HH:mm:ss.ffffff", Cul
 175
 50176            return new AccessToken(accessToken, expiresOn);
 50177        }
 178    }
 179}