diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs b/sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs index c65ff544..fb33f6a9 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs +++ b/sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs @@ -204,14 +204,16 @@ public class AuthorizationController : Controller { 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 class AuthorizationController : Controller { 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 c0347261..ad190685 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs @@ -233,14 +233,16 @@ public class AuthorizationController : Controller { 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 class AuthorizationController : Controller { 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/src/OpenIddict.Abstractions/Caches/IOpenIddictSessionCache.cs b/src/OpenIddict.Abstractions/Caches/IOpenIddictSessionCache.cs index a121458e..95ae7f12 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/Managers/IOpenIddictSessionManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictSessionManager.cs index 6613d3a0..33eba6b0 100644 --- a/src/OpenIddict.Abstractions/Managers/IOpenIddictSessionManager.cs +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictSessionManager.cs @@ -95,7 +95,7 @@ public interface IOpenIddictSessionManager /// 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); /// @@ -313,6 +313,18 @@ public interface IOpenIddictSessionManager /// 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/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index 02703dba..00c09edd 100644 --- a/src/OpenIddict.Abstractions/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx @@ -855,15 +855,9 @@ 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. diff --git a/src/OpenIddict.Abstractions/Stores/IOpenIddictSessionStore.cs b/src/OpenIddict.Abstractions/Stores/IOpenIddictSessionStore.cs index e1af87ef..7d15bbdc 100644 --- a/src/OpenIddict.Abstractions/Stores/IOpenIddictSessionStore.cs +++ b/src/OpenIddict.Abstractions/Stores/IOpenIddictSessionStore.cs @@ -64,7 +64,7 @@ public interface IOpenIddictSessionStore 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. @@ -245,6 +245,18 @@ public interface IOpenIddictSessionStore where TSession : class 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.Core/Caches/OpenIddictSessionCache.cs b/src/OpenIddict.Core/Caches/OpenIddictSessionCache.cs index 0ea33997..04b0a56f 100644 --- a/src/OpenIddict.Core/Caches/OpenIddictSessionCache.cs +++ b/src/OpenIddict.Core/Caches/OpenIddictSessionCache.cs @@ -96,7 +96,7 @@ public sealed class OpenIddictSessionCache : IOpenIddictSessionCache 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/OpenIddictSessionManager.cs b/src/OpenIddict.Core/Managers/OpenIddictSessionManager.cs index 819f1987..bbd910a8 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictSessionManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictSessionManager.cs @@ -217,7 +217,7 @@ public class OpenIddictSessionManager : IOpenIddictSessionManager wher /// 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 @@ -699,6 +699,19 @@ public class OpenIddictSessionManager : IOpenIddictSessionManager wher } } + /// + /// 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 +835,7 @@ public class OpenIddictSessionManager : IOpenIddictSessionManager wher => 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); /// @@ -905,6 +918,10 @@ public class OpenIddictSessionManager : IOpenIddictSessionManager wher 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.EntityFramework.Models/OpenIddictEntityFrameworkApplication.cs b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkApplication.cs index a2447606..74a1985f 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 93b415d1..192f36ea 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 4457d008..85421e54 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 6e9ab164..35dcabb7 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 8f8acd63..174c8956 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 sealed class 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 fcdb5cb9..4e653bf7 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 sealed class 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 0acaf704..0a80916b 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 sealed class 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 d2b0d735..8c23d5d5 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 a37c5ba5..473895f9 100644 --- a/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkBuilder.cs +++ b/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkBuilder.cs @@ -71,12 +71,12 @@ public sealed class OpenIddictEntityFrameworkBuilder [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 sealed class OpenIddictEntityFrameworkBuilder 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 sealed class OpenIddictEntityFrameworkBuilder 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 ef81333d..6f117839 100644 --- a/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkHelpers.cs +++ b/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkHelpers.cs @@ -48,23 +48,23 @@ public static class OpenIddictEntityFrameworkHelpers [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 b402788e..05b81627 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 class 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 class OpenIddictEntityFrameworkApplicationStore< 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 @@ public class OpenIddictEntityFrameworkApplicationStore< // 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 @@ public class OpenIddictEntityFrameworkApplicationStore< 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 @@ public class OpenIddictEntityFrameworkApplicationStore< 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 @@ public class OpenIddictEntityFrameworkApplicationStore< } } + 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 class OpenIddictEntityFrameworkApplicationStore< 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 80037a72..bb86d213 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 class 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 class OpenIddictEntityFrameworkAuthorizationStore< 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 @@ public class OpenIddictEntityFrameworkAuthorizationStore< 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 @@ public class OpenIddictEntityFrameworkAuthorizationStore< if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0243), exceptions); + throw new AggregateException(exceptions); } return result; @@ -605,7 +655,7 @@ public class OpenIddictEntityFrameworkAuthorizationStore< 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 class OpenIddictEntityFrameworkAuthorizationStore< if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + throw new AggregateException(exceptions); } return result; @@ -651,7 +701,7 @@ public class OpenIddictEntityFrameworkAuthorizationStore< 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 class OpenIddictEntityFrameworkAuthorizationStore< if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + throw new AggregateException(exceptions); } return result; @@ -696,7 +746,7 @@ public class OpenIddictEntityFrameworkAuthorizationStore< 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 class OpenIddictEntityFrameworkAuthorizationStore< if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + throw new AggregateException(exceptions); } return result; @@ -885,7 +935,7 @@ public class OpenIddictEntityFrameworkAuthorizationStore< 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 bc7d0c66..e4a8cc1a 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkResourceStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkResourceStore.cs @@ -119,7 +119,7 @@ public class OpenIddictEntityFrameworkResourceStore< 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 class OpenIddictEntityFrameworkResourceStore< 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 fe002dda..21f83168 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkScopeStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkScopeStore.cs @@ -119,7 +119,7 @@ public class OpenIddictEntityFrameworkScopeStore< 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 class OpenIddictEntityFrameworkScopeStore< 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 bacfe81d..e29494b6 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 class OpenIddictEntityFrameworkSessionStore< 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 class OpenIddictEntityFrameworkSessionStore< 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 class OpenIddictEntityFrameworkSessionStore< 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 @@ public class OpenIddictEntityFrameworkSessionStore< } } + /// + 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) { @@ -661,7 +765,7 @@ public class OpenIddictEntityFrameworkSessionStore< 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 class OpenIddictEntityFrameworkSessionStore< 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 a2a5c90b..12149931 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 class 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 class OpenIddictEntityFrameworkTokenStore< 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); @@ -591,7 +595,7 @@ public class OpenIddictEntityFrameworkTokenStore< if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + throw new AggregateException(exceptions); } return result; @@ -643,7 +647,7 @@ public class OpenIddictEntityFrameworkTokenStore< 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 +661,7 @@ public class OpenIddictEntityFrameworkTokenStore< if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + throw new AggregateException(exceptions); } return result; @@ -691,7 +695,7 @@ public class OpenIddictEntityFrameworkTokenStore< 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 +709,7 @@ public class OpenIddictEntityFrameworkTokenStore< if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + throw new AggregateException(exceptions); } return result; @@ -739,7 +743,7 @@ public class OpenIddictEntityFrameworkTokenStore< 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 +757,7 @@ public class OpenIddictEntityFrameworkTokenStore< if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + throw new AggregateException(exceptions); } return result; @@ -786,7 +790,7 @@ public class OpenIddictEntityFrameworkTokenStore< 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 +804,7 @@ public class OpenIddictEntityFrameworkTokenStore< if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + throw new AggregateException(exceptions); } return result; @@ -1012,7 +1016,7 @@ public class OpenIddictEntityFrameworkTokenStore< 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 5c3b985e..eb65067f 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 ae84951b..af309ce8 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 6e89959f..799605e5 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 5bf3b1f5..f6692ab1 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 c6b9520e..e02adf44 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 sealed class OpenIddictEntityFrameworkCoreApplicationConfiguration< 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 9f05ae69..a273f784 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 sealed class OpenIddictEntityFrameworkCoreAuthorizationConfiguration< 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 39697fbe..7c462994 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 sealed class OpenIddictEntityFrameworkCoreSessionConfiguration< 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 03a88ebe..8be38719 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 4bd58a9c..f61fff2b 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs @@ -93,12 +93,12 @@ public sealed class OpenIddictEntityFrameworkCoreBuilder [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 sealed class OpenIddictEntityFrameworkCoreBuilder 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 sealed class OpenIddictEntityFrameworkCoreBuilder 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 45f77e77..dc78913c 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 00a4ac2a..69eaaefb 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs @@ -102,12 +102,12 @@ public static class OpenIddictEntityFrameworkCoreHelpers [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 class OpenIddictEntityFrameworkCoreHelpers [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 class OpenIddictEntityFrameworkCoreHelpers [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 f346446d..fb2f1a24 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 class 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 @@ public class OpenIddictEntityFrameworkCoreApplicationStore< 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 @@ public class OpenIddictEntityFrameworkCoreApplicationStore< 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 @@ public class OpenIddictEntityFrameworkCoreApplicationStore< 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 @@ public class OpenIddictEntityFrameworkCoreApplicationStore< 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 @@ public class OpenIddictEntityFrameworkCoreApplicationStore< } } + 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 class OpenIddictEntityFrameworkCoreApplicationStore< 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 22543e13..dd6fd1c5 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 class 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 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore< 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 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore< 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 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore< 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 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore< if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0243), exceptions); + throw new AggregateException(exceptions); } return result; @@ -655,7 +687,7 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore< 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 class OpenIddictEntityFrameworkCoreAuthorizationStore< if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + throw new AggregateException(exceptions); } return result; @@ -714,7 +746,7 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore< 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 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore< if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + throw new AggregateException(exceptions); } return result; @@ -772,7 +804,7 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore< 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 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore< if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + throw new AggregateException(exceptions); } return result; @@ -915,7 +947,7 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore< 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 a579fef5..56050e1e 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreResourceStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreResourceStore.cs @@ -124,7 +124,7 @@ public class OpenIddictEntityFrameworkCoreResourceStore< 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 class OpenIddictEntityFrameworkCoreResourceStore< 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 56be363b..6353a227 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreScopeStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreScopeStore.cs @@ -124,7 +124,7 @@ public class OpenIddictEntityFrameworkCoreScopeStore< 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 class OpenIddictEntityFrameworkCoreScopeStore< 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 c4777cb9..5e1b5617 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 class OpenIddictEntityFrameworkCoreSessionStore< 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 class OpenIddictEntityFrameworkCoreSessionStore< 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 class OpenIddictEntityFrameworkCoreSessionStore< /// 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 class OpenIddictEntityFrameworkCoreSessionStore< 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 @@ public class OpenIddictEntityFrameworkCoreSessionStore< } } + /// + 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) { @@ -607,7 +750,7 @@ public class OpenIddictEntityFrameworkCoreSessionStore< 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 class OpenIddictEntityFrameworkCoreSessionStore< 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 4e748d33..78a302e5 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 class 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 class OpenIddictEntityFrameworkCoreTokenStore< 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); @@ -611,7 +616,7 @@ public class OpenIddictEntityFrameworkCoreTokenStore< if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + throw new AggregateException(exceptions); } return result; @@ -672,7 +677,7 @@ public class OpenIddictEntityFrameworkCoreTokenStore< 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 +691,7 @@ public class OpenIddictEntityFrameworkCoreTokenStore< if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + throw new AggregateException(exceptions); } return result; @@ -734,7 +739,7 @@ public class OpenIddictEntityFrameworkCoreTokenStore< 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 +753,7 @@ public class OpenIddictEntityFrameworkCoreTokenStore< if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + throw new AggregateException(exceptions); } return result; @@ -796,7 +801,7 @@ public class OpenIddictEntityFrameworkCoreTokenStore< 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 +815,7 @@ public class OpenIddictEntityFrameworkCoreTokenStore< if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + throw new AggregateException(exceptions); } return result; @@ -857,7 +862,7 @@ public class OpenIddictEntityFrameworkCoreTokenStore< 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 +876,7 @@ public class OpenIddictEntityFrameworkCoreTokenStore< if (exceptions is { Count: > 0 }) { - throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + throw new AggregateException(exceptions); } return result; @@ -1058,7 +1063,7 @@ public class OpenIddictEntityFrameworkCoreTokenStore< 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 4e1f8027..ca21d80d 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 d993a3c1..87146007 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbApplicationStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbApplicationStore.cs @@ -109,6 +109,10 @@ public class OpenIddictMongoDbApplicationStore< 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 bea5a66b..0225e790 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbAuthorizationStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbAuthorizationStore.cs @@ -103,6 +103,10 @@ public class OpenIddictMongoDbAuthorizationStore< 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 557f6882..850b9841 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbSessionStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbSessionStore.cs @@ -102,11 +102,15 @@ public class OpenIddictMongoDbSessionStore< { 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 class OpenIddictMongoDbSessionStore< 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 @@ public class OpenIddictMongoDbSessionStore< } } + /// + 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 40ef1484..1b4e341b 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs @@ -423,7 +423,7 @@ public class OpenIddictMongoDbTokenStore< 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 diff --git a/src/OpenIddict.Quartz/OpenIddictQuartzBuilder.cs b/src/OpenIddict.Quartz/OpenIddictQuartzBuilder.cs index 4f78eb21..c11a86f9 100644 --- a/src/OpenIddict.Quartz/OpenIddictQuartzBuilder.cs +++ b/src/OpenIddict.Quartz/OpenIddictQuartzBuilder.cs @@ -62,6 +62,13 @@ public sealed class OpenIddictQuartzBuilder 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 sealed class OpenIddictQuartzBuilder 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 f22264be..4b4277fb 100644 --- a/src/OpenIddict.Quartz/OpenIddictQuartzJob.cs +++ b/src/OpenIddict.Quartz/OpenIddictQuartzJob.cs @@ -146,6 +146,54 @@ public sealed class OpenIddictQuartzJob : IJob } } + // 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 1a77ba31..57539a24 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/OpenIddictServerHandlers.Authentication.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs index 26738742..6310535d 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs @@ -10,7 +10,6 @@ using System.Security.Claims; 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 a3481f33..a4c9a322 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs @@ -9,7 +9,6 @@ using System.Diagnostics; 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 d2387136..3a1568c3 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs @@ -13,7 +13,6 @@ using System.Security.Cryptography; 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 b82adb43..c44f42ab 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs @@ -16,7 +16,6 @@ using System.Text; 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.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index 4cd5fc28..43a2cc20 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -18,7 +18,6 @@ using System.Text.Json; 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/test/OpenIddict.EntityFramework.Tests/OpenIddictEntityFrameworkBuilderTests.cs b/test/OpenIddict.EntityFramework.Tests/OpenIddictEntityFrameworkBuilderTests.cs index 051f04d3..131d23ad 100644 --- a/test/OpenIddict.EntityFramework.Tests/OpenIddictEntityFrameworkBuilderTests.cs +++ b/test/OpenIddict.EntityFramework.Tests/OpenIddictEntityFrameworkBuilderTests.cs @@ -39,11 +39,11 @@ public class OpenIddictEntityFrameworkBuilderTests 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 @@ public class OpenIddictEntityFrameworkBuilderTests 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 @@ public class OpenIddictEntityFrameworkBuilderTests 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 496d31c5..137b165e 100644 --- a/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreBuilderTests.cs +++ b/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreBuilderTests.cs @@ -39,11 +39,11 @@ public class OpenIddictEntityFrameworkCoreBuilderTests 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 @@ public class OpenIddictEntityFrameworkCoreBuilderTests 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 @@ public class OpenIddictEntityFrameworkCoreBuilderTests 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 2470f88f..68df89d7 100644 --- a/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreHelpersTests.cs +++ b/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreHelpersTests.cs @@ -37,9 +37,9 @@ public class OpenIddictEntityFrameworkCoreHelpersTests // 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 class OpenIddictEntityFrameworkCoreHelpersTests 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 class OpenIddictEntityFrameworkCoreHelpersTests // 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 class OpenIddictEntityFrameworkCoreHelpersTests 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 class OpenIddictEntityFrameworkCoreHelpersTests // 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 class OpenIddictEntityFrameworkCoreHelpersTests 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 e82d4110..05d4bfef 100644 --- a/test/OpenIddict.Quartz.Tests/OpenIddictQuartzBuilderTests.cs +++ b/test/OpenIddict.Quartz.Tests/OpenIddictQuartzBuilderTests.cs @@ -63,6 +63,22 @@ public class OpenIddictQuartzBuilderTests 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 class OpenIddictQuartzBuilderTests 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 05798bdf..da5bbae2 100644 --- a/test/OpenIddict.Quartz.Tests/OpenIddictQuartzJobTests.cs +++ b/test/OpenIddict.Quartz.Tests/OpenIddictQuartzJobTests.cs @@ -23,6 +23,7 @@ public class OpenIddictQuartzJobTests // 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 class OpenIddictQuartzJobTests 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 class OpenIddictQuartzJobTests 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 class OpenIddictQuartzJobTests 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 class OpenIddictQuartzJobTests 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 class OpenIddictQuartzJobTests 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 class OpenIddictQuartzJobTests 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 class OpenIddictQuartzJobTests 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 class OpenIddictQuartzJobTests 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 @@ public class OpenIddictQuartzJobTests 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 class OpenIddictQuartzJobTests .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 class OpenIddictQuartzJobTests 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 @@ public class OpenIddictQuartzJobTests 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 class OpenIddictQuartzJobTests 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);