Versatile OpenID Connect stack for ASP.NET Core and Microsoft.Owin (compatible with ASP.NET 4.6.1)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

421 lines
17 KiB

/*
* 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.Concurrent;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using OpenIddict.Abstractions;
namespace OpenIddict.Core
{
/// <summary>
/// Provides methods allowing to cache applications after retrieving them from the store.
/// </summary>
/// <typeparam name="TApplication">The type of the Application entity.</typeparam>
public class OpenIddictApplicationCache<TApplication> : IOpenIddictApplicationCache<TApplication>, IDisposable where TApplication : class
{
private readonly MemoryCache _cache;
private readonly IOptions<OpenIddictCoreOptions> _options;
private readonly ConcurrentDictionary<string, Lazy<CancellationTokenSource>> _signals;
private readonly IOpenIddictApplicationStore<TApplication> _store;
public OpenIddictApplicationCache(
[NotNull] IOptions<OpenIddictCoreOptions> options,
[NotNull] IOpenIddictApplicationStoreResolver resolver)
{
_cache = new MemoryCache(new MemoryCacheOptions());
_options = options;
_signals = new ConcurrentDictionary<string, Lazy<CancellationTokenSource>>(StringComparer.Ordinal);
_store = resolver.Get<TApplication>();
}
/// <summary>
/// Add the specified application to the cache.
/// </summary>
/// <param name="application">The application to add to the cache.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public async Task AddAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
if (_cache.Count >= _options.Value.EntityCacheLimit)
{
_cache.Compact(0.25);
}
_cache.Remove(new
{
Method = nameof(FindByClientIdAsync),
Identifier = await _store.GetClientIdAsync(application, cancellationToken)
});
_cache.Remove(new
{
Method = nameof(FindByIdAsync),
Identifier = await _store.GetIdAsync(application, cancellationToken)
});
foreach (var address in await _store.GetPostLogoutRedirectUrisAsync(application, cancellationToken))
{
_cache.Remove(new
{
Method = nameof(FindByPostLogoutRedirectUriAsync),
Address = address
});
}
foreach (var address in await _store.GetRedirectUrisAsync(application, cancellationToken))
{
_cache.Remove(new
{
Method = nameof(FindByRedirectUriAsync),
Address = address
});
}
var signal = await CreateExpirationSignalAsync(application, cancellationToken);
if (signal == null)
{
throw new InvalidOperationException("An error occurred while creating an expiration signal.");
}
using (var entry = _cache.CreateEntry(new
{
Method = nameof(FindByIdAsync),
Identifier = await _store.GetIdAsync(application, cancellationToken)
}))
{
entry.AddExpirationToken(signal)
.SetValue(application);
}
using (var entry = _cache.CreateEntry(new
{
Method = nameof(FindByClientIdAsync),
Identifier = await _store.GetClientIdAsync(application, cancellationToken)
}))
{
entry.AddExpirationToken(signal)
.SetValue(application);
}
}
/// <summary>
/// Disposes the resources held by this instance.
/// </summary>
public void Dispose()
{
foreach (var signal in _signals)
{
signal.Value.Value.Dispose();
}
_cache.Dispose();
}
/// <summary>
/// Retrieves an application using its client identifier.
/// </summary>
/// <param name="identifier">The client identifier associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
/// </returns>
public ValueTask<TApplication> FindByClientIdAsync([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(FindByClientIdAsync),
Identifier = identifier
};
if (_cache.TryGetValue(parameters, out TApplication application))
{
return new ValueTask<TApplication>(application);
}
async Task<TApplication> ExecuteAsync()
{
if ((application = await _store.FindByClientIdAsync(identifier, cancellationToken)) != null)
{
await AddAsync(application, cancellationToken);
}
using (var entry = _cache.CreateEntry(parameters))
{
if (application != null)
{
var signal = await CreateExpirationSignalAsync(application, cancellationToken);
if (signal == null)
{
throw new InvalidOperationException("An error occurred while creating an expiration signal.");
}
entry.AddExpirationToken(signal);
}
entry.SetValue(application);
}
return application;
}
return new ValueTask<TApplication>(ExecuteAsync());
}
/// <summary>
/// Retrieves an application using its unique identifier.
/// </summary>
/// <param name="identifier">The unique identifier associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
/// </returns>
public ValueTask<TApplication> 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 TApplication application))
{
return new ValueTask<TApplication>(application);
}
async Task<TApplication> ExecuteAsync()
{
if ((application = await _store.FindByIdAsync(identifier, cancellationToken)) != null)
{
await AddAsync(application, cancellationToken);
}
using (var entry = _cache.CreateEntry(parameters))
{
if (application != null)
{
var signal = await CreateExpirationSignalAsync(application, cancellationToken);
if (signal == null)
{
throw new InvalidOperationException("An error occurred while creating an expiration signal.");
}
entry.AddExpirationToken(signal);
}
entry.SetValue(application);
}
return application;
}
return new ValueTask<TApplication>(ExecuteAsync());
}
/// <summary>
/// Retrieves all the applications associated with the specified post_logout_redirect_uri.
/// </summary>
/// <param name="address">The post_logout_redirect_uri associated with the applications.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client applications corresponding to the specified post_logout_redirect_uri.
/// </returns>
public ValueTask<ImmutableArray<TApplication>> FindByPostLogoutRedirectUriAsync(
[NotNull] string address, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(address))
{
throw new ArgumentException("The address cannot be null or empty.", nameof(address));
}
var parameters = new
{
Method = nameof(FindByPostLogoutRedirectUriAsync),
Address = address
};
if (_cache.TryGetValue(parameters, out ImmutableArray<TApplication> applications))
{
return new ValueTask<ImmutableArray<TApplication>>(applications);
}
async Task<ImmutableArray<TApplication>> ExecuteAsync()
{
foreach (var application in (applications = await _store.FindByPostLogoutRedirectUriAsync(address, cancellationToken)))
{
await AddAsync(application, cancellationToken);
}
using (var entry = _cache.CreateEntry(parameters))
{
foreach (var application in applications)
{
var signal = await CreateExpirationSignalAsync(application, cancellationToken);
if (signal == null)
{
throw new InvalidOperationException("An error occurred while creating an expiration signal.");
}
entry.AddExpirationToken(signal);
}
entry.SetValue(applications);
}
return applications;
}
return new ValueTask<ImmutableArray<TApplication>>(ExecuteAsync());
}
/// <summary>
/// Retrieves all the applications associated with the specified redirect_uri.
/// </summary>
/// <param name="address">The redirect_uri associated with the applications.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client applications corresponding to the specified redirect_uri.
/// </returns>
public ValueTask<ImmutableArray<TApplication>> FindByRedirectUriAsync(
[NotNull] string address, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(address))
{
throw new ArgumentException("The address cannot be null or empty.", nameof(address));
}
var parameters = new
{
Method = nameof(FindByRedirectUriAsync),
Address = address
};
if (_cache.TryGetValue(parameters, out ImmutableArray<TApplication> applications))
{
return new ValueTask<ImmutableArray<TApplication>>(applications);
}
async Task<ImmutableArray<TApplication>> ExecuteAsync()
{
foreach (var application in (applications = await _store.FindByRedirectUriAsync(address, cancellationToken)))
{
await AddAsync(application, cancellationToken);
}
using (var entry = _cache.CreateEntry(parameters))
{
foreach (var application in applications)
{
var signal = await CreateExpirationSignalAsync(application, cancellationToken);
if (signal == null)
{
throw new InvalidOperationException("An error occurred while creating an expiration signal.");
}
entry.AddExpirationToken(signal);
}
entry.SetValue(applications);
}
return applications;
}
return new ValueTask<ImmutableArray<TApplication>>(ExecuteAsync());
}
/// <summary>
/// Removes the specified application from the cache.
/// </summary>
/// <param name="application">The application to remove from the cache.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public async Task RemoveAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
var identifier = await _store.GetIdAsync(application, cancellationToken);
if (string.IsNullOrEmpty(identifier))
{
throw new InvalidOperationException("The application identifier cannot be extracted.");
}
if (_signals.TryGetValue(identifier, out Lazy<CancellationTokenSource> signal))
{
signal.Value.Cancel();
_signals.TryRemove(identifier, out signal);
}
}
/// <summary>
/// Creates an expiration signal allowing to invalidate all the
/// cache entries associated with the specified application.
/// </summary>
/// <param name="application">The application associated with the expiration signal.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns an expiration signal for the specified application.
/// </returns>
protected virtual async Task<IChangeToken> CreateExpirationSignalAsync(
[NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
var identifier = await _store.GetIdAsync(application, cancellationToken);
if (string.IsNullOrEmpty(identifier))
{
throw new InvalidOperationException("The application identifier cannot be extracted.");
}
var signal = _signals.GetOrAdd(identifier, delegate
{
// Note: a Lazy<CancellationTokenSource> is used here to ensure only one CancellationTokenSource
// can be created. Not doing so would result in expiration signals being potentially linked to
// multiple sources, with a single one of them being eventually tracked and thus, cancelable.
return new Lazy<CancellationTokenSource>(() => new CancellationTokenSource());
});
return new CancellationChangeToken(signal.Value.Token);
}
}
}