< Summary

Class:Azure.Identity.ManagedIdentityClient
Assembly:Azure.Identity
File(s):C:\Git\azure-sdk-for-net\sdk\identity\Azure.Identity\src\ManagedIdentityClient.cs
Covered lines:143
Uncovered lines:30
Coverable lines:173
Total lines:478
Line coverage:82.6% (143 of 173)
Covered branches:63
Total branches:87
Branch coverage:72.4% (63 of 87)

Metrics

MethodCyclomatic complexity Line coverage Branch coverage
.cctor()-100%100%
.ctor()-0%100%
.ctor(...)-100%100%
get_ClientId()-100%100%
Authenticate(...)-64.29%50%
AuthenticateAsync()-64.29%47.06%
GetMsiType(...)-90.48%100%
GetMsiTypeAsync()-90.48%100%
ImdsAvailable(...)-71.43%50%
ImdsAvailableAsync()-73.33%50%
CreateAuthRequest(...)-85.71%75%
CreateImdsAuthRequest(...)-100%100%
CreateAppServiceAuthRequest(...)-100%100%
CreateCloudShellAuthRequest(...)-100%100%
DeserializeAsync()-100%100%
Deserialize(...)-100%100%
Deserialize(...)-82.35%77.27%
get_Code()-0%100%
get_Message()-0%100%

File(s)

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

