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.
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
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);
}
}
}
https://graph.microsoft.com/v1.0/sites?search=testrukk
https://graph.microsoft.com/v1.0/sites/SiteID/drives
Sites.ReadWrite.All
Microsoft Graph API permission to the application:Reference: