Introduction
If your application connects to SharePoint Online using the SharePointOnlineCredentials class with a username and password, it is time to change that. Microsoft has deprecated this approach, and for good reason — storing credentials in configuration files is a security risk, passwords expire and break applications, and there is no way to scope what the application can actually access.
The modern replacement is OAuth 2.0 app-only authentication using Microsoft Entra ID (formerly Azure Active Directory), a Client ID, and a Certificate. No usernames. No passwords. Just a registered application identity and a certificate that proves it.
This post walks through exactly what changes, why it matters, and how to do it step by step.
Why the Old Way Has to Go
The old approach looked something like this:
C# Code
var securePassword = new SecureString();
foreach (char c in password) securePassword.AppendChar(c);
ctx.Credentials = new SharePointOnlineCredentials(username, securePassword);Simple enough — but the problems run deep:
- Passwords expire. When they do, every application using them breaks until someone manually updates the config.
- User accounts get disabled or deleted. If that service account goes, so does your integration.
- No granular access control. The app inherits every permission the user has — far more than it likely needs.
- Credentials get exposed. Config files get committed to repos, appear in logs, and end up in places they should never be.
- It is deprecated. Microsoft will eventually remove this entirely.
The new approach solves all of these in one go.
How the New Approach Works
Instead of a user identity, the application registers as its own identity in Entra ID — an App Registration. It is assigned a Client ID and authenticated using a Certificate rather than a password.
Here is the flow at a high level:
- The application is registered in Entra ID and given a Client ID.
- A certificate is generated. The public key is uploaded to Entra ID. The private key stays on the server — it never leaves.
- SharePoint API permissions are granted to the App Registration and an admin approves them.
- At runtime, the application uses MSAL (Microsoft Authentication Library) to request an access token from Entra ID. It proves its identity by signing a JWT with the certificate's private key.
- Entra ID verifies the signature using the registered public key and issues an access token.
- The token is attached to every SharePoint CSOM request in the
Authorizationheader.
No passwords. No user accounts. A certificate that can be rotated without changing a single line of code.
Step 1 — Register the Application in Entra ID
Head to the Azure Portal → Microsoft Entra ID → App Registrations → New Registration.
Give it a meaningful name (e.g. SharePointCSOMApp), leave the redirect URI blank, and register it. Note down the Application (Client) ID and Directory (Tenant) ID from the Overview page — you will need both in your application config.
Next, go to API Permissions → Add a permission → SharePoint → Application permissions and add Sites.Selected. This is the recommended permission — it restricts the app to only the site collections you explicitly grant it access to, rather than the entire tenant.
Click Grant admin consent. This step requires a Global Administrator.
Step 2 — Set Up the Certificate
You can use an existing certificate or generate a self-signed one. For this purpose, a self-signed certificate works perfectly — Entra ID uses it only for signature verification, not for trust chain validation.
Generate one with PowerShell:
Powershell Code
$cert = New-SelfSignedCertificate `
-Subject "CN=SharePointCSOMApp" `
-CertStoreLocation "Cert:\LocalMachine\My" `
-KeyExportPolicy Exportable `
-KeyLength 2048 `
-HashAlgorithm SHA256 `
-NotAfter (Get-Date).AddYears(2)
Write-Host "Thumbprint: $($cert.Thumbprint)"Then export the public key only as a .cer file:
Powershell Code
Export-Certificate `
-Cert "Cert:\LocalMachine\My\$($cert.Thumbprint)" `
-FilePath "C:\certs\SharePointCSOMApp.cer" `
-Type CERTGo back to your App Registration in the Azure Portal, navigate to Certificates & secrets → Certificates → Upload certificate, and upload the .cer file. After upload, verify the thumbprint shown in the portal matches the one on your server — they must be identical.
Step 3 — Grant the App Access to Your SharePoint Site
If you used Sites.Selected (recommended), you need to explicitly grant the app access to each site collection it needs. Use PnP PowerShell:
Powershell Code
Connect-PnPOnline -Url "https://<tenant>-admin.sharepoint.com" -Interactive
Grant-PnPAzureADAppSitePermission `
-AppId "<your-client-id>" `
-DisplayName "SharePointCSOMApp" `
-Site "https://<tenant>.sharepoint.com/sites/yoursite" `
-Permissions WriteRepeat for each site the application needs to reach.
Step 4 — Update the Code
Install the required NuGet packages:
Microsoft.Identity.Client
Microsoft.SharePointOnline.CSOMHere is the helper class that handles everything — certificate lookup by thumbprint, token acquisition via MSAL, and injecting the token into CSOM requests:
C# Code
public class SharePointClientHelper : IDisposable
{
private readonly string _siteUrl;
private readonly IConfidentialClientApplication _msalApp;
private readonly string[] _scopes;
public SharePointClientHelper(
string siteUrl, string tenantId,
string clientId, string thumbprint)
{
_siteUrl = siteUrl;
var cert = LoadCertificateByThumbprint(thumbprint);
_msalApp = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithCertificate(cert)
.WithAuthority($"https://login.microsoftonline.com/{tenantId}")
.Build();
var uri = new Uri(siteUrl);
_scopes = new[] { $"{uri.Scheme}://{uri.Host}/.default" };
}
public async Task<ClientContext> GetClientContextAsync()
{
var tokenResult = await _msalApp
.AcquireTokenForClient(_scopes)
.ExecuteAsync();
var ctx = new ClientContext(_siteUrl);
ctx.ExecutingWebRequest += (sender, e) =>
{
e.WebRequestExecutor.RequestHeaders["Authorization"]
= "Bearer " + tokenResult.AccessToken;
};
return ctx;
}
private static X509Certificate2 LoadCertificateByThumbprint(string thumbprint)
{
thumbprint = thumbprint.Replace(" ", "").ToUpperInvariant();
foreach (var location in new[]
{ StoreLocation.CurrentUser, StoreLocation.LocalMachine })
{
using (var store = new X509Store(StoreName.My, location))
{
store.Open(OpenFlags.ReadOnly);
var matches = store.Certificates.Find(
X509FindType.FindByThumbprint, thumbprint, false);
if (matches.Count > 0)
return matches[0];
}
}
throw new InvalidOperationException(
$"Certificate '{thumbprint}' not found in certificate store.");
}
public void Dispose() { }
}And the usage — replacing the old SharePointOnlineCredentials call entirely:
C# Code
// Before
ctx.Credentials = new SharePointOnlineCredentials(username, securePassword);
// After
var helper = new SharePointClientHelper(siteUrl, tenantId, clientId, thumbprint);
using (var ctx = await helper.GetClientContextAsync())
{
ctx.Load(ctx.Web, w => w.Title);
await ctx.ExecuteQueryAsync();
Console.WriteLine(ctx.Web.Title);
}Step 5 — Update Configuration and IIS Permissions
Remove the username and password from your config and replace them with:
Json
{
"SharePoint": {
"SiteUrl": "https://<tenant>.sharepoint.com/sites/yoursite",
"TenantId": "your-tenant-id",
"ClientId": "your-client-id",
"CertificateThumbprint": "A1B2C3D4E5F6..."
}
}If running under IIS, the App Pool identity needs Read access to the certificate's private key. Run this once after installing the certificate:
Powershell Code
$thumbprint = "A1B2C3D4E5..."
$cert = Get-Item "Cert:\LocalMachine\My\$thumbprint"
$rsaKey = [System.Security.Cryptography.X509Certificates
.RSACertificateExtensions]::GetRSAPrivateKey($cert)
$keyPath = "$env:ALLUSERSPROFILE\Microsoft\Crypto\RSA\MachineKeys\" `
+ $rsaKey.Key.UniqueName
$acl = Get-Acl $keyPath
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
"IIS AppPool\DefaultAppPool", "Read", "Allow")
$acl.AddAccessRule($rule)
Set-Acl $keyPath $aclA Note on MSAL and Token Caching
One thing worth highlighting — MSAL handles token caching automatically. Calling GetClientContextAsync() multiple times will not repeatedly call Entra ID. MSAL returns the cached token until it is near expiry (tokens last approximately one hour), then fetches a new one transparently. You do not need to manage any of this yourself.
MSAL also handles building and signing the client assertion JWT internally when you call .WithCertificate(cert). It sets the x5t thumbprint header in the JWT so Entra ID knows which registered certificate to use for verification. All of this happens under the hood — your code just passes the certificate and MSAL takes care of the rest.
Quick Troubleshooting Reference
| Error | Likely Cause |
|---|---|
| AADSTS700027: Certificate not registered | Thumbprint mismatch — verify config vs portal |
| AADSTS70011: Invalid scope | Scope must be https://<tenant>.sharepoint.com/.default |
| 401 from SharePoint | Token missing or scoped to wrong resource |
| 403 from SharePoint | App not granted access to the site (check Sites.Selected grant) |
| Certificate not found | Not installed in LocalMachine\My, or wrong thumbprint |
| CryptographicException on private key | App Pool identity missing Read permission |
Wrapping Up
This migration is a one-time change that pays dividends immediately — no more password rotations breaking applications, no more broad user-level permissions, no more credentials sitting in config files. Once the certificate is in place and the App Registration is configured, the authentication just works, silently and securely, in the background.
The key things to remember:
- Upload only the public key (.cer) to Entra ID — never the private key (.pfx)
- Use Sites.Selected over Sites.FullControl.All wherever possible
- For IIS apps, the certificate must be in LocalMachine\My and the App Pool must have private key Read access
- MSAL handles token caching, assertion signing, and renewal — you do not need to manage any of it manually