diff --git a/src/OpenIddict.Core/Descriptors/OpenIddictAuthorizationDescriptor.cs b/src/OpenIddict.Core/Descriptors/OpenIddictAuthorizationDescriptor.cs new file mode 100644 index 00000000..927661c8 --- /dev/null +++ b/src/OpenIddict.Core/Descriptors/OpenIddictAuthorizationDescriptor.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace OpenIddict.Core +{ + /// + /// Represents an OpenIddict authorization descriptor. + /// + public class OpenIddictAuthorizationDescriptor + { + /// + /// Gets or sets the application identifier associated with the authorization. + /// + public string ApplicationId { get; set; } + + /// + /// Gets or sets the scopes associated with the authorization. + /// + public IEnumerable Scopes { get; set; } + + /// + /// Gets or sets the subject associated with the authorization. + /// + public string Subject { get; set; } + } +} diff --git a/src/OpenIddict.Core/Descriptors/OpenIddictTokenDescriptor.cs b/src/OpenIddict.Core/Descriptors/OpenIddictTokenDescriptor.cs new file mode 100644 index 00000000..724935f5 --- /dev/null +++ b/src/OpenIddict.Core/Descriptors/OpenIddictTokenDescriptor.cs @@ -0,0 +1,50 @@ +using System; + +namespace OpenIddict.Core +{ + /// + /// Represents an OpenIddict token descriptor. + /// + public class OpenIddictTokenDescriptor + { + /// + /// Gets or sets the application identifier associated with the token. + /// + public string ApplicationId { get; set; } + + /// + /// Gets or sets the authorization identifier associated with the token. + /// + public string AuthorizationId { get; set; } + + /// + /// Gets or sets the encrypted payload associated with the token. + /// + public string Ciphertext { get; set; } + + /// + /// Gets or sets the creation date associated with the token. + /// + public DateTimeOffset? CreationDate { get; set; } + + /// + /// Gets or sets the expiration date associated with the token. + /// + public DateTimeOffset? ExpirationDate { get; set; } + + /// + /// Gets or sets the cryptographic hash associated with the token. + /// + public string Hash { get; set; } + + /// + /// Gets or sets the subject associated with the token. + /// + public string Subject { get; set; } + + /// + /// Gets or sets the token type. + /// + public string Type { get; set; } + } +} diff --git a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs index 2910e60e..cd249bb0 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs @@ -65,33 +65,19 @@ namespace OpenIddict.Core /// /// Creates a new authorization. /// - /// The subject associated with the authorization. - /// The client associated with the authorization. - /// The scopes associated with the authorization. + /// The authorization descriptor. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, whose result returns the authorization. /// - public virtual Task CreateAsync( - [NotNull] string subject, [NotNull] string client, - [NotNull] IEnumerable scopes, CancellationToken cancellationToken) + public virtual Task CreateAsync([NotNull] OpenIddictAuthorizationDescriptor descriptor, CancellationToken cancellationToken) { - if (scopes == null) - { - throw new ArgumentNullException(nameof(scopes)); - } - - if (string.IsNullOrEmpty(subject)) - { - throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); - } - - if (string.IsNullOrEmpty(client)) + if (descriptor == null) { - throw new ArgumentException("The client cannot be null or empty.", nameof(subject)); + throw new ArgumentNullException(nameof(descriptor)); } - return Store.CreateAsync(subject, client, scopes, cancellationToken); + return Store.CreateAsync(descriptor, cancellationToken); } /// diff --git a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs index f1612b2a..6d32f488 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs @@ -66,65 +66,39 @@ namespace OpenIddict.Core /// /// Creates a new token, which is associated with a particular subject. /// - /// The token type. - /// The subject associated with the token. - /// The date on which the token will start to be considered valid. - /// The date on which the token will no longer be considered valid. + /// 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 Task CreateAsync( - [NotNull] string type, [NotNull] string subject, - [CanBeNull] DateTimeOffset? start, - [CanBeNull] DateTimeOffset? end, CancellationToken cancellationToken) + public virtual Task CreateAsync([NotNull] OpenIddictTokenDescriptor descriptor, CancellationToken cancellationToken) { - if (string.IsNullOrEmpty(type)) + if (descriptor == null) { - throw new ArgumentException("The token type cannot be null or empty.", nameof(type)); + throw new ArgumentNullException(nameof(descriptor)); } - if (string.IsNullOrEmpty(subject)) - { - throw new ArgumentException("The subject cannot be null or empty."); - } - - return Store.CreateAsync(type, subject, start, end, cancellationToken); + return Store.CreateAsync(descriptor, cancellationToken); } /// - /// Creates a new reference token, which is associated with a particular subject. + /// Extends the specified token by replacing its expiration date. /// - /// The token type. - /// The subject associated with the token. - /// The hash of the crypto-secure random identifier associated with the token. - /// The ciphertext associated with the token. - /// The date on which the token will start to be considered valid. - /// The date on which the token will no longer be considered valid. + /// 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, whose result returns the token. + /// A that can be used to monitor the asynchronous operation. /// - public virtual Task CreateAsync( - [NotNull] string type, [NotNull] string subject, [NotNull] string hash, [NotNull] string ciphertext, - [CanBeNull] DateTimeOffset? start, [CanBeNull] DateTimeOffset? end, CancellationToken cancellationToken) + public virtual async Task ExtendAsync([NotNull] TToken token, [CanBeNull] DateTimeOffset? date, CancellationToken cancellationToken) { - if (string.IsNullOrEmpty(type)) - { - throw new ArgumentException("The token type cannot be null or empty.", nameof(type)); - } - - if (string.IsNullOrEmpty(subject)) - { - throw new ArgumentException("The subject cannot be null or empty."); - } - - if (string.IsNullOrEmpty(ciphertext)) + if (token == null) { - throw new ArgumentException("The ciphertext cannot be null or empty.", nameof(ciphertext)); + throw new ArgumentNullException(nameof(token)); } - return Store.CreateAsync(type, subject, hash, ciphertext, start, end, cancellationToken); + await Store.SetExpirationDateAsync(token, date, cancellationToken); + await UpdateAsync(token, cancellationToken); } /// @@ -221,6 +195,44 @@ namespace OpenIddict.Core return Store.GetCiphertextAsync(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) + { + 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) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + return Store.GetExpirationDateAsync(token, cancellationToken); + } + /// /// Retrieves the hashed identifier associated with a token. /// diff --git a/src/OpenIddict.Core/OpenIddictConstants.cs b/src/OpenIddict.Core/OpenIddictConstants.cs index 97b901b8..65ad9ba7 100644 --- a/src/OpenIddict.Core/OpenIddictConstants.cs +++ b/src/OpenIddict.Core/OpenIddictConstants.cs @@ -33,6 +33,7 @@ namespace OpenIddict.Core public static class Properties { public const string AuthorizationId = ".authorization_id"; + public const string TokenId = ".token_id"; } public static class Scopes diff --git a/src/OpenIddict.Core/Stores/IOpenIddictAuthorizationStore.cs b/src/OpenIddict.Core/Stores/IOpenIddictAuthorizationStore.cs index 269d6fd5..bb5395e2 100644 --- a/src/OpenIddict.Core/Stores/IOpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.Core/Stores/IOpenIddictAuthorizationStore.cs @@ -36,16 +36,12 @@ namespace OpenIddict.Core /// /// Creates a new authorization. /// - /// The subject associated with the authorization. - /// The client associated with the authorization. - /// The scopes associated with the authorization. + /// The authorization descriptor. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, whose result returns the authorization. /// - Task CreateAsync( - [NotNull] string subject, [NotNull] string client, - [NotNull] IEnumerable scopes, CancellationToken cancellationToken); + Task CreateAsync([NotNull] OpenIddictAuthorizationDescriptor descriptor, CancellationToken cancellationToken); /// /// Retrieves an authorization using its unique identifier. diff --git a/src/OpenIddict.Core/Stores/IOpenIddictTokenStore.cs b/src/OpenIddict.Core/Stores/IOpenIddictTokenStore.cs index 5e57edd7..67491003 100644 --- a/src/OpenIddict.Core/Stores/IOpenIddictTokenStore.cs +++ b/src/OpenIddict.Core/Stores/IOpenIddictTokenStore.cs @@ -36,35 +36,12 @@ namespace OpenIddict.Core /// /// Creates a new token, which is associated with a particular subject. /// - /// The token type. - /// The subject associated with the token. - /// The date on which the token will start to be considered valid. - /// The date on which the token will no longer be considered valid. + /// 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. /// - Task CreateAsync( - [NotNull] string type, [NotNull] string subject, - [CanBeNull] DateTimeOffset? start, - [CanBeNull] DateTimeOffset? end, CancellationToken cancellationToken); - - /// - /// Creates a new reference token, which is associated with a particular subject. - /// - /// The token type. - /// The subject associated with the token. - /// The hash of the crypto-secure random identifier associated with the token. - /// The ciphertext associated with the token. - /// The date on which the token will start to be considered valid. - /// 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, whose result returns the token. - /// - Task CreateAsync( - [NotNull] string type, [NotNull] string subject, [NotNull] string hash, [NotNull] string ciphertext, - [CanBeNull] DateTimeOffset? start, [CanBeNull] DateTimeOffset? end, CancellationToken cancellationToken); + Task CreateAsync([NotNull] OpenIddictTokenDescriptor descriptor, CancellationToken cancellationToken); /// /// Removes a token. @@ -140,6 +117,28 @@ namespace OpenIddict.Core /// Task GetCiphertextAsync([NotNull] TToken token, CancellationToken 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. + /// + Task GetCreationDateAsync([NotNull] TToken token, CancellationToken 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. + /// + Task GetExpirationDateAsync([NotNull] TToken token, CancellationToken cancellationToken); + /// /// Retrieves the hashed identifier associated with a token. /// @@ -217,6 +216,17 @@ namespace OpenIddict.Core /// Task SetClientAsync([NotNull] TToken token, [CanBeNull] string identifier, CancellationToken cancellationToken); + /// + /// Sets the expiration date associated with a token. + /// + /// 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. + /// + Task SetExpirationDateAsync([NotNull] TToken token, [CanBeNull] DateTimeOffset? date, CancellationToken cancellationToken); + /// /// Sets the status associated with a token. /// diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs index d6388e61..f6199345 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs @@ -113,28 +113,19 @@ namespace OpenIddict.EntityFrameworkCore /// /// Creates a new authorization. /// - /// The subject associated with the authorization. - /// The client associated with the authorization. - /// The scopes associated with the authorization. + /// The authorization descriptor. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, whose result returns the authorization. /// - public virtual async Task CreateAsync( - [NotNull] string subject, [NotNull] string client, - [NotNull] IEnumerable scopes, CancellationToken cancellationToken) + public virtual async Task CreateAsync([NotNull] OpenIddictAuthorizationDescriptor descriptor, CancellationToken cancellationToken) { - if (string.IsNullOrEmpty(subject)) - { - throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); - } - - if (string.IsNullOrEmpty(client)) + if (descriptor == null) { - throw new ArgumentException("The client cannot be null or empty.", nameof(subject)); + throw new ArgumentNullException(nameof(descriptor)); } - var key = ConvertIdentifierFromString(client); + var key = ConvertIdentifierFromString(descriptor.ApplicationId); var application = await Applications.SingleOrDefaultAsync(entity => entity.Id.Equals(key)); if (application == null) @@ -145,8 +136,8 @@ namespace OpenIddict.EntityFrameworkCore var authorization = new TAuthorization { Application = application, - Scope = string.Join(" ", scopes), - Subject = subject + Scope = string.Join(" ", descriptor.Scopes), + Subject = descriptor.Subject }; return await CreateAsync(authorization, cancellationToken); diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs index aac99c01..c7374c1e 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs @@ -117,78 +117,51 @@ namespace OpenIddict.EntityFrameworkCore /// /// Creates a new token, which is associated with a particular subject. /// - /// The token type. - /// The subject associated with the token. - /// The date on which the token will start to be considered valid. - /// The date on which the token will no longer be considered valid. + /// 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 Task CreateAsync( - [NotNull] string type, [NotNull] string subject, - [CanBeNull] DateTimeOffset? start, - [CanBeNull] DateTimeOffset? end, CancellationToken cancellationToken) + public virtual async Task CreateAsync([NotNull] OpenIddictTokenDescriptor descriptor, CancellationToken cancellationToken) { - if (string.IsNullOrEmpty(type)) + if (descriptor == null) { - throw new ArgumentException("The token type cannot be null or empty."); - } - - if (string.IsNullOrEmpty(subject)) - { - throw new ArgumentException("The subject cannot be null or empty."); + throw new ArgumentNullException(nameof(descriptor)); } var token = new TToken { - End = end, - Start = start, - Subject = subject, - Type = type + Ciphertext = descriptor.Ciphertext, + CreationDate = descriptor.CreationDate, + ExpirationDate = descriptor.ExpirationDate, + Hash = descriptor.Hash, + Subject = descriptor.Subject, + Type = descriptor.Type }; - return CreateAsync(token, cancellationToken); - } + // Bind the token to the specified client application. + var key = ConvertIdentifierFromString(descriptor.ApplicationId); - /// - /// Creates a new reference token, which is associated with a particular subject. - /// - /// The token type. - /// The subject associated with the token. - /// The hash of the crypto-secure random identifier associated with the token. - /// The ciphertext associated with the token. - /// The date on which the token will start to be considered valid. - /// 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, whose result returns the token. - /// - public virtual Task CreateAsync( - [NotNull] string type, [NotNull] string subject, [NotNull] string hash, [NotNull] string ciphertext, - [CanBeNull] DateTimeOffset? start, [CanBeNull] DateTimeOffset? end, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(type)) + var application = await Applications.SingleOrDefaultAsync(entity => entity.Id.Equals(key)); + if (application == null) { - throw new ArgumentException("The token type cannot be null or empty."); + throw new InvalidOperationException("The application associated with the token cannot be found."); } - if (string.IsNullOrEmpty(subject)) + token.Application = application; + + // Bind the token to the specified authorization. + key = ConvertIdentifierFromString(descriptor.AuthorizationId); + + var authorization = await Authorizations.SingleOrDefaultAsync(entity => entity.Id.Equals(key)); + if (authorization == null) { - throw new ArgumentException("The subject cannot be null or empty."); + throw new InvalidOperationException("The authorization associated with the token cannot be found."); } - var token = new TToken - { - Ciphertext = ciphertext, - End = end, - Hash = hash, - Start = start, - Subject = subject, - Type = type - }; + token.Authorization = authorization; - return CreateAsync(token, cancellationToken); + return await CreateAsync(token, cancellationToken); } /// @@ -316,6 +289,44 @@ namespace OpenIddict.EntityFrameworkCore return Task.FromResult(token.Ciphertext); } + /// + /// 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) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + return Task.FromResult(token.CreationDate); + } + + /// + /// 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) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + return Task.FromResult(token.ExpirationDate); + } + /// /// Retrieves the hashed identifier associated with a token. /// @@ -497,6 +508,23 @@ namespace OpenIddict.EntityFrameworkCore } } + /// + /// Sets the expiration date associated with a token. + /// + /// 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 Task SetExpirationDateAsync([NotNull] TToken token, + [CanBeNull] DateTimeOffset? date, CancellationToken cancellationToken) + { + token.ExpirationDate = date; + + return Task.CompletedTask; + } + /// /// Sets the status associated with a token. /// diff --git a/src/OpenIddict.Models/OpenIddictToken.cs b/src/OpenIddict.Models/OpenIddictToken.cs index f9926574..aeef2b6a 100644 --- a/src/OpenIddict.Models/OpenIddictToken.cs +++ b/src/OpenIddict.Models/OpenIddictToken.cs @@ -50,11 +50,17 @@ namespace OpenIddict.Models /// public virtual string Ciphertext { get; set; } + /// + /// Gets or sets the date on which the token + /// will start to be considered valid. + /// + public virtual DateTimeOffset? CreationDate { get; set; } + /// /// Gets or sets the date on which the token /// will no longer be considered valid. /// - public virtual DateTimeOffset? End { get; set; } + public virtual DateTimeOffset? ExpirationDate { get; set; } /// /// Gets or sets the hashed identifier associated @@ -69,12 +75,6 @@ namespace OpenIddict.Models /// public virtual TKey Id { get; set; } - /// - /// Gets or sets the date on which the token - /// will start to be considered valid. - /// - public virtual DateTimeOffset? Start { get; set; } - /// /// Gets or sets the status of the current token. /// diff --git a/src/OpenIddict/OpenIddictExtensions.cs b/src/OpenIddict/OpenIddictExtensions.cs index 91746777..24810e50 100644 --- a/src/OpenIddict/OpenIddictExtensions.cs +++ b/src/OpenIddict/OpenIddictExtensions.cs @@ -940,5 +940,23 @@ namespace Microsoft.AspNetCore.Builder return builder.Configure(options => options.UseReferenceTokens = true); } + + /// + /// Configures OpenIddict to use rolling refresh tokens. When this option is enabled, + /// a new refresh token is issued for each refresh token request and the previous one + /// is automatically revoked (when disabled, no new refresh token is issued and the + /// lifetime of the original refresh token is increased by updating the database entry). + /// + /// The services builder used by OpenIddict to register new services. + /// The . + public static OpenIddictBuilder UseRollingTokens([NotNull] this OpenIddictBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + return builder.Configure(options => options.UseRollingTokens = true); + } } } \ No newline at end of file diff --git a/src/OpenIddict/OpenIddictInitializer.cs b/src/OpenIddict/OpenIddictInitializer.cs index fe7e5744..16a52332 100644 --- a/src/OpenIddict/OpenIddictInitializer.cs +++ b/src/OpenIddict/OpenIddictInitializer.cs @@ -158,6 +158,16 @@ namespace OpenIddict "Reference tokens cannot be used when configuring JWT as the access token format."); } + if (options.UseRollingTokens && options.DisableTokenRevocation) + { + throw new InvalidOperationException("Rolling tokens cannot be used when disabling token expiration."); + } + + if (options.UseRollingTokens && !options.UseSlidingExpiration) + { + throw new InvalidOperationException("Rolling tokens cannot be used without enabling sliding expiration."); + } + if (options.AccessTokenHandler != null && options.SigningCredentials.Count == 0) { throw new InvalidOperationException( diff --git a/src/OpenIddict/OpenIddictOptions.cs b/src/OpenIddict/OpenIddictOptions.cs index f6b00f5e..6e35223f 100644 --- a/src/OpenIddict/OpenIddictOptions.cs +++ b/src/OpenIddict/OpenIddictOptions.cs @@ -87,5 +87,14 @@ namespace OpenIddict /// Note: this option cannot be used when configuring JWT as the access token format. /// public bool UseReferenceTokens { get; set; } + + /// + /// Gets or sets a boolean indicating whether rolling tokens should be used. + /// When disabled, no new token is issued and the refresh token lifetime is + /// dynamically managed by updating the token entry in the database. + /// When this option is enabled, a new refresh token is issued for each + /// refresh token request and the previous one is automatically revoked. + /// + public bool UseRollingTokens { get; set; } } } diff --git a/src/OpenIddict/OpenIddictProvider.Exchange.cs b/src/OpenIddict/OpenIddictProvider.Exchange.cs index 82b6a3fe..47d83703 100644 --- a/src/OpenIddict/OpenIddictProvider.Exchange.cs +++ b/src/OpenIddict/OpenIddictProvider.Exchange.cs @@ -214,6 +214,9 @@ namespace OpenIddict var identifier = context.Ticket.GetProperty(OpenIdConnectConstants.Properties.TokenId); Debug.Assert(!string.IsNullOrEmpty(identifier), "The authentication ticket should contain a ticket identifier."); + // Store the original authorization code/refresh token so it can be later retrieved. + context.Request.SetProperty(OpenIddictConstants.Properties.TokenId, identifier); + if (context.Request.IsAuthorizationCodeGrantType()) { // Retrieve the authorization code from the database and ensure it is still valid. @@ -259,7 +262,7 @@ namespace OpenIddict context.Reject( error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "The specified authorization code has already been redemeed."); + description: "The specified authorization code has already been redeemed."); return; } @@ -283,7 +286,29 @@ namespace OpenIddict { // Retrieve the token from the database and ensure it is still valid. var token = await Tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted); - if (token == null || !await Tokens.IsValidAsync(token, context.HttpContext.RequestAborted)) + if (token == null) + { + Logger.LogError("The token request was rejected because the refresh token was already redeemed."); + + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: "The specified refresh token is no longer valid."); + + return; + } + + else if (await Tokens.IsRedeemedAsync(token, context.HttpContext.RequestAborted)) + { + Logger.LogError("The token request was rejected because the refresh token was no longer valid."); + + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: "The specified refresh token has already been redeemed."); + + return; + } + + else if (!await Tokens.IsValidAsync(token, context.HttpContext.RequestAborted)) { Logger.LogError("The token request was rejected because the refresh token was no longer valid."); @@ -294,10 +319,10 @@ namespace OpenIddict return; } - // When sliding expiration is enabled, immediately + // When rolling tokens are enabled, immediately // redeem the refresh token to prevent future reuse. // See https://tools.ietf.org/html/rfc6749#section-6. - if (options.UseSlidingExpiration) + if (options.UseRollingTokens) { await Tokens.RedeemAsync(token, context.HttpContext.RequestAborted); } diff --git a/src/OpenIddict/OpenIddictProvider.Serialization.cs b/src/OpenIddict/OpenIddictProvider.Serialization.cs index 6133e488..739c6147 100644 --- a/src/OpenIddict/OpenIddictProvider.Serialization.cs +++ b/src/OpenIddict/OpenIddictProvider.Serialization.cs @@ -7,7 +7,6 @@ using System; using System.Diagnostics; using System.Security.Cryptography; -using System.Threading; using System.Threading.Tasks; using AspNet.Security.OpenIdConnect.Extensions; using AspNet.Security.OpenIdConnect.Primitives; @@ -117,6 +116,8 @@ namespace OpenIddict public override async Task SerializeAuthorizationCode([NotNull] SerializeAuthorizationCodeContext context) { + Debug.Assert(context.Request.IsAuthorizationRequest(), "The request should be an authorization request."); + var token = await CreateTokenAsync( OpenIdConnectConstants.TokenUsages.AuthorizationCode, context.Ticket, (OpenIddictOptions) context.Options, @@ -136,9 +137,36 @@ namespace OpenIddict public override async Task SerializeRefreshToken([NotNull] SerializeRefreshTokenContext context) { + var options = (OpenIddictOptions) context.Options; + + Debug.Assert(context.Request.IsTokenRequest(), "The request should be a token request."); + + // When rolling tokens are disabled, extend the expiration date associated with the + // existing token instead of returning a new refresh token with a new expiration date. + if (options.UseSlidingExpiration && !options.UseRollingTokens && context.Request.IsRefreshTokenGrantType()) + { + var identifier = context.Request.GetProperty(OpenIddictConstants.Properties.TokenId); + + var entry = await Tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted); + if (entry != null) + { + Logger.LogInformation("The expiration date of the '{Identifier}' token was automatically updated: {Date}.", + identifier, context.Ticket.Properties.ExpiresUtc); + + await Tokens.ExtendAsync(entry, context.Ticket.Properties.ExpiresUtc, context.HttpContext.RequestAborted); + + context.RefreshToken = null; + context.HandleSerialization(); + + return; + } + + // If the refresh token entry could not be + // found in the database, generate a new one. + } + var token = await CreateTokenAsync( - OpenIdConnectConstants.TokenUsages.RefreshToken, - context.Ticket, (OpenIddictOptions) context.Options, + OpenIdConnectConstants.TokenUsages.RefreshToken, context.Ticket, options, context.HttpContext, context.Request, context.DataFormat); // If a reference token was returned by CreateTokenAsync(), @@ -162,7 +190,10 @@ namespace OpenIddict Debug.Assert(!(options.DisableTokenRevocation && options.UseReferenceTokens), "Token revocation cannot be disabled when using reference tokens."); - Debug.Assert(!string.Equals(type, OpenIdConnectConstants.TokenUsages.IdToken, StringComparison.OrdinalIgnoreCase), + Debug.Assert(!(options.DisableTokenRevocation && options.UseRollingTokens), + "Token revocation cannot be disabled when using rolling tokens."); + + Debug.Assert(type != OpenIdConnectConstants.TokenUsages.IdToken, "Identity tokens shouldn't be stored in the database."); if (options.DisableTokenRevocation) @@ -170,28 +201,34 @@ namespace OpenIddict return null; } - // Resolve the subject from the authentication ticket. If it cannot be found, throw an exception. - var subject = ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.Subject); - if (string.IsNullOrEmpty(subject)) + var descriptor = new OpenIddictTokenDescriptor { - throw new InvalidOperationException("The subject associated with the authentication ticket cannot be retrieved."); - } + CreationDate = ticket.Properties.IssuedUtc, + ExpirationDate = ticket.Properties.ExpiresUtc, + Subject = ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.Subject), + Type = type + }; - TToken token; string result = null; + // When reference tokens are enabled or when the token is an authorization code or a + // refresh token, remove the unnecessary properties from the authentication ticket. + if (options.UseReferenceTokens || + (type == OpenIdConnectConstants.TokenUsages.AuthorizationCode || + type == OpenIdConnectConstants.TokenUsages.RefreshToken)) + { + ticket.Properties.IssuedUtc = ticket.Properties.ExpiresUtc = null; + ticket.RemoveProperty(OpenIdConnectConstants.Properties.TokenId); + } + // If reference tokens are enabled, create a new entry for // authorization codes, refresh tokens and access tokens. if (options.UseReferenceTokens) { - // When the token is a reference token, remove the token identifier from the - // authentication ticket as it is restored when receiving and decrypting it. - ticket.RemoveProperty(OpenIdConnectConstants.Properties.TokenId); - // Note: the data format is automatically replaced at startup time to ensure // that encrypted tokens stored in the database cannot be considered as // valid tokens if the developer decides to disable reference tokens support. - var ciphertext = format.Protect(ticket); + descriptor.Ciphertext = format.Protect(ticket); // Generate a new crypto-secure random identifier that will be // substituted to the ciphertext returned by the data format. @@ -203,49 +240,19 @@ namespace OpenIddict // 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. - string hash; using (var algorithm = SHA256.Create()) { - hash = Convert.ToBase64String(algorithm.ComputeHash(bytes)); + descriptor.Hash = Convert.ToBase64String(algorithm.ComputeHash(bytes)); } - - token = await Tokens.CreateAsync(type, subject, hash, ciphertext, - ticket.Properties.IssuedUtc, - ticket.Properties.ExpiresUtc, context.RequestAborted); } // Otherwise, only create a token metadata entry for authorization codes and refresh tokens. - else if (string.Equals(type, OpenIdConnectConstants.TokenUsages.AuthorizationCode, StringComparison.OrdinalIgnoreCase) || - string.Equals(type, OpenIdConnectConstants.TokenUsages.RefreshToken, StringComparison.OrdinalIgnoreCase)) - { - token = await Tokens.CreateAsync(type, subject, - ticket.Properties.IssuedUtc, - ticket.Properties.ExpiresUtc, context.RequestAborted); - } - - else - { - return null; - } - - // If a null value was returned by CreateAsync(), return immediately. - if (token == null) + else if (type != OpenIdConnectConstants.TokenUsages.AuthorizationCode && + type != OpenIdConnectConstants.TokenUsages.RefreshToken) { return null; } - // Throw an exception if the token identifier can't be resolved. - var identifier = await Tokens.GetIdAsync(token, context.RequestAborted); - if (string.IsNullOrEmpty(identifier)) - { - throw new InvalidOperationException("The unique key associated with a refresh token cannot be null or empty."); - } - - // Attach the key returned by the underlying store - // to the refresh token to override the default GUID - // generated by the OpenID Connect server middleware. - ticket.SetTokenId(identifier); - // If the client application is known, associate it with the token. if (!string.IsNullOrEmpty(request.ClientId)) { @@ -255,41 +262,61 @@ namespace OpenIddict throw new InvalidOperationException("The client application cannot be retrieved from the database."); } - var key = await Applications.GetIdAsync(application, context.RequestAborted); - - await Tokens.SetClientAsync(token, key, context.RequestAborted); + descriptor.ApplicationId = await Applications.GetIdAsync(application, context.RequestAborted); } // If an authorization identifier was specified, bind it to the token. if (ticket.HasProperty(OpenIddictConstants.Properties.AuthorizationId)) { - await Tokens.SetAuthorizationAsync(token, - ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId), context.RequestAborted); + descriptor.AuthorizationId = ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId); } // Otherwise, create an ad-hoc authorization if the token is an authorization code. - else if (string.Equals(type, OpenIdConnectConstants.TokenUsages.AuthorizationCode, StringComparison.OrdinalIgnoreCase)) + else if (type == OpenIdConnectConstants.TokenUsages.AuthorizationCode) { - Debug.Assert(!string.IsNullOrEmpty(request.ClientId), "The client identifier shouldn't be null."); + Debug.Assert(!string.IsNullOrEmpty(descriptor.ApplicationId), "The client identifier shouldn't be null."); - var application = await Applications.FindByClientIdAsync(request.ClientId, context.RequestAborted); - if (application == null) + var authorization = await Authorizations.CreateAsync(new OpenIddictAuthorizationDescriptor { - throw new InvalidOperationException("The client application cannot be retrieved from the database."); - } - - var authorization = await Authorizations.CreateAsync(subject, - await Applications.GetIdAsync(application, context.RequestAborted), request.GetScopes(), context.RequestAborted); + ApplicationId = descriptor.ApplicationId, + Scopes = request.GetScopes(), + Subject = descriptor.Subject + }, context.RequestAborted); if (authorization != null) { - var key = await Authorizations.GetIdAsync(authorization, context.RequestAborted); - ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, key); + descriptor.AuthorizationId = await Authorizations.GetIdAsync(authorization, context.RequestAborted); - await Tokens.SetAuthorizationAsync(token, key, context.RequestAborted); + Logger.LogInformation("An ad-hoc authorization was automatically created and " + + "associated with the '{ClientId}' application: {Identifier}.", + request.ClientId, descriptor.AuthorizationId); } } + // If a null value was returned by CreateAsync(), return immediately. + var token = await Tokens.CreateAsync(descriptor, context.RequestAborted); + if (token == null) + { + return null; + } + + // Throw an exception if the token identifier can't be resolved. + var identifier = await Tokens.GetIdAsync(token, context.RequestAborted); + if (string.IsNullOrEmpty(identifier)) + { + throw new InvalidOperationException("The unique key associated with a refresh token cannot be null or empty."); + } + + // Restore the token identifier using the unique + // identifier attached with the database entry. + ticket.SetTokenId(identifier); + + // Dynamically set the creation and expiration dates. + ticket.Properties.IssuedUtc = await Tokens.GetCreationDateAsync(token, context.RequestAborted); + ticket.Properties.ExpiresUtc = await Tokens.GetExpirationDateAsync(token, context.RequestAborted); + + ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, descriptor.AuthorizationId); + if (!string.IsNullOrEmpty(result)) { Logger.LogTrace("A new reference token was successfully generated and persisted " + @@ -373,6 +400,10 @@ namespace OpenIddict // identifier attached with the database entry. ticket.SetTokenId(identifier); + // Dynamically set the creation and expiration dates. + ticket.Properties.IssuedUtc = await Tokens.GetCreationDateAsync(token, context.RequestAborted); + ticket.Properties.ExpiresUtc = await Tokens.GetExpirationDateAsync(token, context.RequestAborted); + // If the authorization identifier cannot be found in the ticket properties, // try to restore it using the identifier associated with the database entry. if (!ticket.HasProperty(OpenIddictConstants.Properties.AuthorizationId)) diff --git a/test/OpenIddict.Tests/OpenIddictInitializerTests.cs b/test/OpenIddict.Tests/OpenIddictInitializerTests.cs index af5a7f44..19782a02 100644 --- a/test/OpenIddict.Tests/OpenIddictInitializerTests.cs +++ b/test/OpenIddict.Tests/OpenIddictInitializerTests.cs @@ -155,6 +155,52 @@ namespace OpenIddict.Tests Assert.Equal("Reference tokens cannot be used when disabling token revocation.", exception.Message); } + [Fact] + public async Task PostConfigure_ThrowsAnExceptionWhenUsingRollingTokensWithTokenRevocationDisabled() + { + // Arrange + var server = CreateAuthorizationServer(builder => + { + builder.EnableAuthorizationEndpoint("/connect/authorize") + .AllowImplicitFlow() + .DisableTokenRevocation() + .UseRollingTokens(); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act and assert + var exception = await Assert.ThrowsAsync(delegate + { + return client.GetAsync("/"); + }); + + Assert.Equal("Rolling tokens cannot be used when disabling token expiration.", exception.Message); + } + + [Fact] + public async Task PostConfigure_ThrowsAnExceptionWhenUsingRollingTokensWithSlidingExpirationDisabled() + { + // Arrange + var server = CreateAuthorizationServer(builder => + { + builder.EnableAuthorizationEndpoint("/connect/authorize") + .AllowImplicitFlow() + .UseRollingTokens() + .Configure(options => options.UseSlidingExpiration = false); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act and assert + var exception = await Assert.ThrowsAsync(delegate + { + return client.GetAsync("/"); + }); + + Assert.Equal("Rolling tokens cannot be used without enabling sliding expiration.", exception.Message); + } + [Fact] public async Task PostConfigure_ThrowsAnExceptionWhenUsingReferenceTokensIfAnAccessTokenHandlerIsSet() { diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs index 26a8a9f6..47ab37df 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs @@ -554,16 +554,16 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); + + instance.Setup(mock => mock.GetIdAsync(application, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); })); builder.Services.AddSingleton(CreateTokenManager(instance => { var token = new OpenIddictToken(); - instance.Setup(mock => mock.CreateAsync( - OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, "Bob le Magnifique", - It.IsAny(), It.IsAny(), - It.IsAny())) + instance.Setup(mock => mock.CreateAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(token); instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs index fd1034eb..9a653281 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs @@ -577,7 +577,7 @@ namespace OpenIddict.Tests // Assert Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified authorization code has already been redemeed.", response.ErrorDescription); + Assert.Equal("The specified authorization code has already been redeemed.", response.ErrorDescription); Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(token, It.IsAny()), Times.Once()); @@ -660,7 +660,7 @@ namespace OpenIddict.Tests // Assert Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified authorization code has already been redemeed.", response.ErrorDescription); + Assert.Equal("The specified authorization code has already been redeemed.", response.ErrorDescription); Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny()), Times.Once()); @@ -796,6 +796,69 @@ namespace OpenIddict.Tests Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Once()); } + [Fact] + public async Task HandleTokenRequest_RequestIsRejectedWhenRefreshTokenIsAlreadyRedeemed() + { + // Arrange + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); + + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) + .ReturnsAsync(true); + }); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(CreateApplicationManager(instance => + { + var application = new OpenIddictApplication(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); + })); + + builder.Services.AddSingleton(manager); + + builder.Configure(options => options.RefreshTokenFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); + Assert.Equal("The specified refresh token has already been redeemed.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(token, It.IsAny()), Times.Once()); + } + [Fact] public async Task HandleTokenRequest_RequestIsRejectedWhenRefreshTokenIsInvalid() { @@ -820,6 +883,9 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) + .ReturnsAsync(false); + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) .ReturnsAsync(false); }); @@ -926,7 +992,7 @@ namespace OpenIddict.Tests } [Fact] - public async Task HandleTokenRequest_RefreshTokenIsAutomaticallyRedeemedWhenSlidingExpirationIsEnabled() + public async Task HandleTokenRequest_RefreshTokenIsAutomaticallyRedeemedWhenRollingTokensAreEnabled() { // Arrange var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); @@ -974,6 +1040,8 @@ namespace OpenIddict.Tests builder.Services.AddSingleton(manager); + builder.UseRollingTokens(); + builder.Configure(options => options.RefreshTokenFormat = format.Object); }); diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs index 7401d32a..6af88080 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs @@ -650,6 +650,9 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); + instance.Setup(mock => mock.GetIdAsync(application, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) .ReturnsAsync(true); })); diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs index 6599cfa1..fba55324 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs @@ -46,8 +46,9 @@ namespace OpenIddict.Tests Assert.NotNull(response.AccessToken); Mock.Get(manager).Verify(mock => mock.CreateAsync( - OpenIdConnectConstants.TokenTypeHints.AccessToken, "Bob le Magnifique", - It.IsAny(), It.IsAny(), + It.Is(descriptor => + descriptor.Subject == "Bob le Magnifique" && + descriptor.Type == OpenIdConnectConstants.TokenTypeHints.AccessToken), It.IsAny()), Times.Never()); } @@ -57,16 +58,13 @@ namespace OpenIddict.Tests // Arrange var token = new OpenIddictToken { - End = new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero), - Start = new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero), + CreationDate = new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero), + ExpirationDate = new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero) }; var manager = CreateTokenManager(instance => { - instance.Setup(mock => mock.CreateAsync( - OpenIdConnectConstants.TokenTypeHints.AccessToken, "Bob le Magnifique", - It.IsNotNull(), It.IsNotNull(), - token.Start, token.End, It.IsAny())) + instance.Setup(mock => mock.CreateAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(token); instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) @@ -81,8 +79,8 @@ namespace OpenIddict.Tests builder.Configure(options => { - options.SystemClock = Mock.Of(mock => mock.UtcNow == token.Start.Value); - options.AccessTokenLifetime = token.End.Value - token.Start.Value; + options.SystemClock = Mock.Of(mock => mock.UtcNow == token.CreationDate.Value); + options.AccessTokenLifetime = token.ExpirationDate.Value - token.CreationDate.Value; }); }); @@ -101,10 +99,14 @@ namespace OpenIddict.Tests Assert.NotNull(response.AccessToken); Mock.Get(manager).Verify(mock => mock.CreateAsync( - OpenIdConnectConstants.TokenTypeHints.AccessToken, "Bob le Magnifique", - It.IsNotNull(), It.IsNotNull(), - token.Start, token.End, It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.Once()); + It.Is(descriptor => + descriptor.Ciphertext != null && + descriptor.Hash != null && + descriptor.ExpirationDate == token.ExpirationDate && + descriptor.CreationDate == token.CreationDate && + descriptor.Subject == "Bob le Magnifique" && + descriptor.Type == OpenIdConnectConstants.TokenTypeHints.AccessToken), + It.IsAny()), Times.Once()); } [Fact] @@ -115,18 +117,11 @@ namespace OpenIddict.Tests var manager = CreateTokenManager(instance => { - instance.Setup(mock => mock.CreateAsync( - OpenIdConnectConstants.TokenTypeHints.AccessToken, "Bob le Magnifique", - It.IsNotNull(), It.IsNotNull(), - It.IsAny(), It.IsAny(), - It.IsAny())) + instance.Setup(mock => mock.CreateAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(token); instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); - - instance.Setup(mock => mock.SetClientAsync(token, "3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .Returns(Task.FromResult(0)); }); var server = CreateAuthorizationServer(builder => @@ -165,7 +160,12 @@ namespace OpenIddict.Tests // Assert Assert.NotNull(response.AccessToken); - Mock.Get(manager).Verify(mock => mock.SetClientAsync(token, "3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.CreateAsync( + It.Is(descriptor => + descriptor.ApplicationId == "3E228451-1555-46F7-A471-951EFBA23A56" && + descriptor.Subject == "Bob le Magnifique" && + descriptor.Type == OpenIdConnectConstants.TokenTypeHints.AccessToken), + It.IsAny()), Times.Once()); } [Fact] @@ -176,18 +176,11 @@ namespace OpenIddict.Tests var manager = CreateTokenManager(instance => { - instance.Setup(mock => mock.CreateAsync( - OpenIdConnectConstants.TokenTypeHints.AccessToken, "Bob le Magnifique", - It.IsNotNull(), It.IsNotNull(), - It.IsAny(), It.IsAny(), - It.IsAny())) + instance.Setup(mock => mock.CreateAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(token); instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); - - instance.Setup(mock => mock.SetAuthorizationAsync(token, "1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70", It.IsAny())) - .Returns(Task.FromResult(0)); }); var server = CreateAuthorizationServer(builder => @@ -218,7 +211,12 @@ namespace OpenIddict.Tests // Assert Assert.NotNull(response.AccessToken); - Mock.Get(manager).Verify(mock => mock.SetAuthorizationAsync(token, "1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.CreateAsync( + It.Is(descriptor => + descriptor.AuthorizationId == "1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70" && + descriptor.Subject == "Bob le Magnifique" && + descriptor.Type == OpenIdConnectConstants.TokenTypeHints.AccessToken), + It.IsAny()), Times.Once()); } [Fact] @@ -267,8 +265,7 @@ namespace OpenIddict.Tests Assert.NotNull(response.Code); Mock.Get(manager).Verify(mock => mock.CreateAsync( - OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, "Bob le Magnifique", - It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny()), Times.Never()); } @@ -278,15 +275,13 @@ namespace OpenIddict.Tests // Arrange var token = new OpenIddictToken { - End = new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero), - Start = new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero), + CreationDate = new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero), + ExpirationDate = new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) }; var manager = CreateTokenManager(instance => { - instance.Setup(mock => mock.CreateAsync( - OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, "Bob le Magnifique", - token.Start, token.End, It.IsAny())) + instance.Setup(mock => mock.CreateAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(token); instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) @@ -310,14 +305,17 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); + + instance.Setup(mock => mock.GetIdAsync(application, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); })); builder.Services.AddSingleton(manager); builder.Configure(options => { - options.SystemClock = Mock.Of(mock => mock.UtcNow == token.Start.Value); - options.AuthorizationCodeLifetime = token.End.Value - token.Start.Value; + options.SystemClock = Mock.Of(mock => mock.UtcNow == token.CreationDate.Value); + options.AuthorizationCodeLifetime = token.ExpirationDate.Value - token.CreationDate.Value; }); }); @@ -335,9 +333,14 @@ namespace OpenIddict.Tests Assert.NotNull(response.Code); Mock.Get(manager).Verify(mock => mock.CreateAsync( - OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, "Bob le Magnifique", - token.Start, token.End, It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.Once()); + It.Is(descriptor => + descriptor.Ciphertext == null && + descriptor.Hash == null && + descriptor.ExpirationDate == token.ExpirationDate && + descriptor.CreationDate == token.CreationDate && + descriptor.Subject == "Bob le Magnifique" && + descriptor.Type == OpenIdConnectConstants.TokenTypeHints.AuthorizationCode), + It.IsAny()), Times.Once()); } [Fact] @@ -346,16 +349,13 @@ namespace OpenIddict.Tests // Arrange var token = new OpenIddictToken { - End = new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero), - Start = new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero), + CreationDate = new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero), + ExpirationDate = new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) }; var manager = CreateTokenManager(instance => { - instance.Setup(mock => mock.CreateAsync( - OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, "Bob le Magnifique", - It.IsNotNull(), It.IsNotNull(), - token.Start, token.End, It.IsAny())) + instance.Setup(mock => mock.CreateAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(token); instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) @@ -379,6 +379,9 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); + + instance.Setup(mock => mock.GetIdAsync(application, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); })); builder.Services.AddSingleton(manager); @@ -387,8 +390,8 @@ namespace OpenIddict.Tests builder.Configure(options => { - options.SystemClock = Mock.Of(mock => mock.UtcNow == token.Start.Value); - options.AuthorizationCodeLifetime = token.End.Value - token.Start.Value; + options.SystemClock = Mock.Of(mock => mock.UtcNow == token.CreationDate.Value); + options.AuthorizationCodeLifetime = token.ExpirationDate.Value - token.CreationDate.Value; }); }); @@ -406,10 +409,14 @@ namespace OpenIddict.Tests Assert.NotNull(response.Code); Mock.Get(manager).Verify(mock => mock.CreateAsync( - OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, "Bob le Magnifique", - It.IsNotNull(), It.IsNotNull(), - token.Start, token.End, It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.Once()); + It.Is(descriptor => + descriptor.Ciphertext != null && + descriptor.Hash != null && + descriptor.ExpirationDate == token.ExpirationDate && + descriptor.CreationDate == token.CreationDate && + descriptor.Subject == "Bob le Magnifique" && + descriptor.Type == OpenIdConnectConstants.TokenTypeHints.AuthorizationCode), + It.IsAny()), Times.Once()); } [Fact] @@ -420,17 +427,11 @@ namespace OpenIddict.Tests var manager = CreateTokenManager(instance => { - instance.Setup(mock => mock.CreateAsync( - OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, "Bob le Magnifique", - It.IsAny(), It.IsAny(), - It.IsAny())) + instance.Setup(mock => mock.CreateAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(token); instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); - - instance.Setup(mock => mock.SetClientAsync(token, "3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .Returns(Task.FromResult(0)); }); var server = CreateAuthorizationServer(builder => @@ -471,7 +472,12 @@ namespace OpenIddict.Tests // Assert Assert.NotNull(response.Code); - Mock.Get(manager).Verify(mock => mock.SetClientAsync(token, "3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.CreateAsync( + It.Is(descriptor => + descriptor.ApplicationId == "3E228451-1555-46F7-A471-951EFBA23A56" && + descriptor.Subject == "Bob le Magnifique" && + descriptor.Type == OpenIdConnectConstants.TokenTypeHints.AuthorizationCode), + It.IsAny()), Times.Once()); } [Fact] @@ -482,20 +488,11 @@ namespace OpenIddict.Tests var manager = CreateTokenManager(instance => { - instance.Setup(mock => mock.CreateAsync( - OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, "Bob le Magnifique", - It.IsAny(), It.IsAny(), - It.IsAny())) + instance.Setup(mock => mock.CreateAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(token); instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); - - instance.Setup(mock => mock.SetClientAsync(token, "3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .Returns(Task.FromResult(0)); - - instance.Setup(mock => mock.SetAuthorizationAsync(token, "1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70", It.IsAny())) - .Returns(Task.FromResult(0)); }); var server = CreateAuthorizationServer(builder => @@ -543,7 +540,13 @@ namespace OpenIddict.Tests // Assert Assert.NotNull(response.Code); - Mock.Get(manager).Verify(mock => mock.SetAuthorizationAsync(token, "1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.CreateAsync( + It.Is(descriptor => + descriptor.ApplicationId == "3E228451-1555-46F7-A471-951EFBA23A56" && + descriptor.AuthorizationId == "1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70" && + descriptor.Subject == "Bob le Magnifique" && + descriptor.Type == OpenIdConnectConstants.TokenTypeHints.AuthorizationCode), + It.IsAny()), Times.Once()); } [Fact] @@ -582,11 +585,8 @@ namespace OpenIddict.Tests builder.Services.AddSingleton(CreateTokenManager(instance => { - instance.Setup(mock => mock.CreateAsync( - OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, "Bob le Magnifique", - It.IsAny(), It.IsAny(), - It.IsAny())) - .ReturnsAsync(token); + instance.Setup(mock => mock.CreateAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(token); instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); @@ -608,8 +608,61 @@ namespace OpenIddict.Tests // Assert Assert.NotNull(response.Code); - Mock.Get(manager).Verify(mock => mock.CreateAsync("Bob le Magnifique", "3E228451-1555-46F7-A471-951EFBA23A56", - It.IsAny>(), It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.CreateAsync( + It.Is(descriptor => + descriptor.ApplicationId == "3E228451-1555-46F7-A471-951EFBA23A56" && + descriptor.Subject == "Bob le Magnifique"), + It.IsAny()), Times.Once()); + } + + [Fact] + public async Task SerializeRefreshToken_ExtendsLifetimeWhenRollingTokensAreDisabled() + { + // Arrange + var token = new OpenIddictToken + { + CreationDate = new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero), + ExpirationDate = new DateTimeOffset(2017, 01, 10, 00, 00, 00, TimeSpan.Zero) + }; + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByHashAsync("d80c119138b3aaeefce94093032c0204c547dc27cc5fe97f32933becd48b7bf5", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) + .ReturnsAsync(token); + }); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(manager); + + builder.UseReferenceTokens(); + + builder.Configure(options => + { + options.SystemClock = Mock.Of(mock => mock.UtcNow == + new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); + options.RefreshTokenLifetime = TimeSpan.FromDays(10); + }); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "3E228451-1555-46F7-A471-951EFBA23A56" + }); + + // Assert + Assert.Null(response.RefreshToken); + + Mock.Get(manager).Verify(mock => mock.ExtendAsync(token, + new DateTimeOffset(2017, 01, 15, 00, 00, 00, TimeSpan.Zero), + It.IsAny()), Times.Never()); } [Fact] @@ -642,8 +695,7 @@ namespace OpenIddict.Tests Assert.NotNull(response.RefreshToken); Mock.Get(manager).Verify(mock => mock.CreateAsync( - OpenIdConnectConstants.TokenTypeHints.RefreshToken, "Bob le Magnifique", - It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny()), Times.Never()); } @@ -653,15 +705,13 @@ namespace OpenIddict.Tests // Arrange var token = new OpenIddictToken { - End = new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero), - Start = new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero) + CreationDate = new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero), + ExpirationDate = new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) }; var manager = CreateTokenManager(instance => { - instance.Setup(mock => mock.CreateAsync( - OpenIdConnectConstants.TokenTypeHints.RefreshToken, "Bob le Magnifique", - token.Start, token.End, It.IsAny())) + instance.Setup(mock => mock.CreateAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(token); instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) @@ -674,8 +724,8 @@ namespace OpenIddict.Tests builder.Configure(options => { - options.SystemClock = Mock.Of(mock => mock.UtcNow == token.Start.Value); - options.RefreshTokenLifetime = token.End.Value - token.Start.Value; + options.SystemClock = Mock.Of(mock => mock.UtcNow == token.CreationDate.Value); + options.RefreshTokenLifetime = token.ExpirationDate.Value - token.CreationDate.Value; }); }); @@ -694,9 +744,14 @@ namespace OpenIddict.Tests Assert.NotNull(response.RefreshToken); Mock.Get(manager).Verify(mock => mock.CreateAsync( - OpenIdConnectConstants.TokenTypeHints.RefreshToken, "Bob le Magnifique", - token.Start, token.End, It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.Once()); + It.Is(descriptor => + descriptor.Ciphertext == null && + descriptor.Hash == null && + descriptor.ExpirationDate == token.ExpirationDate && + descriptor.CreationDate == token.CreationDate && + descriptor.Subject == "Bob le Magnifique" && + descriptor.Type == OpenIdConnectConstants.TokenTypeHints.RefreshToken), + It.IsAny()), Times.Once()); } [Fact] @@ -705,16 +760,13 @@ namespace OpenIddict.Tests // Arrange var token = new OpenIddictToken { - End = new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero), - Start = new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero), + CreationDate = new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero), + ExpirationDate = new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) }; var manager = CreateTokenManager(instance => { - instance.Setup(mock => mock.CreateAsync( - OpenIdConnectConstants.TokenTypeHints.RefreshToken, "Bob le Magnifique", - It.IsNotNull(), It.IsNotNull(), - token.Start, token.End, It.IsAny())) + instance.Setup(mock => mock.CreateAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(token); instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) @@ -729,8 +781,8 @@ namespace OpenIddict.Tests builder.Configure(options => { - options.SystemClock = Mock.Of(mock => mock.UtcNow == token.Start.Value); - options.RefreshTokenLifetime = token.End.Value - token.Start.Value; + options.SystemClock = Mock.Of(mock => mock.UtcNow == token.CreationDate.Value); + options.RefreshTokenLifetime = token.ExpirationDate.Value - token.CreationDate.Value; }); }); @@ -749,10 +801,14 @@ namespace OpenIddict.Tests Assert.NotNull(response.RefreshToken); Mock.Get(manager).Verify(mock => mock.CreateAsync( - OpenIdConnectConstants.TokenTypeHints.RefreshToken, "Bob le Magnifique", - It.IsNotNull(), It.IsNotNull(), - token.Start, token.End, It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.Once()); + It.Is(descriptor => + descriptor.Ciphertext != null && + descriptor.Hash != null && + descriptor.ExpirationDate == token.ExpirationDate && + descriptor.CreationDate == token.CreationDate && + descriptor.Subject == "Bob le Magnifique" && + descriptor.Type == OpenIdConnectConstants.TokenTypeHints.RefreshToken), + It.IsAny()), Times.Once()); } [Fact] @@ -763,17 +819,11 @@ namespace OpenIddict.Tests var manager = CreateTokenManager(instance => { - instance.Setup(mock => mock.CreateAsync( - OpenIdConnectConstants.TokenTypeHints.RefreshToken, "Bob le Magnifique", - It.IsAny(), It.IsAny(), - It.IsAny())) + instance.Setup(mock => mock.CreateAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(token); instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); - - instance.Setup(mock => mock.SetClientAsync(token, "3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .Returns(Task.FromResult(0)); }); var server = CreateAuthorizationServer(builder => @@ -810,7 +860,12 @@ namespace OpenIddict.Tests // Assert Assert.NotNull(response.RefreshToken); - Mock.Get(manager).Verify(mock => mock.SetClientAsync(token, "3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.CreateAsync( + It.Is(descriptor => + descriptor.ApplicationId == "3E228451-1555-46F7-A471-951EFBA23A56" && + descriptor.Subject == "Bob le Magnifique" && + descriptor.Type == OpenIdConnectConstants.TokenTypeHints.RefreshToken), + It.IsAny()), Times.Once()); } [Fact] @@ -821,17 +876,11 @@ namespace OpenIddict.Tests var manager = CreateTokenManager(instance => { - instance.Setup(mock => mock.CreateAsync( - OpenIdConnectConstants.TokenTypeHints.RefreshToken, "Bob le Magnifique", - It.IsAny(), It.IsAny(), - It.IsAny())) + instance.Setup(mock => mock.CreateAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(token); instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); - - instance.Setup(mock => mock.SetAuthorizationAsync(token, "1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70", It.IsAny())) - .Returns(Task.FromResult(0)); }); var server = CreateAuthorizationServer(builder => @@ -860,7 +909,12 @@ namespace OpenIddict.Tests // Assert Assert.NotNull(response.RefreshToken); - Mock.Get(manager).Verify(mock => mock.SetAuthorizationAsync(token, "1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.CreateAsync( + It.Is(descriptor => + descriptor.AuthorizationId == "1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70" && + descriptor.Subject == "Bob le Magnifique" && + descriptor.Type == OpenIdConnectConstants.TokenTypeHints.RefreshToken), + It.IsAny()), Times.Once()); } } }