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

1256 lines
60 KiB

/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System;
using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CryptoHelper;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
namespace OpenIddict.Core
{
/// <summary>
/// Provides methods allowing to manage the applications stored in the store.
/// </summary>
/// <typeparam name="TApplication">The type of the Application entity.</typeparam>
public class OpenIddictApplicationManager<TApplication> where TApplication : class
{
public OpenIddictApplicationManager(
[NotNull] IOpenIddictApplicationStore<TApplication> store,
[NotNull] ILogger<OpenIddictApplicationManager<TApplication>> logger)
{
Store = store;
Logger = logger;
}
/// <summary>
/// Gets the logger associated with the current manager.
/// </summary>
protected ILogger Logger { get; }
/// <summary>
/// Gets the store associated with the current manager.
/// </summary>
protected IOpenIddictApplicationStore<TApplication> Store { get; }
/// <summary>
/// Determines the number of applications that exist in the database.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of applications in the database.
/// </returns>
public virtual Task<long> CountAsync(CancellationToken cancellationToken = default)
{
return Store.CountAsync(cancellationToken);
}
/// <summary>
/// Determines the number of applications that match the specified query.
/// </summary>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of applications that match the specified query.
/// </returns>
public virtual Task<long> CountAsync<TResult>(
[NotNull] Func<IQueryable<TApplication>, IQueryable<TResult>> query, CancellationToken cancellationToken = default)
{
if (query == null)
{
throw new ArgumentNullException(nameof(query));
}
return Store.CountAsync(query, cancellationToken);
}
/// <summary>
/// Creates a new application.
/// </summary>
/// <param name="application">The application to create.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task CreateAsync([NotNull] TApplication application, CancellationToken cancellationToken = default)
=> CreateAsync(application, /* secret: */ null, cancellationToken);
/// <summary>
/// Creates a new application.
/// Note: the default implementation automatically hashes the client
/// secret before storing it in the database, for security reasons.
/// </summary>
/// <param name="application">The application to create.</param>
/// <param name="secret">The client secret associated with the application, if applicable.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public 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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="descriptor">The application descriptor.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the unique identifier associated with the application.
/// </returns>
public virtual async Task<TApplication> 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;
}
/// <summary>
/// Removes an existing application.
/// </summary>
/// <param name="application">The application to delete.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task DeleteAsync([NotNull] TApplication application, CancellationToken cancellationToken = default)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return Store.DeleteAsync(application, cancellationToken);
}
/// <summary>
/// Retrieves an application using its unique identifier.
/// </summary>
/// <param name="identifier">The unique identifier associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
/// </returns>
public virtual Task<TApplication> 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);
}
/// <summary>
/// Retrieves an application using its client identifier.
/// </summary>
/// <param name="identifier">The client identifier associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
/// </returns>
public virtual Task<TApplication> FindByClientIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
return Store.FindByClientIdAsync(identifier, cancellationToken);
}
/// <summary>
/// Retrieves all the applications associated with the specified post_logout_redirect_uri.
/// </summary>
/// <param name="address">The post_logout_redirect_uri associated with the applications.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified post_logout_redirect_uri.
/// </returns>
public virtual Task<ImmutableArray<TApplication>> FindByPostLogoutRedirectUriAsync(
[NotNull] string address, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(address))
{
throw new ArgumentException("The address cannot be null or empty.", nameof(address));
}
return Store.FindByPostLogoutRedirectUriAsync(address, cancellationToken);
}
/// <summary>
/// Retrieves all the applications associated with the specified redirect_uri.
/// </summary>
/// <param name="address">The redirect_uri associated with the applications.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified redirect_uri.
/// </returns>
public virtual Task<ImmutableArray<TApplication>> FindByRedirectUriAsync(
[NotNull] string address, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(address))
{
throw new ArgumentException("The address cannot be null or empty.", nameof(address));
}
return Store.FindByRedirectUriAsync(address, cancellationToken);
}
/// <summary>
/// Executes the specified query and returns the first element.
/// </summary>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
public virtual Task<TResult> GetAsync<TResult>(
[NotNull] Func<IQueryable<TApplication>, IQueryable<TResult>> query, CancellationToken cancellationToken = default)
{
return GetAsync((applications, state) => state(applications), query, cancellationToken);
}
/// <summary>
/// Executes the specified query and returns the first element.
/// </summary>
/// <typeparam name="TState">The state type.</typeparam>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
public virtual Task<TResult> GetAsync<TState, TResult>(
[NotNull] Func<IQueryable<TApplication>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken = default)
{
if (query == null)
{
throw new ArgumentNullException(nameof(query));
}
return Store.GetAsync(query, state, cancellationToken);
}
/// <summary>
/// Retrieves the client identifier associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client identifier associated with the application.
/// </returns>
public virtual ValueTask<string> GetClientIdAsync([NotNull] TApplication application, CancellationToken cancellationToken = default)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return Store.GetClientIdAsync(application, cancellationToken);
}
/// <summary>
/// Retrieves the client type associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client type of the application (by default, "public").
/// </returns>
public virtual async ValueTask<string> GetClientTypeAsync(
[NotNull] TApplication application, CancellationToken cancellationToken = default)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
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;
}
/// <summary>
/// Retrieves the consent type associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the consent type of the application (by default, "explicit").
/// </returns>
public virtual async ValueTask<string> GetConsentTypeAsync([NotNull] TApplication application, CancellationToken cancellationToken = default)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
var type = await Store.GetConsentTypeAsync(application, cancellationToken);
if (string.IsNullOrEmpty(type))
{
return OpenIddictConstants.ConsentTypes.Explicit;
}
return type;
}
/// <summary>
/// Retrieves the display name associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the display name associated with the application.
/// </returns>
public virtual ValueTask<string> GetDisplayNameAsync([NotNull] TApplication application, CancellationToken cancellationToken = default)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return Store.GetDisplayNameAsync(application, cancellationToken);
}
/// <summary>
/// Retrieves the unique identifier associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the unique identifier associated with the application.
/// </returns>
public virtual ValueTask<string> GetIdAsync([NotNull] TApplication application, CancellationToken cancellationToken = default)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return Store.GetIdAsync(application, cancellationToken);
}
/// <summary>
/// Retrieves the permissions associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the permissions associated with the application.
/// </returns>
public virtual ValueTask<ImmutableArray<string>> GetPermissionsAsync(
[NotNull] TApplication application, CancellationToken cancellationToken = default)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return Store.GetPermissionsAsync(application, cancellationToken);
}
/// <summary>
/// Retrieves the logout callback addresses associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the post_logout_redirect_uri associated with the application.
/// </returns>
public virtual ValueTask<ImmutableArray<string>> GetPostLogoutRedirectUrisAsync(
[NotNull] TApplication application, CancellationToken cancellationToken = default)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return Store.GetPostLogoutRedirectUrisAsync(application, cancellationToken);
}
/// <summary>
/// Retrieves the callback addresses associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the redirect_uri associated with the application.
/// </returns>
public virtual ValueTask<ImmutableArray<string>> GetRedirectUrisAsync(
[NotNull] TApplication application, CancellationToken cancellationToken = default)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return Store.GetRedirectUrisAsync(application, cancellationToken);
}
/// <summary>
/// Determines whether the specified permission has been granted to the application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="permission">The permission.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the application has been granted the specified permission, <c>false</c> otherwise.</returns>
public virtual async Task<bool> 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));
}
var permissions = await Store.GetPermissionsAsync(application, cancellationToken);
bool HasPermission(string name)
{
if (permissions.IsEmpty)
{
return false;
}
return permissions.Contains(name);
}
bool HasEndpointPermission(string name)
{
// If the requested permission is an "endpoint" permission, return true if it has been
// explicitly granted OR if no other endpoint permission has been explicitly registered.
if (permissions.IsEmpty || HasPermission(name))
{
return true;
}
if (permissions.Any(element => element.StartsWith(OpenIddictConstants.Permissions.Prefixes.Endpoint)))
{
return false;
}
return true;
}
bool HasGrantTypePermission(string name)
{
// If the requested permission is a "grant_type" permission, return true if it has been
// explicitly granted OR if the application is allowed to use the corresponding endpoint
// AND no other grant type permission has been explicitly registered.
if (permissions.IsEmpty || HasPermission(name))
{
return true;
}
if (permissions.Any(element => element.StartsWith(OpenIddictConstants.Permissions.Prefixes.GrantType)))
{
return false;
}
switch (permission)
{
case OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode:
return HasEndpointPermission(OpenIddictConstants.Permissions.Endpoints.Authorization) &&
HasEndpointPermission(OpenIddictConstants.Permissions.Endpoints.Token);
case OpenIddictConstants.Permissions.GrantTypes.Implicit:
return HasEndpointPermission(OpenIddictConstants.Permissions.Endpoints.Authorization);
default:
case OpenIddictConstants.Permissions.GrantTypes.ClientCredentials:
case OpenIddictConstants.Permissions.GrantTypes.Password:
case OpenIddictConstants.Permissions.GrantTypes.RefreshToken:
return HasEndpointPermission(OpenIddictConstants.Permissions.Endpoints.Token);
}
}
bool HasScopePermission(string name)
{
// If the requested permission is a "scope" permission, return true if it has been
// explicitly granted OR if the application is allowed to use the authorization or
// token endpoints AND no other scope permission has been explicitly registered.
if (permissions.IsEmpty || HasPermission(name))
{
return true;
}
if (permissions.Any(element => element.StartsWith(OpenIddictConstants.Permissions.Prefixes.Scope)))
{
return false;
}
return HasEndpointPermission(OpenIddictConstants.Permissions.Endpoints.Authorization) ||
HasEndpointPermission(OpenIddictConstants.Permissions.Endpoints.Token);
}
if (permission.StartsWith(OpenIddictConstants.Permissions.Prefixes.Endpoint))
{
return HasEndpointPermission(permission);
}
if (permission.StartsWith(OpenIddictConstants.Permissions.Prefixes.GrantType))
{
return HasGrantTypePermission(permission);
}
if (permission.StartsWith(OpenIddictConstants.Permissions.Prefixes.Scope))
{
return HasScopePermission(permission);
}
return HasPermission(permission);
}
/// <summary>
/// Determines whether an application is a confidential client.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the application is a confidential client, <c>false</c> otherwise.</returns>
public async Task<bool> 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);
}
/// <summary>
/// Determines whether an application is a hybrid client.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the application is a hybrid client, <c>false</c> otherwise.</returns>
public async Task<bool> 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);
}
/// <summary>
/// Determines whether an application is a public client.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the application is a public client, <c>false</c> otherwise.</returns>
public async Task<bool> 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);
}
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <param name="count">The number of results to return.</param>
/// <param name="offset">The number of results to skip.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query.
/// </returns>
public virtual Task<ImmutableArray<TApplication>> ListAsync(
[CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken = default)
{
return Store.ListAsync(count, offset, cancellationToken);
}
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query.
/// </returns>
public virtual Task<ImmutableArray<TResult>> ListAsync<TResult>(
[NotNull] Func<IQueryable<TApplication>, IQueryable<TResult>> query, CancellationToken cancellationToken = default)
{
return ListAsync((applications, state) => state(applications), query, cancellationToken);
}
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <typeparam name="TState">The state type.</typeparam>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query.
/// </returns>
public virtual Task<ImmutableArray<TResult>> ListAsync<TState, TResult>(
[NotNull] Func<IQueryable<TApplication>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken = default)
{
if (query == null)
{
throw new ArgumentNullException(nameof(query));
}
return Store.ListAsync(query, state, cancellationToken);
}
/// <summary>
/// Updates an existing application.
/// </summary>
/// <param name="application">The application to update.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public 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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="application">The application to update.</param>
/// <param name="secret">The client secret associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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);
}
/// <summary>
/// Updates an existing application.
/// </summary>
/// <param name="application">The application to update.</param>
/// <param name="operation">The delegate used to update the application based on the given descriptor.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual async Task UpdateAsync([NotNull] TApplication application,
[NotNull] Func<OpenIddictApplicationDescriptor, Task> operation, CancellationToken cancellationToken = default)
{
if (operation == null)
{
throw new ArgumentNullException(nameof(operation));
}
// Store the original client secret for later comparison.
var secret = await Store.GetClientSecretAsync(application, cancellationToken);
var descriptor = new OpenIddictApplicationDescriptor
{
ClientId = await Store.GetClientIdAsync(application, cancellationToken),
ClientSecret = secret,
ConsentType = await Store.GetConsentTypeAsync(application, cancellationToken),
DisplayName = await Store.GetDisplayNameAsync(application, cancellationToken),
Type = await Store.GetClientTypeAsync(application, cancellationToken)
};
descriptor.Permissions.UnionWith(await Store.GetPermissionsAsync(application, cancellationToken));
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);
}
await operation(descriptor);
await PopulateAsync(application, descriptor, cancellationToken);
// If the client secret was updated, re-obfuscate it before persisting the changes.
var comparand = await Store.GetClientSecretAsync(application, cancellationToken);
if (!string.Equals(secret, comparand, StringComparison.Ordinal))
{
await UpdateAsync(application, comparand, cancellationToken);
return;
}
await UpdateAsync(application, cancellationToken);
}
/// <summary>
/// Validates the application to ensure it's in a consistent state.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the validation error encountered when validating the application.
/// </returns>
public virtual async Task<ImmutableArray<ValidationResult>> ValidateAsync(
[NotNull] TApplication application, CancellationToken cancellationToken = default)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
var results = ImmutableArray.CreateBuilder<ValidationResult>();
var identifier = await Store.GetClientIdAsync(application, cancellationToken);
if (string.IsNullOrEmpty(identifier))
{
results.Add(new ValidationResult("The client identifier cannot be null or empty."));
}
else
{
// Ensure the client_id is not already used for a different application.
var other = await Store.FindByClientIdAsync(identifier, cancellationToken);
if (other != null && !string.Equals(
await Store.GetIdAsync(other, cancellationToken),
await Store.GetIdAsync(application, cancellationToken), StringComparison.Ordinal))
{
results.Add(new ValidationResult("An application with the same client identifier already exists."));
}
}
var type = await Store.GetClientTypeAsync(application, cancellationToken);
if (string.IsNullOrEmpty(type))
{
results.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))
{
results.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))
{
results.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))
{
results.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<string>()
.AddRange(await Store.GetPostLogoutRedirectUrisAsync(application, cancellationToken))
.AddRange(await Store.GetRedirectUrisAsync(application, cancellationToken)))
{
// Ensure the address is not null or empty.
if (string.IsNullOrEmpty(address))
{
results.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())
{
results.Add(new ValidationResult("Callback URLs must be valid absolute URLs."));
break;
}
// Ensure the address doesn't contain a fragment.
if (!string.IsNullOrEmpty(uri.Fragment))
{
results.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)))
{
results.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)))
{
results.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)))
{
results.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)))
{
results.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)))
{
results.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)))
{
results.Add(new ValidationResult(
"The refresh token flow permission requires adding the token endpoint permission."));
}
return results.ToImmutable();
}
/// <summary>
/// Validates the client_secret associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="secret">The secret that should be compared to the client_secret stored in the database.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="Task"/> that can be used to monitor the asynchronous operation.</returns>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns a boolean indicating whether the client secret was valid.
/// </returns>
public virtual async Task<bool> 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;
}
/// <summary>
/// Validates the specified post_logout_redirect_uri.
/// </summary>
/// <param name="address">The address that should be compared to the post_logout_redirect_uri stored in the database.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns a boolean indicating whether the post_logout_redirect_uri was valid.
/// </returns>
public virtual async Task<bool> ValidatePostLogoutRedirectUriAsync(
[NotNull] string address, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(address))
{
throw new ArgumentException("The address cannot be null or empty.", nameof(address));
}
// Warning: SQL engines like Microsoft SQL Server are known to use case-insensitive lookups by default.
// To ensure a case-sensitive comparison is used, string.Equals(Ordinal) is manually called here.
foreach (var application in await Store.FindByPostLogoutRedirectUriAsync(address, cancellationToken))
{
// If the application is not allowed to use the logout endpoint, ignore it and keep iterating.
if (!await HasPermissionAsync(application, OpenIddictConstants.Permissions.Endpoints.Logout, cancellationToken))
{
continue;
}
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))
{
return true;
}
}
}
Logger.LogWarning("Client validation failed because '{PostLogoutRedirectUri}' " +
"was not a valid post_logout_redirect_uri.", address);
return false;
}
/// <summary>
/// Validates the redirect_uri to ensure it's associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="address">The address that should be compared to one of the redirect_uri stored in the database.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns a boolean indicating whether the redirect_uri was valid.
/// </returns>
public virtual async Task<bool> 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;
}
/// <summary>
/// Populates the application using the specified descriptor.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="descriptor">The descriptor.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
protected 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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="secret">The client secret.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
protected virtual Task<string> 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));
}
/// <summary>
/// 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.
/// </summary>
/// <param name="secret">The client secret to compare to the value stored in the database.</param>
/// <param name="comparand">The value stored in the database, which is usually a hashed representation of the secret.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns a boolean indicating whether the specified value was valid.
/// </returns>
protected virtual Task<bool> 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(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);
}
}
}
}