Skip to content

Instantly share code, notes, and snippets.

@tahir-hassan
Created August 4, 2020 18:59
Show Gist options
  • Save tahir-hassan/44a12a9e6212db000270049330320f55 to your computer and use it in GitHub Desktop.
Save tahir-hassan/44a12a9e6212db000270049330320f55 to your computer and use it in GitHub Desktop.
This `AuthenticationManager` class is based on one found at https://www.c-sharpcorner.com/article/sharepoint-csom-for-net-standard/. It authenticates using a client ID and secret that you can set up within Sharepoint. It uses information found in https://www.anexinet.com/blog/getting-an-access-token-for-sharepoint-online/ to get the token. You n…
using Microsoft.SharePoint.Client;
using System;
using System.Collections.Concurrent;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;
namespace NetCoreCSOM
{
public class AuthenticationManager : IDisposable
{
private static readonly HttpClient httpClient = new HttpClient();
private const string SharepointOnlineApplicationPrincipleId = "00000003-0000-0ff1-ce00-000000000000";
// Token cache handling
private static readonly SemaphoreSlim semaphoreSlimTokens = new SemaphoreSlim(1);
private AutoResetEvent tokenResetEvent = null;
private readonly ConcurrentDictionary<string, string> tokenCache = new ConcurrentDictionary<string, string>();
private bool disposedValue;
internal class TokenWaitInfo
{
public RegisteredWaitHandle Handle = null;
}
public ClientContext GetContext(string web, string tenantId, string clientId, string clientSecret)
{
return GetContext(new Uri(web), tenantId, clientId, clientSecret);
}
public ClientContext GetContext(Uri web, string tenantId, string clientId, string clientSecret)
{
var context = new ClientContext(web);
context.ExecutingWebRequest += (sender, e) =>
{
string accessToken = EnsureAccessTokenAsync(
new Uri($"{web.Scheme}://{web.DnsSafeHost}"),
tenantId,
clientId,
clientSecret
).GetAwaiter().GetResult();
e.WebRequestExecutor.RequestHeaders["Authorization"] = "Bearer " + accessToken;
};
return context;
}
public string GetTokenEndPoint(string tenantId) => $"https://accounts.accesscontrol.windows.net/{tenantId}/tokens/OAuth/2";
public async Task<string> EnsureAccessTokenAsync(Uri resourceUri, string tenantId, string clientId, string clientSecret)
{
string accessTokenFromCache = TokenFromCache(resourceUri, tokenCache);
if (accessTokenFromCache == null)
{
await semaphoreSlimTokens.WaitAsync().ConfigureAwait(false);
try
{
// No async methods are allowed in a lock section
string accessToken = await AcquireTokenAsync(resourceUri, tenantId, clientId, clientSecret).ConfigureAwait(false);
Debug.WriteLine($"Successfully requested new access token resource {resourceUri.DnsSafeHost} for clientId {clientId}");
AddTokenToCache(resourceUri, tokenCache, accessToken);
// Register a thread to invalidate the access token once's it's expired
tokenResetEvent = new AutoResetEvent(false);
TokenWaitInfo wi = new TokenWaitInfo();
wi.Handle = ThreadPool.RegisterWaitForSingleObject(
tokenResetEvent,
async (state, timedOut) =>
{
if (!timedOut)
{
TokenWaitInfo wi1 = (TokenWaitInfo)state;
if (wi1.Handle != null)
{
wi1.Handle.Unregister(null);
}
}
else
{
try
{
// Take a lock to ensure no other threads are updating the SharePoint Access token at this time
await semaphoreSlimTokens.WaitAsync().ConfigureAwait(false);
RemoveTokenFromCache(resourceUri, tokenCache);
Debug.WriteLine($"Cached token for resource {resourceUri.DnsSafeHost} and clientId {clientId} expired");
}
catch (Exception ex)
{
Debug.WriteLine($"Something went wrong during cache token invalidation: {ex.Message}");
RemoveTokenFromCache(resourceUri, tokenCache);
}
finally
{
semaphoreSlimTokens.Release();
}
}
},
wi,
(uint)CalculateThreadSleep(accessToken).TotalMilliseconds,
true
);
return accessToken;
}
finally
{
semaphoreSlimTokens.Release();
}
}
else
{
Debug.WriteLine($"Returning token from cache for resource {resourceUri.DnsSafeHost} and clientId {clientId}");
return accessTokenFromCache;
}
}
public async Task<string> AcquireTokenAsync(Uri resourceUri, string tenantId, string clientId, string clientSecret)
{
// this is what I am following: https://www.anexinet.com/blog/getting-an-access-token-for-sharepoint-online/
// see this to get the tenant id https://sharepoint.stackexchange.com/a/278300
var resourceValue = $"{SharepointOnlineApplicationPrincipleId}/{resourceUri.DnsSafeHost}@{tenantId}";
var clientIdValue = $"{clientId}@{tenantId}";
string urlEncode(string x) => System.Net.WebUtility.UrlEncode(x);
var body = $"grant_type=client_credentials&resource={urlEncode(resourceValue)}&client_id={urlEncode(clientIdValue)}&client_secret={urlEncode(clientSecret)}";
using (var stringContent = new StringContent(body, Encoding.UTF8, "application/x-www-form-urlencoded"))
{
var result = await httpClient.PostAsync(/* tokenEndpoint */ GetTokenEndPoint(tenantId), stringContent).ContinueWith((response) =>
{
return response.Result.Content.ReadAsStringAsync().Result;
}).ConfigureAwait(false);
var tokenResult = JsonSerializer.Deserialize<JsonElement>(result);
var token = tokenResult.GetProperty("access_token").GetString();
return token;
}
}
private static string TokenFromCache(Uri web, ConcurrentDictionary<string, string> tokenCache)
{
if (tokenCache.TryGetValue(web.DnsSafeHost, out string accessToken))
{
return accessToken;
}
return null;
}
private static void AddTokenToCache(Uri web, ConcurrentDictionary<string, string> tokenCache, string newAccessToken)
{
if (tokenCache.TryGetValue(web.DnsSafeHost, out string currentAccessToken))
{
tokenCache.TryUpdate(web.DnsSafeHost, newAccessToken, currentAccessToken);
}
else
{
tokenCache.TryAdd(web.DnsSafeHost, newAccessToken);
}
}
private static void RemoveTokenFromCache(Uri web, ConcurrentDictionary<string, string> tokenCache)
{
tokenCache.TryRemove(web.DnsSafeHost, out string currentAccessToken);
}
private static TimeSpan CalculateThreadSleep(string accessToken)
{
var token = new System.IdentityModel.Tokens.Jwt.JwtSecurityToken(accessToken);
var lease = GetAccessTokenLease(token.ValidTo);
lease = TimeSpan.FromSeconds(lease.TotalSeconds - TimeSpan.FromMinutes(5).TotalSeconds > 0 ? lease.TotalSeconds - TimeSpan.FromMinutes(5).TotalSeconds : lease.TotalSeconds);
return lease;
}
private static TimeSpan GetAccessTokenLease(DateTime expiresOn)
{
DateTime now = DateTime.UtcNow;
DateTime expires = expiresOn.Kind == DateTimeKind.Utc ? expiresOn : TimeZoneInfo.ConvertTimeToUtc(expiresOn);
TimeSpan lease = expires - now;
return lease;
}
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
if (tokenResetEvent != null)
{
tokenResetEvent.Set();
tokenResetEvent.Dispose();
}
}
disposedValue = true;
}
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment