Search code examples
c#oauth-2.0jwtazure-ad-graph-apihttp.client

Is it possible to use Bearer Access Token to Authenticate against our tenants SharePoint to allow a daemon to upload files to document library?


Currently we're working with Graph and Http.Client; I found an example that I have been working with and I broke this out into two namespaces; The namespace NetStandardCSOM is probably not accurate any longer, but what I was able to get down was to two operations.

First we have the AuthenticationManager.cs below:

using System;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.SharePoint.Client;

namespace NetStandardCSOM
{
    public class AuthenticationManager : IDisposable
    {
        private static readonly HttpClient httpClient = new HttpClient();
        private readonly string tokenEndpoint = "https://login.microsoftonline.com/{0}/oauth2/v2.0/token";
        private bool disposedValue;

        private readonly string clientId;
        private readonly string clientSecret;
        private readonly string tenantId;
        private string accessToken;

        public AuthenticationManager(string clientId, string clientSecret, string tenantId)
        {
            this.clientId = clientId;
            this.clientSecret = clientSecret;
            this.tenantId = tenantId;
        }

        public async Task<string> GetAccessTokenAsync()
        {
            var request = new HttpRequestMessage(HttpMethod.Post, string.Format(tokenEndpoint, tenantId));
            var content = new MultipartFormDataContent
            {
                { new StringContent(clientId), "client_id" },
                { new StringContent(clientSecret), "client_secret" },
                { new StringContent("client_credentials"), "grant_type" },
                { new StringContent($"https://{tenantId}.sharepoint.com/.default"), "scope" }
            };

            request.Content = content;
            var response = await httpClient.SendAsync(request);
            string responseContent = await response.Content.ReadAsStringAsync();

            if (!response.IsSuccessStatusCode)
            {
                Console.WriteLine($"Failed to acquire token. Status Code: {response.StatusCode}, Error: {responseContent}");
                response.EnsureSuccessStatusCode();
            }

            var tokenResult = JsonSerializer.Deserialize<JsonElement>(responseContent);
            var token = tokenResult.GetProperty("access_token").GetString();

            accessToken = token;
            return token;
        }

        public ClientContext GetContext(Uri web)
        {
            var context = new ClientContext(web);
            context.ExecutingWebRequest += (sender, e) =>
            {
                if (string.IsNullOrEmpty(accessToken) || IsTokenExpired())
                {
                    accessToken = GetAccessTokenAsync().GetAwaiter().GetResult();
                }
                e.WebRequestExecutor.RequestHeaders["Authorization"] = "Bearer " + accessToken;
                e.WebRequestExecutor.RequestHeaders["Accept"] = "application/json;odata-nometadata";
                Console.WriteLine($"Authorization: Bearer {accessToken.Substring(0, 20)}...\n Accept Response: {e.WebRequestExecutor.RequestHeaders["Accept"]} "); // Log the token and the JSON response
            };

            return context;
        }

        private bool IsTokenExpired()
        {
            if (string.IsNullOrEmpty(accessToken))
            {
                return true;
            }

            var handler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler();
            var jsonToken = handler.ReadToken(accessToken) as System.IdentityModel.Tokens.Jwt.JwtSecurityToken;
            var exp = jsonToken?.Claims.FirstOrDefault(claim => claim.Type == "exp")?.Value;

            if (long.TryParse(exp, out var expTime))
            {
                var expirationTime = DateTimeOffset.FromUnixTimeSeconds(expTime);
                return expirationTime < DateTimeOffset.UtcNow;
            }

            return true;
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    httpClient.Dispose();
                }

                disposedValue = true;
            }
        }

        public void Dispose()
        {
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }
    }
}

This is called in reference to our Program.cs which will handle quite a bit, I will remove proprietary items here and only list what we're trying to test below:

using System;
using System.IO;
using System.IO.Compression;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Microsoft.Extensions.Configuration;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Interactions;
using OpenQA.Selenium.Support.UI;
using SeleniumExtras.WaitHelpers;
using System.Text.RegularExpressions;
using OpenQA.Selenium.DevTools.V124.Network;
using System.Diagnostics;
using OpenQA.Selenium.Support.Events;
using Microsoft.Graph.Models.Security;
using OpenQA.Selenium.DevTools.V124.Debugger;
using Microsoft.SharePoint.Client;
using System.Security;
using NetStandardCSOM;
using Microsoft.Graph.Models;

namespace SeleniumADR
{
    internal class Program
    {
        public static IConfiguration Configuration { get; private set; }

        static void Main(string[] args)
        {
            var builder = new ConfigurationBuilder().AddUserSecrets<Program>();
            Configuration = builder.Build();

            var clientId = Configuration["AzureAd:ClientId"];
            var clientSecret = Configuration["AzureAd:ClientSecret"];
            var tenantId = Configuration["AzureAd:TenantId"];
            var siteUrl = Configuration["AzureAd:SiteUrl"];

            var authManager = new AuthenticationManager(clientId, clientSecret, tenantId);

            try
            {
                var token = authManager.GetAccessTokenAsync().GetAwaiter().GetResult();
                Console.WriteLine($"Token: {token.Substring(0, 20)}...");

                TestAuthentication(authManager, siteUrl);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"An error occurred: {ex.Message}");
                Console.WriteLine(ex.StackTrace);
            }

            Console.ReadKey();
        }

