Saturday, May 2, 2026

Migrating SharePoint CSOM from Username & Password to Azure Entra ID Certificate Authentication

 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:

  1. The application is registered in Entra ID and given a Client ID.
  2. A certificate is generated. The public key is uploaded to Entra ID. The private key stays on the server — it never leaves.
  3. SharePoint API permissions are granted to the App Registration and an admin approves them.
  4. 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.
  5. Entra ID verifies the signature using the registered public key and issues an access token.
  6. The token is attached to every SharePoint CSOM request in the Authorization header.

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 CERT

Go 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 Write

Repeat for each site the application needs to reach.


Step 4 — Update the Code

Install the required NuGet packages:

Microsoft.Identity.Client

Microsoft.SharePointOnline.CSOM

Here 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 $acl

A 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

ErrorLikely Cause
AADSTS700027: Certificate not registeredThumbprint mismatch — verify config vs portal
AADSTS70011: Invalid scopeScope must be https://<tenant>.sharepoint.com/.default
401 from SharePointToken missing or scoped to wrong resource
403 from SharePointApp not granted access to the site (check Sites.Selected grant)
Certificate not foundNot installed in LocalMachine\My, or wrong thumbprint
CryptographicException on private keyApp 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

This is the approach I followed, but there are other approach as well like using Microsoft Graph API. This will definitely help if you are migrating from using username & password to use Azure clientID & certificate based authentication.

Enjoy...

Wednesday, April 19, 2023

How to render a Razor view to string in ASP.net MVC

 Sometimes we need to get the Razor view to a string, in order to use it for various purposes. I had a requirement where I needed to generate a PDF file from the Razor view.

I used below method to convert the Razor view to a string and used this string to generate a PDF file.

Below is the detailed explanation of that:

Steps:

1. Get the viewResult using the view name.

2. Get the viewContext using the view.

3. Render the data to the StringWriter class object using the viewResult and viewContext.

4. Finally release the view and get the string from StringWriter class object.

public string RenderRazorViewToString(this Controller controllerName, string viewName, object model)

        {

            controller.ViewData.Model = model;

            //used a StringWriter to get the Razor contents into it.

            using (var sw = new StringWriter())

            {

                var viewResult = ViewEngines.Engines.FindPartialView(controller.ControllerContext, viewName);

                var viewContext = new ViewContext(controller.ControllerContext, viewResult.View, controller.ViewData, controller.TempData, sw);

                viewResult.View.Render(viewContext, sw);

                viewResult.ViewEngine.ReleaseView(controller.ControllerContext, viewResult.View);

                return sw.GetStringBuilder().ToString();

            }

        }


This worked like a charm for me. Now you can use this return string for your custom needs.

Hope this helps somebody out there.



Thursday, July 3, 2014

The type initializer '' threw an exception.

Mainly this error occurs when it fails to load a dependent dll/assembly. It couldn't find the dependency, or the version number was different.

This error mostly occurs when you deploy your application to production server. It might work fine in development machine. Things to make sure in this case:

All your dependencies are deployed properly.
Check the version numbers for the dependencies.

I had this issue when my multiple projects where using a logging dll. It worked fine in dev machine, but when deployed to the test server, booommmm..... ended up with this error.

So I made sure that all the projects in my solution are referring to the logging dll from same common location (may be a folder in the solution/GAC to hold common shared assemblies), with same version number. After making these changes and making sure all other dll's are deployed correctly, it worked like a charm.

Hope this tip helps somebody...... njoy..

Wednesday, June 18, 2014

The Specified Store provider cannot be found in the configuration or is not valid - entity framework

Check your machine.config to see if any providers mentioned in the <DbProviderFactories> section. If not, add the required provider to this section.

This solved mine.

Thursday, March 13, 2014

How to install a windows service in a server which doesn't have Visual Studio installed

If you have visual studio installed in a server, it would be easier to install a windows service, by using installutil.exe. However this have to be done from Visual Studio command prompt.

But how can you install if you don't have Visual studio command prompt in the server.

Below is the solution:

SC Create <Service name> binPath= "complete path of the exe file" DisplayName= "MyService"

ex:-  sc create MyService binPath= "C:\temp\Service1.exe" DisplayName= "MyTestService"

The service will be created and will be displayed in services.msc as MyTestService. If you right click and check the properties of the service, it will show the service name as MyService.

IMPORTANT:- Make sure you add a space after the "=" for the arguments.
Like binPath= "C:\temp\Service1.exe" DisplayName= "MyTestService". You can see the space marked in purple color. This has to be there to run the command successfully.

Otherwise, it doesn't even throw error and you will not have a clue of what is happening.

You can use SC Delete command to delete the service from the console.

Friday, February 21, 2014

IIS Error & Solutions - Tips

Got Error: "http error 500.19 <modules runAllManagedModulesForAllRequests="true" />"

Soultion: Deleting this line from web.config solved it.

Got Error: "HTTP Error 404.3 - Not Found  The page you are requesting cannot be served because of the extension configuration. If the page is a script, add a handler. If the file should be downloaded, add a MIME map."

Solution: Ran the command "aspnet_regiis.exe -ir" from Visual studio command prompt. This .exe can be found in
%windir%\Microsoft.NET\Framework64\v4.0.30319


Got Error while hosting WCF Web service in IIS:
"the configuration section 'protocolmapping' cannot be read because it is missing a section declaration"

Solution: Please make sure the application pool that is used for the website need to be setup to use .Net 4.0 framework.


More tips on the way...

Thursday, December 19, 2013

Extracting values from an XML string using SQL query in oracle database

There are two methods you can use for extracting values from XML string (a column in a table). The column type will be XMLType.

These methods are EXTRACT(columnName, xpath). This returns list of nodes, also a record if extracting with a unique filter value.
You can also use ExtractValue(columnName, xpath) - This will work only for a single node, not for a collection of nodes. This way you can retrieve value from a unique record.


See examples below:

<Person>
    <Name>Test</Name>
    <Address>
        <Street Number=1345>Monroe St</Street>
        <City>TestCity</City>
        <Zip>12345</Zip>
    </Address>
</Person>

Assume this xml data in column
You can extract the street number attribute value using the query as below:

This can return multiple records:
select extract(columnName,'/Person/Address/Street/@Number') as StreetNumber from Table1;

For returning single record:
select extract(columnName,'/Person/Address/Street/@Number') as StreetNumber from Table1 where ID = 200;

Also For returning single record only:
select extractvalue(columnName, '/Person/Address/Street/@Number') as StreetNumber from Table1 where ID = 200;

I have used this and works like a charm.
Hope this finds useful to you also.
Njoyy...