6

I am able to retrieve and use an access token via MSAL in a desktop .Net project. I can successfully retrieve tokens and they are valid in my Graph calls.

However, trying to use the access token with SharePoint Online CSOM results in a 401: Unauthorized. This is similar to accessing sharepoint REST apis using msal throws 401 (except I am using C# and the latest CSOM).

It is my understanding that MSFT is trying to move devs away from ADAL and towards MSAL, but there seems to be some compatibility issue with the tokens.

Has anyone been able to specify the necessary scopes and leverage an OAuth token with bearer authorization from MSAL to access SharePoint Online?

AWeber
  • 381
  • 1
  • 3
  • 14

2 Answers2

11

As far as I know, you can't request SharePoint REST API with a Client / Secret Token through AAD. But it works with a certificate. Below a step by step:

As I was facing the same problems, I'll post here how I managed to connect to SharePoint API through AAD Application with MSAL (OAuth v2 and AAD v2 endpoint). It's in C#.

First, I only succeeded with a certificate (the client / secret method doesn't work as far as I know).

Create certificate

For testing purposes, I've created a self-signed certificate with the "New-PnPAzureCertificate" like this :

$secPassword = ConvertTo-SecureString -String "MyPassword" -AsPlainText -Force
$cert = New-PnPAzureCertificate -OutCert "CertSPUser.cer" -OutPfx "CertSPUser.pfx" -ValidYears 10  -CertificatePassword $secPassword -CommonName "CertSPUser" -Country "FR" -State "France"

(The -Country and the -State parameters doesn't matter for the test)

(It also works with the New-SelfSignedCertificate command)

Register certificate

Then, you have to upload the certificate in your AAD Application (the ".cer" file):

image

Configure Application API Permissions

After that, you have to authorize the SharePoint APIs:

image

Try the access through Daemon App (C#)

NuGet Packages (hope I forgot nothing)

  • Microsoft.SharePointOnline.CSOM
  • Microsoft.Identity.Client

To make things work, you have to it in 3 steps (I've simplified it, but you better separate the actions into methods with some try/catch)

Get Pfx Certificate

For this step, I highly recommand to use a KeyVault (see links on the bottom of the post)

string certPath = System.IO.Path.GetFullPath(@"C:\PathTo\CertSPUser.pfx");
X509Certificate2 certificate = new X509Certificate2(certPath, "MyPassword", X509KeyStorageFlags.MachineKeySet);

Get Token

string tenantId = "yourTenant.onmicrosoft.com" // Or "TenantId"
string applicationId = "IdOfYourAADApp"
IConfidentialClientApplication confApp = ConfidentialClientApplicationBuilder.Create(applicationId)
.WithAuthority($"https://login.microsoftonline.com/{tenantId}")
.WithCertificate(certificate)
.Build();

string sharePointUrl = "https://yourSharePoint.sharepoint.com" // Or "https://yourSharePoint-admin.sharepoint.com" if you want to access the User Profile REST API
var scopes = new[] { $"{sharePointUrl}/.default" };
var authenticationResult = await confApp.AcquireTokenForClient(scopes).ExecuteAsync();
string token = authenticationResult.AccessToken;

Test your connection

ClientContext ctx = new ClientContext(sharePointUrl);
ctx.ExecutingWebRequest += (s, e) =>
{
      e.WebRequestExecutor.RequestHeaders["Authorization"] = "Bearer " + token;
};

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

// OR if you connect to User Profile ("yourSharePoint-admin.sharepoint.com")
/* 
PeopleManager peopleManager = new PeopleManager(ctx);
var personProperties = peopleManager.GetUserProfileProperties("i:0#.f|membership|employee.mail@tenant.onmicrosoft.com");
ctx.ExecuteQuery();
*/

And if I didn't miss anything, you should get some Web / User Info !

Hope that it helps.

Edit (11/14/2020) : Even if in my screen capture on the API Permissions I've added the Application Permission "User.ReadWrite.All", if you try to update the User Profile, it won't work. To solve this, you have to register your AAD Application as a legacy SharePoint App-Only principal (Client ID / Secret). More info here.

Thanks to @laurakokkarinen and @mmsharepoint for their articles that really helped me (here and here)

  • This is an excellent writeup! One question: do you happen to know why using ClientId/ClientSecret combo no longer works for CSOM? Using a certificate is a bit more cumbersome. – Thiago Silva Nov 23 '20 at 21:56
  • Good question! I don't remember that it already worked in the past. If you want to use a Client ID / Secret pair, the only way I know is using the legacy SharePoint App-Only approach. Beware of using this : the secret generated through the "AppRegNew.aspx" is only valid one year. If you want to configure the expiration delay, you can follow the great article made by @sergei-sergeev : https://spblog.net/post/2018/08/24/SharePoint-lifehacks-create-SharePoint-app-registration-with-client-secret-which-never-expires – Michaël Maillot Nov 25 '20 at 09:03
  • I've experienced the issue with user profiles edition you mentionned. So I'm trying to use the app-only authentication to perform my edit operations. However the nuget package SharePointPnPCoreOnline to do so is targeting .net 4.7 framework. I'm working with .net core 3.1 project with the latest version of Microsoft.SharePointOnline.CSOM. Would you have a suggestion about an equivalent of SharePointPnPCoreOnline package targeting .net core 3.1 to allow me to produce clientContext with app-only authentication? Thanks – Jows Nov 27 '20 at 19:56
  • Never mind I found a suitable solution here: https://www.linkedin.com/pulse/csom-net-standard-sharepoint-app-only-principal-elnur-babayev/ https://github.com/elnurrr/CSOM-NET-Standard – Jows Nov 27 '20 at 22:11
  • @Jows FYI, the PnP Team is preparing the next Framework that will succeed to PnP-Sites-Core (aka SharePointPnPCoreOnline), which will be compatible with .Net Core : [PnP Framework](https://github.com/pnp/pnpframework#pnp-net-roadmap-status). The code you've found is quite the same used in the PnP Framework (actually in preview), which you can find [here](https://github.com/pnp/pnpframework/blob/dev/src/lib/PnP.Framework/AuthenticationManager.cs). – Michaël Maillot Nov 29 '20 at 10:51
  • @michaelmaillot - I created a GitHub issue for the MS Docs page detaling the User/Pwd set up for SharePoint CSOM. The MSFT team responded and confirmed that there have been recent changes that invalidate all but the "certificates" approach for talking to SPO. They will be updating the Docs page to reflect: https://github.com/SharePoint/sp-dev-docs/issues/6501#issuecomment-733668395 – Thiago Silva Nov 29 '20 at 22:18
  • When i try to access the admin site with scope "https://yourSharePoint-admin.sharepoint.com", i end up with an exception: "the scope ... is not valid". Has this worked for anyone before? Is there something i'm missing to be able to access the admin site? – Shane Mar 25 '21 at 01:49
  • Hi @Shane, have you got details (like InnerException) regarding your exception? In the `scopes` variable, you just entered only one URL which is the SPO admin one, right? – Michaël Maillot Mar 27 '21 at 00:41
  • @michaelmaillot thanks, the issue was I had two scopes. If I do admit site only it's fine. – Shane Mar 28 '21 at 04:08
8

Not sure why are you using MSAL directly to access SharePoint Online API. The easiest way should be using Microsoft.SharePointOnline.CSOM

var clientContext = new ClientContext(siteUrl);
clientContext.Credentials = new SharePointOnlineCredentials(userName, securedPassword);

and you are done with CSOM API.

But I think that it should be possible with MSAL as well. Keep in mind that token that you acquire is granted for some resource. Copy parse your token to http://jwt.ms/ and take a look at aud value

enter image description here

if this field contains https://graph.microsoft.com it just cannot be valid for you https://yourtenant.sharepoint.com and you need different one!

You should be able request correct token using Microsoft.Identity.Client

var authority = $"https://login.microsoftonline.com/{tenantId}";
var app = ConfidentialClientApplicationBuilder.Create(clientId)
    .WithClientSecret(clientSecret)
    .WithAuthority(new Uri(authority))
    .Build();

var scopes = new[] { "https://yourtenant.sharepoint.com/.default" };
var authenticationResult = await app.AcquireTokenForClient(scopes)
    .ExecuteAsync();

As a result authenticationResult.AccessToken should contain access token valid for SharePoint Online REST API. (https://yourtenant.sharepoint.com/.default means that you requests all scopes configured for your Azure AD application)

When you decode new jet token it should looks like this

enter image description here

Sergey Tihon
  • 12,071
  • 4
  • 23
  • 29
  • 2
    See @michaelmaillot answer: You get a jwt token when using a client secret, but the SharePoint REST API returns a '401 Not authorized' when your try to use it. Using a certificate instead works fine. Not sure if this is documented somewhere, but of course it's really annoying. – krombi May 26 '20 at 09:45
  • @krombi CSOM with `SharePointOnlineCredentials` only works in .NET Framework on Windows. If you're targeting .NET Core on Linux then you have to follow the latest guidance from Microsoft and use MSAL. Their latest docs detail this info. – Thiago Silva Nov 29 '20 at 22:14
  • Similar approach is described here - https://stackoverflow.com/a/63386756/987850 – 23W May 25 '21 at 08:53
  • Something that should be noted, is that the above way - using SharePointOnlineCredentials - is only available in .Net Framework project and not recent versions of .Net and if Legacy authentication blocking is switched on for the Tenant, then you wont be able to use it even in a .Net Framework project. – jimas13 Jan 22 '23 at 12:09