Desktop app using REST endpoint returning 401 (Unauthorized) error

Copper Contributor

I ultimately want to upload files using SharePoint APIs, however initially I am reading a site's lists.

 

I have followed ChengFeng's post up to the instruction that says:

"... click on Site contents. Under Apps you can add, find your app and click on it. Click on Trust It"

My app is not listed under Apps you can add so I cannot trust the app.  When I make a GET request to the lists endpoint I am getting a 401 (Unauthorized) error.

 

The app manifest is the same as the example except for `<AppPermissionRequests AllowAppOnlyPolicy="true">`:

 

<?xml version="1.0" encoding="utf-8"?>
<App xmlns="http://schemas.microsoft.com/sharepoint/2012/app/manifest"
     Name="SharePointUpload"
     ProductID="{xxxxxxxx-803d-45f2-a710-7c22092a0a5e}"
     Version="1.0.0.8"
     SharePointMinVersion="16.0.0.0">
  <Properties>
    <Title>SharePoint Upload</Title>
    <StartPage>~remoteAppUrl/Pages/Default.aspx?{StandardTokens}</StartPage>
    <SupportedLocales>
        <SupportedLocale CultureName="en-US" />
    </SupportedLocales>
  </Properties>
  <AppPrincipal>
    <RemoteWebApplication ClientId="xxxxxxxx-5a22-4a98-9a14-4a4c37b9d13a" />
  </AppPrincipal>
  <AppPermissionRequests>
    <AppPermissionRequest Scope="http://sharepoint/content/sitecollection/web" Right="Read" />
  </AppPermissionRequests>
</App>

 

The call to the REST endpoint:

 

using System;
using System.Text;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Identity.Client;

class Program
{
    static async Task Main(string[] args)
    {
        var siteUrl = "https://xxxxxxxxxxxxxxxx.sharepoint.com/sites/xxxxxxxx";

        // Get an access token from Azure AD using MSAL
        var clientId = "xxxxxxxx-5a22-4a98-9a14-4a4c37b9d13a";
        var tenantId = "xxxxxxxx-191b-4de1-806c-e26a6700397f";
        var authority = $"https://login.microsoftonline.com/{tenantId}";
        var scopes = new[] { "https://graph.microsoft.com/.default" };
        var app = ConfidentialClientApplicationBuilder.Create(clientId)
            .WithAuthority(authority)
            .WithClientSecret("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
            .Build();
        var result = await app.AcquireTokenForClient(scopes).ExecuteAsync();
        var accessToken = result.AccessToken;

        using (var httpClient = new HttpClient())
        {
            httpClient.DefaultRequestHeaders.Add("Accept", "application/json;odata=verbose");

            // Add the access token to the Authorization header
            httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
            var requestUrl = $"{siteUrl}/_api/web/lists";
            Console.WriteLine("REST call...");
            var response = await httpClient.GetAsync(requestUrl);
            response.EnsureSuccessStatusCode();
            Console.WriteLine($"Lists read successfully: {response.StatusCode}");
        }
    }
}

 

I should probably be using the Graph endpoint here but cannot find the site ID!  If anyone has a fix for this or an alternative approach I would be grateful.

2 Replies
Try to follow the below steps:
1. Delete the app from AppCatalog
2. Remove App from Recyclebin and Second state recyclebin of SharePoint.
3. Change the major version of app
4. Again upload the App and trust it.
5. Check the issue is resloved or not.

alternative approach, consider using the SharePoint PnP (Patterns and Practices) library. It provides a set of powerful tools and utilities for working with SharePoint, including authentication, REST API calls, and more.

using System;
using System.Security;
using OfficeDevPnP.Core.Authentication;
using Microsoft.SharePoint.Client;

class Program
{
static void Main(string[] args)
{
string siteUrl = "https://xxxxxxxxxxxxxxxx.sharepoint.com/sites/xxxxxxxx";
string clientId = "xxxxxxxx-5a22-4a98-9a14-4a4c37b9d13a";
string clientSecret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";

var authManager = new AuthenticationManager();
using (var context = authManager.GetAppOnlyAuthenticatedContext(siteUrl, clientId, clientSecret))
{
context.Load(context.Web, w => w.Title);
context.ExecuteQuery();

Console.WriteLine($"Connected to site: {context.Web.Title}");

var lists = context.Web.Lists;
context.Load(lists, l => l.Include(li => li.Title));
context.ExecuteQuery();

Console.WriteLine("Lists in the site:");
foreach (var list in lists)
{
Console.WriteLine($"- {list.Title}");
}
}
}
}

@tibhusha1070 thanks for your reply.

 

The code I got this to work with:

 

using System;
using System.Net.Http;
using System.Threading.Tasks;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;

class Program
{
    static async Task Main(string[] args)
    {
        var siteId = "xxxxxxxx-ac84-4fc9-a4af-e880bbd4f1be";
        var tenantId = "xxxxxxxx-191b-4de1-806c-e26a6700397f";
        var clientId = "xxxxxxxx-2be7-4640-b03f-64b5c6c1f1d9";
        var clientSecret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";

        var url = $"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token";
        var client = new HttpClient();

        // Set the Accept header to application/json
        client.DefaultRequestHeaders.Accept.Clear();
        client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));

        // Form urlencoded values
        var values = new Dictionary<string, string>
        {
            { "grant_type", "client_credentials" },
            { "client_id", clientId },
            { "scope", "https://graph.microsoft.com/.default" },
            { "client_secret", clientSecret }
        };
        var content = new FormUrlEncodedContent(values);

        var response = await client.PostAsync(url, content);
        var jsonString = await response.Content.ReadAsStringAsync();

        var jsonObject = JObject.Parse(jsonString);
        var accessToken = jsonObject["access_token"].ToString();

        Console.WriteLine($"Access Token: {accessToken}");

        // Use the access token to authenticate to the REST endpoint
        var requestUrl = $"https://graph.microsoft.com/v1.0/sites/{siteId}/lists";
        client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);

        var apiResponse = await client.GetAsync(requestUrl);
        var apiJsonString = await apiResponse.Content.ReadAsStringAsync();

        Console.WriteLine($"API Response: {apiJsonString}");
    }
}

 

 

The steps to register the app:

  1. Go to Microsoft Azure or Microsoft Entra -> App Registrations
  2. Select + New Registration, give a display name for the application and click Register
  3. Once the app is registered, note down the ClientId and TenantId from the page
    that appears. Then click on Add a certificate or secret
  4. Under Certificates & secrets, click on + New client secret. Enter a description for the client secret and click Add.
  5. Note down the Value of the client secret.
  6. Go to API Permissions. Click on + Add a permission
  7. Select Microsoft APIs -> Microsoft Graph
  8. Under Application permissions, select Sites.ReadWrite.All
  9. Select Grant admin consent for and approve it.
  10. https://<tenant-name>.sharepoint.com/sites/<TeamSite>/_api/site/id and note down the Site Id

(Credit to Psiog Digital for those instructions.)

 

Add-ins are being retired, authentication is done with Microsoft Entra ID.  I was able to get the PnP code to build with the deprecated `SharePointPnPCoreOnline` DLL (I didn't attempt .net8), but got a `Microsoft.SharePoint.Client.ServerUnauthorizedAccessException: Access is denied.` error.

 

I have to say I have never had so much trouble using a REST API.  The documentation is Greek.  The architecture is a can of worms.  The books are not current or up-to-date.  Do you have to be a Microsoft Certified Engineer to upload a file using a REST endpoint!?  One shouldn't.