/* * 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.Linq; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.Extensions.Logging; namespace OpenIddict.Core { /// /// Provides methods allowing to manage the tokens stored in the store. /// /// The type of the Token entity. public class OpenIddictTokenManager where TToken : class { public OpenIddictTokenManager( [NotNull] IOpenIddictTokenStore store, [NotNull] ILogger> logger) { Logger = logger; Store = store; } /// /// Gets the logger associated with the current manager. /// protected ILogger Logger { get; } /// /// Gets the store associated with the current manager. /// protected IOpenIddictTokenStore Store { get; } /// /// Determines the number of tokens 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 tokens in the database. /// public virtual Task CountAsync(CancellationToken cancellationToken = default) { return Store.CountAsync(cancellationToken); } /// /// Determines the number of tokens 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 tokens 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 token. /// /// The token. /// 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] TToken token, CancellationToken cancellationToken = default) { if (token == null) { throw new ArgumentNullException(nameof(token)); } await ValidateAsync(token, cancellationToken); try { await Store.CreateAsync(token, cancellationToken); } catch (Exception exception) { Logger.LogError(exception, "An exception occurred while trying to create a new token."); throw; } } /// /// Creates a new token based on the specified descriptor. /// /// The token descriptor. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, whose result returns the token. /// public virtual async Task CreateAsync( [NotNull] OpenIddictTokenDescriptor descriptor, CancellationToken cancellationToken = default) { if (descriptor == null) { throw new ArgumentNullException(nameof(descriptor)); } var token = await Store.InstantiateAsync(cancellationToken); if (token == null) { throw new InvalidOperationException("An error occurred while trying to create a new token"); } await PopulateAsync(token, descriptor, cancellationToken); await CreateAsync(token, cancellationToken); return token; } /// /// Removes an existing token. /// /// The token to delete. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation. /// public virtual async Task DeleteAsync([NotNull] TToken token, CancellationToken cancellationToken = default) { if (token == null) { throw new ArgumentNullException(nameof(token)); } try { await Store.DeleteAsync(token, cancellationToken); } catch (Exception exception) { Logger.LogError(exception, "An exception occurred while trying to delete an existing token."); throw; } } /// /// Extends the specified token by replacing its expiration date. /// /// The token. /// The date on which the token will no longer be considered valid. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation. /// public virtual async Task ExtendAsync([NotNull] TToken token, [CanBeNull] DateTimeOffset? date, CancellationToken cancellationToken = default) { if (token == null) { throw new ArgumentNullException(nameof(token)); } await Store.SetExpirationDateAsync(token, date, cancellationToken); await UpdateAsync(token, cancellationToken); } /// /// Retrieves the list of tokens corresponding to the specified application identifier. /// /// The application identifier associated with the tokens. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, /// whose result returns the tokens corresponding to the specified application. /// public virtual Task> FindByApplicationIdAsync( [NotNull] string identifier, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(identifier)) { throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); } return Store.FindByApplicationIdAsync(identifier, cancellationToken); } /// /// Retrieves the list of tokens corresponding to the specified authorization identifier. /// /// The authorization identifier associated with the tokens. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, /// whose result returns the tokens corresponding to the specified authorization. /// public virtual Task> FindByAuthorizationIdAsync( [NotNull] string identifier, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(identifier)) { throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); } return Store.FindByAuthorizationIdAsync(identifier, cancellationToken); } /// /// Retrieves the list of tokens corresponding to the specified reference identifier. /// Note: the reference identifier may be hashed or encrypted for security reasons. /// /// The reference identifier associated with the tokens. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, /// whose result returns the tokens corresponding to the specified reference identifier. /// public virtual async Task FindByReferenceIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(identifier)) { throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); } identifier = await ObfuscateReferenceIdAsync(identifier, cancellationToken); return await Store.FindByReferenceIdAsync(identifier, cancellationToken); } /// /// Retrieves a token using its unique identifier. /// /// The unique identifier associated with the token. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, /// whose result returns the token corresponding to the unique 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 the list of tokens corresponding to the specified subject. /// /// The subject associated with the tokens. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, /// whose result returns the tokens corresponding to the specified subject. /// public virtual Task> FindBySubjectAsync( [NotNull] string subject, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(subject)) { throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); } return Store.FindBySubjectAsync(subject, cancellationToken); } /// /// Retrieves the optional application identifier associated with a token. /// /// The token. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, /// whose result returns the application identifier associated with the token. /// public virtual Task GetApplicationIdAsync([NotNull] TToken token, CancellationToken cancellationToken = default) { if (token == null) { throw new ArgumentNullException(nameof(token)); } return Store.GetApplicationIdAsync(token, cancellationToken); } /// /// 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((tokens, state) => state(tokens), 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 optional authorization identifier associated with a token. /// /// The token. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, /// whose result returns the authorization identifier associated with the token. /// public virtual Task GetAuthorizationIdAsync([NotNull] TToken token, CancellationToken cancellationToken = default) { if (token == null) { throw new ArgumentNullException(nameof(token)); } return Store.GetAuthorizationIdAsync(token, cancellationToken); } /// /// Retrieves the creation date associated with a token. /// /// The token. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, /// whose result returns the creation date associated with the specified token. /// public virtual Task GetCreationDateAsync([NotNull] TToken token, CancellationToken cancellationToken = default) { if (token == null) { throw new ArgumentNullException(nameof(token)); } return Store.GetCreationDateAsync(token, cancellationToken); } /// /// Retrieves the expiration date associated with a token. /// /// The token. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, /// whose result returns the expiration date associated with the specified token. /// public virtual Task GetExpirationDateAsync([NotNull] TToken token, CancellationToken cancellationToken = default) { if (token == null) { throw new ArgumentNullException(nameof(token)); } return Store.GetExpirationDateAsync(token, cancellationToken); } /// /// Retrieves the unique identifier associated with a token. /// /// The token. /// 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 token. /// public virtual Task GetIdAsync([NotNull] TToken token, CancellationToken cancellationToken = default) { if (token == null) { throw new ArgumentNullException(nameof(token)); } return Store.GetIdAsync(token, cancellationToken); } /// /// Retrieves the payload associated with a token. /// /// The token. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, /// whose result returns the payload associated with the specified token. /// public virtual Task GetPayloadAsync([NotNull] TToken token, CancellationToken cancellationToken = default) { if (token == null) { throw new ArgumentNullException(nameof(token)); } return Store.GetPayloadAsync(token, cancellationToken); } /// /// Retrieves the reference identifier associated with a token. /// Note: depending on the manager used to create the token, /// the reference identifier may be hashed for security reasons. /// /// The token. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, /// whose result returns the reference identifier associated with the specified token. /// public virtual Task GetReferenceIdAsync([NotNull] TToken token, CancellationToken cancellationToken = default) { if (token == null) { throw new ArgumentNullException(nameof(token)); } return Store.GetReferenceIdAsync(token, cancellationToken); } /// /// Retrieves the status associated with a token. /// /// The token. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, /// whose result returns the status associated with the specified token. /// public virtual Task GetStatusAsync([NotNull] TToken token, CancellationToken cancellationToken = default) { if (token == null) { throw new ArgumentNullException(nameof(token)); } return Store.GetStatusAsync(token, cancellationToken); } /// /// Determines whether a given token has already been redemeed. /// /// The token. /// The that can be used to abort the operation. /// true if the token has already been redemeed, false otherwise. public virtual async Task IsRedeemedAsync([NotNull] TToken token, CancellationToken cancellationToken = default) { if (token == null) { throw new ArgumentNullException(nameof(token)); } var status = await Store.GetStatusAsync(token, cancellationToken); if (string.IsNullOrEmpty(status)) { return false; } return string.Equals(status, OpenIddictConstants.Statuses.Redeemed, StringComparison.OrdinalIgnoreCase); } /// /// Determines whether a given token has been revoked. /// /// The token. /// The that can be used to abort the operation. /// true if the token has been revoked, false otherwise. public virtual async Task IsRevokedAsync([NotNull] TToken token, CancellationToken cancellationToken = default) { if (token == null) { throw new ArgumentNullException(nameof(token)); } var status = await Store.GetStatusAsync(token, cancellationToken); if (string.IsNullOrEmpty(status)) { return false; } return string.Equals(status, OpenIddictConstants.Statuses.Revoked, StringComparison.OrdinalIgnoreCase); } /// /// Determines whether a given token is valid. /// /// The token. /// The that can be used to abort the operation. /// true if the token is valid, false otherwise. public virtual async Task IsValidAsync([NotNull] TToken token, CancellationToken cancellationToken = default) { if (token == null) { throw new ArgumentNullException(nameof(token)); } var status = await Store.GetStatusAsync(token, cancellationToken); if (string.IsNullOrEmpty(status)) { return false; } return string.Equals(status, OpenIddictConstants.Statuses.Valid, 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, [CanBeNull] int? offset, 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((tokens, state) => state(tokens), 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); } /// /// Lists the tokens that are marked as expired or invalid /// and that can be safely removed from the database. /// /// 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> ListInvalidAsync( [CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken = default) { return Store.ListInvalidAsync(count, offset, cancellationToken); } /// /// Obfuscates the specified reference identifier so it can be safely stored in a database. /// By default, this method returns a simple hashed representation computed using SHA256. /// /// The client identifier. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation. /// public virtual Task ObfuscateReferenceIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(identifier)) { throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); } using (var algorithm = SHA256.Create()) { // Compute the digest of the generated identifier and use it as the hashed identifier of the reference token. // Doing that prevents token identifiers stolen from the database from being used as valid reference tokens. return Task.FromResult(Convert.ToBase64String(algorithm.ComputeHash(Encoding.UTF8.GetBytes(identifier)))); } } /// /// Redeems a token. /// /// The token to redeem. /// The that can be used to abort the operation. /// A that can be used to monitor the asynchronous operation. public virtual async Task RedeemAsync([NotNull] TToken token, CancellationToken cancellationToken = default) { if (token == null) { throw new ArgumentNullException(nameof(token)); } var status = await Store.GetStatusAsync(token, cancellationToken); if (!string.Equals(status, OpenIddictConstants.Statuses.Redeemed, StringComparison.OrdinalIgnoreCase)) { await Store.SetStatusAsync(token, OpenIddictConstants.Statuses.Redeemed, cancellationToken); await UpdateAsync(token, cancellationToken); } } /// /// Revokes a token. /// /// The token to revoke. /// The that can be used to abort the operation. /// A that can be used to monitor the asynchronous operation. public virtual async Task RevokeAsync([NotNull] TToken token, CancellationToken cancellationToken = default) { if (token == null) { throw new ArgumentNullException(nameof(token)); } var status = await Store.GetStatusAsync(token, cancellationToken); if (!string.Equals(status, OpenIddictConstants.Statuses.Revoked, StringComparison.OrdinalIgnoreCase)) { await Store.SetStatusAsync(token, OpenIddictConstants.Statuses.Revoked, cancellationToken); await UpdateAsync(token, cancellationToken); } } /// /// Sets the application identifier associated with a token. /// /// The token. /// The unique identifier associated with the client application. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation. /// public virtual async Task SetApplicationIdAsync([NotNull] TToken token, [CanBeNull] string identifier, CancellationToken cancellationToken = default) { if (token == null) { throw new ArgumentNullException(nameof(token)); } await Store.SetApplicationIdAsync(token, identifier, cancellationToken); await UpdateAsync(token, cancellationToken); } /// /// Sets the authorization identifier associated with a token. /// /// The token. /// The unique identifier associated with the authorization. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation. /// public virtual async Task SetAuthorizationIdAsync([NotNull] TToken token, [CanBeNull] string identifier, CancellationToken cancellationToken = default) { if (token == null) { throw new ArgumentNullException(nameof(token)); } await Store.SetAuthorizationIdAsync(token, identifier, cancellationToken); await UpdateAsync(token, cancellationToken); } /// /// Updates an existing token. /// /// The token 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] TToken token, CancellationToken cancellationToken = default) { if (token == null) { throw new ArgumentNullException(nameof(token)); } try { await Store.UpdateAsync(token, cancellationToken); } catch (Exception exception) { Logger.LogError(exception, "An exception occurred while trying to update an existing token."); throw; } } /// /// Updates an existing token. /// /// The token to update. /// The delegate used to update the token based on the given descriptor. /// 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] TToken token, [NotNull] Func operation, CancellationToken cancellationToken = default) { if (operation == null) { throw new ArgumentNullException(nameof(operation)); } var descriptor = new OpenIddictTokenDescriptor { ApplicationId = await Store.GetApplicationIdAsync(token, cancellationToken), AuthorizationId = await Store.GetAuthorizationIdAsync(token, cancellationToken), CreationDate = await Store.GetCreationDateAsync(token, cancellationToken), ExpirationDate = await Store.GetExpirationDateAsync(token, cancellationToken), Payload = await Store.GetPayloadAsync(token, cancellationToken), ReferenceId = await Store.GetReferenceIdAsync(token, cancellationToken), Status = await Store.GetStatusAsync(token, cancellationToken), Subject = await Store.GetSubjectAsync(token, cancellationToken), Type = await Store.GetTokenTypeAsync(token, cancellationToken) }; await operation(descriptor); await PopulateAsync(token, descriptor, cancellationToken); await UpdateAsync(token, cancellationToken); } /// /// Populates the token using the specified descriptor. /// /// The token. /// The descriptor. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation. /// protected virtual async Task PopulateAsync([NotNull] TToken token, [NotNull] OpenIddictTokenDescriptor descriptor, CancellationToken cancellationToken = default) { if (token == null) { throw new ArgumentNullException(nameof(token)); } if (descriptor == null) { throw new ArgumentNullException(nameof(descriptor)); } await Store.SetApplicationIdAsync(token, descriptor.ApplicationId, cancellationToken); await Store.SetAuthorizationIdAsync(token, descriptor.AuthorizationId, cancellationToken); await Store.SetCreationDateAsync(token, descriptor.CreationDate, cancellationToken); await Store.SetExpirationDateAsync(token, descriptor.ExpirationDate, cancellationToken); await Store.SetPayloadAsync(token, descriptor.Payload, cancellationToken); await Store.SetReferenceIdAsync(token, descriptor.ReferenceId, cancellationToken); await Store.SetStatusAsync(token, descriptor.Status, cancellationToken); await Store.SetSubjectAsync(token, descriptor.Subject, cancellationToken); await Store.SetTokenTypeAsync(token, descriptor.Type, cancellationToken); } /// /// Validates the token to ensure it's in a consistent state. /// /// The token. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation. /// protected virtual async Task ValidateAsync([NotNull] TToken token, CancellationToken cancellationToken = default) { if (token == null) { throw new ArgumentNullException(nameof(token)); } var type = await Store.GetTokenTypeAsync(token, cancellationToken); if (string.IsNullOrEmpty(type)) { throw new ArgumentException("The token type cannot be null or empty.", nameof(token)); } if (!string.Equals(type, OpenIddictConstants.TokenTypes.AccessToken, StringComparison.OrdinalIgnoreCase) && !string.Equals(type, OpenIddictConstants.TokenTypes.AuthorizationCode, StringComparison.OrdinalIgnoreCase) && !string.Equals(type, OpenIddictConstants.TokenTypes.RefreshToken, StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException("The specified token type is not supported by the default token manager.", nameof(token)); } if (string.IsNullOrEmpty(await Store.GetStatusAsync(token, cancellationToken))) { throw new ArgumentException("The status cannot be null or empty.", nameof(token)); } if (string.IsNullOrEmpty(await Store.GetSubjectAsync(token, cancellationToken))) { throw new ArgumentException("The subject cannot be null or empty.", nameof(token)); } } } }