Using a certificate authentication to call SharePoint REST API (CSOM)
Tags: sharepoint, azure
Published: 2023-11-03 23:38:08

## Motivation

Imagine you have a .NET app that stopped working because there is a requirement for 2FA. This article shows how you can take care of that and upgrade your app to use a certificate to call SharePoint instead of username/password or client secret. Please note that this article is referring to the "never" version where you register your app with Azure AD, not the old one where you register your app with SharePoint directly (as an add-in).

I am assuming that we want to call SharePoint unattended, i.e. NOT on behalf of the user, but on behalf of the application itself. If you want a delegated call on behalf of a user, then you don't need this "certificate" authentication. Also, if your app that needs to call SharePoint is itself hosted on azure, you could consider using managed identities instead.

## The certificate

To authenticate using a certificate, we need that certificate, to start with. You can get one in many ways, the easiest (for Windows) is probably the PowerShell `New-SelfSignedCertificate` command or the button "make new self-signed certificate" in IIS (if you have IIS installed). For Linux, it's OpenSSL.

As an output from this step, we should have a .PFX certificate file (say, "my.pfx"). The certificate may be for any domain (i.e. "localhost" is fine) and may be "self-signed", it works fine. The PFX file should include both private and public keys.

In addition to the PFX, we need a public-key-only version of it to use for app registration. If you are using Windows, you could just export a PFX (right mouse click), or you could also use a PowerShell script. Let's name this second file "my.cer".

Below is a simple PowerShell script to make the "my.pfx" file and "my.cer" files:
```powershell
$cert = New-SelfSignedCertificate -DnsName localhost -CertStoreLocation cert:\CurrentUser\My
$pwd = ConvertTo-SecureString -String "P@ss0word!" -Force -AsPlainText
Export-PfxCertificate -Cert $cert -FilePath .\my.pfx -Password $pwd
Export-Certificate -Cert $cert -FilePath .\my.cer
```

So now you should have 2 files ready, **my.pfx** and **my.cer**

## The Azure app registration

After completing this step, we should have an App registered with Azure ID to be used for authenticating the requests to SharePoint API that are coming from our CSOM application. 

First, register an App to be used with this certificate:

- Go to your [Azure portal](https://portal.azure.com) (where the SharePoint belongs to). 
- Go to "App registrations"
- Click "New registration"
- Give your app a name, say "MyApp", and click "Register"

![register the app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9cdk0a24pbkyq9xzir4y.png)

Upload the certificate created at the first part for this app:

- Click tab "Certificates & secrets"
- Select "Certificates" tab.
- Click "Upload certificate" and choose the "my.cer" file

![certificates tab](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gfmzbwg0zbx5h7pflkpm.png)

Add SharePoint API permissions set for the app (unless you have already done that before):

- Add API Permissions to the app to call SharePoint. 
- Select the "API permissions"
- Click "Add permission"
- Choose the "SharePoint" in the list. Do not select "Graph".

![add permissions](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4srefafbgrabfjvou0nt.png)

Select the exact permissions you want the app to have

- Choose "Application permissions" (we want to run unattended) and then appropriate permission for your app. I've selected `Sites.FullControl.All` for simplicity (it will grant all SharePoint permissions to the app). You may want to be more precise here. Click "Add".

![application permissions](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tqu4aefnatgil6d9qju5.png)

Now you should get that in the list of application permissions. You need to consent to these permissions. Note that only admin can do that. Click "Grant admin consent". You should have a green checkbox next to that permission:

![grant consent](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i1hfy580h0tqrebbacyl.png)

That's it. We are done with App registration, and should be able to call it from our application using CSOM. There is one more thing we want for our dotnet code, its application ID and tenant ID. You can find these on the app overview page:

![app overview](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v98n3imy68veatnwtton.png)

## The dotnet app

7. Now we can use the above setup from the dotnet code. For the [PnP.Framework](https://pnp.github.io/pnpframework/using-the-framework/readme.html) library, the code to call SharePoint using that certificate may look somewhat like this:

```cs
var authMgr = PnP.Framework.AuthenticationManager
  .CreateWithCertificate(
    @"11e0257c-66b1-4896-88dd-xxxxx", // Your Azure AD App ID
    @"c:\path\to\my.pfx", 
    @"Pass@word!", 
    @"6ca33c8e-996a-4de8-a843-xxxxx" // Your Tenant ID
  );

var siteUrl = "https://contoso.sharepoint.com";

using (var ctx = authMgr.GetContext(siteUrl))
{
  ctx.Load(ctx.Web, p => p.Title);
  Console.WriteLine(ctx.Web.Title);
}
```

Basically, you just tell the AuthenticationManager to use the certificate to connect. For the newer [PnP Core](https://pnp.github.io/pnpcore/) library, it will look similar, but the details (like the names of methods) will differ a bit. 

For the old `SharePoint.Online.CSOM` it could look like (this one was built in pre-azure times):

```cs
...
var siteUrl = "https://contoso.sharepoint.com";

var clientId = "11e0257c-66b1-4896-88dd-xxxxxx";
var tenantId = "6ca33c8e-996a-4de8-a843-xxxxxx";
var certificate = new X509Certificate2(
  @"C:\path\to\my.pfx", "Pass@word!");

var confidentialClient = ConfidentialClientApplicationBuilder
    .Create(clientId)
    .WithTenantId(tenantId)
    .WithCertificate(certificate)
    .Build();

var token = await confidentialClient
    .AcquireTokenForClient(new[] { $"{siteUrl}/.default" })
    .ExecuteAsync();

var context = new ClientContext(siteUrl);
context.ExecutingWebRequest += (sender, e) =>
{
  e.WebRequestExecutor.RequestHeaders["Authorization"] = 
    "Bearer " + token;
};

context.Load(context.Web, web => web.Title);
await context.ExecuteQueryAsync();
```

## Conclusion

I hope this walkthrough was useful. Would appreciate any comments or notes.