/* * 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 IOpenIddictApplicationManager.CreateAsync(OpenIddictApplicationDescriptor descriptor, CancellationToken cancellationToken) => await CreateAsync(descriptor, cancellationToken); /// ValueTask IOpenIddictApplicationManager.CreateAsync(object application, CancellationToken cancellationToken) => CreateAsync((TApplication) application, cancellationToken); /// ValueTask IOpenIddictApplicationManager.CreateAsync(object application, string? secret, CancellationToken cancellationToken) => CreateAsync((TApplication) application, secret, cancellationToken); /// ValueTask IOpenIddictApplicationManager.DeleteAsync(object application, CancellationToken cancellationToken) => DeleteAsync((TApplication) application, cancellationToken); /// async ValueTask IOpenIddictApplicationManager.FindByClientIdAsync(string identifier, CancellationToken cancellationToken) => await FindByClientIdAsync(identifier, cancellationToken); /// async ValueTask IOpenIddictApplicationManager.FindByIdAsync(string identifier, CancellationToken cancellationToken) => await FindByIdAsync(identifier, cancellationToken); /// IAsyncEnumerable IOpenIddictApplicationManager.FindByPostLogoutRedirectUriAsync(string address, CancellationToken cancellationToken) => FindByPostLogoutRedirectUriAsync(address, cancellationToken); /// IAsyncEnumerable IOpenIddictApplicationManager.FindByRedirectUriAsync(string address, CancellationToken cancellationToken) => FindByRedirectUriAsync(address, cancellationToken); /// ValueTask IOpenIddictApplicationManager.GetAsync(Func, IQueryable> query, CancellationToken cancellationToken) where TResult : default => GetAsync(query, cancellationToken); /// ValueTask IOpenIddictApplicationManager.GetAsync(Func, TState, IQueryable> query, TState state, CancellationToken cancellationToken) where TResult : default => GetAsync(query, state, cancellationToken); /// ValueTask IOpenIddictApplicationManager.GetClientIdAsync(object application, CancellationToken cancellationToken) => GetClientIdAsync((TApplication) application, cancellationToken); /// ValueTask IOpenIddictApplicationManager.GetClientTypeAsync(object application, CancellationToken cancellationToken) => GetClientTypeAsync((TApplication) application, cancellationToken); /// ValueTask IOpenIddictApplicationManager.GetConsentTypeAsync(object application, CancellationToken cancellationToken) => GetConsentTypeAsync((TApplication) application, cancellationToken); /// ValueTask IOpenIddictApplicationManager.GetDisplayNameAsync(object application, CancellationToken cancellationToken) => GetDisplayNameAsync((TApplication) application, cancellationToken); /// ValueTask> IOpenIddictApplicationManager.GetDisplayNamesAsync(object application, CancellationToken cancellationToken) => GetDisplayNamesAsync((TApplication) application, cancellationToken); /// ValueTask IOpenIddictApplicationManager.GetIdAsync(object application, CancellationToken cancellationToken) => GetIdAsync((TApplication) application, cancellationToken); /// ValueTask IOpenIddictApplicationManager.GetLocalizedDisplayNameAsync(object application, CancellationToken cancellationToken) => GetLocalizedDisplayNameAsync((TApplication) application, cancellationToken); /// ValueTask IOpenIddictApplicationManager.GetLocalizedDisplayNameAsync(object application, CultureInfo culture, CancellationToken cancellationToken) => GetLocalizedDisplayNameAsync((TApplication) application, culture, cancellationToken); /// ValueTask> IOpenIddictApplicationManager.GetPermissionsAsync(object application, CancellationToken cancellationToken) => GetPermissionsAsync((TApplication) application, cancellationToken); /// ValueTask> IOpenIddictApplicationManager.GetPostLogoutRedirectUrisAsync(object application, CancellationToken cancellationToken) => GetPostLogoutRedirectUrisAsync((TApplication) application, cancellationToken); /// ValueTask> IOpenIddictApplicationManager.GetPropertiesAsync(object application, CancellationToken cancellationToken) => GetPropertiesAsync((TApplication) application, cancellationToken); /// ValueTask> IOpenIddictApplicationManager.GetRedirectUrisAsync(object application, CancellationToken cancellationToken) => GetRedirectUrisAsync((TApplication) application, cancellationToken); /// ValueTask> IOpenIddictApplicationManager.GetRequirementsAsync(object application, CancellationToken cancellationToken) => GetRequirementsAsync((TApplication) application, cancellationToken); /// ValueTask IOpenIddictApplicationManager.HasClientTypeAsync(object application, string type, CancellationToken cancellationToken) => HasClientTypeAsync((TApplication) application, type, cancellationToken); /// ValueTask IOpenIddictApplicationManager.HasConsentTypeAsync(object application, string type, CancellationToken cancellationToken) => HasConsentTypeAsync((TApplication) application, type, cancellationToken); /// ValueTask IOpenIddictApplicationManager.HasPermissionAsync(object application, string permission, CancellationToken cancellationToken) => HasPermissionAsync((TApplication) application, permission, cancellationToken); /// ValueTask IOpenIddictApplicationManager.HasRequirementAsync(object application, string requirement, CancellationToken cancellationToken) => HasRequirementAsync((TApplication) application, requirement, cancellationToken); /// IAsyncEnumerable IOpenIddictApplicationManager.ListAsync(int? count, int? offset, CancellationToken cancellationToken) => ListAsync(count, offset, cancellationToken); /// IAsyncEnumerable IOpenIddictApplicationManager.ListAsync(Func, IQueryable> query, CancellationToken cancellationToken) => ListAsync(query, cancellationToken); /// IAsyncEnumerable IOpenIddictApplicationManager.ListAsync(Func, TState, IQueryable> query, TState state, CancellationToken cancellationToken) => ListAsync(query, state, cancellationToken); /// ValueTask IOpenIddictApplicationManager.PopulateAsync(OpenIddictApplicationDescriptor descriptor, object application, CancellationToken cancellationToken) => PopulateAsync(descriptor, (TApplication) application, cancellationToken); /// ValueTask IOpenIddictApplicationManager.PopulateAsync(object application, OpenIddictApplicationDescriptor descriptor, CancellationToken cancellationToken) => PopulateAsync((TApplication) application, descriptor, cancellationToken); /// ValueTask IOpenIddictApplicationManager.UpdateAsync(object application, CancellationToken cancellationToken) => UpdateAsync((TApplication) application, cancellationToken); /// ValueTask IOpenIddictApplicationManager.UpdateAsync(object application, OpenIddictApplicationDescriptor descriptor, CancellationToken cancellationToken) => UpdateAsync((TApplication) application, descriptor, cancellationToken); /// ValueTask IOpenIddictApplicationManager.UpdateAsync(object application, string? secret, CancellationToken cancellationToken) => UpdateAsync((TApplication) application, secret, cancellationToken); /// IAsyncEnumerable IOpenIddictApplicationManager.ValidateAsync(object application, CancellationToken cancellationToken) => ValidateAsync((TApplication) application, cancellationToken); /// ValueTask IOpenIddictApplicationManager.ValidateClientSecretAsync(object application, string secret, CancellationToken cancellationToken) => ValidateClientSecretAsync((TApplication) application, secret, cancellationToken); /// ValueTask IOpenIddictApplicationManager.ValidateRedirectUriAsync(object application, string address, CancellationToken cancellationToken) => ValidateRedirectUriAsync((TApplication) application, address, cancellationToken); }