< Summary

Class:Azure.Identity.ClientCertificateCredential
Assembly:Azure.Identity
File(s):C:\Git\azure-sdk-for-net\sdk\identity\Azure.Identity\src\ClientCertificateCredential.cs
Covered lines:91
Uncovered lines:17
Coverable lines:108
Total lines:384
Line coverage:84.2% (91 of 108)
Covered branches:32
Total branches:44
Branch coverage:72.7% (32 of 44)

Metrics

MethodCyclomatic complexity Line coverage Branch coverage
get_TenantId()-100%100%
get_ClientId()-100%100%
get_ClientCertificateProvider()-100%100%
.ctor()-100%100%
.ctor(...)-100%100%
.ctor(...)-100%100%
.ctor(...)-0%100%
.ctor(...)-50%100%
.ctor(...)-100%100%
.ctor(...)-0%100%
.ctor(...)-100%100%
.ctor(...)-100%100%
.ctor(...)-100%100%
GetToken(...)-100%100%
GetTokenAsync()-100%100%
get_Certificate()-100%100%
.ctor(...)-100%50%
GetCertificateAsync(...)-100%100%
get_Certificate()-100%100%
get_CertificatePath()-100%100%
.ctor(...)-100%50%
GetCertificateAsync(...)-85.71%83.33%
LoadCertificateFromPfxFileAsync()-89.47%75%
LoadCertificateFromPemFileAsync()-72.73%50%

File(s)

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

