Browse Source

Revamp refresh tokens

pull/1150/head
Kévin Chalet 5 years ago
parent
commit
a6dd8cf031
  1. 3
      samples/Mvc.Server/Startup.cs
  2. 5
      src/OpenIddict.Abstractions/Descriptors/OpenIddictTokenDescriptor.cs
  3. 11
      src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs
  4. 42
      src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs
  5. 20
      src/OpenIddict.Abstractions/Resources/OpenIddictResources.resx
  6. 20
      src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs
  7. 25
      src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
  8. 153
      src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
  9. 5
      src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkToken.cs
  10. 29
      src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs
  11. 5
      src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreToken.cs
  12. 29
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs
  13. 6
      src/OpenIddict.MongoDb.Models/OpenIddictMongoDbToken.cs
  14. 29
      src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs
  15. 2
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs
  16. 2
      src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs
  17. 28
      src/OpenIddict.Server/OpenIddictServerBuilder.cs
  18. 34
      src/OpenIddict.Server/OpenIddictServerConfiguration.cs
  19. 2
      src/OpenIddict.Server/OpenIddictServerExtensions.cs
  20. 32
      src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs
  21. 314
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  22. 24
      src/OpenIddict.Server/OpenIddictServerOptions.cs
  23. 364
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs
  24. 717
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs
  25. 143
      test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs

3
samples/Mvc.Server/Startup.cs

@ -89,7 +89,7 @@ namespace Mvc.Server
.SetUserinfoEndpointUris("/connect/userinfo")
.SetVerificationEndpointUris("/connect/verify");
// Note: the Mvc.Client sample only uses the code flow and the password flow, but you
// Note: this sample uses the code, device code, password and refresh token flows, but you
// can enable the other flows if you need to support implicit or client credentials.
options.AllowAuthorizationCodeFlow()
.AllowDeviceCodeFlow()
@ -131,6 +131,7 @@ namespace Mvc.Server
//
// options.IgnoreEndpointPermissions()
// .IgnoreGrantTypePermissions()
// .IgnoreResponseTypePermissions()
// .IgnoreScopePermissions();
// Note: when issuing access tokens used by third-party APIs

5
src/OpenIddict.Abstractions/Descriptors/OpenIddictTokenDescriptor.cs

@ -39,6 +39,11 @@ namespace OpenIddict.Abstractions
/// </summary>
public ClaimsPrincipal? Principal { get; set; }
/// <summary>
/// Gets or sets the redemption date associated with the token.
/// </summary>
public DateTimeOffset? RedemptionDate { get; set; }
/// <summary>
/// Gets or sets the reference identifier associated with the token.
/// Note: depending on the application manager used when creating it,

11
src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs

@ -375,17 +375,6 @@ namespace OpenIddict.Abstractions
/// </returns>
ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken = default);
/// <summary>
/// Sets the application identifier associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="identifier">The unique identifier associated with the client application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
ValueTask SetApplicationIdAsync(object authorization, string identifier, CancellationToken cancellationToken = default);
/// <summary>
/// Tries to revoke an authorization.
/// </summary>

42
src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs

@ -255,6 +255,17 @@ namespace OpenIddict.Abstractions
/// </returns>
ValueTask<string> GetPayloadAsync(object token, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the redemption date associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the redemption date associated with the specified token.
/// </returns>
ValueTask<DateTimeOffset?> GetRedemptionDateAsync(object token, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the reference identifier associated with a token.
/// Note: depending on the manager used to create the token,
@ -385,37 +396,6 @@ namespace OpenIddict.Abstractions
/// </returns>
ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken = default);
/// <summary>
/// Sets the application identifier associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="identifier">The unique identifier associated with the client application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
ValueTask SetApplicationIdAsync(object token, string identifier, CancellationToken cancellationToken = default);
/// <summary>
/// Sets the authorization identifier associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="identifier">The unique identifier associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
ValueTask SetAuthorizationIdAsync(object token, string identifier, CancellationToken cancellationToken = default);
/// <summary>
/// Tries to extend the specified token by replacing its expiration date.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="date">The date on which the token will no longer be considered valid.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the token was successfully extended, <c>false</c> otherwise.</returns>
ValueTask<bool> TryExtendAsync(object token, DateTimeOffset? date, CancellationToken cancellationToken = default);
/// <summary>
/// Tries to redeem a token.
/// </summary>

20
src/OpenIddict.Abstractions/Resources/OpenIddictResources.resx

@ -489,10 +489,6 @@ To enable DI support, call 'services.AddQuartz(options =&gt; options.UseMicrosof
<value>Reference tokens cannot be used when disabling token storage.</value>
<comment>{Locked}</comment>
</data>
<data name="ID0084" xml:space="preserve">
<value>Sliding expiration must be disabled when turning off token storage if rolling tokens are not used.</value>
<comment>{Locked}</comment>
</data>
<data name="ID0085" xml:space="preserve">
<value>At least one encryption key must be registered in the OpenIddict server options.
Consider registering a certificate using 'services.AddOpenIddict().AddServer().AddEncryptionCertificate()' or 'services.AddOpenIddict().AddServer().AddDevelopmentEncryptionCertificate()' or call 'services.AddOpenIddict().AddServer().AddEphemeralEncryptionKey()' to use an ephemeral key.</value>
@ -2443,22 +2439,6 @@ This may indicate that the hashed entry is corrupted or malformed.</value>
<value>An exception occurred while trying to revoke the authorization '{Identifier}'.</value>
<comment>{Locked}</comment>
</data>
<data name="ID6167" xml:space="preserve">
<value>The expiration date of the refresh token '{Identifier}' was successfully updated: {Date}.</value>
<comment>{Locked}</comment>
</data>
<data name="ID6168" xml:space="preserve">
<value>The expiration date of the refresh token '{Identifier}' was successfully removed.</value>
<comment>{Locked}</comment>
</data>
<data name="ID6169" xml:space="preserve">
<value>A concurrency exception occurred while trying to update the expiration date of the token '{Identifier}'.</value>
<comment>{Locked}</comment>
</data>
<data name="ID6170" xml:space="preserve">
<value>An exception occurred while trying to update the expiration date of the token '{Identifier}'.</value>
<comment>{Locked}</comment>
</data>
<data name="ID6171" xml:space="preserve">
<value>The token '{Identifier}' was successfully marked as redeemed.</value>
<comment>{Locked}</comment>

20
src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs

@ -233,6 +233,17 @@ namespace OpenIddict.Abstractions
/// </returns>
ValueTask<ImmutableDictionary<string, JsonElement>> GetPropertiesAsync(TToken token, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the redemption date associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the redemption date associated with the specified token.
/// </returns>
ValueTask<DateTimeOffset?> GetRedemptionDateAsync(TToken token, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the reference identifier associated with a token.
/// Note: depending on the manager used to create the token,
@ -375,6 +386,15 @@ namespace OpenIddict.Abstractions
ValueTask SetPropertiesAsync(TToken token,
ImmutableDictionary<string, JsonElement> properties, CancellationToken cancellationToken);
/// <summary>
/// Sets the redemption date associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="date">The redemption date.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
ValueTask SetRedemptionDateAsync(TToken token, DateTimeOffset? date, CancellationToken cancellationToken);
/// <summary>
/// Sets the reference identifier associated with a token.
/// Note: depending on the manager used to create the token,

25
src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs

@ -995,27 +995,6 @@ namespace OpenIddict.Core
public virtual ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken = default)
=> Store.PruneAsync(threshold, cancellationToken);
/// <summary>
/// Sets the application identifier associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="identifier">The unique identifier associated with the client application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual async ValueTask SetApplicationIdAsync(
TAuthorization authorization, string? identifier, CancellationToken cancellationToken = default)
{
if (authorization is null)
{
throw new ArgumentNullException(nameof(authorization));
}
await Store.SetApplicationIdAsync(authorization, identifier, cancellationToken);
await UpdateAsync(authorization, cancellationToken);
}
/// <summary>
/// Tries to revoke an authorization.
/// </summary>
@ -1312,10 +1291,6 @@ namespace OpenIddict.Core
ValueTask IOpenIddictAuthorizationManager.PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken)
=> PruneAsync(threshold, cancellationToken);
/// <inheritdoc/>
ValueTask IOpenIddictAuthorizationManager.SetApplicationIdAsync(object authorization, string? identifier, CancellationToken cancellationToken)
=> SetApplicationIdAsync((TAuthorization) authorization, identifier, cancellationToken);
/// <inheritdoc/>
ValueTask<bool> IOpenIddictAuthorizationManager.TryRevokeAsync(object authorization, CancellationToken cancellationToken)
=> TryRevokeAsync((TAuthorization) authorization, cancellationToken);

153
src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs

