/// <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);
        }
Example #6
0
 internal /* internal for test, otherwise private */ MsalCacheStorage(
     StorageCreationProperties creationProperties,
     ICacheAccessor cacheAccessor,
     TraceSourceLogger logger)
 {
     StorageCreationProperties = creationProperties;
     _logger       = logger;
     CacheAccessor = cacheAccessor;
     _logger.LogInformation($"Initialized '{nameof(MsalCacheStorage)}'");
 }
Example #7
0
        /// <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;
        }
Example #8
0
        /// <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));
        }
Example #10
0
        /// <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);
            }
        }
Example #11
0
        /// <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)}'");
        }
Example #14
0
 /// <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);
 }
Example #15
0
 /// <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"));
 }
Example #16
0
 /// <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)}'");
 }
Example #17
0
 /// <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)}'");
 }