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:
In Azure AD, the API Permissions are set as follows and are all granted admin consent
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
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:
Also upload the .cer
certificate in the Microsoft Entra ID application:
Granted API permissions like below:
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);
}
}
}