diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs b/sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs index c65ff5449..fb33f6a9b 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs +++ b/sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs @@ -204,14 +204,16 @@ public async Task Authorize() { var sessions = await _sessionManager.FindAsync( query: ( - Subject : user.Id, - LoginId : result.Identity.GetClaim("login_id"), - ApplicationId: await _applicationManager.GetIdAsync(application), - Status : Statuses.Valid)).ToListAsync(); + Subject : user.Id, + LoginId : result.Identity.GetClaim("login_id"), + ApplicationId : await _applicationManager.GetIdAsync(application), + AuthorizationId: await _authorizationManager.GetIdAsync(authorization), + Status : Statuses.Valid)).ToListAsync(); var session = sessions.LastOrDefault() ?? await _sessionManager.CreateAsync(new() { ApplicationId = await _applicationManager.GetIdAsync(application), + AuthorizationId = await _authorizationManager.GetIdAsync(authorization), LoginId = result.Identity.GetClaim("login_id"), Subject = user.Id }); @@ -354,14 +356,16 @@ public async Task Accept() { var sessions = await _sessionManager.FindAsync( query: ( - Subject : user.Id, - LoginId : result.Identity.GetClaim("login_id"), - ApplicationId: await _applicationManager.GetIdAsync(application), - Status : Statuses.Valid)).ToListAsync(); + Subject : user.Id, + LoginId : result.Identity.GetClaim("login_id"), + ApplicationId : await _applicationManager.GetIdAsync(application), + AuthorizationId: await _authorizationManager.GetIdAsync(authorization), + Status : Statuses.Valid)).ToListAsync(); var session = sessions.LastOrDefault() ?? await _sessionManager.CreateAsync(new() { ApplicationId = await _applicationManager.GetIdAsync(application), + AuthorizationId = await _authorizationManager.GetIdAsync(authorization), LoginId = result.Identity.GetClaim("login_id"), Subject = user.Id }); diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs index c03472615..ad1906857 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs @@ -233,14 +233,16 @@ public async Task Authorize() { var sessions = await _sessionManager.FindAsync( query: ( - Subject : await _userManager.GetUserIdAsync(user), - LoginId : result.Principal.GetClaim("login_id"), - ApplicationId: await _applicationManager.GetIdAsync(application), - Status : Statuses.Valid)).ToListAsync(); + Subject : await _userManager.GetUserIdAsync(user), + LoginId : result.Principal.GetClaim("login_id"), + ApplicationId : await _applicationManager.GetIdAsync(application), + AuthorizationId: await _authorizationManager.GetIdAsync(authorization), + Status : Statuses.Valid)).ToListAsync(); var session = sessions.LastOrDefault() ?? await _sessionManager.CreateAsync(new() { ApplicationId = await _applicationManager.GetIdAsync(application), + AuthorizationId = await _authorizationManager.GetIdAsync(authorization), LoginId = result.Principal.GetClaim("login_id"), Subject = await _userManager.GetUserIdAsync(user) }); @@ -365,14 +367,16 @@ public async Task Accept() { var sessions = await _sessionManager.FindAsync( query: ( - Subject : await _userManager.GetUserIdAsync(user), - LoginId : User.GetClaim("login_id"), - ApplicationId: await _applicationManager.GetIdAsync(application), - Status : Statuses.Valid)).ToListAsync(); + Subject : await _userManager.GetUserIdAsync(user), + LoginId : User.GetClaim("login_id"), + ApplicationId : await _applicationManager.GetIdAsync(application), + AuthorizationId: await _authorizationManager.GetIdAsync(authorization), + Status : Statuses.Valid)).ToListAsync(); var session = sessions.LastOrDefault() ?? await _sessionManager.CreateAsync(new() { ApplicationId = await _applicationManager.GetIdAsync(application), + AuthorizationId = await _authorizationManager.GetIdAsync(authorization), LoginId = User.GetClaim("login_id"), Subject = await _userManager.GetUserIdAsync(user) }); diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Program.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Program.cs index 869066cc5..62ba3e979 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Program.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Program.cs @@ -320,10 +320,11 @@ // For applications that need immediate access token or authorization // revocation, the database entry of the received tokens and their // associated authorizations can be validated for each API call. - // Enabling these options may have a negative impact on performance. // - // options.EnableAuthorizationEntryValidation(); - // options.EnableTokenEntryValidation(); + // Note: enabling these options may have a negative impact on performance. + options.EnableAuthorizationEntryValidation() + .EnableSessionEntryValidation() + .EnableTokenEntryValidation(); }); builder.Services.AddTransient(); diff --git a/src/OpenIddict.Abstractions/Caches/IOpenIddictSessionCache.cs b/src/OpenIddict.Abstractions/Caches/IOpenIddictSessionCache.cs index a121458ee..95ae7f123 100644 --- a/src/OpenIddict.Abstractions/Caches/IOpenIddictSessionCache.cs +++ b/src/OpenIddict.Abstractions/Caches/IOpenIddictSessionCache.cs @@ -27,7 +27,7 @@ public interface IOpenIddictSessionCache where TSession : class /// The that can be used to abort the operation. /// The sessions corresponding to the criteria. IAsyncEnumerable FindAsync( - (string? Subject, string? LoginId, string? ApplicationId, string? Status) query, CancellationToken cancellationToken); + (string? Subject, string? LoginId, string? ApplicationId, string? AuthorizationId, string? Status) query, CancellationToken cancellationToken); /// /// Retrieves the list of sessions corresponding to the specified application identifier. diff --git a/src/OpenIddict.Abstractions/Descriptors/OpenIddictTokenDescriptor.cs b/src/OpenIddict.Abstractions/Descriptors/OpenIddictTokenDescriptor.cs index e080cc88c..5872da114 100644 --- a/src/OpenIddict.Abstractions/Descriptors/OpenIddictTokenDescriptor.cs +++ b/src/OpenIddict.Abstractions/Descriptors/OpenIddictTokenDescriptor.cs @@ -60,6 +60,11 @@ public class OpenIddictTokenDescriptor /// public string? ReferenceId { get; set; } + /// + /// Gets or sets the identifier of the session associated with the token. + /// + public string? SessionId { get; set; } + /// /// Gets or sets the status of the token. /// diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictSessionManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictSessionManager.cs index 6613d3a06..d864b5f96 100644 --- a/src/OpenIddict.Abstractions/Managers/IOpenIddictSessionManager.cs +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictSessionManager.cs @@ -95,7 +95,7 @@ ValueTask CountAsync( /// The that can be used to abort the operation. /// The sessions corresponding to the criteria. IAsyncEnumerable FindAsync( - (string? Subject, string? LoginId, string? ApplicationId, string? Status) query, + (string? Subject, string? LoginId, string? ApplicationId, string? AuthorizationId, string? Status) query, CancellationToken cancellationToken = default); /// @@ -258,6 +258,15 @@ IAsyncEnumerable FindAsync( /// ValueTask GetSubjectAsync(object session, CancellationToken cancellationToken = default); + /// + /// Determines whether a given session has the specified status. + /// + /// The session. + /// The expected status. + /// The that can be used to abort the operation. + /// if the session has the specified status, otherwise. + ValueTask HasStatusAsync(object session, string status, CancellationToken cancellationToken = default); + /// /// Executes the specified query and returns all the corresponding elements. /// @@ -313,6 +322,18 @@ IAsyncEnumerable ListAsync( /// ValueTask PopulateAsync(object session, OpenIddictSessionDescriptor descriptor, CancellationToken cancellationToken = default); + /// + /// Removes the sessions that are marked as invalid and don't have any token attached. + /// Only sessions created before the specified are removed. + /// + /// + /// Since sessions with tokens still attached are not deleted, tokens should always be pruned first. + /// + /// The date before which sessions are not pruned. + /// The that can be used to abort the operation. + /// The number of sessions that were removed. + ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken); + /// /// Updates an existing session. /// diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs index 7e3ffbbf7..4f6bb35f0 100644 --- a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs @@ -277,6 +277,17 @@ IAsyncEnumerable FindAsync( /// ValueTask GetReferenceIdAsync(object token, CancellationToken cancellationToken = default); + /// + /// Retrieves the optional session identifier associated with a token. + /// + /// The token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the session identifier associated with the token. + /// + ValueTask GetSessionIdAsync(object token, CancellationToken cancellationToken = default); + /// /// Retrieves the status associated with a token. /// diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index 02703dba6..0e5466e45 100644 --- a/src/OpenIddict.Abstractions/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx @@ -537,10 +537,7 @@ Reference the 'OpenIddict.Validation.SystemNetHttp' package and call 'services.A The client secret cannot be null or empty when using introspection. Alternatively, one or multiple signing credentials can be registered and used as TLS client certificates or to produce client assertions if the authorization server supports it. - Authorization entry validation cannot be enabled when using introspection. - - - Token entry validation cannot be enabled when using introspection. + Authorization entry, session entry and token entry validation cannot be enabled when using introspection. A discovery client must be registered when using server discovery. @@ -557,7 +554,7 @@ Reference the 'OpenIddict.Validation.SystemNetHttp' package and call 'services.A This may indicate that it was not properly registered in the dependency injection container. To register an event handler, use 'services.AddOpenIddict().AddValidation().AddEventHandler()'. - The core services must be registered when enabling token entry validation. + The core services must be registered when enabling authorization entry, session entry or token entry validation. To register the OpenIddict core services, reference the 'OpenIddict.Core' package and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'. @@ -566,10 +563,6 @@ To register the OpenIddict core services, reference the 'OpenIddict.Core' packag An unknown error occurred while introspecting the access token. - - The core services must be registered when enabling authorization entry validation. -To register the OpenIddict core services, reference the 'OpenIddict.Core' package and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'. - The URI cannot be null or empty. @@ -855,17 +848,8 @@ Reload the entity from the database and retry the operation. An error occurred while trying to create a new entity instance. Make sure that the entity is not abstract and has a public parameterless constructor or create a custom store that overrides 'InstantiateAsync()' to use a custom factory. - - An error occurred while pruning authorizations. - - The application matching the specified identifier cannot be found in the change tracker or in the database. - - - An error occurred while pruning tokens. - - - The authorization matching the specified identifier cannot be found in the change tracker or in the database. + The entity matching the specified identifier cannot be found in the change tracker or in the database. No Entity Framework Core context was configured to be used with OpenIddict. @@ -2436,6 +2420,9 @@ To use a custom policy relying on the system store, set 'OpenIddictServerOptions The login identifier cannot be null or empty and must match the value used to represent the user session. + + The session associated with the token is no longer valid. + The '{0}' parameter shouldn't be null or empty at this point. @@ -2502,6 +2489,9 @@ To use a custom policy relying on the system store, set 'OpenIddictServerOptions The length of the memory span ({0}) doesn't match the expected value ({1}). + + The session identifier shouldn't be null or empty at this point. + An error occurred while validating the token '{Token}'. @@ -3294,6 +3284,9 @@ This may indicate that the hashed entry is corrupted or malformed. A signing key of type '{Type}' was ignored because its ML-DSA public key couldn't be extracted. + + The session '{Identifier}' was no longer valid. + https://documentation.openiddict.com/errors/{0} diff --git a/src/OpenIddict.Abstractions/Stores/IOpenIddictSessionStore.cs b/src/OpenIddict.Abstractions/Stores/IOpenIddictSessionStore.cs index e1af87efc..7d15bbdc9 100644 --- a/src/OpenIddict.Abstractions/Stores/IOpenIddictSessionStore.cs +++ b/src/OpenIddict.Abstractions/Stores/IOpenIddictSessionStore.cs @@ -64,7 +64,7 @@ ValueTask CountAsync( /// The that can be used to abort the operation. /// The sessions corresponding to the criteria. IAsyncEnumerable FindAsync( - (string? Subject, string? LoginId, string? ApplicationId, string? Status) query, CancellationToken cancellationToken); + (string? Subject, string? LoginId, string? ApplicationId, string? AuthorizationId, string? Status) query, CancellationToken cancellationToken); /// /// Retrieves the list of sessions corresponding to the specified application identifier. @@ -245,6 +245,18 @@ IAsyncEnumerable ListAsync( Func, TState, IQueryable> query, TState state, CancellationToken cancellationToken); + /// + /// Removes the sessions that are marked as invalid and don't have any token attached. + /// Only sessions created before the specified are removed. + /// + /// + /// Since sessions with tokens still attached are not deleted, tokens should always be pruned first. + /// + /// The date before which sessions are not pruned. + /// The that can be used to abort the operation. + /// The number of sessions that were removed. + ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken); + /// /// Sets the application identifier associated with a session. /// diff --git a/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs b/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs index bcac5ea1b..95c79333b 100644 --- a/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs +++ b/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs @@ -232,6 +232,17 @@ IAsyncEnumerable FindAsync( /// ValueTask GetReferenceIdAsync(TToken token, CancellationToken cancellationToken); + /// + /// Retrieves the optional session identifier associated with a token. + /// + /// The token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the session identifier associated with the token. + /// + ValueTask GetSessionIdAsync(TToken token, CancellationToken cancellationToken); + /// /// Retrieves the status associated with a token. /// @@ -359,6 +370,15 @@ IAsyncEnumerable ListAsync( /// A that can be used to monitor the asynchronous operation. ValueTask SetAuthorizationIdAsync(TToken token, string? identifier, CancellationToken cancellationToken); + /// + /// Sets the session identifier associated with a token. + /// + /// The token. + /// The unique identifier associated with the token. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + ValueTask SetSessionIdAsync(TToken token, string? identifier, CancellationToken cancellationToken); + /// /// Sets the creation date associated with a token. /// diff --git a/src/OpenIddict.Core/Caches/OpenIddictSessionCache.cs b/src/OpenIddict.Core/Caches/OpenIddictSessionCache.cs index 0ea339978..04b0a56f5 100644 --- a/src/OpenIddict.Core/Caches/OpenIddictSessionCache.cs +++ b/src/OpenIddict.Core/Caches/OpenIddictSessionCache.cs @@ -96,7 +96,7 @@ public void Dispose() /// public async IAsyncEnumerable FindAsync( - (string? Subject, string? LoginId, string? ApplicationId, string? Status) query, + (string? Subject, string? LoginId, string? ApplicationId, string? AuthorizationId, string? Status) query, [EnumeratorCancellation] CancellationToken cancellationToken) { // Note: this method is only partially cached. diff --git a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs index 67676ff6f..1af5512c4 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs @@ -133,6 +133,12 @@ public virtual async ValueTask CreateAsync(TAuthorization authorization, Cancell await Store.SetStatusAsync(authorization, Statuses.Valid, cancellationToken); } + // If no creation date was explicitly specified, set it to the current time. + if (await Store.GetCreationDateAsync(authorization, cancellationToken) is null) + { + await Store.SetCreationDateAsync(authorization, Options.CurrentValue.TimeProvider.GetUtcNow(), cancellationToken); + } + var results = await GetValidationResultsAsync(authorization, cancellationToken); if (results.Any(static result => result != ValidationResult.Success)) { diff --git a/src/OpenIddict.Core/Managers/OpenIddictSessionManager.cs b/src/OpenIddict.Core/Managers/OpenIddictSessionManager.cs index 819f19872..22ccea720 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictSessionManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictSessionManager.cs @@ -132,6 +132,12 @@ public virtual async ValueTask CreateAsync(TSession session, CancellationToken c await Store.SetStatusAsync(session, Statuses.Valid, cancellationToken); } + // If no creation date was explicitly specified, set it to the current time. + if (await Store.GetCreationDateAsync(session, cancellationToken) is null) + { + await Store.SetCreationDateAsync(session, Options.CurrentValue.TimeProvider.GetUtcNow(), cancellationToken); + } + var results = await GetValidationResultsAsync(session, cancellationToken); if (results.Any(static result => result != ValidationResult.Success)) { @@ -217,7 +223,7 @@ public virtual async ValueTask DeleteAsync(TSession session, CancellationToken c /// The that can be used to abort the operation. /// The sessions corresponding to the criteria. public virtual IAsyncEnumerable FindAsync( - (string? Subject, string? LoginId, string? ApplicationId, string? Status) query, + (string? Subject, string? LoginId, string? ApplicationId, string? AuthorizationId, string? Status) query, CancellationToken cancellationToken = default) { var sessions = Options.CurrentValue.DisableEntityCaching @@ -601,6 +607,21 @@ public virtual ValueTask> GetProperties return Store.GetSubjectAsync(session, cancellationToken); } + /// + /// Determines whether a given session has the specified status. + /// + /// The session. + /// The expected status. + /// The that can be used to abort the operation. + /// if the session has the specified status, otherwise. + public virtual async ValueTask HasStatusAsync(TSession session, string status, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(session); + ArgumentException.ThrowIfNullOrEmpty(status); + + return string.Equals(await GetStatusAsync(session, cancellationToken), status, StringComparison.Ordinal); + } + /// /// Executes the specified query and returns all the corresponding elements. /// @@ -699,6 +720,19 @@ public virtual async ValueTask PopulateAsync( } } + /// + /// Removes the sessions that are marked as invalid and don't have any token attached. + /// Only sessions created before the specified are removed. + /// + /// + /// Since sessions with tokens still attached are not deleted, tokens should always be pruned first. + /// + /// The date before which sessions are not pruned. + /// The that can be used to abort the operation. + /// The number of sessions that were removed. + public virtual ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken) + => Store.PruneAsync(threshold, cancellationToken); + /// /// Updates an existing session. /// @@ -822,7 +856,7 @@ ValueTask IOpenIddictSessionManager.DeleteAsync(object session, CancellationToke => DeleteAsync((TSession) session, cancellationToken); /// - IAsyncEnumerable IOpenIddictSessionManager.FindAsync((string? Subject, string? LoginId, string? ApplicationId, string? Status) query, CancellationToken cancellationToken) + IAsyncEnumerable IOpenIddictSessionManager.FindAsync((string? Subject, string? LoginId, string? ApplicationId, string? AuthorizationId, string? Status) query, CancellationToken cancellationToken) => FindAsync(query, cancellationToken); /// @@ -885,6 +919,10 @@ ValueTask> IOpenIddictSessionManager.Ge ValueTask IOpenIddictSessionManager.GetSubjectAsync(object session, CancellationToken cancellationToken) => GetSubjectAsync((TSession) session, cancellationToken); + /// + ValueTask IOpenIddictSessionManager.HasStatusAsync(object session, string status, CancellationToken cancellationToken) + => HasStatusAsync((TSession) session, status, cancellationToken); + /// IAsyncEnumerable IOpenIddictSessionManager.ListAsync(int? count, int? offset, CancellationToken cancellationToken) => ListAsync(count, offset, cancellationToken); @@ -905,6 +943,10 @@ ValueTask IOpenIddictSessionManager.PopulateAsync(OpenIddictSessionDescriptor de ValueTask IOpenIddictSessionManager.PopulateAsync(object session, OpenIddictSessionDescriptor descriptor, CancellationToken cancellationToken) => PopulateAsync((TSession) session, descriptor, cancellationToken); + /// + ValueTask IOpenIddictSessionManager.PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken) + => PruneAsync(threshold, cancellationToken); + /// ValueTask IOpenIddictSessionManager.UpdateAsync(object session, CancellationToken cancellationToken) => UpdateAsync((TSession) session, cancellationToken); diff --git a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs index abeec9690..1e181c51e 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs @@ -134,6 +134,12 @@ public virtual async ValueTask CreateAsync(TToken token, CancellationToken cance await Store.SetStatusAsync(token, Statuses.Valid, cancellationToken); } + // If no creation date was explicitly specified, set it to the current time. + if (await Store.GetCreationDateAsync(token, cancellationToken) is null) + { + await Store.SetCreationDateAsync(token, Options.CurrentValue.TimeProvider.GetUtcNow(), cancellationToken); + } + // If a reference identifier was set, obfuscate it. var identifier = await Store.GetReferenceIdAsync(token, cancellationToken); if (!string.IsNullOrEmpty(identifier)) @@ -632,6 +638,22 @@ public virtual ValueTask> GetProperties return Store.GetReferenceIdAsync(token, cancellationToken); } + /// + /// Retrieves the optional session identifier associated with a token. + /// + /// The token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the session identifier associated with the token. + /// + public virtual ValueTask GetSessionIdAsync(TToken token, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(token); + + return Store.GetSessionIdAsync(token, cancellationToken); + } + /// /// Retrieves the status associated with a token. /// @@ -820,6 +842,7 @@ public virtual async ValueTask PopulateAsync(TToken token, await Store.SetPropertiesAsync(token, descriptor.Properties.ToImmutableDictionary(), cancellationToken); await Store.SetRedemptionDateAsync(token, descriptor.RedemptionDate, cancellationToken); await Store.SetReferenceIdAsync(token, descriptor.ReferenceId, cancellationToken); + await Store.SetSessionIdAsync(token, descriptor.SessionId, cancellationToken); await Store.SetStatusAsync(token, descriptor.Status, cancellationToken); await Store.SetSubjectAsync(token, descriptor.Subject, cancellationToken); await Store.SetTypeAsync(token, descriptor.Type, cancellationToken); @@ -848,6 +871,7 @@ public virtual async ValueTask PopulateAsync( descriptor.Payload = await Store.GetPayloadAsync(token, cancellationToken); descriptor.RedemptionDate = await Store.GetRedemptionDateAsync(token, cancellationToken); descriptor.ReferenceId = await Store.GetReferenceIdAsync(token, cancellationToken); + descriptor.SessionId = await Store.GetSessionIdAsync(token, cancellationToken); descriptor.Status = await Store.GetStatusAsync(token, cancellationToken); descriptor.Subject = await Store.GetSubjectAsync(token, cancellationToken); descriptor.Type = await Store.GetTypeAsync(token, cancellationToken); @@ -1272,6 +1296,10 @@ ValueTask> IOpenIddictTokenManager.GetP ValueTask IOpenIddictTokenManager.GetReferenceIdAsync(object token, CancellationToken cancellationToken) => GetReferenceIdAsync((TToken) token, cancellationToken); + /// + ValueTask IOpenIddictTokenManager.GetSessionIdAsync(object token, CancellationToken cancellationToken) + => GetSessionIdAsync((TToken) token, cancellationToken); + /// ValueTask IOpenIddictTokenManager.GetStatusAsync(object token, CancellationToken cancellationToken) => GetStatusAsync((TToken) token, cancellationToken); diff --git a/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkApplication.cs b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkApplication.cs index a24476064..74a1985f1 100644 --- a/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkApplication.cs +++ b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkApplication.cs @@ -15,6 +15,7 @@ namespace OpenIddict.EntityFramework.Models; public class OpenIddictEntityFrameworkApplication : OpenIddictEntityFrameworkApplication { public OpenIddictEntityFrameworkApplication() => Id = Guid.NewGuid().ToString(); @@ -24,9 +25,10 @@ public class OpenIddictEntityFrameworkApplication : /// Represents an OpenIddict application. /// [DebuggerDisplay("Id = {Id.ToString(),nq} ; ClientId = {ClientId,nq} ; ClientType = {ClientType,nq}")] -public class OpenIddictEntityFrameworkApplication +public class OpenIddictEntityFrameworkApplication where TKey : notnull, IEquatable where TAuthorization : class + where TSession : class where TToken : class { /// @@ -134,6 +136,11 @@ public class OpenIddictEntityFrameworkApplication [StringSyntax(StringSyntaxAttribute.Json)] public virtual string? Settings { get; set; } + /// + /// Gets the list of the sessions associated with the application. + /// + public virtual ICollection Sessions { get; } = new HashSet(); + /// /// Gets the list of the tokens associated with the application. /// diff --git a/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkAuthorization.cs b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkAuthorization.cs index 93b415d11..192f36ea9 100644 --- a/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkAuthorization.cs +++ b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkAuthorization.cs @@ -15,6 +15,7 @@ namespace OpenIddict.EntityFramework.Models; public class OpenIddictEntityFrameworkAuthorization : OpenIddictEntityFrameworkAuthorization { public OpenIddictEntityFrameworkAuthorization() => Id = Guid.NewGuid().ToString(); @@ -24,9 +25,10 @@ public class OpenIddictEntityFrameworkAuthorization : /// Represents an OpenIddict authorization. /// [DebuggerDisplay("Id = {Id.ToString(),nq} ; Subject = {Subject,nq} ; Type = {Type,nq} ; Status = {Status,nq}")] -public class OpenIddictEntityFrameworkAuthorization +public class OpenIddictEntityFrameworkAuthorization where TKey : notnull, IEquatable where TApplication : class + where TSession : class where TToken : class { /// @@ -61,6 +63,11 @@ public class OpenIddictEntityFrameworkAuthorization [StringSyntax(StringSyntaxAttribute.Json)] public virtual string? Scopes { get; set; } + /// + /// Gets the list of the sessions associated with the authorization. + /// + public virtual ICollection Sessions { get; } = new HashSet(); + /// /// Gets or sets the status of the authorization. /// diff --git a/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkSession.cs b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkSession.cs index 4457d008c..85421e543 100644 --- a/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkSession.cs +++ b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkSession.cs @@ -15,7 +15,8 @@ namespace OpenIddict.EntityFramework.Models; public class OpenIddictEntityFrameworkSession : OpenIddictEntityFrameworkSession + OpenIddictEntityFrameworkAuthorization, + OpenIddictEntityFrameworkToken> { public OpenIddictEntityFrameworkSession() => Id = Guid.NewGuid().ToString(); } @@ -24,10 +25,11 @@ public class OpenIddictEntityFrameworkSession : /// Represents an OpenIddict session. /// [DebuggerDisplay("Id = {Id.ToString(),nq} ; Subject = {Subject,nq} ; LoginId = {LoginId,nq} ; Status = {Status,nq}")] -public class OpenIddictEntityFrameworkSession +public class OpenIddictEntityFrameworkSession where TKey : notnull, IEquatable where TApplication : class where TAuthorization : class + where TToken : class { /// /// Gets or sets the application associated with the session. @@ -74,4 +76,9 @@ public class OpenIddictEntityFrameworkSession public virtual string? Subject { get; set; } + + /// + /// Gets the list of the tokens associated with the session. + /// + public virtual ICollection Tokens { get; } = new HashSet(); } diff --git a/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkToken.cs b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkToken.cs index 6e9ab164b..35dcabb74 100644 --- a/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkToken.cs +++ b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkToken.cs @@ -15,7 +15,8 @@ namespace OpenIddict.EntityFramework.Models; public class OpenIddictEntityFrameworkToken : OpenIddictEntityFrameworkToken + OpenIddictEntityFrameworkAuthorization, + OpenIddictEntityFrameworkSession> { public OpenIddictEntityFrameworkToken() => Id = Guid.NewGuid().ToString(); } @@ -24,10 +25,11 @@ public class OpenIddictEntityFrameworkToken : /// Represents an OpenIddict token. /// [DebuggerDisplay("Id = {Id.ToString(),nq} ; Subject = {Subject,nq} ; Type = {Type,nq} ; Status = {Status,nq}")] -public class OpenIddictEntityFrameworkToken +public class OpenIddictEntityFrameworkToken where TKey : notnull, IEquatable where TApplication : class where TAuthorization : class + where TSession : class { /// /// Gets or sets the application associated with the token. @@ -88,6 +90,11 @@ public class OpenIddictEntityFrameworkToken /// public virtual string? ReferenceId { get; set; } + /// + /// Gets or sets the session associated with the token. + /// + public virtual TSession? Session { get; set; } + /// /// Gets or sets the status of the token. /// diff --git a/src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkApplicationConfiguration.cs b/src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkApplicationConfiguration.cs index 8f8acd632..174c8956c 100644 --- a/src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkApplicationConfiguration.cs +++ b/src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkApplicationConfiguration.cs @@ -17,17 +17,20 @@ namespace OpenIddict.EntityFramework; /// /// The type of the application entity. /// The type of the authorization entity. +/// The type of the session entity. /// The type of the token entity. /// The type of the primary key. [EditorBrowsable(EditorBrowsableState.Never)] public sealed class OpenIddictEntityFrameworkApplicationConfiguration< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApplication, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : EntityTypeConfiguration - where TApplication : OpenIddictEntityFrameworkApplication - where TAuthorization : OpenIddictEntityFrameworkAuthorization - where TToken : OpenIddictEntityFrameworkToken + where TApplication : OpenIddictEntityFrameworkApplication + where TAuthorization : OpenIddictEntityFrameworkAuthorization + where TSession : OpenIddictEntityFrameworkSession + where TToken : OpenIddictEntityFrameworkToken where TKey : notnull, IEquatable { public OpenIddictEntityFrameworkApplicationConfiguration() @@ -75,6 +78,14 @@ public OpenIddictEntityFrameworkApplicationConfiguration() Property(lambda).HasMaxLength(100); } + HasMany(static application => application.Sessions) + .WithOptional(static session => session.Application!) + .Map(static association => + { + association.MapKey(nameof(OpenIddictEntityFrameworkSession.Application) + + nameof(OpenIddictEntityFrameworkApplication.Id)); + }); + HasMany(static application => application.Tokens) .WithOptional(static token => token.Application!) .Map(static association => diff --git a/src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkAuthorizationConfiguration.cs b/src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkAuthorizationConfiguration.cs index fcdb5cb97..4e653bf74 100644 --- a/src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkAuthorizationConfiguration.cs +++ b/src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkAuthorizationConfiguration.cs @@ -17,17 +17,20 @@ namespace OpenIddict.EntityFramework; /// /// The type of the authorization entity. /// The type of the application entity. +/// The type of the session entity. /// The type of the token entity. /// The type of the primary key. [EditorBrowsable(EditorBrowsableState.Never)] public sealed class OpenIddictEntityFrameworkAuthorizationConfiguration< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApplication, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : EntityTypeConfiguration - where TAuthorization : OpenIddictEntityFrameworkAuthorization - where TApplication : OpenIddictEntityFrameworkApplication - where TToken : OpenIddictEntityFrameworkToken + where TAuthorization : OpenIddictEntityFrameworkAuthorization + where TApplication : OpenIddictEntityFrameworkApplication + where TSession : OpenIddictEntityFrameworkSession + where TToken : OpenIddictEntityFrameworkToken where TKey : notnull, IEquatable { public OpenIddictEntityFrameworkAuthorizationConfiguration() @@ -52,6 +55,12 @@ public OpenIddictEntityFrameworkAuthorizationConfiguration() Property(lambda).HasMaxLength(100); } + HasMany(static authorization => authorization.Sessions) + .WithOptional(static session => session.Authorization!) + .Map(static association => association.MapKey(nameof(OpenIddictEntityFrameworkSession.Authorization) + + nameof(OpenIddictEntityFrameworkAuthorization.Id))) + .WillCascadeOnDelete(); + Property(static authorization => authorization.Status) .HasMaxLength(50); diff --git a/src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkSessionConfiguration.cs b/src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkSessionConfiguration.cs index 0acaf7045..0a80916ba 100644 --- a/src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkSessionConfiguration.cs +++ b/src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkSessionConfiguration.cs @@ -27,10 +27,10 @@ public sealed class OpenIddictEntityFrameworkSessionConfiguration< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : EntityTypeConfiguration - where TSession : OpenIddictEntityFrameworkSession - where TApplication : OpenIddictEntityFrameworkApplication - where TAuthorization : OpenIddictEntityFrameworkAuthorization - where TToken : OpenIddictEntityFrameworkToken + where TSession : OpenIddictEntityFrameworkSession + where TApplication : OpenIddictEntityFrameworkApplication + where TAuthorization : OpenIddictEntityFrameworkAuthorization + where TToken : OpenIddictEntityFrameworkToken where TKey : notnull, IEquatable { public OpenIddictEntityFrameworkSessionConfiguration() @@ -66,6 +66,14 @@ public OpenIddictEntityFrameworkSessionConfiguration() Property(static session => session.Subject) .HasMaxLength(400); + HasMany(static session => session.Tokens) + .WithOptional(static token => token.Session!) + .Map(static association => + { + association.MapKey(nameof(OpenIddictEntityFrameworkToken.Session) + + nameof(OpenIddictEntityFrameworkSession.Id)); + }); + ToTable("OpenIddictSessions"); } } diff --git a/src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkTokenConfiguration.cs b/src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkTokenConfiguration.cs index d2b0d7358..8c23d5d5b 100644 --- a/src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkTokenConfiguration.cs +++ b/src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkTokenConfiguration.cs @@ -18,16 +18,19 @@ namespace OpenIddict.EntityFramework; /// The type of the token entity. /// The type of the application entity. /// The type of the authorization entity. +/// The type of the session entity. /// The type of the primary key. [EditorBrowsable(EditorBrowsableState.Never)] public sealed class OpenIddictEntityFrameworkTokenConfiguration< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApplication, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : EntityTypeConfiguration - where TToken : OpenIddictEntityFrameworkToken - where TApplication : OpenIddictEntityFrameworkApplication - where TAuthorization : OpenIddictEntityFrameworkAuthorization + where TToken : OpenIddictEntityFrameworkToken + where TApplication : OpenIddictEntityFrameworkApplication + where TAuthorization : OpenIddictEntityFrameworkAuthorization + where TSession : OpenIddictEntityFrameworkSession where TKey : notnull, IEquatable { public OpenIddictEntityFrameworkTokenConfiguration() diff --git a/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkBuilder.cs b/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkBuilder.cs index a37c5ba5f..473895f9e 100644 --- a/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkBuilder.cs +++ b/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkBuilder.cs @@ -71,12 +71,12 @@ public OpenIddictEntityFrameworkBuilder ReplaceDefaultEntities< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey>() - where TApplication : OpenIddictEntityFrameworkApplication - where TAuthorization : OpenIddictEntityFrameworkAuthorization + where TApplication : OpenIddictEntityFrameworkApplication + where TAuthorization : OpenIddictEntityFrameworkAuthorization where TResource : OpenIddictEntityFrameworkResource where TScope : OpenIddictEntityFrameworkScope - where TSession : OpenIddictEntityFrameworkSession - where TToken : OpenIddictEntityFrameworkToken + where TSession : OpenIddictEntityFrameworkSession + where TToken : OpenIddictEntityFrameworkToken where TKey : notnull, IEquatable { // Note: unlike Entity Framework Core, Entity Framework 6.x always @@ -113,9 +113,9 @@ public OpenIddictEntityFrameworkBuilder ReplaceDefaultEntities< provider.GetRequiredService>())); Services.Replace(ServiceDescriptor.Scoped, - OpenIddictEntityFrameworkApplicationStore>()); + OpenIddictEntityFrameworkApplicationStore>()); Services.Replace(ServiceDescriptor.Scoped, - OpenIddictEntityFrameworkAuthorizationStore>()); + OpenIddictEntityFrameworkAuthorizationStore>()); Services.Replace(ServiceDescriptor.Scoped, OpenIddictEntityFrameworkResourceStore>()); Services.Replace(ServiceDescriptor.Scoped, @@ -123,7 +123,7 @@ public OpenIddictEntityFrameworkBuilder ReplaceDefaultEntities< Services.Replace(ServiceDescriptor.Scoped, OpenIddictEntityFrameworkSessionStore>()); Services.Replace(ServiceDescriptor.Scoped, - OpenIddictEntityFrameworkTokenStore>()); + OpenIddictEntityFrameworkTokenStore>()); return this; } diff --git a/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkHelpers.cs b/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkHelpers.cs index ef81333da..6f117839e 100644 --- a/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkHelpers.cs +++ b/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkHelpers.cs @@ -48,23 +48,23 @@ public static DbModelBuilder UseOpenIddict< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey>(this DbModelBuilder builder) - where TApplication : OpenIddictEntityFrameworkApplication - where TAuthorization : OpenIddictEntityFrameworkAuthorization + where TApplication : OpenIddictEntityFrameworkApplication + where TAuthorization : OpenIddictEntityFrameworkAuthorization where TResource : OpenIddictEntityFrameworkResource where TScope : OpenIddictEntityFrameworkScope - where TSession : OpenIddictEntityFrameworkSession - where TToken : OpenIddictEntityFrameworkToken + where TSession : OpenIddictEntityFrameworkSession + where TToken : OpenIddictEntityFrameworkToken where TKey : notnull, IEquatable { ArgumentNullException.ThrowIfNull(builder); builder.Configurations - .Add(new OpenIddictEntityFrameworkApplicationConfiguration()) - .Add(new OpenIddictEntityFrameworkAuthorizationConfiguration()) + .Add(new OpenIddictEntityFrameworkApplicationConfiguration()) + .Add(new OpenIddictEntityFrameworkAuthorizationConfiguration()) .Add(new OpenIddictEntityFrameworkResourceConfiguration()) .Add(new OpenIddictEntityFrameworkScopeConfiguration()) .Add(new OpenIddictEntityFrameworkSessionConfiguration()) - .Add(new OpenIddictEntityFrameworkTokenConfiguration()); + .Add(new OpenIddictEntityFrameworkTokenConfiguration()); return builder; } diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkApplicationStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkApplicationStore.cs index b402788e5..05b816278 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkApplicationStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkApplicationStore.cs @@ -28,6 +28,7 @@ namespace OpenIddict.EntityFramework; public class OpenIddictEntityFrameworkApplicationStore : OpenIddictEntityFrameworkApplicationStore { public OpenIddictEntityFrameworkApplicationStore( @@ -44,16 +45,19 @@ public OpenIddictEntityFrameworkApplicationStore( /// /// The type of the application entity. /// The type of the authorization entity. +/// The type of the session entity. /// The type of the token entity. /// The type of the entity primary keys. public class OpenIddictEntityFrameworkApplicationStore< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApplication, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : IOpenIddictApplicationStore - where TApplication : OpenIddictEntityFrameworkApplication - where TAuthorization : OpenIddictEntityFrameworkAuthorization - where TToken : OpenIddictEntityFrameworkToken + where TApplication : OpenIddictEntityFrameworkApplication + where TAuthorization : OpenIddictEntityFrameworkAuthorization + where TToken : OpenIddictEntityFrameworkToken + where TSession : OpenIddictEntityFrameworkSession where TKey : notnull, IEquatable { public OpenIddictEntityFrameworkApplicationStore( @@ -120,17 +124,6 @@ public virtual async ValueTask DeleteAsync(TApplication application, Cancellatio var context = await Context.GetDbContextAsync(cancellationToken); - Task> ListAuthorizationsAsync() - => (from authorization in context.Set().Include(static authorization => authorization.Tokens) - where authorization.Application!.Id!.Equals(application.Id) - select authorization).ToListAsync(cancellationToken); - - Task> ListTokensAsync() - => (from token in context.Set() - where token.Authorization == null - where token.Application!.Id!.Equals(application.Id) - select token).ToListAsync(cancellationToken); - // To prevent an SQL exception from being thrown if a new associated entity is // created after the existing entries have been listed, the following logic is // executed in a serializable transaction, that will lock the affected tables. @@ -138,7 +131,11 @@ Task> ListTokensAsync() // Remove all the authorizations associated with the application and // the tokens attached to these implicit or explicit authorizations. - var authorizations = await ListAuthorizationsAsync(); + var authorizations = await + (from authorization in context.Set().Include(static authorization => authorization.Tokens) + where authorization.Application!.Id!.Equals(application.Id) + select authorization).ToListAsync(cancellationToken); + foreach (var authorization in authorizations) { foreach (var token in authorization.Tokens) @@ -149,8 +146,42 @@ Task> ListTokensAsync() context.Set().Remove(authorization); } + // Remove all the sessions associated with the application, the authorizations associated + // with the session and the tokens attached to these authorizations and sessions. + var sessions = await + (from session in context.Set() + .Include(static session => session.Authorization!.Tokens) + .Include(static session => session.Tokens) + where session.Application!.Id!.Equals(application.Id) + select session).ToListAsync(cancellationToken); + + foreach (var session in sessions) + { + if (session.Authorization is not null) + { + foreach (var token in session.Authorization.Tokens) + { + context.Set().Remove(token); + } + + context.Set().Remove(session.Authorization); + } + + foreach (var token in session.Tokens) + { + context.Set().Remove(token); + } + + context.Set().Remove(session); + } + // Remove all the tokens associated with the application. - var tokens = await ListTokensAsync(); + var tokens = await + (from token in context.Set() + where token.Authorization == null + where token.Application!.Id!.Equals(application.Id) + select token).ToListAsync(cancellationToken); + foreach (var token in tokens) { context.Set().Remove(token); @@ -166,7 +197,7 @@ Task> ListTokensAsync() catch (DbUpdateConcurrencyException exception) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(application).State = EntityState.Unchanged; foreach (var authorization in authorizations) @@ -179,6 +210,26 @@ Task> ListTokensAsync() } } + foreach (var session in sessions) + { + context.Entry(session).State = EntityState.Unchanged; + + if (session.Authorization is not null) + { + context.Entry(session.Authorization).State = EntityState.Unchanged; + + foreach (var token in session.Authorization.Tokens) + { + context.Entry(token).State = EntityState.Unchanged; + } + } + + foreach (var token in session.Tokens) + { + context.Entry(token).State = EntityState.Unchanged; + } + } + foreach (var token in tokens) { context.Entry(token).State = EntityState.Unchanged; @@ -1043,7 +1094,7 @@ public virtual async ValueTask UpdateAsync(TApplication application, Cancellatio catch (DbUpdateConcurrencyException exception) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(application).State = EntityState.Unchanged; throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception); diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkAuthorizationStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkAuthorizationStore.cs index 80037a727..bb86d213d 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkAuthorizationStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkAuthorizationStore.cs @@ -26,6 +26,7 @@ namespace OpenIddict.EntityFramework; public class OpenIddictEntityFrameworkAuthorizationStore : OpenIddictEntityFrameworkAuthorizationStore { public OpenIddictEntityFrameworkAuthorizationStore( @@ -42,16 +43,19 @@ public OpenIddictEntityFrameworkAuthorizationStore( /// /// The type of the authorization entity. /// The type of the application entity. +/// The type of the session entity. /// The type of the token entity. /// The type of the entity primary keys. public class OpenIddictEntityFrameworkAuthorizationStore< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApplication, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : IOpenIddictAuthorizationStore - where TAuthorization : OpenIddictEntityFrameworkAuthorization - where TApplication : OpenIddictEntityFrameworkApplication - where TToken : OpenIddictEntityFrameworkToken + where TAuthorization : OpenIddictEntityFrameworkAuthorization + where TApplication : OpenIddictEntityFrameworkApplication + where TToken : OpenIddictEntityFrameworkToken + where TSession : OpenIddictEntityFrameworkSession where TKey : notnull, IEquatable { public OpenIddictEntityFrameworkAuthorizationStore( @@ -118,23 +122,49 @@ public virtual async ValueTask DeleteAsync(TAuthorization authorization, Cancell var context = await Context.GetDbContextAsync(cancellationToken); - Task> ListTokensAsync() - => (from token in context.Set() - where token.Authorization!.Id!.Equals(authorization.Id) - select token).ToListAsync(cancellationToken); - // To prevent an SQL exception from being thrown if a new associated entity is // created after the existing entries have been listed, the following logic is // executed in a serializable transaction, that will lock the affected tables. using var transaction = CreateTransaction(context, IsolationLevel.Serializable); // Remove all the tokens associated with the authorization. - var tokens = await ListTokensAsync(); + var tokens = await + (from token in context.Set() + where token.Authorization!.Id!.Equals(authorization.Id) + select token).ToListAsync(cancellationToken); + foreach (var token in tokens) { context.Set().Remove(token); } + // Remove all the sessions associated with the authorization + // and the tokens attached to these authorizations and sessions. + var sessions = await + (from session in context.Set().Include(static session => session.Tokens) + where session.Authorization!.Id!.Equals(authorization.Id) + select session).ToListAsync(cancellationToken); + + foreach (var session in sessions) + { + if (session.Authorization is not null) + { + foreach (var token in session.Authorization.Tokens) + { + context.Set().Remove(token); + } + + context.Set().Remove(session.Authorization); + } + + foreach (var token in session.Tokens) + { + context.Set().Remove(token); + } + + context.Set().Remove(session); + } + context.Set().Remove(authorization); try @@ -145,9 +175,29 @@ Task> ListTokensAsync() catch (DbUpdateConcurrencyException exception) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(authorization).State = EntityState.Unchanged; + foreach (var session in sessions) + { + context.Entry(session).State = EntityState.Unchanged; + + if (session.Authorization is not null) + { + context.Entry(session.Authorization).State = EntityState.Unchanged; + + foreach (var token in session.Authorization.Tokens) + { + context.Entry(token).State = EntityState.Unchanged; + } + } + + foreach (var token in session.Tokens) + { + context.Entry(token).State = EntityState.Unchanged; + } + } + foreach (var token in tokens) { context.Entry(token).State = EntityState.Unchanged; @@ -554,7 +604,7 @@ orderby authorization.Id if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0243), exceptions); + throw new AggregateException(exceptions); } return result; @@ -605,7 +655,7 @@ public virtual async ValueTask RevokeAsync(string? subject, string? client catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(authorization).State = EntityState.Unchanged; exceptions ??= new List(capacity: 1); @@ -619,7 +669,7 @@ public virtual async ValueTask RevokeAsync(string? subject, string? client if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + throw new AggregateException(exceptions); } return result; @@ -651,7 +701,7 @@ public virtual async ValueTask RevokeByApplicationIdAsync(string identifie catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(authorization).State = EntityState.Unchanged; exceptions ??= new List(capacity: 1); @@ -665,7 +715,7 @@ public virtual async ValueTask RevokeByApplicationIdAsync(string identifie if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + throw new AggregateException(exceptions); } return result; @@ -696,7 +746,7 @@ public virtual async ValueTask RevokeBySubjectAsync(string subject, Cancel catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(authorization).State = EntityState.Unchanged; exceptions ??= new List(capacity: 1); @@ -710,7 +760,7 @@ public virtual async ValueTask RevokeBySubjectAsync(string subject, Cancel if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + throw new AggregateException(exceptions); } return result; @@ -885,7 +935,7 @@ public virtual async ValueTask UpdateAsync(TAuthorization authorization, Cancell catch (DbUpdateConcurrencyException exception) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(authorization).State = EntityState.Unchanged; throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception); diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkResourceStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkResourceStore.cs index bc7d0c66c..e4a8cc1a9 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkResourceStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkResourceStore.cs @@ -119,7 +119,7 @@ public virtual async ValueTask DeleteAsync(TResource resource, CancellationToken catch (DbUpdateConcurrencyException exception) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(resource).State = EntityState.Unchanged; throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception); @@ -561,7 +561,7 @@ public virtual async ValueTask UpdateAsync(TResource resource, CancellationToken catch (DbUpdateConcurrencyException exception) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(resource).State = EntityState.Unchanged; throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception); diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkScopeStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkScopeStore.cs index fe002dda1..21f831686 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkScopeStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkScopeStore.cs @@ -119,7 +119,7 @@ public virtual async ValueTask DeleteAsync(TScope scope, CancellationToken cance catch (DbUpdateConcurrencyException exception) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(scope).State = EntityState.Unchanged; throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception); @@ -667,7 +667,7 @@ public virtual async ValueTask UpdateAsync(TScope scope, CancellationToken cance catch (DbUpdateConcurrencyException exception) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(scope).State = EntityState.Unchanged; throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception); diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkSessionStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkSessionStore.cs index bacfe81da..7e08034d8 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkSessionStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkSessionStore.cs @@ -6,6 +6,7 @@ using System.Collections.Immutable; using System.ComponentModel; +using System.Data; using System.Data.Entity.Infrastructure; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -51,10 +52,10 @@ public class OpenIddictEntityFrameworkSessionStore< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : IOpenIddictSessionStore - where TSession : OpenIddictEntityFrameworkSession - where TApplication : OpenIddictEntityFrameworkApplication - where TAuthorization : OpenIddictEntityFrameworkAuthorization - where TToken : OpenIddictEntityFrameworkToken + where TSession : OpenIddictEntityFrameworkSession + where TApplication : OpenIddictEntityFrameworkApplication + where TAuthorization : OpenIddictEntityFrameworkAuthorization + where TToken : OpenIddictEntityFrameworkToken where TKey : notnull, IEquatable { public OpenIddictEntityFrameworkSessionStore( @@ -121,6 +122,23 @@ public virtual async ValueTask DeleteAsync(TSession session, CancellationToken c var context = await Context.GetDbContextAsync(cancellationToken); + // To prevent an SQL exception from being thrown if a new associated entity is + // created after the existing entries have been listed, the following logic is + // executed in a serializable transaction, that will lock the affected tables. + using var transaction = CreateTransaction(context, IsolationLevel.Serializable); + + // Remove all the tokens associated with the session. + var tokens = await + (from token in context.Set() + where token.Authorization == null + where token.Session!.Id!.Equals(session.Id) + select token).ToListAsync(cancellationToken); + + foreach (var token in tokens) + { + context.Set().Remove(token); + } + context.Set().Remove(session); try @@ -130,16 +148,21 @@ public virtual async ValueTask DeleteAsync(TSession session, CancellationToken c catch (DbUpdateConcurrencyException exception) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(session).State = EntityState.Unchanged; + foreach (var token in tokens) + { + context.Entry(token).State = EntityState.Unchanged; + } + throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception); } } /// public virtual async IAsyncEnumerable FindAsync( - (string? Subject, string? LoginId, string? ApplicationId, string? Status) query, + (string? Subject, string? LoginId, string? ApplicationId, string? AuthorizationId, string? Status) query, [EnumeratorCancellation] CancellationToken cancellationToken) { var context = await Context.GetDbContextAsync(cancellationToken); @@ -159,6 +182,12 @@ public virtual async IAsyncEnumerable FindAsync( sessions = sessions.Where(session => session.Application!.Id!.Equals(key)); } + if (!string.IsNullOrEmpty(query.AuthorizationId)) + { + var key = ConvertIdentifierFromString(query.AuthorizationId); + sessions = sessions.Where(session => session.Authorization!.Id!.Equals(key)); + } + if (!string.IsNullOrEmpty(query.LoginId)) { sessions = sessions.Where(session => session.LoginId == query.LoginId); @@ -495,6 +524,81 @@ async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] Cancellati } } + /// + public virtual async ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken) + { + var context = await Context.GetDbContextAsync(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. + // To work around this limitation, entities are manually listed and deleted using a batch logic. + + List? exceptions = null; + + var result = 0L; + + // Note: to avoid sending too many queries, the maximum number of elements + // that can be removed by a single call to PruneAsync() is deliberately limited. + for (var index = 0; index < 1_000; index++) + { + cancellationToken.ThrowIfCancellationRequested(); + + // To prevent concurrency exceptions from being thrown if an entry is modified + // after it was retrieved from the database, the following logic is executed in + // a repeatable read transaction, that will put a lock on the retrieved entries + // and thus prevent them from being concurrently modified outside this block. + using var transaction = CreateTransaction(context, IsolationLevel.RepeatableRead); + + // Note: the Oracle MySQL provider doesn't support DateTimeOffset and is unable + // to create a SQL query with an expression calling DateTimeOffset.UtcDateTime. + // To work around this limitation, the threshold represented as a DateTimeOffset + // instance is manually converted to a UTC DateTime instance outside the query. + var date = threshold.UtcDateTime; + + var sessions = + await (from session in context.Set().Include(static session => session.Tokens) + where session.CreationDate < date + where session.Status != Statuses.Valid + where !session.Tokens.Any() + orderby session.Id + select session).Take(1_000).ToListAsync(cancellationToken); + + if (sessions.Count is 0) + { + break; + } + + // Note: new tokens may be attached after the sessions were retrieved + // from the database since the transaction level is deliberately limited to + // repeatable read instead of serializable for performance reasons). In this + // case, the operation will fail, which is considered an acceptable risk. + context.Set().RemoveRange(sessions); + + try + { + await context.SaveChangesAsync(cancellationToken); + transaction?.Commit(); + } + + catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) + { + exceptions ??= new List(capacity: 1); + exceptions.Add(exception); + + continue; + } + + result += sessions.Count; + } + + if (exceptions is { Count: > 0 }) + { + throw new AggregateException(exceptions); + } + + return result; + } + /// public virtual async ValueTask SetApplicationIdAsync(TSession session, string? identifier, CancellationToken cancellationToken) { @@ -540,7 +644,7 @@ public virtual async ValueTask SetAuthorizationIdAsync(TSession session, string? session.Authorization = await context.Set().FindAsync( cancellationToken, ConvertIdentifierFromString(identifier)) - ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0251)); + ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0244)); } else @@ -661,7 +765,7 @@ public virtual async ValueTask UpdateAsync(TSession session, CancellationToken c catch (DbUpdateConcurrencyException exception) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(session).State = EntityState.Unchanged; throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception); @@ -723,4 +827,25 @@ public virtual async ValueTask UpdateAsync(TSession session, CancellationToken c return converter.ConvertToInvariantString(identifier); } + + /// + /// Tries to create a new with the specified . + /// + /// The Entity Framework context. + /// The desired level of isolation. + /// The if it could be created, otherwise. + protected virtual DbContextTransaction? CreateTransaction(DbContext context, IsolationLevel level) + { + ArgumentNullException.ThrowIfNull(context); + + try + { + return context.Database.BeginTransaction(level); + } + + catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) + { + return null; + } + } } diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs index a2a5c90b5..0bf777e20 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs @@ -26,7 +26,8 @@ namespace OpenIddict.EntityFramework; public class OpenIddictEntityFrameworkTokenStore : OpenIddictEntityFrameworkTokenStore + OpenIddictEntityFrameworkAuthorization, + OpenIddictEntityFrameworkSession, string> { public OpenIddictEntityFrameworkTokenStore( IMemoryCache cache, @@ -43,15 +44,18 @@ public OpenIddictEntityFrameworkTokenStore( /// The type of the token entity. /// The type of the application entity. /// The type of the authorization entity. +/// The type of the session entity. /// The type of the entity primary keys. public class OpenIddictEntityFrameworkTokenStore< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApplication, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : IOpenIddictTokenStore - where TToken : OpenIddictEntityFrameworkToken - where TApplication : OpenIddictEntityFrameworkApplication - where TAuthorization : OpenIddictEntityFrameworkAuthorization + where TToken : OpenIddictEntityFrameworkToken + where TApplication : OpenIddictEntityFrameworkApplication + where TAuthorization : OpenIddictEntityFrameworkAuthorization + where TSession : OpenIddictEntityFrameworkSession where TKey : notnull, IEquatable { public OpenIddictEntityFrameworkTokenStore( @@ -127,7 +131,7 @@ public virtual async ValueTask DeleteAsync(TToken token, CancellationToken cance catch (DbUpdateConcurrencyException exception) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(token).State = EntityState.Unchanged; throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception); @@ -433,6 +437,33 @@ public virtual ValueTask> GetProperties return new(token.ReferenceId); } + /// + public virtual async ValueTask GetSessionIdAsync(TToken token, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(token); + + // If the session is not attached to the token, try to load it manually. + if (token.Session is null) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + var reference = context.Entry(token).Reference(static entry => entry.Session); + if (reference.EntityEntry.State is EntityState.Detached) + { + return null; + } + + await reference.LoadAsync(cancellationToken); + } + + if (token.Session is null) + { + return null; + } + + return ConvertIdentifierToString(token.Session.Id); + } + /// public virtual ValueTask GetStatusAsync(TToken token, CancellationToken cancellationToken) { @@ -591,7 +622,7 @@ orderby token.Id if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + throw new AggregateException(exceptions); } return result; @@ -643,7 +674,7 @@ public virtual async ValueTask RevokeAsync(string? subject, string? client catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(token).State = EntityState.Unchanged; exceptions ??= new List(capacity: 1); @@ -657,7 +688,7 @@ public virtual async ValueTask RevokeAsync(string? subject, string? client if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + throw new AggregateException(exceptions); } return result; @@ -691,7 +722,7 @@ public virtual async ValueTask RevokeByApplicationIdAsync(string identifie catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(token).State = EntityState.Unchanged; exceptions ??= new List(capacity: 1); @@ -705,7 +736,7 @@ public virtual async ValueTask RevokeByApplicationIdAsync(string identifie if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + throw new AggregateException(exceptions); } return result; @@ -739,7 +770,7 @@ public virtual async ValueTask RevokeByAuthorizationIdAsync(string identif catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(token).State = EntityState.Unchanged; exceptions ??= new List(capacity: 1); @@ -753,7 +784,7 @@ public virtual async ValueTask RevokeByAuthorizationIdAsync(string identif if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + throw new AggregateException(exceptions); } return result; @@ -786,7 +817,7 @@ public virtual async ValueTask RevokeBySubjectAsync(string subject, Cancel catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(token).State = EntityState.Unchanged; exceptions ??= new List(capacity: 1); @@ -800,7 +831,7 @@ public virtual async ValueTask RevokeBySubjectAsync(string subject, Cancel if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + throw new AggregateException(exceptions); } return result; @@ -851,7 +882,7 @@ public virtual async ValueTask SetAuthorizationIdAsync(TToken token, string? ide token.Authorization = await context.Set().FindAsync( cancellationToken, ConvertIdentifierFromString(identifier)) - ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0251)); + ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0244)); } else @@ -960,6 +991,40 @@ public virtual ValueTask SetReferenceIdAsync(TToken token, string? identifier, C return ValueTask.CompletedTask; } + /// + public virtual async ValueTask SetSessionIdAsync(TToken token, string? identifier, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(token); + + if (!string.IsNullOrEmpty(identifier)) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + token.Session = await context.Set().FindAsync( + cancellationToken, ConvertIdentifierFromString(identifier)) + ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0244)); + } + + else + { + // If the session is not attached to the token, try to load it manually. + if (token.Session is null) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + var reference = context.Entry(token).Reference(static entry => entry.Session); + if (reference.EntityEntry.State is EntityState.Detached) + { + return; + } + + await reference.LoadAsync(cancellationToken); + } + + token.Session = null; + } + } + /// public virtual ValueTask SetStatusAsync(TToken token, string? status, CancellationToken cancellationToken) { @@ -1012,7 +1077,7 @@ public virtual async ValueTask UpdateAsync(TToken token, CancellationToken cance catch (DbUpdateConcurrencyException exception) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(token).State = EntityState.Unchanged; throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception); diff --git a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreApplication.cs b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreApplication.cs index 5c3b985e4..eb65067fa 100644 --- a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreApplication.cs +++ b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreApplication.cs @@ -16,6 +16,7 @@ namespace OpenIddict.EntityFrameworkCore.Models; public class OpenIddictEntityFrameworkCoreApplication : OpenIddictEntityFrameworkCoreApplication { public OpenIddictEntityFrameworkCoreApplication() => Id = Guid.NewGuid().ToString(); @@ -27,6 +28,7 @@ public class OpenIddictEntityFrameworkCoreApplication : public class OpenIddictEntityFrameworkCoreApplication : OpenIddictEntityFrameworkCoreApplication, + OpenIddictEntityFrameworkCoreSession, OpenIddictEntityFrameworkCoreToken> where TKey : notnull, IEquatable; @@ -34,9 +36,10 @@ public class OpenIddictEntityFrameworkCoreApplication : /// Represents an OpenIddict application. /// [DebuggerDisplay("Id = {Id.ToString(),nq} ; ClientId = {ClientId,nq} ; ClientType = {ClientType,nq}")] -public class OpenIddictEntityFrameworkCoreApplication +public class OpenIddictEntityFrameworkCoreApplication where TKey : notnull, IEquatable where TAuthorization : class + where TSession : class where TToken : class { /// @@ -136,6 +139,11 @@ public class OpenIddictEntityFrameworkCoreApplication public virtual IDictionary? Settings { get; set; } + /// + /// Gets the list of the sessions associated with the application. + /// + public virtual ICollection Sessions { get; } = new HashSet(); + /// /// Gets the list of the tokens associated with the application. /// diff --git a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreAuthorization.cs b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreAuthorization.cs index ae84951bb..af309ce87 100644 --- a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreAuthorization.cs +++ b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreAuthorization.cs @@ -15,6 +15,7 @@ namespace OpenIddict.EntityFrameworkCore.Models; public class OpenIddictEntityFrameworkCoreAuthorization : OpenIddictEntityFrameworkCoreAuthorization { public OpenIddictEntityFrameworkCoreAuthorization() => Id = Guid.NewGuid().ToString(); @@ -26,6 +27,7 @@ public class OpenIddictEntityFrameworkCoreAuthorization : public class OpenIddictEntityFrameworkCoreAuthorization : OpenIddictEntityFrameworkCoreAuthorization, + OpenIddictEntityFrameworkCoreSession, OpenIddictEntityFrameworkCoreToken> where TKey : notnull, IEquatable; @@ -33,13 +35,14 @@ public class OpenIddictEntityFrameworkCoreAuthorization : /// Represents an OpenIddict authorization. /// [DebuggerDisplay("Id = {Id.ToString(),nq} ; Subject = {Subject,nq} ; Type = {Type,nq} ; Status = {Status,nq}")] -public class OpenIddictEntityFrameworkCoreAuthorization +public class OpenIddictEntityFrameworkCoreAuthorization where TKey : notnull, IEquatable where TApplication : class + where TSession : class where TToken : class { /// - /// Gets or sets the application of the authorization. + /// Gets or sets the application associated with the authorization. /// public virtual TApplication? Application { get; set; } @@ -68,6 +71,11 @@ public class OpenIddictEntityFrameworkCoreAuthorization public virtual string[]? Scopes { get; set; } + /// + /// Gets the list of the sessions associated with the authorization. + /// + public virtual ICollection Sessions { get; } = new HashSet(); + /// /// Gets or sets the status of the authorization. /// diff --git a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreSession.cs b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreSession.cs index 6e89959f0..799605e51 100644 --- a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreSession.cs +++ b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreSession.cs @@ -15,7 +15,8 @@ namespace OpenIddict.EntityFrameworkCore.Models; public class OpenIddictEntityFrameworkCoreSession : OpenIddictEntityFrameworkCoreSession + OpenIddictEntityFrameworkCoreAuthorization, + OpenIddictEntityFrameworkCoreToken> { public OpenIddictEntityFrameworkCoreSession() => Id = Guid.NewGuid().ToString(); } @@ -26,17 +27,19 @@ public class OpenIddictEntityFrameworkCoreSession : public class OpenIddictEntityFrameworkCoreSession : OpenIddictEntityFrameworkCoreSession, - OpenIddictEntityFrameworkCoreAuthorization> + OpenIddictEntityFrameworkCoreAuthorization, + OpenIddictEntityFrameworkCoreToken> where TKey : notnull, IEquatable; /// /// Represents an OpenIddict session. /// [DebuggerDisplay("Id = {Id.ToString(),nq} ; Subject = {Subject,nq} ; LoginId = {LoginId,nq} ; Status = {Status,nq}")] -public class OpenIddictEntityFrameworkCoreSession +public class OpenIddictEntityFrameworkCoreSession where TKey : notnull, IEquatable where TApplication : class where TAuthorization : class + where TToken : class { /// /// Gets or sets the application associated with the session. @@ -82,4 +85,9 @@ public class OpenIddictEntityFrameworkCoreSession public virtual string? Subject { get; set; } + + /// + /// Gets the list of the tokens associated with the session. + /// + public virtual ICollection Tokens { get; } = new HashSet(); } diff --git a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreToken.cs b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreToken.cs index 5bf3b1f5d..f6692ab17 100644 --- a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreToken.cs +++ b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreToken.cs @@ -15,7 +15,8 @@ namespace OpenIddict.EntityFrameworkCore.Models; public class OpenIddictEntityFrameworkCoreToken : OpenIddictEntityFrameworkCoreToken + OpenIddictEntityFrameworkCoreAuthorization, + OpenIddictEntityFrameworkCoreSession> { public OpenIddictEntityFrameworkCoreToken() => Id = Guid.NewGuid().ToString(); } @@ -26,17 +27,19 @@ public class OpenIddictEntityFrameworkCoreToken : public class OpenIddictEntityFrameworkCoreToken : OpenIddictEntityFrameworkCoreToken, - OpenIddictEntityFrameworkCoreAuthorization> + OpenIddictEntityFrameworkCoreAuthorization, + OpenIddictEntityFrameworkCoreSession> where TKey : notnull, IEquatable; /// /// Represents an OpenIddict token. /// [DebuggerDisplay("Id = {Id.ToString(),nq} ; Subject = {Subject,nq} ; Type = {Type,nq} ; Status = {Status,nq}")] -public class OpenIddictEntityFrameworkCoreToken +public class OpenIddictEntityFrameworkCoreToken where TKey : notnull, IEquatable where TApplication : class where TAuthorization : class + where TSession : class { /// /// Gets or sets the application associated with the token. @@ -96,6 +99,11 @@ public class OpenIddictEntityFrameworkCoreToken public virtual string? ReferenceId { get; set; } + /// + /// Gets or sets the session associated with the token. + /// + public virtual TSession? Session { get; set; } + /// /// Gets or sets the status of the token. /// diff --git a/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreApplicationConfiguration.cs b/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreApplicationConfiguration.cs index c6b9520ec..e02adf44f 100644 --- a/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreApplicationConfiguration.cs +++ b/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreApplicationConfiguration.cs @@ -19,17 +19,20 @@ namespace OpenIddict.EntityFrameworkCore; /// /// The type of the application entity. /// The type of the authorization entity. +/// The type of the session entity. /// The type of the token entity. /// The type of the primary key. [EditorBrowsable(EditorBrowsableState.Never)] public sealed class OpenIddictEntityFrameworkCoreApplicationConfiguration< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApplication, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : IEntityTypeConfiguration - where TApplication : OpenIddictEntityFrameworkCoreApplication - where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization - where TToken : OpenIddictEntityFrameworkCoreToken + where TApplication : OpenIddictEntityFrameworkCoreApplication + where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization + where TSession : OpenIddictEntityFrameworkCoreSession + where TToken : OpenIddictEntityFrameworkCoreToken where TKey : notnull, IEquatable { public void Configure(EntityTypeBuilder builder) @@ -99,6 +102,11 @@ public void Configure(EntityTypeBuilder builder) static value => JsonSerializer.Deserialize(value, OpenIddictSerializer.Default.IDictionaryStringString), CreateDictionaryComparer()); + builder.HasMany(static application => application.Sessions) + .WithOne(static session => session.Application!) + .HasForeignKey(nameof(OpenIddictEntityFrameworkCoreSession.Application) + nameof(OpenIddictEntityFrameworkCoreApplication.Id)) + .IsRequired(required: false); + builder.HasMany(static application => application.Tokens) .WithOne(static token => token.Application!) .HasForeignKey(nameof(OpenIddictEntityFrameworkCoreToken.Application) + nameof(OpenIddictEntityFrameworkCoreApplication.Id)) diff --git a/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreAuthorizationConfiguration.cs b/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreAuthorizationConfiguration.cs index 9f05ae694..a273f7840 100644 --- a/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreAuthorizationConfiguration.cs +++ b/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreAuthorizationConfiguration.cs @@ -18,17 +18,20 @@ namespace OpenIddict.EntityFrameworkCore; /// /// The type of the authorization entity. /// The type of the application entity. +/// The type of the session entity. /// The type of the token entity. /// The type of the primary key. [EditorBrowsable(EditorBrowsableState.Never)] public sealed class OpenIddictEntityFrameworkCoreAuthorizationConfiguration< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApplication, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : IEntityTypeConfiguration - where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization - where TApplication : OpenIddictEntityFrameworkCoreApplication - where TToken : OpenIddictEntityFrameworkCoreToken + where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization + where TApplication : OpenIddictEntityFrameworkCoreApplication + where TSession : OpenIddictEntityFrameworkCoreSession + where TToken : OpenIddictEntityFrameworkCoreToken where TKey : notnull, IEquatable { public void Configure(EntityTypeBuilder builder) @@ -66,6 +69,12 @@ public void Configure(EntityTypeBuilder builder) static value => JsonSerializer.Deserialize(value, OpenIddictSerializer.Default.IDictionaryStringJsonElement), CreateDictionaryComparer()); + builder.HasMany(static authorization => authorization.Sessions) + .WithOne(static session => session.Authorization!) + .HasForeignKey(nameof(OpenIddictEntityFrameworkCoreSession.Authorization) + + nameof(OpenIddictEntityFrameworkCoreAuthorization.Id)) + .IsRequired(required: false); + builder.Property(static authorization => authorization.Status) .HasMaxLength(50); diff --git a/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreSessionConfiguration.cs b/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreSessionConfiguration.cs index 39697fbe1..7c462994b 100644 --- a/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreSessionConfiguration.cs +++ b/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreSessionConfiguration.cs @@ -28,10 +28,10 @@ public sealed class OpenIddictEntityFrameworkCoreSessionConfiguration< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : IEntityTypeConfiguration - where TSession : OpenIddictEntityFrameworkCoreSession - where TApplication : OpenIddictEntityFrameworkCoreApplication - where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization - where TToken : OpenIddictEntityFrameworkCoreToken + where TSession : OpenIddictEntityFrameworkCoreSession + where TApplication : OpenIddictEntityFrameworkCoreApplication + where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization + where TToken : OpenIddictEntityFrameworkCoreToken where TKey : notnull, IEquatable { public void Configure(EntityTypeBuilder builder) @@ -74,6 +74,12 @@ public void Configure(EntityTypeBuilder builder) static value => JsonSerializer.Deserialize(value, OpenIddictSerializer.Default.IDictionaryStringJsonElement), CreateDictionaryComparer()); + builder.HasMany(static session => session.Tokens) + .WithOne(static token => token.Session!) + .HasForeignKey(nameof(OpenIddictEntityFrameworkCoreToken.Session) + + nameof(OpenIddictEntityFrameworkCoreSession.Id)) + .IsRequired(required: false); + builder.ToTable("OpenIddictSessions"); static ValueComparer CreateDictionaryComparer() => new ValueComparer>( diff --git a/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreTokenConfiguration.cs b/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreTokenConfiguration.cs index 03a88ebec..8be387192 100644 --- a/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreTokenConfiguration.cs +++ b/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreTokenConfiguration.cs @@ -19,16 +19,19 @@ namespace OpenIddict.EntityFrameworkCore; /// The type of the token entity. /// The type of the application entity. /// The type of the authorization entity. +/// The type of the session entity. /// The type of the primary key. [EditorBrowsable(EditorBrowsableState.Never)] public sealed class OpenIddictEntityFrameworkCoreTokenConfiguration< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApplication, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : IEntityTypeConfiguration - where TToken : OpenIddictEntityFrameworkCoreToken - where TApplication : OpenIddictEntityFrameworkCoreApplication - where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization + where TToken : OpenIddictEntityFrameworkCoreToken + where TApplication : OpenIddictEntityFrameworkCoreApplication + where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization + where TSession : OpenIddictEntityFrameworkCoreSession where TKey : notnull, IEquatable { public void Configure(EntityTypeBuilder builder) diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs index 4bd58a9c3..f61fff2bf 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs @@ -93,12 +93,12 @@ public OpenIddictEntityFrameworkCoreBuilder ReplaceDefaultEntities< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey>() - where TApplication : OpenIddictEntityFrameworkCoreApplication - where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization + where TApplication : OpenIddictEntityFrameworkCoreApplication + where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization where TResource : OpenIddictEntityFrameworkCoreResource where TScope : OpenIddictEntityFrameworkCoreScope - where TSession : OpenIddictEntityFrameworkCoreSession - where TToken : OpenIddictEntityFrameworkCoreToken + where TSession : OpenIddictEntityFrameworkCoreSession + where TToken : OpenIddictEntityFrameworkCoreToken where TKey : notnull, IEquatable { // If the specified key type isn't a string (which is special-cased by the stores to avoid having to resolve @@ -123,9 +123,9 @@ public OpenIddictEntityFrameworkCoreBuilder ReplaceDefaultEntities< provider.GetRequiredService>())); Services.Replace(ServiceDescriptor.Scoped, - OpenIddictEntityFrameworkCoreApplicationStore>()); + OpenIddictEntityFrameworkCoreApplicationStore>()); Services.Replace(ServiceDescriptor.Scoped, - OpenIddictEntityFrameworkCoreAuthorizationStore>()); + OpenIddictEntityFrameworkCoreAuthorizationStore>()); Services.Replace(ServiceDescriptor.Scoped, OpenIddictEntityFrameworkCoreResourceStore>()); Services.Replace(ServiceDescriptor.Scoped, @@ -133,7 +133,7 @@ public OpenIddictEntityFrameworkCoreBuilder ReplaceDefaultEntities< Services.Replace(ServiceDescriptor.Scoped, OpenIddictEntityFrameworkCoreSessionStore>()); Services.Replace(ServiceDescriptor.Scoped, - OpenIddictEntityFrameworkCoreTokenStore>()); + OpenIddictEntityFrameworkCoreTokenStore>()); return this; } diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreCustomizer.cs b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreCustomizer.cs index 45f77e77b..dc78913cb 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreCustomizer.cs +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreCustomizer.cs @@ -23,12 +23,12 @@ public sealed class OpenIddictEntityFrameworkCoreCustomizer< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : RelationalModelCustomizer - where TApplication : OpenIddictEntityFrameworkCoreApplication - where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization + where TApplication : OpenIddictEntityFrameworkCoreApplication + where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization where TResource : OpenIddictEntityFrameworkCoreResource where TScope : OpenIddictEntityFrameworkCoreScope - where TSession : OpenIddictEntityFrameworkCoreSession - where TToken : OpenIddictEntityFrameworkCoreToken + where TSession : OpenIddictEntityFrameworkCoreSession + where TToken : OpenIddictEntityFrameworkCoreToken where TKey : notnull, IEquatable { public OpenIddictEntityFrameworkCoreCustomizer(ModelCustomizerDependencies dependencies) diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs index 00a4ac2a9..69eaaefbf 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs @@ -102,12 +102,12 @@ public static DbContextOptionsBuilder UseOpenIddict< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey>( this DbContextOptionsBuilder builder) - where TApplication : OpenIddictEntityFrameworkCoreApplication - where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization + where TApplication : OpenIddictEntityFrameworkCoreApplication + where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization where TResource : OpenIddictEntityFrameworkCoreResource where TScope : OpenIddictEntityFrameworkCoreScope - where TSession : OpenIddictEntityFrameworkCoreSession - where TToken : OpenIddictEntityFrameworkCoreToken + where TSession : OpenIddictEntityFrameworkCoreSession + where TToken : OpenIddictEntityFrameworkCoreToken where TKey : notnull, IEquatable { ArgumentNullException.ThrowIfNull(builder); @@ -142,12 +142,12 @@ public static DbContextOptionsBuilder UseOpenIddict< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey, TContext>( this DbContextOptionsBuilder builder) - where TApplication : OpenIddictEntityFrameworkCoreApplication - where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization + where TApplication : OpenIddictEntityFrameworkCoreApplication + where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization where TResource : OpenIddictEntityFrameworkCoreResource where TScope : OpenIddictEntityFrameworkCoreScope - where TSession : OpenIddictEntityFrameworkCoreSession - where TToken : OpenIddictEntityFrameworkCoreToken + where TSession : OpenIddictEntityFrameworkCoreSession + where TToken : OpenIddictEntityFrameworkCoreToken where TKey : notnull, IEquatable where TContext : DbContext { @@ -206,22 +206,22 @@ public static ModelBuilder UseOpenIddict< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey>(this ModelBuilder builder) - where TApplication : OpenIddictEntityFrameworkCoreApplication - where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization + where TApplication : OpenIddictEntityFrameworkCoreApplication + where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization where TResource : OpenIddictEntityFrameworkCoreResource where TScope : OpenIddictEntityFrameworkCoreScope - where TSession : OpenIddictEntityFrameworkCoreSession - where TToken : OpenIddictEntityFrameworkCoreToken + where TSession : OpenIddictEntityFrameworkCoreSession + where TToken : OpenIddictEntityFrameworkCoreToken where TKey : notnull, IEquatable { ArgumentNullException.ThrowIfNull(builder); return builder - .ApplyConfiguration(new OpenIddictEntityFrameworkCoreApplicationConfiguration()) - .ApplyConfiguration(new OpenIddictEntityFrameworkCoreAuthorizationConfiguration()) + .ApplyConfiguration(new OpenIddictEntityFrameworkCoreApplicationConfiguration()) + .ApplyConfiguration(new OpenIddictEntityFrameworkCoreAuthorizationConfiguration()) .ApplyConfiguration(new OpenIddictEntityFrameworkCoreResourceConfiguration()) .ApplyConfiguration(new OpenIddictEntityFrameworkCoreScopeConfiguration()) .ApplyConfiguration(new OpenIddictEntityFrameworkCoreSessionConfiguration()) - .ApplyConfiguration(new OpenIddictEntityFrameworkCoreTokenConfiguration()); + .ApplyConfiguration(new OpenIddictEntityFrameworkCoreTokenConfiguration()); } } diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreApplicationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreApplicationStore.cs index f346446d0..fb2f1a24c 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreApplicationStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreApplicationStore.cs @@ -24,6 +24,7 @@ namespace OpenIddict.EntityFrameworkCore; public class OpenIddictEntityFrameworkCoreApplicationStore : OpenIddictEntityFrameworkCoreApplicationStore { public OpenIddictEntityFrameworkCoreApplicationStore( @@ -42,6 +43,7 @@ public class OpenIddictEntityFrameworkCoreApplicationStore< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : OpenIddictEntityFrameworkCoreApplicationStore, OpenIddictEntityFrameworkCoreAuthorization, + OpenIddictEntityFrameworkCoreSession, OpenIddictEntityFrameworkCoreToken, TKey> where TKey : notnull, IEquatable { @@ -58,16 +60,19 @@ public OpenIddictEntityFrameworkCoreApplicationStore( /// /// The type of the application entity. /// The type of the authorization entity. +/// The type of the session entity. /// The type of the token entity. /// The type of the entity primary keys. public class OpenIddictEntityFrameworkCoreApplicationStore< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApplication, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : IOpenIddictApplicationStore - where TApplication : OpenIddictEntityFrameworkCoreApplication - where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization - where TToken : OpenIddictEntityFrameworkCoreToken + where TApplication : OpenIddictEntityFrameworkCoreApplication + where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization + where TToken : OpenIddictEntityFrameworkCoreToken + where TSession : OpenIddictEntityFrameworkCoreSession where TKey : notnull, IEquatable { public OpenIddictEntityFrameworkCoreApplicationStore( @@ -142,12 +147,16 @@ await strategy.ExecuteAsync(async () => where token.Application!.Id!.Equals(application.Id) select token).ExecuteDeleteAsync(cancellationToken); - // Remove all the authorizations associated with the application and - // the tokens attached to these implicit or explicit authorizations. + // Remove all the authorizations associated with the application. await (from authorization in context.Set() where authorization.Application!.Id!.Equals(application.Id) select authorization).ExecuteDeleteAsync(cancellationToken); + // Remove all the sessions associated with the application. + await (from session in context.Set() + where session.Application!.Id!.Equals(application.Id) + select session).ExecuteDeleteAsync(cancellationToken); + // Note: calling DbContext.SaveChangesAsync() is not necessary // with bulk delete operations as they are executed immediately. @@ -165,7 +174,7 @@ await strategy.ExecuteAsync(async () => catch (DbUpdateConcurrencyException exception) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(application).State = EntityState.Unchanged; throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception); @@ -203,6 +212,36 @@ from authorization in context.Set() context.Remove(authorization); } + // Remove all the sessions associated with the application, the authorizations associated + // with the session and the tokens attached to these authorizations and sessions. + var sessions = await + (from session in context.Set() + .Include(static session => session.Authorization!.Tokens) + .Include(static session => session.Tokens) + .AsTracking() + where session.Application!.Id!.Equals(application.Id) + select session).ToListAsync(cancellationToken); + + foreach (var session in sessions) + { + if (session.Authorization is not null) + { + foreach (var token in session.Authorization.Tokens) + { + context.Remove(token); + } + + context.Remove(session.Authorization); + } + + foreach (var token in session.Tokens) + { + context.Remove(token); + } + + context.Remove(session); + } + // Remove all the tokens associated with the application. var tokens = await ( from token in context.Set().AsTracking() @@ -229,7 +268,7 @@ from token in context.Set().AsTracking() catch (DbUpdateConcurrencyException exception) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(application).State = EntityState.Unchanged; foreach (var authorization in authorizations) @@ -242,6 +281,26 @@ from token in context.Set().AsTracking() } } + foreach (var session in sessions) + { + context.Entry(session).State = EntityState.Unchanged; + + if (session.Authorization is not null) + { + context.Entry(session.Authorization).State = EntityState.Unchanged; + + foreach (var token in session.Authorization.Tokens) + { + context.Entry(token).State = EntityState.Unchanged; + } + } + + foreach (var token in session.Tokens) + { + context.Entry(token).State = EntityState.Unchanged; + } + } + foreach (var token in tokens) { context.Entry(token).State = EntityState.Unchanged; @@ -691,7 +750,7 @@ public virtual async ValueTask UpdateAsync(TApplication application, Cancellatio catch (DbUpdateConcurrencyException exception) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(application).State = EntityState.Unchanged; throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception); diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs index 22543e132..dd6fd1c50 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs @@ -22,6 +22,7 @@ namespace OpenIddict.EntityFrameworkCore; public class OpenIddictEntityFrameworkCoreAuthorizationStore : OpenIddictEntityFrameworkCoreAuthorizationStore { public OpenIddictEntityFrameworkCoreAuthorizationStore( @@ -40,6 +41,7 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : OpenIddictEntityFrameworkCoreAuthorizationStore, OpenIddictEntityFrameworkCoreApplication, + OpenIddictEntityFrameworkCoreSession, OpenIddictEntityFrameworkCoreToken, TKey> where TKey : notnull, IEquatable { @@ -56,16 +58,19 @@ public OpenIddictEntityFrameworkCoreAuthorizationStore( /// /// The type of the authorization entity. /// The type of the application entity. +/// The type of the session entity. /// The type of the token entity. /// The type of the entity primary keys. public class OpenIddictEntityFrameworkCoreAuthorizationStore< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApplication, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : IOpenIddictAuthorizationStore - where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization - where TApplication : OpenIddictEntityFrameworkCoreApplication - where TToken : OpenIddictEntityFrameworkCoreToken + where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization + where TApplication : OpenIddictEntityFrameworkCoreApplication + where TToken : OpenIddictEntityFrameworkCoreToken + where TSession : OpenIddictEntityFrameworkCoreSession where TKey : notnull, IEquatable { public OpenIddictEntityFrameworkCoreAuthorizationStore( @@ -158,7 +163,7 @@ await strategy.ExecuteAsync(async () => catch (DbUpdateConcurrencyException exception) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(authorization).State = EntityState.Unchanged; throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception); @@ -187,6 +192,33 @@ await strategy.ExecuteAsync(async () => context.Remove(token); } + // Remove all the sessions associated with the authorization + // and the tokens attached to these authorizations and sessions. + var sessions = await + (from session in context.Set().Include(static session => session.Tokens).AsTracking() + where session.Authorization!.Id!.Equals(authorization.Id) + select session).ToListAsync(cancellationToken); + + foreach (var session in sessions) + { + if (session.Authorization is not null) + { + foreach (var token in session.Authorization.Tokens) + { + context.Remove(token); + } + + context.Remove(session.Authorization); + } + + foreach (var token in session.Tokens) + { + context.Remove(token); + } + + context.Remove(session); + } + context.Remove(authorization); try @@ -201,7 +233,7 @@ await strategy.ExecuteAsync(async () => catch (DbUpdateConcurrencyException exception) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(authorization).State = EntityState.Unchanged; foreach (var token in tokens) @@ -594,7 +626,7 @@ orderby authorization.Id if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0243), exceptions); + throw new AggregateException(exceptions); } return result; @@ -655,7 +687,7 @@ public virtual async ValueTask RevokeAsync(string? subject, string? client catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(authorization).State = EntityState.Unchanged; exceptions ??= new List(capacity: 1); @@ -669,7 +701,7 @@ public virtual async ValueTask RevokeAsync(string? subject, string? client if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + throw new AggregateException(exceptions); } return result; @@ -714,7 +746,7 @@ from authorization in context.Set() catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(authorization).State = EntityState.Unchanged; exceptions ??= new List(capacity: 1); @@ -728,7 +760,7 @@ from authorization in context.Set() if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + throw new AggregateException(exceptions); } return result; @@ -772,7 +804,7 @@ from authorization in context.Set() catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(authorization).State = EntityState.Unchanged; exceptions ??= new List(capacity: 1); @@ -786,7 +818,7 @@ from authorization in context.Set() if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + throw new AggregateException(exceptions); } return result; @@ -915,7 +947,7 @@ public virtual async ValueTask UpdateAsync(TAuthorization authorization, Cancell catch (DbUpdateConcurrencyException exception) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(authorization).State = EntityState.Unchanged; throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception); diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreResourceStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreResourceStore.cs index a579fef57..56050e1e9 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreResourceStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreResourceStore.cs @@ -124,7 +124,7 @@ public virtual async ValueTask DeleteAsync(TResource resource, CancellationToken catch (DbUpdateConcurrencyException exception) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(resource).State = EntityState.Unchanged; throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception); @@ -406,7 +406,7 @@ public virtual async ValueTask UpdateAsync(TResource resource, CancellationToken catch (DbUpdateConcurrencyException exception) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(resource).State = EntityState.Unchanged; throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception); diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreScopeStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreScopeStore.cs index 56be363bd..6353a227a 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreScopeStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreScopeStore.cs @@ -124,7 +124,7 @@ public virtual async ValueTask DeleteAsync(TScope scope, CancellationToken cance catch (DbUpdateConcurrencyException exception) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(scope).State = EntityState.Unchanged; throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception); @@ -444,7 +444,7 @@ public virtual async ValueTask UpdateAsync(TScope scope, CancellationToken cance catch (DbUpdateConcurrencyException exception) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(scope).State = EntityState.Unchanged; throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception); diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreSessionStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreSessionStore.cs index c4777cb94..db734be4e 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreSessionStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreSessionStore.cs @@ -6,6 +6,7 @@ using System.Collections.Immutable; using System.ComponentModel; +using System.Data; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Text.Json; @@ -66,10 +67,10 @@ public class OpenIddictEntityFrameworkCoreSessionStore< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : IOpenIddictSessionStore - where TSession : OpenIddictEntityFrameworkCoreSession - where TApplication : OpenIddictEntityFrameworkCoreApplication - where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization - where TToken : OpenIddictEntityFrameworkCoreToken + where TSession : OpenIddictEntityFrameworkCoreSession + where TApplication : OpenIddictEntityFrameworkCoreApplication + where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization + where TToken : OpenIddictEntityFrameworkCoreToken where TKey : notnull, IEquatable { public OpenIddictEntityFrameworkCoreSessionStore( @@ -129,6 +130,24 @@ public virtual async ValueTask DeleteAsync(TSession session, CancellationToken c var context = await Context.GetDbContextAsync(cancellationToken); + // To prevent an SQL exception from being thrown if a new associated entity is + // created after the existing entries have been listed, the following logic is + // executed in a serializable transaction, that will lock the affected tables. + using var transaction = await CreateTransactionAsync(context, + IsolationLevel.Serializable, cancellationToken); + + // Remove all the tokens associated with the session. + var tokens = await + (from token in context.Set() + where token.Authorization == null + where token.Session!.Id!.Equals(session.Id) + select token).ToListAsync(cancellationToken); + + foreach (var token in tokens) + { + context.Remove(token); + } + context.Remove(session); try @@ -138,7 +157,7 @@ public virtual async ValueTask DeleteAsync(TSession session, CancellationToken c catch (DbUpdateConcurrencyException exception) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(session).State = EntityState.Unchanged; throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception); @@ -147,7 +166,7 @@ public virtual async ValueTask DeleteAsync(TSession session, CancellationToken c /// public virtual async IAsyncEnumerable FindAsync( - (string? Subject, string? LoginId, string? ApplicationId, string? Status) query, + (string? Subject, string? LoginId, string? ApplicationId, string? AuthorizationId, string? Status) query, [EnumeratorCancellation] CancellationToken cancellationToken) { var context = await Context.GetDbContextAsync(cancellationToken); @@ -168,6 +187,12 @@ public virtual async IAsyncEnumerable FindAsync( sessions = sessions.Where(session => session.Application!.Id!.Equals(key)); } + if (!string.IsNullOrEmpty(query.AuthorizationId)) + { + var key = ConvertIdentifierFromString(query.AuthorizationId); + sessions = sessions.Where(session => session.Authorization!.Id!.Equals(key)); + } + if (!string.IsNullOrEmpty(query.LoginId)) { sessions = sessions.Where(session => session.LoginId == query.LoginId); @@ -466,6 +491,124 @@ async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] Cancellati } } + /// + public virtual async ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + List? exceptions = null; + + var result = 0L; + + // Note: the Oracle MySQL provider doesn't support DateTimeOffset and is unable + // to create a SQL query with an expression calling DateTimeOffset.UtcDateTime. + // To work around this limitation, the threshold represented as a DateTimeOffset + // instance is manually converted to a UTC DateTime instance outside the query. + var date = threshold.UtcDateTime; + + // Note: to avoid sending too many queries, the maximum number of elements + // that can be removed by a single call to PruneAsync() is deliberately limited. + for (var index = 0; index < 1_000; index++) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (!Options.CurrentValue.DisableBulkOperations) + { + try + { + var count = await + (from session in context.Set() + where session.CreationDate < date + where session.Status != Statuses.Valid + where !session.Tokens.Any() + orderby session.Id + select session).Take(1_000).ExecuteDeleteAsync(cancellationToken); + + if (count is 0) + { + break; + } + + // Note: calling DbContext.SaveChangesAsync() is not necessary + // with bulk delete operations as they are executed immediately. + + result += count; + } + + catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) + { + exceptions ??= new List(capacity: 1); + exceptions.Add(exception); + } + } + + else + { + var strategy = context.Database.CreateExecutionStrategy(); + var count = await strategy.ExecuteAsync(async () => + { + // To prevent concurrency exceptions from being thrown if an entry is modified + // after it was retrieved from the database, the following logic is executed in + // a repeatable read transaction, that will put a lock on the retrieved entries + // and thus prevent them from being concurrently modified outside this block. + await using var transaction = await CreateTransactionAsync(context, + IsolationLevel.RepeatableRead, cancellationToken); + + var sessions = await + (from session in context.Set() + .Include(static session => session.Tokens) + .AsTracking() + where session.CreationDate < date + where session.Status != Statuses.Valid + where !session.Tokens.Any() + orderby session.Id + select session).Take(1_000).ToListAsync(cancellationToken); + + if (sessions.Count is not 0) + { + // Note: new tokens may be attached after the sessions were retrieved + // from the database since the transaction level is deliberately limited to + // repeatable read instead of serializable for performance reasons). In this + // case, the operation will fail, which is considered an acceptable risk. + context.RemoveRange(sessions); + + try + { + await context.SaveChangesAsync(cancellationToken); + + if (transaction is not null) + { + await transaction.CommitAsync(cancellationToken); + } + } + + catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) + { + exceptions ??= new List(capacity: 1); + exceptions.Add(exception); + } + } + + return sessions.Count; + }); + + if (count is 0) + { + break; + } + + result += count; + } + } + + if (exceptions is { Count: > 0 }) + { + throw new AggregateException(exceptions); + } + + return result; + } + /// public virtual async ValueTask SetApplicationIdAsync(TSession session, string? identifier, CancellationToken cancellationToken) { @@ -511,7 +654,7 @@ public virtual async ValueTask SetAuthorizationIdAsync(TSession session, string? session.Authorization = await context.Set() .FindAsync([ConvertIdentifierFromString(identifier)], cancellationToken) - ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0251)); + ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0244)); } else @@ -607,7 +750,7 @@ public virtual async ValueTask UpdateAsync(TSession session, CancellationToken c catch (DbUpdateConcurrencyException exception) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(session).State = EntityState.Unchanged; throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception); @@ -659,4 +802,37 @@ public virtual async ValueTask UpdateAsync(TSession session, CancellationToken c return converter.ConvertToInvariantString(identifier); } + + /// + /// Tries to create a new with the specified . + /// + /// The Entity Framework Core context. + /// The desired level of isolation. + /// The that can be used to abort the operation. + /// The if it could be created, otherwise. + protected virtual async ValueTask CreateTransactionAsync( + DbContext context, IsolationLevel level, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(context); + + // Note: transactions that specify an explicit isolation level are only supported by + // relational providers and trying to use them with a different provider results in + // an invalid operation exception being thrown at runtime. To prevent that, a manual + // check is made to ensure the underlying transaction manager is relational. + var manager = context.GetService(); + if (manager is IRelationalTransactionManager) + { + try + { + return await context.Database.BeginTransactionAsync(level, cancellationToken); + } + + catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) + { + return null; + } + } + + return null; + } } diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs index 4e748d336..c224d7be1 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs @@ -22,7 +22,8 @@ namespace OpenIddict.EntityFrameworkCore; public class OpenIddictEntityFrameworkCoreTokenStore : OpenIddictEntityFrameworkCoreTokenStore + OpenIddictEntityFrameworkCoreAuthorization, + OpenIddictEntityFrameworkCoreSession, string> { public OpenIddictEntityFrameworkCoreTokenStore( IOpenIddictEntityFrameworkCoreContext context, @@ -40,7 +41,8 @@ public class OpenIddictEntityFrameworkCoreTokenStore< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : OpenIddictEntityFrameworkCoreTokenStore, OpenIddictEntityFrameworkCoreApplication, - OpenIddictEntityFrameworkCoreAuthorization, TKey> + OpenIddictEntityFrameworkCoreAuthorization, + OpenIddictEntityFrameworkCoreSession, TKey> where TKey : notnull, IEquatable { public OpenIddictEntityFrameworkCoreTokenStore( @@ -57,15 +59,18 @@ public OpenIddictEntityFrameworkCoreTokenStore( /// The type of the token entity. /// The type of the application entity. /// The type of the authorization entity. +/// The type of the session entity. /// The type of the entity primary keys. public class OpenIddictEntityFrameworkCoreTokenStore< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApplication, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : IOpenIddictTokenStore - where TToken : OpenIddictEntityFrameworkCoreToken - where TApplication : OpenIddictEntityFrameworkCoreApplication - where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization + where TToken : OpenIddictEntityFrameworkCoreToken + where TApplication : OpenIddictEntityFrameworkCoreApplication + where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization + where TSession : OpenIddictEntityFrameworkCoreSession where TKey : notnull, IEquatable { public OpenIddictEntityFrameworkCoreTokenStore( @@ -134,7 +139,7 @@ public virtual async ValueTask DeleteAsync(TToken token, CancellationToken cance catch (DbUpdateConcurrencyException exception) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(token).State = EntityState.Unchanged; throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception); @@ -413,6 +418,33 @@ public virtual ValueTask> GetProperties return new(token.ReferenceId); } + /// + public virtual async ValueTask GetSessionIdAsync(TToken token, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(token); + + // If the session is not attached to the token, try to load it manually. + if (token.Session is null) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + var reference = context.Entry(token).Reference(static entry => entry.Session); + if (reference.EntityEntry.State is EntityState.Detached) + { + return null; + } + + await reference.LoadAsync(cancellationToken); + } + + if (token.Session is null) + { + return null; + } + + return ConvertIdentifierToString(token.Session.Id); + } + /// public virtual ValueTask GetStatusAsync(TToken token, CancellationToken cancellationToken) { @@ -611,7 +643,7 @@ orderby token.Id if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + throw new AggregateException(exceptions); } return result; @@ -672,7 +704,7 @@ public virtual async ValueTask RevokeAsync(string? subject, string? client catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(token).State = EntityState.Unchanged; exceptions ??= new List(capacity: 1); @@ -686,7 +718,7 @@ public virtual async ValueTask RevokeAsync(string? subject, string? client if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + throw new AggregateException(exceptions); } return result; @@ -734,7 +766,7 @@ from token in context.Set() catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(token).State = EntityState.Unchanged; exceptions ??= new List(capacity: 1); @@ -748,7 +780,7 @@ from token in context.Set() if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + throw new AggregateException(exceptions); } return result; @@ -796,7 +828,7 @@ from token in context.Set() catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(token).State = EntityState.Unchanged; exceptions ??= new List(capacity: 1); @@ -810,7 +842,7 @@ from token in context.Set() if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + throw new AggregateException(exceptions); } return result; @@ -857,7 +889,7 @@ from token in context.Set() catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(token).State = EntityState.Unchanged; exceptions ??= new List(capacity: 1); @@ -871,7 +903,7 @@ from token in context.Set() if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + throw new AggregateException(exceptions); } return result; @@ -922,7 +954,7 @@ public virtual async ValueTask SetAuthorizationIdAsync(TToken token, string? ide token.Authorization = await context.Set() .FindAsync([ConvertIdentifierFromString(identifier)], cancellationToken) - ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0251)); + ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0244)); } else @@ -945,6 +977,40 @@ public virtual async ValueTask SetAuthorizationIdAsync(TToken token, string? ide } } + /// + public virtual async ValueTask SetSessionIdAsync(TToken token, string? identifier, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(token); + + if (!string.IsNullOrEmpty(identifier)) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + token.Session = await context.Set() + .FindAsync([ConvertIdentifierFromString(identifier)], cancellationToken) + ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0244)); + } + + else + { + // If the session is not attached to the token, try to load it manually. + if (token.Session is null) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + var reference = context.Entry(token).Reference(static entry => entry.Session); + if (reference.EntityEntry.State is EntityState.Detached) + { + return; + } + + await reference.LoadAsync(cancellationToken); + } + + token.Session = null; + } + } + /// public virtual ValueTask SetCreationDateAsync(TToken token, DateTimeOffset? date, CancellationToken cancellationToken) { @@ -1058,7 +1124,7 @@ public virtual async ValueTask UpdateAsync(TToken token, CancellationToken cance catch (DbUpdateConcurrencyException exception) { - // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + // Reset the state of the updated entities to prevents future calls from failing. context.Entry(token).State = EntityState.Unchanged; throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception); diff --git a/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbToken.cs b/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbToken.cs index 4e1f8027b..ca21d80de 100644 --- a/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbToken.cs +++ b/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbToken.cs @@ -82,6 +82,12 @@ public class OpenIddictMongoDbToken [BsonElement("reference_id"), BsonIgnoreIfNull] public virtual string? ReferenceId { get; set; } + /// + /// Gets or sets the identifier of the session associated with the token. + /// + [BsonElement("session_id"), BsonIgnoreIfDefault] + public virtual ObjectId SessionId { get; set; } + /// /// Gets or sets the status of the token. /// diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbApplicationStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbApplicationStore.cs index d993a3c1f..87146007d 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbApplicationStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbApplicationStore.cs @@ -109,6 +109,10 @@ public virtual async ValueTask DeleteAsync(TApplication application, Cancellatio await database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName) .DeleteManyAsync(authorization => authorization.ApplicationId == application.Id, cancellationToken); + // Delete the sessions associated with the application. + await database.GetCollection(Options.CurrentValue.SessionsCollectionName) + .DeleteManyAsync(session => session.ApplicationId == application.Id, cancellationToken); + // Delete the tokens associated with the application. await database.GetCollection(Options.CurrentValue.TokensCollectionName) .DeleteManyAsync(token => token.ApplicationId == application.Id, cancellationToken); diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbAuthorizationStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbAuthorizationStore.cs index bea5a66b3..0225e7902 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbAuthorizationStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbAuthorizationStore.cs @@ -103,6 +103,10 @@ public virtual async ValueTask DeleteAsync(TAuthorization authorization, Cancell throw new ConcurrencyException(SR.GetResourceString(SR.ID0239)); } + // Delete the sessions associated with the authorization. + await database.GetCollection(Options.CurrentValue.SessionsCollectionName) + .DeleteManyAsync(session => session.AuthorizationId == authorization.Id, cancellationToken); + // Delete the tokens associated with the authorization. await database.GetCollection(Options.CurrentValue.TokensCollectionName) .DeleteManyAsync(token => token.AuthorizationId == authorization.Id, cancellationToken); diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbSessionStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbSessionStore.cs index 557f68827..850b98411 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbSessionStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbSessionStore.cs @@ -102,11 +102,15 @@ public virtual async ValueTask DeleteAsync(TSession session, CancellationToken c { throw new ConcurrencyException(SR.GetResourceString(SR.ID0239)); } + + // Delete the tokens associated with the session. + await database.GetCollection(Options.CurrentValue.TokensCollectionName) + .DeleteManyAsync(token => token.SessionId == session.Id, cancellationToken); } /// public virtual async IAsyncEnumerable FindAsync( - (string? Subject, string? LoginId, string? ApplicationId, string? Status) query, + (string? Subject, string? LoginId, string? ApplicationId, string? AuthorizationId, string? Status) query, [EnumeratorCancellation] CancellationToken cancellationToken) { var database = await Context.GetDatabaseAsync(cancellationToken); @@ -124,6 +128,11 @@ public virtual async IAsyncEnumerable FindAsync( sessions = sessions.Where(session => session.ApplicationId == ObjectId.Parse(query.ApplicationId)); } + if (!string.IsNullOrEmpty(query.AuthorizationId)) + { + sessions = sessions.Where(session => session.AuthorizationId == ObjectId.Parse(query.AuthorizationId)); + } + if (!string.IsNullOrEmpty(query.LoginId)) { sessions = sessions.Where(session => session.LoginId == query.LoginId); @@ -380,6 +389,37 @@ async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] Cancellati } } + /// + public virtual async ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.SessionsCollectionName); + + var result = 0L; + + // Note: directly deleting the resulting set of an aggregate query is not supported by MongoDB. + // To work around this limitation, the session identifiers are stored in an intermediate + // list and delete requests are sent to remove the documents corresponding to these identifiers. + + var identifiers = + await (from session in collection.AsQueryable() + join token in database.GetCollection(Options.CurrentValue.TokensCollectionName).AsQueryable() + on session.Id equals token.SessionId into tokens + where session.CreationDate < threshold.UtcDateTime + where session.Status != Statuses.Valid + where !tokens.Any() + select session.Id).ToListAsync(cancellationToken); + + // Note: to avoid generating delete requests with very large filters, chunking is used here and the + // maximum number of elements that can be removed by a single call to PruneAsync() is deliberately limited. + foreach (var chunk in identifiers.Take(1_000_000).Chunk(1_000)) + { + result += (await collection.DeleteManyAsync(session => chunk.Contains(session.Id), cancellationToken)).DeletedCount; + } + + return result; + } + /// public virtual ValueTask SetApplicationIdAsync(TSession session, string? identifier, CancellationToken cancellationToken) { diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs index 40ef14841..a7bbcc146 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs @@ -319,6 +319,14 @@ public virtual ValueTask> GetProperties return new(token.ReferenceId); } + /// + public virtual ValueTask GetSessionIdAsync(TToken token, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(token); + + return new(token.SessionId != ObjectId.Empty ? token.SessionId.ToString() : null); + } + /// public virtual ValueTask GetStatusAsync(TToken token, CancellationToken cancellationToken) { @@ -423,7 +431,7 @@ on token.AuthorizationId equals authorization.Id into authorizations where token.CreationDate < threshold.UtcDateTime where (token.Status != Statuses.Inactive && token.Status != Statuses.Valid) || token.ExpirationDate < DateTime.UtcNow || - authorizations.Any(token => token.Status != Statuses.Valid) + authorizations.Any(static authorization => authorization.Status != Statuses.Valid) select token.Id).ToListAsync(cancellationToken); // Note: to avoid generating delete requests with very large filters, chunking is used here and the @@ -622,6 +630,16 @@ public virtual ValueTask SetReferenceIdAsync(TToken token, string? identifier, C return ValueTask.CompletedTask; } + /// + public virtual ValueTask SetSessionIdAsync(TToken token, string? identifier, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(token); + + token.SessionId = !string.IsNullOrEmpty(identifier) ? ObjectId.Parse(identifier) : ObjectId.Empty; + + return ValueTask.CompletedTask; + } + /// public virtual ValueTask SetStatusAsync(TToken token, string? status, CancellationToken cancellationToken) { diff --git a/src/OpenIddict.Quartz/OpenIddictQuartzBuilder.cs b/src/OpenIddict.Quartz/OpenIddictQuartzBuilder.cs index 4f78eb21a..c11a86f9d 100644 --- a/src/OpenIddict.Quartz/OpenIddictQuartzBuilder.cs +++ b/src/OpenIddict.Quartz/OpenIddictQuartzBuilder.cs @@ -62,6 +62,13 @@ public OpenIddictQuartzBuilder Configure(Action configu public OpenIddictQuartzBuilder DisableAuthorizationPruning() => Configure(options => options.DisableAuthorizationPruning = true); + /// + /// Disables sessions pruning. + /// + /// The instance. + public OpenIddictQuartzBuilder DisableSessionPruning() + => Configure(options => options.DisableSessionPruning = true); + /// /// Disables tokens pruning. /// @@ -93,6 +100,18 @@ public OpenIddictQuartzBuilder SetMinimumAuthorizationLifespan(TimeSpan lifespan return Configure(options => options.MinimumAuthorizationLifespan = lifespan); } + /// + /// Sets the minimum lifespan sessions must have to be pruned. + /// + /// The minimum lifespan sessions must have to be pruned. + /// The instance. + public OpenIddictQuartzBuilder SetMinimumSessionLifespan(TimeSpan lifespan) + { + ArgumentOutOfRangeException.ThrowIfLessThan(lifespan, TimeSpan.FromMinutes(10)); + + return Configure(options => options.MinimumSessionLifespan = lifespan); + } + /// /// Sets the minimum lifespan tokens must have to be pruned. /// diff --git a/src/OpenIddict.Quartz/OpenIddictQuartzJob.cs b/src/OpenIddict.Quartz/OpenIddictQuartzJob.cs index f22264be1..4b4277fb1 100644 --- a/src/OpenIddict.Quartz/OpenIddictQuartzJob.cs +++ b/src/OpenIddict.Quartz/OpenIddictQuartzJob.cs @@ -146,6 +146,54 @@ public async Task Execute(IJobExecutionContext context) } } + // Important: since sessions that still have tokens attached are never + // pruned, the tokens MUST be deleted before deleting the sessions. + + if (!_options.CurrentValue.DisableSessionPruning) + { + var manager = scope.ServiceProvider.GetService() + ?? throw new JobExecutionException(new InvalidOperationException(SR.GetResourceString(SR.ID0278))) + { + RefireImmediately = false, + UnscheduleAllTriggers = true, + UnscheduleFiringTrigger = true + }; + + var threshold = _options.CurrentValue.TimeProvider.GetUtcNow() - _options.CurrentValue.MinimumSessionLifespan; + + try + { + await manager.PruneAsync(threshold, context.CancellationToken); + } + + // OperationCanceledExceptions are typically thrown when the host is about to shut down. + // To allow the host to shut down as fast as possible, this exception type is special-cased + // to prevent further processing in this job and inform Quartz.NET it shouldn't be refired. + catch (OperationCanceledException exception) when (context.CancellationToken.IsCancellationRequested) + { + throw new JobExecutionException(exception) + { + RefireImmediately = false + }; + } + + // AggregateExceptions are generally thrown by the manager itself when one or multiple exception(s) + // occurred while trying to prune the entities. In this case, add the inner exceptions to the collection. + catch (AggregateException exception) when (!OpenIddictHelpers.IsFatal(exception)) + { + exceptions ??= new List(capacity: exception.InnerExceptions.Count); + exceptions.AddRange(exception.InnerExceptions); + } + + // Other non-fatal exceptions are assumed to be transient and are added to the exceptions collection + // to be re-thrown later (typically, at the very end of this job, as an AggregateException). + catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) + { + exceptions ??= new List(capacity: 1); + exceptions.Add(exception); + } + } + if (exceptions is { Count: > 0 }) { throw new JobExecutionException(new AggregateException(exceptions)) diff --git a/src/OpenIddict.Quartz/OpenIddictQuartzOptions.cs b/src/OpenIddict.Quartz/OpenIddictQuartzOptions.cs index 1a77ba310..57539a241 100644 --- a/src/OpenIddict.Quartz/OpenIddictQuartzOptions.cs +++ b/src/OpenIddict.Quartz/OpenIddictQuartzOptions.cs @@ -16,6 +16,11 @@ public sealed class OpenIddictQuartzOptions /// public bool DisableAuthorizationPruning { get; set; } + /// + /// Gets or sets a boolean indicating whether sessions pruning should be disabled. + /// + public bool DisableSessionPruning { get; set; } + /// /// Gets or sets a boolean indicating whether tokens pruning should be disabled. /// @@ -33,6 +38,12 @@ public sealed class OpenIddictQuartzOptions /// public TimeSpan MinimumAuthorizationLifespan { get; set; } = TimeSpan.FromDays(14); + /// + /// Gets or sets the minimum lifespan sessions must have to be pruned. + /// By default, this value is set to 14 days and cannot be less than 10 minutes. + /// + public TimeSpan MinimumSessionLifespan { 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. diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs index 32c71a11c..c863b4f8b 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs @@ -189,6 +189,11 @@ public OpenIddictRequest? Request /// public string? AuthorizationId { get; set; } + /// + /// Gets or sets the session entry identifier associated with the token, if applicable. + /// + public string? SessionId { get; set; } + /// /// Gets or sets the token entry identifier associated with the token, if applicable. /// diff --git a/src/OpenIddict.Server/OpenIddictServerExtensions.cs b/src/OpenIddict.Server/OpenIddictServerExtensions.cs index baba1377c..ecf195ea3 100644 --- a/src/OpenIddict.Server/OpenIddictServerExtensions.cs +++ b/src/OpenIddict.Server/OpenIddictServerExtensions.cs @@ -82,6 +82,7 @@ public static OpenIddictServerBuilder AddServer(this OpenIddictBuilder builder) builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); diff --git a/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs b/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs index 3a9b15002..d13b5333c 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs @@ -655,6 +655,20 @@ public ValueTask IsActiveAsync(BaseContext context) } } + /// + /// Represents a filter that excludes the associated handlers if no session identifier is resolved from the token. + /// + public sealed class RequireSessionIdResolved : IOpenIddictServerHandlerFilter + { + /// + public ValueTask IsActiveAsync(ValidateTokenContext context) + { + ArgumentNullException.ThrowIfNull(context); + + return new(!string.IsNullOrEmpty(context.SessionId)); + } + } + /// /// Represents a filter that excludes the associated handlers if sliding refresh token expiration was disabled. /// diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs index 267387428..6310535d0 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs @@ -10,7 +10,6 @@ using System.Text.Json; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; namespace OpenIddict.Server; diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs index a3481f331..a4c9a3228 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs @@ -9,7 +9,6 @@ using System.Security.Claims; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; namespace OpenIddict.Server; diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs index d23871367..3a1568c38 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs @@ -13,7 +13,6 @@ using System.Text; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; namespace OpenIddict.Server; diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs index b82adb439..01b1aed90 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs @@ -16,7 +16,6 @@ using System.Text.Json.Nodes; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; @@ -45,6 +44,7 @@ public static class Protection ValidateProofOfPossession.Descriptor, ValidateTokenEntry.Descriptor, ValidateAuthorizationEntry.Descriptor, + ValidateSessionEntry.Descriptor, /* * Token generation: @@ -833,6 +833,7 @@ public async ValueTask HandleAsync(ValidateTokenContext context) .SetCreationDate(await manager.GetCreationDateAsync(token, context.CancellationToken)) .SetExpirationDate(await manager.GetExpirationDateAsync(token, context.CancellationToken)) .SetAuthorizationId(context.AuthorizationId = await manager.GetAuthorizationIdAsync(token, context.CancellationToken)) + .SetSessionId(context.SessionId = await manager.GetSessionIdAsync(token, context.CancellationToken)) .SetTokenId(context.TokenId = await manager.GetIdAsync(token, context.CancellationToken)) .SetTokenType(await manager.GetTypeAsync(token, context.CancellationToken)); } @@ -1414,6 +1415,50 @@ public async ValueTask HandleAsync(ValidateTokenContext context) } } + /// + /// Contains the logic responsible for rejecting tokens whose + /// associated session entry is no longer valid (e.g was revoked). + /// Note: this handler is not used when the degraded mode is enabled. + /// + public sealed class ValidateSessionEntry : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateAuthorizationEntry.Descriptor.Order + 1_000) + .SetType(OpenIddictServerHandlerType.BuiltIn) + .Build(); + + public async ValueTask HandleAsync(ValidateTokenContext context) + { + ArgumentNullException.ThrowIfNull(context); + + Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + Debug.Assert(!string.IsNullOrEmpty(context.SessionId), SR.GetResourceString(SR.ID4022)); + + var manager = context.ServiceProvider.GetService() + ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); + + var session = await manager.FindByIdAsync(context.SessionId, context.CancellationToken); + if (session is null || !await manager.HasStatusAsync(session, Statuses.Valid, context.CancellationToken)) + { + context.Logger.LogInformation(6297, SR.GetResourceString(SR.ID6297), context.SessionId); + + context.Reject( + error: Errors.InvalidToken, + description: SR.GetResourceString(SR.ID2210), + uri: SR.FormatID8000(SR.ID2210)); + + return; + } + } + } + /// /// Contains the logic responsible for resolving the signing and encryption credentials used to protect tokens. /// @@ -1489,6 +1534,7 @@ public async ValueTask HandleAsync(GenerateTokenContext context) CreationDate = context.Principal.GetCreationDate(), ExpirationDate = context.Principal.GetExpirationDate(), Principal = context.Principal, + SessionId = context.Principal.GetSessionId(), Type = context.TokenType }; diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index 4cd5fc288..43a2cc209 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -18,7 +18,6 @@ using System.Text.Json.Nodes; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; diff --git a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs index bd8937fc9..95611c298 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs @@ -592,6 +592,16 @@ public OpenIddictValidationBuilder AddAudiences(params string[] audiences) public OpenIddictValidationBuilder EnableAuthorizationEntryValidation() => Configure(options => options.EnableAuthorizationEntryValidation = true); + /// + /// Enables session validation so that a database call is made for each API request + /// to ensure the session associated with the access token is still valid. + /// Note: enabling this option may have an impact on performance and + /// can only be used with an OpenIddict-based authorization server. + /// + /// The instance. + public OpenIddictValidationBuilder EnableSessionEntryValidation() + => Configure(options => options.EnableSessionEntryValidation = true); + /// /// Enables token validation so that a database call is made for each API request /// to ensure the token entry associated with the access token is still valid. diff --git a/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs b/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs index 4e3b326ae..0df41d8e8 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs @@ -172,15 +172,11 @@ public ValidateOptionsResult Validate(string? name, OpenIddictValidationOptions builder.AddError(SR.GetResourceString(SR.ID0132)); } - if (options.EnableAuthorizationEntryValidation) + if (options.EnableAuthorizationEntryValidation || + options.EnableSessionEntryValidation || options.EnableTokenEntryValidation) { builder.AddError(SR.GetResourceString(SR.ID0133)); } - - if (options.EnableTokenEntryValidation) - { - builder.AddError(SR.GetResourceString(SR.ID0134)); - } } var now = options.TimeProvider.GetUtcNow().LocalDateTime; diff --git a/src/OpenIddict.Validation/OpenIddictValidationEvents.Protection.cs b/src/OpenIddict.Validation/OpenIddictValidationEvents.Protection.cs index 04b691323..ac9ed97a1 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationEvents.Protection.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationEvents.Protection.cs @@ -178,6 +178,11 @@ public OpenIddictRequest? Request /// public string? AuthorizationId { get; set; } + /// + /// Gets or sets the session entry identifier associated with the token, if applicable. + /// + public string? SessionId { get; set; } + /// /// Gets or sets the token entry identifier associated with the token, if applicable. /// diff --git a/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs b/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs index 67e1f76c9..eafca58f9 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs @@ -45,6 +45,8 @@ public static OpenIddictValidationBuilder AddValidation(this OpenIddictBuilder b builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs index 4257ab06f..07d2ac38b 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs @@ -123,6 +123,34 @@ public ValueTask IsActiveAsync(BaseContext context) } } + /// + /// Represents a filter that excludes the associated handlers if session validation was not enabled. + /// + public sealed class RequireSessionEntryValidationEnabled : IOpenIddictValidationHandlerFilter + { + /// + public ValueTask IsActiveAsync(BaseContext context) + { + ArgumentNullException.ThrowIfNull(context); + + return new(context.Options.EnableSessionEntryValidation); + } + } + + /// + /// Represents a filter that excludes the associated handlers if no session identifier is resolved from the token. + /// + public sealed class RequireSessionIdResolved : IOpenIddictValidationHandlerFilter + { + /// + public ValueTask IsActiveAsync(ValidateTokenContext context) + { + ArgumentNullException.ThrowIfNull(context); + + return new(!string.IsNullOrEmpty(context.SessionId)); + } + } + /// /// Represents a filter that excludes the associated handlers if token audience validation was disabled. /// diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs index b5c15cc62..6414021d1 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs @@ -42,6 +42,7 @@ public static class Protection ValidateProofOfPossession.Descriptor, ValidateTokenEntry.Descriptor, ValidateAuthorizationEntry.Descriptor, + ValidateSessionEntry.Descriptor, /* * Token generation: @@ -586,6 +587,7 @@ public async ValueTask HandleAsync(ValidateTokenContext context) .SetCreationDate(await manager.GetCreationDateAsync(token, context.CancellationToken)) .SetExpirationDate(await manager.GetExpirationDateAsync(token, context.CancellationToken)) .SetAuthorizationId(context.AuthorizationId = await manager.GetAuthorizationIdAsync(token, context.CancellationToken)) + .SetSessionId(context.SessionId = await manager.GetSessionIdAsync(token, context.CancellationToken)) .SetTokenId(context.TokenId = await manager.GetIdAsync(token, context.CancellationToken)) .SetTokenType(await manager.GetTypeAsync(token, context.CancellationToken)); } @@ -955,7 +957,7 @@ public async ValueTask HandleAsync(ValidateTokenContext context) Debug.Assert(!string.IsNullOrEmpty(context.AuthorizationId), SR.GetResourceString(SR.ID4018)); var manager = context.ServiceProvider.GetService() - ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0142)); + ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0139)); var authorization = await manager.FindByIdAsync(context.AuthorizationId, context.CancellationToken); if (authorization is null || !await manager.HasStatusAsync(authorization, Statuses.Valid, context.CancellationToken)) @@ -972,6 +974,50 @@ public async ValueTask HandleAsync(ValidateTokenContext context) } } + /// + /// Contains the logic responsible for rejecting tokens whose + /// associated session entry is no longer valid (e.g was revoked). + /// + public sealed class ValidateSessionEntry : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateAuthorizationEntry.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(ValidateTokenContext context) + { + ArgumentNullException.ThrowIfNull(context); + + Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + Debug.Assert(!string.IsNullOrEmpty(context.SessionId), SR.GetResourceString(SR.ID4022)); + + var manager = context.ServiceProvider.GetService() + ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0139)); + + var session = await manager.FindByIdAsync(context.SessionId, context.CancellationToken); + if (session is null || !await manager.HasStatusAsync(session, Statuses.Valid, context.CancellationToken)) + { + context.Logger.LogInformation(6297, SR.GetResourceString(SR.ID6297), context.SessionId); + + context.Reject( + error: Errors.InvalidToken, + description: SR.GetResourceString(SR.ID2210), + uri: SR.FormatID8000(SR.ID2210)); + + return; + } + } + } + /// /// Contains the logic responsible for resolving the signing and encryption credentials used to protect tokens. /// diff --git a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs index a0a2aff85..1a6d3a250 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs @@ -101,6 +101,14 @@ public sealed class OpenIddictValidationOptions /// public bool EnableAuthorizationEntryValidation { get; set; } + /// + /// Gets or sets a boolean indicating whether a database call is made + /// to validate the session entry associated with the received tokens. + /// Note: enabling this option may have an impact on performance and + /// can only be used with an OpenIddict-based authorization server. + /// + public bool EnableSessionEntryValidation { get; set; } + /// /// Gets or sets a boolean indicating whether a database call is made /// to validate the token entry associated with the received tokens. diff --git a/test/OpenIddict.EntityFramework.Tests/OpenIddictEntityFrameworkBuilderTests.cs b/test/OpenIddict.EntityFramework.Tests/OpenIddictEntityFrameworkBuilderTests.cs index 051f04d37..131d23add 100644 --- a/test/OpenIddict.EntityFramework.Tests/OpenIddictEntityFrameworkBuilderTests.cs +++ b/test/OpenIddict.EntityFramework.Tests/OpenIddictEntityFrameworkBuilderTests.cs @@ -39,11 +39,11 @@ public void ReplaceDefaultEntities_StoresAreCorrectlyReplaced() Assert.Contains(services, service => service.Lifetime is ServiceLifetime.Scoped && service.ServiceType == typeof(IOpenIddictApplicationStore) && - service.ImplementationType == typeof(OpenIddictEntityFrameworkApplicationStore)); + service.ImplementationType == typeof(OpenIddictEntityFrameworkApplicationStore)); Assert.Contains(services, service => service.Lifetime is ServiceLifetime.Scoped && service.ServiceType == typeof(IOpenIddictAuthorizationStore) && - service.ImplementationType == typeof(OpenIddictEntityFrameworkAuthorizationStore)); + service.ImplementationType == typeof(OpenIddictEntityFrameworkAuthorizationStore)); Assert.Contains(services, service => service.Lifetime is ServiceLifetime.Scoped && service.ServiceType == typeof(IOpenIddictResourceStore) && @@ -59,7 +59,7 @@ service.Lifetime is ServiceLifetime.Scoped && Assert.Contains(services, service => service.Lifetime is ServiceLifetime.Scoped && service.ServiceType == typeof(IOpenIddictTokenStore) && - service.ImplementationType == typeof(OpenIddictEntityFrameworkTokenStore)); + service.ImplementationType == typeof(OpenIddictEntityFrameworkTokenStore)); } [Fact] @@ -90,12 +90,12 @@ private static IServiceCollection CreateServices() return services; } - public class CustomApplication : OpenIddictEntityFrameworkApplication; - public class CustomAuthorization : OpenIddictEntityFrameworkAuthorization; + public class CustomApplication : OpenIddictEntityFrameworkApplication; + public class CustomAuthorization : OpenIddictEntityFrameworkAuthorization; public class CustomResource : OpenIddictEntityFrameworkResource; public class CustomScope : OpenIddictEntityFrameworkScope; - public class CustomSession : OpenIddictEntityFrameworkSession; - public class CustomToken : OpenIddictEntityFrameworkToken; + public class CustomSession : OpenIddictEntityFrameworkSession; + public class CustomToken : OpenIddictEntityFrameworkToken; public class CustomDbContext : DbContext { diff --git a/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreBuilderTests.cs b/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreBuilderTests.cs index 496d31c5b..137b165e8 100644 --- a/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreBuilderTests.cs +++ b/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreBuilderTests.cs @@ -39,11 +39,11 @@ public void ReplaceDefaultEntities_StoresAreCorrectlyReplaced() Assert.Contains(services, service => service.Lifetime is ServiceLifetime.Scoped && service.ServiceType == typeof(IOpenIddictApplicationStore) && - service.ImplementationType == typeof(OpenIddictEntityFrameworkCoreApplicationStore)); + service.ImplementationType == typeof(OpenIddictEntityFrameworkCoreApplicationStore)); Assert.Contains(services, service => service.Lifetime is ServiceLifetime.Scoped && service.ServiceType == typeof(IOpenIddictAuthorizationStore) && - service.ImplementationType == typeof(OpenIddictEntityFrameworkCoreAuthorizationStore)); + service.ImplementationType == typeof(OpenIddictEntityFrameworkCoreAuthorizationStore)); Assert.Contains(services, service => service.Lifetime is ServiceLifetime.Scoped && service.ServiceType == typeof(IOpenIddictResourceStore) && @@ -59,7 +59,7 @@ service.Lifetime is ServiceLifetime.Scoped && Assert.Contains(services, service => service.Lifetime is ServiceLifetime.Scoped && service.ServiceType == typeof(IOpenIddictTokenStore) && - service.ImplementationType == typeof(OpenIddictEntityFrameworkCoreTokenStore)); + service.ImplementationType == typeof(OpenIddictEntityFrameworkCoreTokenStore)); } [Fact] @@ -90,12 +90,12 @@ private static IServiceCollection CreateServices() return services; } - public class CustomApplication : OpenIddictEntityFrameworkCoreApplication; - public class CustomAuthorization : OpenIddictEntityFrameworkCoreAuthorization; + public class CustomApplication : OpenIddictEntityFrameworkCoreApplication; + public class CustomAuthorization : OpenIddictEntityFrameworkCoreAuthorization; public class CustomResource : OpenIddictEntityFrameworkCoreResource; public class CustomScope : OpenIddictEntityFrameworkCoreScope; - public class CustomSession : OpenIddictEntityFrameworkCoreSession; - public class CustomToken : OpenIddictEntityFrameworkCoreToken; + public class CustomSession : OpenIddictEntityFrameworkCoreSession; + public class CustomToken : OpenIddictEntityFrameworkCoreToken; public class CustomDbContext : DbContext { diff --git a/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreHelpersTests.cs b/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreHelpersTests.cs index 2470f88fd..68df89d71 100644 --- a/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreHelpersTests.cs +++ b/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreHelpersTests.cs @@ -37,9 +37,9 @@ public void UseOpenIddict_RegistersDefaultEntityConfigurations() // Assert builder.Verify(mock => mock.ApplyConfiguration( - It.IsAny>()), Times.Once()); + It.IsAny>()), Times.Once()); builder.Verify(mock => mock.ApplyConfiguration( - It.IsAny>()), Times.Once()); + It.IsAny>()), Times.Once()); builder.Verify(mock => mock.ApplyConfiguration( It.IsAny>()), Times.Once()); builder.Verify(mock => mock.ApplyConfiguration( @@ -47,7 +47,7 @@ public void UseOpenIddict_RegistersDefaultEntityConfigurations() builder.Verify(mock => mock.ApplyConfiguration( It.IsAny>()), Times.Once()); builder.Verify(mock => mock.ApplyConfiguration( - It.IsAny>()), Times.Once()); + It.IsAny>()), Times.Once()); } [Fact] @@ -73,9 +73,9 @@ public void UseOpenIddict_RegistersDefaultEntityConfigurationsWithCustomKeyType( // Assert builder.Verify(mock => mock.ApplyConfiguration( - It.IsAny, OpenIddictEntityFrameworkCoreAuthorization, OpenIddictEntityFrameworkCoreToken, long>>()), Times.Once()); + It.IsAny, OpenIddictEntityFrameworkCoreAuthorization, OpenIddictEntityFrameworkCoreSession, OpenIddictEntityFrameworkCoreToken, long>>()), Times.Once()); builder.Verify(mock => mock.ApplyConfiguration( - It.IsAny, OpenIddictEntityFrameworkCoreApplication, OpenIddictEntityFrameworkCoreToken, long>>()), Times.Once()); + It.IsAny, OpenIddictEntityFrameworkCoreApplication, OpenIddictEntityFrameworkCoreSession, OpenIddictEntityFrameworkCoreToken, long>>()), Times.Once()); builder.Verify(mock => mock.ApplyConfiguration( It.IsAny, long>>()), Times.Once()); builder.Verify(mock => mock.ApplyConfiguration( @@ -83,7 +83,7 @@ public void UseOpenIddict_RegistersDefaultEntityConfigurationsWithCustomKeyType( builder.Verify(mock => mock.ApplyConfiguration( It.IsAny, OpenIddictEntityFrameworkCoreApplication, OpenIddictEntityFrameworkCoreAuthorization, OpenIddictEntityFrameworkCoreToken, long>>()), Times.Once()); builder.Verify(mock => mock.ApplyConfiguration( - It.IsAny, OpenIddictEntityFrameworkCoreApplication, OpenIddictEntityFrameworkCoreAuthorization, long>>()), Times.Once()); + It.IsAny, OpenIddictEntityFrameworkCoreApplication, OpenIddictEntityFrameworkCoreAuthorization, OpenIddictEntityFrameworkCoreSession, long>>()), Times.Once()); } [Fact] @@ -109,9 +109,9 @@ public void UseOpenIddict_RegistersCustomEntityConfigurations() // Assert builder.Verify(mock => mock.ApplyConfiguration( - It.IsAny>()), Times.Once()); + It.IsAny>()), Times.Once()); builder.Verify(mock => mock.ApplyConfiguration( - It.IsAny>()), Times.Once()); + It.IsAny>()), Times.Once()); builder.Verify(mock => mock.ApplyConfiguration( It.IsAny>()), Times.Once()); builder.Verify(mock => mock.ApplyConfiguration( @@ -119,13 +119,13 @@ public void UseOpenIddict_RegistersCustomEntityConfigurations() builder.Verify(mock => mock.ApplyConfiguration( It.IsAny>()), Times.Once()); builder.Verify(mock => mock.ApplyConfiguration( - It.IsAny>()), Times.Once()); + It.IsAny>()), Times.Once()); } - public class CustomApplication : OpenIddictEntityFrameworkCoreApplication; - public class CustomAuthorization : OpenIddictEntityFrameworkCoreAuthorization; + public class CustomApplication : OpenIddictEntityFrameworkCoreApplication; + public class CustomAuthorization : OpenIddictEntityFrameworkCoreAuthorization; public class CustomResource : OpenIddictEntityFrameworkCoreResource; public class CustomScope : OpenIddictEntityFrameworkCoreScope; - public class CustomSession : OpenIddictEntityFrameworkCoreSession; - public class CustomToken : OpenIddictEntityFrameworkCoreToken; + public class CustomSession : OpenIddictEntityFrameworkCoreSession; + public class CustomToken : OpenIddictEntityFrameworkCoreToken; } diff --git a/test/OpenIddict.Quartz.Tests/OpenIddictQuartzBuilderTests.cs b/test/OpenIddict.Quartz.Tests/OpenIddictQuartzBuilderTests.cs index e82d41104..05d4bfef7 100644 --- a/test/OpenIddict.Quartz.Tests/OpenIddictQuartzBuilderTests.cs +++ b/test/OpenIddict.Quartz.Tests/OpenIddictQuartzBuilderTests.cs @@ -63,6 +63,22 @@ public void DisableAuthorizationPruning_AuthorizationPruningIsDisabled() Assert.True(options.DisableAuthorizationPruning); } + [Fact] + public void DisableSessionPruning_SessionPruningIsDisabled() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.DisableSessionPruning(); + + var options = GetOptions(services); + + // Assert + Assert.True(options.DisableSessionPruning); + } + [Fact] public void DisableTokenPruning_TokenPruningIsDisabled() { @@ -137,6 +153,35 @@ public void SetMinimumAuthorizationLifespan_MinimumAuthorizationLifespanIsSet() Assert.Equal(42, options.MinimumAuthorizationLifespan.TotalDays); } + [Fact] + public void SetMinimumSessionLifespan_ThrowsAnExceptionForNegativeLifespan() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act and assert + var exception = Assert.Throws(() => builder.SetMinimumSessionLifespan(TimeSpan.FromSeconds(-1))); + + Assert.Equal("lifespan", exception.ParamName); + } + + [Fact] + public void SetMinimumSessionLifespan_MinimumSessionLifespanIsSet() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.SetMinimumSessionLifespan(TimeSpan.FromDays(42)); + + var options = GetOptions(services); + + // Assert + Assert.Equal(42, options.MinimumSessionLifespan.TotalDays); + } + [Fact] public void SetMinimumTokenLifespan_ThrowsAnExceptionForNegativeLifespan() { diff --git a/test/OpenIddict.Quartz.Tests/OpenIddictQuartzJobTests.cs b/test/OpenIddict.Quartz.Tests/OpenIddictQuartzJobTests.cs index 05798bdfd..da5bbae2b 100644 --- a/test/OpenIddict.Quartz.Tests/OpenIddictQuartzJobTests.cs +++ b/test/OpenIddict.Quartz.Tests/OpenIddictQuartzJobTests.cs @@ -23,6 +23,7 @@ public async Task Execute_UsesServiceScope() // Arrange var provider = Mock.Of(provider => provider.GetService(typeof(IOpenIddictAuthorizationManager)) == Mock.Of() && + provider.GetService(typeof(IOpenIddictSessionManager)) == Mock.Of() && provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of()); var scope = Mock.Of(scope => scope.ServiceProvider == provider); @@ -52,6 +53,7 @@ public async Task Execute_IgnoresPruningWhenTokenPruningIsDisabled() var provider = Mock.Of(provider => provider.GetService(typeof(IOpenIddictAuthorizationManager)) == Mock.Of() && + provider.GetService(typeof(IOpenIddictSessionManager)) == Mock.Of() && provider.GetService(typeof(IOpenIddictTokenManager)) == manager.Object); var job = CreateJob(provider, options => options.DisableTokenPruning = true); @@ -72,6 +74,7 @@ public async Task Execute_IgnoresPruningWhenAuthorizationPruningIsDisabled() var provider = Mock.Of(provider => provider.GetService(typeof(IOpenIddictAuthorizationManager)) == manager.Object && + provider.GetService(typeof(IOpenIddictSessionManager)) == Mock.Of() && provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of()); var job = CreateJob(provider, options => options.DisableAuthorizationPruning = true); @@ -83,12 +86,34 @@ public async Task Execute_IgnoresPruningWhenAuthorizationPruningIsDisabled() manager.Verify(manager => manager.PruneAsync(It.IsAny(), It.IsAny()), Times.Never()); } + [Fact] + public async Task Execute_IgnoresPruningWhenSessionPruningIsDisabled() + { + // Arrange + + var manager = new Mock(); + + var provider = Mock.Of(provider => + provider.GetService(typeof(IOpenIddictAuthorizationManager)) == Mock.Of() && + provider.GetService(typeof(IOpenIddictSessionManager)) == manager.Object && + provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of()); + + var job = CreateJob(provider, options => options.DisableSessionPruning = true); + + // Act + await job.Execute(Mock.Of()); + + // Assert + manager.Verify(manager => manager.PruneAsync(It.IsAny(), It.IsAny()), Times.Never()); + } + [Fact] public async Task Execute_UnschedulesTriggersWhenTokenManagerIsMissing() { // Arrange var provider = Mock.Of(provider => provider.GetService(typeof(IOpenIddictAuthorizationManager)) == Mock.Of() && + provider.GetService(typeof(IOpenIddictSessionManager)) == Mock.Of() && provider.GetService(typeof(IOpenIddictTokenManager)) == null); var job = CreateJob(provider); @@ -124,6 +149,26 @@ public async Task Execute_UnschedulesTriggersWhenAuthorizationManagerIsMissing() Assert.Equal(SR.GetResourceString(SR.ID0278), exception.InnerException!.Message); } + [Fact] + public async Task Execute_UnschedulesTriggersWhenSessionManagerIsMissing() + { + // Arrange + var provider = Mock.Of(provider => + provider.GetService(typeof(IOpenIddictSessionManager)) == null); + + var job = CreateJob(provider); + + // Act and assert + var exception = await Assert.ThrowsAsync(() => job.Execute(Mock.Of())); + + Assert.False(exception.RefireImmediately); + Assert.True(exception.UnscheduleAllTriggers); + Assert.True(exception.UnscheduleFiringTrigger); + + Assert.IsType(exception.InnerException); + Assert.Equal(SR.GetResourceString(SR.ID0278), exception.InnerException!.Message); + } + [Fact] public async Task Execute_RethrowsOutOfMemoryExceptionsThrownDuringTokenPruning() { @@ -160,6 +205,25 @@ public async Task Execute_RethrowsOutOfMemoryExceptionsThrownDuringAuthorization await Assert.ThrowsAsync(() => job.Execute(Mock.Of())); } + [Fact] + public async Task Execute_RethrowsOutOfMemoryExceptionsThrownDuringSessionPruning() + { + // Arrange + 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)) == Mock.Of() && + provider.GetService(typeof(IOpenIddictSessionManager)) == manager.Object); + + var job = CreateJob(provider); + + // Act and assert + await Assert.ThrowsAsync(() => job.Execute(Mock.Of())); + } + [Fact] public async Task Execute_DisablesRefiringWhenJobIsCanceledDuringTokenPruning() { @@ -212,6 +276,33 @@ public async Task Execute_DisablesRefiringWhenJobIsCanceledDuringAuthorizationPr manager.Verify(manager => manager.PruneAsync(It.IsAny(), It.IsAny()), Times.Once()); } + [Fact] + public async Task Execute_DisablesRefiringWhenJobIsCanceledDuringSessionPruning() + { + // Arrange + var token = new CancellationToken(canceled: true); + + 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)) == Mock.Of() && + provider.GetService(typeof(IOpenIddictSessionManager)) == manager.Object); + + var context = Mock.Of(context => context.CancellationToken == token); + + var job = CreateJob(provider); + + // Act and assert + var exception = await Assert.ThrowsAsync(() => job.Execute(context)); + + Assert.False(exception.RefireImmediately); + + manager.Verify(manager => manager.PruneAsync(It.IsAny(), It.IsAny()), Times.Once()); + } + [Fact] public async Task Execute_AllowsRefiringWhenExceptionsAreThrown() { @@ -220,6 +311,9 @@ public async Task Execute_AllowsRefiringWhenExceptionsAreThrown() provider.Setup(provider => provider.GetService(typeof(IOpenIddictAuthorizationManager))) .Returns(CreateAuthorizationManager(new ApplicationException())); + provider.Setup(provider => provider.GetService(typeof(IOpenIddictSessionManager))) + .Returns(CreateSessionManager(new ApplicationException())); + provider.Setup(provider => provider.GetService(typeof(IOpenIddictTokenManager))) .Returns(CreateTokenManager(new ApplicationException())); @@ -232,9 +326,10 @@ public async Task Execute_AllowsRefiringWhenExceptionsAreThrown() Assert.True(exception.RefireImmediately); Assert.IsType(exception.InnerException); - Assert.Equal(2, ((AggregateException) exception.InnerException!).InnerExceptions.Count); + Assert.Equal(3, ((AggregateException) exception.InnerException!).InnerExceptions.Count); Assert.IsType(((AggregateException) exception.InnerException!).InnerExceptions[0]); Assert.IsType(((AggregateException) exception.InnerException!).InnerExceptions[1]); + Assert.IsType(((AggregateException) exception.InnerException!).InnerExceptions[2]); static IOpenIddictAuthorizationManager CreateAuthorizationManager(Exception exception) { @@ -245,6 +340,15 @@ static IOpenIddictAuthorizationManager CreateAuthorizationManager(Exception exce return mock.Object; } + static IOpenIddictSessionManager CreateSessionManager(Exception exception) + { + var mock = new Mock(); + mock.Setup(manager => manager.PruneAsync(It.IsAny(), It.IsAny())) + .Throws(exception); + + return mock.Object; + } + static IOpenIddictTokenManager CreateTokenManager(Exception exception) { var mock = new Mock(); @@ -264,6 +368,10 @@ public async Task Execute_AllowsRefiringWhenAggregateExceptionsAreThrown() .Returns(CreateAuthorizationManager(new AggregateException( new InvalidOperationException(), new ApplicationException()))); + provider.Setup(provider => provider.GetService(typeof(IOpenIddictSessionManager))) + .Returns(CreateSessionManager(new AggregateException( + new InvalidOperationException(), new ApplicationException()))); + provider.Setup(provider => provider.GetService(typeof(IOpenIddictTokenManager))) .Returns(CreateTokenManager(new AggregateException( new InvalidOperationException(), new ApplicationException()))); @@ -277,11 +385,13 @@ public async Task Execute_AllowsRefiringWhenAggregateExceptionsAreThrown() Assert.True(exception.RefireImmediately); Assert.IsType(exception.InnerException); - Assert.Equal(4, ((AggregateException) exception.InnerException!).InnerExceptions.Count); + Assert.Equal(6, ((AggregateException) exception.InnerException!).InnerExceptions.Count); Assert.IsType(((AggregateException) exception.InnerException!).InnerExceptions[0]); Assert.IsType(((AggregateException) exception.InnerException!).InnerExceptions[1]); Assert.IsType(((AggregateException) exception.InnerException!).InnerExceptions[2]); Assert.IsType(((AggregateException) exception.InnerException!).InnerExceptions[3]); + Assert.IsType(((AggregateException) exception.InnerException!).InnerExceptions[4]); + Assert.IsType(((AggregateException) exception.InnerException!).InnerExceptions[5]); static IOpenIddictAuthorizationManager CreateAuthorizationManager(Exception exception) { @@ -292,6 +402,15 @@ static IOpenIddictAuthorizationManager CreateAuthorizationManager(Exception exce return mock.Object; } + static IOpenIddictSessionManager CreateSessionManager(Exception exception) + { + var mock = new Mock(); + mock.Setup(manager => manager.PruneAsync(It.IsAny(), It.IsAny())) + .Throws(exception); + + return mock.Object; + } + static IOpenIddictTokenManager CreateTokenManager(Exception exception) { var mock = new Mock(); @@ -312,6 +431,7 @@ public async Task Execute_DisallowsRefiringWhenMaximumRefireCountIsReached() var provider = Mock.Of(provider => provider.GetService(typeof(IOpenIddictAuthorizationManager)) == manager.Object && + provider.GetService(typeof(IOpenIddictSessionManager)) == Mock.Of() && provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of()); var context = Mock.Of(context => context.RefireCount == 5); diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs index 327acbbcd..c03e75245 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs @@ -4757,1161 +4757,6 @@ public async Task HandleTokenRequest_RequestIsRejectedWhenActorTokenIsInvalid() Mock.Get(manager).Verify(manager => manager.HasStatusAsync(tokens[1], Statuses.Valid, It.IsAny()), Times.Once()); } - [Fact] - public async Task HandleTokenRequest_AuthorizationAssociatedWithCodeIsIgnoredWhenAuthorizationStorageIsDisabled() - { - // Arrange - var manager = CreateAuthorizationManager(mock => - { - mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(new OpenIddictAuthorization()); - }); - - await using var server = await CreateServerAsync(options => - { - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal([TokenTypeIdentifiers.Private.AuthorizationCode], context.ValidTokenTypes); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeIdentifiers.Private.AuthorizationCode) - .SetPresenters("Fabrikam") - .SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56") - .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") - .SetClaim(Claims.Subject, "Bob le Bricoleur"); - - return ValueTask.CompletedTask; - }); - - builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); - }); - - options.AddEventHandler(builder => - builder.UseInlineHandler(context => - { - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetClaim(Claims.Subject, "Bob le Magnifique"); - - return ValueTask.CompletedTask; - })); - - options.Services.AddSingleton(CreateApplicationManager(mock => - { - var application = new OpenIddictApplication(); - - mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.GetSettingsAsync(application, It.IsAny())) - .ReturnsAsync(ImmutableDictionary.Create(StringComparer.Ordinal)); - })); - - options.Services.AddSingleton(CreateTokenManager(mock => - { - var token = new OpenIddictToken(); - - mock.Setup(manager => manager.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) - .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); - - mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) - .ReturnsAsync(TokenTypeIdentifiers.Private.AuthorizationCode); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) - .ReturnsAsync(false); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.GetAuthorizationIdAsync(token, It.IsAny())) - .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - - mock.Setup(manager => manager.TryRedeemAsync(token, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.CreateAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(new OpenIddictToken()); - })); - - options.Services.AddSingleton(manager); - - options.DisableAuthorizationStorage(); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/token", new OpenIddictRequest - { - ClientId = "Fabrikam", - Code = "SplxlOBeZQQYbYS6WxSbIA", - GrantType = GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.NotNull(response.AccessToken); - - Mock.Get(manager).Verify(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Never()); - } - - [Fact] - public async Task HandleTokenRequest_AuthorizationAssociatedWithRefreshTokenIsIgnoredWhenAuthorizationStorageIsDisabled() - { - // Arrange - var authorization = new OpenIddictAuthorization(); - - var manager = CreateAuthorizationManager(mock => - { - mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(new OpenIddictAuthorization()); - }); - - await using var server = await CreateServerAsync(options => - { - options.DisableRollingRefreshTokens(); - - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeIdentifiers.RefreshToken) - .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") - .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") - .SetClaim(Claims.Subject, "Bob le Bricoleur"); - - return ValueTask.CompletedTask; - }); - - builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); - }); - - options.AddEventHandler(builder => - builder.UseInlineHandler(context => - { - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetClaim(Claims.Subject, "Bob le Magnifique"); - - return ValueTask.CompletedTask; - })); - - options.Services.AddSingleton(CreateTokenManager(mock => - { - var token = new OpenIddictToken(); - - mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) - .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); - - mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) - .ReturnsAsync(TokenTypeIdentifiers.RefreshToken); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) - .ReturnsAsync(false); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.GetAuthorizationIdAsync(token, It.IsAny())) - .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - - mock.Setup(manager => manager.CreateAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(new OpenIddictToken()); - })); - - options.Services.AddSingleton(manager); - - options.DisableAuthorizationStorage(); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/token", new OpenIddictRequest - { - GrantType = GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.NotNull(response.AccessToken); - - Mock.Get(manager).Verify(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Never()); - } - - [Fact] - public async Task HandleTokenRequest_AuthorizationAssociatedWithSubjectTokenIsIgnoredWhenAuthorizationStorageIsDisabled() - { - // Arrange - var authorization = new OpenIddictAuthorization(); - - var manager = CreateAuthorizationManager(mock => - { - mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(new OpenIddictAuthorization()); - }); - - await using var server = await CreateServerAsync(options => - { - options.DisableRollingRefreshTokens(); - - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeIdentifiers.RefreshToken) - .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") - .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") - .SetClaim(Claims.Subject, "Bob le Bricoleur"); - - return ValueTask.CompletedTask; - }); - - builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); - }); - - options.Services.AddSingleton(CreateTokenManager(mock => - { - var token = new OpenIddictToken(); - - mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) - .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); - - mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) - .ReturnsAsync(TokenTypeIdentifiers.RefreshToken); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) - .ReturnsAsync(false); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.GetAuthorizationIdAsync(token, It.IsAny())) - .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - - mock.Setup(manager => manager.CreateAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(new OpenIddictToken()); - })); - - options.Services.AddSingleton(manager); - - options.DisableAuthorizationStorage(); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/token", new OpenIddictRequest - { - GrantType = GrantTypes.TokenExchange, - SubjectToken = "8xLOxBtZp8", - SubjectTokenType = TokenTypeIdentifiers.RefreshToken - }); - - // Assert - Assert.NotNull(response.AccessToken); - - Mock.Get(manager).Verify(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Never()); - } - - [Fact] - public async Task HandleTokenRequest_AuthorizationAssociatedWithActorTokenIsIgnoredWhenAuthorizationStorageIsDisabled() - { - // Arrange - var authorization = new OpenIddictAuthorization(); - - var manager = CreateAuthorizationManager(mock => - { - mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(new OpenIddictAuthorization()); - }); - - await using var server = await CreateServerAsync(options => - { - options.DisableRollingRefreshTokens(); - - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - if (string.Equals(context.Token, "accVkjcJyb4BWCxGsndESCJQbdFMogUC5PbRDqceLTC", StringComparison.Ordinal)) - { - Assert.Equal([TokenTypeIdentifiers.AccessToken], context.ValidTokenTypes); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeIdentifiers.AccessToken) - .SetTokenId("E2894547-277E-4E09-A0C0-0A0631B54052") - .SetClaim(Claims.Subject, "Bob le Bricoleur"); - - return ValueTask.CompletedTask; - } - - Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeIdentifiers.RefreshToken) - .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") - .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") - .SetClaim(Claims.Subject, "Bob le Bricoleur"); - - return ValueTask.CompletedTask; - }); - - builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); - }); - - options.Services.AddSingleton(CreateTokenManager(mock => - { - ImmutableArray tokens = [new(), new()]; - - mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(tokens[0]); - - mock.Setup(manager => manager.GetIdAsync(tokens[0], It.IsAny())) - .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); - - mock.Setup(manager => manager.GetTypeAsync(tokens[0], It.IsAny())) - .ReturnsAsync(TokenTypeIdentifiers.RefreshToken); - - mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Redeemed, It.IsAny())) - .ReturnsAsync(false); - - mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.GetAuthorizationIdAsync(tokens[0], It.IsAny())) - .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - - mock.Setup(manager => manager.FindByIdAsync("E2894547-277E-4E09-A0C0-0A0631B54052", It.IsAny())) - .ReturnsAsync(tokens[1]); - - mock.Setup(manager => manager.GetIdAsync(tokens[1], It.IsAny())) - .ReturnsAsync("E2894547-277E-4E09-A0C0-0A0631B54052"); - - mock.Setup(manager => manager.GetTypeAsync(tokens[1], It.IsAny())) - .ReturnsAsync(TokenTypeIdentifiers.AccessToken); - - mock.Setup(manager => manager.HasStatusAsync(tokens[1], Statuses.Redeemed, It.IsAny())) - .ReturnsAsync(false); - - mock.Setup(manager => manager.HasStatusAsync(tokens[1], Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.GetAuthorizationIdAsync(tokens[1], It.IsAny())) - .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - - mock.Setup(manager => manager.CreateAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(new OpenIddictToken()); - })); - - options.Services.AddSingleton(manager); - - options.DisableAuthorizationStorage(); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/token", new OpenIddictRequest - { - ActorToken = "accVkjcJyb4BWCxGsndESCJQbdFMogUC5PbRDqceLTC", - ActorTokenType = TokenTypeIdentifiers.AccessToken, - GrantType = GrantTypes.TokenExchange, - SubjectToken = "8xLOxBtZp8", - SubjectTokenType = TokenTypeIdentifiers.RefreshToken - }); - - // Assert - Assert.NotNull(response.AccessToken); - - Mock.Get(manager).Verify(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Never()); - } - - [Fact] - public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationAssociatedWithAuthorizationCodeCannotBeFound() - { - // Arrange - var manager = CreateAuthorizationManager(mock => - { - mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(value: null); - }); - - await using var server = await CreateServerAsync(options => - { - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal([TokenTypeIdentifiers.Private.AuthorizationCode], context.ValidTokenTypes); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeIdentifiers.Private.AuthorizationCode) - .SetPresenters("Fabrikam") - .SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56") - .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") - .SetClaim(Claims.Subject, "Bob le Bricoleur"); - - return ValueTask.CompletedTask; - }); - - builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); - }); - - options.AddEventHandler(builder => - builder.UseInlineHandler(context => - { - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetClaim(Claims.Subject, "Bob le Magnifique"); - - return ValueTask.CompletedTask; - })); - - options.Services.AddSingleton(CreateApplicationManager(mock => - { - var application = new OpenIddictApplication(); - - mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny())) - .ReturnsAsync(true); - })); - - options.Services.AddSingleton(CreateTokenManager(mock => - { - var token = new OpenIddictToken(); - - mock.Setup(manager => manager.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) - .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); - - mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) - .ReturnsAsync(TokenTypeIdentifiers.Private.AuthorizationCode); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) - .ReturnsAsync(false); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.GetAuthorizationIdAsync(token, It.IsAny())) - .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - })); - - options.Services.AddSingleton(manager); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/token", new OpenIddictRequest - { - ClientId = "Fabrikam", - Code = "SplxlOBeZQQYbYS6WxSbIA", - GrantType = GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.Equal(Errors.InvalidGrant, response.Error); - Assert.Equal(SR.GetResourceString(SR.ID2020), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2020), response.ErrorUri); - - Mock.Get(manager).Verify(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationAssociatedWithAuthorizationCodeIsInvalid() - { - // Arrange - var authorization = new OpenIddictAuthorization(); - - var manager = CreateAuthorizationManager(mock => - { - mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(authorization); - - mock.Setup(manager => manager.HasStatusAsync(authorization, Statuses.Valid, It.IsAny())) - .ReturnsAsync(false); - }); - - await using var server = await CreateServerAsync(options => - { - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal([TokenTypeIdentifiers.Private.AuthorizationCode], context.ValidTokenTypes); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeIdentifiers.Private.AuthorizationCode) - .SetPresenters("Fabrikam") - .SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56") - .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") - .SetClaim(Claims.Subject, "Bob le Bricoleur"); - - return ValueTask.CompletedTask; - }); - - builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); - }); - - options.AddEventHandler(builder => - builder.UseInlineHandler(context => - { - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetClaim(Claims.Subject, "Bob le Magnifique"); - - return ValueTask.CompletedTask; - })); - - options.Services.AddSingleton(CreateApplicationManager(mock => - { - var application = new OpenIddictApplication(); - - mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny())) - .ReturnsAsync(true); - })); - - options.Services.AddSingleton(CreateTokenManager(mock => - { - var token = new OpenIddictToken(); - - mock.Setup(manager => manager.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) - .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); - - mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) - .ReturnsAsync(TokenTypeIdentifiers.Private.AuthorizationCode); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) - .ReturnsAsync(false); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.GetAuthorizationIdAsync(token, It.IsAny())) - .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - })); - - options.Services.AddSingleton(manager); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/token", new OpenIddictRequest - { - ClientId = "Fabrikam", - Code = "SplxlOBeZQQYbYS6WxSbIA", - GrantType = GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.Equal(Errors.InvalidGrant, response.Error); - Assert.Equal(SR.GetResourceString(SR.ID2020), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2020), response.ErrorUri); - - Mock.Get(manager).Verify(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(manager => manager.HasStatusAsync(authorization, Statuses.Valid, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationAssociatedWithRefreshTokenCannotBeFound() - { - // Arrange - var manager = CreateAuthorizationManager(mock => - { - mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(value: null); - }); - - await using var server = await CreateServerAsync(options => - { - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeIdentifiers.RefreshToken) - .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") - .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") - .SetClaim(Claims.Subject, "Bob le Bricoleur"); - - return ValueTask.CompletedTask; - }); - - builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); - }); - - options.AddEventHandler(builder => - builder.UseInlineHandler(context => - { - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetClaim(Claims.Subject, "Bob le Magnifique"); - - return ValueTask.CompletedTask; - })); - - options.Services.AddSingleton(CreateTokenManager(mock => - { - var token = new OpenIddictToken(); - - mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) - .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); - - mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) - .ReturnsAsync(TokenTypeIdentifiers.RefreshToken); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) - .ReturnsAsync(false); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.GetAuthorizationIdAsync(token, It.IsAny())) - .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - })); - - options.Services.AddSingleton(manager); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/token", new OpenIddictRequest - { - GrantType = GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.Equal(Errors.InvalidGrant, response.Error); - Assert.Equal(SR.GetResourceString(SR.ID2022), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2022), response.ErrorUri); - - Mock.Get(manager).Verify(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationAssociatedWithRefreshTokenIsInvalid() - { - // Arrange - var authorization = new OpenIddictAuthorization(); - - var manager = CreateAuthorizationManager(mock => - { - mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(authorization); - - mock.Setup(manager => manager.HasStatusAsync(authorization, Statuses.Valid, It.IsAny())) - .ReturnsAsync(false); - }); - - await using var server = await CreateServerAsync(options => - { - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeIdentifiers.RefreshToken) - .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") - .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") - .SetClaim(Claims.Subject, "Bob le Bricoleur"); - - return ValueTask.CompletedTask; - }); - - builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); - }); - - options.AddEventHandler(builder => - builder.UseInlineHandler(context => - { - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetClaim(Claims.Subject, "Bob le Magnifique"); - - return ValueTask.CompletedTask; - })); - - options.Services.AddSingleton(CreateTokenManager(mock => - { - var token = new OpenIddictToken(); - - mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) - .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); - - mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) - .ReturnsAsync(TokenTypeIdentifiers.RefreshToken); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) - .ReturnsAsync(false); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.GetAuthorizationIdAsync(token, It.IsAny())) - .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - })); - - options.Services.AddSingleton(manager); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/token", new OpenIddictRequest - { - GrantType = GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.Equal(Errors.InvalidGrant, response.Error); - Assert.Equal(SR.GetResourceString(SR.ID2022), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2022), response.ErrorUri); - - Mock.Get(manager).Verify(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(manager => manager.HasStatusAsync(authorization, Statuses.Valid, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationAssociatedWithSubjectTokenCannotBeFound() - { - // Arrange - var manager = CreateAuthorizationManager(mock => - { - mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(value: null); - }); - - await using var server = await CreateServerAsync(options => - { - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeIdentifiers.RefreshToken) - .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") - .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") - .SetClaim(Claims.Subject, "Bob le Bricoleur"); - - return ValueTask.CompletedTask; - }); - - builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); - }); - - options.Services.AddSingleton(CreateTokenManager(mock => - { - var token = new OpenIddictToken(); - - mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) - .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); - - mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) - .ReturnsAsync(TokenTypeIdentifiers.RefreshToken); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) - .ReturnsAsync(false); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.GetAuthorizationIdAsync(token, It.IsAny())) - .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - })); - - options.Services.AddSingleton(manager); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/token", new OpenIddictRequest - { - GrantType = GrantTypes.TokenExchange, - SubjectToken = "8xLOxBtZp8", - SubjectTokenType = TokenTypeIdentifiers.RefreshToken - }); - - // Assert - Assert.Equal(Errors.InvalidGrant, response.Error); - Assert.Equal(SR.GetResourceString(SR.ID2022), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2022), response.ErrorUri); - - Mock.Get(manager).Verify(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationAssociatedWithSubjectTokenIsInvalid() - { - // Arrange - var authorization = new OpenIddictAuthorization(); - - var manager = CreateAuthorizationManager(mock => - { - mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(authorization); - - mock.Setup(manager => manager.HasStatusAsync(authorization, Statuses.Valid, It.IsAny())) - .ReturnsAsync(false); - }); - - await using var server = await CreateServerAsync(options => - { - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeIdentifiers.RefreshToken) - .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") - .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") - .SetClaim(Claims.Subject, "Bob le Bricoleur"); - - return ValueTask.CompletedTask; - }); - - builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); - }); - - options.Services.AddSingleton(CreateTokenManager(mock => - { - var token = new OpenIddictToken(); - - mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) - .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); - - mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) - .ReturnsAsync(TokenTypeIdentifiers.RefreshToken); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) - .ReturnsAsync(false); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.GetAuthorizationIdAsync(token, It.IsAny())) - .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - })); - - options.Services.AddSingleton(manager); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/token", new OpenIddictRequest - { - ActorToken = "accVkjcJyb4BWCxGsndESCJQbdFMogUC5PbRDqceLTC", - ActorTokenType = TokenTypeIdentifiers.AccessToken, - GrantType = GrantTypes.TokenExchange, - SubjectToken = "8xLOxBtZp8", - SubjectTokenType = TokenTypeIdentifiers.RefreshToken - }); - - // Assert - Assert.Equal(Errors.InvalidGrant, response.Error); - Assert.Equal(SR.GetResourceString(SR.ID2022), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2022), response.ErrorUri); - - Mock.Get(manager).Verify(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(manager => manager.HasStatusAsync(authorization, Statuses.Valid, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationAssociatedWithActorTokenCannotBeFound() - { - // Arrange - var authorization = new OpenIddictAuthorization(); - - var manager = CreateAuthorizationManager(mock => - { - mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(authorization); - - mock.Setup(manager => manager.HasStatusAsync(authorization, Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.FindByIdAsync("224E3451-A3D4-48C3-A189-2D95B97C212A", It.IsAny())) - .ReturnsAsync(value: null); - }); - - await using var server = await CreateServerAsync(options => - { - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - if (string.Equals(context.Token, "accVkjcJyb4BWCxGsndESCJQbdFMogUC5PbRDqceLTC", StringComparison.Ordinal)) - { - Assert.Equal([TokenTypeIdentifiers.AccessToken], context.ValidTokenTypes); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeIdentifiers.AccessToken) - .SetTokenId("E2894547-277E-4E09-A0C0-0A0631B54052") - .SetAuthorizationId("224E3451-A3D4-48C3-A189-2D95B97C212A") - .SetClaim(Claims.Subject, "Bob le Bricoleur"); - - return ValueTask.CompletedTask; - } - - Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeIdentifiers.RefreshToken) - .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") - .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") - .SetClaim(Claims.Subject, "Bob le Bricoleur"); - - return ValueTask.CompletedTask; - }); - - builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); - }); - - options.Services.AddSingleton(CreateTokenManager(mock => - { - ImmutableArray tokens = [new(), new()]; - - mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(tokens[0]); - - mock.Setup(manager => manager.GetIdAsync(tokens[0], It.IsAny())) - .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); - - mock.Setup(manager => manager.GetTypeAsync(tokens[0], It.IsAny())) - .ReturnsAsync(TokenTypeIdentifiers.RefreshToken); - - mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Redeemed, It.IsAny())) - .ReturnsAsync(false); - - mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.GetAuthorizationIdAsync(tokens[0], It.IsAny())) - .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - - mock.Setup(manager => manager.FindByIdAsync("E2894547-277E-4E09-A0C0-0A0631B54052", It.IsAny())) - .ReturnsAsync(tokens[1]); - - mock.Setup(manager => manager.GetIdAsync(tokens[1], It.IsAny())) - .ReturnsAsync("E2894547-277E-4E09-A0C0-0A0631B54052"); - - mock.Setup(manager => manager.GetTypeAsync(tokens[1], It.IsAny())) - .ReturnsAsync(TokenTypeIdentifiers.AccessToken); - - mock.Setup(manager => manager.HasStatusAsync(tokens[1], Statuses.Redeemed, It.IsAny())) - .ReturnsAsync(false); - - mock.Setup(manager => manager.HasStatusAsync(tokens[1], Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.GetAuthorizationIdAsync(tokens[1], It.IsAny())) - .ReturnsAsync("224E3451-A3D4-48C3-A189-2D95B97C212A"); - })); - - options.Services.AddSingleton(manager); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/token", new OpenIddictRequest - { - ActorToken = "accVkjcJyb4BWCxGsndESCJQbdFMogUC5PbRDqceLTC", - ActorTokenType = TokenTypeIdentifiers.AccessToken, - GrantType = GrantTypes.TokenExchange, - SubjectToken = "8xLOxBtZp8", - SubjectTokenType = TokenTypeIdentifiers.RefreshToken - }); - - // Assert - Assert.Equal(Errors.InvalidGrant, response.Error); - Assert.Equal(SR.GetResourceString(SR.ID2023), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2023), response.ErrorUri); - - Mock.Get(manager).Verify(manager => manager.FindByIdAsync("224E3451-A3D4-48C3-A189-2D95B97C212A", It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationAssociatedWithActorTokenIsInvalid() - { - // Arrange - ImmutableArray authorizations = [new(), new()]; - - var manager = CreateAuthorizationManager(mock => - { - mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(authorizations[0]); - - mock.Setup(manager => manager.HasStatusAsync(authorizations[0], Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.FindByIdAsync("224E3451-A3D4-48C3-A189-2D95B97C212A", It.IsAny())) - .ReturnsAsync(authorizations[1]); - - mock.Setup(manager => manager.HasStatusAsync(authorizations[1], Statuses.Valid, It.IsAny())) - .ReturnsAsync(false); - }); - - await using var server = await CreateServerAsync(options => - { - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - if (string.Equals(context.Token, "accVkjcJyb4BWCxGsndESCJQbdFMogUC5PbRDqceLTC", StringComparison.Ordinal)) - { - Assert.Equal([TokenTypeIdentifiers.AccessToken], context.ValidTokenTypes); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeIdentifiers.AccessToken) - .SetTokenId("E2894547-277E-4E09-A0C0-0A0631B54052") - .SetAuthorizationId("224E3451-A3D4-48C3-A189-2D95B97C212A") - .SetClaim(Claims.Subject, "Bob le Bricoleur"); - - return ValueTask.CompletedTask; - } - - Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeIdentifiers.RefreshToken) - .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") - .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") - .SetClaim(Claims.Subject, "Bob le Bricoleur"); - - return ValueTask.CompletedTask; - }); - - builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); - }); - - options.Services.AddSingleton(CreateTokenManager(mock => - { - ImmutableArray tokens = [new(), new()]; - - mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(tokens[0]); - - mock.Setup(manager => manager.GetIdAsync(tokens[0], It.IsAny())) - .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); - - mock.Setup(manager => manager.GetTypeAsync(tokens[0], It.IsAny())) - .ReturnsAsync(TokenTypeIdentifiers.RefreshToken); - - mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Redeemed, It.IsAny())) - .ReturnsAsync(false); - - mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.GetAuthorizationIdAsync(tokens[0], It.IsAny())) - .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - - mock.Setup(manager => manager.FindByIdAsync("E2894547-277E-4E09-A0C0-0A0631B54052", It.IsAny())) - .ReturnsAsync(tokens[1]); - - mock.Setup(manager => manager.GetIdAsync(tokens[1], It.IsAny())) - .ReturnsAsync("E2894547-277E-4E09-A0C0-0A0631B54052"); - - mock.Setup(manager => manager.GetTypeAsync(tokens[1], It.IsAny())) - .ReturnsAsync(TokenTypeIdentifiers.AccessToken); - - mock.Setup(manager => manager.HasStatusAsync(tokens[1], Statuses.Redeemed, It.IsAny())) - .ReturnsAsync(false); - - mock.Setup(manager => manager.HasStatusAsync(tokens[1], Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.GetAuthorizationIdAsync(tokens[1], It.IsAny())) - .ReturnsAsync("224E3451-A3D4-48C3-A189-2D95B97C212A"); - })); - - options.Services.AddSingleton(manager); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/token", new OpenIddictRequest - { - ActorToken = "accVkjcJyb4BWCxGsndESCJQbdFMogUC5PbRDqceLTC", - ActorTokenType = TokenTypeIdentifiers.AccessToken, - GrantType = GrantTypes.TokenExchange, - SubjectToken = "8xLOxBtZp8", - SubjectTokenType = TokenTypeIdentifiers.RefreshToken - }); - - // Assert - Assert.Equal(Errors.InvalidGrant, response.Error); - Assert.Equal(SR.GetResourceString(SR.ID2023), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2023), response.ErrorUri); - - Mock.Get(manager).Verify(manager => manager.FindByIdAsync("224E3451-A3D4-48C3-A189-2D95B97C212A", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(manager => manager.HasStatusAsync(authorizations[1], Statuses.Valid, It.IsAny()), Times.Once()); - } - [Fact] public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationCodeCannotBeRedeemed() { diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Protection.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Protection.cs index 9c5aa557b..4fa9b1847 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Protection.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Protection.cs @@ -630,4 +630,320 @@ public async Task ValidateToken_InvalidTokenTypeThrowsAnException() // Assert Assert.Equal(SR.FormatID0005(TokenTypeIdentifiers.Private.AuthorizationCode, TokenTypeIdentifiers.AccessToken), exception.Message); } + + [Fact] + public async Task ValidateToken_RequestIsRejectedWhenAuthorizationAssociatedWithTokenCannotBeFound() + { + // Arrange + var manager = CreateAuthorizationManager(mock => + { + mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) + .ReturnsAsync(value: null); + }); + + await using var server = await CreateServerAsync(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetTokenType(TokenTypeIdentifiers.RefreshToken) + .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return ValueTask.CompletedTask; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.Services.AddSingleton(CreateTokenManager(mock => + { + var token = new OpenIddictToken(); + + mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(token); + + mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); + + mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) + .ReturnsAsync(TokenTypeIdentifiers.RefreshToken); + + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) + .ReturnsAsync(false); + + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) + .ReturnsAsync(true); + + mock.Setup(manager => manager.GetAuthorizationIdAsync(token, It.IsAny())) + .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + })); + + options.Services.AddSingleton(manager); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.TokenExchange, + SubjectToken = "8xLOxBtZp8", + SubjectTokenType = TokenTypeIdentifiers.RefreshToken + }); + + // Assert + Assert.Equal(Errors.InvalidGrant, response.Error); + Assert.Equal(SR.GetResourceString(SR.ID2022), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2022), response.ErrorUri); + + Mock.Get(manager).Verify(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ValidateToken_RequestIsRejectedWhenAuthorizationAssociatedWithTokenIsInvalid() + { + // Arrange + var authorization = new OpenIddictAuthorization(); + + var manager = CreateAuthorizationManager(mock => + { + mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) + .ReturnsAsync(authorization); + + mock.Setup(manager => manager.HasStatusAsync(authorization, Statuses.Valid, It.IsAny())) + .ReturnsAsync(false); + }); + + await using var server = await CreateServerAsync(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetTokenType(TokenTypeIdentifiers.RefreshToken) + .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return ValueTask.CompletedTask; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.Services.AddSingleton(CreateTokenManager(mock => + { + var token = new OpenIddictToken(); + + mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(token); + + mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); + + mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) + .ReturnsAsync(TokenTypeIdentifiers.RefreshToken); + + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) + .ReturnsAsync(false); + + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) + .ReturnsAsync(true); + + mock.Setup(manager => manager.GetAuthorizationIdAsync(token, It.IsAny())) + .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + })); + + options.Services.AddSingleton(manager); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ActorToken = "accVkjcJyb4BWCxGsndESCJQbdFMogUC5PbRDqceLTC", + ActorTokenType = TokenTypeIdentifiers.AccessToken, + GrantType = GrantTypes.TokenExchange, + SubjectToken = "8xLOxBtZp8", + SubjectTokenType = TokenTypeIdentifiers.RefreshToken + }); + + // Assert + Assert.Equal(Errors.InvalidGrant, response.Error); + Assert.Equal(SR.GetResourceString(SR.ID2022), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2022), response.ErrorUri); + + Mock.Get(manager).Verify(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(manager => manager.HasStatusAsync(authorization, Statuses.Valid, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ValidateToken_RequestIsRejectedWhenSessionAssociatedWithTokenCannotBeFound() + { + // Arrange + var manager = CreateSessionManager(mock => + { + mock.Setup(manager => manager.FindByIdAsync("DE7F0AF0-9595-4546-BE3D-F6BB43FB5FA5", It.IsAny())) + .ReturnsAsync(value: null); + }); + + await using var server = await CreateServerAsync(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetTokenType(TokenTypeIdentifiers.RefreshToken) + .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return ValueTask.CompletedTask; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.Services.AddSingleton(CreateTokenManager(mock => + { + var token = new OpenIddictToken(); + + mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(token); + + mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); + + mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) + .ReturnsAsync(TokenTypeIdentifiers.RefreshToken); + + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) + .ReturnsAsync(false); + + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) + .ReturnsAsync(true); + + mock.Setup(manager => manager.GetSessionIdAsync(token, It.IsAny())) + .ReturnsAsync("DE7F0AF0-9595-4546-BE3D-F6BB43FB5FA5"); + })); + + options.Services.AddSingleton(manager); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.TokenExchange, + SubjectToken = "8xLOxBtZp8", + SubjectTokenType = TokenTypeIdentifiers.RefreshToken + }); + + // Assert + Assert.Equal(Errors.InvalidGrant, response.Error); + Assert.Equal(SR.GetResourceString(SR.ID2210), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2210), response.ErrorUri); + + Mock.Get(manager).Verify(manager => manager.FindByIdAsync("DE7F0AF0-9595-4546-BE3D-F6BB43FB5FA5", It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ValidateToken_RequestIsRejectedWhenSessionAssociatedWithTokenIsInvalid() + { + // Arrange + var session = new OpenIddictSession(); + + var manager = CreateSessionManager(mock => + { + mock.Setup(manager => manager.FindByIdAsync("DE7F0AF0-9595-4546-BE3D-F6BB43FB5FA5", It.IsAny())) + .ReturnsAsync(session); + + mock.Setup(manager => manager.HasStatusAsync(session, Statuses.Valid, It.IsAny())) + .ReturnsAsync(false); + }); + + await using var server = await CreateServerAsync(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetTokenType(TokenTypeIdentifiers.RefreshToken) + .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetSessionId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return ValueTask.CompletedTask; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.Services.AddSingleton(CreateTokenManager(mock => + { + var token = new OpenIddictToken(); + + mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(token); + + mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); + + mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) + .ReturnsAsync(TokenTypeIdentifiers.RefreshToken); + + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) + .ReturnsAsync(false); + + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) + .ReturnsAsync(true); + + mock.Setup(manager => manager.GetSessionIdAsync(token, It.IsAny())) + .ReturnsAsync("DE7F0AF0-9595-4546-BE3D-F6BB43FB5FA5"); + })); + + options.Services.AddSingleton(manager); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ActorToken = "accVkjcJyb4BWCxGsndESCJQbdFMogUC5PbRDqceLTC", + ActorTokenType = TokenTypeIdentifiers.AccessToken, + GrantType = GrantTypes.TokenExchange, + SubjectToken = "8xLOxBtZp8", + SubjectTokenType = TokenTypeIdentifiers.RefreshToken + }); + + // Assert + Assert.Equal(Errors.InvalidGrant, response.Error); + Assert.Equal(SR.GetResourceString(SR.ID2210), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2210), response.ErrorUri); + + Mock.Get(manager).Verify(manager => manager.FindByIdAsync("DE7F0AF0-9595-4546-BE3D-F6BB43FB5FA5", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(manager => manager.HasStatusAsync(session, Statuses.Valid, It.IsAny()), Times.Once()); + } } diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs index 4ade1b675..fd3569a45 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs @@ -5203,12 +5203,14 @@ protected virtual void ConfigureServices(IServiceCollection services) .SetDefaultAuthorizationEntity() .SetDefaultResourceEntity() .SetDefaultScopeEntity() + .SetDefaultSessionEntity() .SetDefaultTokenEntity(); options.Services.AddSingleton(CreateApplicationManager()) .AddSingleton(CreateAuthorizationManager()) .AddSingleton(CreateResourceManager()) .AddSingleton(CreateScopeManager()) + .AddSingleton(CreateSessionManager()) .AddSingleton(CreateTokenManager()); }) @@ -5343,6 +5345,20 @@ protected OpenIddictScopeManager CreateScopeManager( return manager.Object; } + protected OpenIddictSessionManager CreateSessionManager( + Action>>? configuration = null) + { + var manager = new Mock>( + Mock.Of>(), + OutputHelper.ToLogger>(), + Mock.Of>(), + Mock.Of>()); + + configuration?.Invoke(manager); + + return manager.Object; + } + protected OpenIddictTokenManager CreateTokenManager( Action>>? configuration = null) { @@ -5361,5 +5377,6 @@ public class OpenIddictApplication; public class OpenIddictAuthorization; public class OpenIddictResource; public class OpenIddictScope; + public class OpenIddictSession; public class OpenIddictToken; } diff --git a/test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTests.cs b/test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTests.cs index 5ef0cafdb..6c3ccad75 100644 --- a/test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTests.cs +++ b/test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTests.cs @@ -385,9 +385,11 @@ protected virtual void ConfigureServices(IServiceCollection services) .AddCore(options => { options.SetDefaultAuthorizationEntity() + .SetDefaultSessionEntity() .SetDefaultTokenEntity(); options.Services.AddSingleton(CreateAuthorizationManager()) + .AddSingleton(CreateSessionManager()) .AddSingleton(CreateTokenManager()); }) @@ -435,6 +437,20 @@ protected OpenIddictAuthorizationManager CreateAuthoriz return manager.Object; } + protected OpenIddictSessionManager CreateSessionManager( + Action>>? configuration = null) + { + var manager = new Mock>( + Mock.Of>(), + OutputHelper.ToLogger>(), + Mock.Of>(), + Mock.Of>()); + + configuration?.Invoke(manager); + + return manager.Object; + } + protected OpenIddictTokenManager CreateTokenManager( Action>>? configuration = null) { @@ -450,5 +466,6 @@ protected OpenIddictTokenManager CreateTokenManager( } public class OpenIddictAuthorization; + public class OpenIddictSession; public class OpenIddictToken; } diff --git a/test/OpenIddict.Validation.Tests/OpenIddictValidationConfigurationTests.cs b/test/OpenIddict.Validation.Tests/OpenIddictValidationConfigurationTests.cs index 22c80c828..c5e28b7f4 100644 --- a/test/OpenIddict.Validation.Tests/OpenIddictValidationConfigurationTests.cs +++ b/test/OpenIddict.Validation.Tests/OpenIddictValidationConfigurationTests.cs @@ -308,7 +308,7 @@ public void Validate_ReturnsAnErrorWhenIntrospectionClientCredentialsAreMissing( } [Fact] - public void Validate_ReturnsAnErrorWhenAuthorizationOrTokenEntryValidationIsEnabledInIntrospectionMode() + public void Validate_ReturnsAnErrorWhenAuthorizationEntryValidationIsEnabledInIntrospectionMode() { // Arrange var configuration = new OpenIddictValidationConfiguration(new ServiceCollection().BuildServiceProvider()); @@ -320,6 +320,49 @@ public void Validate_ReturnsAnErrorWhenAuthorizationOrTokenEntryValidationIsEnab options.ClientId = "client_id"; options.ClientSecret = "client_secret"; options.EnableAuthorizationEntryValidation = true; + options.ConfigurationManager = new StaticConfigurationManager(new OpenIddictConfiguration()); + + // Act + var result = configuration.Validate(name: null, options); + + // Assert + Assert.Contains(SR.GetResourceString(SR.ID0133), result.Failures!, StringComparer.Ordinal); + } + + [Fact] + public void Validate_ReturnsAnErrorWhenSessionEntryValidationIsEnabledInIntrospectionMode() + { + // Arrange + var configuration = new OpenIddictValidationConfiguration(new ServiceCollection().BuildServiceProvider()); + var options = CreateBaseOptions(); + + options.ValidationType = OpenIddictValidationType.Introspection; + options.Issuer = new Uri("https://www.contoso.com/"); + options.ConfigurationEndpoint = new Uri("https://www.contoso.com/.well-known/openid-configuration"); + options.ClientId = "client_id"; + options.ClientSecret = "client_secret"; + options.EnableSessionEntryValidation = true; + options.ConfigurationManager = new StaticConfigurationManager(new OpenIddictConfiguration()); + + // Act + var result = configuration.Validate(name: null, options); + + // Assert + Assert.Contains(SR.GetResourceString(SR.ID0133), result.Failures!, StringComparer.Ordinal); + } + + [Fact] + public void Validate_ReturnsAnErrorWhenTokenEntryValidationIsEnabledInIntrospectionMode() + { + // Arrange + var configuration = new OpenIddictValidationConfiguration(new ServiceCollection().BuildServiceProvider()); + var options = CreateBaseOptions(); + + options.ValidationType = OpenIddictValidationType.Introspection; + options.Issuer = new Uri("https://www.contoso.com/"); + options.ConfigurationEndpoint = new Uri("https://www.contoso.com/.well-known/openid-configuration"); + options.ClientId = "client_id"; + options.ClientSecret = "client_secret"; options.EnableTokenEntryValidation = true; options.ConfigurationManager = new StaticConfigurationManager(new OpenIddictConfiguration()); @@ -328,7 +371,6 @@ public void Validate_ReturnsAnErrorWhenAuthorizationOrTokenEntryValidationIsEnab // Assert Assert.Contains(SR.GetResourceString(SR.ID0133), result.Failures!, StringComparer.Ordinal); - Assert.Contains(SR.GetResourceString(SR.ID0134), result.Failures!, StringComparer.Ordinal); } [Fact]