Connecting to SharePoint Online using an Azure AD Service Principle (Application) and CSOM
Microsoft is giving us a push to use Microsoft Graph as an alternative to using SharePoint CSOM. Unfortunately, not everything is available in Microsoft Graph. When you are automating, you want to use a service account that has no user identity (delegation) in it and can be autonomous. However, the only way right now to get an application token that can be used to consume the SharePoint Online CSOM, is to authenticate your application using an authentication certificate.
We will make use of the KeyVault to store the authentication certificate and then add it to the application as a key credential used for authentication.
KeyVault and Application setup
KeyVault
Generate or add a certificate in the KeyVault to use for the authentication. In my case, I generated a self-signed certificate with the name as SPO<AppName>Authentication and the subject CN=SPO<AppName>Authentication, where <AppName> is the name of my Azure AD application.
Adding the certificate to the Application
Time to add the certificate to the application. To do that we can do it with either the az cli or PowerShell.
PowerShell:
1 2 3 4 5 6 |
$keyVaultCertificate = Get-AzKeyVaultCertificate -VaultName <keyvault_name> -Name <certificate_name> $base64Cert = [System.Convert]::ToBase64String($keyVaultCertificate.Certificate.GetRawCertData()) New-AzADAppCredential -ApplicationId <app_id> ` -CertValue $base64Cert ` -EndDate $keyVaultCertificate.Certificate.NotAfter ` -StartDate $keyVaultCertificate.Certificate.NotBefore |
az cli:
1 |
az ad sp credential reset --name <app_id> --cert <certificate_name> --keyvault <vault_name> --append |
Once added, you should see in the application manifest, under the keyCredentials property, something like this:
1 2 3 4 5 6 7 8 9 10 11 12 |
"keyCredentials": [ { "customKeyIdentifier": "<certificate_thumbprint>", "endDate": "2021-11-10T19:03:11Z", "keyId": "<key_id>", "startDate": "2020-11-10T19:26:49.403452Z", "type": "AsymmetricX509Cert", "usage": "Verify", "value": "<public_cert_in_base64>", "displayName": "<certificate_subject>" } ] |
Adding the code to the SPOAuthenticationManager
In my other post, I used a delegate to manage the token fetching. In this scenario, we create another overload of the GetContext method, and have a different delegate body that will fetch the token using the provided certificate.
GetContext method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public ClientContext GetContext(Uri web, string clientId, string tenantId, X509Certificate2 authCertificate) { var context = new ClientContext(web); var resourceUri = new Uri($"{web.Scheme}://{web.DnsSafeHost}"); async Task<string> AcquireTokenAsyncFunc(string clientIdFunc, string tenantIdFunc) => await AcquireTokenAsync(resourceUri, clientIdFunc, tenantIdFunc, authCertificate); context.ExecutingWebRequest += (sender, e) => { var cacheKey = $"{web.DnsSafeHost}_{clientId}"; string accessToken = EnsureAccessTokenAsync(resourceUri, cacheKey, clientId, tenantId, AcquireTokenAsyncFunc).GetAwaiter().GetResult(); e.WebRequestExecutor.RequestHeaders["Authorization"] = $"Bearer {accessToken}"; }; return context; } |
AcquireTokenAsync method:
1 2 3 4 5 6 7 8 9 10 11 12 |
private async Task<string> AcquireTokenAsync(Uri resourceUri, string clientId, string tenantId, X509Certificate2 authCertificate) { string[] scopes = new string[] { $"{resourceUri.Scheme}://{resourceUri.DnsSafeHost}/.default" }; var app = ConfidentialClientApplicationBuilder.Create(clientId) .WithTenantId(tenantId) .WithCertificate(authCertificate) .Build(); var result = await app.AcquireTokenForClient(scopes).ExecuteAsync(); return result.AccessToken; } |
As you can see, we authenticate through a client_credential grant, using the certificate passed as parameter.
Tying everything together
In the console application, we now need to grab the certificate from the keyVault and pass it to our authentication manager.
Grabbing the certificate
To grab the certificate, you can use the new Azure.Security.KeyVault.Secrets library. If you are not aware of the new Azure.Security.KeyVault.* libraries, check out my post on that subject!
1 2 3 4 5 6 7 8 9 10 11 12 |
var tenantId = "<tenant_id>"; var clientId = "<client_id>"; var clientSecret = "<client_secret>"; var keyVaultUrl = "https://<key_vault_name>.vault.azure.net"; var siteUrl = new Uri("https://<tenant_name>.sharepoint.com/sites/<site_name>"); var credentials = new ClientSecretCredential(tenantId,clientId,clientSecret); var client = new SecretClient(vaultUri: new Uri(keyVaultUrl), credential: credentials); var certificate = await client.GetSecretAsync("<authentification_certificate_name>"); var certificateX509 = new X509Certificate2(Convert.FromBase64String(certificate.Value.Value)); |
Calling SharePoint CSOM
We can then call the API using the CSOM library
1 2 3 4 5 6 7 |
using (var cc = new SPOAuthenticationManager().GetContext(siteUrl, clientId, tenantId, certificateX509)) { Console.WriteLine("Using app cert auth"); cc.Load(cc.Web, p => p.Title); await cc.ExecuteQueryAsync(); Console.WriteLine(cc.Web.Title); }; |
Conclusion
As you can see you can authenticate using a certificate and generate yourself a token to consume SharePoint CSOM.
You can use the same token to query the REST API (_api/*). If you wish to do that, you can use a similar logic (authenticate your user, swap for a delegated token, cache the token, etc.) and use it within a DelegatingHandler that will be responsible to acquire the token and add it to your Authentication header. Voila!