Created
August 4, 2020 18:59
-
-
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…
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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