#LineLine coverage
 1// Copyright (c) Microsoft Corporation. All rights reserved.
 2// Licensed under the MIT License.
 3
 4using System;
 5using System.Globalization;
 6using System.IO;
 7using System.Net;
 8using System.Net.Sockets;
 9using System.Text;
 10using System.Text.Json;
 11using System.Threading;
 12using System.Threading.Tasks;
 13using Azure.Core;
 14
 15namespace Azure.Identity
 16{
 17    internal class ManagedIdentityClient
 18    {
 19        private const string AuthenticationResponseInvalidFormatError = "Invalid response, the authentication response w
 20        private const string MsiEndpointInvalidUriError = "The environment variable MSI_ENDPOINT contains an invalid Uri
 21        internal const string MsiUnavailableError = "ManagedIdentityCredential authentication unavailable. No Managed Id
 22        internal const string IdentityUnavailableError = "ManagedIdentityCredential authentication unavailable. The requ
 23
 24        // IMDS constants. Docs for IMDS are available here https://docs.microsoft.com/en-us/azure/active-directory/mana
 225        private static readonly Uri s_imdsEndpoint = new Uri("http://169.254.169.254/metadata/identity/oauth2/token");
 226        private static readonly IPAddress s_imdsHostIp = IPAddress.Parse("169.254.169.254");
 27        private const int s_imdsPort = 80;
 28        private const string ImdsApiVersion = "2018-02-01";
 29        private const int ImdsAvailableTimeoutMs = 1000;
 30
 31        // MSI Constants. Docs for MSI are available here https://docs.microsoft.com/en-us/azure/app-service/overview-ma
 32        private const string AppServiceMsiApiVersion = "2017-09-01";
 33
 234        private static readonly SemaphoreSlim _initLock = new SemaphoreSlim(1, 1);
 35        private MsiType _msiType;
 36        private Uri _endpoint;
 37
 38        private readonly CredentialPipeline _pipeline;
 39
 040        protected ManagedIdentityClient()
 41        {
 042        }
 43
 7044        public ManagedIdentityClient(CredentialPipeline pipeline, string clientId = null)
 45        {
 7046            _pipeline = pipeline;
 47
 7048            ClientId = clientId;
 7049        }
 50
 4051        protected string ClientId { get; }
 52
 53        public virtual AccessToken Authenticate(string[] scopes, CancellationToken cancellationToken)
 54        {
 3255            MsiType msiType = GetMsiType(cancellationToken);
 56
 57            // if msi is unavailable or we were unable to determine the type return CredentialUnavailable exception that
 2858            if (msiType == MsiType.Unavailable || msiType == MsiType.Unknown)
 59            {
 1660                throw new CredentialUnavailableException(MsiUnavailableError);
 61            }
 62
 1263            using Request request = CreateAuthRequest(msiType, scopes);
 64
 1265            Response response = _pipeline.HttpPipeline.SendRequest(request, cancellationToken);
 66
 1267            if (response.Status == 200)
 68            {
 1269                AccessToken result = Deserialize(response.ContentStream);
 70
 1271                return result;
 72            }
 73
 074            if (response.Status == 400 && msiType == MsiType.Imds)
 75            {
 076                _msiType = MsiType.Unavailable;
 77
 078                string message = _pipeline.Diagnostics.CreateRequestFailedMessage(response, message: IdentityUnavailable
 79
 080                throw new CredentialUnavailableException(message);
 81            }
 82
 083            throw _pipeline.Diagnostics.CreateRequestFailedException(response);
 1284        }
 85
 86        public virtual async Task<AccessToken> AuthenticateAsync(string[] scopes, CancellationToken cancellationToken)
 87        {
 3088            MsiType msiType = await GetMsiTypeAsync(cancellationToken).ConfigureAwait(false);
 89
 90            // if msi is unavailable or we were unable to determine the type return CredentialUnavailable exception that
 2691            if (msiType == MsiType.Unavailable || msiType == MsiType.Unknown)
 92            {
 1493                throw new CredentialUnavailableException(MsiUnavailableError);
 94            }
 95
 1296            using Request request = CreateAuthRequest(msiType, scopes);
 97
 1298            Response response = await _pipeline.HttpPipeline.SendRequestAsync(request, cancellationToken).ConfigureAwait
 99
 12100            if (response.Status == 200)
 101            {
 12102                AccessToken result = await DeserializeAsync(response.ContentStream, cancellationToken).ConfigureAwait(fa
 103
 12104                return result;
 105            }
 106
 0107            if (response.Status == 400 && msiType == MsiType.Imds)
 108            {
 0109                _msiType = MsiType.Unavailable;
 110
 0111                string message = await _pipeline.Diagnostics.CreateRequestFailedMessageAsync(response, message: Identity
 112
 0113                throw new CredentialUnavailableException(message);
 114            }
 115
 0116            throw await _pipeline.Diagnostics.CreateRequestFailedExceptionAsync(response).ConfigureAwait(false);
 12117        }
 118
 119        protected virtual MsiType GetMsiType(CancellationToken cancellationToken)
 120        {
 121            // if we haven't already determined the msi type
 28122            if (_msiType == MsiType.Unknown)
 123            {
 124                // acquire the init lock
 26125                _initLock.Wait(cancellationToken);
 126
 127                try
 128                {
 129                    // check again if the we already determined the msiType now that we hold the lock
 24130                    if (_msiType == MsiType.Unknown)
 131                    {
 24132                        string endpointEnvVar = EnvironmentVariables.MsiEndpoint;
 24133                        string secretEnvVar = EnvironmentVariables.MsiSecret;
 134
 135                        // if the env var MSI_ENDPOINT is set
 24136                        if (!string.IsNullOrEmpty(endpointEnvVar))
 137                        {
 138                            try
 139                            {
 8140                                _endpoint = new Uri(endpointEnvVar);
 8141                            }
 0142                            catch (FormatException ex)
 143                            {
 0144                                throw new AuthenticationFailedException(MsiEndpointInvalidUriError, ex);
 145                            }
 146
 147                            // if BOTH the env vars MSI_ENDPOINT and MSI_SECRET are set the MsiType is AppService
 8148                            if (!string.IsNullOrEmpty(secretEnvVar))
 149                            {
 4150                                _msiType = MsiType.AppService;
 151                            }
 152                            // if ONLY the env var MSI_ENDPOINT is set the MsiType is CloudShell
 153                            else
 154                            {
 4155                                _msiType = MsiType.CloudShell;
 156                            }
 157                        }
 158                        // if MSI_ENDPOINT is NOT set AND the IMDS endpoint is available the MsiType is Imds
 16159                        else if (ImdsAvailable(cancellationToken))
 160                        {
 4161                            _endpoint = s_imdsEndpoint;
 4162                            _msiType = MsiType.Imds;
 163                        }
 164                        // if MSI_ENDPOINT is NOT set and IMDS endpoint is not available ManagedIdentity is not availabl
 165                        else
 166                        {
 12167                            _msiType = MsiType.Unavailable;
 168                        }
 169                    }
 12170                }
 171                // release the init lock
 172                finally
 173                {
 24174                    _initLock.Release();
 24175                }
 176            }
 177
 26178            return _msiType;
 179        }
 180
 181        protected virtual async Task<MsiType> GetMsiTypeAsync(CancellationToken cancellationToken)
 182        {
 183            // if we haven't already determined the msi type
 26184            if (_msiType == MsiType.Unknown)
 185            {
 186                // acquire the init lock
 26187                await _initLock.WaitAsync(cancellationToken).ConfigureAwait(false);
 188
 189                try
 190                {
 191                    // check again if the we already determined the msiType now that we hold the lock
 24192                    if (_msiType == MsiType.Unknown)
 193                    {
 24194                        string endpointEnvVar = EnvironmentVariables.MsiEndpoint;
 24195                        string secretEnvVar = EnvironmentVariables.MsiSecret;
 196
 197                        // if the env var MSI_ENDPOINT is set
 24198                        if (!string.IsNullOrEmpty(endpointEnvVar))
 199                        {
 200                            try
 201                            {
 8202                                _endpoint = new Uri(endpointEnvVar);
 8203                            }
 0204                            catch (FormatException ex)
 205                            {
 0206                                throw new AuthenticationFailedException(MsiEndpointInvalidUriError, ex);
 207                            }
 208
 209                            // if BOTH the env vars MSI_ENDPOINT and MSI_SECRET are set the MsiType is AppService
 8210                            if (!string.IsNullOrEmpty(secretEnvVar))
 211                            {
 4212                                _msiType = MsiType.AppService;
 213                            }
 214                            // if ONLY the env var MSI_ENDPOINT is set the MsiType is CloudShell
 215                            else
 216                            {
 4217                                _msiType = MsiType.CloudShell;
 218                            }
 219                        }
 220                        // if MSI_ENDPOINT is NOT set AND the IMDS endpoint is available the MsiType is Imds
 16221                        else if (await ImdsAvailableAsync(cancellationToken).ConfigureAwait(false))
 222                        {
 4223                            _endpoint = s_imdsEndpoint;
 4224                            _msiType = MsiType.Imds;
 225                        }
 226                        // if MSI_ENDPOINT is NOT set and IMDS endpoint is not available ManagedIdentity is not availabl
 227                        else
 228                        {
 12229                            _msiType = MsiType.Unavailable;
 230                        }
 231                    }
 24232                }
 233                // release the init lock
 234                finally
 235                {
 24236                    _initLock.Release();
 237                }
 238            }
 239
 24240            return _msiType;
 24241        }
 242
 243        protected virtual bool ImdsAvailable(CancellationToken cancellationToken)
 244        {
 12245            AzureIdentityEventSource.Singleton.ProbeImdsEndpoint(s_imdsEndpoint);
 246
 247            bool available;
 248            // try to create a TCP connection to the IMDS IP address. If the connection can be established
 249            // we assume that IMDS is available. If connecting times out or fails to connect assume that
 250            // IMDS is not available in this environment.
 251            try
 252            {
 12253                using (var client = new TcpClient())
 254                {
 12255                    var result = client.BeginConnect(s_imdsHostIp, s_imdsPort, null, null);
 256
 12257                    var success = result.AsyncWaitHandle.WaitOne(ImdsAvailableTimeoutMs);
 258
 12259                    available = success && client.Connected;
 12260                }
 12261            }
 0262            catch
 263            {
 0264                available = false;
 0265            }
 266
 12267            if (available)
 268            {
 0269                AzureIdentityEventSource.Singleton.ImdsEndpointFound(s_imdsEndpoint);
 270            }
 271            else
 272            {
 12273                AzureIdentityEventSource.Singleton.ImdsEndpointUnavailable(s_imdsEndpoint);
 274            }
 275
 12276            return available;
 277        }
 278
 279        protected virtual async Task<bool> ImdsAvailableAsync(CancellationToken cancellationToken)
 280        {
 12281            AzureIdentityEventSource.Singleton.ProbeImdsEndpoint(s_imdsEndpoint);
 282
 283            bool available;
 284            // try to create a TCP connection to the IMDS IP address. If the connection can be established
 285            // we assume that IMDS is available. If connecting times out or fails to connect assume that
 286            // IMDS is not available in this environment.
 287            try
 288            {
 12289                using (var client = new TcpClient())
 290                {
 12291                    var result = client.BeginConnect(s_imdsHostIp, s_imdsPort, null, null);
 292
 24293                    var success = await Task.Run<bool>(() => result.AsyncWaitHandle.WaitOne(ImdsAvailableTimeoutMs), can
 294
 12295                    available = success && client.Connected;
 12296                }
 12297            }
 0298            catch
 299            {
 0300                available = false;
 0301            }
 302
 12303            if (available)
 304            {
 0305                AzureIdentityEventSource.Singleton.ImdsEndpointFound(s_imdsEndpoint);
 306            }
 307            else
 308            {
 12309                AzureIdentityEventSource.Singleton.ImdsEndpointUnavailable(s_imdsEndpoint);
 310            }
 311
 12312            return available;
 12313        }
 314
 315        private Request CreateAuthRequest(MsiType msiType, string[] scopes)
 316        {
 24317            return msiType switch
 24318            {
 32319                MsiType.Imds => CreateImdsAuthRequest(scopes),
 32320                MsiType.AppService => CreateAppServiceAuthRequest(scopes),
 32321                MsiType.CloudShell => CreateCloudShellAuthRequest(scopes),
 0322                _ => default,
 24323            };
 324        }
 325
 326        private Request CreateImdsAuthRequest(string[] scopes)
 327        {
 328            // covert the scopes to a resource string
 8329            string resource = ScopeUtilities.ScopesToResource(scopes);
 330
 8331            Request request = _pipeline.HttpPipeline.CreateRequest();
 332
 8333            request.Method = RequestMethod.Get;
 334
 8335            request.Headers.Add("Metadata", "true");
 336
 8337            request.Uri.Reset(_endpoint);
 338
 8339            request.Uri.AppendQuery("api-version", ImdsApiVersion);
 340
 8341            request.Uri.AppendQuery("resource", resource);
 342
 8343            if (!string.IsNullOrEmpty(ClientId))
 344            {
 8345                request.Uri.AppendQuery("client_id", ClientId);
 346            }
 347
 8348            return request;
 349        }
 350
 351        private Request CreateAppServiceAuthRequest(string[] scopes)
 352        {
 353            // covert the scopes to a resource string
 8354            string resource = ScopeUtilities.ScopesToResource(scopes);
 355
 8356            Request request = _pipeline.HttpPipeline.CreateRequest();
 357
 8358            request.Method = RequestMethod.Get;
 359
 8360            request.Headers.Add("secret", EnvironmentVariables.MsiSecret);
 361
 8362            request.Uri.Reset(_endpoint);
 363
 8364            request.Uri.AppendQuery("api-version", AppServiceMsiApiVersion);
 365
 8366            request.Uri.AppendQuery("resource", resource);
 367
 8368            if (!string.IsNullOrEmpty(ClientId))
 369            {
 4370                request.Uri.AppendQuery("clientid", ClientId);
 371            }
 372
 8373            return request;
 374        }
 375
 376        private Request CreateCloudShellAuthRequest(string[] scopes)
 377        {
 378            // covert the scopes to a resource string
 8379            string resource = ScopeUtilities.ScopesToResource(scopes);
 380
 8381            Request request = _pipeline.HttpPipeline.CreateRequest();
 382
 8383            request.Method = RequestMethod.Post;
 384
 8385            request.Headers.Add(HttpHeader.Common.FormUrlEncodedContentType);
 386
 8387            request.Uri.Reset(_endpoint);
 388
 8389            request.Headers.Add("Metadata", "true");
 390
 8391            var bodyStr = $"resource={Uri.EscapeDataString(resource)}";
 392
 8393            if (!string.IsNullOrEmpty(ClientId))
 394            {
 4395                bodyStr += $"&client_id={Uri.EscapeDataString(ClientId)}";
 396            }
 397
 8398            ReadOnlyMemory<byte> content = Encoding.UTF8.GetBytes(bodyStr).AsMemory();
 399
 8400            request.Content = RequestContent.Create(content);
 401
 8402            return request;
 403        }
 404
 405        private async Task<AccessToken> DeserializeAsync(Stream content, CancellationToken cancellationToken)
 406        {
 12407            using (JsonDocument json = await JsonDocument.ParseAsync(content, default, cancellationToken).ConfigureAwait
 408            {
 12409                return Deserialize(json.RootElement);
 410            }
 12411        }
 412
 413        private AccessToken Deserialize(Stream content)
 414        {
 12415            using (JsonDocument json = JsonDocument.Parse(content))
 416            {
 12417                return Deserialize(json.RootElement);
 418            }
 12419        }
 420
 421        private AccessToken Deserialize(JsonElement json)
 422        {
 24423            string accessToken = null;
 24424            JsonElement? expiresOnProp = null;
 425
 144426            foreach (JsonProperty prop in json.EnumerateObject())
 427            {
 48428                switch (prop.Name)
 429                {
 430                    case "access_token":
 24431                        accessToken = prop.Value.GetString();
 24432                        break;
 433
 434                    case "expires_on":
 24435                        expiresOnProp = prop.Value;
 436                        break;
 437                }
 438            }
 439
 24440            if (accessToken is null || !expiresOnProp.HasValue)
 441            {
 0442                throw new AuthenticationFailedException(AuthenticationResponseInvalidFormatError);
 443            }
 444
 445            DateTimeOffset expiresOn;
 446            // if s_msiType is AppService expires_on will be a string formatted datetimeoffset
 24447            if (_msiType == MsiType.AppService)
 448            {
 8449                if (!DateTimeOffset.TryParse(expiresOnProp.Value.GetString(), CultureInfo.InvariantCulture, DateTimeStyl
 450                {
 0451                    throw new AuthenticationFailedException(AuthenticationResponseInvalidFormatError);
 452                }
 453            }
 454            // otherwise expires_on will be a unix timestamp seconds from epoch
 455            else
 456            {
 457                // the seconds from epoch may be returned as a Json number or a Json string which is a number
 458                // depending on the environment.  If neither of these are the case we throw an AuthException.
 16459                if (!(expiresOnProp.Value.ValueKind == JsonValueKind.Number && expiresOnProp.Value.TryGetInt64(out long 
 16460                    !(expiresOnProp.Value.ValueKind == JsonValueKind.String && long.TryParse(expiresOnProp.Value.GetStri
 461                {
 0462                    throw new AuthenticationFailedException(AuthenticationResponseInvalidFormatError);
 463                }
 464
 16465                expiresOn = DateTimeOffset.FromUnixTimeSeconds(expiresOnSec);
 466            }
 467
 24468            return new AccessToken(accessToken, expiresOn);
 469        }
 470
 471        private struct Error
 472        {
 0473            public string Code { get; set; }
 474
 0475            public string Message { get; set; }
 476        }
 477    }
 478}