Files
console_spo_utils/library_spo_utils/Services/AutenticationManager.cs
T
2023-06-21 10:38:57 +02:00

205 lines
8.2 KiB
C#

using System.Collections.Concurrent;
using System.Text;
using System.Text.Json;
using System.Web;
using library_spo_utils.Interfaces.Services;
using library_spo_utils.Model;
using Microsoft.Extensions.Logging;
using Microsoft.SharePoint.Client;
namespace library_spo_utils.Services
{
internal sealed class SharePointAuthenticationManager : IDisposable, ISharePointAuthenticationManager
{
private readonly ILogger<SharePointAuthenticationManager> logger;
private readonly ISiteOptions siteOptions;
public SharePointAuthenticationManager(
ILogger<SharePointAuthenticationManager> logger,
ISiteOptions siteOptions)
{
this.logger = logger;
this.siteOptions = siteOptions;
}
private static readonly HttpClient HttpClient = new();
private static readonly SemaphoreSlim SemaphoreSlimTokens = new(1);
private AutoResetEvent tokenResetEvent = null;
private readonly ConcurrentDictionary<string, string> tokenCache = new();
private bool disposedValue;
#region CSOM
public ClientContext GetContext(Uri web)
{
var context = new ClientContext(web);
context.ExecutingWebRequest += (sender, e) =>
{
var accessToken = EnsureAccessTokenAsync(new Uri($"{web.Scheme}://{web.DnsSafeHost}"),
siteOptions.GetUser(),
new System.Net.NetworkCredential(string.Empty, siteOptions.GetPassword()).Password)
.GetAwaiter()
.GetResult();
e.WebRequestExecutor.RequestHeaders["Authorization"] = "Bearer " + accessToken;
};
return context;
}
public async Task<string> EnsureAccessTokenAsync(Uri resourceUri, string userPrincipalName, string userPassword)
{
var accessTokenFromCache = TokenFromCache(resourceUri, tokenCache);
if (accessTokenFromCache == null)
{
await SemaphoreSlimTokens.WaitAsync();
try
{
// No async methods are allowed in a lock section
var accessToken = await AcquireTokenAsync(resourceUri, userPrincipalName, userPassword);
logger.LogInformation($"Successfully requested new access token resource {resourceUri.DnsSafeHost} for user {userPrincipalName}");
AddTokenToCache(resourceUri, tokenCache, accessToken);
// Register a thread to invalidate the access token once's it's expired
tokenResetEvent = new AutoResetEvent(false);
var wi = new TokenWaitInfo();
wi.Handle = ThreadPool.RegisterWaitForSingleObject(
tokenResetEvent,
(state, timedOut) =>
{
if (!timedOut)
{
var internalWaitToken = (TokenWaitInfo)state;
if (internalWaitToken?.Handle is not null)
{
internalWaitToken.Handle.Unregister(null);
}
}
else
{
try
{
// Take a lock to ensure no other threads are updating the SharePoint Access token at this time
SemaphoreSlimTokens.Wait();
RemoveTokenFromCache(resourceUri, tokenCache);
logger.LogInformation($"Cached token for resource {resourceUri.DnsSafeHost} and user {userPrincipalName} expired");
}
catch (Exception ex)
{
logger.LogInformation($"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
{
logger.LogInformation("OK - Execution Query");
return accessTokenFromCache;
}
}
private async Task<string> AcquireTokenAsync(Uri resourceUri, string username, string password)
{
var resource = $"{resourceUri.Scheme}://{resourceUri.DnsSafeHost}";
var clientId = siteOptions.DefaultAadAppId;
var body = $"resource={resource}&client_id={clientId}&grant_type=password&username={HttpUtility.UrlEncode(username)}&password={HttpUtility.UrlEncode(password)}";
using var stringContent = new StringContent(body, Encoding.UTF8, "application/x-www-form-urlencoded");
var result = await HttpClient.PostAsync(siteOptions.TokenEndpoint, 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 var accessToken))
{
return accessToken;
}
return null;
}
private static void AddTokenToCache(Uri web, ConcurrentDictionary<string, string> tokenCache, string newAccessToken)
{
if (tokenCache.TryGetValue(web.DnsSafeHost, out var 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 _);
}
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)
{
var now = DateTime.UtcNow;
var expires = expiresOn.Kind == DateTimeKind.Utc ? expiresOn : TimeZoneInfo.ConvertTimeToUtc(expiresOn);
var lease = expires - now;
return lease;
}
private void Dispose(bool disposing)
{
if (disposedValue)
{
return;
}
if (disposing)
{
if (tokenResetEvent is not 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);
}
#endregion
}
}