/*
* 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.Buffers.Binary;
using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
#if !SUPPORTS_KEY_DERIVATION_WITH_SPECIFIED_HASH_ALGORITHM
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Digests;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Parameters;
#endif
#if !SUPPORTS_TIME_CONSTANT_COMPARISONS
using Org.BouncyCastle.Utilities;
#endif
namespace OpenIddict.Core;
///
/// Provides methods allowing to manage the applications stored in the store.
///
///
/// Applications that do not want to depend on a specific entity type can use the non-generic
/// instead, for which the actual entity type
/// is resolved at runtime based on the default entity type registered in the core options.
///
/// The type of the Application entity.
public class OpenIddictApplicationManager : IOpenIddictApplicationManager where TApplication : class
{
public OpenIddictApplicationManager(
IOpenIddictApplicationCache cache!!,
ILogger> logger!!,
IOptionsMonitor options!!,
IOpenIddictApplicationStoreResolver resolver!!)
{
Cache = cache;
Logger = logger;
Options = options;
Store = resolver.Get();
}
///
/// Gets the cache associated with the current manager.
///
protected IOpenIddictApplicationCache Cache { get; }
///
/// Gets the logger associated with the current manager.
///
protected ILogger Logger { get; }
///
/// Gets the options associated with the current manager.
///
protected IOptionsMonitor Options { get; }
///
/// Gets the store associated with the current manager.
///
protected IOpenIddictApplicationStore Store { get; }
///
/// Determines the number of applications that exist in the database.
///
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the number of applications in the database.
///
public virtual ValueTask CountAsync(CancellationToken cancellationToken = default)
=> Store.CountAsync(cancellationToken);
///
/// Determines the number of applications that match the specified query.
///
/// The result type.
/// The query to execute.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the number of applications that match the specified query.
///
public virtual ValueTask CountAsync(
Func, IQueryable> query!!, CancellationToken cancellationToken = default)
=> Store.CountAsync(query, cancellationToken);
///
/// Creates a new application.
///
/// The application to create.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation.
///
public virtual ValueTask CreateAsync(TApplication application, CancellationToken cancellationToken = default)
=> CreateAsync(application, secret: null, cancellationToken);
///
/// Creates a new application.
/// Note: the default implementation automatically hashes the client
/// secret before storing it in the database, for security reasons.
///
/// The application to create.
/// The client secret associated with the application, if applicable.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation.
///
public virtual async ValueTask CreateAsync(TApplication application!!, string? secret, CancellationToken cancellationToken = default)
{
if (!string.IsNullOrEmpty(await Store.GetClientSecretAsync(application, cancellationToken)))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0206), nameof(application));
}
// If no client type was specified, assume it's a public application if no secret was provided.
var type = await Store.GetClientTypeAsync(application, cancellationToken);
if (string.IsNullOrEmpty(type))
{
await Store.SetClientTypeAsync(application, string.IsNullOrEmpty(secret) ?
ClientTypes.Public : ClientTypes.Confidential, cancellationToken);
}
// If a client secret was provided, obfuscate it.
if (!string.IsNullOrEmpty(secret))
{
secret = await ObfuscateClientSecretAsync(secret, cancellationToken);
await Store.SetClientSecretAsync(application, secret, cancellationToken);
}
var results = await GetValidationResultsAsync(application, cancellationToken);
if (results.Any(result => result != ValidationResult.Success))
{
var builder = new StringBuilder();
builder.AppendLine(SR.GetResourceString(SR.ID0207));
builder.AppendLine();
foreach (var result in results)
{
builder.AppendLine(result.ErrorMessage);
}
throw new OpenIddictExceptions.ValidationException(builder.ToString(), results);
}
await Store.CreateAsync(application, cancellationToken);
if (!Options.CurrentValue.DisableEntityCaching)
{
await Cache.AddAsync(application, cancellationToken);
}
async Task> GetValidationResultsAsync(
TApplication application, CancellationToken cancellationToken)
{
var builder = ImmutableArray.CreateBuilder();
await foreach (var result in ValidateAsync(application, cancellationToken))
{
builder.Add(result);
}
return builder.ToImmutable();
}
}
///
/// Creates a new application based on the specified descriptor.
/// Note: the default implementation automatically hashes the client
/// secret before storing it in the database, for security reasons.
///
/// The application descriptor.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the unique identifier associated with the application.
///
public virtual async ValueTask CreateAsync(
OpenIddictApplicationDescriptor descriptor!!, CancellationToken cancellationToken = default)
{
var application = await Store.InstantiateAsync(cancellationToken);
if (application is null)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0208));
}
await PopulateAsync(application, descriptor, cancellationToken);
var secret = await Store.GetClientSecretAsync(application, cancellationToken);
if (!string.IsNullOrEmpty(secret))
{
await Store.SetClientSecretAsync(application, secret: null, cancellationToken);
await CreateAsync(application, secret, cancellationToken);
}
else
{
await CreateAsync(application, cancellationToken);
}
return application;
}
///
/// Removes an existing application.
///
/// The application to delete.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation.
///
public virtual async ValueTask DeleteAsync(TApplication application!!, CancellationToken cancellationToken = default)
{
if (!Options.CurrentValue.DisableEntityCaching)
{
await Cache.RemoveAsync(application, cancellationToken);
}
await Store.DeleteAsync(application, cancellationToken);
}
///
/// Retrieves an application using its client identifier.
///
/// The client identifier associated with the application.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
///
public virtual async ValueTask FindByClientIdAsync(
string identifier, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0195), nameof(identifier));
}
var application = Options.CurrentValue.DisableEntityCaching ?
await Store.FindByClientIdAsync(identifier, cancellationToken) :
await Cache.FindByClientIdAsync(identifier, cancellationToken);
if (application is null)
{
return null;
}
// SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
// To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
// used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
if (!Options.CurrentValue.DisableAdditionalFiltering &&
!string.Equals(await Store.GetClientIdAsync(application, cancellationToken), identifier, StringComparison.Ordinal))
{
return null;
}
return application;
}
///
/// Retrieves an application using its unique identifier.
///
/// The unique identifier associated with the application.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
///
public virtual async ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0195), nameof(identifier));
}
var application = Options.CurrentValue.DisableEntityCaching ?
await Store.FindByIdAsync(identifier, cancellationToken) :
await Cache.FindByIdAsync(identifier, cancellationToken);
if (application is null)
{
return null;
}
// SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
// To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
// used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
if (!Options.CurrentValue.DisableAdditionalFiltering &&
!string.Equals(await Store.GetIdAsync(application, cancellationToken), identifier, StringComparison.Ordinal))
{
return null;
}
return application;
}
///
/// Retrieves all the applications associated with the specified post_logout_redirect_uri.
///
/// The post_logout_redirect_uri associated with the applications.
/// The that can be used to abort the operation.
/// The client applications corresponding to the specified post_logout_redirect_uri.
public virtual IAsyncEnumerable FindByPostLogoutRedirectUriAsync(
string address, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(address))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0143), nameof(address));
}
var applications = Options.CurrentValue.DisableEntityCaching ?
Store.FindByPostLogoutRedirectUriAsync(address, cancellationToken) :
Cache.FindByPostLogoutRedirectUriAsync(address, cancellationToken);
if (Options.CurrentValue.DisableAdditionalFiltering)
{
return applications;
}
return ExecuteAsync(cancellationToken);
// SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
// To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
// used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var application in applications)
{
var addresses = await Store.GetPostLogoutRedirectUrisAsync(application, cancellationToken);
if (addresses.Contains(address, StringComparer.Ordinal))
{
yield return application;
}
}
}
}
///
/// Retrieves all the applications associated with the specified redirect_uri.
///
/// The redirect_uri associated with the applications.
/// The that can be used to abort the operation.
/// The client applications corresponding to the specified redirect_uri.
public virtual IAsyncEnumerable FindByRedirectUriAsync(
string address, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(address))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0143), nameof(address));
}
var applications = Options.CurrentValue.DisableEntityCaching ?
Store.FindByRedirectUriAsync(address, cancellationToken) :
Cache.FindByRedirectUriAsync(address, cancellationToken);
if (Options.CurrentValue.DisableAdditionalFiltering)
{
return applications;
}
// SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
// To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
// used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
return ExecuteAsync(cancellationToken);
async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var application in applications)
{
var addresses = await Store.GetRedirectUrisAsync(application, cancellationToken);
if (addresses.Contains(address, StringComparer.Ordinal))
{
yield return application;
}
}
}
}
///
/// Executes the specified query and returns the first element.
///
/// The result type.
/// The query to execute.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
///
public virtual ValueTask GetAsync(
Func, IQueryable> query!!, CancellationToken cancellationToken = default)
=> GetAsync(static (applications, query) => query(applications), query, cancellationToken);
///
/// Executes the specified query and returns the first element.
///
/// The state type.
/// The result type.
/// The query to execute.
/// The optional state.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
///
public virtual ValueTask GetAsync(
Func, TState, IQueryable> query!!,
TState state, CancellationToken cancellationToken = default)
=> Store.GetAsync(query, state, cancellationToken);
///
/// Retrieves the client identifier associated with an application.
///
/// The application.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the client identifier associated with the application.
///
public virtual ValueTask GetClientIdAsync(
TApplication application!!, CancellationToken cancellationToken = default)
=> Store.GetClientIdAsync(application, cancellationToken);
///
/// Retrieves the client type associated with an application.
///
/// The application.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the client type of the application (by default, "public").
///
public virtual ValueTask GetClientTypeAsync(
TApplication application!!, CancellationToken cancellationToken = default)
=> Store.GetClientTypeAsync(application, cancellationToken);
///
/// Retrieves the consent type associated with an application.
///
/// The application.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the consent type of the application (by default, "explicit").
///
public virtual async ValueTask GetConsentTypeAsync(
TApplication application!!, CancellationToken cancellationToken = default)
{
var type = await Store.GetConsentTypeAsync(application, cancellationToken);
if (string.IsNullOrEmpty(type))
{
return ConsentTypes.Explicit;
}
return type;
}
///
/// Retrieves the display name associated with an application.
///
/// The application.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the display name associated with the application.
///
public virtual ValueTask GetDisplayNameAsync(
TApplication application!!, CancellationToken cancellationToken = default)
=> Store.GetDisplayNameAsync(application, cancellationToken);
///
/// Retrieves the localized display names associated with an application.
///
/// The application.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation,
/// whose result returns all the localized display names associated with the application.
///
public virtual async ValueTask> GetDisplayNamesAsync(
TApplication application!!, CancellationToken cancellationToken = default)
=> await Store.GetDisplayNamesAsync(application, cancellationToken) is { Count: > 0 } names ?
names : ImmutableDictionary.Create();
///
/// Retrieves the unique identifier associated with an application.
///
/// The application.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the unique identifier associated with the application.
///
public virtual ValueTask GetIdAsync(TApplication application!!, CancellationToken cancellationToken = default)
=> Store.GetIdAsync(application, cancellationToken);
///
/// Retrieves the localized display name associated with an application
/// and corresponding to the current UI culture or one of its parents.
/// If no matching value can be found, the non-localized value is returned.
///
/// The application.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the matching localized display name associated with the application.
///
public virtual ValueTask GetLocalizedDisplayNameAsync(
TApplication application, CancellationToken cancellationToken = default)
=> GetLocalizedDisplayNameAsync(application, CultureInfo.CurrentUICulture, cancellationToken);
///
/// Retrieves the localized display name associated with an application
/// and corresponding to the specified culture or one of its parents.
/// If no matching value can be found, the non-localized value is returned.
///
/// The application.
/// The culture (typically ).
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the matching localized display name associated with the application.
///
public virtual async ValueTask GetLocalizedDisplayNameAsync(
TApplication application!!, CultureInfo culture!!, CancellationToken cancellationToken = default)
{
var names = await Store.GetDisplayNamesAsync(application, cancellationToken);
if (names is not { IsEmpty: false })
{
return await Store.GetDisplayNameAsync(application, cancellationToken);
}
do
{
if (names.TryGetValue(culture, out var name))
{
return name;
}
culture = culture.Parent;
}
while (culture != CultureInfo.InvariantCulture);
return await Store.GetDisplayNameAsync(application, cancellationToken);
}
///
/// Retrieves the permissions associated with an application.
///
/// The application.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation,
/// whose result returns all the permissions associated with the application.
///
public virtual ValueTask> GetPermissionsAsync(
TApplication application!!, CancellationToken cancellationToken = default)
=> Store.GetPermissionsAsync(application, cancellationToken);
///
/// Retrieves the logout callback addresses associated with an application.
///
/// The application.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation,
/// whose result returns all the post_logout_redirect_uri associated with the application.
///
public virtual ValueTask> GetPostLogoutRedirectUrisAsync(
TApplication application!!, CancellationToken cancellationToken = default)
=> Store.GetPostLogoutRedirectUrisAsync(application, cancellationToken);
///
/// Retrieves the additional properties associated with an application.
///
/// The application.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation,
/// whose result returns all the additional properties associated with the application.
///
public virtual ValueTask> GetPropertiesAsync(
TApplication application!!, CancellationToken cancellationToken = default)
=> Store.GetPropertiesAsync(application, cancellationToken);
///
/// Retrieves the callback addresses associated with an application.
///
/// The application.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation,
/// whose result returns all the redirect_uri associated with the application.
///
public virtual ValueTask> GetRedirectUrisAsync(
TApplication application!!, CancellationToken cancellationToken = default)
=> Store.GetRedirectUrisAsync(application, cancellationToken);
///
/// Retrieves the requirements associated with an application.
///
/// The application.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation,
/// whose result returns all the requirements associated with the application.
///
public virtual ValueTask> GetRequirementsAsync(
TApplication application!!, CancellationToken cancellationToken = default)
=> Store.GetRequirementsAsync(application, cancellationToken);
///
/// Determines whether a given application has the specified client type.
///
/// The application.
/// The expected client type.
/// The that can be used to abort the operation.
/// true if the application has the specified client type, false otherwise.
public virtual async ValueTask HasClientTypeAsync(
TApplication application!!, string type, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(type))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0209), nameof(type));
}
return string.Equals(await GetClientTypeAsync(application, cancellationToken), type, StringComparison.OrdinalIgnoreCase);
}
///
/// Determines whether a given application has the specified consent type.
///
/// The application.
/// The expected consent type.
/// The that can be used to abort the operation.
/// true if the application has the specified consent type, false otherwise.
public virtual async ValueTask HasConsentTypeAsync(
TApplication application!!, string type, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(type))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0210), nameof(type));
}
return string.Equals(await GetConsentTypeAsync(application, cancellationToken), type, StringComparison.OrdinalIgnoreCase);
}
///
/// Determines whether the specified permission has been granted to the application.
///
/// The application.
/// The permission.
/// The that can be used to abort the operation.
/// true if the application has been granted the specified permission, false otherwise.
public virtual async ValueTask HasPermissionAsync(
TApplication application!!, string permission, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(permission))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0211), nameof(permission));
}
return (await GetPermissionsAsync(application, cancellationToken)).Contains(permission, StringComparer.Ordinal);
}
///
/// Determines whether the specified requirement has been enforced for the specified application.
///
/// The application.
/// The requirement.
/// The that can be used to abort the operation.
/// true if the requirement has been enforced for the specified application, false otherwise.
public virtual async ValueTask HasRequirementAsync(
TApplication application!!, string requirement, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(requirement))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0212), nameof(requirement));
}
return (await GetRequirementsAsync(application, cancellationToken)).Contains(requirement, StringComparer.Ordinal);
}
///
/// Executes the specified query and returns all the corresponding elements.
///
/// The number of results to return.
/// The number of results to skip.
/// The that can be used to abort the operation.
/// All the elements returned when executing the specified query.
public virtual IAsyncEnumerable ListAsync(
int? count = null, int? offset = null, CancellationToken cancellationToken = default)
=> Store.ListAsync(count, offset, cancellationToken);
///
/// Executes the specified query and returns all the corresponding elements.
///
/// The result type.
/// The query to execute.
/// The that can be used to abort the operation.
/// All the elements returned when executing the specified query.
public virtual IAsyncEnumerable ListAsync(
Func, IQueryable> query!!, CancellationToken cancellationToken = default)
=> ListAsync(static (applications, query) => query(applications), query, cancellationToken);
///
/// Executes the specified query and returns all the corresponding elements.
///
/// The state type.
/// The result type.
/// The query to execute.
/// The optional state.
/// The that can be used to abort the operation.
/// All the elements returned when executing the specified query.
public virtual IAsyncEnumerable ListAsync(
Func, TState, IQueryable> query!!,
TState state, CancellationToken cancellationToken = default)
=> Store.ListAsync(query, state, cancellationToken);
///
/// Populates the application using the specified descriptor.
///
/// The application.
/// The descriptor.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation.
///
public virtual async ValueTask PopulateAsync(TApplication application!!,
OpenIddictApplicationDescriptor descriptor!!, CancellationToken cancellationToken = default)
{
await Store.SetClientIdAsync(application, descriptor.ClientId, cancellationToken);
await Store.SetClientSecretAsync(application, descriptor.ClientSecret, cancellationToken);
await Store.SetClientTypeAsync(application, descriptor.Type, cancellationToken);
await Store.SetConsentTypeAsync(application, descriptor.ConsentType, cancellationToken);
await Store.SetDisplayNameAsync(application, descriptor.DisplayName, cancellationToken);
await Store.SetDisplayNamesAsync(application, descriptor.DisplayNames.ToImmutableDictionary(), cancellationToken);
await Store.SetPermissionsAsync(application, descriptor.Permissions.ToImmutableArray(), cancellationToken);
await Store.SetPostLogoutRedirectUrisAsync(application, ImmutableArray.CreateRange(
descriptor.PostLogoutRedirectUris.Select(address => address.OriginalString)), cancellationToken);
await Store.SetPropertiesAsync(application, descriptor.Properties.ToImmutableDictionary(), cancellationToken);
await Store.SetRedirectUrisAsync(application, ImmutableArray.CreateRange(
descriptor.RedirectUris.Select(address => address.OriginalString)), cancellationToken);
await Store.SetRequirementsAsync(application, descriptor.Requirements.ToImmutableArray(), cancellationToken);
}
///
/// Populates the specified descriptor using the properties exposed by the application.
///
/// The descriptor.
/// The application.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation.
///
public virtual async ValueTask PopulateAsync(
OpenIddictApplicationDescriptor descriptor!!,
TApplication application!!, CancellationToken cancellationToken = default)
{
descriptor.ClientId = await Store.GetClientIdAsync(application, cancellationToken);
descriptor.ClientSecret = await Store.GetClientSecretAsync(application, cancellationToken);
descriptor.ConsentType = await Store.GetConsentTypeAsync(application, cancellationToken);
descriptor.DisplayName = await Store.GetDisplayNameAsync(application, cancellationToken);
descriptor.Type = await Store.GetClientTypeAsync(application, cancellationToken);
descriptor.Permissions.Clear();
descriptor.Permissions.UnionWith(await Store.GetPermissionsAsync(application, cancellationToken));
descriptor.Requirements.Clear();
descriptor.Requirements.UnionWith(await Store.GetRequirementsAsync(application, cancellationToken));
descriptor.DisplayNames.Clear();
foreach (var pair in await Store.GetDisplayNamesAsync(application, cancellationToken))
{
descriptor.DisplayNames.Add(pair.Key, pair.Value);
}
descriptor.PostLogoutRedirectUris.Clear();
foreach (var address in await Store.GetPostLogoutRedirectUrisAsync(application, cancellationToken))
{
// Ensure the address is not null or empty.
if (string.IsNullOrEmpty(address))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0213));
}
// Ensure the address is a valid absolute URL.
if (!Uri.TryCreate(address, UriKind.Absolute, out Uri? uri) || !uri.IsWellFormedOriginalString())
{
throw new ArgumentException(SR.GetResourceString(SR.ID0214));
}
descriptor.PostLogoutRedirectUris.Add(uri);
}
descriptor.Properties.Clear();
foreach (var pair in await Store.GetPropertiesAsync(application, cancellationToken))
{
descriptor.Properties.Add(pair.Key, pair.Value);
}
descriptor.RedirectUris.Clear();
foreach (var address in await Store.GetRedirectUrisAsync(application, cancellationToken))
{
// Ensure the address is not null or empty.
if (string.IsNullOrEmpty(address))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0213));
}
// Ensure the address is a valid absolute URL.
if (!Uri.TryCreate(address, UriKind.Absolute, out Uri? uri) || !uri.IsWellFormedOriginalString())
{
throw new ArgumentException(SR.GetResourceString(SR.ID0214));
}
descriptor.RedirectUris.Add(uri);
}
}
///
/// Updates an existing application.
///
/// The application to update.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation.
///
public virtual async ValueTask UpdateAsync(TApplication application!!, CancellationToken cancellationToken = default)
{
var results = await GetValidationResultsAsync(application, cancellationToken);
if (results.Any(result => result != ValidationResult.Success))
{
var builder = new StringBuilder();
builder.AppendLine(SR.GetResourceString(SR.ID0215));
builder.AppendLine();
foreach (var result in results)
{
builder.AppendLine(result.ErrorMessage);
}
throw new OpenIddictExceptions.ValidationException(builder.ToString(), results);
}
await Store.UpdateAsync(application, cancellationToken);
if (!Options.CurrentValue.DisableEntityCaching)
{
await Cache.RemoveAsync(application, cancellationToken);
await Cache.AddAsync(application, cancellationToken);
}
async Task> GetValidationResultsAsync(
TApplication application, CancellationToken cancellationToken)
{
var builder = ImmutableArray.CreateBuilder();
await foreach (var result in ValidateAsync(application, cancellationToken))
{
builder.Add(result);
}
return builder.ToImmutable();
}
}
///
/// Updates an existing application and replaces the existing secret.
/// Note: the default implementation automatically hashes the client
/// secret before storing it in the database, for security reasons.
///
/// The application to update.
/// The client secret associated with the application.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation.
///
public virtual async ValueTask UpdateAsync(TApplication application!!, string? secret, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(secret))
{
await Store.SetClientSecretAsync(application, null, cancellationToken);
}
else
{
secret = await ObfuscateClientSecretAsync(secret, cancellationToken);
await Store.SetClientSecretAsync(application, secret, cancellationToken);
}
await UpdateAsync(application, cancellationToken);
}
///
/// Updates an existing application.
///
/// The application to update.
/// The descriptor used to update the application.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation.
///
public virtual async ValueTask UpdateAsync(TApplication application!!,
OpenIddictApplicationDescriptor descriptor!!, CancellationToken cancellationToken = default)
{
// Store the original client secret for later comparison.
var comparand = await Store.GetClientSecretAsync(application, cancellationToken);
await PopulateAsync(application, descriptor, cancellationToken);
// If the client secret was updated, use the overload accepting a secret parameter.
var secret = await Store.GetClientSecretAsync(application, cancellationToken);
if (!string.Equals(secret, comparand, StringComparison.Ordinal))
{
await UpdateAsync(application, secret, cancellationToken);
return;
}
await UpdateAsync(application, cancellationToken);
}
///
/// Validates the application to ensure it's in a consistent state.
///
/// The application.
/// The that can be used to abort the operation.
/// The validation error encountered when validating the application.
public virtual async IAsyncEnumerable ValidateAsync(
TApplication application!!, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Ensure the client_id is not null or empty and is not already used for a different application.
var identifier = await Store.GetClientIdAsync(application, cancellationToken);
if (string.IsNullOrEmpty(identifier))
{
yield return new ValidationResult(SR.GetResourceString(SR.ID2036));
}
else
{
// Note: depending on the database/table/query collation used by the store, an application
// whose client_id doesn't exactly match the specified value may be returned (e.g because
// the casing is different). To avoid issues when the client identifier is part of an index
// using the same collation, an error is added even if the two identifiers don't exactly match.
var other = await Store.FindByClientIdAsync(identifier, cancellationToken);
if (other is not null && !string.Equals(
await Store.GetIdAsync(other, cancellationToken),
await Store.GetIdAsync(application, cancellationToken), StringComparison.Ordinal))
{
yield return new ValidationResult(SR.GetResourceString(SR.ID2111));
}
}
var type = await Store.GetClientTypeAsync(application, cancellationToken);
if (string.IsNullOrEmpty(type))
{
yield return new ValidationResult(SR.GetResourceString(SR.ID2050));
}
else
{
// Ensure the application type is supported by the manager.
if (!string.Equals(type, ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase) &&
!string.Equals(type, ClientTypes.Public, StringComparison.OrdinalIgnoreCase))
{
yield return new ValidationResult(SR.GetResourceString(SR.ID2112));
}
// Ensure a client secret was specified if the client is a confidential application.
var secret = await Store.GetClientSecretAsync(application, cancellationToken);
if (string.IsNullOrEmpty(secret) && string.Equals(type, ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase))
{
yield return new ValidationResult(SR.GetResourceString(SR.ID2113));
}
// Ensure no client secret was specified if the client is a public application.
else if (!string.IsNullOrEmpty(secret) && string.Equals(type, ClientTypes.Public, StringComparison.OrdinalIgnoreCase))
{
yield return new ValidationResult(SR.GetResourceString(SR.ID2114));
}
}
// When callback URLs are specified, ensure they are valid and spec-compliant.
// See https://tools.ietf.org/html/rfc6749#section-3.1 for more information.
foreach (var address in ImmutableArray.Create()
.AddRange(await Store.GetPostLogoutRedirectUrisAsync(application, cancellationToken))
.AddRange(await Store.GetRedirectUrisAsync(application, cancellationToken)))
{
// Ensure the address is not null or empty.
if (string.IsNullOrEmpty(address))
{
yield return new ValidationResult(SR.GetResourceString(SR.ID2061));
break;
}
// Ensure the address is a valid absolute URL.
if (!Uri.TryCreate(address, UriKind.Absolute, out Uri? uri) || !uri.IsWellFormedOriginalString())
{
yield return new ValidationResult(SR.GetResourceString(SR.ID2062));
break;
}
// Ensure the address doesn't contain a fragment.
if (!string.IsNullOrEmpty(uri.Fragment))
{
yield return new ValidationResult(SR.GetResourceString(SR.ID2115));
break;
}
// To prevent issuer fixation attacks where a malicious client would specify an "iss" parameter
// in the callback URL, ensure the query - if present - doesn't include an "iss" parameter.
if (!string.IsNullOrEmpty(uri.Query) && uri.Query.TrimStart(Separators.QuestionMark[0])
.Split(new[] { Separators.Ampersand[0], Separators.Semicolon[0] }, StringSplitOptions.RemoveEmptyEntries)
.Select(parameter => parameter.Split(Separators.EqualsSign, StringSplitOptions.RemoveEmptyEntries))
.Select(parts => parts[0] is string value ? Uri.UnescapeDataString(value) : null)
.Contains(Parameters.Iss, StringComparer.OrdinalIgnoreCase))
{
yield return new ValidationResult(SR.FormatID2134(Parameters.Iss));
break;
}
}
}
///
/// Validates the client_secret associated with an application.
///
/// The application.
/// The secret that should be compared to the client_secret stored in the database.
/// The that can be used to abort the operation.
/// A that can be used to monitor the asynchronous operation.
///
/// A that can be used to monitor the asynchronous operation,
/// whose result returns a boolean indicating whether the client secret was valid.
///
public virtual async ValueTask ValidateClientSecretAsync(
TApplication application!!, string secret, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(secret))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0216), nameof(secret));
}
if (await HasClientTypeAsync(application, ClientTypes.Public, cancellationToken))
{
Logger.LogWarning(SR.GetResourceString(SR.ID6159));
return false;
}
var value = await Store.GetClientSecretAsync(application, cancellationToken);
if (string.IsNullOrEmpty(value))
{
Logger.LogError(SR.GetResourceString(SR.ID6160), await GetClientIdAsync(application, cancellationToken));
return false;
}
if (!await ValidateClientSecretAsync(secret, value, cancellationToken))
{
Logger.LogInformation(SR.GetResourceString(SR.ID6161), await GetClientIdAsync(application, cancellationToken));
return false;
}
return true;
}
///
/// Validates the redirect_uri to ensure it's associated with an application.
///
/// The application.
/// The address that should be compared to one of the redirect_uri stored in the database.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation,
/// whose result returns a boolean indicating whether the redirect_uri was valid.
///
public virtual async ValueTask ValidateRedirectUriAsync(
TApplication application!!, string address, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(address))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0143), nameof(address));
}
foreach (var uri in await Store.GetRedirectUrisAsync(application, cancellationToken))
{
// Note: the redirect_uri must be compared using case-sensitive "Simple String Comparison".
// See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest for more information.
if (string.Equals(uri, address, StringComparison.Ordinal))
{
return true;
}
}
Logger.LogInformation(SR.GetResourceString(SR.ID6162), address, await GetClientIdAsync(application, cancellationToken));
return false;
}
///
/// Obfuscates the specified client secret so it can be safely stored in a database.
/// By default, this method returns a complex hashed representation computed using PBKDF2.
///
/// The client secret.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation.
///
protected virtual ValueTask ObfuscateClientSecretAsync(string secret, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(secret))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0216), nameof(secret));
}
// Note: the PRF, iteration count, salt length and key length currently all match the default values
// used by CryptoHelper and ASP.NET Core Identity but this may change in the future, if necessary.
var salt = new byte[128 / 8];
#if SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS
RandomNumberGenerator.Fill(salt);
#else
using var generator = RandomNumberGenerator.Create();
generator.GetBytes(salt);
#endif
var hash = HashSecret(secret, salt, HashAlgorithmName.SHA256, iterations: 10_000, length: 256 / 8);
return new(
#if SUPPORTS_BASE64_SPAN_CONVERSION
Convert.ToBase64String(hash)
#else
Convert.ToBase64String(hash.ToArray())
#endif
);
// Note: the following logic deliberately uses the same format as CryptoHelper (used in OpenIddict 1.x/2.x),
// which was itself based on ASP.NET Core Identity's latest hashed password format. This guarantees that
// secrets hashed using a recent OpenIddict version can still be read by older packages (and vice versa).
static ReadOnlySpan HashSecret(string secret, ReadOnlySpan salt,
HashAlgorithmName algorithm, int iterations, int length)
{
var key = DeriveKey(secret, salt, algorithm, iterations, length);
var payload = new Span(new byte[13 + salt.Length + key.Length]);
// Write the format marker.
payload[0] = 0x01;
// Write the hashing algorithm version.
BinaryPrimitives.WriteUInt32BigEndian(payload.Slice(1, 4), algorithm switch
{
var name when name == HashAlgorithmName.SHA1 => 0,
var name when name == HashAlgorithmName.SHA256 => 1,
var name when name == HashAlgorithmName.SHA512 => 2,
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0217))
});
// Write the iteration count of the algorithm.
BinaryPrimitives.WriteUInt32BigEndian(payload.Slice(5, 8), (uint) iterations);
// Write the size of the salt.
BinaryPrimitives.WriteUInt32BigEndian(payload.Slice(9, 12), (uint) salt.Length);
// Write the salt.
salt.CopyTo(payload.Slice(13));
// Write the subkey.
key.CopyTo(payload.Slice(13 + salt.Length));
return payload;
}
}
///
/// Validates the specified value to ensure it corresponds to the client secret.
/// Note: when overriding this method, using a time-constant comparer is strongly recommended.
///
/// The client secret to compare to the value stored in the database.
/// The value stored in the database, which is usually a hashed representation of the secret.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation,
/// whose result returns a boolean indicating whether the specified value was valid.
///
protected virtual ValueTask ValidateClientSecretAsync(
string secret, string comparand, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(secret))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0216), nameof(secret));
}
if (string.IsNullOrEmpty(comparand))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0218), nameof(comparand));
}
try
{
return new(VerifyHashedSecret(comparand, secret));
}
catch (Exception exception)
{
Logger.LogWarning(exception, SR.GetResourceString(SR.ID6163));
return new(false);
}
// Note: the following logic deliberately uses the same format as CryptoHelper (used in OpenIddict 1.x/2.x),
// which was itself based on ASP.NET Core Identity's latest hashed password format. This guarantees that
// secrets hashed using a recent OpenIddict version can still be read by older packages (and vice versa).
static bool VerifyHashedSecret(string hash, string secret)
{
var payload = new ReadOnlySpan(Convert.FromBase64String(hash));
if (payload.Length == 0)
{
return false;
}
// Verify the hashing format version.
if (payload[0] != 0x01)
{
return false;
}
// Read the hashing algorithm version.
var algorithm = (int) BinaryPrimitives.ReadUInt32BigEndian(payload.Slice(1, 4)) switch
{
0 => HashAlgorithmName.SHA1,
1 => HashAlgorithmName.SHA256,
2 => HashAlgorithmName.SHA512,
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0217))
};
// Read the iteration count of the algorithm.
var iterations = (int) BinaryPrimitives.ReadUInt32BigEndian(payload.Slice(5, 8));
// Read the size of the salt and ensure it's more than 128 bits.
var saltLength = (int) BinaryPrimitives.ReadUInt32BigEndian(payload.Slice(9, 12));
if (saltLength < 128 / 8)
{
return false;
}
// Read the salt.
var salt = payload.Slice(13, saltLength);
// Ensure the derived key length is more than 128 bits.
var keyLength = payload.Length - 13 - salt.Length;
if (keyLength < 128 / 8)
{
return false;
}
#if SUPPORTS_TIME_CONSTANT_COMPARISONS
return CryptographicOperations.FixedTimeEquals(
left: payload.Slice(13 + salt.Length, keyLength),
right: DeriveKey(secret, salt, algorithm, iterations, keyLength));
#else
return Arrays.ConstantTimeAreEqual(
a: payload.Slice(13 + salt.Length, keyLength).ToArray(),
b: DeriveKey(secret, salt, algorithm, iterations, keyLength));
#endif
}
}
[SuppressMessage("Security", "CA5379:Do not use weak key derivation function algorithm",
Justification = "The SHA-1 digest algorithm is still supported for backward compatibility.")]
private static byte[] DeriveKey(string secret, ReadOnlySpan salt,
HashAlgorithmName algorithm, int iterations, int length)
{
#if SUPPORTS_KEY_DERIVATION_WITH_SPECIFIED_HASH_ALGORITHM
using var generator = new Rfc2898DeriveBytes(secret, salt.ToArray(), iterations, algorithm);
return generator.GetBytes(length);
#else
var generator = new Pkcs5S2ParametersGenerator(algorithm switch
{
var name when name == HashAlgorithmName.SHA1 => new Sha1Digest(),
var name when name == HashAlgorithmName.SHA256 => new Sha256Digest(),
var name when name == HashAlgorithmName.SHA512 => new Sha512Digest(),
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0217))
});
generator.Init(PbeParametersGenerator.Pkcs5PasswordToBytes(secret.ToCharArray()), salt.ToArray(), iterations);
var key = (KeyParameter) generator.GenerateDerivedMacParameters(length * 8);
return key.GetKey();
#endif
}
///
ValueTask IOpenIddictApplicationManager.CountAsync(CancellationToken cancellationToken)
=> CountAsync(cancellationToken);
///
ValueTask IOpenIddictApplicationManager.CountAsync(Func, IQueryable> query, CancellationToken cancellationToken)
=> CountAsync(query, cancellationToken);
///
async ValueTask