diff --git a/samples/Mvc.Server/Startup.cs b/samples/Mvc.Server/Startup.cs
index e125ba21..5745de46 100644
--- a/samples/Mvc.Server/Startup.cs
+++ b/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
diff --git a/src/OpenIddict.Abstractions/Descriptors/OpenIddictTokenDescriptor.cs b/src/OpenIddict.Abstractions/Descriptors/OpenIddictTokenDescriptor.cs
index 0dea593a..f75d81f7 100644
--- a/src/OpenIddict.Abstractions/Descriptors/OpenIddictTokenDescriptor.cs
+++ b/src/OpenIddict.Abstractions/Descriptors/OpenIddictTokenDescriptor.cs
@@ -39,6 +39,11 @@ namespace OpenIddict.Abstractions
///
public ClaimsPrincipal? Principal { get; set; }
+ ///
+ /// Gets or sets the redemption date associated with the token.
+ ///
+ public DateTimeOffset? RedemptionDate { get; set; }
+
///
/// Gets or sets the reference identifier associated with the token.
/// Note: depending on the application manager used when creating it,
diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs
index ed2dbf3d..f6734c70 100644
--- a/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs
+++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs
@@ -375,17 +375,6 @@ namespace OpenIddict.Abstractions
///
ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken = default);
- ///
- /// Sets the application identifier associated with an authorization.
- ///
- /// The authorization.
- /// The unique identifier associated with the client application.
- /// The that can be used to abort the operation.
- ///
- /// A that can be used to monitor the asynchronous operation.
- ///
- ValueTask SetApplicationIdAsync(object authorization, string identifier, CancellationToken cancellationToken = default);
-
///
/// Tries to revoke an authorization.
///
diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs
index 90775c84..4d80308a 100644
--- a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs
+++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs
@@ -255,6 +255,17 @@ namespace OpenIddict.Abstractions
///
ValueTask GetPayloadAsync(object token, CancellationToken cancellationToken = default);
+ ///
+ /// Retrieves the redemption date associated with a token.
+ ///
+ /// The token.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the redemption date associated with the specified token.
+ ///
+ ValueTask GetRedemptionDateAsync(object token, CancellationToken cancellationToken = default);
+
///
/// 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
///
ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken = default);
- ///
- /// Sets the application identifier associated with a token.
- ///
- /// The token.
- /// The unique identifier associated with the client application.
- /// The that can be used to abort the operation.
- ///
- /// A that can be used to monitor the asynchronous operation.
- ///
- ValueTask SetApplicationIdAsync(object token, string identifier, CancellationToken cancellationToken = default);
-
- ///
- /// Sets the authorization identifier associated with a token.
- ///
- /// The token.
- /// The unique identifier associated with the authorization.
- /// The that can be used to abort the operation.
- ///
- /// A that can be used to monitor the asynchronous operation.
- ///
- ValueTask SetAuthorizationIdAsync(object token, string identifier, CancellationToken cancellationToken = default);
-
- ///
- /// Tries to extend the specified token by replacing its expiration date.
- ///
- /// The token.
- /// The date on which the token will no longer be considered valid.
- /// The that can be used to abort the operation.
- /// true if the token was successfully extended, false otherwise.
- ValueTask TryExtendAsync(object token, DateTimeOffset? date, CancellationToken cancellationToken = default);
-
///
/// Tries to redeem a token.
///
diff --git a/src/OpenIddict.Abstractions/Resources/OpenIddictResources.resx b/src/OpenIddict.Abstractions/Resources/OpenIddictResources.resx
index cb0755ec..4fcb9e1c 100644
--- a/src/OpenIddict.Abstractions/Resources/OpenIddictResources.resx
+++ b/src/OpenIddict.Abstractions/Resources/OpenIddictResources.resx
@@ -489,10 +489,6 @@ To enable DI support, call 'services.AddQuartz(options => options.UseMicrosof
Reference tokens cannot be used when disabling token storage.
{Locked}
-
- Sliding expiration must be disabled when turning off token storage if rolling tokens are not used.
- {Locked}
-
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.
@@ -2443,22 +2439,6 @@ This may indicate that the hashed entry is corrupted or malformed.
An exception occurred while trying to revoke the authorization '{Identifier}'.
{Locked}
-
- The expiration date of the refresh token '{Identifier}' was successfully updated: {Date}.
- {Locked}
-
-
- The expiration date of the refresh token '{Identifier}' was successfully removed.
- {Locked}
-
-
- A concurrency exception occurred while trying to update the expiration date of the token '{Identifier}'.
- {Locked}
-
-
- An exception occurred while trying to update the expiration date of the token '{Identifier}'.
- {Locked}
-
The token '{Identifier}' was successfully marked as redeemed.
{Locked}
diff --git a/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs b/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs
index d64a839a..33f55b65 100644
--- a/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs
+++ b/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs
@@ -233,6 +233,17 @@ namespace OpenIddict.Abstractions
///
ValueTask> GetPropertiesAsync(TToken token, CancellationToken cancellationToken);
+ ///
+ /// Retrieves the redemption date associated with a token.
+ ///
+ /// The token.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the redemption date associated with the specified token.
+ ///
+ ValueTask GetRedemptionDateAsync(TToken token, CancellationToken cancellationToken);
+
///
/// 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 properties, CancellationToken cancellationToken);
+ ///
+ /// Sets the redemption date associated with a token.
+ ///
+ /// The token.
+ /// The redemption date.
+ /// The that can be used to abort the operation.
+ /// A that can be used to monitor the asynchronous operation.
+ ValueTask SetRedemptionDateAsync(TToken token, DateTimeOffset? date, CancellationToken cancellationToken);
+
///
/// Sets the reference identifier associated with a token.
/// Note: depending on the manager used to create the token,
diff --git a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
index 91b12daf..f4330388 100644
--- a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
+++ b/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);
- ///
- /// Sets the application identifier associated with an authorization.
- ///
- /// The authorization.
- /// The unique identifier associated with the client application.
- /// The that can be used to abort the operation.
- ///
- /// A that can be used to monitor the asynchronous operation.
- ///
- public virtual async 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);
- }
-
///
/// Tries to revoke an authorization.
///
@@ -1312,10 +1291,6 @@ namespace OpenIddict.Core
ValueTask IOpenIddictAuthorizationManager.PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken)
=> PruneAsync(threshold, cancellationToken);
- ///
- ValueTask IOpenIddictAuthorizationManager.SetApplicationIdAsync(object authorization, string? identifier, CancellationToken cancellationToken)
- => SetApplicationIdAsync((TAuthorization) authorization, identifier, cancellationToken);
-
///
ValueTask IOpenIddictAuthorizationManager.TryRevokeAsync(object authorization, CancellationToken cancellationToken)
=> TryRevokeAsync((TAuthorization) authorization, cancellationToken);
diff --git a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
index 222fb3fb..bb1f309a 100644
--- a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
+++ b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
@@ -744,6 +744,25 @@ namespace OpenIddict.Core
return Store.GetPayloadAsync(token, cancellationToken);
}
+ ///
+ /// Retrieves the redemption date associated with a token.
+ ///
+ /// The token.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the redemption date associated with the specified token.
+ ///
+ public virtual ValueTask GetRedemptionDateAsync(TToken token, CancellationToken cancellationToken = default)
+ {
+ if (token is null)
+ {
+ throw new ArgumentNullException(nameof(token));
+ }
+
+ return Store.GetRedemptionDateAsync(token, cancellationToken);
+ }
+
///
/// 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
///
public virtual ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken = default)
=> Store.PruneAsync(threshold, cancellationToken);
-
- ///
- /// Sets the application identifier associated with a token.
- ///
- /// The token.
- /// The unique identifier associated with the client application.
- /// The that can be used to abort the operation.
- ///
- /// A that can be used to monitor the asynchronous operation.
- ///
- public virtual async 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);
- }
-
- ///
- /// Sets the authorization identifier associated with a token.
- ///
- /// The token.
- /// The unique identifier associated with the authorization.
- /// The that can be used to abort the operation.
- ///
- /// A that can be used to monitor the asynchronous operation.
- ///
- public virtual async 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);
- }
-
- ///
- /// Tries to extend the specified token by replacing its expiration date.
- ///
- /// The token.
- /// The date on which the token will no longer be considered valid.
- /// The that can be used to abort the operation.
- /// true if the token was successfully extended, false otherwise.
- public virtual async ValueTask 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;
- }
- }
-
///
/// Tries to redeem a token.
///
@@ -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 IOpenIddictTokenManager.GetPayloadAsync(object token, CancellationToken cancellationToken)
=> GetPayloadAsync((TToken) token, cancellationToken);
+ ///
+ ValueTask IOpenIddictTokenManager.GetRedemptionDateAsync(object token, CancellationToken cancellationToken)
+ => GetRedemptionDateAsync((TToken) token, cancellationToken);
+
///
ValueTask 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);
- ///
- ValueTask IOpenIddictTokenManager.SetApplicationIdAsync(object token, string? identifier, CancellationToken cancellationToken)
- => SetApplicationIdAsync((TToken) token, identifier, cancellationToken);
-
- ///
- ValueTask IOpenIddictTokenManager.SetAuthorizationIdAsync(object token, string? identifier, CancellationToken cancellationToken)
- => SetAuthorizationIdAsync((TToken) token, identifier, cancellationToken);
-
- ///
- ValueTask IOpenIddictTokenManager.TryExtendAsync(object token, DateTimeOffset? date, CancellationToken cancellationToken)
- => TryExtendAsync((TToken) token, date, cancellationToken);
-
///
ValueTask IOpenIddictTokenManager.TryRedeemAsync(object token, CancellationToken cancellationToken)
=> TryRedeemAsync((TToken) token, cancellationToken);
diff --git a/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkToken.cs b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkToken.cs
index 73547d5f..4f5c5f07 100644
--- a/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkToken.cs
+++ b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkToken.cs
@@ -73,6 +73,11 @@ namespace OpenIddict.EntityFramework.Models
///
public virtual string? Properties { get; set; }
+ ///
+ /// Gets or sets the UTC redemption date of the current token.
+ ///
+ public virtual DateTime? RedemptionDate { get; set; }
+
///
/// Gets or sets the reference identifier associated
/// with the current token, if applicable.
diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs
index 10c9d099..fe00d7b0 100644
--- a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs
+++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs
@@ -470,6 +470,22 @@ namespace OpenIddict.EntityFramework
return new ValueTask>(properties);
}
+ ///
+ public virtual ValueTask GetRedemptionDateAsync(TToken token, CancellationToken cancellationToken)
+ {
+ if (token is null)
+ {
+ throw new ArgumentNullException(nameof(token));
+ }
+
+ if (token.RedemptionDate is null)
+ {
+ return new ValueTask(result: null);
+ }
+
+ return new ValueTask(DateTime.SpecifyKind(token.RedemptionDate.Value, DateTimeKind.Utc));
+ }
+
///
public virtual ValueTask GetReferenceIdAsync(TToken token, CancellationToken cancellationToken)
{
@@ -789,6 +805,19 @@ namespace OpenIddict.EntityFramework
return default;
}
+ ///
+ 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;
+ }
+
///
public virtual ValueTask SetReferenceIdAsync(TToken token, string? identifier, CancellationToken cancellationToken)
{
diff --git a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreToken.cs b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreToken.cs
index 5099de03..70cbd5b7 100644
--- a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreToken.cs
+++ b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreToken.cs
@@ -81,6 +81,11 @@ namespace OpenIddict.EntityFrameworkCore.Models
///
public virtual string? Properties { get; set; }
+ ///
+ /// Gets or sets the UTC redemption date of the current token.
+ ///
+ public virtual DateTime? RedemptionDate { get; set; }
+
///
/// Gets or sets the reference identifier associated
/// with the current token, if applicable.
diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs
index b14ac2c0..bcc0d5e6 100644
--- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs
+++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs
@@ -522,6 +522,22 @@ namespace OpenIddict.EntityFrameworkCore
return new ValueTask>(properties);
}
+ ///
+ public virtual ValueTask GetRedemptionDateAsync(TToken token, CancellationToken cancellationToken)
+ {
+ if (token is null)
+ {
+ throw new ArgumentNullException(nameof(token));
+ }
+
+ if (token.RedemptionDate is null)
+ {
+ return new ValueTask(result: null);
+ }
+
+ return new ValueTask(DateTime.SpecifyKind(token.RedemptionDate.Value, DateTimeKind.Utc));
+ }
+
///
public virtual ValueTask GetReferenceIdAsync(TToken token, CancellationToken cancellationToken)
{
@@ -864,6 +880,19 @@ namespace OpenIddict.EntityFrameworkCore
return default;
}
+ ///
+ 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;
+ }
+
///
public virtual ValueTask SetReferenceIdAsync(TToken token, string? identifier, CancellationToken cancellationToken)
{
diff --git a/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbToken.cs b/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbToken.cs
index 6f3e26c7..89a53e85 100644
--- a/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbToken.cs
+++ b/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbToken.cs
@@ -67,6 +67,12 @@ namespace OpenIddict.MongoDb.Models
[BsonElement("properties"), BsonIgnoreIfNull]
public virtual BsonDocument? Properties { get; set; }
+ ///
+ /// Gets or sets the UTC redemption date of the current token.
+ ///
+ [BsonElement("redemption_date"), BsonIgnoreIfNull]
+ public virtual DateTime? RedemptionDate { get; set; }
+
///
/// Gets or sets the reference identifier associated
/// with the current token, if applicable.
diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs
index 605cff42..c34f7b43 100644
--- a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs
+++ b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs
@@ -440,6 +440,22 @@ namespace OpenIddict.MongoDb
return new ValueTask>(builder.ToImmutable());
}
+ ///
+ public virtual ValueTask GetRedemptionDateAsync(TToken token, CancellationToken cancellationToken)
+ {
+ if (token is null)
+ {
+ throw new ArgumentNullException(nameof(token));
+ }
+
+ if (token.RedemptionDate is null)
+ {
+ return new ValueTask(result: null);
+ }
+
+ return new ValueTask(DateTime.SpecifyKind(token.RedemptionDate.Value, DateTimeKind.Utc));
+ }
+
///
public virtual ValueTask GetReferenceIdAsync(TToken token, CancellationToken cancellationToken)
{
@@ -719,6 +735,19 @@ namespace OpenIddict.MongoDb
return default;
}
+ ///
+ 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;
+ }
+
///
public virtual ValueTask SetReferenceIdAsync(TToken token, string? identifier, CancellationToken cancellationToken)
{
diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs
index 34a6a0e9..038cc08d 100644
--- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs
+++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs
@@ -52,7 +52,7 @@ namespace Microsoft.Extensions.DependencyInjection
}
///
- /// Disables the transport security requirement (HTTPS) during development.
+ /// Disables the transport security requirement (HTTPS).
///
/// The .
public OpenIddictServerAspNetCoreBuilder DisableTransportSecurityRequirement()
diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs
index 8062c6e9..e7bfade9 100644
--- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs
+++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs
@@ -52,7 +52,7 @@ namespace Microsoft.Extensions.DependencyInjection
}
///
- /// Disables the transport security requirement (HTTPS) during development.
+ /// Disables the transport security requirement (HTTPS).
///
/// The .
public OpenIddictServerOwinBuilder DisableTransportSecurityRequirement()
diff --git a/src/OpenIddict.Server/OpenIddictServerBuilder.cs b/src/OpenIddict.Server/OpenIddictServerBuilder.cs
index f866c607..6a6ff4be 100644
--- a/src/OpenIddict.Server/OpenIddictServerBuilder.cs
+++ b/src/OpenIddict.Server/OpenIddictServerBuilder.cs
@@ -1630,6 +1630,16 @@ namespace Microsoft.Extensions.DependencyInjection
public OpenIddictServerBuilder DisableAuthorizationStorage()
=> Configure(options => options.DisableAuthorizationStorage = true);
+ ///
+ /// 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.
+ ///
+ /// The .
+ public OpenIddictServerBuilder DisableRollingRefreshTokens()
+ => Configure(options => options.DisableRollingRefreshTokens = true);
+
///
/// Allows processing authorization and token requests that specify scopes that have not
/// been registered using or the scope manager.
@@ -1804,6 +1814,15 @@ namespace Microsoft.Extensions.DependencyInjection
public OpenIddictServerBuilder SetRefreshTokenLifetime(TimeSpan? lifetime)
=> Configure(options => options.RefreshTokenLifetime = lifetime);
+ ///
+ /// Sets the refresh token reuse leeway, during which rolling refresh tokens marked
+ /// as redeemed can still be used to make concurrent refresh token requests.
+ ///
+ /// The refresh token reuse interval.
+ /// The .
+ public OpenIddictServerBuilder SetRefreshTokenReuseLeeway(TimeSpan? leeway)
+ => Configure(options => options.RefreshTokenReuseLeeway = leeway);
+
///
/// 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);
- ///
- /// 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).
- ///
- /// The .
- public OpenIddictServerBuilder UseRollingRefreshTokens()
- => Configure(options => options.UseRollingRefreshTokens = true);
-
///
[EditorBrowsable(EditorBrowsableState.Never)]
public override bool Equals(object? obj) => base.Equals(obj);
diff --git a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs
index 72786913..60651df8 100644
--- a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs
+++ b/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);
}
diff --git a/src/OpenIddict.Server/OpenIddictServerExtensions.cs b/src/OpenIddict.Server/OpenIddictServerExtensions.cs
index ecf35b3c..52d287d9 100644
--- a/src/OpenIddict.Server/OpenIddictServerExtensions.cs
+++ b/src/OpenIddict.Server/OpenIddictServerExtensions.cs
@@ -69,8 +69,6 @@ namespace Microsoft.Extensions.DependencyInjection
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
- builder.Services.TryAddSingleton();
- builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs b/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs
index 2ac7443b..35276797 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs
+++ b/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs
@@ -350,38 +350,6 @@ namespace OpenIddict.Server
}
}
- ///
- /// Represents a filter that excludes the associated handlers if rolling tokens were enabled.
- ///
- public class RequireRollingTokensDisabled : IOpenIddictServerHandlerFilter
- {
- public ValueTask IsActiveAsync(BaseContext context)
- {
- if (context is null)
- {
- throw new ArgumentNullException(nameof(context));
- }
-
- return new ValueTask(!context.Options.UseRollingRefreshTokens);
- }
- }
-
- ///
- /// Represents a filter that excludes the associated handlers if rolling refresh tokens were not enabled.
- ///
- public class RequireRollingRefreshTokensEnabled : IOpenIddictServerHandlerFilter
- {
- public ValueTask IsActiveAsync(BaseContext context)
- {
- if (context is null)
- {
- throw new ArgumentNullException(nameof(context));
- }
-
- return new ValueTask(context.Options.UseRollingRefreshTokens);
- }
- }
-
///
/// Represents a filter that excludes the associated handlers if scope permissions were disabled.
///
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs
index f70c6fed..cddc1f30 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs
+++ b/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 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(
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;
- }
- }
- }
-
- ///
- /// 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.
- ///
- public class RevokeExistingTokenEntries : IOpenIddictServerHandler
- {
- private readonly IOpenIddictTokenManager _tokenManager;
-
- public RevokeExistingTokenEntries() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
-
- public RevokeExistingTokenEntries(IOpenIddictTokenManager tokenManager)
- => _tokenManager = tokenManager;
-
- ///
- /// Gets the default descriptor definition assigned to this handler.
- ///
- public static OpenIddictServerHandlerDescriptor Descriptor { get; }
- = OpenIddictServerHandlerDescriptor.CreateBuilder()
- .AddFilter()
- .AddFilter()
- .AddFilter()
- .UseScopedHandler()
- .SetOrder(RedeemTokenEntry.Descriptor.Order + 1_000)
- .SetType(OpenIddictServerHandlerType.BuiltIn)
- .Build();
-
- ///
- 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);
- }
- }
- }
-
- ///
- /// 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.
- ///
- public class ExtendRefreshTokenEntry : IOpenIddictServerHandler
- {
- private readonly IOpenIddictTokenManager _tokenManager;
-
- public ExtendRefreshTokenEntry() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
-
- public ExtendRefreshTokenEntry(IOpenIddictTokenManager tokenManager)
- => _tokenManager = tokenManager;
-
- ///
- /// Gets the default descriptor definition assigned to this handler.
- ///
- public static OpenIddictServerHandlerDescriptor Descriptor { get; }
- = OpenIddictServerHandlerDescriptor.CreateBuilder()
- .AddFilter()
- .AddFilter()
- .AddFilter()
- .AddFilter()
- .UseScopedHandler()
- .SetOrder(RevokeExistingTokenEntries.Descriptor.Order + 1_000)
- .SetType(OpenIddictServerHandlerType.BuiltIn)
- .Build();
-
- ///
- 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()
.AddFilter()
.UseScopedHandler()
- .SetOrder(ExtendRefreshTokenEntry.Descriptor.Order + 1_000)
+ .SetOrder(RedeemTokenEntry.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
diff --git a/src/OpenIddict.Server/OpenIddictServerOptions.cs b/src/OpenIddict.Server/OpenIddictServerOptions.cs
index bd5cdae0..beb95478 100644
--- a/src/OpenIddict.Server/OpenIddictServerOptions.cs
+++ b/src/OpenIddict.Server/OpenIddictServerOptions.cs
@@ -214,6 +214,12 @@ namespace OpenIddict.Server
///
public TimeSpan? RefreshTokenLifetime { get; set; } = TimeSpan.FromDays(14);
+ ///
+ /// 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.
+ ///
+ public TimeSpan? RefreshTokenReuseLeeway { get; set; } = TimeSpan.FromSeconds(15);
+
///
/// 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
///
public bool DisableAuthorizationStorage { get; set; }
+ ///
+ /// 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.
+ ///
+ public bool DisableRollingRefreshTokens { get; set; }
+
///
/// Gets or sets a boolean indicating whether sliding expiration is disabled
/// for refresh tokens. When this option is set to ,
@@ -379,15 +393,5 @@ namespace OpenIddict.Server
/// that provides additional protection against token leakage.
///
public bool UseReferenceRefreshTokens { get; set; }
-
- ///
- /// Gets or sets a boolean indicating whether rolling tokens should be used.
- /// When disabled, no new token is issued and the refresh token lifetime is
- /// dynamically managed by updating the token entry in the database.
- /// When this option is enabled, a new refresh token is issued for each
- /// refresh token request (and the previous one is automatically revoked
- /// unless token storage was explicitly disabled in the options).
- ///
- public bool UseRollingRefreshTokens { get; set; }
}
}
diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs
index 619b9dbe..8a50e29a 100644
--- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs
+++ b/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()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny()), Times.Once());
+ Mock.Get(manager).Verify(manager => manager.GetRedemptionDateAsync(token, It.IsAny()), 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()))
.ReturnsAsync(true);
+
+ mock.Setup(manager => manager.GetRedemptionDateAsync(token, It.IsAny()))
+ .ReturnsAsync(DateTimeOffset.UtcNow);
});
await using var server = await CreateServerAsync(options =>
{
+ options.SetRefreshTokenReuseLeeway(leeway: null);
+
options.AddEventHandler(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()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny()), Times.Once());
+ Mock.Get(manager).Verify(manager => manager.GetRedemptionDateAsync(token, It.IsAny()), 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()))
+ .ReturnsAsync(token);
+
+ mock.Setup(manager => manager.GetIdAsync(token, It.IsAny()))
+ .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
+
+ mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny()))
+ .ReturnsAsync(true);
+
+ mock.Setup(manager => manager.GetRedemptionDateAsync(token, It.IsAny()))
+ .ReturnsAsync(DateTimeOffset.UtcNow - TimeSpan.FromMinutes(1));
+ });
+
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.SetRefreshTokenReuseLeeway(TimeSpan.FromSeconds(5));
+
+ options.AddEventHandler(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(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()), Times.AtLeastOnce());
+ Mock.Get(manager).Verify(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny()), Times.Once());
+ Mock.Get(manager).Verify(manager => manager.GetRedemptionDateAsync(token, It.IsAny()), 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()))
+ .ReturnsAsync(token);
+
+ mock.Setup(manager => manager.GetIdAsync(token, It.IsAny()))
+ .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
+
+ mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny()))
+ .ReturnsAsync(true);
+
+ mock.Setup(manager => manager.GetRedemptionDateAsync(token, It.IsAny()))
+ .ReturnsAsync(DateTimeOffset.UtcNow - TimeSpan.FromMinutes(1));
+
+ mock.Setup(manager => manager.CreateAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(new OpenIddictToken());
+ });
+
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.SetRefreshTokenReuseLeeway(TimeSpan.FromMinutes(5));
+
+ options.AddEventHandler(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(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()), Times.AtLeastOnce());
+ Mock.Get(manager).Verify(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny()), Times.Once());
+ Mock.Get(manager).Verify(manager => manager.GetRedemptionDateAsync(token, It.IsAny()), 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()))
+ .ReturnsAsync(tokens[0]);
+
+ mock.Setup(manager => manager.GetIdAsync(tokens[0], It.IsAny()))
+ .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
+
+ mock.Setup(manager => manager.GetIdAsync(tokens[1], It.IsAny()))
+ .ReturnsAsync("47468A64-C9A7-49C7-939C-19CC0F5DD166");
+
+ mock.Setup(manager => manager.GetIdAsync(tokens[2], It.IsAny()))
+ .ReturnsAsync("3BEA7A94-5ADA-49AF-9F41-8AB6156E31A8");
+
+ mock.Setup(manager => manager.GetAuthorizationIdAsync(tokens[0], It.IsAny()))
+ .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0");
+
+ mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Redeemed, It.IsAny()))
+ .ReturnsAsync(true);
+
+ mock.Setup(manager => manager.GetRedemptionDateAsync(tokens[0], It.IsAny()))
+ .ReturnsAsync(DateTimeOffset.UtcNow);
+
+ mock.Setup(manager => manager.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()))
+ .Returns(tokens.ToAsyncEnumerable());
+ });
+
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.SetRefreshTokenReuseLeeway(leeway: null);
+
+ options.AddEventHandler(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(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()), Times.AtLeastOnce());
+ Mock.Get(manager).Verify(manager => manager.HasStatusAsync(tokens[0], Statuses.Redeemed, It.IsAny()), Times.Once());
+ Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[0], It.IsAny()), Times.Once());
+ Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[1], It.IsAny()), Times.Once());
+ Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[2], It.IsAny()), 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()))
.ReturnsAsync(true);
+ mock.Setup(manager => manager.GetRedemptionDateAsync(tokens[0], It.IsAny()))
+ .ReturnsAsync(DateTimeOffset.UtcNow - TimeSpan.FromMinutes(1));
+
mock.Setup(manager => manager.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()))
.Returns(tokens.ToAsyncEnumerable());
});
await using var server = await CreateServerAsync(options =>
{
+ options.SetRefreshTokenReuseLeeway(TimeSpan.FromSeconds(5));
+
options.AddEventHandler(builder =>
{
builder.UseInlineHandler(context =>
@@ -2624,6 +2876,110 @@ namespace OpenIddict.Server.IntegrationTests
Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[2], It.IsAny()), 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()))
+ .ReturnsAsync(tokens[0]);
+
+ mock.Setup(manager => manager.GetIdAsync(tokens[0], It.IsAny()))
+ .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
+
+ mock.Setup(manager => manager.GetIdAsync(tokens[1], It.IsAny()))
+ .ReturnsAsync("47468A64-C9A7-49C7-939C-19CC0F5DD166");
+
+ mock.Setup(manager => manager.GetIdAsync(tokens[2], It.IsAny()))
+ .ReturnsAsync("3BEA7A94-5ADA-49AF-9F41-8AB6156E31A8");
+
+ mock.Setup(manager => manager.GetAuthorizationIdAsync(tokens[0], It.IsAny()))
+ .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0");
+
+ mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Redeemed, It.IsAny()))
+ .ReturnsAsync(true);
+
+ mock.Setup(manager => manager.GetRedemptionDateAsync(tokens[0], It.IsAny()))
+ .ReturnsAsync(DateTimeOffset.UtcNow - TimeSpan.FromMinutes(1));
+
+ mock.Setup(manager => manager.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()))
+ .Returns(tokens.ToAsyncEnumerable());
+
+ mock.Setup(manager => manager.CreateAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(new OpenIddictToken());
+ });
+
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.SetRefreshTokenReuseLeeway(TimeSpan.FromMinutes(5));
+
+ options.AddEventHandler(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(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()))
+ .ReturnsAsync(authorization);
+
+ mock.Setup(manager => manager.HasStatusAsync(authorization, Statuses.Valid, It.IsAny()))
+ .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()), Times.AtLeastOnce());
+ Mock.Get(manager).Verify(manager => manager.HasStatusAsync(tokens[0], Statuses.Redeemed, It.IsAny()), Times.Once());
+ Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[0], It.IsAny()), Times.Never());
+ Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[1], It.IsAny()), Times.Never());
+ Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[2], It.IsAny()), 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(builder =>
{
builder.UseInlineHandler(context =>
@@ -3357,6 +3715,8 @@ namespace OpenIddict.Server.IntegrationTests
await using var server = await CreateServerAsync(options =>
{
+ options.DisableRollingRefreshTokens();
+
options.AddEventHandler(builder =>
{
builder.UseInlineHandler(context =>
diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs
index 1f41fa0c..94f4113d 100644
--- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs
+++ b/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(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(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(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(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()), 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()))
- .ReturnsAsync(token);
-
- mock.Setup(manager => manager.GetIdAsync(token, It.IsAny()))
- .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56");
-
- mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny()))
- .ReturnsAsync(true);
-
- mock.Setup(manager => manager.TryRedeemAsync(token, It.IsAny()))
- .ReturnsAsync(false);
- });
-
- await using var server = await CreateServerAsync(options =>
- {
- options.AddEventHandler(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()))
- .ReturnsAsync(application);
-
- mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
- .ReturnsAsync(true);
-
- mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
- .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()), Times.AtLeastOnce());
- Mock.Get(manager).Verify(manager => manager.TryRedeemAsync(token, It.IsAny()), 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(builder =>
@@ -3286,74 +3162,6 @@ namespace OpenIddict.Server.IntegrationTests
Mock.Get(manager).Verify(manager => manager.TryRedeemAsync(token, It.IsAny()), 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()))
- .ReturnsAsync(token);
-
- mock.Setup(manager => manager.GetIdAsync(token, It.IsAny()))
- .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
-
- mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny()))
- .ReturnsAsync(false);
-
- mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny()))
- .ReturnsAsync(true);
-
- mock.Setup(manager => manager.TryRedeemAsync(token, It.IsAny()))
- .ReturnsAsync(false);
- });
-
- await using var server = await CreateServerAsync(options =>
- {
- options.UseRollingRefreshTokens();
- options.DisableAuthorizationStorage();
-
- options.AddEventHandler(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()), Times.AtLeastOnce());
- Mock.Get(manager).Verify(manager => manager.TryRedeemAsync(token, It.IsAny()), 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(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()), Times.AtLeastOnce());
- Mock.Get(manager).Verify(manager => manager.TryRedeemAsync(token, It.IsAny()), 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()))
- .ReturnsAsync(tokens[0]);
-
- mock.Setup(manager => manager.GetIdAsync(tokens[0], It.IsAny()))
- .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
-
- mock.Setup(manager => manager.GetIdAsync(tokens[1], It.IsAny()))
- .ReturnsAsync("481FCAC6-06BC-43EE-92DB-37A78AA09B595073CC313103");
-
- mock.Setup(manager => manager.GetIdAsync(tokens[2], It.IsAny()))
- .ReturnsAsync("3BEA7A94-5ADA-49AF-9F41-8AB6156E31A8");
-
- mock.Setup(manager => manager.GetAuthorizationIdAsync(tokens[0], It.IsAny()))
- .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0");
-
- mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Redeemed, It.IsAny()))
- .ReturnsAsync(false);
-
- mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Valid, It.IsAny()))
- .ReturnsAsync(true);
-
- mock.Setup(manager => manager.TryRedeemAsync(tokens[0], It.IsAny()))
- .ReturnsAsync(true);
-
- mock.Setup(manager => manager.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()))
- .Returns(tokens.ToAsyncEnumerable());
-
- mock.Setup(manager => manager.CreateAsync(It.IsAny(), It.IsAny()))
- .ReturnsAsync(new OpenIddictToken());
- });
-
- await using var server = await CreateServerAsync(options =>
- {
- options.UseRollingRefreshTokens();
+ options.DisableAuthorizationStorage();
+ options.DisableRollingRefreshTokens();
options.AddEventHandler(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()))
- .ReturnsAsync(authorization);
-
- mock.Setup(manager => manager.HasStatusAsync(authorization, Statuses.Valid, It.IsAny()))
- .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()), Times.AtLeastOnce());
- Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[0], It.IsAny()), Times.Never());
- Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[1], It.IsAny()), Times.Once());
- Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[2], It.IsAny()), 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()))
- .ReturnsAsync(tokens[0]);
-
- mock.Setup(manager => manager.GetIdAsync(tokens[0], It.IsAny()))
- .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
-
- mock.Setup(manager => manager.GetIdAsync(tokens[1], It.IsAny()))
- .ReturnsAsync("481FCAC6-06BC-43EE-92DB-37A78AA09B595073CC313103");
-
- mock.Setup(manager => manager.GetIdAsync(tokens[2], It.IsAny()))
- .ReturnsAsync("3BEA7A94-5ADA-49AF-9F41-8AB6156E31A8");
-
- mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Redeemed, It.IsAny()))
- .ReturnsAsync(false);
-
- mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Valid, It.IsAny()))
- .ReturnsAsync(true);
-
- mock.Setup(manager => manager.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()))
- .Returns(tokens.ToAsyncEnumerable());
-
- mock.Setup(manager => manager.CreateAsync(It.IsAny(), It.IsAny()))
- .ReturnsAsync(new OpenIddictToken());
- });
-
- await using var server = await CreateServerAsync(options =>
- {
- options.AddEventHandler(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()))
- .ReturnsAsync(authorization);
-
- mock.Setup(manager => manager.HasStatusAsync(authorization, Statuses.Valid, It.IsAny()))
- .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()), Times.AtLeastOnce());
- Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[0], It.IsAny()), Times.Never());
- Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[1], It.IsAny()), Times.Never());
- Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[2], It.IsAny()), 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()))
- .ReturnsAsync(token);
-
- mock.Setup(manager => manager.GetIdAsync(token, It.IsAny()))
- .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
-
- mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny()))
- .ReturnsAsync(false);
-
- mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny()))
- .ReturnsAsync(true);
-
- mock.Setup(manager => manager.CreateAsync(It.IsAny(), It.IsAny()))
- .ReturnsAsync(new OpenIddictToken());
- });
-
- await using var server = await CreateServerAsync(options =>
- {
- options.AddEventHandler(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(), It.IsAny()), 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()))
- .ReturnsAsync(token);
-
- mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny()))
- .ReturnsAsync(false);
-
- mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny()))
- .ReturnsAsync(true);
-
- mock.Setup(manager => manager.CreateAsync(It.IsAny(), It.IsAny()))
- .ReturnsAsync(new OpenIddictToken());
- });
-
- await using var server = await CreateServerAsync(options =>
- {
- options.DisableSlidingRefreshTokenExpiration();
-
- options.AddEventHandler(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(), It.IsAny()), 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()))
- .ReturnsAsync(token);
-
- mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny()))
- .ReturnsAsync(false);
-
- mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny()))
- .ReturnsAsync(true);
-
- mock.Setup(manager => manager.GetExpirationDateAsync(token, It.IsAny()))
- .ReturnsAsync(value: null);
-
- mock.Setup(manager => manager.CreateAsync(It.IsAny(), It.IsAny()))
- .ReturnsAsync(new OpenIddictToken());
- });
-
- await using var server = await CreateServerAsync(options =>
- {
- options.AddEventHandler(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()), 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()))
- .ReturnsAsync(token);
-
- mock.Setup(manager => manager.GetIdAsync(token, It.IsAny()))
- .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
-
- mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny()))
- .ReturnsAsync(false);
-
- mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny()))
- .ReturnsAsync(true);
-
- mock.Setup(manager => manager.GetExpirationDateAsync(token, It.IsAny()))
- .ReturnsAsync(DateTimeOffset.Now + TimeSpan.FromDays(1));
-
- mock.Setup(manager => manager.CreateAsync(It.IsAny(), It.IsAny()))
- .ReturnsAsync(new OpenIddictToken());
- });
-
- await using var server = await CreateServerAsync(options =>
- {
- options.SetRefreshTokenLifetime(lifetime: null);
-
- options.AddEventHandler(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()), 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()))
- .ReturnsAsync(token);
-
- mock.Setup(manager => manager.GetIdAsync(token, It.IsAny()))
- .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
-
- mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny()))
- .ReturnsAsync(false);
-
- mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny()))
- .ReturnsAsync(true);
-
- mock.Setup(manager => manager.TryExtendAsync(token, It.IsAny(), It.IsAny()))
- .ReturnsAsync(false);
-
- mock.Setup(manager => manager.CreateAsync(It.IsAny(), It.IsAny()))
- .ReturnsAsync(new OpenIddictToken());
- });
-
- await using var server = await CreateServerAsync(options =>
- {
- options.AddEventHandler(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(), It.IsAny()), Times.Once());
+ Mock.Get(manager).Verify(manager => manager.TryRedeemAsync(token, It.IsAny()), Times.Never());
}
[Fact]
diff --git a/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs b/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs
index 5a7215b6..27d08474 100644
--- a/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs
+++ b/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(() => 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(() => 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();