/* * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) * See https://github.com/openiddict/openiddict-core for more information concerning * the license and the contributors participating to this project. */ using System; using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using OpenIddict.Abstractions; namespace OpenIddict.Core { /// /// Provides methods allowing to cache tokens after retrieving them from the store. /// /// The type of the Token entity. public class OpenIddictTokenCache : IOpenIddictTokenCache, IDisposable where TToken : class { private readonly MemoryCache _cache; private readonly IOpenIddictTokenStore _store; private readonly IOptions _options; public OpenIddictTokenCache( [NotNull] IOptions options, [NotNull] IOpenIddictTokenStoreResolver resolver) { _cache = new MemoryCache(new MemoryCacheOptions()); _options = options; _store = resolver.Get(); } /// /// Add the specified token to the cache. /// /// The token to add to the cache. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation. /// public async Task AddAsync([NotNull] TToken token, CancellationToken cancellationToken) { if (token == null) { throw new ArgumentNullException(nameof(token)); } if (_cache.Count >= _options.Value.EntityCacheLimit) { _cache.Compact(0.25); } using (var entry = _cache.CreateEntry(new { Method = nameof(FindByIdAsync), Identifier = await _store.GetIdAsync(token, cancellationToken) })) { entry.SetValue(token); } using (var entry = _cache.CreateEntry(new { Method = nameof(FindByReferenceIdAsync), Identifier = await _store.GetReferenceIdAsync(token, cancellationToken) })) { entry.SetValue(token); } } /// /// Disposes the cache held by this instance. /// public void Dispose() => _cache.Dispose(); /// /// Retrieves the tokens corresponding to the specified /// subject and associated with the application identifier. /// /// The subject associated with the token. /// The client associated with the token. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, /// whose result returns the tokens corresponding to the subject/client. /// public ValueTask> FindAsync([NotNull] string subject, [NotNull] string client, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(subject)) { throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); } if (string.IsNullOrEmpty(client)) { throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client)); } var parameters = new { Method = nameof(FindAsync), Subject = subject, Client = client }; if (_cache.TryGetValue(parameters, out ImmutableArray tokens)) { return new ValueTask>(tokens); } async Task> ExecuteAsync() { foreach (var token in (tokens = await _store.FindAsync(subject, client, cancellationToken))) { await AddAsync(token, cancellationToken); } using (var entry = _cache.CreateEntry(parameters)) { entry.SetValue(tokens); } return tokens; } return new ValueTask>(ExecuteAsync()); } /// /// Retrieves the tokens matching the specified parameters. /// /// The subject associated with the token. /// The client associated with the token. /// The token status. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, /// whose result returns the tokens corresponding to the criteria. /// public ValueTask> FindAsync( [NotNull] string subject, [NotNull] string client, [NotNull] string status, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(subject)) { throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); } if (string.IsNullOrEmpty(client)) { throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client)); } if (string.IsNullOrEmpty(status)) { throw new ArgumentException("The status cannot be null or empty.", nameof(status)); } var parameters = new { Method = nameof(FindAsync), Subject = subject, Client = client, Status = status }; if (_cache.TryGetValue(parameters, out ImmutableArray tokens)) { return new ValueTask>(tokens); } async Task> ExecuteAsync() { foreach (var token in (tokens = await _store.FindAsync(subject, client, status, cancellationToken))) { await AddAsync(token, cancellationToken); } using (var entry = _cache.CreateEntry(parameters)) { entry.SetValue(tokens); } return tokens; } return new ValueTask>(ExecuteAsync()); } /// /// Retrieves the tokens matching the specified parameters. /// /// The subject associated with the token. /// The client associated with the token. /// The token status. /// The token type. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, /// whose result returns the tokens corresponding to the criteria. /// public ValueTask> FindAsync( [NotNull] string subject, [NotNull] string client, [NotNull] string status, [NotNull] string type, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(subject)) { throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); } if (string.IsNullOrEmpty(client)) { throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client)); } if (string.IsNullOrEmpty(status)) { throw new ArgumentException("The status cannot be null or empty.", nameof(status)); } if (string.IsNullOrEmpty(type)) { throw new ArgumentException("The type cannot be null or empty.", nameof(type)); } var parameters = new { Method = nameof(FindAsync), Subject = subject, Client = client, Status = status, Type = type }; if (_cache.TryGetValue(parameters, out ImmutableArray tokens)) { return new ValueTask>(tokens); } async Task> ExecuteAsync() { foreach (var token in (tokens = await _store.FindAsync(subject, client, status, type, cancellationToken))) { await AddAsync(token, cancellationToken); } using (var entry = _cache.CreateEntry(parameters)) { entry.SetValue(tokens); } return tokens; } return new ValueTask>(ExecuteAsync()); } /// /// Retrieves the list of tokens corresponding to the specified application identifier. /// /// The application identifier associated with the tokens. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, /// whose result returns the tokens corresponding to the specified application. /// public ValueTask> FindByApplicationIdAsync( [NotNull] string identifier, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(identifier)) { throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); } var parameters = new { Method = nameof(FindByApplicationIdAsync), Identifier = identifier }; if (_cache.TryGetValue(parameters, out ImmutableArray tokens)) { return new ValueTask>(tokens); } async Task> ExecuteAsync() { foreach (var token in (tokens = await _store.FindByApplicationIdAsync(identifier, cancellationToken))) { await AddAsync(token, cancellationToken); } using (var entry = _cache.CreateEntry(parameters)) { entry.SetValue(tokens); } return tokens; } return new ValueTask>(ExecuteAsync()); } /// /// Retrieves the list of tokens corresponding to the specified authorization identifier. /// /// The authorization identifier associated with the tokens. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, /// whose result returns the tokens corresponding to the specified authorization. /// public ValueTask> FindByAuthorizationIdAsync( [NotNull] string identifier, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(identifier)) { throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); } var parameters = new { Method = nameof(FindByAuthorizationIdAsync), Identifier = identifier }; if (_cache.TryGetValue(parameters, out ImmutableArray tokens)) { return new ValueTask>(tokens); } async Task> ExecuteAsync() { foreach (var token in (tokens = await _store.FindByAuthorizationIdAsync(identifier, cancellationToken))) { await AddAsync(token, cancellationToken); } using (var entry = _cache.CreateEntry(parameters)) { entry.SetValue(tokens); } return tokens; } return new ValueTask>(ExecuteAsync()); } /// /// Retrieves a token using its unique identifier. /// /// The unique identifier associated with the token. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, /// whose result returns the token corresponding to the unique identifier. /// public ValueTask FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(identifier)) { throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); } var parameters = new { Method = nameof(FindByIdAsync), Identifier = identifier }; if (_cache.TryGetValue(parameters, out TToken token)) { return new ValueTask(token); } async Task ExecuteAsync() { if ((token = await _store.FindByIdAsync(identifier, cancellationToken)) != null) { await AddAsync(token, cancellationToken); } using (var entry = _cache.CreateEntry(parameters)) { entry.SetValue(token); } return token; } return new ValueTask(ExecuteAsync()); } /// /// Retrieves the list of tokens corresponding to the specified reference identifier. /// Note: the reference identifier may be hashed or encrypted for security reasons. /// /// The reference identifier associated with the tokens. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, /// whose result returns the tokens corresponding to the specified reference identifier. /// public ValueTask FindByReferenceIdAsync([NotNull] string identifier, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(identifier)) { throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); } var parameters = new { Method = nameof(FindByReferenceIdAsync), Identifier = identifier }; if (_cache.TryGetValue(parameters, out TToken token)) { return new ValueTask(token); } async Task ExecuteAsync() { if ((token = await _store.FindByReferenceIdAsync(identifier, cancellationToken)) != null) { await AddAsync(token, cancellationToken); } using (var entry = _cache.CreateEntry(parameters)) { entry.SetValue(token); } return token; } return new ValueTask(ExecuteAsync()); } /// /// Retrieves the list of tokens corresponding to the specified subject. /// /// The subject associated with the tokens. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, /// whose result returns the tokens corresponding to the specified subject. /// public ValueTask> FindBySubjectAsync([NotNull] string subject, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(subject)) { throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); } var parameters = new { Method = nameof(FindBySubjectAsync), Identifier = subject }; if (_cache.TryGetValue(parameters, out ImmutableArray tokens)) { return new ValueTask>(tokens); } async Task> ExecuteAsync() { foreach (var token in (tokens = await _store.FindBySubjectAsync(subject, cancellationToken))) { await AddAsync(token, cancellationToken); } using (var entry = _cache.CreateEntry(parameters)) { entry.SetValue(tokens); } return tokens; } return new ValueTask>(ExecuteAsync()); } /// /// Removes the specified token from the cache. /// /// The token to remove from the cache. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation. /// public async Task RemoveAsync([NotNull] TToken token, CancellationToken cancellationToken) { if (token == null) { throw new ArgumentNullException(nameof(token)); } _cache.Remove(new { Method = nameof(FindAsync), Subject = await _store.GetSubjectAsync(token, cancellationToken), Client = await _store.GetApplicationIdAsync(token, cancellationToken) }); _cache.Remove(new { Method = nameof(FindAsync), Subject = await _store.GetSubjectAsync(token, cancellationToken), Client = await _store.GetApplicationIdAsync(token, cancellationToken), Status = await _store.GetStatusAsync(token, cancellationToken) }); _cache.Remove(new { Method = nameof(FindAsync), Subject = await _store.GetSubjectAsync(token, cancellationToken), Client = await _store.GetApplicationIdAsync(token, cancellationToken), Status = await _store.GetStatusAsync(token, cancellationToken), Type = await _store.GetTypeAsync(token, cancellationToken) }); _cache.Remove(new { Method = nameof(FindByApplicationIdAsync), Identifier = await _store.GetApplicationIdAsync(token, cancellationToken) }); _cache.Remove(new { Method = nameof(FindByAuthorizationIdAsync), Identifier = await _store.GetAuthorizationIdAsync(token, cancellationToken) }); _cache.Remove(new { Method = nameof(FindByIdAsync), Identifier = await _store.GetIdAsync(token, cancellationToken) }); _cache.Remove(new { Method = nameof(FindByReferenceIdAsync), Identifier = await _store.GetReferenceIdAsync(token, cancellationToken) }); _cache.Remove(new { Method = nameof(FindBySubjectAsync), Subject = await _store.GetSubjectAsync(token, cancellationToken) }); } } }