@ -744,6 +744,25 @@ namespace OpenIddict.Core
return Store.GetPayloadAsync(token, cancellationToken);
}
/// <summary>
/// Retrieves the redemption date associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the redemption date associated with the specified token.
/// </returns>
public virtual ValueTask<DateTimeOffset?> GetRedemptionDateAsync(TToken token, CancellationToken cancellationToken = default)
{
if (token is null)
{
throw new ArgumentNullException(nameof(token));
}
return Store.GetRedemptionDateAsync(token, cancellationToken);
}
/// <summary>
/// Retrieves the reference identifier associated with a token.
/// Note: depending on the manager used to create the token,
@ -943,6 +962,7 @@ namespace OpenIddict.Core
await Store.SetCreationDateAsync(token, descriptor.CreationDate, cancellationToken);
await Store.SetExpirationDateAsync(token, descriptor.ExpirationDate, cancellationToken);
await Store.SetPayloadAsync(token, descriptor.Payload, cancellationToken);
await Store.SetRedemptionDateAsync(token, descriptor.RedemptionDate, cancellationToken);
await Store.SetReferenceIdAsync(token, descriptor.ReferenceId, cancellationToken);
await Store.SetStatusAsync(token, descriptor.Status, cancellationToken);
await Store.SetSubjectAsync(token, descriptor.Subject, cancellationToken);
@ -977,6 +997,7 @@ namespace OpenIddict.Core
descriptor.CreationDate = await Store.GetCreationDateAsync(token, cancellationToken);
descriptor.ExpirationDate = await Store.GetExpirationDateAsync(token, cancellationToken);
descriptor.Payload = await Store.GetPayloadAsync(token, cancellationToken);
descriptor.RedemptionDate = await Store.GetRedemptionDateAsync(token, cancellationToken);
descriptor.ReferenceId = await Store.GetReferenceIdAsync(token, cancellationToken);
descriptor.Status = await Store.GetStatusAsync(token, cancellationToken);
descriptor.Subject = await Store.GetSubjectAsync(token, cancellationToken);
@ -994,103 +1015,6 @@ namespace OpenIddict.Core
/// </returns>
public virtual ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken = default)
=> Store.PruneAsync(threshold, cancellationToken);
/// <summary>
/// Sets the application identifier associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="identifier">The unique identifier associated with the client application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual async ValueTask SetApplicationIdAsync(TToken token,
string? identifier, CancellationToken cancellationToken = default)
{
if (token is null)
{
throw new ArgumentNullException(nameof(token));
}
await Store.SetApplicationIdAsync(token, identifier, cancellationToken);
await UpdateAsync(token, cancellationToken);
}
/// <summary>
/// Sets the authorization identifier associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="identifier">The unique identifier associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual async ValueTask SetAuthorizationIdAsync(TToken token,
string? identifier, CancellationToken cancellationToken = default)
{
if (token is null)
{
throw new ArgumentNullException(nameof(token));
}
await Store.SetAuthorizationIdAsync(token, identifier, cancellationToken);
await UpdateAsync(token, cancellationToken);
}
/// <summary>
/// Tries to extend the specified token by replacing its expiration date.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="date">The date on which the token will no longer be considered valid.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the token was successfully extended, <c>false</c> otherwise.</returns>
public virtual async ValueTask<bool> TryExtendAsync(TToken token,
DateTimeOffset? date, CancellationToken cancellationToken = default)
{
if (token is null)
{
throw new ArgumentNullException(nameof(token));
}
if (date == await Store.GetExpirationDateAsync(token, cancellationToken))
{
return true;
}
await Store.SetExpirationDateAsync(token, date, cancellationToken);
try
{
await UpdateAsync(token, cancellationToken);
if (date is not null)
{
Logger.LogInformation(SR.GetResourceString(SR.ID6167), await Store.GetIdAsync(token, cancellationToken), date);
}
else
{
Logger.LogInformation(SR.GetResourceString(SR.ID6168), await Store.GetIdAsync(token, cancellationToken));
}
return true;
}
catch (ConcurrencyException exception)
{
Logger.LogDebug(exception, SR.GetResourceString(SR.ID6169), await Store.GetIdAsync(token, cancellationToken));
return false;
}
catch (Exception exception)
{
Logger.LogWarning(exception, SR.GetResourceString(SR.ID6170), await Store.GetIdAsync(token, cancellationToken));
return false;
}
}
/// <summary>
/// Tries to redeem a token.
/// </summary>
@ -1104,10 +1028,11 @@ namespace OpenIddict.Core
throw new ArgumentNullException(nameof(token));
}
var status = await Store.GetStatusAsync(token, cancellationToken);
if (string.Equals(status, Statuses.Redeemed, StringComparison.OrdinalIgnoreCase))
// If the token doesn't have a redemption date attached, this likely means it's
// the first time the token is redeemed. In this case, attach the current date.
if (await Store.GetRedemptionDateAsync(token, cancellationToken) is null)
{
return true;
await Store.SetRedemptionDateAsync(token, DateTimeOffset.UtcNow, cancellationToken);
}
await Store.SetStatusAsync(token, Statuses.Redeemed, cancellationToken);
@ -1149,12 +1074,6 @@ namespace OpenIddict.Core
throw new ArgumentNullException(nameof(token));
}
var status = await Store.GetStatusAsync(token, cancellationToken);
if (string.Equals(status, Statuses.Rejected, StringComparison.OrdinalIgnoreCase))
{
return true;
}
await Store.SetStatusAsync(token, Statuses.Rejected, cancellationToken);
try
@ -1194,12 +1113,6 @@ namespace OpenIddict.Core
throw new ArgumentNullException(nameof(token));
}
var status = await Store.GetStatusAsync(token, cancellationToken);
if (string.Equals(status, Statuses.Revoked, StringComparison.OrdinalIgnoreCase))
{
return true;
}
await Store.SetStatusAsync(token, Statuses.Revoked, cancellationToken);
try
@ -1465,6 +1378,10 @@ namespace OpenIddict.Core
ValueTask<string?> IOpenIddictTokenManager.GetPayloadAsync(object token, CancellationToken cancellationToken)
=> GetPayloadAsync((TToken) token, cancellationToken);
/// <inheritdoc/>
ValueTask<DateTimeOffset?> IOpenIddictTokenManager.GetRedemptionDateAsync(object token, CancellationToken cancellationToken)
=> GetRedemptionDateAsync((TToken) token, cancellationToken);
/// <inheritdoc/>
ValueTask<string?> IOpenIddictTokenManager.GetReferenceIdAsync(object token, CancellationToken cancellationToken)
=> GetReferenceIdAsync((TToken) token, cancellationToken);
@ -1513,18 +1430,6 @@ namespace OpenIddict.Core
ValueTask IOpenIddictTokenManager.PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken)
=> PruneAsync(threshold, cancellationToken);
/// <inheritdoc/>
ValueTask IOpenIddictTokenManager.SetApplicationIdAsync(object token, string? identifier, CancellationToken cancellationToken)
=> SetApplicationIdAsync((TToken) token, identifier, cancellationToken);
/// <inheritdoc/>
ValueTask IOpenIddictTokenManager.SetAuthorizationIdAsync(object token, string? identifier, CancellationToken cancellationToken)
=> SetAuthorizationIdAsync((TToken) token, identifier, cancellationToken);
/// <inheritdoc/>
ValueTask<bool> IOpenIddictTokenManager.TryExtendAsync(object token, DateTimeOffset? date, CancellationToken cancellationToken)
=> TryExtendAsync((TToken) token, date, cancellationToken);
/// <inheritdoc/>
ValueTask<bool> IOpenIddictTokenManager.TryRedeemAsync(object token, CancellationToken cancellationToken)
=> TryRedeemAsync((TToken) token, cancellationToken);

5
src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkToken.cs

@ -73,6 +73,11 @@ namespace OpenIddict.EntityFramework.Models
/// </summary>
public virtual string? Properties { get; set; }
/// <summary>
/// Gets or sets the UTC redemption date of the current token.
/// </summary>
public virtual DateTime? RedemptionDate { get; set; }
/// <summary>
/// Gets or sets the reference identifier associated
/// with the current token, if applicable.

29
src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs

