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();