5
votes

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?

2

2 Answers

10
votes

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|[email protected]");
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)

6
votes

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