@ -470,6 +470,22 @@ namespace OpenIddict.EntityFramework
return new ValueTask<ImmutableDictionary<string, JsonElement>>(properties);
}
/// <inheritdoc/>
public virtual ValueTask<DateTimeOffset?> GetRedemptionDateAsync(TToken token, CancellationToken cancellationToken)
{
if (token is null)
{
throw new ArgumentNullException(nameof(token));
}
if (token.RedemptionDate is null)
{
return new ValueTask<DateTimeOffset?>(result: null);
}
return new ValueTask<DateTimeOffset?>(DateTime.SpecifyKind(token.RedemptionDate.Value, DateTimeKind.Utc));
}
/// <inheritdoc/>
public virtual ValueTask<string?> GetReferenceIdAsync(TToken token, CancellationToken cancellationToken)
{
@ -789,6 +805,19 @@ namespace OpenIddict.EntityFramework
return default;
}
/// <inheritdoc/>
public virtual ValueTask SetRedemptionDateAsync(TToken token, DateTimeOffset? date, CancellationToken cancellationToken)
{
if (token is null)
{
throw new ArgumentNullException(nameof(token));
}
token.RedemptionDate = date?.UtcDateTime;
return default;
}
/// <inheritdoc/>
public virtual ValueTask SetReferenceIdAsync(TToken token, string? identifier, CancellationToken cancellationToken)
{

5
src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreToken.cs

@ -81,6 +81,11 @@ namespace OpenIddict.EntityFrameworkCore.Models
/// </summary>
public virtual string? Properties { get; set; }
/// <summary>
/// Gets or sets the UTC redemption date of the current token.
/// </summary>
public virtual DateTime? RedemptionDate { get; set; }
/// <summary>
/// Gets or sets the reference identifier associated
/// with the current token, if applicable.

29
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs

@ -522,6 +522,22 @@ namespace OpenIddict.EntityFrameworkCore
return new ValueTask<ImmutableDictionary<string, JsonElement>>(properties);
}
/// <inheritdoc/>
public virtual ValueTask<DateTimeOffset?> GetRedemptionDateAsync(TToken token, CancellationToken cancellationToken)
{
if (token is null)
{
throw new ArgumentNullException(nameof(token));
}
if (token.RedemptionDate is null)
{
return new ValueTask<DateTimeOffset?>(result: null);
}
return new ValueTask<DateTimeOffset?>(DateTime.SpecifyKind(token.RedemptionDate.Value, DateTimeKind.Utc));
}
/// <inheritdoc/>
public virtual ValueTask<string?> GetReferenceIdAsync(TToken token, CancellationToken cancellationToken)
{
@ -864,6 +880,19 @@ namespace OpenIddict.EntityFrameworkCore
return default;
}
/// <inheritdoc/>
public virtual ValueTask SetRedemptionDateAsync(TToken token, DateTimeOffset? date, CancellationToken cancellationToken)
{
if (token is null)
{
throw new ArgumentNullException(nameof(token));
}
token.RedemptionDate = date?.UtcDateTime;
return default;
}
/// <inheritdoc/>
public virtual ValueTask SetReferenceIdAsync(TToken token, string? identifier, CancellationToken cancellationToken)
{

6
src/OpenIddict.MongoDb.Models/OpenIddictMongoDbToken.cs

@ -67,6 +67,12 @@ namespace OpenIddict.MongoDb.Models
[BsonElement("properties"), BsonIgnoreIfNull]
public virtual BsonDocument? Properties { get; set; }
/// <summary>
/// Gets or sets the UTC redemption date of the current token.
/// </summary>
[BsonElement("redemption_date"), BsonIgnoreIfNull]
public virtual DateTime? RedemptionDate { get; set; }
/// <summary>
/// Gets or sets the reference identifier associated
/// with the current token, if applicable.

29
src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs

@ -440,6 +440,22 @@ namespace OpenIddict.MongoDb
return new ValueTask<ImmutableDictionary<string, JsonElement>>(builder.ToImmutable());
}
/// <inheritdoc/>
public virtual ValueTask<DateTimeOffset?> GetRedemptionDateAsync(TToken token, CancellationToken cancellationToken)
{
if (token is null)
{
throw new ArgumentNullException(nameof(token));
}
if (token.RedemptionDate is null)
{
return new ValueTask<DateTimeOffset?>(result: null);
}
return new ValueTask<DateTimeOffset?>(DateTime.SpecifyKind(token.RedemptionDate.Value, DateTimeKind.Utc));
}
/// <inheritdoc/>
public virtual ValueTask<string?> GetReferenceIdAsync(TToken token, CancellationToken cancellationToken)
{
@ -719,6 +735,19 @@ namespace OpenIddict.MongoDb
return default;
}
/// <inheritdoc/>
public virtual ValueTask SetRedemptionDateAsync(TToken token, DateTimeOffset? date, CancellationToken cancellationToken)
{
if (token is null)
{
throw new ArgumentNullException(nameof(token));
}
token.RedemptionDate = date?.UtcDateTime;
return default;
}
/// <inheritdoc/>
public virtual ValueTask SetReferenceIdAsync(TToken token, string? identifier, CancellationToken cancellationToken)
{

2
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs

@ -52,7 +52,7 @@ namespace Microsoft.Extensions.DependencyInjection
}
/// <summary>
/// Disables the transport security requirement (HTTPS) during development.
/// Disables the transport security requirement (HTTPS).
/// </summary>
/// <returns>The <see cref="OpenIddictServerAspNetCoreBuilder"/>.</returns>
public OpenIddictServerAspNetCoreBuilder DisableTransportSecurityRequirement()

2
src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs

@ -52,7 +52,7 @@ namespace Microsoft.Extensions.DependencyInjection
}
/// <summary>
/// Disables the transport security requirement (HTTPS) during development.
/// Disables the transport security requirement (HTTPS).
/// </summary>
/// <returns>The <see cref="OpenIddictServerOwinBuilder"/>.</returns>
public OpenIddictServerOwinBuilder DisableTransportSecurityRequirement()

28
src/OpenIddict.Server/OpenIddictServerBuilder.cs

@ -1630,6 +1630,16 @@ namespace Microsoft.Extensions.DependencyInjection
public OpenIddictServerBuilder DisableAuthorizationStorage()
=> Configure(options => options.DisableAuthorizationStorage = true);
/// <summary>
/// Configures OpenIddict to disable rolling refresh tokens so
/// that refresh tokens used in a token request are not marked
/// as redeemed and can still be used until they expire. Disabling
/// rolling refresh tokens is NOT recommended, for security reasons.
/// </summary>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder DisableRollingRefreshTokens()
=> Configure(options => options.DisableRollingRefreshTokens = true);
/// <summary>
/// Allows processing authorization and token requests that specify scopes that have not
/// been registered using <see cref="RegisterScopes(string[])"/> or the scope manager.
@ -1804,6 +1814,15 @@ namespace Microsoft.Extensions.DependencyInjection
public OpenIddictServerBuilder SetRefreshTokenLifetime(TimeSpan? lifetime)
=> Configure(options => options.RefreshTokenLifetime = lifetime);
/// <summary>
/// Sets the refresh token reuse leeway, during which rolling refresh tokens marked
/// as redeemed can still be used to make concurrent refresh token requests.
/// </summary>
/// <param name="leeway">The refresh token reuse interval.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder SetRefreshTokenReuseLeeway(TimeSpan? leeway)
=> Configure(options => options.RefreshTokenReuseLeeway = leeway);
/// <summary>
/// Sets the user code lifetime, after which they'll no longer be considered valid.
/// Using short-lived device codes is strongly recommended.
@ -1852,15 +1871,6 @@ namespace Microsoft.Extensions.DependencyInjection
public OpenIddictServerBuilder UseReferenceRefreshTokens()
=> Configure(options => options.UseReferenceRefreshTokens = true);
/// <summary>
/// Configures OpenIddict to use rolling refresh tokens. When this option is enabled,
/// a new refresh token is always issued for each refresh token request (and the previous
/// one is automatically revoked unless token storage was explicitly disabled).
/// </summary>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder UseRollingRefreshTokens()
=> Configure(options => options.UseRollingRefreshTokens = true);
/// <inheritdoc/>
[EditorBrowsable(EditorBrowsableState.Never)]
public override bool Equals(object? obj) => base.Equals(obj);

34
src/OpenIddict.Server/OpenIddictServerConfiguration.cs

@ -38,16 +38,16 @@ namespace OpenIddict.Server
if (options.EnableDegradedMode)
{
// Explicitly disable all the features that are implicitly excluded when the degraded mode is active.
options.DisableAuthorizationStorage = options.DisableTokenStorage = true;
options.DisableAuthorizationStorage = options.DisableTokenStorage = options.DisableRollingRefreshTokens = true;
options.IgnoreEndpointPermissions = options.IgnoreGrantTypePermissions = true;
options.IgnoreResponseTypePermissions = options.IgnoreScopePermissions = true;
options.UseReferenceAccessTokens = options.UseReferenceRefreshTokens = false;
}
// When the degraded mode is enabled (and the token storage disabled), OpenIddict is not able to dynamically
// update the expiration date of a token. As such, either rolling tokens MUST be enabled or sliding token
// expiration MUST be disabled to always issue new refresh tokens with the same fixed expiration date.
// By default, OpenIddict will automatically force the rolling tokens option when using the degraded mode.
options.UseRollingRefreshTokens |= !options.UseRollingRefreshTokens && !options.DisableSlidingRefreshTokenExpiration;
if (options.DisableTokenStorage)
{
// Explicitly disable rolling refresh tokens token stroage is disabled.
options.DisableRollingRefreshTokens = true;
}
if (options.JsonWebTokenHandler is null)
@ -112,17 +112,10 @@ namespace OpenIddict.Server
}
}
if (options.DisableTokenStorage)
// Ensure reference tokens support was not enabled when token storage is disabled.
if (options.DisableTokenStorage && (options.UseReferenceAccessTokens || options.UseReferenceRefreshTokens))
{
if (options.UseReferenceAccessTokens || options.UseReferenceRefreshTokens)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0083));
}
if (!options.DisableSlidingRefreshTokenExpiration && !options.UseRollingRefreshTokens)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0084));
}
throw new InvalidOperationException(SR.GetResourceString(SR.ID0083));
}
if (options.EncryptionCredentials.Count == 0)
@ -240,15 +233,12 @@ namespace OpenIddict.Server
options.EncryptionCredentials.Sort((left, right) => Compare(left.Key, right.Key));
options.SigningCredentials.Sort((left, right) => Compare(left.Key, right.Key));
// Generate a key identifier for the encryption/signing keys that don't already have one.
foreach (var key in options.EncryptionCredentials
.Select(credentials => credentials.Key)
.Concat(options.SigningCredentials.Select(credentials => credentials.Key)))
.Concat(options.SigningCredentials.Select(credentials => credentials.Key))
.Where(key => string.IsNullOrEmpty(key.KeyId)))
{
if (!string.IsNullOrEmpty(key.KeyId))
{
continue;
}
key.KeyId = GetKeyIdentifier(key);
}

2
src/OpenIddict.Server/OpenIddictServerExtensions.cs

@ -69,8 +69,6 @@ namespace Microsoft.Extensions.DependencyInjection
builder.Services.TryAddSingleton<RequireRefreshTokenGenerated>();
builder.Services.TryAddSingleton<RequireResponseTypePermissionsEnabled>();
builder.Services.TryAddSingleton<RequireRevocationRequest>();
builder.Services.TryAddSingleton<RequireRollingTokensDisabled>();
builder.Services.TryAddSingleton<RequireRollingRefreshTokensEnabled>();
builder.Services.TryAddSingleton<RequireSlidingRefreshTokenExpirationEnabled>();
builder.Services.TryAddSingleton<RequireScopePermissionsEnabled>();
builder.Services.TryAddSingleton<RequireScopeValidationEnabled>();

32
src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs

