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 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; return pca; } finally { _lock.Release(); } } }