/// <summary> /// Creates a new instance of <see cref="MsalCacheHelper"/>. To configure MSAL to use this cache persistence, call <see cref="RegisterCache(ITokenCache)"/> /// </summary> /// <param name="storageCreationProperties">Properties to use when creating storage on disk.</param> /// <param name="logger">Passing null uses a default logger</param> /// <returns>A new instance of <see cref="MsalCacheHelper"/>.</returns> public static async Task <MsalCacheHelper> CreateAsync(StorageCreationProperties storageCreationProperties, TraceSource logger = null) { if (storageCreationProperties is null) { throw new ArgumentNullException(nameof(storageCreationProperties)); } // We want CrossPlatLock around this operation so that we don't have a race against first read of the file and creating the watcher using (CreateCrossPlatLock(storageCreationProperties)) { // Cache the list of accounts var ts = logger == null ? s_staticLogger.Value : new TraceSourceLogger(logger); var accountIdentifiers = await GetAccountIdentifiersAsync(storageCreationProperties, ts).ConfigureAwait(false); var cacheWatcher = new FileSystemWatcher(storageCreationProperties.CacheDirectory, storageCreationProperties.CacheFileName); var helper = new MsalCacheHelper(storageCreationProperties, logger, accountIdentifiers, cacheWatcher); try { cacheWatcher.EnableRaisingEvents = true; } catch (PlatformNotSupportedException) { helper._logger.LogError( "Cannot fire the CacheChanged event because the target framework does not support FileSystemWatcher. " + "This is a known issue in Xamarin / Mono."); } return(helper); } }
/// <summary> /// Gets the current set of accounts in the cache by creating a new public client, and /// deserializing the cache into a temporary object. /// </summary> private static async Task <HashSet <string> > GetAccountIdentifiersAsync(StorageCreationProperties storageCreationProperties) { var accountIdentifiers = new HashSet <string>(); if (File.Exists(storageCreationProperties.CacheFilePath)) { var pca = PublicClientApplicationBuilder.Create(storageCreationProperties.ClientId).Build(); pca.UserTokenCache.SetBeforeAccess((args) => { var tempCache = new MsalCacheStorage(storageCreationProperties, s_staticLogger.Value); // We're using ReadData here so that decryption is gets handled within the store. args.TokenCache.DeserializeMsalV3(tempCache.ReadData()); }); var accounts = await pca.GetAccountsAsync().ConfigureAwait(false); foreach (var account in accounts) { accountIdentifiers.Add(account.HomeAccountId.Identifier); } } return(accountIdentifiers); }
/// <summary> /// Gets a new instance of a lock for synchronizing against a cache made with the same creation properties. /// </summary> private static CrossPlatLock CreateCrossPlatLock(StorageCreationProperties storageCreationProperties) { return(new CrossPlatLock( storageCreationProperties.CacheFilePath + ".lockfile", storageCreationProperties.LockRetryDelay, storageCreationProperties.LockRetryCount)); }
/// <summary> /// Initializes a new instance of the <see cref="MsalCacheStorage"/> class. /// </summary> /// <param name="creationProperties">Properties for creating the cache storage on disk</param> /// <param name="logger">logger</param> public MsalCacheStorage(StorageCreationProperties creationProperties, TraceSource logger) { _creationProperties = creationProperties; _logger = logger ?? throw new ArgumentNullException(nameof(logger)); logger.TraceEvent(TraceEventType.Information, /*id*/ 0, $"Initializing '{nameof(MsalCacheStorage)}' with cacheFilePath '{creationProperties.CacheDirectory}'"); // When calling get last writetimeUtc and the file is not found, the time returned is not the minimum date time or datetime offset. // Get a baseline value by trying to get the last write time of a file we know doesn't exist. Then HasChanged will return false // if the cache file actually doesn't exist. DateTimeOffset fileNotFoundOffset; try { logger.TraceEvent(TraceEventType.Information, /*id*/ 0, $"Getting last write file time for a missing file in localappdata"); fileNotFoundOffset = File.GetLastWriteTimeUtc(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), $"{Guid.NewGuid().FormatGuidAsString()}.dll")); } catch (Exception e) { logger.TraceEvent(TraceEventType.Information, /*id*/ 0, $"Problem getting last file write time for missing file, trying temp path('{Path.GetTempPath()}'). {e.Message}"); fileNotFoundOffset = File.GetLastWriteTimeUtc(Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid().FormatGuidAsString()}.dll")); } _lastWriteTime = fileNotFoundOffset; logger.TraceEvent(TraceEventType.Information, /*id*/ 0, $"Finished initializing '{nameof(MsalCacheStorage)}'"); }
/// <summary> /// An internal constructor allowing unit tests to data explicitly rather than initializing here. /// </summary> /// <param name="userTokenCache">The token cache to synchronize with the backing store</param> /// <param name="store">The backing store to use.</param> /// <param name="logger">Passing null uses the default logger</param> internal MsalCacheHelper(ITokenCache userTokenCache, MsalCacheStorage store, TraceSource logger = null) { _logger = logger == null ? s_staticLogger.Value : new TraceSourceLogger(logger); CacheStore = store; _storageCreationProperties = store.StorageCreationProperties; RegisterCache(userTokenCache); }
internal /* internal for test, otherwise private */ MsalCacheStorage( StorageCreationProperties creationProperties, ICacheAccessor cacheAccessor, TraceSourceLogger logger) { StorageCreationProperties = creationProperties; _logger = logger; CacheAccessor = cacheAccessor; _logger.LogInformation($"Initialized '{nameof(MsalCacheStorage)}'"); }
/// <summary> /// Creates a new instance of this class. /// </summary> /// <param name="storageCreationProperties">Properties to use when creating storage on disk.</param> /// <param name="logger">Passing null uses a default logger</param> /// <param name="knownAccountIds">The set of known accounts</param> /// <param name="cacheWatcher">Watcher for the cache file, to enable sending updated events</param> private MsalCacheHelper(StorageCreationProperties storageCreationProperties, TraceSource logger, HashSet <string> knownAccountIds, FileSystemWatcher cacheWatcher) { _logger = logger ?? s_staticLogger.Value; _storageCreationProperties = storageCreationProperties; _store = new MsalCacheStorage(_storageCreationProperties, _logger); _knownAccountIds = knownAccountIds; _cacheWatcher = cacheWatcher; _cacheWatcher.Changed += OnCacheFileChangedAsync; _cacheWatcher.Deleted += OnCacheFileChangedAsync; }
/// <summary> /// Gets the current set of accounts in the cache by creating a new public client, and /// deserializing the cache into a temporary object. /// </summary> private static async Task <HashSet <string> > GetAccountIdentifiersNoLockAsync( // executed in a cross plat lock context StorageCreationProperties storageCreationProperties, TraceSourceLogger logger) { var accountIdentifiers = new HashSet <string>(); if (storageCreationProperties.IsCacheEventConfigured && File.Exists(storageCreationProperties.CacheFilePath)) { var pca = PublicClientApplicationBuilder .Create(storageCreationProperties.ClientId) .WithAuthority(storageCreationProperties.Authority) .Build(); pca.UserTokenCache.SetBeforeAccess((args) => { Storage tempCache = null; try { tempCache = Storage.Create(storageCreationProperties, s_staticLogger.Value.Source); // We're using ReadData here so that decryption is handled within the store. byte[] data = null; try { data = tempCache.ReadData(); } catch { // ignore read failures, we will try again } if (data != null) { args.TokenCache.DeserializeMsalV3(data); } } catch (Exception e) { logger.LogError("An error occured while reading the token cache: " + e); logger.LogError("Deleting the token cache as it might be corrupt."); tempCache.Clear(ignoreExceptions: true); } }); var accounts = await pca.GetAccountsAsync().ConfigureAwait(false); foreach (var account in accounts) { accountIdentifiers.Add(account.HomeAccountId.Identifier); } } return(accountIdentifiers); }
/// <summary> /// Initializes a new instance of the <see cref="Storage"/> class. /// The actual cache reading and writing is OS specific: /// <list type="bullet"> /// <item> /// <term>Windows</term> /// <description>DPAPI encrypted file on behalf of the user. </description> /// </item> /// <item> /// <term>Mac</term> /// <description>Cache is stored in KeyChain. </description> /// </item> /// <item> /// <term>Linux</term> /// <description>Cache is stored in Gnome KeyRing - https://developer.gnome.org/libsecret/0.18/ </description> /// </item> /// </list> /// </summary> /// <param name="creationProperties">Properties for creating the cache storage on disk</param> /// <param name="logger">logger</param> /// <returns></returns> public static Storage Create(StorageCreationProperties creationProperties, TraceSource logger = null) { TraceSourceLogger actualLogger = logger == null ? s_staticLogger.Value : new TraceSourceLogger(logger); ICacheAccessor cacheAccessor; if (creationProperties.UseUnencryptedFallback) { cacheAccessor = new FileAccessor(creationProperties.CacheFilePath, setOwnerOnlyPermissions: true, logger: actualLogger); } else { if (SharedUtilities.IsWindowsPlatform()) { cacheAccessor = new DpApiEncryptedFileAccessor(creationProperties.CacheFilePath, logger: actualLogger); } else if (SharedUtilities.IsMacPlatform()) { cacheAccessor = new MacKeychainAccessor( creationProperties.CacheFilePath, creationProperties.MacKeyChainServiceName, creationProperties.MacKeyChainAccountName, actualLogger); } else if (SharedUtilities.IsLinuxPlatform()) { if (creationProperties.UseLinuxUnencryptedFallback) { cacheAccessor = new FileAccessor(creationProperties.CacheFilePath, setOwnerOnlyPermissions: true, actualLogger); } else { cacheAccessor = new LinuxKeyringAccessor( creationProperties.CacheFilePath, creationProperties.KeyringCollection, creationProperties.KeyringSchemaName, creationProperties.KeyringSecretLabel, creationProperties.KeyringAttribute1.Key, creationProperties.KeyringAttribute1.Value, creationProperties.KeyringAttribute2.Key, creationProperties.KeyringAttribute2.Value, actualLogger); } } else { throw new PlatformNotSupportedException(); } } return(new Storage(creationProperties, cacheAccessor, actualLogger)); }
/// <summary> /// Creates a new instance of <see cref="MsalCacheHelper"/>. /// </summary> /// <param name="storageCreationProperties">Properties to use when creating storage on disk.</param> /// <param name="logger">Passing null uses a default logger</param> /// <returns>A new instance of <see cref="MsalCacheHelper"/>.</returns> public static async Task <MsalCacheHelper> CreateAsync(StorageCreationProperties storageCreationProperties, TraceSource logger = null) { // We want CrossPlatLock around this operation so that we don't have a race against first read of the file and creating the watcher using (CreateCrossPlatLock(storageCreationProperties)) { // Cache the list of accounts var accountIdentifiers = await GetAccountIdentifiersAsync(storageCreationProperties).ConfigureAwait(false); var cacheWatcher = new FileSystemWatcher(storageCreationProperties.CacheDirectory, storageCreationProperties.CacheFileName); var helper = new MsalCacheHelper(storageCreationProperties, logger, accountIdentifiers, cacheWatcher); cacheWatcher.EnableRaisingEvents = true; return(helper); } }
/// <summary> /// Creates a new instance of this class. /// </summary> /// <param name="storageCreationProperties">Properties to use when creating storage on disk.</param> /// <param name="logger">Passing null uses a default logger</param> /// <param name="knownAccountIds">The set of known accounts</param> /// <param name="cacheWatcher">Watcher for the cache file, to enable sending updated events</param> private MsalCacheHelper( StorageCreationProperties storageCreationProperties, TraceSource logger, HashSet <string> knownAccountIds, // only used for CacheChangedEvent FileSystemWatcher cacheWatcher) { _logger = logger == null ? s_staticLogger.Value : new TraceSourceLogger(logger); _storageCreationProperties = storageCreationProperties; CacheStore = Storage.Create(_storageCreationProperties, _logger.Source); _knownAccountIds = knownAccountIds; _cacheWatcher = cacheWatcher; if (_cacheWatcher != null) { _cacheWatcher.Changed += OnCacheFileChangedAsync; _cacheWatcher.Deleted += OnCacheFileChangedAsync; } }
/// <summary> /// Gets the current set of accounts in the cache by creating a new public client, and /// deserializing the cache into a temporary object. /// </summary> private static async Task <HashSet <string> > GetAccountIdentifiersAsync( StorageCreationProperties storageCreationProperties, TraceSourceLogger logger) { var accountIdentifiers = new HashSet <string>(); if (File.Exists(storageCreationProperties.CacheFilePath)) { var pca = PublicClientApplicationBuilder.Create(storageCreationProperties.ClientId).Build(); pca.UserTokenCache.SetBeforeAccess((args) => { MsalCacheStorage tempCache = null; try { tempCache = MsalCacheStorage.Create(storageCreationProperties, s_staticLogger.Value.Source); // We're using ReadData here so that decryption is handled within the store. var data = tempCache.ReadData(); args.TokenCache.DeserializeMsalV3(data); } catch (Exception e) { logger.LogError("An error occured while reading the token cache: " + e); logger.LogError("Deleting the token cache as it might be corrupt."); tempCache.Clear(); } }); var accounts = await pca.GetAccountsAsync().ConfigureAwait(false); foreach (var account in accounts) { accountIdentifiers.Add(account.HomeAccountId.Identifier); } } return(accountIdentifiers); }
/// <summary> /// Initializes a new instance of the <see cref="MsalCacheStorage"/> class. /// </summary> /// <param name="creationProperties">Properties for creating the cache storage on disk</param> /// <param name="logger">logger</param> public MsalCacheStorage(StorageCreationProperties creationProperties, TraceSource logger) { _creationProperties = creationProperties; _logger = logger ?? throw new ArgumentNullException(nameof(logger)); logger.TraceEvent(TraceEventType.Information, /*id*/ 0, $"Initializing '{nameof(MsalCacheStorage)}' with cacheFilePath '{creationProperties.CacheDirectory}'"); try { logger.TraceEvent(TraceEventType.Information, /*id*/ 0, $"Getting last write file time for a missing file in localappdata"); _fileNotFoundOffset = File.GetLastWriteTimeUtc(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), $"{Guid.NewGuid().FormatGuidAsString()}.dll")); } catch (Exception e) { logger.TraceEvent(TraceEventType.Information, /*id*/ 0, $"Problem getting last file write time for missing file, trying temp path('{Path.GetTempPath()}'). {e.Message}"); _fileNotFoundOffset = File.GetLastWriteTimeUtc(Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid().FormatGuidAsString()}.dll")); } CacheFilePath = Path.Combine(creationProperties.CacheDirectory, creationProperties.CacheFileName); _lastWriteTime = _fileNotFoundOffset; logger.TraceEvent(TraceEventType.Information, /*id*/ 0, $"Finished initializing '{nameof(MsalCacheStorage)}'"); }
/// <summary> /// Creates a new instance of this class. /// </summary> /// <param name="storageCreationProperties">Properties to use when creating storage on disk.</param> /// <param name="logger">Passing null uses the default logger</param> public MsalCacheHelper(StorageCreationProperties storageCreationProperties, TraceSource logger = null) { _logger = logger ?? s_staticLogger.Value; _storageCreationProperties = storageCreationProperties; _store = new MsalCacheStorage(_storageCreationProperties, logger); }
/// <summary> /// Gets a new instance of a lock for synchronizing against a cache made with the same creation properties. /// </summary> private static CrossPlatLock CreateCrossPlatLock(StorageCreationProperties storageCreationProperties) { return(new CrossPlatLock(storageCreationProperties.CacheFilePath + ".lockfile")); }
/// <summary> /// Initializes a new instance of the <see cref="MsalCacheStorage"/> class. /// </summary> /// <param name="creationProperties">Properties for creating the cache storage on disk</param> /// <param name="logger">logger</param> public MsalCacheStorage(StorageCreationProperties creationProperties, TraceSource logger = null) { _creationProperties = creationProperties; _logger = logger ?? s_staticLogger.Value; _logger.TraceEvent(TraceEventType.Information, /*id*/ 0, $"Initialized '{nameof(MsalCacheStorage)}'"); }
/// <summary> /// Initializes a new instance of the <see cref="MsalCacheStorage"/> class. /// </summary> /// <param name="creationProperties">Properties for creating the cache storage on disk</param> /// <param name="logger">logger</param> public MsalCacheStorage(StorageCreationProperties creationProperties, TraceSource logger = null) { _creationProperties = creationProperties; _logger = logger ?? throw new ArgumentNullException(nameof(logger)); logger.TraceEvent(TraceEventType.Information, /*id*/ 0, $"Initialized '{nameof(MsalCacheStorage)}'"); }