/* * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) * See https://github.com/openiddict/openiddict-core for more information concerning * the license and the contributors participating to this project. */ using System; using System.Collections.Immutable; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading; using System.Threading.Tasks; using CryptoHelper; using JetBrains.Annotations; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using OpenIddict.Abstractions; namespace OpenIddict.Core { /// /// Provides methods allowing to manage the applications stored in the store. /// /// The type of the Application entity. public class OpenIddictApplicationManager : IOpenIddictApplicationManager where TApplication : class { public OpenIddictApplicationManager( [NotNull] IOpenIddictApplicationStoreResolver resolver, [NotNull] ILogger> logger, [NotNull] IOptions options) { Store = resolver.Get(); Logger = logger; Options = options; } /// /// Gets the logger associated with the current manager. /// protected ILogger Logger { get; } /// /// Gets the options associated with the current manager. /// protected IOptions 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 Task 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 Task CountAsync( [NotNull] Func, IQueryable> query, CancellationToken cancellationToken = default) { if (query == null) { throw new ArgumentNullException(nameof(query)); } return 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 Task CreateAsync([NotNull] 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 Task CreateAsync( [NotNull] TApplication application, [CanBeNull] string secret, CancellationToken cancellationToken = default) { if (application == null) { throw new ArgumentNullException(nameof(application)); } if (!string.IsNullOrEmpty(await Store.GetClientSecretAsync(application, cancellationToken))) { throw new ArgumentException("The client secret hash cannot be directly set on the application entity."); } // 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) ? OpenIddictConstants.ClientTypes.Public : OpenIddictConstants.ClientTypes.Confidential, cancellationToken); } // If the client is not a public application, throw an // exception as the client secret is required in this case. if (string.IsNullOrEmpty(secret) && !await IsPublicAsync(application, cancellationToken)) { throw new InvalidOperationException("A client secret must be provided when creating " + "a confidential or hybrid application."); } // 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 ValidateAsync(application, cancellationToken); if (results.Any(result => result != ValidationResult.Success)) { throw new ValidationException(results.FirstOrDefault(result => result != ValidationResult.Success), null, application); } await Store.CreateAsync(application, cancellationToken); } /// /// 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 Task CreateAsync( [NotNull] OpenIddictApplicationDescriptor descriptor, CancellationToken cancellationToken = default) { if (descriptor == null) { throw new ArgumentNullException(nameof(descriptor)); } var application = await Store.InstantiateAsync(cancellationToken); if (application == null) { throw new InvalidOperationException("An error occurred while trying to create a new application"); } 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 Task DeleteAsync([NotNull] TApplication application, CancellationToken cancellationToken = default) { if (application == null) { throw new ArgumentNullException(nameof(application)); } return Store.DeleteAsync(application, cancellationToken); } /// /// 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 Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(identifier)) { throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); } return Store.FindByIdAsync(identifier, 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 Task FindByClientIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(identifier)) { throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); } // 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. var application = await Store.FindByClientIdAsync(identifier, cancellationToken); if (application == null || !string.Equals(await Store.GetClientIdAsync(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. /// /// A that can be used to monitor the asynchronous operation, whose result /// returns the client applications corresponding to the specified post_logout_redirect_uri. /// public virtual async Task> FindByPostLogoutRedirectUriAsync( [NotNull] string address, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(address)) { throw new ArgumentException("The address cannot be null or empty.", nameof(address)); } // 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. var applications = await Store.FindByPostLogoutRedirectUriAsync(address, cancellationToken); if (applications.IsEmpty) { return ImmutableArray.Create(); } var builder = ImmutableArray.CreateBuilder(applications.Length); foreach (var application in applications) { foreach (var uri in await Store.GetPostLogoutRedirectUrisAsync(application, cancellationToken)) { // Note: the post_logout_redirect_uri must be compared using case-sensitive "Simple String Comparison". if (string.Equals(uri, address, StringComparison.Ordinal)) { builder.Add(application); } } } return builder.Count == builder.Capacity ? builder.MoveToImmutable() : builder.ToImmutable(); } /// /// 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. /// /// A that can be used to monitor the asynchronous operation, whose result /// returns the client applications corresponding to the specified redirect_uri. /// public virtual async Task> FindByRedirectUriAsync( [NotNull] string address, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(address)) { throw new ArgumentException("The address cannot be null or empty.", nameof(address)); } // 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. var applications = await Store.FindByRedirectUriAsync(address, cancellationToken); if (applications.IsEmpty) { return ImmutableArray.Create(); } var builder = ImmutableArray.CreateBuilder(applications.Length); foreach (var application in applications) { foreach (var uri in await Store.GetRedirectUrisAsync(application, cancellationToken)) { // Note: the post_logout_redirect_uri must be compared using case-sensitive "Simple String Comparison". if (string.Equals(uri, address, StringComparison.Ordinal)) { builder.Add(application); } } } return builder.Count == builder.Capacity ? builder.MoveToImmutable() : builder.ToImmutable(); } /// /// 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 Task GetAsync( [NotNull] Func, IQueryable> query, CancellationToken cancellationToken = default) { return GetAsync((applications, state) => state(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 Task GetAsync( [NotNull] Func, TState, IQueryable> query, [CanBeNull] TState state, CancellationToken cancellationToken = default) { if (query == null) { throw new ArgumentNullException(nameof(query)); } return 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([NotNull] TApplication application, CancellationToken cancellationToken = default) { if (application == null) { throw new ArgumentNullException(nameof(application)); } return 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( [NotNull] TApplication application, CancellationToken cancellationToken = default) { if (application == null) { throw new ArgumentNullException(nameof(application)); } async Task ResolveClientTypeAsync() { var type = await Store.GetClientTypeAsync(application, cancellationToken); // Ensure the application type returned by the store is supported by the manager. if (!string.Equals(type, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase) && !string.Equals(type, OpenIddictConstants.ClientTypes.Hybrid, StringComparison.OrdinalIgnoreCase) && !string.Equals(type, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase)) { throw new InvalidOperationException("Only 'confidential', 'hybrid' or 'public' applications are " + "supported by the default application manager."); } return type; } return new ValueTask(ResolveClientTypeAsync()); } /// /// 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 ValueTask GetConsentTypeAsync([NotNull] TApplication application, CancellationToken cancellationToken = default) { if (application == null) { throw new ArgumentNullException(nameof(application)); } async Task ResolveConsentTypeAsync() { var type = await Store.GetConsentTypeAsync(application, cancellationToken); if (string.IsNullOrEmpty(type)) { return OpenIddictConstants.ConsentTypes.Explicit; } return type; } return new ValueTask(ResolveConsentTypeAsync()); } /// /// 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([NotNull] TApplication application, CancellationToken cancellationToken = default) { if (application == null) { throw new ArgumentNullException(nameof(application)); } return Store.GetDisplayNameAsync(application, cancellationToken); } /// /// 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([NotNull] TApplication application, CancellationToken cancellationToken = default) { if (application == null) { throw new ArgumentNullException(nameof(application)); } return Store.GetIdAsync(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( [NotNull] TApplication application, CancellationToken cancellationToken = default) { if (application == null) { throw new ArgumentNullException(nameof(application)); } return 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( [NotNull] TApplication application, CancellationToken cancellationToken = default) { if (application == null) { throw new ArgumentNullException(nameof(application)); } return Store.GetPostLogoutRedirectUrisAsync(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( [NotNull] TApplication application, CancellationToken cancellationToken = default) { if (application == null) { throw new ArgumentNullException(nameof(application)); } return Store.GetRedirectUrisAsync(application, cancellationToken); } /// /// 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 Task HasPermissionAsync( [NotNull] TApplication application, [NotNull] string permission, CancellationToken cancellationToken = default) { if (application == null) { throw new ArgumentNullException(nameof(application)); } if (string.IsNullOrEmpty(permission)) { throw new ArgumentException("The permission name cannot be null or empty.", nameof(permission)); } return (await GetPermissionsAsync(application, cancellationToken)).Contains(permission); } /// /// Determines whether an application is a confidential client. /// /// The application. /// The that can be used to abort the operation. /// true if the application is a confidential client, false otherwise. public async Task IsConfidentialAsync([NotNull] TApplication application, CancellationToken cancellationToken = default) { if (application == null) { throw new ArgumentNullException(nameof(application)); } var type = await GetClientTypeAsync(application, cancellationToken); if (string.IsNullOrEmpty(type)) { return false; } return string.Equals(type, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase); } /// /// Determines whether an application is a hybrid client. /// /// The application. /// The that can be used to abort the operation. /// true if the application is a hybrid client, false otherwise. public async Task IsHybridAsync([NotNull] TApplication application, CancellationToken cancellationToken = default) { if (application == null) { throw new ArgumentNullException(nameof(application)); } var type = await GetClientTypeAsync(application, cancellationToken); if (string.IsNullOrEmpty(type)) { return false; } return string.Equals(type, OpenIddictConstants.ClientTypes.Hybrid, StringComparison.OrdinalIgnoreCase); } /// /// Determines whether an application is a public client. /// /// The application. /// The that can be used to abort the operation. /// true if the application is a public client, false otherwise. public async Task IsPublicAsync([NotNull] TApplication application, CancellationToken cancellationToken = default) { if (application == null) { throw new ArgumentNullException(nameof(application)); } // Assume client applications are public if their type is not explicitly set. var type = await GetClientTypeAsync(application, cancellationToken); if (string.IsNullOrEmpty(type)) { return true; } return string.Equals(type, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase); } /// /// 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. /// /// A that can be used to monitor the asynchronous operation, /// whose result returns all the elements returned when executing the specified query. /// public virtual Task> ListAsync( [CanBeNull] int? count = null, [CanBeNull] int? offset = null, CancellationToken cancellationToken = default) { return 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. /// /// A that can be used to monitor the asynchronous operation, /// whose result returns all the elements returned when executing the specified query. /// public virtual Task> ListAsync( [NotNull] Func, IQueryable> query, CancellationToken cancellationToken = default) { return ListAsync((applications, state) => state(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. /// /// A that can be used to monitor the asynchronous operation, /// whose result returns all the elements returned when executing the specified query. /// public virtual Task> ListAsync( [NotNull] Func, TState, IQueryable> query, [CanBeNull] TState state, CancellationToken cancellationToken = default) { if (query == null) { throw new ArgumentNullException(nameof(query)); } return 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 Task PopulateAsync([NotNull] TApplication application, [NotNull] OpenIddictApplicationDescriptor descriptor, CancellationToken cancellationToken = default) { if (application == null) { throw new ArgumentNullException(nameof(application)); } if (descriptor == null) { throw new ArgumentNullException(nameof(descriptor)); } 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.SetPermissionsAsync(application, ImmutableArray.CreateRange(descriptor.Permissions), cancellationToken); await Store.SetPostLogoutRedirectUrisAsync(application, ImmutableArray.CreateRange( descriptor.PostLogoutRedirectUris.Select(address => address.OriginalString)), cancellationToken); await Store.SetRedirectUrisAsync(application, ImmutableArray.CreateRange( descriptor.RedirectUris.Select(address => address.OriginalString)), 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 Task PopulateAsync( [NotNull] OpenIddictApplicationDescriptor descriptor, [NotNull] TApplication application, CancellationToken cancellationToken = default) { if (descriptor == null) { throw new ArgumentNullException(nameof(descriptor)); } if (application == null) { throw new ArgumentNullException(nameof(application)); } 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.PostLogoutRedirectUris.Clear(); descriptor.RedirectUris.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("Callback URLs cannot be null or empty."); } // Ensure the address is a valid absolute URL. if (!Uri.TryCreate(address, UriKind.Absolute, out Uri uri) || !uri.IsWellFormedOriginalString()) { throw new ArgumentException("Callback URLs must be valid absolute URLs."); } descriptor.PostLogoutRedirectUris.Add(uri); } foreach (var address in await Store.GetRedirectUrisAsync(application, cancellationToken)) { // Ensure the address is not null or empty. if (string.IsNullOrEmpty(address)) { throw new ArgumentException("Callback URLs cannot be null or empty."); } // Ensure the address is a valid absolute URL. if (!Uri.TryCreate(address, UriKind.Absolute, out Uri uri) || !uri.IsWellFormedOriginalString()) { throw new ArgumentException("Callback URLs must be valid absolute URLs."); } 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 Task UpdateAsync([NotNull] TApplication application, CancellationToken cancellationToken = default) { if (application == null) { throw new ArgumentNullException(nameof(application)); } var results = await ValidateAsync(application, cancellationToken); if (results.Any(result => result != ValidationResult.Success)) { throw new ValidationException(results.FirstOrDefault(result => result != ValidationResult.Success), null, application); } await Store.UpdateAsync(application, cancellationToken); } /// /// 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 Task UpdateAsync([NotNull] TApplication application, [CanBeNull] string secret, CancellationToken cancellationToken = default) { if (application == null) { throw new ArgumentNullException(nameof(application)); } if (string.IsNullOrEmpty(secret)) { await Store.SetClientSecretAsync(application, null, cancellationToken); } else { secret = await ObfuscateClientSecretAsync(secret, cancellationToken); await Store.SetClientSecretAsync(application, secret, cancellationToken); } var results = await ValidateAsync(application, cancellationToken); if (results.Any(result => result != ValidationResult.Success)) { throw new ValidationException(results.FirstOrDefault(result => result != ValidationResult.Success), null, application); } 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 Task UpdateAsync([NotNull] TApplication application, [NotNull] OpenIddictApplicationDescriptor descriptor, CancellationToken cancellationToken = default) { if (application == null) { throw new ArgumentNullException(nameof(application)); } if (descriptor == null) { throw new ArgumentNullException(nameof(descriptor)); } // 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, re-obfuscate it before persisting the changes. 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. /// /// A that can be used to monitor the asynchronous operation, /// whose result returns the validation error encountered when validating the application. /// public virtual async Task> ValidateAsync( [NotNull] TApplication application, CancellationToken cancellationToken = default) { if (application == null) { throw new ArgumentNullException(nameof(application)); } var builder = ImmutableArray.CreateBuilder(); // 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)) { builder.Add(new ValidationResult("The client identifier cannot be null or empty.")); } 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 != null && !string.Equals( await Store.GetIdAsync(other, cancellationToken), await Store.GetIdAsync(application, cancellationToken), StringComparison.Ordinal)) { builder.Add(new ValidationResult("An application with the same client identifier already exists.")); } } var type = await Store.GetClientTypeAsync(application, cancellationToken); if (string.IsNullOrEmpty(type)) { builder.Add(new ValidationResult("The client type cannot be null or empty.")); } else { // Ensure the application type is supported by the manager. if (!string.Equals(type, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase) && !string.Equals(type, OpenIddictConstants.ClientTypes.Hybrid, StringComparison.OrdinalIgnoreCase) && !string.Equals(type, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase)) { builder.Add(new ValidationResult("Only 'confidential', 'hybrid' or 'public' applications are " + "supported by the default application manager.")); } // 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, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase)) { builder.Add(new ValidationResult("The client secret cannot be null or empty for a confidential application.")); } // Ensure no client secret was specified if the client is a public application. else if (!string.IsNullOrEmpty(secret) && string.Equals(type, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase)) { builder.Add(new ValidationResult("A client secret cannot be associated with a public application.")); } } // 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)) { builder.Add(new ValidationResult("Callback URLs cannot be null or empty.")); break; } // Ensure the address is a valid absolute URL. if (!Uri.TryCreate(address, UriKind.Absolute, out Uri uri) || !uri.IsWellFormedOriginalString()) { builder.Add(new ValidationResult("Callback URLs must be valid absolute URLs.")); break; } // Ensure the address doesn't contain a fragment. if (!string.IsNullOrEmpty(uri.Fragment)) { builder.Add(new ValidationResult("Callback URLs cannot contain a fragment.")); break; } } var permissions = await Store.GetPermissionsAsync(application, cancellationToken); if (permissions.Contains(OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode)) { if (!permissions.Contains(OpenIddictConstants.Permissions.Endpoints.Authorization) && permissions.Any(permission => permission.StartsWith(OpenIddictConstants.Permissions.Prefixes.Endpoint))) { builder.Add(new ValidationResult( "The authorization code flow permission requires adding the authorization endpoint permission.")); } if (!permissions.Contains(OpenIddictConstants.Permissions.Endpoints.Token) && permissions.Any(permission => permission.StartsWith(OpenIddictConstants.Permissions.Prefixes.Endpoint))) { builder.Add(new ValidationResult( "The authorization code flow permission requires adding the token endpoint permission.")); } } if (permissions.Contains(OpenIddictConstants.Permissions.GrantTypes.ClientCredentials) && !permissions.Contains(OpenIddictConstants.Permissions.Endpoints.Token) && permissions.Any(permission => permission.StartsWith(OpenIddictConstants.Permissions.Prefixes.Endpoint))) { builder.Add(new ValidationResult( "The client credentials flow permission requires adding the token endpoint permission.")); } if (permissions.Contains(OpenIddictConstants.Permissions.GrantTypes.Implicit) && !permissions.Contains(OpenIddictConstants.Permissions.Endpoints.Authorization) && permissions.Any(permission => permission.StartsWith(OpenIddictConstants.Permissions.Prefixes.Endpoint))) { builder.Add(new ValidationResult( "The implicit flow permission requires adding the authorization endpoint permission.")); } if (permissions.Contains(OpenIddictConstants.Permissions.GrantTypes.Password) && !permissions.Contains(OpenIddictConstants.Permissions.Endpoints.Token) && permissions.Any(permission => permission.StartsWith(OpenIddictConstants.Permissions.Prefixes.Endpoint))) { builder.Add(new ValidationResult( "The password flow permission requires adding the token endpoint permission.")); } if (permissions.Contains(OpenIddictConstants.Permissions.GrantTypes.RefreshToken) && !permissions.Contains(OpenIddictConstants.Permissions.Endpoints.Token) && permissions.Any(permission => permission.StartsWith(OpenIddictConstants.Permissions.Prefixes.Endpoint))) { builder.Add(new ValidationResult( "The refresh token flow permission requires adding the token endpoint permission.")); } return builder.Count == builder.Capacity ? builder.MoveToImmutable() : builder.ToImmutable(); } /// /// 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 Task ValidateClientSecretAsync( [NotNull] TApplication application, string secret, CancellationToken cancellationToken = default) { if (application == null) { throw new ArgumentNullException(nameof(application)); } if (await IsPublicAsync(application, cancellationToken)) { Logger.LogWarning("Client authentication cannot be enforced for public applications."); return false; } var value = await Store.GetClientSecretAsync(application, cancellationToken); if (string.IsNullOrEmpty(value)) { Logger.LogError("Client authentication failed for {Client} because " + "no client secret was associated with the application."); return false; } if (!await ValidateClientSecretAsync(secret, value, cancellationToken)) { Logger.LogWarning("Client authentication failed for {Client}.", 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 Task ValidateRedirectUriAsync( [NotNull] TApplication application, [NotNull] string address, CancellationToken cancellationToken = default) { if (application == null) { throw new ArgumentNullException(nameof(application)); } if (string.IsNullOrEmpty(address)) { throw new ArgumentException("The address cannot be null or empty.", 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.LogWarning("Client validation failed because '{RedirectUri}' was not a valid redirect_uri " + "for {Client}.", 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 Task ObfuscateClientSecretAsync([NotNull] string secret, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(secret)) { throw new ArgumentException("The secret cannot be null or empty.", nameof(secret)); } return Task.FromResult(Crypto.HashPassword(secret)); } /// /// 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 Task ValidateClientSecretAsync( [NotNull] string secret, [NotNull] string comparand, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(secret)) { throw new ArgumentException("The secret cannot be null or empty.", nameof(secret)); } if (string.IsNullOrEmpty(comparand)) { throw new ArgumentException("The comparand cannot be null or empty.", nameof(comparand)); } try { return Task.FromResult(Crypto.VerifyHashedPassword(comparand, secret)); } catch (Exception exception) { Logger.LogWarning(0, exception, "An error occurred while trying to verify a client secret. " + "This may indicate that the hashed entry is corrupted or malformed."); return Task.FromResult(false); } } Task IOpenIddictApplicationManager.CountAsync(CancellationToken cancellationToken) => CountAsync(cancellationToken); Task IOpenIddictApplicationManager.CountAsync(Func, IQueryable> query, CancellationToken cancellationToken) => CountAsync(query, cancellationToken); async Task IOpenIddictApplicationManager.CreateAsync(OpenIddictApplicationDescriptor descriptor, CancellationToken cancellationToken) => await CreateAsync(descriptor, cancellationToken); Task IOpenIddictApplicationManager.CreateAsync(object application, CancellationToken cancellationToken) => CreateAsync((TApplication) application, cancellationToken); Task IOpenIddictApplicationManager.CreateAsync(object application, string secret, CancellationToken cancellationToken) => CreateAsync((TApplication) application, secret, cancellationToken); Task IOpenIddictApplicationManager.DeleteAsync(object application, CancellationToken cancellationToken) => DeleteAsync((TApplication) application, cancellationToken); async Task IOpenIddictApplicationManager.FindByClientIdAsync(string identifier, CancellationToken cancellationToken) => await FindByClientIdAsync(identifier, cancellationToken); async Task IOpenIddictApplicationManager.FindByIdAsync(string identifier, CancellationToken cancellationToken) => await FindByIdAsync(identifier, cancellationToken); async Task> IOpenIddictApplicationManager.FindByPostLogoutRedirectUriAsync(string address, CancellationToken cancellationToken) => (await FindByPostLogoutRedirectUriAsync(address, cancellationToken)).CastArray(); async Task> IOpenIddictApplicationManager.FindByRedirectUriAsync(string address, CancellationToken cancellationToken) => (await FindByRedirectUriAsync(address, cancellationToken)).CastArray(); Task IOpenIddictApplicationManager.GetAsync(Func, IQueryable> query, CancellationToken cancellationToken) => GetAsync(query, cancellationToken); Task IOpenIddictApplicationManager.GetAsync(Func, TState, IQueryable> query, TState state, CancellationToken cancellationToken) => 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.GetIdAsync(object application, CancellationToken cancellationToken) => GetIdAsync((TApplication) application, 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.GetRedirectUrisAsync(object application, CancellationToken cancellationToken) => GetRedirectUrisAsync((TApplication) application, cancellationToken); Task IOpenIddictApplicationManager.HasPermissionAsync(object application, string permission, CancellationToken cancellationToken) => HasPermissionAsync((TApplication) application, permission, cancellationToken); Task IOpenIddictApplicationManager.IsConfidentialAsync(object application, CancellationToken cancellationToken) => IsConfidentialAsync((TApplication) application, cancellationToken); Task IOpenIddictApplicationManager.IsHybridAsync(object application, CancellationToken cancellationToken) => IsHybridAsync((TApplication) application, cancellationToken); Task IOpenIddictApplicationManager.IsPublicAsync(object application, CancellationToken cancellationToken) => IsPublicAsync((TApplication) application, cancellationToken); async Task> IOpenIddictApplicationManager.ListAsync(int? count, int? offset, CancellationToken cancellationToken) => (await ListAsync(count, offset, cancellationToken)).CastArray(); Task> IOpenIddictApplicationManager.ListAsync(Func, IQueryable> query, CancellationToken cancellationToken) => ListAsync(query, cancellationToken); Task> IOpenIddictApplicationManager.ListAsync(Func, TState, IQueryable> query, TState state, CancellationToken cancellationToken) => ListAsync(query, state, cancellationToken); Task IOpenIddictApplicationManager.PopulateAsync(OpenIddictApplicationDescriptor descriptor, object application, CancellationToken cancellationToken) => PopulateAsync(descriptor, (TApplication) application, cancellationToken); Task IOpenIddictApplicationManager.PopulateAsync(object application, OpenIddictApplicationDescriptor descriptor, CancellationToken cancellationToken) => PopulateAsync((TApplication) application, descriptor, cancellationToken); Task IOpenIddictApplicationManager.UpdateAsync(object application, CancellationToken cancellationToken) => UpdateAsync((TApplication) application, cancellationToken); Task IOpenIddictApplicationManager.UpdateAsync(object application, OpenIddictApplicationDescriptor descriptor, CancellationToken cancellationToken) => UpdateAsync((TApplication) application, descriptor, cancellationToken); Task IOpenIddictApplicationManager.UpdateAsync(object application, string secret, CancellationToken cancellationToken) => UpdateAsync((TApplication) application, secret, cancellationToken); Task> IOpenIddictApplicationManager.ValidateAsync(object application, CancellationToken cancellationToken) => ValidateAsync((TApplication) application, cancellationToken); Task IOpenIddictApplicationManager.ValidateClientSecretAsync(object application, string secret, CancellationToken cancellationToken) => ValidateClientSecretAsync((TApplication) application, secret, cancellationToken); Task IOpenIddictApplicationManager.ValidateRedirectUriAsync(object application, string address, CancellationToken cancellationToken) => ValidateRedirectUriAsync((TApplication) application, address, cancellationToken); } }