From f84a10270ee6a010a3cb1447796ff9853c047dff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Thu, 20 Aug 2020 16:10:36 +0200 Subject: [PATCH] Revamp the authorizations/tokens pruning mechanism --- samples/Mvc.Server/Mvc.Server.csproj | 1 + .../OpenIddictAuthorizationDescriptor.cs | 5 + .../IOpenIddictAuthorizationManager.cs | 21 +- .../Managers/IOpenIddictTokenManager.cs | 6 +- .../OpenIddictBuilder.cs | 16 +- .../Resources/OpenIddictResources.resx | 4 + .../Resources/xlf/OpenIddictResources.ar.xlf | 2 +- .../Resources/xlf/OpenIddictResources.es.xlf | 2 +- .../Resources/xlf/OpenIddictResources.tr.xlf | 4 +- .../xlf/OpenIddictResources.zh-Hans.xlf | 238 +++++++++--------- .../xlf/OpenIddictResources.zh-Hant.xlf | 238 +++++++++--------- .../Stores/IOpenIddictAuthorizationStore.cs | 30 ++- .../Stores/IOpenIddictTokenStore.cs | 6 +- .../OpenIddictAuthorizationManager.cs | 43 +++- .../Managers/OpenIddictTokenManager.cs | 12 +- src/OpenIddict.Core/OpenIddictCoreBuilder.cs | 16 +- .../OpenIddictEntityFrameworkAuthorization.cs | 5 + .../OpenIddictEntityFrameworkToken.cs | 4 +- .../OpenIddictEntityFrameworkBuilder.cs | 16 +- ...IddictEntityFrameworkAuthorizationStore.cs | 39 ++- .../OpenIddictEntityFrameworkTokenStore.cs | 22 +- ...nIddictEntityFrameworkCoreAuthorization.cs | 5 + .../OpenIddictEntityFrameworkCoreToken.cs | 4 +- .../OpenIddictEntityFrameworkCoreBuilder.cs | 16 +- ...ctEntityFrameworkCoreAuthorizationStore.cs | 39 ++- ...OpenIddictEntityFrameworkCoreTokenStore.cs | 22 +- .../OpenIddictMongoDbAuthorization.cs | 5 + .../OpenIddictMongoDbToken.cs | 4 +- .../OpenIddictMongoDbBuilder.cs | 16 +- .../OpenIddictMongoDbAuthorizationStore.cs | 42 +++- .../Stores/OpenIddictMongoDbTokenStore.cs | 48 +++- .../OpenIddictServerAspNetCoreBuilder.cs | 16 +- .../OpenIddictServerDataProtectionBuilder.cs | 16 +- .../OpenIddictServerOwinBuilder.cs | 16 +- .../OpenIddictServerQuartzBuilder.cs | 38 ++- .../OpenIddictServerQuartzJob.cs | 27 +- .../OpenIddictServerQuartzOptions.cs | 18 +- .../OpenIddictServerBuilder.cs | 16 +- .../OpenIddictServerHandlers.cs | 1 + .../OpenIddictValidationAspNetCoreBuilder.cs | 16 +- ...enIddictValidationDataProtectionBuilder.cs | 16 +- .../OpenIddictValidationOwinBuilder.cs | 16 +- ...ddictValidationServerIntegrationBuilder.cs | 16 +- ...penIddictValidationSystemNetHttpBuilder.cs | 16 +- .../OpenIddictValidationBuilder.cs | 16 +- .../OpenIddictServerIntegrationTests.cs | 1 + .../OpenIddictServerQuartzBuilderTests.cs | 72 +++++- .../OpenIddictServerQuartzJobTests.cs | 84 +++---- 48 files changed, 745 insertions(+), 587 deletions(-) diff --git a/samples/Mvc.Server/Mvc.Server.csproj b/samples/Mvc.Server/Mvc.Server.csproj index 5e76d4a6..164c4213 100644 --- a/samples/Mvc.Server/Mvc.Server.csproj +++ b/samples/Mvc.Server/Mvc.Server.csproj @@ -3,6 +3,7 @@ netcoreapp3.1 false + false false diff --git a/src/OpenIddict.Abstractions/Descriptors/OpenIddictAuthorizationDescriptor.cs b/src/OpenIddict.Abstractions/Descriptors/OpenIddictAuthorizationDescriptor.cs index f53911a5..88563153 100644 --- a/src/OpenIddict.Abstractions/Descriptors/OpenIddictAuthorizationDescriptor.cs +++ b/src/OpenIddict.Abstractions/Descriptors/OpenIddictAuthorizationDescriptor.cs @@ -14,6 +14,11 @@ namespace OpenIddict.Abstractions /// public string? ApplicationId { get; set; } + /// + /// Gets or sets the creation date associated with the authorization. + /// + public DateTimeOffset? CreationDate { get; set; } + /// /// Gets or sets the optional principal associated with the authorization. /// Note: this property is not stored by the default authorization stores. diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs index 4d6f111c..ed2dbf3d 100644 --- a/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs @@ -212,6 +212,17 @@ namespace OpenIddict.Abstractions Func, TState, IQueryable> query, TState state, CancellationToken cancellationToken = default); + /// + /// Retrieves the creation date associated with an authorization. + /// + /// The authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the creation date associated with the specified authorization. + /// + ValueTask GetCreationDateAsync(object authorization, CancellationToken cancellationToken = default); + /// /// Retrieves the unique identifier associated with an authorization. /// @@ -350,13 +361,19 @@ namespace OpenIddict.Abstractions ValueTask PopulateAsync(object authorization, OpenIddictAuthorizationDescriptor descriptor, CancellationToken cancellationToken = default); /// - /// Removes the authorizations that are marked as invalid and the ad-hoc ones that have no valid/nonexpired token attached. + /// Removes the authorizations that are marked as invalid and the ad-hoc ones that have no token attached. + /// Only authorizations created before the specified are removed. /// + /// + /// To ensure ad-hoc authorizations that no longer have any valid/non-expired token + /// attached are correctly removed, the tokens should always be pruned first. + /// + /// The date before which authorizations are not pruned. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation. /// - ValueTask PruneAsync(CancellationToken cancellationToken = default); + ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken = default); /// /// Sets the application identifier associated with an authorization. diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs index ba889d5a..90775c84 100644 --- a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs @@ -375,13 +375,15 @@ namespace OpenIddict.Abstractions ValueTask PopulateAsync(object token, OpenIddictTokenDescriptor descriptor, CancellationToken cancellationToken = default); /// - /// Removes the tokens that are marked as expired or invalid. + /// Removes the tokens that are marked as invalid or whose attached authorization is no longer valid. + /// Only tokens created before the specified are removed. /// + /// The date before which tokens are not pruned. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation. /// - ValueTask PruneAsync(CancellationToken cancellationToken = default); + ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken = default); /// /// Sets the application identifier associated with a token. diff --git a/src/OpenIddict.Abstractions/OpenIddictBuilder.cs b/src/OpenIddict.Abstractions/OpenIddictBuilder.cs index 6274e1e1..a7c0d7c4 100644 --- a/src/OpenIddict.Abstractions/OpenIddictBuilder.cs +++ b/src/OpenIddict.Abstractions/OpenIddictBuilder.cs @@ -27,25 +27,15 @@ namespace Microsoft.Extensions.DependencyInjection [EditorBrowsable(EditorBrowsableState.Never)] public IServiceCollection Services { get; } - /// - /// Determines whether the specified object is equal to the current object. - /// - /// The object to compare with the current object. - /// true if the specified object is equal to the current object; otherwise, false. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override bool Equals(object? obj) => base.Equals(obj); - /// - /// Serves as the default hash function. - /// - /// A hash code for the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override int GetHashCode() => base.GetHashCode(); - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override string? ToString() => base.ToString(); } diff --git a/src/OpenIddict.Abstractions/Resources/OpenIddictResources.resx b/src/OpenIddict.Abstractions/Resources/OpenIddictResources.resx index 92883256..c4fbcd42 100644 --- a/src/OpenIddict.Abstractions/Resources/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/Resources/OpenIddictResources.resx @@ -1377,6 +1377,10 @@ To register the OpenIddict core services, reference the 'OpenIddict.Core' packag The maximum refire count cannot be negative. {Locked} + + The duration cannot be less than 10 minutes. + {Locked} + The security token is missing. diff --git a/src/OpenIddict.Abstractions/Resources/xlf/OpenIddictResources.ar.xlf b/src/OpenIddict.Abstractions/Resources/xlf/OpenIddictResources.ar.xlf index 6b33f5ca..17172ecf 100644 --- a/src/OpenIddict.Abstractions/Resources/xlf/OpenIddictResources.ar.xlf +++ b/src/OpenIddict.Abstractions/Resources/xlf/OpenIddictResources.ar.xlf @@ -599,4 +599,4 @@ - + \ No newline at end of file diff --git a/src/OpenIddict.Abstractions/Resources/xlf/OpenIddictResources.es.xlf b/src/OpenIddict.Abstractions/Resources/xlf/OpenIddictResources.es.xlf index 191b0e2e..4fc2ae02 100644 --- a/src/OpenIddict.Abstractions/Resources/xlf/OpenIddictResources.es.xlf +++ b/src/OpenIddict.Abstractions/Resources/xlf/OpenIddictResources.es.xlf @@ -599,4 +599,4 @@ - + \ No newline at end of file diff --git a/src/OpenIddict.Abstractions/Resources/xlf/OpenIddictResources.tr.xlf b/src/OpenIddict.Abstractions/Resources/xlf/OpenIddictResources.tr.xlf index d71d1bb8..2901f2c9 100644 --- a/src/OpenIddict.Abstractions/Resources/xlf/OpenIddictResources.tr.xlf +++ b/src/OpenIddict.Abstractions/Resources/xlf/OpenIddictResources.tr.xlf @@ -519,7 +519,7 @@ The '{0}' claim is malformed or isn't of the expected type. - '{0}' hakkı kusurlu veya beklenen tipte değil. + '{0}' hakkı kusurlu veya beklenen tipte değil. @@ -599,4 +599,4 @@ - + \ No newline at end of file diff --git a/src/OpenIddict.Abstractions/Resources/xlf/OpenIddictResources.zh-Hans.xlf b/src/OpenIddict.Abstractions/Resources/xlf/OpenIddictResources.zh-Hans.xlf index 30fdcbcd..1a25ec8d 100644 --- a/src/OpenIddict.Abstractions/Resources/xlf/OpenIddictResources.zh-Hans.xlf +++ b/src/OpenIddict.Abstractions/Resources/xlf/OpenIddictResources.zh-Hans.xlf @@ -5,597 +5,597 @@ The security token is missing. 缺少安全令牌. - + The specified authorization code is invalid. 指定的授权码无效. - + The specified device code is invalid. 指定的设备码无效. - + The specified refresh token is invalid. 指定的刷新令牌无效. - + The specified token is invalid. 指定的令牌无效. - + The specified token is not an authorization code. 指定的令牌不是授权码. - + The specified token is not an device code. 指定的令牌不是设备码. - + The specified token is not a refresh token. 指定的令牌不是刷新令牌. - + The specified token is not an access token. 指定的令牌不是访问令牌. - + The specified identity token is invalid. 指定的身份令牌无效. - + The specified authorization code has already been redeemed. 指定的授权码已兑换. - + The specified device code has already been redeemed. 指定的设备码已兑换. - + The specified refresh token has already been redeemed. 指定的刷新令牌已兑换. - + The specified token has already been redeemed. 指定的令牌已兑换. - + The authorization has not been granted yet by the end user. 最终用户尚未授权. - + The authorization was denied by the end user. 最终用户拒绝授权. - + The specified authorization code is no longer valid. 指定的授权码不再有效. - + The specified device code is no longer valid. 指定的设备码不再有效. - + The specified refresh token is no longer valid. 指定的刷新令牌不再有效. - + The specified token is no longer valid. 指定的令牌不再有效. - + The authorization associated with the authorization code is no longer valid. 与授权码关联的授权不再有效. - + The authorization associated with the device code is no longer valid. 与设备码关联的授权不再有效. - + The authorization associated with the refresh token is no longer valid. 与刷新令牌关联的授权不再有效. - + The authorization associated with the token is no longer valid. 与令牌关联的授权不再有效. - + The token request was rejected by the authentication server. 身份验证服务器拒绝了令牌请求. - + The user information access demand was rejected by the authentication server. 身份验证服务器拒绝了用户信息访问请求. - + The specified user code is no longer valid. 指定的用户码不再有效. - + The '{0}' parameter is not supported. 不支持参数 ‘{0}’. - + The mandatory '{0}' parameter is missing. 缺少必需参数 ‘{0}’. - + The '{0}' parameter must be a valid absolute URL. 参数 ’{0}’ 必须是有效的绝对路径网址. - + The '{0}' parameter must not include a fragment. 参数 ‘{0}’ 不能包含 fragment. - + The specified '{0}' is not supported. 不支持指定的 ‘{0}’. - + The specified '{0}'/'{1}' combination is invalid. 指定的 ‘{0}’ / ‘{1}’ 组合无效. - + The mandatory '{0}' scope is missing. 缺少必需 Scope ‘{0}’. - + The '{0}' scope is not allowed. 不允许 Scope ‘{0}’. - + The client identifier cannot be null or empty. 客户端 ID 不能为空. - + The '{0}' parameter cannot be used without '{1}'. 参数 ‘{0}’ 要求 ‘{1}’. - + The status cannot be null or empty. 状态不能为空. - + Scopes cannot be null or empty. Scope 不能为空. - + The '{0}' and '{1}' parameters can only be used with a response type containing '{2}'. 参数 ‘{0}’ 和 ‘{1}’ 只能在响应类型包含 ‘{2}’ 时使用. - + The specified '{0}' is not allowed when using PKCE. 使用 PKCE 时不允许指定的 ‘{0}’. - + Scopes cannot contain spaces. Scope 不能包含空格. - + The specified '{0}' is not valid for this client application. 指定的 ‘{0}’ 对此客户端无效. - + The scope name cannot be null or empty. Scope 不能为空. - + The scope name cannot contain spaces. Scope 不能包含空格. - + This client application is not allowed to use the authorization endpoint. 不允许此客户端使用授权端点. - + The client application is not allowed to use the authorization code flow. 不允许此客户端使用授权代码流程. - + The client application is not allowed to use the implicit flow. 不允许此客户端使用隐式流程. - + The client application is not allowed to use the hybrid flow. 不允许此客户端使用混合流程. - + This client application is not allowed to use the specified scope. 不允许此客户端使用指定的 Scope. - + The specified '{0}' is invalid. 指定的 ‘{0}’ 无效. - + The '{0}' parameter is not valid for this client application. 参数 ‘{0}’ 对此客户端无效. - + The '{0}' parameter required for this client application is missing. 未提供此客户端必需参数 ‘{0}’. - + The specified client credentials are invalid. 指定的客户端凭据无效. - + This client application is not allowed to use the device endpoint. 不允许此客户端使用设备端点. - + The '{0}' and '{1}' parameters are required when using the client credentials grant. 使用客户端凭据授权时, 必须提供参数 ‘{0}’ 和 ‘{1}’. - + The '{0}' parameter is required when using the device code grant. 使用设备码授权时, 必须提供参数 ‘{0}’. - + The mandatory '{0}' and/or '{1}' parameters are missing. 未提供必需参数 ‘{0}’ 和/或 ‘{1}’. - + A scope with the same name already exists. 已存在具有相同名称的 Scope. - + This client application is not allowed to use the token endpoint. 不允许此客户端使用令牌端点. - + This client application is not allowed to use the specified grant type. 不允许此客户端使用指定的授予类型. - + The client application is not allowed to use the '{0}' scope. 不允许此客户端使用 Scope ‘{0}’. - + The specified authorization code cannot be used without sending a client identifier. 若不发送客户端 ID, 则不能使用指定的授权码. - + The specified device code cannot be used without sending a client identifier. 若不发送客户端 ID, 则不能使用指定的设备码. - + The specified refresh token cannot be used without sending a client identifier. 若不发送客户端 ID, 则不能使用指定的刷新令牌. - + The specified authorization code cannot be used by this client application. 此客户端不能使用指定的授权码. - + The specified device code cannot be used by this client application. 此客户端不能使用指定的设备码. - + The specified refresh token cannot be used by this client application. 此客户端不能使用指定的刷新令牌. - + The specified '{0}' parameter doesn't match the client redirection address the authorization code was initially sent to. 指定的参数 ‘{0}’ 与最初发送的客户端重定向地址不匹配. - + The '{0}' parameter cannot be used when no '{1}' was specified in the authorization request. 若授权请求中未指定 ‘{1}’, 则不能使用指定的参数 ‘{0}’. - + The '{0}' parameter is not valid in this context. 参数 ‘{0}’ 在此上下文中无效. - + This client application is not allowed to use the introspection endpoint. 不允许此客户端使用自省端点. - + The specified token cannot be introspected. 指定的令牌不能自省. - + The client application is not allowed to introspect the specified token. 不允许此客户端自省指定的令牌. - + This client application is not allowed to use the revocation endpoint. 此客户端不允许使用吊销端点. - + This token cannot be revoked. 无法吊销此令牌. - + The client application is not allowed to revoke the specified token. 不允许此客户端吊销指定的令牌. - + The mandatory '{0}' header is missing. 缺少必需标头 ‘{0}’. - + The specified '{0}' header is invalid. 指定的标头 ’{0}’ 无效. - + This server only accepts HTTPS requests. 此服务器仅接受 HTTPS 请求. - + The specified HTTP method is not valid. 指定的 HTTP 方法无效. - + A token with the same reference identifier already exists. 已存在具有相同引用 ID 的令牌. - + The token type cannot be null or empty. 令牌类型不能为空. - + Multiple client credentials cannot be specified. 无法指定多个客户端凭据. - + The issuer associated to the specified token is not valid. 与指定令牌关联的颁发者无效. - + The specified token is not of the expected type. 指定的令牌不是预期类型. - + The signing key associated to the specified token was not found. 未找到与指定令牌关联的签名密钥. - + The signature associated to the specified token is not valid. 与指定令牌关联的签名无效. - + This resource server is currently unavailable. 此资源服务器当前不可用. - + The specified token doesn't contain any audience. 指定的令牌不包含任何目标受众. - + The specified token cannot be used with this resource server. 指定的令牌不能用于此资源服务器. - + The user represented by the token is not allowed to perform the requested action. 不允许此令牌代表的用户执行请求的操作. - + No issuer could be found in the server configuration. 在服务器配置中找不到颁发者. - + A server configuration containing an invalid issuer was returned. 返回了包含无效颁发者的服务器配置. - + The issuer returned in the server configuration is not valid. 服务器配置中返回的颁发者无效. - + No JWKS endpoint could be found in the server configuration. 在服务器配置中找不到 JWKS 端点. - + A server configuration containing an invalid JWKS endpoint URL was returned. 返回了包含无效 JWKS 端点的服务器配置. - + A server configuration containing an invalid introspection endpoint URL was returned. 返回了包含无效自省端点的服务器配置. - + The JWKS document didn't contain a valid '{0}' node with at least one key. JWKS 文档中没有 ‘{0}’ 节点. - + A JWKS response containing an unsupported key was returned. 返回了包含不受支持的密钥的 JWKS 响应. - + A JWKS response containing an invalid key was returned. 返回了包含无效密钥的 JWKS 响应. - + The mandatory '{0}' parameter couldn't be found in the introspection response. 自省响应中找不到必需参数 ‘{0}’. - + The token was rejected by the remote authentication server. 令牌被远程身份验证服务器拒绝. - + The '{0}' claim is malformed or isn't of the expected type. Claim ‘{0}’ 格式错误或不是预期类型. - + An introspection response containing a malformed issuer was returned. 返回了包含错误格式颁发者的自省响应. - + The issuer returned in the introspection response is not valid. 自省响应返回的颁发者无效. - + The type of the introspected token doesn't match the expected type. 自省令牌的类型与预期类型不匹配. - + An application with the same client identifier already exists. 已存在具有相同客户端 ID 的应用程序. - + Only confidential, hybrid or public applications are supported by the default application manager. 默认应用程序管理器仅支持机密、混合或公共应用程序. - + The client secret cannot be null or empty for a confidential application. 对于机密应用程序, 客户端密钥不能为空. - + A client secret cannot be associated with a public application. 客户端密钥不能与公共应用程序关联. - + Callback URLs cannot contain a fragment. 回调网址不能包含 fragment. - + The authorization type cannot be null or empty. 授权类型不能为空. - + The specified authorization type is not supported by the default token manager. 默认令牌管理器不支持指定的授权类型. - + The client type cannot be null or empty. 客户端类型不能为空. - + Callback URLs cannot be null or empty. 回调网址不能为空. - + Callback URLs must be valid absolute URLs. 回调网址必须是有效的绝对路径网址. - + Removes orphaned tokens and authorizations from the database. 从数据库中删除孤立的令牌和授权. - + Starts the scheduled task at regular intervals. 定期启动计划的任务. - + diff --git a/src/OpenIddict.Abstractions/Resources/xlf/OpenIddictResources.zh-Hant.xlf b/src/OpenIddict.Abstractions/Resources/xlf/OpenIddictResources.zh-Hant.xlf index d14d73c9..1a5997d4 100644 --- a/src/OpenIddict.Abstractions/Resources/xlf/OpenIddictResources.zh-Hant.xlf +++ b/src/OpenIddict.Abstractions/Resources/xlf/OpenIddictResources.zh-Hant.xlf @@ -5,597 +5,597 @@ The security token is missing. 缺少安全權杖. - + The specified authorization code is invalid. 指定的授權代碼無效. - + The specified device code is invalid. 指定的裝置代碼無效. - + The specified refresh token is invalid. 指定的重新整理權杖無效. - + The specified token is invalid. 指定的權杖无效. - + The specified token is not an authorization code. 指定的權杖不是授權代碼. - + The specified token is not an device code. 指定的權杖不是裝置代碼. - + The specified token is not a refresh token. 指定的權杖不是重新整理權杖. - + The specified token is not an access token. 指定的權杖不是存取權杖. - + The specified identity token is invalid. 指定的識別碼權杖無效. - + The specified authorization code has already been redeemed. 指定的授權代碼已兌換. - + The specified device code has already been redeemed. 指定的设备代碼已兌換. - + The specified refresh token has already been redeemed. 指定的重新整理權杖已兌換. - + The specified token has already been redeemed. 指定的權杖已兌換. - + The authorization has not been granted yet by the end user. 使用者尚未批准授權. - + The authorization was denied by the end user. 使用者拒絕授權. - + The specified authorization code is no longer valid. 指定的授權代碼不再有效. - + The specified device code is no longer valid. 指定的裝置代碼不再有效. - + The specified refresh token is no longer valid. 指定的重新整理權杖不再有效. - + The specified token is no longer valid. 指定的權杖不再有效. - + The authorization associated with the authorization code is no longer valid. 與授權代碼關聯的授權不再有效. - + The authorization associated with the device code is no longer valid. 與裝置代碼關聯的授權不再有效. - + The authorization associated with the refresh token is no longer valid. 與重新整理權杖關聯的授權不再有效. - + The authorization associated with the token is no longer valid. 與權杖關聯的授權不再有效. - + The token request was rejected by the authentication server. 身份驗證伺服器拒絕了權杖請求. - + The user information access demand was rejected by the authentication server. 身份驗證伺服器拒絕了使用者資訊存取請求. - + The specified user code is no longer valid. 指定的使用者代碼不再有效. - + The '{0}' parameter is not supported. 不支援引數 ‘{0}’. - + The mandatory '{0}' parameter is missing. 缺少必要引數 ‘{0}’. - + The '{0}' parameter must be a valid absolute URL. 引數 ’{0}’ 必須是有效的絕對路徑網址. - + The '{0}' parameter must not include a fragment. 引數 ‘{0}’ 不能包含 fragment. - + The specified '{0}' is not supported. 不支援指定的 ‘{0}’. - + The specified '{0}'/'{1}' combination is invalid. 指定的 ‘{0}’ / ‘{1}’ 組合無效. - + The mandatory '{0}' scope is missing. 缺少必要 Scope ‘{0}’. - + The '{0}' scope is not allowed. 不允許 Scope ‘{0}’. - + The client identifier cannot be null or empty. 消費者金鑰不能為空. - + The '{0}' parameter cannot be used without '{1}'. 引數 ‘{0}’ 要求 ‘{1}’. - + The status cannot be null or empty. 狀態不能為空. - + Scopes cannot be null or empty. Scope 不能為空. - + The '{0}' and '{1}' parameters can only be used with a response type containing '{2}'. 引數 ‘{0}’ 和 ‘{1}’ 只能在響應型別包含 ‘{2}’ 時使用. - + The specified '{0}' is not allowed when using PKCE. 使用 PKCE 時不允許指定的 ‘{0}’. - + Scopes cannot contain spaces. Scope 不能包含空格. - + The specified '{0}' is not valid for this client application. 指定的 ‘{0}’ 對此連線應用程式無效. - + The scope name cannot be null or empty. Scope 不能為空. - + The scope name cannot contain spaces. Scope 不能包含空格. - + This client application is not allowed to use the authorization endpoint. 不允許此連線應用程式使用授權端點. - + The client application is not allowed to use the authorization code flow. 不允許此連線應用程式使用授權代碼流程. - + The client application is not allowed to use the implicit flow. 不允許此連線應用程式使用隱式流程. - + The client application is not allowed to use the hybrid flow. 不允許此連線應用程式使用混合流程. - + This client application is not allowed to use the specified scope. 不允許此連線應用程式使用指定的 Scope. - + The specified '{0}' is invalid. 指定的 ‘{0}’ 無效. - + The '{0}' parameter is not valid for this client application. 引數 ‘{0}’ 對此連線應用程式無效. - + The '{0}' parameter required for this client application is missing. 未提供此連線應用程式必要引數 ‘{0}’. - + The specified client credentials are invalid. 指定的連線應用程式機密無效. - + This client application is not allowed to use the device endpoint. 不允許此連線應用程式使用裝置端點. - + The '{0}' and '{1}' parameters are required when using the client credentials grant. 使用連線應用程式機密授權時, 必須提供引數 ‘{0}’ 和 ‘{1}’. - + The '{0}' parameter is required when using the device code grant. 使用裝置代碼授權時, 必須提供引數 ‘{0}’. - + The mandatory '{0}' and/or '{1}' parameters are missing. 未提供必要引數 ‘{0}’ 和/或 ‘{1}’. - + A scope with the same name already exists. 已存在具有相同名稱的 Scope. - + This client application is not allowed to use the token endpoint. 不允許此連線應用程式使用權杖端點. - + This client application is not allowed to use the specified grant type. 不允許此連線應用程式使用指定的驗證類型. - + The client application is not allowed to use the '{0}' scope. 不允許此連線應用程式使用 Scope ‘{0}’. - + The specified authorization code cannot be used without sending a client identifier. 若不傳送連線應用程式 ID, 則不能使用指定的授權代碼. - + The specified device code cannot be used without sending a client identifier. 若不傳送連線應用程式 ID, 則不能使用指定的裝置代碼. - + The specified refresh token cannot be used without sending a client identifier. 若不傳送連線應用程式 ID, 則不能使用指定的重新整理權杖. - + The specified authorization code cannot be used by this client application. 此連線應用程式不能使用指定的授權代碼. - + The specified device code cannot be used by this client application. 此連線應用程式不能使用指定的裝置代碼. - + The specified refresh token cannot be used by this client application. 此連線應用程式不能使用指定的重新整理權杖. - + The specified '{0}' parameter doesn't match the client redirection address the authorization code was initially sent to. 指定的引數 ‘{0}’ 與最初發送的連線應用程式重新導向位址不匹配. - + The '{0}' parameter cannot be used when no '{1}' was specified in the authorization request. 若授權請求中未指定 ‘{1}’, 則不能使用指定的引數 ‘{0}’. - + The '{0}' parameter is not valid in this context. 引數 ‘{0}’ 在此語境中無效. - + This client application is not allowed to use the introspection endpoint. 不允許此連線應用程式使用自省端點. - + The specified token cannot be introspected. 指定的權杖不能自省. - + The client application is not allowed to introspect the specified token. 不允許此連線應用程式自省指定的權杖. - + This client application is not allowed to use the revocation endpoint. 此連線應用程式不允許使用吊銷端點. - + This token cannot be revoked. 無法吊銷此權杖. - + The client application is not allowed to revoke the specified token. 不允許此連線應用程式吊銷指定的權杖. - + The mandatory '{0}' header is missing. 缺少必要標頭 ‘{0}’. - + The specified '{0}' header is invalid. 指定的標頭 ’{0}’ 無效. - + This server only accepts HTTPS requests. 此伺服器僅接受 HTTPS 請求. - + The specified HTTP method is not valid. 指定的 HTTP 方法無效. - + A token with the same reference identifier already exists. 已存在具有相同引用 ID 的權杖. - + The token type cannot be null or empty. 權杖型別不能為空. - + Multiple client credentials cannot be specified. 無法指定多個連線應用程式機密. - + The issuer associated to the specified token is not valid. 與指定權杖關聯的頒發者無效. - + The specified token is not of the expected type. 指定的權杖不是預期型別. - + The signing key associated to the specified token was not found. 未找到與指定權杖關聯的簽名金鑰. - + The signature associated to the specified token is not valid. 與指定權杖關聯的簽名無效. - + This resource server is currently unavailable. 此資源伺服器當前不可用. - + The specified token doesn't contain any audience. 指定的權杖不包含任何目標受眾. - + The specified token cannot be used with this resource server. 指定的權杖不能用於此資源伺服器. - + The user represented by the token is not allowed to perform the requested action. 不允許此權杖代表的使用者執行請求的操作. - + No issuer could be found in the server configuration. 在伺服器配置中找不到頒發者. - + A server configuration containing an invalid issuer was returned. 返回了包含無效頒發者的伺服器配置. - + The issuer returned in the server configuration is not valid. 伺服器配置中返回的頒發者無效. - + No JWKS endpoint could be found in the server configuration. 在伺服器配置中找不到 JWKS 端點. - + A server configuration containing an invalid JWKS endpoint URL was returned. 返回了包含無效 JWKS 端點的伺服器配置. - + A server configuration containing an invalid introspection endpoint URL was returned. 返回了包含無效自省端點的伺服器配置. - + The JWKS document didn't contain a valid '{0}' node with at least one key. JWKS 文件中沒有 ‘{0}’ 節點. - + A JWKS response containing an unsupported key was returned. 返回了包含不受支援的金鑰的 JWKS 響應. - + A JWKS response containing an invalid key was returned. 返回了包含無效金鑰的 JWKS 響應. - + The mandatory '{0}' parameter couldn't be found in the introspection response. 自省響應中找不到必要引數 ‘{0}’. - + The token was rejected by the remote authentication server. 權杖被遠端身份驗證伺服器拒絕. - + The '{0}' claim is malformed or isn't of the expected type. Claim ‘{0}’ 格式錯誤或不是預期型別. - + An introspection response containing a malformed issuer was returned. 返回了包含錯誤格式頒發者的自省響應. - + The issuer returned in the introspection response is not valid. 自省響應返回的頒發者無效. - + The type of the introspected token doesn't match the expected type. 自省權杖的型別與預期型別不匹配. - + An application with the same client identifier already exists. 已存在具有相同消費者金鑰的應用程式. - + Only confidential, hybrid or public applications are supported by the default application manager. 預設應用程式管理器僅支援機密、混合或公共應用程式. - + The client secret cannot be null or empty for a confidential application. 對於機密應用程式, 連線應用程式金鑰不能為空. - + A client secret cannot be associated with a public application. 連線應用程式金鑰不能與公共應用程式關聯. - + Callback URLs cannot contain a fragment. 重新導向位址不能包含 fragment. - + The authorization type cannot be null or empty. 授權型別不能為空. - + The specified authorization type is not supported by the default token manager. 預設權杖管理器不支援指定的授權型別. - + The client type cannot be null or empty. 連線應用程式型別不能為空. - + Callback URLs cannot be null or empty. 重新導向位址不能為空. - + Callback URLs must be valid absolute URLs. 重新導向位址必須是有效的絕對路徑網址. - + Removes orphaned tokens and authorizations from the database. 從資料庫中刪除孤立的權杖和授權. - + Starts the scheduled task at regular intervals. 定期啟動計劃的任務. - + diff --git a/src/OpenIddict.Abstractions/Stores/IOpenIddictAuthorizationStore.cs b/src/OpenIddict.Abstractions/Stores/IOpenIddictAuthorizationStore.cs index f2e00fca..acd76f6a 100644 --- a/src/OpenIddict.Abstractions/Stores/IOpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.Abstractions/Stores/IOpenIddictAuthorizationStore.cs @@ -162,6 +162,17 @@ namespace OpenIddict.Abstractions Func, TState, IQueryable> query, TState state, CancellationToken cancellationToken); + /// + /// Retrieves the creation date associated with an authorization. + /// + /// The authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the creation date associated with the specified authorization. + /// + ValueTask GetCreationDateAsync(TAuthorization authorization, CancellationToken cancellationToken); + /// /// Retrieves the unique identifier associated with an authorization. /// @@ -261,11 +272,17 @@ namespace OpenIddict.Abstractions TState state, CancellationToken cancellationToken); /// - /// Removes the authorizations that are marked as invalid and the ad-hoc ones that have no valid/nonexpired token attached. + /// Removes the authorizations that are marked as invalid and the ad-hoc ones that have no token attached. + /// Only authorizations created before the specified are removed. /// + /// + /// To ensure ad-hoc authorizations that no longer have any valid/non-expired token + /// attached are correctly removed, the tokens should always be pruned first. + /// + /// The date before which authorizations are not pruned. /// The that can be used to abort the operation. /// A that can be used to monitor the asynchronous operation. - ValueTask PruneAsync(CancellationToken cancellationToken); + ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken); /// /// Sets the application identifier associated with an authorization. @@ -276,6 +293,15 @@ namespace OpenIddict.Abstractions /// A that can be used to monitor the asynchronous operation. ValueTask SetApplicationIdAsync(TAuthorization authorization, string? identifier, CancellationToken cancellationToken); + /// + /// Sets the creation date associated with an authorization. + /// + /// The authorization. + /// The expiration date. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + ValueTask SetCreationDateAsync(TAuthorization authorization, DateTimeOffset? date, CancellationToken cancellationToken); + /// /// Sets the additional properties associated with an authorization. /// diff --git a/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs b/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs index 04918101..d64a839a 100644 --- a/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs +++ b/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs @@ -312,11 +312,13 @@ namespace OpenIddict.Abstractions TState state, CancellationToken cancellationToken); /// - /// Removes the tokens that are marked as expired or invalid. + /// Removes the tokens that are marked as invalid or whose attached authorization is no longer valid. + /// Only tokens created before the specified are removed. /// + /// The date before which tokens are not pruned. /// The that can be used to abort the operation. /// A that can be used to monitor the asynchronous operation. - ValueTask PruneAsync(CancellationToken cancellationToken); + ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken); /// /// Sets the application identifier associated with a token. diff --git a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs index 5d8d75ae..55d37f86 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs @@ -683,6 +683,26 @@ namespace OpenIddict.Core return Store.GetAsync(query, state, cancellationToken); } + /// + /// Retrieves the creation date associated with an authorization. + /// + /// The authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the creation date associated with the specified authorization. + /// + public virtual ValueTask GetCreationDateAsync( + TAuthorization authorization, CancellationToken cancellationToken = default) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + return Store.GetCreationDateAsync(authorization, cancellationToken); + } + /// /// Retrieves the unique identifier associated with an authorization. /// @@ -920,7 +940,8 @@ namespace OpenIddict.Core } await Store.SetApplicationIdAsync(authorization, descriptor.ApplicationId, cancellationToken); - await Store.SetScopesAsync(authorization, ImmutableArray.CreateRange(descriptor.Scopes), cancellationToken); + await Store.SetCreationDateAsync(authorization, descriptor.CreationDate, cancellationToken); + await Store.SetScopesAsync(authorization, descriptor.Scopes.ToImmutableArray(), cancellationToken); await Store.SetStatusAsync(authorization, descriptor.Status, cancellationToken); await Store.SetSubjectAsync(authorization, descriptor.Subject, cancellationToken); await Store.SetTypeAsync(authorization, descriptor.Type, cancellationToken); @@ -950,6 +971,7 @@ namespace OpenIddict.Core } descriptor.ApplicationId = await Store.GetApplicationIdAsync(authorization, cancellationToken); + descriptor.CreationDate = await Store.GetCreationDateAsync(authorization, cancellationToken); descriptor.Scopes.Clear(); descriptor.Scopes.UnionWith(await Store.GetScopesAsync(authorization, cancellationToken)); descriptor.Status = await Store.GetStatusAsync(authorization, cancellationToken); @@ -958,14 +980,20 @@ namespace OpenIddict.Core } /// - /// Removes the authorizations that are marked as invalid and the ad-hoc ones that have no valid/nonexpired token attached. + /// Removes the authorizations that are marked as invalid and the ad-hoc ones that have no token attached. + /// Only authorizations created before the specified are removed. /// + /// + /// To ensure ad-hoc authorizations that no longer have any valid/non-expired token + /// attached are correctly removed, the tokens should always be pruned first. + /// + /// The date before which authorizations are not pruned. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation. /// - public virtual ValueTask PruneAsync(CancellationToken cancellationToken = default) - => Store.PruneAsync(cancellationToken); + public virtual ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken = default) + => Store.PruneAsync(threshold, cancellationToken); /// /// Sets the application identifier associated with an authorization. @@ -1225,6 +1253,9 @@ namespace OpenIddict.Core ValueTask IOpenIddictAuthorizationManager.GetAsync(Func, TState, IQueryable> query, TState state, CancellationToken cancellationToken) => GetAsync(query, state, cancellationToken); + ValueTask IOpenIddictAuthorizationManager.GetCreationDateAsync(object authorization, CancellationToken cancellationToken) + => GetCreationDateAsync((TAuthorization) authorization, cancellationToken); + /// ValueTask IOpenIddictAuthorizationManager.GetIdAsync(object authorization, CancellationToken cancellationToken) => GetIdAsync((TAuthorization) authorization, cancellationToken); @@ -1278,8 +1309,8 @@ namespace OpenIddict.Core => PopulateAsync((TAuthorization) authorization, descriptor, cancellationToken); /// - ValueTask IOpenIddictAuthorizationManager.PruneAsync(CancellationToken cancellationToken) - => PruneAsync(cancellationToken); + ValueTask IOpenIddictAuthorizationManager.PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken) + => PruneAsync(threshold, cancellationToken); /// ValueTask IOpenIddictAuthorizationManager.SetApplicationIdAsync(object authorization, string? identifier, CancellationToken cancellationToken) diff --git a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs index 665d7797..5a3d6508 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs @@ -984,14 +984,16 @@ namespace OpenIddict.Core } /// - /// Removes the tokens that are marked as expired or invalid. + /// Removes the tokens that are marked as invalid or whose attached authorization is no longer valid. + /// Only tokens created before the specified are removed. /// + /// The date before which tokens are not pruned. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation. /// - public virtual ValueTask PruneAsync(CancellationToken cancellationToken = default) - => Store.PruneAsync(cancellationToken); + public virtual ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken = default) + => Store.PruneAsync(threshold, cancellationToken); /// /// Sets the application identifier associated with a token. @@ -1508,8 +1510,8 @@ namespace OpenIddict.Core => PopulateAsync((TToken) token, descriptor, cancellationToken); /// - ValueTask IOpenIddictTokenManager.PruneAsync(CancellationToken cancellationToken) - => PruneAsync(cancellationToken); + ValueTask IOpenIddictTokenManager.PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken) + => PruneAsync(threshold, cancellationToken); /// ValueTask IOpenIddictTokenManager.SetApplicationIdAsync(object token, string? identifier, CancellationToken cancellationToken) diff --git a/src/OpenIddict.Core/OpenIddictCoreBuilder.cs b/src/OpenIddict.Core/OpenIddictCoreBuilder.cs index ee83367e..bd0c6d73 100644 --- a/src/OpenIddict.Core/OpenIddictCoreBuilder.cs +++ b/src/OpenIddict.Core/OpenIddictCoreBuilder.cs @@ -794,25 +794,15 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => options.EntityCacheLimit = limit); } - /// - /// Determines whether the specified object is equal to the current object. - /// - /// The object to compare with the current object. - /// true if the specified object is equal to the current object; otherwise, false. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override bool Equals(object? obj) => base.Equals(obj); - /// - /// Serves as the default hash function. - /// - /// A hash code for the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override int GetHashCode() => base.GetHashCode(); - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override string? ToString() => base.ToString(); } diff --git a/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkAuthorization.cs b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkAuthorization.cs index cb18cf81..0a68bdfd 100644 --- a/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkAuthorization.cs +++ b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkAuthorization.cs @@ -42,6 +42,11 @@ namespace OpenIddict.EntityFramework.Models /// public virtual string? ConcurrencyToken { get; set; } = Guid.NewGuid().ToString(); + /// + /// Gets or sets the creation date of the current authorization. + /// + public virtual DateTimeOffset? CreationDate { get; set; } + /// /// Gets or sets the unique identifier associated with the current authorization. /// diff --git a/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkToken.cs b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkToken.cs index c264753d..3316071c 100644 --- a/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkToken.cs +++ b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkToken.cs @@ -47,12 +47,12 @@ namespace OpenIddict.EntityFramework.Models public virtual string? ConcurrencyToken { get; set; } = Guid.NewGuid().ToString(); /// - /// Gets or sets the date on which the token will start to be considered valid. + /// Gets or sets the creation date of the current token. /// public virtual DateTimeOffset? CreationDate { get; set; } /// - /// Gets or sets the date on which the token will no longer be considered valid. + /// Gets or sets the expiration date of the current token. /// public virtual DateTimeOffset? ExpirationDate { get; set; } diff --git a/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkBuilder.cs b/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkBuilder.cs index 414cfea7..48b486b7 100644 --- a/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkBuilder.cs +++ b/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkBuilder.cs @@ -114,25 +114,15 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => options.DbContextType = type); } - /// - /// Determines whether the specified object is equal to the current object. - /// - /// The object to compare with the current object. - /// true if the specified object is equal to the current object; otherwise, false. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override bool Equals(object? obj) => base.Equals(obj); - /// - /// Serves as the default hash function. - /// - /// A hash code for the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override int GetHashCode() => base.GetHashCode(); - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override string? ToString() => base.ToString(); } diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkAuthorizationStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkAuthorizationStore.cs index 051f5d67..dca2714e 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkAuthorizationStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkAuthorizationStore.cs @@ -414,6 +414,17 @@ namespace OpenIddict.EntityFramework Authorizations.Include(authorization => authorization.Application), state).FirstOrDefaultAsync(cancellationToken); } + /// + public virtual ValueTask GetCreationDateAsync(TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + return new ValueTask(authorization.CreationDate); + } + /// public virtual ValueTask GetIdAsync(TAuthorization authorization, CancellationToken cancellationToken) { @@ -561,7 +572,7 @@ namespace OpenIddict.EntityFramework } /// - public virtual async ValueTask PruneAsync(CancellationToken cancellationToken) + public virtual async ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken) { // Note: Entity Framework 6.x doesn't support set-based deletes, which prevents removing // entities in a single command without having to retrieve and materialize them first. @@ -597,10 +608,9 @@ namespace OpenIddict.EntityFramework var authorizations = await (from authorization in Authorizations.Include(authorization => authorization.Tokens) + where authorization.CreationDate < threshold where authorization.Status != OpenIddictConstants.Statuses.Valid || - (authorization.Type == OpenIddictConstants.AuthorizationTypes.AdHoc && - !authorization.Tokens.Any(token => token.Status == OpenIddictConstants.Statuses.Valid && - token.ExpirationDate > DateTimeOffset.UtcNow)) + (authorization.Type == OpenIddictConstants.AuthorizationTypes.AdHoc && !authorization.Tokens.Any()) orderby authorization.Id select authorization).Skip(offset).Take(1_000).ToListAsync(cancellationToken); @@ -614,7 +624,6 @@ namespace OpenIddict.EntityFramework // repeatable read instead of serializable for performance reasons). In this // case, the operation will fail, which is considered an acceptable risk. Authorizations.RemoveRange(authorizations); - Tokens.RemoveRange(authorizations.SelectMany(authorization => authorization.Tokens)); try { @@ -624,11 +633,7 @@ namespace OpenIddict.EntityFramework catch (Exception exception) { - if (exceptions == null) - { - exceptions = new List(capacity: 1); - } - + exceptions ??= new List(capacity: 1); exceptions.Add(exception); } } @@ -677,6 +682,20 @@ namespace OpenIddict.EntityFramework } } + /// + public virtual ValueTask SetCreationDateAsync(TAuthorization authorization, + DateTimeOffset? date, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + authorization.CreationDate = date; + + return default; + } + /// public virtual ValueTask SetPropertiesAsync(TAuthorization authorization, ImmutableDictionary properties, CancellationToken cancellationToken) diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs index bdc810ea..f537aa88 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs @@ -21,6 +21,7 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using OpenIddict.Abstractions; using OpenIddict.EntityFramework.Models; +using static OpenIddict.Abstractions.OpenIddictConstants; using SR = OpenIddict.Abstractions.OpenIddictResources; namespace OpenIddict.EntityFramework @@ -551,7 +552,7 @@ namespace OpenIddict.EntityFramework } /// - public virtual async ValueTask PruneAsync(CancellationToken cancellationToken) + public virtual async ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken) { // Note: Entity Framework 6.x doesn't support set-based deletes, which prevents removing // entities in a single command without having to retrieve and materialize them first. @@ -587,11 +588,14 @@ namespace OpenIddict.EntityFramework // and thus prevent them from being concurrently modified outside this block. using var transaction = CreateTransaction(); - var tokens = await (from token in Tokens - where token.Status != OpenIddictConstants.Statuses.Valid || - token.ExpirationDate < DateTimeOffset.UtcNow - orderby token.Id - select token).Skip(offset).Take(1_000).ToListAsync(cancellationToken); + var tokens = await + (from token in Tokens + where token.CreationDate < threshold + where (token.Status != Statuses.Inactive && token.Status != Statuses.Valid) || + (token.Authorization != null && token.Authorization.Status != Statuses.Valid) || + token.ExpirationDate < DateTimeOffset.UtcNow + orderby token.Id + select token).Skip(offset).Take(1_000).ToListAsync(cancellationToken); if (tokens.Count == 0) { @@ -608,11 +612,7 @@ namespace OpenIddict.EntityFramework catch (Exception exception) { - if (exceptions == null) - { - exceptions = new List(capacity: 1); - } - + exceptions ??= new List(capacity: 1); exceptions.Add(exception); } } diff --git a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreAuthorization.cs b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreAuthorization.cs index 697ede91..311a148a 100644 --- a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreAuthorization.cs +++ b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreAuthorization.cs @@ -50,6 +50,11 @@ namespace OpenIddict.EntityFrameworkCore.Models /// public virtual string? ConcurrencyToken { get; set; } = Guid.NewGuid().ToString(); + /// + /// Gets or sets the creation date of the current authorization. + /// + public virtual DateTimeOffset? CreationDate { get; set; } + /// /// Gets or sets the unique identifier associated with the current authorization. /// diff --git a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreToken.cs b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreToken.cs index 47d0cb5c..7c211f3b 100644 --- a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreToken.cs +++ b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreToken.cs @@ -55,12 +55,12 @@ namespace OpenIddict.EntityFrameworkCore.Models public virtual string? ConcurrencyToken { get; set; } = Guid.NewGuid().ToString(); /// - /// Gets or sets the date on which the token will start to be considered valid. + /// Gets or sets the creation date of the current token. /// public virtual DateTimeOffset? CreationDate { get; set; } /// - /// Gets or sets the date on which the token will no longer be considered valid. + /// Gets or sets the expiration date of the current token. /// public virtual DateTimeOffset? ExpirationDate { get; set; } diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs index d78ff90f..443e7a4d 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs @@ -114,25 +114,15 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => options.DbContextType = type); } - /// - /// Determines whether the specified object is equal to the current object. - /// - /// The object to compare with the current object. - /// true if the specified object is equal to the current object; otherwise, false. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override bool Equals(object? obj) => base.Equals(obj); - /// - /// Serves as the default hash function. - /// - /// A hash code for the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override int GetHashCode() => base.GetHashCode(); - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override string? ToString() => base.ToString(); } diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs index 3b936cfc..25282d3e 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs @@ -478,6 +478,17 @@ namespace OpenIddict.EntityFrameworkCore .AsTracking(), state).FirstOrDefaultAsync(cancellationToken); } + /// + public virtual ValueTask GetCreationDateAsync(TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + return new ValueTask(authorization.CreationDate); + } + /// public virtual ValueTask GetIdAsync(TAuthorization authorization, CancellationToken cancellationToken) { @@ -627,7 +638,7 @@ namespace OpenIddict.EntityFrameworkCore } /// - public virtual async ValueTask PruneAsync(CancellationToken cancellationToken) + public virtual async ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken) { // Note: Entity Framework Core doesn't support set-based deletes, which prevents removing // entities in a single command without having to retrieve and materialize them first. @@ -673,10 +684,9 @@ namespace OpenIddict.EntityFrameworkCore var authorizations = await (from authorization in Authorizations.Include(authorization => authorization.Tokens).AsTracking() + where authorization.CreationDate < threshold where authorization.Status != OpenIddictConstants.Statuses.Valid || - (authorization.Type == OpenIddictConstants.AuthorizationTypes.AdHoc && - !authorization.Tokens.Any(token => token.Status == OpenIddictConstants.Statuses.Valid && - token.ExpirationDate > DateTimeOffset.UtcNow)) + (authorization.Type == OpenIddictConstants.AuthorizationTypes.AdHoc && !authorization.Tokens.Any()) orderby authorization.Id select authorization).Skip(offset).Take(1_000).ToListAsync(cancellationToken); @@ -690,7 +700,6 @@ namespace OpenIddict.EntityFrameworkCore // repeatable read instead of serializable for performance reasons). In this // case, the operation will fail, which is considered an acceptable risk. Context.RemoveRange(authorizations); - Context.RemoveRange(authorizations.SelectMany(authorization => authorization.Tokens)); try { @@ -700,11 +709,7 @@ namespace OpenIddict.EntityFrameworkCore catch (Exception exception) { - if (exceptions == null) - { - exceptions = new List(capacity: 1); - } - + exceptions ??= new List(capacity: 1); exceptions.Add(exception); } } @@ -759,6 +764,20 @@ namespace OpenIddict.EntityFrameworkCore } } + /// + public virtual ValueTask SetCreationDateAsync(TAuthorization authorization, + DateTimeOffset? date, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + authorization.CreationDate = date; + + return default; + } + /// public virtual ValueTask SetPropertiesAsync(TAuthorization authorization, ImmutableDictionary properties, CancellationToken cancellationToken) diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs index ab0e7fbe..8d1dc3bf 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs @@ -22,6 +22,7 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using OpenIddict.Abstractions; using OpenIddict.EntityFrameworkCore.Models; +using static OpenIddict.Abstractions.OpenIddictConstants; using SR = OpenIddict.Abstractions.OpenIddictResources; namespace OpenIddict.EntityFrameworkCore @@ -599,7 +600,7 @@ namespace OpenIddict.EntityFrameworkCore } /// - public virtual async ValueTask PruneAsync(CancellationToken cancellationToken) + public virtual async ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken) { // Note: Entity Framework Core doesn't support set-based deletes, which prevents removing // entities in a single command without having to retrieve and materialize them first. @@ -645,11 +646,14 @@ namespace OpenIddict.EntityFrameworkCore // and thus prevent them from being concurrently modified outside this block. using var transaction = await CreateTransactionAsync(); - var tokens = await (from token in Tokens.AsTracking() - where token.Status != OpenIddictConstants.Statuses.Valid || - token.ExpirationDate < DateTimeOffset.UtcNow - orderby token.Id - select token).Skip(offset).Take(1_000).ToListAsync(cancellationToken); + var tokens = await + (from token in Tokens.AsTracking() + where token.CreationDate < threshold + where (token.Status != Statuses.Inactive && token.Status != Statuses.Valid) || + (token.Authorization != null && token.Authorization.Status != Statuses.Valid) || + token.ExpirationDate < DateTimeOffset.UtcNow + orderby token.Id + select token).Skip(offset).Take(1_000).ToListAsync(cancellationToken); if (tokens.Count == 0) { @@ -666,11 +670,7 @@ namespace OpenIddict.EntityFrameworkCore catch (Exception exception) { - if (exceptions == null) - { - exceptions = new List(capacity: 1); - } - + exceptions ??= new List(capacity: 1); exceptions.Add(exception); } } diff --git a/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbAuthorization.cs b/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbAuthorization.cs index 6e78ffa3..317adf27 100644 --- a/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbAuthorization.cs +++ b/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbAuthorization.cs @@ -31,6 +31,11 @@ namespace OpenIddict.MongoDb.Models [BsonElement("concurrency_token"), BsonIgnoreIfNull] public virtual string? ConcurrencyToken { get; set; } = Guid.NewGuid().ToString(); + /// + /// Gets or sets the creation date of the current authorization. + /// + public virtual DateTime? CreationDate { get; set; } + /// /// Gets or sets the unique identifier associated with the current authorization. /// diff --git a/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbToken.cs b/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbToken.cs index 7a5f177f..5ba033ed 100644 --- a/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbToken.cs +++ b/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbToken.cs @@ -36,13 +36,13 @@ namespace OpenIddict.MongoDb.Models public virtual string? ConcurrencyToken { get; set; } = Guid.NewGuid().ToString(); /// - /// Gets or sets the date on which the token will start to be considered valid. + /// Gets or sets the creation date of the current token. /// [BsonElement("creation_date"), BsonIgnoreIfNull] public virtual DateTime? CreationDate { get; set; } /// - /// Gets or sets the date on which the token will no longer be considered valid. + /// Gets or sets the expiration date of the current token. /// [BsonElement("expiration_date"), BsonIgnoreIfNull] public virtual DateTime? ExpirationDate { get; set; } diff --git a/src/OpenIddict.MongoDb/OpenIddictMongoDbBuilder.cs b/src/OpenIddict.MongoDb/OpenIddictMongoDbBuilder.cs index a94c9196..e2ec1599 100644 --- a/src/OpenIddict.MongoDb/OpenIddictMongoDbBuilder.cs +++ b/src/OpenIddict.MongoDb/OpenIddictMongoDbBuilder.cs @@ -174,25 +174,15 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => options.Database = database); } - /// - /// Determines whether the specified object is equal to the current object. - /// - /// The object to compare with the current object. - /// true if the specified object is equal to the current object; otherwise, false. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override bool Equals(object? obj) => base.Equals(obj); - /// - /// Serves as the default hash function. - /// - /// A hash code for the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override int GetHashCode() => base.GetHashCode(); - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override string? ToString() => base.ToString(); } diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbAuthorizationStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbAuthorizationStore.cs index a5b0b8cf..b2fbd743 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbAuthorizationStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbAuthorizationStore.cs @@ -361,6 +361,17 @@ namespace OpenIddict.MongoDb return await ((IMongoQueryable) query(collection.AsQueryable(), state)).FirstOrDefaultAsync(cancellationToken); } + /// + public virtual ValueTask GetCreationDateAsync(TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + return new ValueTask(authorization.CreationDate); + } + /// public virtual ValueTask GetIdAsync(TAuthorization authorization, CancellationToken cancellationToken) { @@ -503,7 +514,7 @@ namespace OpenIddict.MongoDb } /// - public virtual async ValueTask PruneAsync(CancellationToken cancellationToken) + public virtual async ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken) { var database = await Context.GetDatabaseAsync(cancellationToken); var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); @@ -516,10 +527,9 @@ namespace OpenIddict.MongoDb await (from authorization in collection.AsQueryable() join token in database.GetCollection(Options.CurrentValue.TokensCollectionName).AsQueryable() on authorization.Id equals token.AuthorizationId into tokens + where authorization.CreationDate < threshold.UtcDateTime where authorization.Status != OpenIddictConstants.Statuses.Valid || - (authorization.Type == OpenIddictConstants.AuthorizationTypes.AdHoc && - !tokens.Any(token => token.Status == OpenIddictConstants.Statuses.Valid && - token.ExpirationDate > DateTime.UtcNow)) + (authorization.Type == OpenIddictConstants.AuthorizationTypes.AdHoc && !tokens.Any()) select authorization.Id).ToListAsync(cancellationToken); // Note: to avoid generating delete requests with very large filters, a buffer is used here and the @@ -527,10 +537,6 @@ namespace OpenIddict.MongoDb foreach (var buffer in Buffer(identifiers.Take(50_000), 1_000)) { await collection.DeleteManyAsync(authorization => buffer.Contains(authorization.Id)); - - // Delete the tokens associated with the pruned authorizations. - await database.GetCollection(Options.CurrentValue.TokensCollectionName) - .DeleteManyAsync(token => buffer.Contains(token.AuthorizationId), cancellationToken); } static IEnumerable> Buffer(IEnumerable source, int count) @@ -539,11 +545,7 @@ namespace OpenIddict.MongoDb foreach (var element in source) { - if (buffer == null) - { - buffer = new List(); - } - + buffer ??= new List(capacity: 1); buffer.Add(element); if (buffer.Count == count) @@ -583,6 +585,20 @@ namespace OpenIddict.MongoDb return default; } + /// + public virtual ValueTask SetCreationDateAsync(TAuthorization authorization, + DateTimeOffset? date, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + authorization.CreationDate = date?.UtcDateTime; + + return default; + } + /// public virtual ValueTask SetPropertiesAsync(TAuthorization authorization, ImmutableDictionary properties, CancellationToken cancellationToken) diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs index 4bd7478e..6dac66b6 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs @@ -19,6 +19,7 @@ using MongoDB.Driver; using MongoDB.Driver.Linq; using OpenIddict.Abstractions; using OpenIddict.MongoDb.Models; +using static OpenIddict.Abstractions.OpenIddictConstants; using SR = OpenIddict.Abstractions.OpenIddictResources; namespace OpenIddict.MongoDb @@ -529,13 +530,54 @@ namespace OpenIddict.MongoDb } /// - public virtual async ValueTask PruneAsync(CancellationToken cancellationToken) + public virtual async ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken) { var database = await Context.GetDatabaseAsync(cancellationToken); var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); - await collection.DeleteManyAsync(token => token.Status != OpenIddictConstants.Statuses.Valid || - token.ExpirationDate < DateTime.UtcNow, cancellationToken); + // Note: directly deleting the resulting set of an aggregate query is not supported by MongoDB. + // To work around this limitation, the token identifiers are stored in an intermediate list + // and delete requests are sent to remove the documents corresponding to these identifiers. + + var identifiers = + await (from token in collection.AsQueryable() + join authorization in database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName).AsQueryable() + on token.AuthorizationId equals authorization.Id into authorizations + where token.CreationDate < threshold + where (token.Status != Statuses.Inactive && token.Status != Statuses.Valid) || + token.ExpirationDate < DateTime.UtcNow || + authorizations.Any(authorization => authorization.Status != Statuses.Valid) + select token.Id).ToListAsync(cancellationToken); + + // Note: to avoid generating delete requests with very large filters, a buffer is used here and the + // maximum number of elements that can be removed by a single call to PruneAsync() is limited to 50000. + foreach (var buffer in Buffer(identifiers.Take(50_000), 1_000)) + { + await collection.DeleteManyAsync(token => buffer.Contains(token.Id)); + } + + static IEnumerable> Buffer(IEnumerable source, int count) + { + List? buffer = null; + + foreach (var element in source) + { + buffer ??= new List(capacity: 1); + buffer.Add(element); + + if (buffer.Count == count) + { + yield return buffer; + + buffer = null; + } + } + + if (buffer != null) + { + yield return buffer; + } + } } /// diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs index d384cc5d..60cb703f 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs @@ -196,25 +196,15 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => options.LogoutEndpointCachingPolicy = policy); } - /// - /// Determines whether the specified object is equal to the current object. - /// - /// The object to compare with the current object. - /// true if the specified object is equal to the current object; otherwise, false. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override bool Equals(object? obj) => base.Equals(obj); - /// - /// Serves as the default hash function. - /// - /// A hash code for the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override int GetHashCode() => base.GetHashCode(); - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override string? ToString() => base.ToString(); } diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionBuilder.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionBuilder.cs index a47c1aaa..99456e91 100644 --- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionBuilder.cs +++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionBuilder.cs @@ -114,25 +114,15 @@ namespace Microsoft.Extensions.DependencyInjection public OpenIddictServerDataProtectionBuilder PreferDefaultUserCodeFormat() => Configure(options => options.PreferDefaultUserCodeFormat = true); - /// - /// Determines whether the specified object is equal to the current object. - /// - /// The object to compare with the current object. - /// true if the specified object is equal to the current object; otherwise, false. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override bool Equals(object? obj) => base.Equals(obj); - /// - /// Serves as the default hash function. - /// - /// A hash code for the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override int GetHashCode() => base.GetHashCode(); - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override string? ToString() => base.ToString(); } diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs index eb6b2a77..310fa59e 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs @@ -185,25 +185,15 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => options.LogoutEndpointCachingPolicy = policy); } - /// - /// Determines whether the specified object is equal to the current object. - /// - /// The object to compare with the current object. - /// true if the specified object is equal to the current object; otherwise, false. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override bool Equals(object? obj) => base.Equals(obj); - /// - /// Serves as the default hash function. - /// - /// A hash code for the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override int GetHashCode() => base.GetHashCode(); - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override string? ToString() => base.ToString(); } diff --git a/src/OpenIddict.Server.Quartz/OpenIddictServerQuartzBuilder.cs b/src/OpenIddict.Server.Quartz/OpenIddictServerQuartzBuilder.cs index 7bca725d..f07d1f72 100644 --- a/src/OpenIddict.Server.Quartz/OpenIddictServerQuartzBuilder.cs +++ b/src/OpenIddict.Server.Quartz/OpenIddictServerQuartzBuilder.cs @@ -52,15 +52,15 @@ namespace Microsoft.Extensions.DependencyInjection /// Disables authorizations pruning. /// /// The . - public OpenIddictServerQuartzBuilder DisableAuthorizationsPruning() - => Configure(options => options.DisableAuthorizationsPruning = true); + public OpenIddictServerQuartzBuilder DisableAuthorizationPruning() + => Configure(options => options.DisableAuthorizationPruning = true); /// /// Disables tokens pruning. /// /// The . - public OpenIddictServerQuartzBuilder DisableTokensPruning() - => Configure(options => options.DisableTokensPruning = true); + public OpenIddictServerQuartzBuilder DisableTokenPruning() + => Configure(options => options.DisableTokenPruning = true); /// /// Sets the number of times a failed Quartz.NET job can be retried. @@ -77,6 +77,36 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => options.MaximumRefireCount = count); } + /// + /// Sets the minimum lifespan authorizations must have to be pruned. + /// + /// The minimum lifespan authorizations must have to be pruned. + /// The . + public OpenIddictServerQuartzBuilder SetMinimumAuthorizationLifespan(TimeSpan lifespan) + { + if (lifespan < TimeSpan.FromMinutes(10)) + { + throw new ArgumentOutOfRangeException(nameof(lifespan), SR.GetResourceString(SR.ID1279)); + } + + return Configure(options => options.MinimumAuthorizationLifespan = lifespan); + } + + /// + /// Sets the minimum lifespan tokens must have to be pruned. + /// + /// The minimum lifespan tokens must have to be pruned. + /// The . + public OpenIddictServerQuartzBuilder SetMinimumTokenLifespan(TimeSpan lifespan) + { + if (lifespan < TimeSpan.FromMinutes(10)) + { + throw new ArgumentOutOfRangeException(nameof(lifespan), SR.GetResourceString(SR.ID1279)); + } + + return Configure(options => options.MinimumTokenLifespan = lifespan); + } + /// [EditorBrowsable(EditorBrowsableState.Never)] public override bool Equals(object? obj) => base.Equals(obj); diff --git a/src/OpenIddict.Server.Quartz/OpenIddictServerQuartzJob.cs b/src/OpenIddict.Server.Quartz/OpenIddictServerQuartzJob.cs index 189e95f9..2586b665 100644 --- a/src/OpenIddict.Server.Quartz/OpenIddictServerQuartzJob.cs +++ b/src/OpenIddict.Server.Quartz/OpenIddictServerQuartzJob.cs @@ -65,15 +65,12 @@ namespace OpenIddict.Server.Quartz { // Note: this background task is responsible of automatically removing orphaned tokens/authorizations // (i.e tokens that are no longer valid and ad-hoc authorizations that have no valid tokens associated). - // Since ad-hoc authorizations and their associated tokens are removed as part of the same operation - // when they no longer have any token attached, it's more efficient to remove the authorizations first. + // Import: since tokens associated to ad-hoc authorizations are not removed as part of the same operation, + // the tokens MUST be deleted before removing the ad-hoc authorizations that no longer have any token. - // Note: the authorization/token managers MUST be resolved from the scoped provider - // as they depend on scoped stores that should be disposed as soon as possible. - - if (!_options.CurrentValue.DisableAuthorizationsPruning) + if (!_options.CurrentValue.DisableTokenPruning) { - var manager = scope.ServiceProvider.GetService(); + var manager = scope.ServiceProvider.GetService(); if (manager == null) { // Inform Quartz.NET that the triggers associated with this job should be removed, @@ -87,9 +84,11 @@ namespace OpenIddict.Server.Quartz }; } + var threshold = DateTimeOffset.UtcNow - _options.CurrentValue.MinimumTokenLifespan; + try { - await manager.PruneAsync(context.CancellationToken); + await manager.PruneAsync(threshold, context.CancellationToken); } // OutOfMemoryExceptions are treated as fatal errors and are always re-thrown as-is. @@ -113,7 +112,7 @@ namespace OpenIddict.Server.Quartz // occurred while trying to prune the entities. In this case, add the inner exceptions to the collection. catch (AggregateException exception) { - exceptions ??= new List(capacity: 1); + exceptions ??= new List(capacity: exception.InnerExceptions.Count); exceptions.AddRange(exception.InnerExceptions); } @@ -126,9 +125,9 @@ namespace OpenIddict.Server.Quartz } } - if (!_options.CurrentValue.DisableTokensPruning) + if (!_options.CurrentValue.DisableAuthorizationPruning) { - var manager = scope.ServiceProvider.GetService(); + var manager = scope.ServiceProvider.GetService(); if (manager == null) { // Inform Quartz.NET that the triggers associated with this job should be removed, @@ -142,9 +141,11 @@ namespace OpenIddict.Server.Quartz }; } + var threshold = DateTimeOffset.UtcNow - _options.CurrentValue.MinimumAuthorizationLifespan; + try { - await manager.PruneAsync(context.CancellationToken); + await manager.PruneAsync(threshold, context.CancellationToken); } // OutOfMemoryExceptions are treated as fatal errors and are always re-thrown as-is. @@ -168,7 +169,7 @@ namespace OpenIddict.Server.Quartz // occurred while trying to prune the entities. In this case, add the inner exceptions to the collection. catch (AggregateException exception) { - exceptions ??= new List(capacity: 1); + exceptions ??= new List(capacity: exception.InnerExceptions.Count); exceptions.AddRange(exception.InnerExceptions); } diff --git a/src/OpenIddict.Server.Quartz/OpenIddictServerQuartzOptions.cs b/src/OpenIddict.Server.Quartz/OpenIddictServerQuartzOptions.cs index 948f66c3..d19c8329 100644 --- a/src/OpenIddict.Server.Quartz/OpenIddictServerQuartzOptions.cs +++ b/src/OpenIddict.Server.Quartz/OpenIddictServerQuartzOptions.cs @@ -4,6 +4,8 @@ * the license and the contributors participating to this project. */ +using System; + namespace OpenIddict.Server.Quartz { /// @@ -14,17 +16,29 @@ namespace OpenIddict.Server.Quartz /// /// Gets or sets a boolean indicating whether authorizations pruning should be disabled. /// - public bool DisableAuthorizationsPruning { get; set; } + public bool DisableAuthorizationPruning { get; set; } /// /// Gets or sets a boolean indicating whether tokens pruning should be disabled. /// - public bool DisableTokensPruning { get; set; } + public bool DisableTokenPruning { get; set; } /// /// Gets or sets the number of times a failed Quartz.NET job can be retried. /// By default, failed jobs are automatically retried twice after the initial failure. /// public int MaximumRefireCount { get; set; } = 2; + + /// + /// Gets or sets the minimum lifespan authorizations must have to be pruned. + /// By default, this value is set to 14 days and cannot be less than 10 minutes. + /// + public TimeSpan MinimumAuthorizationLifespan { get; set; } = TimeSpan.FromDays(14); + + /// + /// Gets or sets the minimum lifespan tokens must have to be pruned. + /// By default, this value is set to 14 days and cannot be less than 10 minutes. + /// + public TimeSpan MinimumTokenLifespan { get; set; } = TimeSpan.FromDays(14); } } diff --git a/src/OpenIddict.Server/OpenIddictServerBuilder.cs b/src/OpenIddict.Server/OpenIddictServerBuilder.cs index 8fb6aadc..4360c644 100644 --- a/src/OpenIddict.Server/OpenIddictServerBuilder.cs +++ b/src/OpenIddict.Server/OpenIddictServerBuilder.cs @@ -1793,25 +1793,15 @@ namespace Microsoft.Extensions.DependencyInjection public OpenIddictServerBuilder UseRollingRefreshTokens() => Configure(options => options.UseRollingRefreshTokens = true); - /// - /// Determines whether the specified object is equal to the current object. - /// - /// The object to compare with the current object. - /// true if the specified object is equal to the current object; otherwise, false. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override bool Equals(object? obj) => base.Equals(obj); - /// - /// Serves as the default hash function. - /// - /// A hash code for the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override int GetHashCode() => base.GetHashCode(); - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override string? ToString() => base.ToString(); } diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index 274cd095..9630b2d0 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -1727,6 +1727,7 @@ namespace OpenIddict.Server var descriptor = new OpenIddictAuthorizationDescriptor { + CreationDate = DateTimeOffset.UtcNow, Principal = context.Principal, Status = Statuses.Valid, Subject = context.Principal.GetClaim(Claims.Subject), diff --git a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreBuilder.cs b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreBuilder.cs index ed47e11b..58eaa123 100644 --- a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreBuilder.cs +++ b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreBuilder.cs @@ -63,25 +63,15 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => options.Realm = realm); } - /// - /// Determines whether the specified object is equal to the current object. - /// - /// The object to compare with the current object. - /// true if the specified object is equal to the current object; otherwise, false. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override bool Equals(object? obj) => base.Equals(obj); - /// - /// Serves as the default hash function. - /// - /// A hash code for the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override int GetHashCode() => base.GetHashCode(); - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override string? ToString() => base.ToString(); } diff --git a/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionBuilder.cs b/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionBuilder.cs index 4faee373..011209ea 100644 --- a/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionBuilder.cs +++ b/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionBuilder.cs @@ -79,25 +79,15 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => options.Formatter = formatter); } - /// - /// Determines whether the specified object is equal to the current object. - /// - /// The object to compare with the current object. - /// true if the specified object is equal to the current object; otherwise, false. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override bool Equals(object? obj) => base.Equals(obj); - /// - /// Serves as the default hash function. - /// - /// A hash code for the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override int GetHashCode() => base.GetHashCode(); - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override string? ToString() => base.ToString(); } diff --git a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinBuilder.cs b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinBuilder.cs index 3d0f26f3..f3ce787f 100644 --- a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinBuilder.cs +++ b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinBuilder.cs @@ -79,25 +79,15 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => options.Realm = realm); } - /// - /// Determines whether the specified object is equal to the current object. - /// - /// The object to compare with the current object. - /// true if the specified object is equal to the current object; otherwise, false. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override bool Equals(object? obj) => base.Equals(obj); - /// - /// Serves as the default hash function. - /// - /// A hash code for the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override int GetHashCode() => base.GetHashCode(); - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override string? ToString() => base.ToString(); } diff --git a/src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationBuilder.cs b/src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationBuilder.cs index 9a2b065e..48a2f127 100644 --- a/src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationBuilder.cs +++ b/src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationBuilder.cs @@ -46,25 +46,15 @@ namespace Microsoft.Extensions.DependencyInjection return this; } - /// - /// Determines whether the specified object is equal to the current object. - /// - /// The object to compare with the current object. - /// true if the specified object is equal to the current object; otherwise, false. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override bool Equals(object? obj) => base.Equals(obj); - /// - /// Serves as the default hash function. - /// - /// A hash code for the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override int GetHashCode() => base.GetHashCode(); - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override string? ToString() => base.ToString(); } diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpBuilder.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpBuilder.cs index 2e1fb375..1fd436f8 100644 --- a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpBuilder.cs +++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpBuilder.cs @@ -56,25 +56,15 @@ namespace Microsoft.Extensions.DependencyInjection public OpenIddictValidationSystemNetHttpBuilder SetHttpErrorPolicy(IAsyncPolicy policy) => Configure(options => options.HttpErrorPolicy = policy); - /// - /// Determines whether the specified object is equal to the current object. - /// - /// The object to compare with the current object. - /// true if the specified object is equal to the current object; otherwise, false. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override bool Equals(object? obj) => base.Equals(obj); - /// - /// Serves as the default hash function. - /// - /// A hash code for the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override int GetHashCode() => base.GetHashCode(); - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override string? ToString() => base.ToString(); } diff --git a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs index 4db921b1..b8cfd52a 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs @@ -504,25 +504,15 @@ namespace Microsoft.Extensions.DependencyInjection public OpenIddictValidationBuilder UseIntrospection() => Configure(options => options.ValidationType = OpenIddictValidationType.Introspection); - /// - /// Determines whether the specified object is equal to the current object. - /// - /// The object to compare with the current object. - /// true if the specified object is equal to the current object; otherwise, false. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override bool Equals(object? obj) => base.Equals(obj); - /// - /// Serves as the default hash function. - /// - /// A hash code for the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override int GetHashCode() => base.GetHashCode(); - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override string? ToString() => base.ToString(); } diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs index a5365970..7fc965a6 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs @@ -3928,6 +3928,7 @@ namespace OpenIddict.Server.IntegrationTests Mock.Get(manager).Verify(manager => manager.CreateAsync( It.Is(descriptor => descriptor.ApplicationId == "3E228451-1555-46F7-A471-951EFBA23A56" && + descriptor.CreationDate != null && descriptor.Subject == "Bob le Magnifique" && descriptor.Type == AuthorizationTypes.AdHoc), It.IsAny()), Times.Once()); diff --git a/test/OpenIddict.Server.Quartz.Tests/OpenIddictServerQuartzBuilderTests.cs b/test/OpenIddict.Server.Quartz.Tests/OpenIddictServerQuartzBuilderTests.cs index 3c7cbda4..d14414fb 100644 --- a/test/OpenIddict.Server.Quartz.Tests/OpenIddictServerQuartzBuilderTests.cs +++ b/test/OpenIddict.Server.Quartz.Tests/OpenIddictServerQuartzBuilderTests.cs @@ -50,35 +50,35 @@ namespace OpenIddict.Server.Quartz.Tests } [Fact] - public void DisableAuthorizationsPruning_AuthorizationsPruningIsDisabled() + public void DisableAuthorizationPruning_AuthorizationPruningIsDisabled() { // Arrange var services = CreateServices(); var builder = CreateBuilder(services); // Act - builder.DisableAuthorizationsPruning(); + builder.DisableAuthorizationPruning(); var options = GetOptions(services); // Assert - Assert.True(options.DisableAuthorizationsPruning); + Assert.True(options.DisableAuthorizationPruning); } [Fact] - public void DisableTokensPruning_TokensPruningIsDisabled() + public void DisableTokenPruning_TokenPruningIsDisabled() { // Arrange var services = CreateServices(); var builder = CreateBuilder(services); // Act - builder.DisableTokensPruning(); + builder.DisableTokenPruning(); var options = GetOptions(services); // Assert - Assert.True(options.DisableTokensPruning); + Assert.True(options.DisableTokenPruning); } [Fact] @@ -111,6 +111,66 @@ namespace OpenIddict.Server.Quartz.Tests Assert.Equal(42, options.MaximumRefireCount); } + [Fact] + public void SetMinimumAuthorizationLifespan_ThrowsAnExceptionForNegativeLifespan() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act and assert + var exception = Assert.Throws(() => builder.SetMinimumAuthorizationLifespan(TimeSpan.FromSeconds(-1))); + + Assert.Equal("lifespan", exception.ParamName); + Assert.StartsWith(SR.GetResourceString(SR.ID1279), exception.Message); + } + + [Fact] + public void SetMinimumAuthorizationLifespan_MinimumAuthorizationLifespanIsSet() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.SetMinimumAuthorizationLifespan(TimeSpan.FromDays(42)); + + var options = GetOptions(services); + + // Assert + Assert.Equal(42, options.MinimumAuthorizationLifespan.TotalDays); + } + + [Fact] + public void SetMinimumTokenLifespan_ThrowsAnExceptionForNegativeLifespan() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act and assert + var exception = Assert.Throws(() => builder.SetMinimumTokenLifespan(TimeSpan.FromSeconds(-1))); + + Assert.Equal("lifespan", exception.ParamName); + Assert.StartsWith(SR.GetResourceString(SR.ID1279), exception.Message); + } + + [Fact] + public void SetMinimumTokenLifespan_MinimumTokenLifespanIsSet() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.SetMinimumTokenLifespan(TimeSpan.FromDays(42)); + + var options = GetOptions(services); + + // Assert + Assert.Equal(42, options.MinimumTokenLifespan.TotalDays); + } + private static IServiceCollection CreateServices() => new ServiceCollection().AddOptions(); diff --git a/test/OpenIddict.Server.Quartz.Tests/OpenIddictServerQuartzJobTests.cs b/test/OpenIddict.Server.Quartz.Tests/OpenIddictServerQuartzJobTests.cs index a2d2b9e3..da30fe5e 100644 --- a/test/OpenIddict.Server.Quartz.Tests/OpenIddictServerQuartzJobTests.cs +++ b/test/OpenIddict.Server.Quartz.Tests/OpenIddictServerQuartzJobTests.cs @@ -46,57 +46,58 @@ namespace OpenIddict.Server.Quartz.Tests } [Fact] - public async Task Execute_IgnoresPruningWhenAuthorizationsPruningIsDisabled() + public async Task Execute_IgnoresPruningWhenTokenPruningIsDisabled() { // Arrange var manager = new Mock(); var provider = Mock.Of(provider => - provider.GetService(typeof(IOpenIddictAuthorizationManager)) == manager.Object && - provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of()); + provider.GetService(typeof(IOpenIddictAuthorizationManager)) == Mock.Of() && + provider.GetService(typeof(IOpenIddictTokenManager)) == manager.Object); var job = CreateJob(provider, new OpenIddictServerQuartzOptions { - DisableAuthorizationsPruning = true + DisableTokenPruning = true }); // Act await job.Execute(Mock.Of()); // Assert - manager.Verify(manager => manager.PruneAsync(It.IsAny()), Times.Never()); + manager.Verify(manager => manager.PruneAsync(It.IsAny(), It.IsAny()), Times.Never()); } [Fact] - public async Task Execute_IgnoresPruningWhenTokensPruningIsDisabled() + public async Task Execute_IgnoresPruningWhenAuthorizationPruningIsDisabled() { // Arrange var manager = new Mock(); var provider = Mock.Of(provider => - provider.GetService(typeof(IOpenIddictAuthorizationManager)) == Mock.Of() && - provider.GetService(typeof(IOpenIddictTokenManager)) == manager.Object); + provider.GetService(typeof(IOpenIddictAuthorizationManager)) == manager.Object && + provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of()); var job = CreateJob(provider, new OpenIddictServerQuartzOptions { - DisableTokensPruning = true + DisableAuthorizationPruning = true }); // Act await job.Execute(Mock.Of()); // Assert - manager.Verify(manager => manager.PruneAsync(It.IsAny()), Times.Never()); + manager.Verify(manager => manager.PruneAsync(It.IsAny(), It.IsAny()), Times.Never()); } [Fact] - public async Task Execute_UnschedulesTriggersWhenAuthorizationManagerIsMissing() + public async Task Execute_UnschedulesTriggersWhenTokenManagerIsMissing() { // Arrange var provider = Mock.Of(provider => - provider.GetService(typeof(IOpenIddictAuthorizationManager)) == null); + provider.GetService(typeof(IOpenIddictAuthorizationManager)) == Mock.Of() && + provider.GetService(typeof(IOpenIddictTokenManager)) == null); var job = CreateJob(provider); @@ -112,12 +113,11 @@ namespace OpenIddict.Server.Quartz.Tests } [Fact] - public async Task Execute_UnschedulesTriggersWhenTokenManagerIsMissing() + public async Task Execute_UnschedulesTriggersWhenAuthorizationManagerIsMissing() { // Arrange var provider = Mock.Of(provider => - provider.GetService(typeof(IOpenIddictAuthorizationManager)) == Mock.Of() && - provider.GetService(typeof(IOpenIddictTokenManager)) == null); + provider.GetService(typeof(IOpenIddictAuthorizationManager)) == null); var job = CreateJob(provider); @@ -133,16 +133,16 @@ namespace OpenIddict.Server.Quartz.Tests } [Fact] - public async Task Execute_RethrowsOutOfMemoryExceptionsThrownDuringAuthorizationsPruning() + public async Task Execute_RethrowsOutOfMemoryExceptionsThrownDuringTokenPruning() { // Arrange - var manager = new Mock(); - manager.Setup(manager => manager.PruneAsync(It.IsAny())) + var manager = new Mock(); + manager.Setup(manager => manager.PruneAsync(It.IsAny(), It.IsAny())) .Throws(new OutOfMemoryException()); var provider = Mock.Of(provider => - provider.GetService(typeof(IOpenIddictAuthorizationManager)) == manager.Object && - provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of()); + provider.GetService(typeof(IOpenIddictAuthorizationManager)) == Mock.Of() && + provider.GetService(typeof(IOpenIddictTokenManager)) == manager.Object); var job = CreateJob(provider); @@ -151,16 +151,16 @@ namespace OpenIddict.Server.Quartz.Tests } [Fact] - public async Task Execute_RethrowsOutOfMemoryExceptionsThrownDuringTokensPruning() + public async Task Execute_RethrowsOutOfMemoryExceptionsThrownDuringAuthorizationPruning() { // Arrange - var manager = new Mock(); - manager.Setup(manager => manager.PruneAsync(It.IsAny())) + var manager = new Mock(); + manager.Setup(manager => manager.PruneAsync(It.IsAny(), It.IsAny())) .Throws(new OutOfMemoryException()); var provider = Mock.Of(provider => - provider.GetService(typeof(IOpenIddictAuthorizationManager)) == Mock.Of() && - provider.GetService(typeof(IOpenIddictTokenManager)) == manager.Object); + provider.GetService(typeof(IOpenIddictAuthorizationManager)) == manager.Object && + provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of()); var job = CreateJob(provider); @@ -169,18 +169,18 @@ namespace OpenIddict.Server.Quartz.Tests } [Fact] - public async Task Execute_DisablesRefiringWhenJobIsCanceledDuringAuthorizationsPruning() + public async Task Execute_DisablesRefiringWhenJobIsCanceledDuringTokenPruning() { // Arrange var token = new CancellationToken(canceled: true); - var manager = new Mock(); - manager.Setup(manager => manager.PruneAsync(It.IsAny())) + var manager = new Mock(); + manager.Setup(manager => manager.PruneAsync(It.IsAny(), It.IsAny())) .Throws(new OperationCanceledException(token)); var provider = Mock.Of(provider => - provider.GetService(typeof(IOpenIddictAuthorizationManager)) == manager.Object && - provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of()); + provider.GetService(typeof(IOpenIddictAuthorizationManager)) == Mock.Of() && + provider.GetService(typeof(IOpenIddictTokenManager)) == manager.Object); var context = Mock.Of(context => context.CancellationToken == token); @@ -191,22 +191,22 @@ namespace OpenIddict.Server.Quartz.Tests Assert.False(exception.RefireImmediately); - manager.Verify(manager => manager.PruneAsync(It.IsAny()), Times.Once()); + manager.Verify(manager => manager.PruneAsync(It.IsAny(), It.IsAny()), Times.Once()); } [Fact] - public async Task Execute_DisablesRefiringWhenJobIsCanceledDuringTokensPruning() + public async Task Execute_DisablesRefiringWhenJobIsCanceledDuringAuthorizationPruning() { // Arrange var token = new CancellationToken(canceled: true); - var manager = new Mock(); - manager.Setup(manager => manager.PruneAsync(It.IsAny())) + var manager = new Mock(); + manager.Setup(manager => manager.PruneAsync(It.IsAny(), It.IsAny())) .Throws(new OperationCanceledException(token)); var provider = Mock.Of(provider => - provider.GetService(typeof(IOpenIddictAuthorizationManager)) == Mock.Of() && - provider.GetService(typeof(IOpenIddictTokenManager)) == manager.Object); + provider.GetService(typeof(IOpenIddictAuthorizationManager)) == manager.Object && + provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of()); var context = Mock.Of(context => context.CancellationToken == token); @@ -217,7 +217,7 @@ namespace OpenIddict.Server.Quartz.Tests Assert.False(exception.RefireImmediately); - manager.Verify(manager => manager.PruneAsync(It.IsAny()), Times.Once()); + manager.Verify(manager => manager.PruneAsync(It.IsAny(), It.IsAny()), Times.Once()); } [Fact] @@ -247,7 +247,7 @@ namespace OpenIddict.Server.Quartz.Tests static IOpenIddictAuthorizationManager CreateAuthorizationManager(Exception exception) { var mock = new Mock(); - mock.Setup(manager => manager.PruneAsync(It.IsAny())) + mock.Setup(manager => manager.PruneAsync(It.IsAny(), It.IsAny())) .Throws(exception); return mock.Object; @@ -256,7 +256,7 @@ namespace OpenIddict.Server.Quartz.Tests static IOpenIddictTokenManager CreateTokenManager(Exception exception) { var mock = new Mock(); - mock.Setup(manager => manager.PruneAsync(It.IsAny())) + mock.Setup(manager => manager.PruneAsync(It.IsAny(), It.IsAny())) .Throws(exception); return mock.Object; @@ -294,7 +294,7 @@ namespace OpenIddict.Server.Quartz.Tests static IOpenIddictAuthorizationManager CreateAuthorizationManager(Exception exception) { var mock = new Mock(); - mock.Setup(manager => manager.PruneAsync(It.IsAny())) + mock.Setup(manager => manager.PruneAsync(It.IsAny(), It.IsAny())) .Throws(exception); return mock.Object; @@ -303,7 +303,7 @@ namespace OpenIddict.Server.Quartz.Tests static IOpenIddictTokenManager CreateTokenManager(Exception exception) { var mock = new Mock(); - mock.Setup(manager => manager.PruneAsync(It.IsAny())) + mock.Setup(manager => manager.PruneAsync(It.IsAny(), It.IsAny())) .Throws(exception); return mock.Object; @@ -315,7 +315,7 @@ namespace OpenIddict.Server.Quartz.Tests { // Arrange var manager = new Mock(); - manager.Setup(manager => manager.PruneAsync(It.IsAny())) + manager.Setup(manager => manager.PruneAsync(It.IsAny(), It.IsAny())) .Throws(new ApplicationException()); var provider = Mock.Of(provider =>