22// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33
44using System ;
5+ using System . Linq ;
6+ using System . Security . Cryptography . X509Certificates ;
57using Azure ;
68using Azure . Core ;
79using Azure . Identity ;
@@ -57,6 +59,19 @@ public static CloudBlobClientWrapper UsingMsi(
5759 return new CloudBlobClientWrapper ( storageConnectionString , tokenCredential , readAccessGeoRedundant , requestTimeout ) ;
5860 }
5961
62+ public static CloudBlobClientWrapper UsingServicePrincipal (
63+ string storageConnectionString ,
64+ string appID ,
65+ string subjectAlternativeName ,
66+ string tenantId ,
67+ string authorityHost ,
68+ bool readAccessGeoRedundant = false ,
69+ TimeSpan ? requestTimeout = null )
70+ {
71+ var tokenCredential = GetCredentialUsingServicePrincipal ( appID , subjectAlternativeName , tenantId , authorityHost ) ;
72+ return new CloudBlobClientWrapper ( storageConnectionString , tokenCredential , readAccessGeoRedundant , requestTimeout ) ;
73+ }
74+
6075 public ISimpleCloudBlob GetBlobFromUri ( Uri uri )
6176 {
6277 // For Azure blobs, the query string is assumed to be the SAS token.
@@ -217,5 +232,36 @@ private string GetSecondaryConnectionString()
217232 . Replace ( $ "AccountName={ primaryAccountName } ;", $ "AccountName={ secondaryAccountName } ;") ;
218233 return secondaryConnectionString ;
219234 }
235+
236+ /// <summary>
237+ /// Gets credential using the Service Principal. If the resource is in a different tenant, this is how to access it.
238+ /// The ServicePrincipal needs to be a "Storage Table/Blob/Queue Data Contributor" role on the storage account. Owner isn't enough.
239+ /// </summary>
240+ /// <returns>ClientCertificatCredential to be used to communicate with Storage.</returns>
241+ private static ClientCertificateCredential GetCredentialUsingServicePrincipal ( string appID , string subjectAlternativeName , string tenantId , string authorityHost )
242+ {
243+ X509Certificate2 clientCert ;
244+
245+ // Azure.Identity library doesn't support referencing cert by Store + Subject name, so we need to load it ourselves.
246+ using ( X509Store store = new X509Store ( StoreName . My , StoreLocation . CurrentUser ) )
247+ {
248+ store . Open ( OpenFlags . ReadOnly ) ;
249+
250+ X509Certificate2Collection certs = store . Certificates . Find ( X509FindType . FindBySubjectName , subjectAlternativeName , true ) ;
251+
252+ if ( certs . Count == 0 )
253+ {
254+ throw new InvalidOperationException ( $ "Unable to find certificate with subject name '{ subjectAlternativeName } '") ;
255+ }
256+
257+ // As an exception to comment in GetKeyVaultCertsAsync method, this X509Certificate2 object does not have to be disposed
258+ // because it is referencing a platform certificate from CurrentUser certificate store, so no temporary files are created for this object.
259+ clientCert = certs . Cast < X509Certificate2 > ( )
260+ . Where ( c => c . NotBefore < DateTime . UtcNow && c . NotAfter > DateTime . UtcNow )
261+ . OrderBy ( x => x . NotAfter ) . Last ( ) ;
262+ }
263+
264+ return new ClientCertificateCredential ( tenantId , appID , clientCert , new ClientCertificateCredentialOptions { AuthorityHost = new Uri ( authorityHost ) , SendCertificateChain = true } ) ;
265+ }
220266 }
221267}
0 commit comments