using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Extensions.Msal;
namespace SharepointToolbox.Infrastructure.Auth;
///
/// Creates and caches one IPublicClientApplication per ClientId with a persistent MsalCacheHelper.
/// Never shares instances across different ClientIds (per-tenant isolation).
///
public class MsalClientFactory
{
private readonly Dictionary _clients = new();
private readonly Dictionary _helpers = new();
private readonly SemaphoreSlim _lock = new(1, 1);
/// Cache directory for MSAL token files.
public string CacheDirectory { get; }
/// Default constructor — uses %AppData%\SharepointToolbox\auth.
public MsalClientFactory()
: this(Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"SharepointToolbox", "auth"))
{ }
/// Constructor allowing a custom cache directory (used in tests).
public MsalClientFactory(string cacheDirectory)
{
CacheDirectory = cacheDirectory;
}
///
/// Returns the cached IPublicClientApplication for the given clientId,
/// or creates a new one (with MsalCacheHelper) on first call.
/// Thread-safe: concurrent callers with the same clientId receive the same instance.
///
public async Task GetOrCreateAsync(string clientId)
{
await _lock.WaitAsync();
try
{
if (_clients.TryGetValue(clientId, out var existing))
return existing;
Directory.CreateDirectory(CacheDirectory);
var storageProps = new StorageCreationPropertiesBuilder(
$"msal_{clientId}.cache", CacheDirectory)
.Build();
var pca = PublicClientApplicationBuilder
.Create(clientId)
.WithDefaultRedirectUri()
.WithLegacyCacheCompatibility(false)
.Build();
var helper = await MsalCacheHelper.CreateAsync(storageProps);
helper.RegisterCache(pca.UserTokenCache);
_clients[clientId] = pca;
_helpers[clientId] = helper;
return pca;
}
finally { _lock.Release(); }
}
///
/// Returns the MsalCacheHelper for the given clientId.
/// GetOrCreateAsync must be called first — throws InvalidOperationException otherwise.
/// Used by SessionManager to wire PnP's internal token cache to the same persistent cache file.
///
public MsalCacheHelper GetCacheHelper(string clientId)
{
if (!_helpers.TryGetValue(clientId, out var helper))
throw new InvalidOperationException(
$"No cache helper found for clientId '{clientId}'. Call GetOrCreateAsync first.");
return helper;
}
}