Search code examples
c#.netazureazure-active-directorysharepoint-online

Access Sharepoint Online Files from .NET Worker Service via Microsoft Graph/OAuth - Unauthorized or Access Denied error (401)


I want to create a .NET Worker Service (c#) to access files hosted on Sharepoint online.

I have registered an application for this in the Azure portal as follows:

  1. Register an application (Single tenant)
  2. API permissions -> Add a permission -> SharePoint -> Application permissions -> Sites.FullControl.All
  3. Grant admin consent confirmation.
  4. Certificates & secrets -> Add a client secret

The code looks like this:

using Microsoft.Identity.Client;
using Microsoft.SharePoint.Client;

string siteUrl = "https://myname.sharepoint.com/sites/sharepointwebsite/";
string clientId = "XXX";
string tenantId = "XXX";
string clientSecret = "XXX";

var confidentialClientApp = ConfidentialClientApplicationBuilder.Create(clientId)
    .WithTenantId(tenantId)
    .WithClientSecret(clientSecret)
    .Build();

var authResult = await confidentialClientApp.AcquireTokenForClient(new[] { "https://myname.sharepoint.com/.default" }).ExecuteAsync();

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

    Web web = context.Web;
    context.Load(web);
    context.ExecuteQuery();

    Console.WriteLine("Title: " + web.Title);
}

I get an access token, but access to the SharePoint site is denied (401).

Which step (permissions) may I have forgotten?

Thank you very much for your help.


Solution

  • When you use an Azure AD registered application to authenticate with SharePoint online, you'll need to use a certificate to prove the application’s identity while requesting an access token. In a normal scenario you could use either a secret or a certificate, but AFAIK SharePoint online blocks anything other than certificate. Microsoft documentation for the whole scenario is available here -


    Step 1 - Associate certificate credential with your registered application

    a. If you have a certificate already you don't need to do this part and simply upload the certificate, otherwise you can create a self-signed certificate using the following PowerShell script. (NOTE: self-signed cert may not be good for production scenarios, but can help you verify the concept in dev)

    $certname = "MyTestCertificate"    
    $cert = New-SelfSignedCertificate -Subject "CN=$certname" -CertStoreLocation "Cert:\CurrentUser\My" -KeyExportPolicy Exportable -KeySpec Signature -KeyLength 2048 -KeyAlgorithm RSA -HashAlgorithm SHA256
    Export-Certificate -Cert $cert -FilePath "C:\WorkFolder\$certname.cer"  
    $mypwd = ConvertTo-SecureString -String "MyStrongPassword123#" -Force -AsPlainText  
    Export-PfxCertificate -Cert $cert -FilePath "C:\WorkFolder\$certname.pfx" -Password $mypwd   
    

    b In the App registrations tab for the client application:

    enter image description here

    • Select Certificates & secrets > Certificates.
    • Click on Upload certificate and select the certificate file to upload.
    • Once the certificate is uploaded, the thumbprint, start date, and expiration values are displayed.

    Step 2 - Use certificate instead of client secret to acquire token

    I've just taken your C# code and modified it to use a certificate.

    Important part:

    var confidentialClientApp = ConfidentialClientApplicationBuilder.Create(clientId)
        .WithTenantId(tenantId)
        .WithCertificate(new X509Certificate2(@"C:\WorkFolder\MyTestCertificate.pfx", "MyStrongPassword123#")).Build();
    
    var authResult = await confidentialClientApp.AcquireTokenForClient(new[] { "https://myname.sharepoint.com/.default" }).ExecuteAsync();
    

    Complete code:

    using Microsoft.Identity.Client;
    using Microsoft.SharePoint.Client;
    using System.Security.Cryptography.X509Certificates;
    
    string siteUrl = "https://myname.sharepoint.com/sites/sharepointwebsite/";
    string clientId = "XXX";
    string tenantId = "XXX";
    
    var confidentialClientApp = ConfidentialClientApplicationBuilder.Create(clientId)
        .WithTenantId(tenantId)
        .WithCertificate(new X509Certificate2(@"C:\WorkFolder\MyTestCertificate.pfx", "MyStrongPassword123#")).Build();
    
    var authResult = await confidentialClientApp.AcquireTokenForClient(new[] { "https://myname.sharepoint.com/.default" }).ExecuteAsync();
    
    using (var context = new ClientContext(siteUrl))
    {
        context.ExecutingWebRequest += (sender, e) =>
        {
            e.WebRequestExecutor.RequestHeaders["Authorization"] = "Bearer " + authResult.AccessToken;
        };
    
        Web web = context.Web;
        context.Load(web);
        context.ExecuteQuery();
    
        Console.WriteLine("Title: " + web.Title);
        Console.ReadLine();
    }