update directory name and access visibility
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
using console_spo_utils.Model;
|
||||
using Microsoft.SharePoint.Client;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Web;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using console_spo_utils.Interfaces.Services;
|
||||
|
||||
namespace console_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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user