@ -350,38 +350,6 @@ namespace OpenIddict.Server
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if rolling tokens were enabled.
/// </summary>
public class RequireRollingTokensDisabled : IOpenIddictServerHandlerFilter<BaseContext>
{
public ValueTask<bool> IsActiveAsync(BaseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(!context.Options.UseRollingRefreshTokens);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if rolling refresh tokens were not enabled.
/// </summary>
public class RequireRollingRefreshTokensEnabled : IOpenIddictServerHandlerFilter<BaseContext>
{
public ValueTask<bool> IsActiveAsync(BaseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.Options.UseRollingRefreshTokens);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if scope permissions were disabled.
/// </summary>

314
src/OpenIddict.Server/OpenIddictServerHandlers.cs

@ -74,8 +74,6 @@ namespace OpenIddict.Server
PrepareUserCodePrincipal.Descriptor,
RedeemTokenEntry.Descriptor,
RevokeExistingTokenEntries.Descriptor,
ExtendRefreshTokenEntry.Descriptor,
CreateAccessTokenEntry.Descriptor,
GenerateIdentityModelAccessToken.Descriptor,
@ -859,42 +857,45 @@ namespace OpenIddict.Server
return;
}
if (context.EndpointType == OpenIddictServerEndpointType.Token &&
(context.Request.IsAuthorizationCodeGrantType() ||
context.Request.IsDeviceCodeGrantType() ||
context.Request.IsRefreshTokenGrantType()))
if (context.EndpointType == OpenIddictServerEndpointType.Token && (context.Request.IsAuthorizationCodeGrantType() ||
context.Request.IsDeviceCodeGrantType() ||
context.Request.IsRefreshTokenGrantType()))
{
// If the authorization code/device code/refresh token is already marked as redeemed, this may indicate
// that it was compromised. In this case, revoke the entire chain of tokens associated with the authorization.
// Special logic is used to avoid revoking refresh tokens already marked as redeemed to allow for a small leeway.
// Note: the authorization itself is not revoked to allow the legitimate client to start a new flow.
// See https://tools.ietf.org/html/rfc6749#section-10.5 for more information.
if (await _tokenManager.HasStatusAsync(token, Statuses.Redeemed))
{
// First, mark the redeemed token submitted by the client as revoked.
await _tokenManager.TryRevokeAsync(token);
// Then, try to revoke the token entries associated with the authorization.
await TryRevokeChainAsync(context.Principal.GetAuthorizationId());
context.Logger.LogError(SR.GetResourceString(SR.ID6002), identifier);
if (!context.Request.IsRefreshTokenGrantType() || !await IsReusableAsync(token))
{
context.Logger.LogError(SR.GetResourceString(SR.ID6002), identifier);
context.Reject(
error: context.EndpointType switch
{
OpenIddictServerEndpointType.Token => Errors.InvalidGrant,
_ => Errors.InvalidToken
},
description: context.EndpointType switch
{
OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType()
=> context.Localizer[SR.ID2010],
OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType()
=> context.Localizer[SR.ID2011],
OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType()
=> context.Localizer[SR.ID2012],
context.Reject(
error: context.EndpointType switch
{
OpenIddictServerEndpointType.Token => Errors.InvalidGrant,
_ => Errors.InvalidToken
},
description: context.EndpointType switch
{
OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType()
=> context.Localizer[SR.ID2010],
OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType()
=> context.Localizer[SR.ID2011],
OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType()
=> context.Localizer[SR.ID2012],
_ => context.Localizer[SR.ID2013]
});
// Revoke all the token entries associated with the authorization.
await TryRevokeChainAsync(await _tokenManager.GetAuthorizationIdAsync(token));
_ => context.Localizer[SR.ID2013]
});
return;
}
return;
}
@ -954,10 +955,28 @@ namespace OpenIddict.Server
// Restore the creation/expiration dates/identifiers from the token entry metadata.
context.Principal.SetCreationDate(await _tokenManager.GetCreationDateAsync(token))
.SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token))
.SetAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token))
.SetTokenId(await _tokenManager.GetIdAsync(token))
.SetTokenType(await _tokenManager.GetTypeAsync(token));
.SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token))
.SetAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token))
.SetTokenId(await _tokenManager.GetIdAsync(token))
.SetTokenType(await _tokenManager.GetTypeAsync(token));
async ValueTask<bool> IsReusableAsync(object token)
{
// If the reuse leeway was set to null, return false to indicate
// that the refresh token is already redeemed and cannot be reused.
if (context.Options.RefreshTokenReuseLeeway is null)
{
return false;
}
var date = await _tokenManager.GetRedemptionDateAsync(token);
if (date is null || DateTimeOffset.UtcNow < date + context.Options.RefreshTokenReuseLeeway)
{
return true;
}
return false;
}
async ValueTask TryRevokeChainAsync(string? identifier)
{
@ -966,15 +985,10 @@ namespace OpenIddict.Server
return;
}
// Revoke all the token entries associated with the authorization,
// including the redeemed token that was used in the token request.
await foreach (var token in _tokenManager.FindByAuthorizationIdAsync(identifier))
{
// Don't change the status of the token used in the token request.
if (string.Equals(context.Principal.GetTokenId(),
await _tokenManager.GetIdAsync(token), StringComparison.Ordinal))
{
continue;
}
await _tokenManager.TryRevokeAsync(token);
}
}
@ -1138,16 +1152,16 @@ namespace OpenIddict.Server
throw new ArgumentNullException(nameof(context));
}
switch (context.EndpointType)
return context.EndpointType switch
{
case OpenIddictServerEndpointType.Authorization:
case OpenIddictServerEndpointType.Token:
case OpenIddictServerEndpointType.Userinfo:
case OpenIddictServerEndpointType.Verification:
return default;
OpenIddictServerEndpointType.Authorization or
OpenIddictServerEndpointType.Token or
OpenIddictServerEndpointType.Userinfo or
OpenIddictServerEndpointType.Verification
=> default,
default: throw new InvalidOperationException(SR.GetResourceString(SR.ID0006));
}
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0006)),
};
}
}
@ -1652,17 +1666,10 @@ namespace OpenIddict.Server
(context.GenerateRefreshToken, context.IncludeRefreshToken) = context.EndpointType switch
{
// For token requests, never generate a refresh token if the offline_access scope was not granted.
OpenIddictServerEndpointType.Token when !context.Principal.HasScope(Scopes.OfflineAccess)
=> (false, false),
// For grant_type=refresh_token token requests, only generate
// and return a refresh token if rolling tokens are enabled.
OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() &&
context.Options.UseRollingRefreshTokens => (true, true),
// For token requests that don't meet the previous criteria, allow a refresh token to be returned.
OpenIddictServerEndpointType.Token when !context.Request.IsRefreshTokenGrantType() => (true, true),
// For token requests, allow a refresh token to be returned
// if the special offline_access protocol scope was granted.
OpenIddictServerEndpointType.Token when context.Principal.HasScope(Scopes.OfflineAccess)
=> (true, true),
_ => (false, false)
};
@ -2123,7 +2130,8 @@ namespace OpenIddict.Server
// When sliding expiration is disabled, the expiration date of generated refresh tokens is fixed
// and must exactly match the expiration date of the refresh token used in the token request.
if (context.EndpointType == OpenIddictServerEndpointType.Token &&
context.Request.IsRefreshTokenGrantType() && !context.Options.DisableSlidingRefreshTokenExpiration)
context.Request.IsRefreshTokenGrantType() &&
context.Options.DisableSlidingRefreshTokenExpiration)
{
var notification = context.Transaction.GetProperty<ProcessAuthenticationContext>(
typeof(ProcessAuthenticationContext).FullName!) ??
@ -2367,25 +2375,16 @@ namespace OpenIddict.Server
throw new ArgumentNullException(nameof(context));
}
if (context.EndpointType != OpenIddictServerEndpointType.Token &&
context.EndpointType != OpenIddictServerEndpointType.Verification)
{
return;
}
if (context.EndpointType == OpenIddictServerEndpointType.Token)
switch (context.EndpointType)
{
if (!context.Request.IsAuthorizationCodeGrantType() &&
!context.Request.IsDeviceCodeGrantType() &&
!context.Request.IsRefreshTokenGrantType())
{
return;
}
case OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType():
case OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType():
case OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() &&
!context.Options.DisableRollingRefreshTokens:
case OpenIddictServerEndpointType.Verification:
break;
if (context.Request.IsRefreshTokenGrantType() && !context.Options.UseRollingRefreshTokens)
{
return;
}
default: return;
}
Debug.Assert(context.Principal is not null, SR.GetResourceString(SR.ID4006));
@ -2400,165 +2399,10 @@ namespace OpenIddict.Server
// If rolling tokens are enabled or if the request is a a code or device code token request
// or a user code verification request, mark the token as redeemed to prevent future reuses.
// If the operation fails, return an error indicating the code/token is no longer valid.
// See https://tools.ietf.org/html/rfc6749#section-6 for more information.
var token = await _tokenManager.FindByIdAsync(identifier);
if (token is null || !await _tokenManager.TryRedeemAsync(token))
{
context.Reject(
error: Errors.InvalidGrant,
description: context.EndpointType switch
{
OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType()
=> context.Localizer[SR.ID2016],
OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType()
=> context.Localizer[SR.ID2017],
OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType()
=> context.Localizer[SR.ID2018],
OpenIddictServerEndpointType.Verification
=> context.Localizer[SR.ID2026],
_ => context.Localizer[SR.ID2019]
});
return;
}
}
}
/// <summary>
/// Contains the logic responsible of revoking all the tokens that were previously issued.
/// Note: this handler is not used when the degraded mode is enabled.
/// </summary>
public class RevokeExistingTokenEntries : IOpenIddictServerHandler<ProcessSignInContext>
{
private readonly IOpenIddictTokenManager _tokenManager;
public RevokeExistingTokenEntries() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
public RevokeExistingTokenEntries(IOpenIddictTokenManager tokenManager)
=> _tokenManager = tokenManager;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignInContext>()
.AddFilter<RequireDegradedModeDisabled>()
.AddFilter<RequireTokenStorageEnabled>()
.AddFilter<RequireRollingRefreshTokensEnabled>()
.UseScopedHandler<RevokeExistingTokenEntries>()
.SetOrder(RedeemTokenEntry.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ProcessSignInContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.EndpointType != OpenIddictServerEndpointType.Token || !context.Request.IsRefreshTokenGrantType())
{
return;
}
Debug.Assert(context.Principal is not null, SR.GetResourceString(SR.ID4006));
// When rolling tokens are enabled, try to revoke all the previously issued tokens
// associated with the authorization if the request is a refresh_token request.
// If the operation fails, silently ignore the error and keep processing the request:
// this may indicate that one of the revoked tokens was modified by a concurrent request.
var identifier = context.Principal.GetAuthorizationId();
if (string.IsNullOrEmpty(identifier))
{
return;
}
await foreach (var token in _tokenManager.FindByAuthorizationIdAsync(identifier))
{
// Don't change the status of the token used in the token request.
if (string.Equals(context.Principal.GetTokenId(),
await _tokenManager.GetIdAsync(token), StringComparison.Ordinal))
{
continue;
}
await _tokenManager.TryRevokeAsync(token);
}
}
}
/// <summary>
/// Contains the logic responsible of extending the lifetime of the refresh token entry.
/// Note: this handler is not used when the degraded mode is enabled.
/// </summary>
public class ExtendRefreshTokenEntry : IOpenIddictServerHandler<ProcessSignInContext>
{
private readonly IOpenIddictTokenManager _tokenManager;
public ExtendRefreshTokenEntry() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
public ExtendRefreshTokenEntry(IOpenIddictTokenManager tokenManager)
=> _tokenManager = tokenManager;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignInContext>()
.AddFilter<RequireDegradedModeDisabled>()
.AddFilter<RequireTokenStorageEnabled>()
.AddFilter<RequireSlidingRefreshTokenExpirationEnabled>()
.AddFilter<RequireRollingTokensDisabled>()
.UseScopedHandler<ExtendRefreshTokenEntry>()
.SetOrder(RevokeExistingTokenEntries.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ProcessSignInContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.EndpointType != OpenIddictServerEndpointType.Token || !context.Request.IsRefreshTokenGrantType())
{
return;
}
Debug.Assert(context.Principal is not null, SR.GetResourceString(SR.ID4006));
// Extract the token identifier from the authentication principal.
// If no token identifier can be found, this indicates that the token has no backing database entry.
var identifier = context.Principal.GetTokenId();
if (string.IsNullOrEmpty(identifier))
{
return;
}
var token = await _tokenManager.FindByIdAsync(identifier);
if (token is null)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0265));
}
// Compute the new expiration date of the refresh token and update the token entry.
var lifetime = context.Principal.GetRefreshTokenLifetime() ?? context.Options.RefreshTokenLifetime;
if (lifetime.HasValue)
{
await _tokenManager.TryExtendAsync(token, DateTimeOffset.UtcNow + lifetime.Value);
}
else
if (token is not null)
{
await _tokenManager.TryExtendAsync(token, date: null);
await _tokenManager.TryRedeemAsync(token);
}
}
}
@ -2591,7 +2435,7 @@ namespace OpenIddict.Server
.AddFilter<RequireTokenStorageEnabled>()
.AddFilter<RequireAccessTokenGenerated>()
.UseScopedHandler<CreateAccessTokenEntry>()
.SetOrder(ExtendRefreshTokenEntry.Descriptor.Order + 1_000)
.SetOrder(RedeemTokenEntry.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();

24
src/OpenIddict.Server/OpenIddictServerOptions.cs

@ -214,6 +214,12 @@ namespace OpenIddict.Server
/// </summary>
public TimeSpan? RefreshTokenLifetime { get; set; } = TimeSpan.FromDays(14);
/// <summary>
/// Gets or sets the period of time rolling refresh tokens marked as redeemed can still be
/// used to make concurrent refresh token requests. The default value is 15 seconds.
/// </summary>
public TimeSpan? RefreshTokenReuseLeeway { get; set; } = TimeSpan.FromSeconds(15);
/// <summary>
/// Gets or sets the period of time user codes remain valid after being issued. The default value is 10 minutes.
/// The client application is expected to start a whole new authentication flow after the user code has expired.
@ -274,6 +280,14 @@ namespace OpenIddict.Server
/// </summary>
public bool DisableAuthorizationStorage { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether rolling tokens are disabled.
/// When disabled, refresh tokens used in a token request are not marked
/// as redeemed and can still be used until they expire. Disabling
/// rolling refresh tokens is NOT recommended, for security reasons.
/// </summary>
public bool DisableRollingRefreshTokens { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether sliding expiration is disabled
/// for refresh tokens. When this option is set to <see langword="true"/>,
@ -379,15 +393,5 @@ namespace OpenIddict.Server
/// that provides additional protection against token leakage.
/// </summary>
public bool UseReferenceRefreshTokens { get; set; }
/// <summary>
/// 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
/// unless token storage was explicitly disabled in the options).
/// </summary>
public bool UseRollingRefreshTokens { get; set; }
}
}

364
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs

@ -2368,10 +2368,11 @@ namespace OpenIddict.Server.IntegrationTests
Mock.Get(manager).Verify(manager => manager.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(manager => manager.GetRedemptionDateAsync(token, It.IsAny<CancellationToken>()), Times.Never());
}
[Fact]
public async Task HandleTokenRequest_RequestIsRejectedWhenRefreshTokenIsAlreadyRedeemed()
public async Task HandleTokenRequest_RequestIsRejectedWhenRefreshTokenIsAlreadyRedeemedAndLeewayIsNull()
{
// Arrange
var token = new OpenIddictToken();
@ -2386,10 +2387,15 @@ namespace OpenIddict.Server.IntegrationTests
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.GetRedemptionDateAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(DateTimeOffset.UtcNow);
});
await using var server = await CreateServerAsync(options =>
{
options.SetRefreshTokenReuseLeeway(leeway: null);
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
builder.UseInlineHandler(context =>
@ -2435,6 +2441,155 @@ namespace OpenIddict.Server.IntegrationTests
Mock.Get(manager).Verify(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(manager => manager.GetRedemptionDateAsync(token, It.IsAny<CancellationToken>()), Times.Never());
}
[Fact]
public async Task HandleTokenRequest_RequestIsRejectedWhenRefreshTokenIsAlreadyRedeemedAndCannotBeReused()
{
// Arrange
var token = new OpenIddictToken();
var manager = CreateTokenManager(mock =>
{
mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
mock.Setup(manager => manager.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.GetRedemptionDateAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(DateTimeOffset.UtcNow - TimeSpan.FromMinutes(1));
});
await using var server = await CreateServerAsync(options =>
{
options.SetRefreshTokenReuseLeeway(TimeSpan.FromSeconds(5));
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("8xLOxBtZp8", context.Token);
Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetTokenType(TokenTypeHints.RefreshToken)
.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103")
.SetClaim(Claims.Subject, "Bob le Bricoleur");
return default;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
options.AddEventHandler<HandleTokenRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetClaim(Claims.Subject, "Bob le Magnifique");
return default;
}));
options.Services.AddSingleton(manager);
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
GrantType = GrantTypes.RefreshToken,
RefreshToken = "8xLOxBtZp8"
});
// Assert
Assert.Equal(Errors.InvalidGrant, response.Error);
Assert.Equal(SR.GetResourceString(SR.ID2012), response.ErrorDescription);
Mock.Get(manager).Verify(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(manager => manager.GetRedemptionDateAsync(token, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task HandleTokenRequest_RequestIsValidatedWhenRefreshTokenIsAlreadyRedeemedAndCanBeReused()
{
// Arrange
var token = new OpenIddictToken();
var manager = CreateTokenManager(mock =>
{
mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
mock.Setup(manager => manager.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.GetRedemptionDateAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(DateTimeOffset.UtcNow - TimeSpan.FromMinutes(1));
mock.Setup(manager => manager.CreateAsync(It.IsAny<OpenIddictTokenDescriptor>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new OpenIddictToken());
});
await using var server = await CreateServerAsync(options =>
{
options.SetRefreshTokenReuseLeeway(TimeSpan.FromMinutes(5));
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("8xLOxBtZp8", context.Token);
Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetTokenType(TokenTypeHints.RefreshToken)
.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103")
.SetClaim(Claims.Subject, "Bob le Bricoleur");
return default;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
options.AddEventHandler<HandleTokenRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetClaim(Claims.Subject, "Bob le Magnifique");
return default;
}));
options.Services.AddSingleton(manager);
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
GrantType = GrantTypes.RefreshToken,
RefreshToken = "8xLOxBtZp8"
});
// Assert
Assert.NotNull(response.AccessToken);
Mock.Get(manager).Verify(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(manager => manager.GetRedemptionDateAsync(token, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
@ -2538,7 +2693,99 @@ namespace OpenIddict.Server.IntegrationTests
}
[Fact]
public async Task HandleTokenRequest_RevokesTokensWhenRefreshTokenIsAlreadyRedeemed()
public async Task HandleTokenRequest_RevokesTokensWhenRefreshTokenIsAlreadyRedeemedAndLeewayIsNull()
{
// Arrange
var tokens = ImmutableArray.Create(
new OpenIddictToken(),
new OpenIddictToken(),
new OpenIddictToken());
var manager = CreateTokenManager(mock =>
{
mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(tokens[0]);
mock.Setup(manager => manager.GetIdAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
mock.Setup(manager => manager.GetIdAsync(tokens[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("47468A64-C9A7-49C7-939C-19CC0F5DD166");
mock.Setup(manager => manager.GetIdAsync(tokens[2], It.IsAny<CancellationToken>()))
.ReturnsAsync("3BEA7A94-5ADA-49AF-9F41-8AB6156E31A8");
mock.Setup(manager => manager.GetAuthorizationIdAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0");
mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.GetRedemptionDateAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync(DateTimeOffset.UtcNow);
mock.Setup(manager => manager.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny<CancellationToken>()))
.Returns(tokens.ToAsyncEnumerable());
});
await using var server = await CreateServerAsync(options =>
{
options.SetRefreshTokenReuseLeeway(leeway: null);
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("8xLOxBtZp8", context.Token);
Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetTokenType(TokenTypeHints.RefreshToken)
.SetPresenters("Fabrikam")
.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103")
.SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")
.SetClaim(Claims.Subject, "Bob le Bricoleur");
return default;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
options.AddEventHandler<HandleTokenRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetClaim(Claims.Subject, "Bob le Magnifique");
return default;
}));
options.Services.AddSingleton(manager);
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
GrantType = GrantTypes.RefreshToken,
RefreshToken = "8xLOxBtZp8"
});
// Assert
Assert.Equal(Errors.InvalidGrant, response.Error);
Assert.Equal(SR.GetResourceString(SR.ID2012), response.ErrorDescription);
Mock.Get(manager).Verify(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasStatusAsync(tokens[0], Statuses.Redeemed, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[0], It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[1], It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[2], It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task HandleTokenRequest_RevokesTokensWhenRefreshTokenIsAlreadyRedeemedAndCannotBeReused()
{
// Arrange
var tokens = ImmutableArray.Create(
@ -2566,12 +2813,17 @@ namespace OpenIddict.Server.IntegrationTests
mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.GetRedemptionDateAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync(DateTimeOffset.UtcNow - TimeSpan.FromMinutes(1));
mock.Setup(manager => manager.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny<CancellationToken>()))
.Returns(tokens.ToAsyncEnumerable());
});
await using var server = await CreateServerAsync(options =>
{
options.SetRefreshTokenReuseLeeway(TimeSpan.FromSeconds(5));
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
builder.UseInlineHandler(context =>
@ -2624,6 +2876,110 @@ namespace OpenIddict.Server.IntegrationTests
Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[2], It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task HandleTokenRequest_DoesNotRevokeTokensWhenRefreshTokenIsAlreadyRedeemedAndCanBeReused()
{
// Arrange
var tokens = ImmutableArray.Create(
new OpenIddictToken(),
new OpenIddictToken(),
new OpenIddictToken());
var manager = CreateTokenManager(mock =>
{
mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(tokens[0]);
mock.Setup(manager => manager.GetIdAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
mock.Setup(manager => manager.GetIdAsync(tokens[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("47468A64-C9A7-49C7-939C-19CC0F5DD166");
mock.Setup(manager => manager.GetIdAsync(tokens[2], It.IsAny<CancellationToken>()))
.ReturnsAsync("3BEA7A94-5ADA-49AF-9F41-8AB6156E31A8");
mock.Setup(manager => manager.GetAuthorizationIdAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0");
mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.GetRedemptionDateAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync(DateTimeOffset.UtcNow - TimeSpan.FromMinutes(1));
mock.Setup(manager => manager.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny<CancellationToken>()))
.Returns(tokens.ToAsyncEnumerable());
mock.Setup(manager => manager.CreateAsync(It.IsAny<OpenIddictTokenDescriptor>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new OpenIddictToken());
});
await using var server = await CreateServerAsync(options =>
{
options.SetRefreshTokenReuseLeeway(TimeSpan.FromMinutes(5));
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("8xLOxBtZp8", context.Token);
Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetTokenType(TokenTypeHints.RefreshToken)
.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103")
.SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")
.SetClaim(Claims.Subject, "Bob le Bricoleur");
return default;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
options.AddEventHandler<HandleTokenRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetClaim(Claims.Subject, "Bob le Magnifique");
return default;
}));
options.Services.AddSingleton(manager);
options.Services.AddSingleton(CreateAuthorizationManager(mock =>
{
var authorization = new OpenIddictAuthorization();
mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny<CancellationToken>()))
.ReturnsAsync(authorization);
mock.Setup(manager => manager.HasStatusAsync(authorization, Statuses.Valid, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
}));
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
GrantType = GrantTypes.RefreshToken,
RefreshToken = "8xLOxBtZp8"
});
// Assert
Assert.NotNull(response.AccessToken);
Mock.Get(manager).Verify(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasStatusAsync(tokens[0], Statuses.Redeemed, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[0], It.IsAny<CancellationToken>()), Times.Never());
Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[1], It.IsAny<CancellationToken>()), Times.Never());
Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[2], It.IsAny<CancellationToken>()), Times.Never());
}
[Fact]
public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationCodeIsInvalid()
{
@ -2896,6 +3252,8 @@ namespace OpenIddict.Server.IntegrationTests
await using var server = await CreateServerAsync(options =>
{
options.DisableRollingRefreshTokens();
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
builder.UseInlineHandler(context =>
@ -3357,6 +3715,8 @@ namespace OpenIddict.Server.IntegrationTests
await using var server = await CreateServerAsync(options =>
{
options.DisableRollingRefreshTokens();
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
builder.UseInlineHandler(context =>

717
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs

@ -8,7 +8,6 @@
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
@ -2892,7 +2891,6 @@ namespace OpenIddict.Server.IntegrationTests
await using var server = await CreateServerAsync(options =>
{
options.EnableDegradedMode();
options.UseRollingRefreshTokens();
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
@ -2938,13 +2936,12 @@ namespace OpenIddict.Server.IntegrationTests
}
[Fact]
public async Task ProcessSignIn_RefreshTokenIsIssuedForAuthorizationCodeRequestsWhenRollingTokensAreEnabled()
public async Task ProcessSignIn_RefreshTokenIsIssuedForAuthorizationCodeRequests()
{
// Arrange
await using var server = await CreateServerAsync(options =>
{
options.EnableDegradedMode();
options.UseRollingRefreshTokens();
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
@ -2982,13 +2979,12 @@ namespace OpenIddict.Server.IntegrationTests
}
[Fact]
public async Task ProcessSignIn_RefreshTokenIsAlwaysIssuedWhenRollingTokensAreEnabled()
public async Task ProcessSignIn_RefreshTokenIsAlwaysIssued()
{
// Arrange
await using var server = await CreateServerAsync(options =>
{
options.EnableDegradedMode();
options.UseRollingRefreshTokens();
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
@ -3022,47 +3018,6 @@ namespace OpenIddict.Server.IntegrationTests
Assert.NotNull(response.RefreshToken);
}
[Fact]
public async Task ProcessSignIn_RefreshTokenIsNotIssuedWhenRollingTokensAreDisabled()
{
// Arrange
await using var server = await CreateServerAsync(options =>
{
options.EnableDegradedMode();
options.DisableSlidingRefreshTokenExpiration();
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("8xLOxBtZp8", context.Token);
Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetTokenType(TokenTypeHints.RefreshToken)
.SetScopes(Scopes.OpenId, Scopes.OfflineAccess)
.SetClaim(Claims.Subject, "Bob le Bricoleur");
return default;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
GrantType = GrantTypes.RefreshToken,
RefreshToken = "8xLOxBtZp8"
});
// Assert
Assert.Null(response.RefreshToken);
}
[Fact]
public async Task ProcessSignIn_AuthorizationCodeIsAutomaticallyRedeemed()
{
@ -3138,84 +3093,6 @@ namespace OpenIddict.Server.IntegrationTests
Mock.Get(manager).Verify(manager => manager.TryRedeemAsync(token, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task ProcessSignIn_ReturnsErrorResponseWhenRedeemingAuthorizationCodeFails()
{
// Arrange
var token = new OpenIddictToken();
var manager = CreateTokenManager(mock =>
{
mock.Setup(manager => manager.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
mock.Setup(manager => manager.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56");
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.TryRedeemAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
});
await using var server = await CreateServerAsync(options =>
{
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token);
Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetTokenType(TokenTypeHints.AuthorizationCode)
.SetPresenters("Fabrikam")
.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56")
.SetClaim(Claims.Subject, "Bob le Bricoleur");
return default;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
options.Services.AddSingleton(CreateApplicationManager(mock =>
{
var application = new OpenIddictApplication();
mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
}));
options.Services.AddSingleton(manager);
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
ClientId = "Fabrikam",
Code = "SplxlOBeZQQYbYS6WxSbIA",
GrantType = GrantTypes.AuthorizationCode,
RedirectUri = "http://www.fabrikam.com/path"
});
// Assert
Assert.Equal(Errors.InvalidGrant, response.Error);
Assert.Equal(SR.GetResourceString(SR.ID2016), response.ErrorDescription);
Mock.Get(manager).Verify(manager => manager.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.TryRedeemAsync(token, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task ProcessSignIn_RefreshTokenIsAutomaticallyRedeemedWhenRollingTokensAreEnabled()
{
@ -3245,7 +3122,6 @@ namespace OpenIddict.Server.IntegrationTests
await using var server = await CreateServerAsync(options =>
{
options.UseRollingRefreshTokens();
options.DisableAuthorizationStorage();
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
@ -3286,74 +3162,6 @@ namespace OpenIddict.Server.IntegrationTests
Mock.Get(manager).Verify(manager => manager.TryRedeemAsync(token, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task ProcessSignIn_ReturnsErrorResponseWhenRedeemingRefreshTokenFails()
{
// Arrange
var token = new OpenIddictToken();
var manager = CreateTokenManager(mock =>
{
mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
mock.Setup(manager => manager.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.TryRedeemAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
});
await using var server = await CreateServerAsync(options =>
{
options.UseRollingRefreshTokens();
options.DisableAuthorizationStorage();
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("8xLOxBtZp8", context.Token);
Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetTokenType(TokenTypeHints.RefreshToken)
.SetScopes(Scopes.OpenId, Scopes.OfflineAccess)
.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103")
.SetClaim(Claims.Subject, "Bob le Bricoleur");
return default;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
options.Services.AddSingleton(manager);
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
GrantType = GrantTypes.RefreshToken,
RefreshToken = "8xLOxBtZp8"
});
// Assert
Assert.Equal(Errors.InvalidGrant, response.Error);
Assert.Equal(SR.GetResourceString(SR.ID2018), response.ErrorDescription);
Mock.Get(manager).Verify(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.TryRedeemAsync(token, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task ProcessSignIn_RefreshTokenIsNotRedeemedWhenRollingTokensAreDisabled()
{
@ -3377,91 +3185,8 @@ namespace OpenIddict.Server.IntegrationTests
await using var server = await CreateServerAsync(options =>
{
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("8xLOxBtZp8", context.Token);
Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetTokenType(TokenTypeHints.RefreshToken)
.SetScopes(Scopes.OpenId, Scopes.OfflineAccess)
.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103")
.SetClaim(Claims.Subject, "Bob le Bricoleur");
return default;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
options.Services.AddSingleton(manager);
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
GrantType = GrantTypes.RefreshToken,
RefreshToken = "8xLOxBtZp8"
});
// Assert
Assert.Null(response.RefreshToken);
Mock.Get(manager).Verify(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.TryRedeemAsync(token, It.IsAny<CancellationToken>()), Times.Never());
}
[Fact]
public async Task ProcessSignIn_PreviousTokensAreAutomaticallyRevokedWhenRollingTokensAreEnabled()
{
// Arrange
var tokens = new[]
{
new OpenIddictToken(),
new OpenIddictToken(),
new OpenIddictToken()
};
var manager = CreateTokenManager(mock =>
{
mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(tokens[0]);
mock.Setup(manager => manager.GetIdAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
mock.Setup(manager => manager.GetIdAsync(tokens[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("481FCAC6-06BC-43EE-92DB-37A78AA09B595073CC313103");
mock.Setup(manager => manager.GetIdAsync(tokens[2], It.IsAny<CancellationToken>()))
.ReturnsAsync("3BEA7A94-5ADA-49AF-9F41-8AB6156E31A8");
mock.Setup(manager => manager.GetAuthorizationIdAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0");
mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Valid, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.TryRedeemAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny<CancellationToken>()))
.Returns(tokens.ToAsyncEnumerable());
mock.Setup(manager => manager.CreateAsync(It.IsAny<OpenIddictTokenDescriptor>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new OpenIddictToken());
});
await using var server = await CreateServerAsync(options =>
{
options.UseRollingRefreshTokens();
options.DisableAuthorizationStorage();
options.DisableRollingRefreshTokens();
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
@ -3473,7 +3198,6 @@ namespace OpenIddict.Server.IntegrationTests
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetTokenType(TokenTypeHints.RefreshToken)
.SetScopes(Scopes.OpenId, Scopes.OfflineAccess)
.SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")
.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103")
.SetClaim(Claims.Subject, "Bob le Bricoleur");
@ -3483,17 +3207,6 @@ namespace OpenIddict.Server.IntegrationTests
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
options.Services.AddSingleton(CreateAuthorizationManager(mock =>
{
var authorization = new OpenIddictAuthorization();
mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny<CancellationToken>()))
.ReturnsAsync(authorization);
mock.Setup(manager => manager.HasStatusAsync(authorization, Statuses.Valid, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
}));
options.Services.AddSingleton(manager);
});
@ -3510,427 +3223,7 @@ namespace OpenIddict.Server.IntegrationTests
Assert.NotNull(response.RefreshToken);
Mock.Get(manager).Verify(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[0], It.IsAny<CancellationToken>()), Times.Never());
Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[1], It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[2], It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task ProcessSignIn_PreviousTokensAreNotRevokedWhenRollingTokensAreDisabled()
{
// Arrange
var tokens = new[]
{
new OpenIddictToken(),
new OpenIddictToken(),
new OpenIddictToken()
};
var manager = CreateTokenManager(mock =>
{
mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(tokens[0]);
mock.Setup(manager => manager.GetIdAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
mock.Setup(manager => manager.GetIdAsync(tokens[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("481FCAC6-06BC-43EE-92DB-37A78AA09B595073CC313103");
mock.Setup(manager => manager.GetIdAsync(tokens[2], It.IsAny<CancellationToken>()))
.ReturnsAsync("3BEA7A94-5ADA-49AF-9F41-8AB6156E31A8");
mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Valid, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny<CancellationToken>()))
.Returns(tokens.ToAsyncEnumerable());
mock.Setup(manager => manager.CreateAsync(It.IsAny<OpenIddictTokenDescriptor>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new OpenIddictToken());
});
await using var server = await CreateServerAsync(options =>
{
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("8xLOxBtZp8", context.Token);
Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetTokenType(TokenTypeHints.RefreshToken)
.SetScopes(Scopes.OpenId, Scopes.OfflineAccess)
.SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")
.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103")
.SetClaim(Claims.Subject, "Bob le Bricoleur");
return default;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
options.Services.AddSingleton(CreateAuthorizationManager(mock =>
{
var authorization = new OpenIddictAuthorization();
mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny<CancellationToken>()))
.ReturnsAsync(authorization);
mock.Setup(manager => manager.HasStatusAsync(authorization, Statuses.Valid, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
}));
options.Services.AddSingleton(manager);
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
GrantType = GrantTypes.RefreshToken,
RefreshToken = "8xLOxBtZp8"
});
// Assert
Assert.NotNull(response.AccessToken);
Assert.Null(response.RefreshToken);
Mock.Get(manager).Verify(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[0], It.IsAny<CancellationToken>()), Times.Never());
Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[1], It.IsAny<CancellationToken>()), Times.Never());
Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[2], It.IsAny<CancellationToken>()), Times.Never());
}
[Fact]
public async Task ProcessSignIn_ExtendsLifetimeWhenRollingTokensAreDisabledAndSlidingExpirationEnabled()
{
// Arrange
var token = new OpenIddictToken();
var manager = CreateTokenManager(mock =>
{
mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
mock.Setup(manager => manager.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.CreateAsync(It.IsAny<OpenIddictTokenDescriptor>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new OpenIddictToken());
});
await using var server = await CreateServerAsync(options =>
{
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("8xLOxBtZp8", context.Token);
Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetTokenType(TokenTypeHints.RefreshToken)
.SetScopes(Scopes.OpenId, Scopes.OfflineAccess)
.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103")
.SetClaim(Claims.Subject, "Bob le Bricoleur");
return default;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
options.Services.AddSingleton(manager);
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
GrantType = GrantTypes.RefreshToken,
RefreshToken = "8xLOxBtZp8"
});
// Assert
Assert.Null(response.RefreshToken);
Mock.Get(manager).Verify(manager => manager.TryExtendAsync(token,
It.IsAny<DateTimeOffset>(), It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task ProcessSignIn_DoesNotExtendLifetimeWhenSlidingExpirationIsDisabled()
{
// Arrange
var token = new OpenIddictToken();
var manager = CreateTokenManager(mock =>
{
mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.CreateAsync(It.IsAny<OpenIddictTokenDescriptor>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new OpenIddictToken());
});
await using var server = await CreateServerAsync(options =>
{
options.DisableSlidingRefreshTokenExpiration();
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("8xLOxBtZp8", context.Token);
Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetTokenType(TokenTypeHints.RefreshToken)
.SetScopes(Scopes.OpenId, Scopes.OfflineAccess)
.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103")
.SetClaim(Claims.Subject, "Bob le Bricoleur");
return default;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
options.Services.AddSingleton(manager);
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
GrantType = GrantTypes.RefreshToken,
RefreshToken = "8xLOxBtZp8"
});
// Assert
Assert.Null(response.RefreshToken);
Mock.Get(manager).Verify(manager => manager.TryExtendAsync(token,
It.IsAny<DateTimeOffset>(), It.IsAny<CancellationToken>()), Times.Never());
}
[Fact]
public async Task ProcessSignIn_DoesNotUpdateExpirationDateWhenAlreadyNull()
{
// Arrange
var token = new OpenIddictToken();
var manager = CreateTokenManager(mock =>
{
mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.GetExpirationDateAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(value: null);
mock.Setup(manager => manager.CreateAsync(It.IsAny<OpenIddictTokenDescriptor>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new OpenIddictToken());
});
await using var server = await CreateServerAsync(options =>
{
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("8xLOxBtZp8", context.Token);
Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetTokenType(TokenTypeHints.RefreshToken)
.SetScopes(Scopes.OpenId, Scopes.OfflineAccess)
.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103")
.SetClaim(Claims.Subject, "Bob le Bricoleur");
return default;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
options.Services.AddSingleton(manager);
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
GrantType = GrantTypes.RefreshToken,
RefreshToken = "8xLOxBtZp8"
});
// Assert
Assert.Null(response.RefreshToken);
Mock.Get(manager).Verify(manager => manager.TryExtendAsync(token, null, It.IsAny<CancellationToken>()), Times.Never());
}
[Fact]
public async Task ProcessSignIn_SetsExpirationDateToNullWhenLifetimeIsNull()
{
// Arrange
var token = new OpenIddictToken();
var manager = CreateTokenManager(mock =>
{
mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
mock.Setup(manager => manager.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.GetExpirationDateAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(DateTimeOffset.Now + TimeSpan.FromDays(1));
mock.Setup(manager => manager.CreateAsync(It.IsAny<OpenIddictTokenDescriptor>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new OpenIddictToken());
});
await using var server = await CreateServerAsync(options =>
{
options.SetRefreshTokenLifetime(lifetime: null);
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("8xLOxBtZp8", context.Token);
Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetTokenType(TokenTypeHints.RefreshToken)
.SetScopes(Scopes.OpenId, Scopes.OfflineAccess)
.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103")
.SetClaim(Claims.Subject, "Bob le Bricoleur");
return default;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
options.Services.AddSingleton(manager);
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
GrantType = GrantTypes.RefreshToken,
RefreshToken = "8xLOxBtZp8"
});
// Assert
Assert.Null(response.RefreshToken);
Mock.Get(manager).Verify(manager => manager.TryExtendAsync(token, null, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task ProcessSignIn_IgnoresErrorWhenExtendingLifetimeOfExistingTokenFailed()
{
// Arrange
var token = new OpenIddictToken();
var manager = CreateTokenManager(mock =>
{
mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
mock.Setup(manager => manager.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.TryExtendAsync(token, It.IsAny<DateTimeOffset?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
mock.Setup(manager => manager.CreateAsync(It.IsAny<OpenIddictTokenDescriptor>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new OpenIddictToken());
});
await using var server = await CreateServerAsync(options =>
{
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("8xLOxBtZp8", context.Token);
Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetTokenType(TokenTypeHints.RefreshToken)
.SetScopes(Scopes.OpenId, Scopes.OfflineAccess)
.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103")
.SetClaim(Claims.Subject, "Bob le Bricoleur");
return default;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
options.Services.AddSingleton(manager);
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
GrantType = GrantTypes.RefreshToken,
RefreshToken = "8xLOxBtZp8"
});
// Assert
Assert.NotNull(response.AccessToken);
Mock.Get(manager).Verify(manager => manager.TryExtendAsync(token,
It.IsAny<DateTimeOffset>(), It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(manager => manager.TryRedeemAsync(token, It.IsAny<CancellationToken>()), Times.Never());
}
[Fact]

143
test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs

@ -417,7 +417,7 @@ namespace OpenIddict.Server.Tests
}
[Fact]
public void AllowAuthorizationCodeFlow_CodeFlowIsAddedToGrantTypes()
public void AllowAuthorizationCodeFlow_CodeFlowIsAdded()
{
// Arrange
var services = CreateServices();
@ -429,11 +429,19 @@ namespace OpenIddict.Server.Tests
var options = GetOptions(services);
// Assert
Assert.Contains(CodeChallengeMethods.Sha256, options.CodeChallengeMethods);
Assert.Contains(GrantTypes.AuthorizationCode, options.GrantTypes);
Assert.Contains(ResponseModes.FormPost, options.ResponseModes);
Assert.Contains(ResponseModes.Fragment, options.ResponseModes);
Assert.Contains(ResponseModes.Query, options.ResponseModes);
Assert.Contains(ResponseTypes.Code, options.ResponseTypes);
}
[Fact]
public void AllowClientCredentialsFlow_ClientCredentialsFlowIsAddedToGrantTypes()
public void AllowClientCredentialsFlow_ClientCredentialsFlowIsAdded()
{
// Arrange
var services = CreateServices();
@ -448,8 +456,24 @@ namespace OpenIddict.Server.Tests
Assert.Contains(GrantTypes.ClientCredentials, options.GrantTypes);
}
[Theory]
[InlineData(null)]
[InlineData("")]
public void AllowCustomFlow_ThrowsAnExceptionForType(string type)
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentException>(() => builder.AllowCustomFlow(type));
Assert.Equal("type", exception.ParamName);
Assert.Contains("The grant type cannot be null or empty.", exception.Message);
}
[Fact]
public void AllowCustomFlow_CustomFlowIsAddedToGrantTypes()
public void AllowCustomFlow_CustomFlowIsAdded()
{
// Arrange
var services = CreateServices();
@ -464,24 +488,50 @@ namespace OpenIddict.Server.Tests
Assert.Contains("urn:ietf:params:oauth:grant-type:custom_grant", options.GrantTypes);
}
[Theory]
[InlineData(null)]
[InlineData("")]
public void AllowCustomFlow_ThrowsAnExceptionForType(string type)
[Fact]
public void AddDeviceCodeFlow_DeviceFlowIsAdded()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentException>(() => builder.AllowCustomFlow(type));
// Act
builder.AllowDeviceCodeFlow();
Assert.Equal("type", exception.ParamName);
Assert.Contains("The grant type cannot be null or empty.", exception.Message);
var options = GetOptions(services);
// Assert
Assert.Contains(GrantTypes.DeviceCode, options.GrantTypes);
}
[Fact]
public void AllowHybridFlow_HybridFlowIsAdded()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.AllowHybridFlow();
var options = GetOptions(services);
// Assert
Assert.Contains(CodeChallengeMethods.Sha256, options.CodeChallengeMethods);
Assert.Contains(GrantTypes.AuthorizationCode, options.GrantTypes);
Assert.Contains(GrantTypes.Implicit, options.GrantTypes);
Assert.Contains(ResponseModes.FormPost, options.ResponseModes);
Assert.Contains(ResponseModes.Fragment, options.ResponseModes);
Assert.Contains(ResponseTypes.Code + ' ' + ResponseTypes.IdToken, options.ResponseTypes);
Assert.Contains(ResponseTypes.Code + ' ' + ResponseTypes.IdToken + ' ' + ResponseTypes.Token, options.ResponseTypes);
Assert.Contains(ResponseTypes.Code + ' ' + ResponseTypes.Token, options.ResponseTypes);
}
[Fact]
public void AllowImplicitFlow_ImplicitFlowIsAddedToGrantTypes()
public void AllowImplicitFlow_ImplicitFlowIsAdded()
{
// Arrange
var services = CreateServices();
@ -494,10 +544,17 @@ namespace OpenIddict.Server.Tests
// Assert
Assert.Contains(GrantTypes.Implicit, options.GrantTypes);
Assert.Contains(ResponseModes.FormPost, options.ResponseModes);
Assert.Contains(ResponseModes.Fragment, options.ResponseModes);
Assert.Contains(ResponseTypes.IdToken, options.ResponseTypes);
Assert.Contains(ResponseTypes.IdToken + ' ' + ResponseTypes.Token, options.ResponseTypes);
Assert.Contains(ResponseTypes.Token, options.ResponseTypes);
}
[Fact]
public void AllowPasswordFlow_PasswordFlowIsAddedToGrantTypes()
public void AllowPasswordFlow_PasswordFlowIsAdded()
{
// Arrange
var services = CreateServices();
@ -513,7 +570,7 @@ namespace OpenIddict.Server.Tests
}
[Fact]
public void AllowRefreshTokenFlow_RefreshTokenFlowIsAddedToGrantTypes()
public void AllowRefreshTokenFlow_RefreshTokenFlowIsAdded()
{
// Arrange
var services = CreateServices();
@ -529,115 +586,115 @@ namespace OpenIddict.Server.Tests
}
[Fact]
public void DisableAuthorizationStorage_AuthorizationStorageIsDisabled()
public void DisableAccessTokenEncryption_AccessTokenEncryptionIsDisabled()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.DisableAuthorizationStorage();
builder.DisableAccessTokenEncryption();
var options = GetOptions(services);
// Assert
Assert.True(options.DisableAuthorizationStorage);
Assert.True(options.DisableAccessTokenEncryption);
}
[Fact]
public void DisableScopeValidation_ScopeValidationIsDisabled()
public void DisableAuthorizationStorage_AuthorizationStorageIsDisabled()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.DisableScopeValidation();
builder.DisableAuthorizationStorage();
var options = GetOptions(services);
// Assert
Assert.True(options.DisableScopeValidation);
Assert.True(options.DisableAuthorizationStorage);
}
[Fact]
public void DisableSlidingRefreshTokenExpiration_SlidingExpirationIsDisabled()
public void DisableRollingRefreshTokens_RollingRefreshTokensAreDisabled()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.DisableSlidingRefreshTokenExpiration();
builder.DisableRollingRefreshTokens();
var options = GetOptions(services);
// Assert
Assert.True(options.DisableSlidingRefreshTokenExpiration);
Assert.True(options.DisableRollingRefreshTokens);
}
[Fact]
public void DisableTokenStorage_TokenStorageIsDisabled()
public void DisableScopeValidation_ScopeValidationIsDisabled()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.DisableTokenStorage();
builder.DisableScopeValidation();
var options = GetOptions(services);
// Assert
Assert.True(options.DisableTokenStorage);
Assert.True(options.DisableScopeValidation);
}
[Fact]
public void DisableAccessTokenEncryption_AccessTokenEncryptionIsDisabled()
public void DisableSlidingRefreshTokenExpiration_SlidingExpirationIsDisabled()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.DisableAccessTokenEncryption();
builder.DisableSlidingRefreshTokenExpiration();
var options = GetOptions(services);
// Assert
Assert.True(options.DisableAccessTokenEncryption);
Assert.True(options.DisableSlidingRefreshTokenExpiration);
}
[Fact]
public void RequireProofKeyForCodeExchange_PkceIsEnforced()
public void DisableTokenStorage_TokenStorageIsDisabled()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.RequireProofKeyForCodeExchange();
builder.DisableTokenStorage();
var options = GetOptions(services);
// Assert
Assert.True(options.RequireProofKeyForCodeExchange);
Assert.True(options.DisableTokenStorage);
}
[Fact]
public void AddDeviceCodeFlow_AddsDeviceCodeGrantType()
public void RequireProofKeyForCodeExchange_PkceIsEnforced()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.AllowDeviceCodeFlow();
builder.RequireProofKeyForCodeExchange();
var options = GetOptions(services);
// Assert
Assert.Contains(GrantTypes.DeviceCode, options.GrantTypes);
Assert.True(options.RequireProofKeyForCodeExchange);
}
[Fact]
@ -1841,22 +1898,6 @@ namespace OpenIddict.Server.Tests
Assert.True(options.UseReferenceRefreshTokens);
}
[Fact]
public void UseRollingRefreshTokens_RollingRefreshTokensAreEnabled()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.UseRollingRefreshTokens();
var options = GetOptions(services);
// Assert
Assert.True(options.UseRollingRefreshTokens);
}
private static IServiceCollection CreateServices()
{
return new ServiceCollection().AddOptions();

Loading…
Cancel
Save