        private static void TestAuthentication(AuthenticationManager authManager, string siteUrl)
        {
            using (var context = authManager.GetContext(new Uri(siteUrl)))
            {
                try
                {
                    var web = context.Web;
                    context.Load(web, w => w.Title, w => w.Lists);
                    context.ExecuteQuery();

                    Console.WriteLine($"Connected to site: {web.Title}");
                    Console.WriteLine("Available Document Libraries:");

                    foreach (var list in web.Lists)
                    {
                        if (list.BaseTemplate == (int)ListTemplateType.DocumentLibrary)
                        {
                            context.Load(list, l => l.Title, l => l.RootFolder.ServerRelativeUrl);
                            context.ExecuteQuery();
                            Console.WriteLine($"- Title: {list.Title}, Url: {list.RootFolder.ServerRelativeUrl}");
                        }
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"An error occurred while accessing the SharePoint site: {ex.Message}");
                    Console.WriteLine(ex.StackTrace);
                }
            }
        }

Working with my team, I feel like I am onto the fact that we're missing a GET for Authorization but I am seeing that WebRequestExecutor.RequestHeaders being sent with the GET Header keys needed that I have followed in testing through Postman.

I get the bearer token, but I can't get it to authenticate, if this is a bad approach or poorly designed, please any and all feedback is appreciated! Thank you guys for everything, look forward to learning together :)

I tried using PnP, as well as Credential Authentication using the deprecated SharePointOnlineCredentials which is tied to another Microsoft.SharePoint.Client library in NuGet that is old, requesting I set my target framework back to net48, not happening. Though it works, I lose the functionality of the current projects methods and functions written. I've read plenty of stackoverflow questions and answers, I have even asked ChatGPT which alas, always gets me nowhere outside of occassionally helping me debug something I missed in a hurry; Commonly a missed ; or an out of scope declaration lol I have watched a view videos on YouTube as well, and I am not understanding a few of them as they were also a few years old 3-4 years different.


Solution

  • Note that: If you are making use of client credential flow that is generating access token in app-only scenarios SharePoint only accepts access tokens requested with a client certificate, not a client secret. Refer this MsDoc

    • If you want to make use of client credential flow and call SharePoint REST API then generate the self signed in certificate and refer this GitHub blog to generate access token.

    Hence, as a workaround as you mentioned you are working with Graph and Http.Client, you can make use of below code to upload file to SharePoint document library:

    using System.Net.Http.Headers;
    using Azure.Core;
    using Azure.Identity;
    using Microsoft.Graph;
    
    namespace UserProperties
    {
        public class GraphHandler
        {
            public GraphServiceClient GraphClient { get; set; }
    
            public GraphHandler()
            {
                var clientId = "ClientID";
                var clientSecret = "ClientSecret";
                var tenantId = "TenantID";
    
                var options = new TokenCredentialOptions
                {
                    AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
                };
    
                var clientSecretCredential = new ClientSecretCredential(tenantId, clientId, clientSecret, options);
                var scopes = new[] { "https://graph.microsoft.com/.default" };
    
                GraphClient = new GraphServiceClient(clientSecretCredential, scopes);
            }
    
            public async Task UploadFileToSharePoint(string siteId, string driveId, string fileName, string filePath)
            {
                try
                {
                    var uploadUrl = $"https://graph.microsoft.com/v1.0/sites/{siteId}/drives/{driveId}/items/root:/{fileName}:/content";
                    byte[] fileBytes = File.ReadAllBytes(filePath);
                    using (var httpClient = new HttpClient())
                    {
                        var accessToken = await GetAccessTokenAsync();
                        httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
                        using (var httpRequest = new HttpRequestMessage(HttpMethod.Put, uploadUrl))
                        {
                            httpRequest.Content = new ByteArrayContent(fileBytes);
                            httpRequest.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
                            using (var response = await httpClient.SendAsync(httpRequest))
                            {
                                response.EnsureSuccessStatusCode();
                                Console.WriteLine("File uploaded successfully.");
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"Error uploading file: {ex.Message}");
                }
            }
    
            private async Task<string> GetAccessTokenAsync()
            {
                var clientId = "ClientID";
                var clientSecret = "ClientSecret";
                var tenantId = "TenantID";
    
                var options = new TokenCredentialOptions
                {
                    AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
                };
    
                var clientSecretCredential = new ClientSecretCredential(tenantId, clientId, clientSecret, options);
                var tokenRequestContext = new TokenRequestContext(new string[] { "https://graph.microsoft.com/.default" });
    
                var accessTokenResult = await clientSecretCredential.GetTokenAsync(tokenRequestContext);
    
                return accessTokenResult.Token;
            }
        }
    
        class Program
        {
            static async Task Main(string[] args)
            {
                var handler = new GraphHandler();
                var siteId = "SiteID"; 
                var driveId = "DriveID"; 
                var fileName = "rukk.txt"; 
                var filePath = "C:/Users/rukmini/Downloads/rukk.txt"; 
                await handler.UploadFileToSharePoint(siteId, driveId, fileName, filePath);
            }
        }
    }
    

    enter image description here

    enter image description here

    • To get the site ID of the site: : https://graph.microsoft.com/v1.0/sites?search=testrukk
    • To get the Drive ID (Document library ID): : https://graph.microsoft.com/v1.0/sites/SiteID/drives
    • Make sure to grant Sites.ReadWrite.All Microsoft Graph API permission to the application:

    enter image description here

    Reference:

    Upload small files - Microsoft Graph v1.0 | Microsoft