Search code examples
azure-active-directorysharepoint-onlinemicrosoft-entra-id

API Access to SharePoint Files/Lists via SharePoint REST, API & MSAL, using deamon/service account


For my own API I use a Sharepoint REST service that saves & retrieves documents from a sharepoint site. This process should be user independent, i.e. using a deamon account.

So far I have been using the 'normal' username/password OAuth flow to identify a dedicated app user who has sufficient access rights on the Sharepoint site to do the things that need to be done, like up- and downloading files adding and removing list entries etc.

For the above I used grant_type=password calling the following:

endpoint: https://login.microsoftonline.com/common/oauth2/token
method: POST
body:
  resource="https://<tenant_name>.sharepoint.com"
  &grant_type=password
  &client_id=<clientId>
  &password={HttpUtility.UrlEncode(password)}

That gives me an access token and the user has been assigned access in SharePoint tenant as well. That in itself works.

As it would be better to use a DEAMON or service account for this process I am trying to refactor the Service to use deamon/service account access. According to several sites, this should be posisble using client_credential flow.

If I try to call the same method but with a different endpoint and body:

endpoint: https://login.microsoftonline.com/<tenant_id>/oauth2/v2.0/token
method: POST
body:
  client_id=<client_id>
  &scope={HttpUtility.UrlEncode("https://<tenant_name>.sharepoint.com/.default")}
  &client_secret={HttpUtility.UrlEncode(<client_secret>)}
  &password={HttpUtility.UrlEncode(password)}
  &grant_type=client_credentials

I get a 401 Error, Unsupported app only token

If I call the same endpoint i.e. ../oauth2/token i.s.o. ../oauth/v2.0/token I get an MS IdentityModel error.

{"error_description":"Exception of type 'Microsoft.IdentityModel.Tokens.AudienceUriValidationFailedException' was thrown."}

I have tried using the MSAL library from Microsoft as that is recommended but I fail to get that to work properly.

Now, the below, should give me the name of the site:

const string SERVER = "https://<tenant_name>.sharepoint.com";
const string SITE = "/sites/<site_name>";
string webUrl = string.Concat(new string[] { SERVER, SITE });

var _scopes = new[] { "https://<tenant_name>.sharepoint.com/.default" };

var confidentialClient = ConfidentialClientApplicationBuilder
.Create(<clientId>)
.WithTenantId(<tenantId>)
.WithClientSecret(<clientSecret>)
.Build();

AuthenticationResult r = await confidentialClient.AcquireTokenForClient(_scopes).ExecuteAsync();
Debug.WriteLine(r.AccessToken);

string restUrl = webUrl.CombineUrl("/_api/web");
var httpRequest = GetHttpRequestMessage(restUrl, r.AccessToken, HttpMethod.Get, null, OData.VERBOSE);
var httpResponse = await httpClient.SendAsync(httpRequest);

string propertyValue = null;
if (httpResponse?.StatusCode is not Response.OK)
{
    var response = await httpResponse.Content.ReadAsStringAsync();
    Debug.WriteLine(response);
}
else if (httpResponse?.StatusCode is Response.OK)
{
    Stream stream = await httpResponse.Content.ReadAsStreamAsync();
    JsonElement element = GetJsonElementFromStream(stream);
    propertyValue = element.GetJsonElementFromPath("d.Title").GetString();
    Debug.WriteLine(propertyValue);
}

I get the same 401 Error, Unsupported app only token

I have tried generating a self-signed certificate using powershell and uploaded that to the Azure App registration as per this instruction: https://learn.microsoft.com/en-us/sharepoint/dev/solution-guidance/security-apponly-azuread

But when I load the private key file for that into a X509 certificate to include in the ClientApplicationBuilder

var certificate = X509CertificateLoader.LoadPkcs12FromFile(@"C:\windows\system32\somecertificate.pfx", "somepassword");

var confidentialClient = ConfidentialClientApplicationBuilder
    .Create(<clientId>)
    .WithTenantId(<tenantId>)
    .WithCertificate(certificate)
    .Build();

If I try this method I get an error: Keyset does not exist

I did upload the certificate file "somecertificate.cer" to Azure AD App registration:

enter image description here

In Azure AD, the API Permissions are set as follows and are all granted admin consent

enter image description here

I have looked through several dozen how-to sites, on Microsoft learn and on SO but nothing that has been able to point me in the right direction.

Other info: https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow


Solution

  • The error "401 Error, Unsupported app only token" usually occurs if you are generating access token using client secret to call the SharePoint API with scope as SharePoint scope.

    To resolve the error, you need to generate access token using certificate.

    Make sure that the pfx certificate exists on the local machine:

    enter image description here

    Also upload the .cer certificate in the Microsoft Entra ID application:

    enter image description here

    Granted API permissions like below:

    enter image description here

    To get the site details using certificate modify the code like below:

    class Program
    {
        const string SERVER = "https://xxx.sharepoint.com";
        const string SITE = "/sites/RukSite";
    
        static async Task Main(string[] args)
        {
            string webUrl = string.Concat(SERVER, SITE);
            var _scopes = new[] { "https://xxx.sharepoint.com/.default" };
    
            // Load certificate from file
            string certificatePath = @"C:\Users\mrrukmini\Downloads\finalpfx.pfx"; // Update this path
            string certificatePassword = "XXX";  // Provide password if applicable
    
            X509Certificate2 certificate = new X509Certificate2(certificatePath, certificatePassword);
    
            var confidentialClient = ConfidentialClientApplicationBuilder
                .Create("ClientID") // Replace with your actual client ID
                .WithTenantId("TenantID") // Replace with your actual tenant ID
                .WithCertificate(certificate)
                .Build();
    
            AuthenticationResult r = await confidentialClient.AcquireTokenForClient(_scopes).ExecuteAsync();
            Console.WriteLine("Access Token: " + r.AccessToken);
    
            using var httpClient = new HttpClient();
            string restUrl = new Uri(new Uri(webUrl), "/_api/web").ToString();
    
            // Make sure to set the Accept header to 'application/json' to force SharePoint to return JSON
            var httpRequest = new HttpRequestMessage(HttpMethod.Get, restUrl)
            {
                Headers =
                {
                    { "Authorization", "Bearer " + r.AccessToken },
                    { "Accept", "application/json" } // Add this header to request JSON response
                }
            };
    
            var httpResponse = await httpClient.SendAsync(httpRequest);
    
            // Read the raw response content as string first
            string responseContent = await httpResponse.Content.ReadAsStringAsync();
    
            // Check if the response is OK (200 OK)
            if (httpResponse?.StatusCode != System.Net.HttpStatusCode.OK)
            {
                Console.WriteLine($"Error: {httpResponse.StatusCode}");
                Console.WriteLine("Response content: ");
                Console.WriteLine(responseContent);  // Log the error response (likely HTML or error message)
            }
            else
            {
                // Print the full raw JSON response
                Console.WriteLine("Full Response Content: ");
                Console.WriteLine(responseContent);
            }
        }
    }
    

    enter image description here