#LineLine coverage
 1// Copyright (c) Microsoft Corporation. All rights reserved.
 2// Licensed under the MIT License.
 3
 4
 5using Azure.Core;
 6using Azure.Core.Pipeline;
 7using Microsoft.Identity.Client;
 8using System;
 9using System.Collections.Generic;
 10using System.IO;
 11using System.Reflection;
 12using System.Security.Cryptography;
 13using System.Security.Cryptography.X509Certificates;
 14using System.Text.RegularExpressions;
 15using System.Threading;
 16using System.Threading.Tasks;
 17
 18namespace Azure.Identity
 19{
 20    /// <summary>
 21    /// Enables authentication of a service principal in to Azure Active Directory using a X509 certificate that is assi
 22    /// on how to configure certificate authentication can be found here:
 23    /// https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-certificate-credentials#registe
 24    /// </summary>
 25    public class ClientCertificateCredential : TokenCredential
 26    {
 27        /// <summary>
 28        /// Gets the Azure Active Directory tenant (directory) Id of the service principal
 29        /// </summary>
 430        internal string TenantId { get; }
 31
 32        /// <summary>
 33        /// Gets the client (application) ID of the service principal
 34        /// </summary>
 435        internal string ClientId { get; }
 36
 437        internal IX509Certificate2Provider ClientCertificateProvider { get; }
 38
 39        private readonly MsalConfidentialClient _client;
 40        private readonly CredentialPipeline _pipeline;
 41
 42        /// <summary>
 43        /// Protected constructor for mocking.
 44        /// </summary>
 2445        protected ClientCertificateCredential()
 46        {
 2447        }
 48
 49        /// <summary>
 50        /// Creates an instance of the ClientCertificateCredential with the details needed to authenticate against Azure
 51        /// </summary>
 52        /// <param name="tenantId">The Azure Active Directory tenant (directory) Id of the service principal.</param>
 53        /// <param name="clientId">The client (application) ID of the service principal</param>
 54        /// <param name="clientCertificatePath">The path to a file which contains both the client certificate and privat
 55        public ClientCertificateCredential(string tenantId, string clientId, string clientCertificatePath)
 2856            : this(tenantId, clientId, clientCertificatePath, null, null, null)
 57        {
 1658        }
 59
 60        /// <summary>
 61        /// Creates an instance of the ClientCertificateCredential with the details needed to authenticate against Azure
 62        /// </summary>
 63        /// <param name="tenantId">The Azure Active Directory tenant (directory) Id of the service principal.</param>
 64        /// <param name="clientId">The client (application) ID of the service principal</param>
 65        /// <param name="clientCertificatePath">The path to a file which contains both the client certificate and privat
 66        /// <param name="options">Options that allow to configure the management of the requests sent to the Azure Activ
 67        public ClientCertificateCredential(string tenantId, string clientId, string clientCertificatePath, TokenCredenti
 2068            : this(tenantId, clientId, clientCertificatePath, options, null, null)
 69        {
 2070        }
 71
 72        /// <summary>
 73        /// Creates an instance of the ClientCertificateCredential with the details needed to authenticate against Azure
 74        /// </summary>
 75        /// <param name="tenantId">The Azure Active Directory tenant (directory) Id of the service principal.</param>
 76        /// <param name="clientId">The client (application) ID of the service principal</param>
 77        /// <param name="clientCertificatePath">The path to a file which contains both the client certificate and privat
 78        /// <param name="options">Options that allow to configure the management of the requests sent to the Azure Activ
 79        internal ClientCertificateCredential(string tenantId, string clientId, string clientCertificatePath, ClientCerti
 080            : this(tenantId, clientId, clientCertificatePath, options, null, null)
 081        { }
 82
 83        /// <summary>
 84        /// Creates an instance of the ClientCertificateCredential with the details needed to authenticate against Azure
 85        /// </summary>
 86        /// <param name="tenantId">The Azure Active Directory tenant (directory) Id of the service principal.</param>
 87        /// <param name="clientId">The client (application) ID of the service principal</param>
 88        /// <param name="clientCertificate">The authentication X509 Certificate of the service principal</param>
 89        public ClientCertificateCredential(string tenantId, string clientId, X509Certificate2 clientCertificate)
 1290            : this(tenantId, clientId, clientCertificate, null, null, null)
 91        {
 092        }
 93
 94        /// <summary>
 95        /// Creates an instance of the ClientCertificateCredential with the details needed to authenticate against Azure
 96        /// </summary>
 97        /// <param name="tenantId">The Azure Active Directory tenant (directory) Id of the service principal.</param>
 98        /// <param name="clientId">The client (application) ID of the service principal</param>
 99        /// <param name="clientCertificate">The authentication X509 Certificate of the service principal</param>
 100        /// <param name="options">Options that allow to configure the management of the requests sent to the Azure Activ
 101        public ClientCertificateCredential(string tenantId, string clientId, X509Certificate2 clientCertificate, TokenCr
 32102            : this(tenantId, clientId, clientCertificate, options, null, null) {}
 103
 104        /// <summary>
 105        /// Creates an instance of the ClientCertificateCredential with the details needed to authenticate against Azure
 106        /// </summary>
 107        /// <param name="tenantId">The Azure Active Directory tenant (directory) Id of the service principal.</param>
 108        /// <param name="clientId">The client (application) ID of the service principal</param>
 109        /// <param name="clientCertificate">The authentication X509 Certificate of the service principal</param>
 110        /// <param name="options">Options that allow to configure the management of the requests sent to the Azure Activ
 111        internal ClientCertificateCredential(string tenantId, string clientId, X509Certificate2 clientCertificate, Clien
 0112            : this(tenantId, clientId, clientCertificate, options, null, null)
 113        {
 0114        }
 115
 116        internal ClientCertificateCredential(string tenantId, string clientId, string certificatePath, TokenCredentialOp
 56117            : this(tenantId, clientId, new X509Certificate2FromFileProvider(certificatePath ?? throw new ArgumentNullExc
 118        {
 44119        }
 120
 121        internal ClientCertificateCredential(string tenantId, string clientId, X509Certificate2 certificate, TokenCreden
 40122            : this(tenantId, clientId, new X509Certificate2FromObjectProvider(certificate ?? throw new ArgumentNullExcep
 123        {
 28124        }
 125
 88126        internal ClientCertificateCredential(string tenantId, string clientId, IX509Certificate2Provider certificateProv
 127        {
 88128            TenantId = tenantId ?? throw new ArgumentNullException(nameof(tenantId));
 129
 80130            ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
 131
 72132            ClientCertificateProvider = certificateProvider;
 133
 72134            _pipeline = pipeline ?? CredentialPipeline.GetInstance(options);
 135
 72136            _client = client ?? new MsalConfidentialClient(_pipeline, tenantId, clientId, certificateProvider, options a
 72137        }
 138
 139        /// <summary>
 140        /// Obtains a token from the Azure Active Directory service, using the specified X509 certificate to authenticat
 141        /// </summary>
 142        /// <param name="requestContext">The details of the authentication request.</param>
 143        /// <param name="cancellationToken">A <see cref="CancellationToken"/> controlling the request lifetime.</param>
 144        /// <returns>An <see cref="AccessToken"/> which can be used to authenticate service client calls.</returns>
 145        public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken = d
 146        {
 28147            using CredentialDiagnosticScope scope = _pipeline.StartGetTokenScope("ClientCertificateCredential.GetToken",
 148
 149            try
 150            {
 28151                AuthenticationResult result = _client.AcquireTokenForClientAsync(requestContext.Scopes, false, cancellat
 152
 2153                return scope.Succeeded(new AccessToken(result.AccessToken, result.ExpiresOn));
 154            }
 26155            catch (Exception e)
 156            {
 26157                throw scope.FailWrapAndThrow(e);
 158            }
 2159        }
 160
 161        /// <summary>
 162        /// Obtains a token from the Azure Active Directory service, using the specified X509 certificate to authenticat
 163        /// </summary>
 164        /// <param name="requestContext">The details of the authentication request.</param>
 165        /// <param name="cancellationToken">A <see cref="CancellationToken"/> controlling the request lifetime.</param>
 166        /// <returns>An <see cref="AccessToken"/> which can be used to authenticate service client calls.</returns>
 167        public override async ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken
 168        {
 68169            using CredentialDiagnosticScope scope = _pipeline.StartGetTokenScope("ClientCertificateCredential.GetToken",
 170
 171            try
 172            {
 68173                AuthenticationResult result = await _client.AcquireTokenForClientAsync(requestContext.Scopes, true, canc
 174
 38175                return scope.Succeeded(new AccessToken(result.AccessToken, result.ExpiresOn));
 176            }
 30177            catch (Exception e)
 178            {
 30179                throw scope.FailWrapAndThrow(e);
 180            }
 38181        }
 182
 183        /// <summary>
 184        /// IX509Certificate2Provider provides a way to control how the X509Certificate2 object is fetched.
 185        /// </summary>
 186        internal interface IX509Certificate2Provider
 187        {
 188            ValueTask<X509Certificate2> GetCertificateAsync(bool async, CancellationToken cancellationToken);
 189        }
 190
 191        /// <summary>
 192        /// X509Certificate2FromObjectProvider provides an X509Certificate2 from an existing instance.
 193        /// </summary>
 194        private class X509Certificate2FromObjectProvider : IX509Certificate2Provider
 195        {
 16196            private X509Certificate2 Certificate { get; }
 197
 36198            public X509Certificate2FromObjectProvider(X509Certificate2 clientCertificate)
 199            {
 36200                Certificate = clientCertificate ?? throw new ArgumentNullException(nameof(clientCertificate));
 36201            }
 202
 203            public ValueTask<X509Certificate2> GetCertificateAsync(bool async, CancellationToken cancellationToken)
 204            {
 16205                return new ValueTask<X509Certificate2>(Certificate);
 206            }
 207        }
 208
 209        /// <summary>
 210        /// X509Certificate2FromFileProvider provides an X509Certificate2 from a file on disk.  It supports both
 211        /// "pfx" and "pem" encoded certificates.
 212        /// </summary>
 213        internal class X509Certificate2FromFileProvider : IX509Certificate2Provider
 214        {
 215            // Lazy initialized on the first call to GetCertificateAsync, based on CertificatePath.
 120216            private X509Certificate2 Certificate { get; set; }
 72217            internal string CertificatePath { get; }
 218
 52219            public X509Certificate2FromFileProvider(string clientCertificatePath)
 220            {
 52221                CertificatePath = clientCertificatePath ?? throw new ArgumentNullException(nameof(clientCertificatePath)
 52222            }
 223
 224            public ValueTask<X509Certificate2> GetCertificateAsync(bool async, CancellationToken cancellationToken)
 225            {
 36226                if (!(Certificate is null))
 227                {
 0228                    return new ValueTask<X509Certificate2>(Certificate);
 229                }
 230
 36231                string fileType = Path.GetExtension(CertificatePath);
 232
 36233                switch (fileType.ToLowerInvariant())
 234                {
 235                    case ".pfx":
 12236                        return LoadCertificateFromPfxFileAsync(async, CertificatePath, cancellationToken);
 237                    case ".pem":
 20238                        return LoadCertificateFromPemFileAsync(async, CertificatePath, cancellationToken);
 239                    default:
 4240                        throw new CredentialUnavailableException("Only .pfx and .pem files are supported.");
 241                }
 242            }
 243
 244            private async ValueTask<X509Certificate2> LoadCertificateFromPfxFileAsync(bool async, string clientCertifica
 245            {
 246                const int BufferSize = 4 * 1024;
 247
 12248                if (!(Certificate is null))
 249                {
 0250                    return Certificate;
 251                }
 252
 253                try
 254                {
 12255                    if (!async)
 256                    {
 0257                        Certificate = new X509Certificate2(clientCertificatePath);
 258                    }
 259                    else
 260                    {
 12261                        List<byte> certContents = new List<byte>();
 12262                        byte[] buf = new byte[BufferSize];
 12263                        int offset = 0;
 12264                        using (Stream s = File.OpenRead(clientCertificatePath))
 265                        {
 266                            while (true)
 267                            {
 28268                                int read = await s.ReadAsync(buf, offset, buf.Length, cancellationToken).ConfigureAwait(
 75232269                                for (int i = 0; i < read; i++)
 270                                {
 37588271                                    certContents.Add(buf[i]);
 272                                }
 273
 28274                                if (read == 0)
 275                                {
 276                                    break;
 277                                }
 278                            }
 12279                        }
 280
 12281                        Certificate = new X509Certificate2(certContents.ToArray());
 8282                    }
 283
 8284                    return Certificate;
 285                }
 4286                catch (Exception e) when (!(e is OperationCanceledException))
 287                {
 4288                    throw new CredentialUnavailableException("Could not load certificate file", e);
 289                }
 8290            }
 291
 292            private async ValueTask<X509Certificate2> LoadCertificateFromPemFileAsync(bool async, string clientCertifica
 293            {
 20294                if (!(Certificate is null))
 295                {
 0296                    return Certificate;
 297                }
 298
 299                string certficateText;
 300
 301                try
 302                {
 20303                    if (!async)
 304                    {
 0305                        certficateText = File.ReadAllText(clientCertificatePath);
 306                    }
 307                    else
 308                    {
 20309                        cancellationToken.ThrowIfCancellationRequested();
 310
 20311                        using (StreamReader sr = new StreamReader(clientCertificatePath))
 312                        {
 16313                            certficateText = await sr.ReadToEndAsync().ConfigureAwait(false);
 16314                        }
 315                    }
 316
 16317                    Regex certificateRegex = new Regex("(-+BEGIN CERTIFICATE-+)(\n\r?|\r\n?)([A-Za-z0-9+/\n\r]+=*)(\n\r?
 16318                    Regex privateKeyRegex = new Regex("(-+BEGIN PRIVATE KEY-+)(\n\r?|\r\n?)([A-Za-z0-9+/\n\r]+=*)(\n\r?|
 319
 16320                    Match certificateMatch = certificateRegex.Match(certficateText);
 16321                    Match privateKeyMatch = privateKeyRegex.Match(certficateText);
 322
 16323                    if (!certificateMatch.Success)
 324                    {
 0325                        throw new InvalidDataException("Could not find certificate in PEM file");
 326                    }
 327
 16328                    if (!privateKeyMatch.Success)
 329                    {
 0330                        throw new InvalidDataException("Could not find private key in PEM file");
 331                    }
 332
 333                    // ImportPkcs8PrivateKey was added in .NET Core 3.0, it is only present on Core.  If we can't find t
 16334                    MethodInfo importPkcs8PrivateKeyMethodInfo = typeof(RSA).GetMethod("ImportPkcs8PrivateKey", BindingF
 335
 336                    // CopyWithPrivateKey is present in .NET Core 2.0+ and .NET 4.7.2+.
 16337                    MethodInfo copyWithPrivateKeyMethodInfo = typeof(RSACertificateExtensions).GetMethod("CopyWithPrivat
 338
 16339                    if (copyWithPrivateKeyMethodInfo == null)
 340                    {
 0341                        throw new PlatformNotSupportedException("The current platform does not support reading a private
 342                    }
 343
 344                    RSA privateKey;
 345
 16346                    if (importPkcs8PrivateKeyMethodInfo != null)
 347                    {
 0348                        privateKey = RSA.Create();
 349
 350                        // Because ImportPkcs8PrivateKey takes a ReadOnlySpan<byte> as an argument, we can not call it d
 351                        // have to be passed to MethodInfo.Invoke in an object array, and you can't put a byref type lik
 352                        // correct signature bound to the privateKey we want to import into and invoke that.
 0353                        ImportPkcs8PrivateKeyDelegate importPrivateKey = (ImportPkcs8PrivateKeyDelegate)importPkcs8Priva
 0354                        importPrivateKey(Convert.FromBase64String(privateKeyMatch.Groups[3].Value), out int _);
 355                    }
 356                    else
 357                    {
 16358                        privateKey = LightweightPkcs8Decoder.DecodeRSAPkcs8(Convert.FromBase64String(privateKeyMatch.Gro
 359                    }
 360
 12361                    X509Certificate2 certWithoutPrivateKey = new X509Certificate2(Convert.FromBase64String(certificateMa
 12362                    Certificate = (X509Certificate2)copyWithPrivateKeyMethodInfo.Invoke(null, new object[] { certWithout
 363
 364                    // On desktop NetFX it appears the PrivateKey property is not initialized after calling CopyWithPriv
 365                    // this leads to an issue when using the MSAL ConfidentialClient which uses the PrivateKey property 
 366                    // signing key vs. the extension method GetRsaPrivateKey which we were previously using when signing
 367                    // Because of this we need to set PrivateKey to the instance we created to deserialize the private k
 12368                    if (Certificate.PrivateKey == null)
 369                    {
 0370                        Certificate.PrivateKey = privateKey;
 371                    }
 372
 12373                    return Certificate;
 374                }
 8375                catch (Exception e) when (!(e is OperationCanceledException))
 376                {
 8377                    throw new CredentialUnavailableException("Could not load certificate file", e);
 378                }
 12379            }
 380
 381            private delegate void ImportPkcs8PrivateKeyDelegate(ReadOnlySpan<byte> blob, out int bytesRead);
 382        }
 383    }
 384}