From 4f6882bc0a2e7af65f9924ee1da422ba763fc9c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Wed, 29 Jul 2026 17:16:30 +0200 Subject: [PATCH] Introduce a new session entity and store the session identifier in the tokens --- .../App_Start/IdentityConfig.cs | 118 ++- .../Controllers/AuthorizationController.cs | 123 ++- .../Models/IdentityModels.cs | 21 +- .../Startup.cs | 5 +- .../Controllers/AuthorizationController.cs | 136 ++- .../Program.cs | 16 + .../Services/UserClaimsPrincipalFactory.cs | 36 + .../Caches/IOpenIddictAuthorizationCache.cs | 12 +- .../Caches/IOpenIddictSessionCache.cs | 82 ++ .../Caches/IOpenIddictTokenCache.cs | 16 +- .../OpenIddictApplicationDescriptor.cs | 36 +- .../OpenIddictAuthorizationDescriptor.cs | 22 +- .../OpenIddictResourceDescriptor.cs | 12 +- .../Descriptors/OpenIddictScopeDescriptor.cs | 14 +- .../OpenIddictSessionDescriptor.cs | 53 + .../Descriptors/OpenIddictTokenDescriptor.cs | 32 +- .../Managers/IOpenIddictApplicationManager.cs | 26 +- .../IOpenIddictAuthorizationManager.cs | 59 +- .../Managers/IOpenIddictResourceManager.cs | 16 +- .../Managers/IOpenIddictScopeManager.cs | 16 +- .../Managers/IOpenIddictSessionManager.cs | 346 +++++++ .../Managers/IOpenIddictTokenManager.cs | 33 +- .../OpenIddictConstants.cs | 2 + .../OpenIddictResources.resx | 10 +- .../Primitives/OpenIddictExtensions.cs | 34 + .../Stores/IOpenIddictApplicationStore.cs | 10 +- .../Stores/IOpenIddictAuthorizationStore.cs | 21 +- .../Stores/IOpenIddictResourceStore.cs | 10 +- .../Stores/IOpenIddictScopeStore.cs | 10 +- .../Stores/IOpenIddictSessionStore.cs | 319 ++++++ .../Stores/IOpenIddictTokenStore.cs | 26 +- ...OpenIddictClientDataProtectionFormatter.cs | 2 +- .../IOpenIddictClientHandlerFilter.cs | 2 +- .../OpenIddictClientHandlers.cs | 6 +- .../Caches/OpenIddictApplicationCache.cs | 2 +- .../Caches/OpenIddictAuthorizationCache.cs | 9 +- .../Caches/OpenIddictResourceCache.cs | 2 +- .../Caches/OpenIddictScopeCache.cs | 2 +- .../Caches/OpenIddictSessionCache.cs | 383 ++++++++ .../Caches/OpenIddictTokenCache.cs | 8 +- .../Managers/OpenIddictApplicationManager.cs | 28 +- .../OpenIddictAuthorizationManager.cs | 105 +- .../Managers/OpenIddictResourceManager.cs | 16 +- .../Managers/OpenIddictScopeManager.cs | 16 +- .../Managers/OpenIddictSessionManager.cs | 919 ++++++++++++++++++ .../Managers/OpenIddictTokenManager.cs | 50 +- src/OpenIddict.Core/OpenIddictCoreBuilder.cs | 93 ++ .../OpenIddictCoreExtensions.cs | 4 + .../OpenIddictEntityFrameworkApplication.cs | 5 +- .../OpenIddictEntityFrameworkAuthorization.cs | 5 +- .../OpenIddictEntityFrameworkResource.cs | 3 +- .../OpenIddictEntityFrameworkScope.cs | 3 +- .../OpenIddictEntityFrameworkSession.cs | 77 ++ .../OpenIddictEntityFrameworkToken.cs | 7 +- ...dictEntityFrameworkSessionConfiguration.cs | 74 ++ .../OpenIddictEntityFrameworkBuilder.cs | 9 +- .../OpenIddictEntityFrameworkExtensions.cs | 2 + .../OpenIddictEntityFrameworkHelpers.cs | 8 +- ...IddictEntityFrameworkAuthorizationStore.cs | 51 +- .../OpenIddictEntityFrameworkSessionStore.cs | 706 ++++++++++++++ .../OpenIddictEntityFrameworkTokenStore.cs | 61 +- ...penIddictEntityFrameworkCoreApplication.cs | 10 +- ...nIddictEntityFrameworkCoreAuthorization.cs | 10 +- .../OpenIddictEntityFrameworkCoreResource.cs | 3 +- .../OpenIddictEntityFrameworkCoreScope.cs | 3 +- .../OpenIddictEntityFrameworkCoreSession.cs | 85 ++ .../OpenIddictEntityFrameworkCoreToken.cs | 14 +- ...EntityFrameworkCoreSessionConfiguration.cs | 85 ++ .../OpenIddictEntityFrameworkCoreBuilder.cs | 7 + ...OpenIddictEntityFrameworkCoreCustomizer.cs | 4 +- ...OpenIddictEntityFrameworkCoreExtensions.cs | 2 + .../OpenIddictEntityFrameworkCoreHelpers.cs | 21 +- ...ctEntityFrameworkCoreAuthorizationStore.cs | 54 +- ...nIddictEntityFrameworkCoreResourceStore.cs | 6 +- ...OpenIddictEntityFrameworkCoreScopeStore.cs | 6 +- ...enIddictEntityFrameworkCoreSessionStore.cs | 668 +++++++++++++ ...OpenIddictEntityFrameworkCoreTokenStore.cs | 72 +- .../OpenIddictMongoDbAuthorization.cs | 2 +- .../OpenIddictMongoDbSession.cs | 70 ++ .../OpenIddictMongoDbToken.cs | 2 +- .../OpenIddictMongoDbBuilder.cs | 29 + .../OpenIddictMongoDbExtensions.cs | 2 + .../OpenIddictMongoDbOptions.cs | 5 + .../OpenIddictMongoDbAuthorizationStore.cs | 41 +- .../Stores/OpenIddictMongoDbSessionStore.cs | 499 ++++++++++ .../Stores/OpenIddictMongoDbTokenStore.cs | 44 +- ...OpenIddictServerDataProtectionConstants.cs | 1 + ...OpenIddictServerDataProtectionFormatter.cs | 5 +- .../IOpenIddictServerHandlerFilter.cs | 2 +- .../OpenIddictServerHandlers.cs | 17 +- ...IddictValidationDataProtectionConstants.cs | 1 + ...IddictValidationDataProtectionFormatter.cs | 1 + .../IOpenIddictValidationHandlerFilter.cs | 2 +- .../Primitives/OpenIddictExtensionsTests.cs | 148 +++ .../Caches/OpenIddictSessionCacheTests.cs | 549 +++++++++++ .../OpenIddictAuthorizationManagerTests.cs | 20 +- .../Managers/OpenIddictSessionManagerTests.cs | 861 ++++++++++++++++ .../OpenIddictCoreBuilderTests.cs | 88 ++ .../OpenIddictCoreExtensionsTests.cs | 20 + .../OpenIddictEntityFrameworkBuilderTests.cs | 7 +- ...enIddictEntityFrameworkCoreBuilderTests.cs | 7 +- ...enIddictEntityFrameworkCoreHelpersTests.cs | 15 +- .../OpenIddictMongoDbBuilderTests.cs | 50 + ...dictServerDataProtectionFormatterTests.cs} | 0 104 files changed, 7193 insertions(+), 675 deletions(-) create mode 100644 sandbox/OpenIddict.Sandbox.AspNetCore.Server/Services/UserClaimsPrincipalFactory.cs create mode 100644 src/OpenIddict.Abstractions/Caches/IOpenIddictSessionCache.cs create mode 100644 src/OpenIddict.Abstractions/Descriptors/OpenIddictSessionDescriptor.cs create mode 100644 src/OpenIddict.Abstractions/Managers/IOpenIddictSessionManager.cs create mode 100644 src/OpenIddict.Abstractions/Stores/IOpenIddictSessionStore.cs create mode 100644 src/OpenIddict.Core/Caches/OpenIddictSessionCache.cs create mode 100644 src/OpenIddict.Core/Managers/OpenIddictSessionManager.cs create mode 100644 src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkSession.cs create mode 100644 src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkSessionConfiguration.cs create mode 100644 src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkSessionStore.cs create mode 100644 src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreSession.cs create mode 100644 src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreSessionConfiguration.cs create mode 100644 src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreSessionStore.cs create mode 100644 src/OpenIddict.MongoDb.Models/OpenIddictMongoDbSession.cs create mode 100644 src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbSessionStore.cs create mode 100644 test/OpenIddict.Core.Tests/Caches/OpenIddictSessionCacheTests.cs create mode 100644 test/OpenIddict.Core.Tests/Managers/OpenIddictSessionManagerTests.cs rename test/OpenIddict.Server.DataProtection.Tests/{OpenIddictServerBuilderTests.cs => OpenIddictServerDataProtectionFormatterTests.cs} (100%) diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Server/App_Start/IdentityConfig.cs b/sandbox/OpenIddict.Sandbox.AspNet.Server/App_Start/IdentityConfig.cs index 7fdf2fc6..c6c63b17 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Server/App_Start/IdentityConfig.cs +++ b/sandbox/OpenIddict.Sandbox.AspNet.Server/App_Start/IdentityConfig.cs @@ -1,4 +1,6 @@ using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNet.Identity; @@ -6,29 +8,22 @@ using Microsoft.AspNet.Identity.EntityFramework; using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin; using Microsoft.Owin.Security; +using Microsoft.Owin.Security.Cookies; +using OpenIddict.Abstractions; using OpenIddict.Sandbox.AspNet.Server.Models; namespace OpenIddict.Sandbox.AspNet.Server; public class EmailService : IIdentityMessageService { - public Task SendAsync(IdentityMessage message) - { - // Connectez votre service e-mail ici pour envoyer un e-mail. - return Task.FromResult(0); - } + public Task SendAsync(IdentityMessage message) => Task.CompletedTask; } public class SmsService : IIdentityMessageService { - public Task SendAsync(IdentityMessage message) - { - // Connectez votre service SMS ici pour envoyer un message texte. - return Task.FromResult(0); - } + public Task SendAsync(IdentityMessage message) => Task.CompletedTask; } -// Configurer l'application que le gestionnaire des utilisateurs a utilisée dans cette application. UserManager est défini dans ASP.NET Identity et est utilisé par l'application. public class ApplicationUserManager : UserManager { public ApplicationUserManager(IUserStore store) @@ -39,14 +34,13 @@ public class ApplicationUserManager : UserManager public static ApplicationUserManager Create(IdentityFactoryOptions options, IOwinContext context) { var manager = new ApplicationUserManager(new UserStore(context.Get())); - // Configurer la logique de validation pour les noms d'utilisateur + manager.UserValidator = new UserValidator(manager) { AllowOnlyAlphanumericUserNames = false, RequireUniqueEmail = true }; - // Configurer la logique de validation pour les mots de passe manager.PasswordValidator = new PasswordValidator { RequiredLength = 6, @@ -56,13 +50,10 @@ public class ApplicationUserManager : UserManager RequireUppercase = true, }; - // Configurer les valeurs par défaut du verrouillage de l'utilisateur manager.UserLockoutEnabledByDefault = true; manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5); manager.MaxFailedAccessAttemptsBeforeLockout = 5; - // Inscrire les fournisseurs d'authentification à 2 facteurs. Cette application utilise le téléphone et l'e-mail comme procédure de réception d'un code de vérification de l'utilisateur - // Vous pouvez écrire votre propre fournisseur et le connecter ici. manager.RegisterTwoFactorProvider("Code téléphonique ", new PhoneNumberTokenProvider { MessageFormat = "Votre code de sécurité est {0}" @@ -84,7 +75,6 @@ public class ApplicationUserManager : UserManager } } -// Configurer le gestionnaire de connexion d'application qui est utilisé dans cette application. public class ApplicationSignInManager : SignInManager { public ApplicationSignInManager(ApplicationUserManager userManager, IAuthenticationManager authenticationManager) @@ -101,4 +91,98 @@ public class ApplicationSignInManager : SignInManager { return new ApplicationSignInManager(context.GetUserManager(), context.Authentication); } + + public static async Task OnValidateIdentity(CookieValidateIdentityContext context) + { + // Note: the logic implemented here is equivalent to the default security stamp validation logic used + // by ASP.NET Identity but allows overriding the login identifier claim to ensure it is preserved + // when the identity is regenerated and the new application cookie is returned to the user agent. + // + // Unlike the default implementation, this method also uses a time-constant comparison + // to prevent leaking information about the security stamp value through timing attacks. + + if ((context.Options.SystemClock.UtcNow - context.Properties.IssuedUtc) < TimeSpan.FromMinutes(30)) + { + return; + } + + var manager = context.OwinContext.GetUserManager() + ?? throw new InvalidOperationException("The user manager cannot be resolved from the context."); + + if (!manager.SupportsUserSecurityStamp) + { + throw new InvalidOperationException("The user manager does not support security stamp-based validation."); + } + + if (string.IsNullOrEmpty(context.Identity.GetUserId())) + { + throw new InvalidOperationException("The user ID cannot be resolved from the user identity."); + } + + if (string.IsNullOrEmpty(context.Identity.FindFirstValue(Constants.DefaultSecurityStampClaimType))) + { + throw new InvalidOperationException("The security stamp cannot be resolved from the user identity."); + } + + var user = await manager.FindByIdAsync(context.Identity.GetUserId()); + if (user is null || await manager.GetSecurityStampAsync(user.Id) is not { Length: > 0 } value) + { + context.RejectIdentity(); + context.OwinContext.Authentication.SignOut(context.Options.AuthenticationType); + return; + } + + if (!FixedTimeEquals( + left : MemoryMarshal.AsBytes(value), + right: MemoryMarshal.AsBytes(context.Identity.FindFirstValue(Constants.DefaultSecurityStampClaimType)))) + { + context.RejectIdentity(); + context.OwinContext.Authentication.SignOut(context.Options.AuthenticationType); + return; + } + + if (await user.GenerateUserIdentityAsync(manager) is not ClaimsIdentity identity) + { + throw new InvalidOperationException("The user identity cannot be generated for the specified user."); + } + + var identifier = context.Identity.GetClaim("login_id"); + if (!string.IsNullOrEmpty(identifier)) + { + identity.SetClaim("login_id", identifier); + } + + context.Properties.IssuedUtc = null; + context.Properties.ExpiresUtc = null; + context.OwinContext.Authentication.SignIn(context.Properties, identity); + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + static bool FixedTimeEquals(ReadOnlySpan left, ReadOnlySpan right) + { + // Note: the logic used here is directly taken from the official implementation of + // the CryptographicOperations.FixedTimeEquals() method introduced in .NET Core 2.1. + // + // See https://github.com/dotnet/corefx/pull/27103 for more information. + + // Note: these null checks can be theoretically considered as early checks + // (which would defeat the purpose of a time-constant comparison method), + // but the expected string length is the only information an attacker + // could get at this stage, which is not critical where this method is used. + + if (left.Length != right.Length) + { + return false; + } + + var length = left.Length; + var accumulator = 0; + + for (var index = 0; index < length; index++) + { + accumulator |= left[index] - right[index]; + } + + return accumulator is 0; + } + } } diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs b/sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs index 4ae967d8..7f31fdcf 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs +++ b/sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs @@ -31,17 +31,20 @@ public class AuthorizationController : Controller private readonly IOpenIddictAuthorizationManager _authorizationManager; private readonly OpenIddictClientService _clientService; private readonly IOpenIddictScopeManager _scopeManager; + private readonly IOpenIddictSessionManager _sessionManager; public AuthorizationController( IOpenIddictApplicationManager applicationManager, IOpenIddictAuthorizationManager authorizationManager, OpenIddictClientService clientService, - IOpenIddictScopeManager scopeManager) + IOpenIddictScopeManager scopeManager, + IOpenIddictSessionManager sessionManager) { _applicationManager = applicationManager; _authorizationManager = authorizationManager; _clientService = clientService; _scopeManager = scopeManager; + _sessionManager = sessionManager; } [HttpGet, Route("~/connect/authorize")] @@ -124,8 +127,12 @@ public class AuthorizationController : Controller } // Retrieve the profile of the logged in user. - var user = await context.GetUserManager().FindByIdAsync(result.Identity.GetUserId()) - ?? throw new InvalidOperationException("The user details cannot be retrieved."); + var user = await context.GetUserManager().FindByIdAsync(result.Identity.GetUserId()); + if (user is null) + { + context.Authentication.Challenge(DefaultAuthenticationTypes.ApplicationCookie); + return new EmptyResult(); + } // Retrieve the application details from the database. var application = await _applicationManager.FindByClientIdAsync(request.ClientId) @@ -133,11 +140,12 @@ public class AuthorizationController : Controller // Retrieve the permanent authorizations associated with the user and the calling client application. var authorizations = await _authorizationManager.FindAsync( - subject: user.Id, - client : await _applicationManager.GetIdAsync(application), - status : Statuses.Valid, - type : AuthorizationTypes.Permanent, - scopes : request.GetScopes()).ToListAsync(); + query: ( + Subject : user.Id, + ApplicationId : await _applicationManager.GetIdAsync(application), + Status : Statuses.Valid, + Type : AuthorizationTypes.Permanent, + RequiredScopes: request.GetScopes())).ToListAsync(); switch (await _applicationManager.GetConsentTypeAsync(application)) { @@ -181,13 +189,34 @@ public class AuthorizationController : Controller // Automatically create a permanent authorization to avoid requiring explicit consent // for future authorization or token requests containing the same scopes. - var authorization = authorizations.LastOrDefault(); - authorization ??= await _authorizationManager.CreateAsync( - identity: identity, - subject : user.Id, - client : await _applicationManager.GetIdAsync(application), - type : AuthorizationTypes.Permanent, - scopes : identity.GetScopes()); + var authorization = authorizations.LastOrDefault() ?? await _authorizationManager.CreateAsync(new() + { + ApplicationId = await _applicationManager.GetIdAsync(application), + Principal = new ClaimsPrincipal(identity), + Scopes = [.. request.GetScopes()], + Subject = user.Id, + Type = AuthorizationTypes.Permanent, + }); + + // If available, resolve the latest session corresponding to the login identifier stored + // in the authentication cookie or create a new one if no valid session can be found. + if (result.Identity.HasClaim("login_id")) + { + var sessions = await _sessionManager.FindAsync( + query: ( + Subject : user.Id, + LoginId : result.Identity.GetClaim("login_id"), + ApplicationId: await _applicationManager.GetIdAsync(application), + Status : Statuses.Valid)).ToListAsync(); + + var session = sessions.LastOrDefault() ?? await _sessionManager.CreateAsync(new() + { + ApplicationId = await _applicationManager.GetIdAsync(application), + Subject = user.Id + }); + + identity.SetSessionId(await _sessionManager.GetIdAsync(session)); + } identity.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization)); identity.SetDestinations(GetDestinations); @@ -235,7 +264,7 @@ public class AuthorizationController : Controller // Retrieve the user principal stored in the authentication cookie. var result = await context.Authentication.AuthenticateAsync(DefaultAuthenticationTypes.ApplicationCookie); - if (result == null || result.Identity == null) + if (result is not { Identity.IsAuthenticated: true }) { context.Authentication.Challenge(DefaultAuthenticationTypes.ApplicationCookie); @@ -243,8 +272,20 @@ public class AuthorizationController : Controller } // Retrieve the profile of the logged in user. - var user = await context.GetUserManager().FindByIdAsync(result.Identity.GetUserId()) - ?? throw new InvalidOperationException("The user details cannot be retrieved."); + var user = await context.GetUserManager().FindByIdAsync(result.Identity.GetUserId()); + if (user is null) + { + context.Authentication.Challenge( + authenticationTypes: OpenIddictServerOwinDefaults.AuthenticationType, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerOwinConstants.Properties.Error] = Errors.LoginRequired, + [OpenIddictServerOwinConstants.Properties.ErrorDescription] = + "The account associated with the logged in user was removed." + })); + + return new EmptyResult(); + } // Retrieve the application details from the database. var application = await _applicationManager.FindByClientIdAsync(request.ClientId) @@ -252,11 +293,12 @@ public class AuthorizationController : Controller // Retrieve the permanent authorizations associated with the user and the calling client application. var authorizations = await _authorizationManager.FindAsync( - subject: user.Id, - client : await _applicationManager.GetIdAsync(application), - status : Statuses.Valid, - type : AuthorizationTypes.Permanent, - scopes : request.GetScopes()).ToListAsync(); + query: ( + Subject : user.Id, + ApplicationId : await _applicationManager.GetIdAsync(application), + Status : Statuses.Valid, + Type : AuthorizationTypes.Permanent, + RequiredScopes: request.GetScopes())).ToListAsync(); // Note: the same check is already made in the other action but is repeated // here to ensure a malicious user can't abuse this POST-only endpoint and @@ -296,13 +338,34 @@ public class AuthorizationController : Controller // Automatically create a permanent authorization to avoid requiring explicit consent // for future authorization or token requests containing the same scopes. - var authorization = authorizations.LastOrDefault(); - authorization ??= await _authorizationManager.CreateAsync( - identity: identity, - subject : user.Id, - client : await _applicationManager.GetIdAsync(application), - type : AuthorizationTypes.Permanent, - scopes : identity.GetScopes()); + var authorization = authorizations.LastOrDefault() ?? await _authorizationManager.CreateAsync(new() + { + ApplicationId = await _applicationManager.GetIdAsync(application), + Principal = new ClaimsPrincipal(identity), + Scopes = [.. request.GetScopes()], + Subject = user.Id, + Type = AuthorizationTypes.Permanent, + }); + + // If available, resolve the latest session corresponding to the login identifier stored + // in the authentication cookie or create a new one if no valid session can be found. + if (result.Identity.HasClaim("login_id")) + { + var sessions = await _sessionManager.FindAsync( + query: ( + Subject : user.Id, + LoginId : result.Identity.GetClaim("login_id"), + ApplicationId: await _applicationManager.GetIdAsync(application), + Status : Statuses.Valid)).ToListAsync(); + + var session = sessions.LastOrDefault() ?? await _sessionManager.CreateAsync(new() + { + ApplicationId = await _applicationManager.GetIdAsync(application), + Subject = user.Id + }); + + identity.SetSessionId(await _sessionManager.GetIdAsync(session)); + } identity.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization)); identity.SetDestinations(GetDestinations); diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Server/Models/IdentityModels.cs b/sandbox/OpenIddict.Sandbox.AspNet.Server/Models/IdentityModels.cs index b9847ebe..ac1842b7 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Server/Models/IdentityModels.cs +++ b/sandbox/OpenIddict.Sandbox.AspNet.Server/Models/IdentityModels.cs @@ -1,20 +1,29 @@ -using System.Data.Entity; +using System.Buffers.Text; +using System.Data.Entity; using System.Security.Claims; +using System.Security.Cryptography; using System.Threading.Tasks; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.EntityFramework; namespace OpenIddict.Sandbox.AspNet.Server.Models; -// Vous pouvez ajouter des données de profil pour l'utilisateur en ajoutant d'autres propriétés à votre classe ApplicationUser. Pour en savoir plus, consultez https://go.microsoft.com/fwlink/?LinkID=317594. public class ApplicationUser : IdentityUser { public async Task GenerateUserIdentityAsync(UserManager manager) { - // Notez que l'authenticationType doit correspondre à celui défini dans CookieAuthenticationOptions.AuthenticationType - var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie); - // Ajouter des revendications utilisateur personnalisées ici - return userIdentity; + var identity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie); + + // Generate and attach a unique login identifier to the claims identity: this value will + // be used by the authorization controller to infer a unique identifier representing the + // current user session and bind the tokens issued by OpenIddict to a specific session. + // + // Note: this method is also called when the application cookie is refreshed: to ensure + // the login identifier is preserved, a custom OnRefreshingPrincipal event handler is used + // to copy the login identifier from the existing principal to the refreshed instance. + identity.AddClaim(new Claim("login_id", Base64Url.EncodeToString(RandomNumberGenerator.GetBytes(256 / 8)))); + + return identity; } } diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs b/sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs index c5e20904..ae3b031c 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs +++ b/sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs @@ -7,7 +7,6 @@ using Autofac.Extensions.DependencyInjection; using Autofac.Integration.Mvc; using Autofac.Integration.WebApi; using Microsoft.AspNet.Identity; -using Microsoft.AspNet.Identity.Owin; using Microsoft.Extensions.DependencyInjection; using Microsoft.Owin; using Microsoft.Owin.Host.SystemWeb; @@ -150,9 +149,7 @@ public class Startup LoginPath = new PathString("/Account/Login"), Provider = new CookieAuthenticationProvider { - OnValidateIdentity = SecurityStampValidator.OnValidateIdentity( - validateInterval: TimeSpan.FromMinutes(30), - regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager)) + OnValidateIdentity = ApplicationSignInManager.OnValidateIdentity } }); diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs index 10ecbfb6..ef32ad5e 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs @@ -29,6 +29,7 @@ public class AuthorizationController : Controller private readonly IOpenIddictAuthorizationManager _authorizationManager; private readonly OpenIddictClientService _clientService; private readonly IOpenIddictScopeManager _scopeManager; + private readonly IOpenIddictSessionManager _sessionManager; private readonly SignInManager _signInManager; private readonly UserManager _userManager; @@ -37,6 +38,7 @@ public class AuthorizationController : Controller IOpenIddictAuthorizationManager authorizationManager, OpenIddictClientService clientService, IOpenIddictScopeManager scopeManager, + IOpenIddictSessionManager sessionManager, SignInManager signInManager, UserManager userManager) { @@ -44,6 +46,7 @@ public class AuthorizationController : Controller _authorizationManager = authorizationManager; _clientService = clientService; _scopeManager = scopeManager; + _sessionManager = sessionManager; _signInManager = signInManager; _userManager = userManager; } @@ -148,8 +151,15 @@ public class AuthorizationController : Controller } // Retrieve the profile of the logged in user. - var user = await _userManager.GetUserAsync(result.Principal) - ?? throw new InvalidOperationException("The user details cannot be retrieved."); + var user = await _userManager.GetUserAsync(result.Principal); + if (user is null) + { + return Challenge(new AuthenticationProperties + { + RedirectUri = Request.PathBase + Request.Path + QueryString.Create( + Request.HasFormContentType ? Request.Form : Request.Query) + }); + } // Retrieve the application details from the database. var application = await _applicationManager.FindByClientIdAsync(request.ClientId!) @@ -157,11 +167,12 @@ public class AuthorizationController : Controller // Retrieve the permanent authorizations associated with the user and the calling client application. var authorizations = await _authorizationManager.FindAsync( - subject: await _userManager.GetUserIdAsync(user), - client : await _applicationManager.GetIdAsync(application), - status : Statuses.Valid, - type : AuthorizationTypes.Permanent, - scopes : request.GetScopes()).ToListAsync(); + query: ( + Subject : await _userManager.GetUserIdAsync(user), + ApplicationId : await _applicationManager.GetIdAsync(application), + Status : Statuses.Valid, + Type : AuthorizationTypes.Permanent, + RequiredScopes: request.GetScopes())).ToListAsync(); switch (await _applicationManager.GetConsentTypeAsync(application)) { @@ -203,17 +214,39 @@ public class AuthorizationController : Controller // Automatically create a permanent authorization to avoid requiring explicit consent // for future authorization or token requests containing the same scopes. - var authorization = authorizations.LastOrDefault(); - authorization ??= await _authorizationManager.CreateAsync( - identity: identity, - subject : await _userManager.GetUserIdAsync(user), - client : (await _applicationManager.GetIdAsync(application))!, - type : AuthorizationTypes.Permanent, - scopes : identity.GetScopes()); + var authorization = authorizations.LastOrDefault() ?? await _authorizationManager.CreateAsync(new() + { + ApplicationId = await _applicationManager.GetIdAsync(application), + Principal = new ClaimsPrincipal(identity), + Scopes = [.. request.GetScopes()], + Subject = await _userManager.GetUserIdAsync(user), + Type = AuthorizationTypes.Permanent, + }); identity.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization)); identity.SetDestinations(GetDestinations); + // If available, resolve the latest session corresponding to the login identifier stored + // in the authentication cookie or create a new one if no valid session can be found. + if (result.Principal.HasClaim("login_id")) + { + 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(); + + var session = sessions.LastOrDefault() ?? await _sessionManager.CreateAsync(new() + { + ApplicationId = await _applicationManager.GetIdAsync(application), + LoginId = result.Principal.GetClaim("login_id"), + Subject = await _userManager.GetUserIdAsync(user) + }); + + identity.SetSessionId(await _sessionManager.GetIdAsync(session)); + } + return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); // At this point, no authorization was found in the database and an error must be returned @@ -251,8 +284,18 @@ public class AuthorizationController : Controller ?? throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); // Retrieve the profile of the logged in user. - var user = await _userManager.GetUserAsync(User) - ?? throw new InvalidOperationException("The user details cannot be retrieved."); + var user = await _userManager.GetUserAsync(User); + if (user is null) + { + return Forbid( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = + "The account associated with the logged in user was removed." + })); + } // Retrieve the application details from the database. var application = await _applicationManager.FindByClientIdAsync(request.ClientId!) @@ -260,11 +303,12 @@ public class AuthorizationController : Controller // Retrieve the permanent authorizations associated with the user and the calling client application. var authorizations = await _authorizationManager.FindAsync( - subject: await _userManager.GetUserIdAsync(user), - client : await _applicationManager.GetIdAsync(application), - status : Statuses.Valid, - type : AuthorizationTypes.Permanent, - scopes : request.GetScopes()).ToListAsync(); + query: ( + Subject : await _userManager.GetUserIdAsync(user), + ApplicationId : await _applicationManager.GetIdAsync(application), + Status : Statuses.Valid, + Type : AuthorizationTypes.Permanent, + RequiredScopes: request.GetScopes())).ToListAsync(); // Note: the same check is already made in the other action but is repeated // here to ensure a malicious user can't abuse this POST-only endpoint and @@ -302,17 +346,39 @@ public class AuthorizationController : Controller // Automatically create a permanent authorization to avoid requiring explicit consent // for future authorization or token requests containing the same scopes. - var authorization = authorizations.LastOrDefault(); - authorization ??= await _authorizationManager.CreateAsync( - identity: identity, - subject : await _userManager.GetUserIdAsync(user), - client : (await _applicationManager.GetIdAsync(application))!, - type : AuthorizationTypes.Permanent, - scopes : identity.GetScopes()); + var authorization = authorizations.LastOrDefault() ?? await _authorizationManager.CreateAsync(new() + { + ApplicationId = await _applicationManager.GetIdAsync(application), + Principal = new ClaimsPrincipal(identity), + Scopes = [.. request.GetScopes()], + Subject = await _userManager.GetUserIdAsync(user), + Type = AuthorizationTypes.Permanent, + }); identity.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization)); identity.SetDestinations(GetDestinations); + // If available, resolve the latest session corresponding to the login identifier stored + // in the authentication cookie or create a new one if no valid session can be found. + if (User.HasClaim("login_id")) + { + 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(); + + var session = sessions.LastOrDefault() ?? await _sessionManager.CreateAsync(new() + { + ApplicationId = await _applicationManager.GetIdAsync(application), + LoginId = User.GetClaim("login_id"), + Subject = await _userManager.GetUserIdAsync(user) + }); + + identity.SetSessionId(await _sessionManager.GetIdAsync(session)); + } + // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens. return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } @@ -366,8 +432,18 @@ public class AuthorizationController : Controller public async Task VerifyAccept() { // Retrieve the profile of the logged in user. - var user = await _userManager.GetUserAsync(User) - ?? throw new InvalidOperationException("The user details cannot be retrieved."); + var user = await _userManager.GetUserAsync(User); + if (user is null) + { + return Forbid( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = + "The account associated with the logged in user was removed." + })); + } // Retrieve the claims principal associated with the user code. var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Program.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Program.cs index bdb1d17a..baf9e5a1 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Program.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Program.cs @@ -31,9 +31,25 @@ builder.Services.AddDbContext(options => // Register the Identity builder.Services. builder.Services.AddIdentity() + .AddClaimsPrincipalFactory() .AddEntityFrameworkStores() .AddDefaultTokenProviders(); +// Note: ASP.NET Core Identity doesn't store a unique identifier representing user sessions. +// +// To work around that, a custom IUserClaimsPrincipalFactory is used to attach a unique identifier and a +// custom OnRefreshingPrincipal event handler is used to restore that identifier when the cookie is refreshed. +builder.Services.Configure(options => options.OnRefreshingPrincipal = static context => +{ + var identifier = context.CurrentPrincipal?.GetClaim("login_id"); + if (!string.IsNullOrEmpty(identifier)) + { + context.NewPrincipal?.SetClaim("login_id", identifier); + } + + return Task.CompletedTask; +}); + // OpenIddict offers native integration with Quartz.NET to perform scheduled tasks // (like pruning orphaned authorizations/tokens from the database) at regular intervals. builder.Services.AddQuartz(options => diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Services/UserClaimsPrincipalFactory.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Services/UserClaimsPrincipalFactory.cs new file mode 100644 index 00000000..70f7826a --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Services/UserClaimsPrincipalFactory.cs @@ -0,0 +1,36 @@ +using System.Buffers.Text; +using System.Security.Claims; +using System.Security.Cryptography; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using OpenIddict.Sandbox.AspNetCore.Server.Models; + +namespace OpenIddict.Sandbox.AspNetCore.Server.Services; + +public sealed class UserClaimsPrincipalFactory : UserClaimsPrincipalFactory +{ + public UserClaimsPrincipalFactory( + UserManager userManager, + IOptions optionsAccessor) + : base(userManager, optionsAccessor) + { + } + + protected override async Task GenerateClaimsAsync(ApplicationUser user) + { + ArgumentNullException.ThrowIfNull(user); + + var identity = await base.GenerateClaimsAsync(user); + + // Generate and attach a unique login identifier to the claims identity: this value will + // be used by the authorization controller to infer a unique identifier representing the + // current user session and bind the tokens issued by OpenIddict to a specific session. + // + // Note: this method is also called when the application cookie is refreshed: to ensure + // the login identifier is preserved, a custom OnValidateIdentity event handler is used + // to copy the login identifier from the existing principal to the refreshed instance. + identity.AddClaim(new Claim("login_id", Base64Url.EncodeToString(RandomNumberGenerator.GetBytes(256 / 8)))); + + return identity; + } +} diff --git a/src/OpenIddict.Abstractions/Caches/IOpenIddictAuthorizationCache.cs b/src/OpenIddict.Abstractions/Caches/IOpenIddictAuthorizationCache.cs index 405ef7a7..1a796a61 100644 --- a/src/OpenIddict.Abstractions/Caches/IOpenIddictAuthorizationCache.cs +++ b/src/OpenIddict.Abstractions/Caches/IOpenIddictAuthorizationCache.cs @@ -23,18 +23,14 @@ public interface IOpenIddictAuthorizationCache where TAuthorizat ValueTask AddAsync(TAuthorization authorization, CancellationToken cancellationToken); /// - /// Retrieves the authorizations matching the specified parameters. + /// Retrieves the authorizations matching the specified query. /// - /// The subject associated with the authorization, or not to filter out specific subjects. - /// The client associated with the authorization, or not to filter out specific clients. - /// The authorization status, or not to filter out specific authorization statuses. - /// The authorization type, or not to filter out specific authorization types. - /// The minimal scopes associated with the authorization, or not to filter out scopes. + /// The query parameters: if a parameter is , it will not be used to filter the results. /// The that can be used to abort the operation. /// The authorizations corresponding to the criteria. IAsyncEnumerable FindAsync( - string? subject, string? client, string? status, - string? type, ImmutableArray? scopes, CancellationToken cancellationToken); + (string? Subject, string? ApplicationId, string? Status, + string? Type, ImmutableArray? RequiredScopes) query, CancellationToken cancellationToken); /// /// Retrieves the list of authorizations corresponding to the specified application identifier. diff --git a/src/OpenIddict.Abstractions/Caches/IOpenIddictSessionCache.cs b/src/OpenIddict.Abstractions/Caches/IOpenIddictSessionCache.cs new file mode 100644 index 00000000..a121458e --- /dev/null +++ b/src/OpenIddict.Abstractions/Caches/IOpenIddictSessionCache.cs @@ -0,0 +1,82 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +namespace OpenIddict.Abstractions; + +/// +/// Provides methods allowing to cache sessions after retrieving them from the store. +/// +/// The type of the session entity. +public interface IOpenIddictSessionCache where TSession : class +{ + /// + /// Add the specified session to the cache. + /// + /// The session to add to the cache. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + ValueTask AddAsync(TSession session, CancellationToken cancellationToken); + + /// + /// Retrieves the sessions matching the specified query. + /// + /// The query parameters: if a parameter is , it will not be used to filter the results. + /// 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); + + /// + /// Retrieves the list of sessions corresponding to the specified application identifier. + /// + /// The application identifier associated with the sessions. + /// The that can be used to abort the operation. + /// The sessions corresponding to the specified application. + IAsyncEnumerable FindByApplicationIdAsync(string identifier, CancellationToken cancellationToken); + + /// + /// Retrieves the list of sessions corresponding to the specified authorization identifier. + /// + /// The authorization identifier associated with the sessions. + /// The that can be used to abort the operation. + /// The sessions corresponding to the specified authorization. + IAsyncEnumerable FindByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken); + + /// + /// Retrieves the list of sessions corresponding to the specified login identifier. + /// + /// The login identifier associated with the sessions. + /// The that can be used to abort the operation. + /// The sessions corresponding to the specified login identifier. + IAsyncEnumerable FindByLoginIdAsync(string identifier, CancellationToken cancellationToken); + + /// + /// Retrieves a session using its unique identifier. + /// + /// The unique identifier associated with the session. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the session corresponding to the identifier. + /// + ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken); + + /// + /// Retrieves the list of sessions corresponding to the specified subject. + /// + /// The subject associated with the sessions. + /// The that can be used to abort the operation. + /// The sessions corresponding to the specified subject. + IAsyncEnumerable FindBySubjectAsync(string subject, CancellationToken cancellationToken); + + /// + /// Removes the specified session from the cache. + /// + /// The session to remove from the cache. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + ValueTask RemoveAsync(TSession session, CancellationToken cancellationToken); +} diff --git a/src/OpenIddict.Abstractions/Caches/IOpenIddictTokenCache.cs b/src/OpenIddict.Abstractions/Caches/IOpenIddictTokenCache.cs index f1c74a41..53fcc6ad 100644 --- a/src/OpenIddict.Abstractions/Caches/IOpenIddictTokenCache.cs +++ b/src/OpenIddict.Abstractions/Caches/IOpenIddictTokenCache.cs @@ -21,17 +21,13 @@ public interface IOpenIddictTokenCache where TToken : class ValueTask AddAsync(TToken token, CancellationToken cancellationToken); /// - /// Retrieves the tokens matching the specified parameters. + /// Retrieves the tokens matching the specified query. /// - /// The subject associated with the token, or not to filter out specific subjects. - /// The client associated with the token, or not to filter out specific clients. - /// The token status, or not to filter out specific token statuses. - /// The token type, or not to filter out specific token types. + /// The query parameters: if a parameter is , it will not be used to filter the results. /// The that can be used to abort the operation. /// The tokens corresponding to the criteria. IAsyncEnumerable FindAsync( - string? subject, string? client, - string? status, string? type, CancellationToken cancellationToken); + (string? Subject, string? ApplicationId, string? Status, string? Type) query, CancellationToken cancellationToken); /// /// Retrieves the list of tokens corresponding to the specified application identifier. @@ -61,9 +57,11 @@ public interface IOpenIddictTokenCache where TToken : class ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken); /// - /// Retrieves the list of tokens corresponding to the specified reference identifier. - /// Note: the reference identifier may be hashed or encrypted for security reasons. + /// Retrieves a token using its unique reference identifier. /// + /// + /// Note: the reference identifier may be hashed or encrypted for security reasons. + /// /// The reference identifier associated with the tokens. /// The that can be used to abort the operation. /// diff --git a/src/OpenIddict.Abstractions/Descriptors/OpenIddictApplicationDescriptor.cs b/src/OpenIddict.Abstractions/Descriptors/OpenIddictApplicationDescriptor.cs index b5f504d6..b64962a7 100644 --- a/src/OpenIddict.Abstractions/Descriptors/OpenIddictApplicationDescriptor.cs +++ b/src/OpenIddict.Abstractions/Descriptors/OpenIddictApplicationDescriptor.cs @@ -10,80 +10,84 @@ namespace OpenIddict.Abstractions; public class OpenIddictApplicationDescriptor { /// - /// Gets or sets the application type associated with the application. + /// Gets or sets the application type of the application. /// public string? ApplicationType { get; set; } /// - /// Gets or sets the client identifier associated with the application. + /// Gets or sets the client identifier of the application. /// public string? ClientId { get; set; } /// - /// Gets or sets the client secret associated with the application. - /// Note: depending on the application manager used when creating it, - /// this property may be hashed or encrypted for security reasons. + /// Gets or sets the client secret of the application. /// /// + /// + /// Note: depending on the application manager used to create this instance, + /// this property may be hashed or encrypted for security reasons. + /// + /// /// Note: client authentication based on shared secrets is not recommended and should /// only be used for backward compatibility with legacy applications that only support /// client secrets. When possible, consider using public/private key pairs or TLS client /// certificates instead, as these client authentication methods are significantly safer. + /// /// public string? ClientSecret { get; set; } /// - /// Gets or sets the client type associated with the application. + /// Gets or sets the client type of the application. /// public string? ClientType { get; set; } /// - /// Gets or sets the consent type associated with the application. + /// Gets or sets the consent type of the application. /// public string? ConsentType { get; set; } /// - /// Gets or sets the display name associated with the application. + /// Gets or sets the display name of the application. /// public string? DisplayName { get; set; } /// - /// Gets the localized display names associated with the application. + /// Gets the localized display names of the application. /// public Dictionary DisplayNames { get; } = []; /// - /// Gets or sets the JSON Web Key Set associated with the application. + /// Gets or sets the JSON Web Key Set of the application. /// public JsonWebKeySet? JsonWebKeySet { get; set; } /// - /// Gets the permissions associated with the application. + /// Gets the permissions of the application. /// public HashSet Permissions { get; } = new(StringComparer.Ordinal); /// - /// Gets the post-logout redirect URIs associated with the application. + /// Gets the post-logout redirect URIs of the application. /// public HashSet PostLogoutRedirectUris { get; } = []; /// - /// Gets the additional properties associated with the application. + /// Gets the additional properties of the application. /// public Dictionary Properties { get; } = new(StringComparer.Ordinal); /// - /// Gets the redirect URIs associated with the application. + /// Gets the redirect URIs of the application. /// public HashSet RedirectUris { get; } = []; /// - /// Gets the requirements associated with the application. + /// Gets the requirements of the application. /// public HashSet Requirements { get; } = new(StringComparer.Ordinal); /// - /// Gets the settings associated with the application. + /// Gets the settings of the application. /// public Dictionary Settings { get; } = new(StringComparer.Ordinal); diff --git a/src/OpenIddict.Abstractions/Descriptors/OpenIddictAuthorizationDescriptor.cs b/src/OpenIddict.Abstractions/Descriptors/OpenIddictAuthorizationDescriptor.cs index e182dea7..cc848c55 100644 --- a/src/OpenIddict.Abstractions/Descriptors/OpenIddictAuthorizationDescriptor.cs +++ b/src/OpenIddict.Abstractions/Descriptors/OpenIddictAuthorizationDescriptor.cs @@ -9,38 +9,40 @@ namespace OpenIddict.Abstractions; public class OpenIddictAuthorizationDescriptor { /// - /// Gets or sets the application identifier associated with the authorization. + /// Gets or sets the identifier of the application associated with the authorization. /// public string? ApplicationId { get; set; } /// - /// Gets or sets the creation date associated with the authorization. + /// Gets or sets the creation date of the authorization. /// public DateTimeOffset? CreationDate { get; set; } /// - /// Gets or sets the optional principal associated with the authorization. - /// Note: this property is not stored by the default authorization stores. + /// Gets or sets the optional principal specified by the caller. /// + /// + /// Note: this property is not stored by the default stores. + /// public ClaimsPrincipal? Principal { get; set; } /// - /// Gets the additional properties associated with the authorization. + /// Gets or sets the additional properties of the authorization. /// - public Dictionary Properties { get; } = new(StringComparer.Ordinal); + public Dictionary Properties { get; set; } = new(StringComparer.Ordinal); /// - /// Gets the scopes associated with the authorization. + /// Gets or sets the scopes of the authorization. /// - public HashSet Scopes { get; } = new(StringComparer.Ordinal); + public HashSet Scopes { get; set; } = []; /// - /// Gets or sets the status associated with the authorization. + /// Gets or sets the status of the authorization. /// public string? Status { get; set; } /// - /// Gets or sets the subject associated with the authorization. + /// Gets or sets the subject of the authorization. /// public string? Subject { get; set; } diff --git a/src/OpenIddict.Abstractions/Descriptors/OpenIddictResourceDescriptor.cs b/src/OpenIddict.Abstractions/Descriptors/OpenIddictResourceDescriptor.cs index 72458f9f..4b5f5165 100644 --- a/src/OpenIddict.Abstractions/Descriptors/OpenIddictResourceDescriptor.cs +++ b/src/OpenIddict.Abstractions/Descriptors/OpenIddictResourceDescriptor.cs @@ -9,32 +9,32 @@ namespace OpenIddict.Abstractions; public class OpenIddictResourceDescriptor { /// - /// Gets or sets the description associated with the resource. + /// Gets or sets the description of the resource. /// public string? Description { get; set; } /// - /// Gets the localized descriptions associated with the resource. + /// Gets the localized descriptions of the resource. /// public Dictionary Descriptions { get; } = []; /// - /// Gets or sets the display name associated with the resource. + /// Gets or sets the display name of the resource. /// public string? DisplayName { get; set; } /// - /// Gets the localized display names associated with the resource. + /// Gets the localized display names of the resource. /// public Dictionary DisplayNames { get; } = []; /// - /// Gets or sets the unique name associated with the resource. + /// Gets or sets the unique name of the resource. /// public string? Name { get; set; } /// - /// Gets the additional properties associated with the resource. + /// Gets the additional properties of the resource. /// public Dictionary Properties { get; } = new(StringComparer.Ordinal); } diff --git a/src/OpenIddict.Abstractions/Descriptors/OpenIddictScopeDescriptor.cs b/src/OpenIddict.Abstractions/Descriptors/OpenIddictScopeDescriptor.cs index ee7d2a2c..e4340442 100644 --- a/src/OpenIddict.Abstractions/Descriptors/OpenIddictScopeDescriptor.cs +++ b/src/OpenIddict.Abstractions/Descriptors/OpenIddictScopeDescriptor.cs @@ -9,37 +9,37 @@ namespace OpenIddict.Abstractions; public class OpenIddictScopeDescriptor { /// - /// Gets or sets the description associated with the scope. + /// Gets or sets the description of the scope. /// public string? Description { get; set; } /// - /// Gets the localized descriptions associated with the scope. + /// Gets the localized descriptions of the scope. /// public Dictionary Descriptions { get; } = []; /// - /// Gets or sets the display name associated with the scope. + /// Gets or sets the display name of the scope. /// public string? DisplayName { get; set; } /// - /// Gets the localized display names associated with the scope. + /// Gets the localized display names of the scope. /// public Dictionary DisplayNames { get; } = []; /// - /// Gets or sets the unique name associated with the scope. + /// Gets or sets the unique name of the scope. /// public string? Name { get; set; } /// - /// Gets the additional properties associated with the scope. + /// Gets the additional properties of the scope. /// public Dictionary Properties { get; } = new(StringComparer.Ordinal); /// - /// Gets the resources associated with the scope. + /// Gets the resources of the scope. /// public HashSet Resources { get; } = new(StringComparer.Ordinal); } diff --git a/src/OpenIddict.Abstractions/Descriptors/OpenIddictSessionDescriptor.cs b/src/OpenIddict.Abstractions/Descriptors/OpenIddictSessionDescriptor.cs new file mode 100644 index 00000000..5b60a7d3 --- /dev/null +++ b/src/OpenIddict.Abstractions/Descriptors/OpenIddictSessionDescriptor.cs @@ -0,0 +1,53 @@ +using System.Security.Claims; +using System.Text.Json; + +namespace OpenIddict.Abstractions; + +/// +/// Represents an OpenIddict session descriptor. +/// +public class OpenIddictSessionDescriptor +{ + /// + /// Gets or sets the identifier of the application associated with the session. + /// + public string? ApplicationId { get; set; } + + /// + /// Gets or sets the identifier of the authorization associated with the session. + /// + public string? AuthorizationId { get; set; } + + /// + /// Gets or sets the creation date of the session. + /// + public DateTimeOffset? CreationDate { get; set; } + + /// + /// Gets or sets the login identifier of the session. + /// + public string? LoginId { get; set; } + + /// + /// Gets or sets the optional principal specified by the caller. + /// + /// + /// Note: this property is not stored by the default stores. + /// + public ClaimsPrincipal? Principal { get; set; } + + /// + /// Gets the additional properties of the session. + /// + public Dictionary Properties { get; } = new(StringComparer.Ordinal); + + /// + /// Gets or sets the status of the session. + /// + public string? Status { get; set; } + + /// + /// Gets or sets the subject of the session. + /// + public string? Subject { get; set; } +} diff --git a/src/OpenIddict.Abstractions/Descriptors/OpenIddictTokenDescriptor.cs b/src/OpenIddict.Abstractions/Descriptors/OpenIddictTokenDescriptor.cs index adb999be..e080cc88 100644 --- a/src/OpenIddict.Abstractions/Descriptors/OpenIddictTokenDescriptor.cs +++ b/src/OpenIddict.Abstractions/Descriptors/OpenIddictTokenDescriptor.cs @@ -9,65 +9,69 @@ namespace OpenIddict.Abstractions; public class OpenIddictTokenDescriptor { /// - /// Gets or sets the application identifier associated with the token. + /// Gets or sets the identifier of the application associated with the token. /// public string? ApplicationId { get; set; } /// - /// Gets or sets the authorization identifier associated with the token. + /// Gets or sets the identifier of the authorization associated with the token. /// public string? AuthorizationId { get; set; } /// - /// Gets or sets the creation date associated with the token. + /// Gets or sets the creation date of the token. /// public DateTimeOffset? CreationDate { get; set; } /// - /// Gets or sets the expiration date associated with the token. + /// Gets or sets the expiration date of the token. /// public DateTimeOffset? ExpirationDate { get; set; } /// - /// Gets or sets the payload associated with the token. + /// Gets or sets the payload of the token. /// public string? Payload { get; set; } /// - /// Gets or sets the optional principal associated with the token. - /// Note: this property is not stored by the default token stores. + /// Gets or sets the optional principal specified by the caller. /// + /// + /// Note: this property is not stored by the default stores. + /// public ClaimsPrincipal? Principal { get; set; } /// - /// Gets the additional properties associated with the token. + /// Gets the additional properties of the token. /// public Dictionary Properties { get; } = new(StringComparer.Ordinal); /// - /// Gets or sets the redemption date associated with the token. + /// Gets or sets the redemption date of the token. /// public DateTimeOffset? RedemptionDate { get; set; } /// - /// Gets or sets the reference identifier associated with the token. + /// Gets or sets the reference identifier of the token. + /// + /// /// Note: depending on the application manager used when creating it, /// this property may be hashed or encrypted for security reasons. - /// + /// public string? ReferenceId { get; set; } /// - /// Gets or sets the status associated with the token. + /// Gets or sets the status of the token. /// public string? Status { get; set; } /// - /// Gets or sets the subject associated with the token. + /// Gets or sets the subject of the token. /// public string? Subject { get; set; } /// - /// Gets or sets the token type. + /// Gets or sets the type of the token. /// public string? Type { get; set; } } diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs index a1f5f0b9..9bc6a6fd 100644 --- a/src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs @@ -31,7 +31,7 @@ public interface IOpenIddictApplicationManager /// /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of applications in the database. /// ValueTask CountAsync(CancellationToken cancellationToken = default); @@ -43,7 +43,7 @@ public interface IOpenIddictApplicationManager /// The query to execute. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of applications that match the specified query. /// ValueTask CountAsync(Func, IQueryable> query, CancellationToken cancellationToken = default); @@ -57,7 +57,7 @@ public interface IOpenIddictApplicationManager /// The optional state. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of applications that match the specified query. /// ValueTask CountAsync( @@ -72,7 +72,7 @@ public interface IOpenIddictApplicationManager /// The application descriptor. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the unique identifier associated with the application. /// ValueTask CreateAsync(OpenIddictApplicationDescriptor descriptor, CancellationToken cancellationToken = default); @@ -122,7 +122,7 @@ public interface IOpenIddictApplicationManager /// The client identifier associated with the application. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the client application corresponding to the identifier. /// ValueTask FindByClientIdAsync(string identifier, CancellationToken cancellationToken = default); @@ -133,7 +133,7 @@ public interface IOpenIddictApplicationManager /// The unique identifier associated with the application. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the client application corresponding to the identifier. /// ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken = default); @@ -174,7 +174,7 @@ public interface IOpenIddictApplicationManager /// The query to execute. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// ValueTask GetAsync( @@ -189,7 +189,7 @@ public interface IOpenIddictApplicationManager /// The optional state. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// ValueTask GetAsync( @@ -554,7 +554,7 @@ public interface IOpenIddictApplicationManager /// The that can be used to abort the operation. /// A that can be used to monitor the asynchronous operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns a boolean indicating whether the client secret was valid. /// ValueTask ValidateClientSecretAsync(object application, string secret, CancellationToken cancellationToken = default); @@ -567,7 +567,7 @@ public interface IOpenIddictApplicationManager /// The that can be used to abort the operation. /// Note: if no client_id parameter is specified in end session requests, this method may not be called. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns a boolean indicating whether the post_logout_redirect_uri was valid. /// ValueTask ValidatePostLogoutRedirectUriAsync(object application, @@ -582,7 +582,7 @@ public interface IOpenIddictApplicationManager /// The that can be used to abort the operation. /// A that can be used to monitor the asynchronous operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns a boolean indicating whether the client certificate was valid. /// ValueTask ValidatePublicKeyInfrastructureTlsClientCertificateAsync(object application, @@ -595,7 +595,7 @@ public interface IOpenIddictApplicationManager /// The URI that should be compared to one of the redirect_uri stored in the database. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns a boolean indicating whether the redirect_uri was valid. /// ValueTask ValidateRedirectUriAsync(object application, @@ -610,7 +610,7 @@ public interface IOpenIddictApplicationManager /// The that can be used to abort the operation. /// A that can be used to monitor the asynchronous operation. /// - /// A that can be used to monitor the asynchronous operation, whose + /// A that can be used to monitor the asynchronous operation, whose /// result returns a boolean indicating whether the self-signed client certificate was valid. /// ValueTask ValidateSelfSignedTlsClientCertificateAsync( diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs index ffa85785..5f05412a 100644 --- a/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs @@ -6,7 +6,6 @@ using System.Collections.Immutable; using System.ComponentModel.DataAnnotations; -using System.Security.Claims; using System.Text.Json; namespace OpenIddict.Abstractions; @@ -28,7 +27,7 @@ public interface IOpenIddictAuthorizationManager /// /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of authorizations in the database. /// ValueTask CountAsync(CancellationToken cancellationToken = default); @@ -40,7 +39,7 @@ public interface IOpenIddictAuthorizationManager /// The query to execute. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of authorizations that match the specified query. /// ValueTask CountAsync( @@ -55,52 +54,20 @@ public interface IOpenIddictAuthorizationManager /// The optional state. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of authorizations that match the specified query. /// ValueTask CountAsync( Func, TState, IQueryable> query, TState state, CancellationToken cancellationToken = default); - /// - /// Creates a new permanent authorization based on the specified parameters. - /// - /// The identity associated with the authorization. - /// The subject associated with the authorization. - /// The client associated with the authorization. - /// The authorization type. - /// The minimal scopes associated with the authorization. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, whose result returns the authorization. - /// - ValueTask CreateAsync( - ClaimsIdentity identity, string subject, string client, - string type, ImmutableArray scopes, CancellationToken cancellationToken = default); - - /// - /// Creates a new permanent authorization based on the specified parameters. - /// - /// The principal associated with the authorization. - /// The subject associated with the authorization. - /// The client associated with the authorization. - /// The authorization type. - /// The minimal scopes associated with the authorization. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, whose result returns the authorization. - /// - ValueTask CreateAsync( - ClaimsPrincipal principal, string subject, string client, - string type, ImmutableArray scopes, CancellationToken cancellationToken = default); - /// /// Creates a new authorization based on the specified descriptor. /// /// The authorization descriptor. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, whose result returns the authorization. + /// A that can be used to monitor the asynchronous operation, whose result returns the authorization. /// ValueTask CreateAsync(OpenIddictAuthorizationDescriptor descriptor, CancellationToken cancellationToken = default); @@ -125,18 +92,14 @@ public interface IOpenIddictAuthorizationManager ValueTask DeleteAsync(object authorization, CancellationToken cancellationToken = default); /// - /// Retrieves the authorizations matching the specified parameters. + /// Retrieves the authorizations matching the specified query. /// - /// The subject associated with the authorization, or not to filter out specific subjects. - /// The client associated with the authorization, or not to filter out specific clients. - /// The authorization status, or not to filter out specific authorization statuses. - /// The authorization type, or not to filter out specific authorization types. - /// The minimal scopes associated with the authorization, or not to filter out scopes. + /// The query parameters: if a parameter is , it will not be used to filter the results. /// The that can be used to abort the operation. /// The authorizations corresponding to the criteria. IAsyncEnumerable FindAsync( - string? subject, string? client, string? status, - string? type, ImmutableArray? scopes, CancellationToken cancellationToken = default); + (string? Subject, string? ApplicationId, string? Status, + string? Type, ImmutableArray? RequiredScopes) query, CancellationToken cancellationToken = default); /// /// Retrieves the list of authorizations corresponding to the specified application identifier. @@ -152,7 +115,7 @@ public interface IOpenIddictAuthorizationManager /// The unique identifier associated with the authorization. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the authorization corresponding to the identifier. /// ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken = default); @@ -183,7 +146,7 @@ public interface IOpenIddictAuthorizationManager /// The query to execute. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// ValueTask GetAsync( @@ -198,7 +161,7 @@ public interface IOpenIddictAuthorizationManager /// The optional state. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// ValueTask GetAsync( diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictResourceManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictResourceManager.cs index 03443a03..37c77b34 100644 --- a/src/OpenIddict.Abstractions/Managers/IOpenIddictResourceManager.cs +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictResourceManager.cs @@ -28,7 +28,7 @@ public interface IOpenIddictResourceManager /// /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of resources in the database. /// ValueTask CountAsync(CancellationToken cancellationToken = default); @@ -40,7 +40,7 @@ public interface IOpenIddictResourceManager /// The query to execute. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of resources that match the specified query. /// ValueTask CountAsync(Func, IQueryable> query, CancellationToken cancellationToken = default); @@ -54,7 +54,7 @@ public interface IOpenIddictResourceManager /// The optional state. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of resources that match the specified query. /// ValueTask CountAsync( @@ -67,7 +67,7 @@ public interface IOpenIddictResourceManager /// The resource descriptor. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, whose result returns the resource. + /// A that can be used to monitor the asynchronous operation, whose result returns the resource. /// ValueTask CreateAsync(OpenIddictResourceDescriptor descriptor, CancellationToken cancellationToken = default); @@ -97,7 +97,7 @@ public interface IOpenIddictResourceManager /// The unique identifier associated with the resource. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the resource corresponding to the identifier. /// ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken = default); @@ -108,7 +108,7 @@ public interface IOpenIddictResourceManager /// The name associated with the resource. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the resource corresponding to the specified name. /// ValueTask FindByNameAsync(string name, CancellationToken cancellationToken = default); @@ -128,7 +128,7 @@ public interface IOpenIddictResourceManager /// The query to execute. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// ValueTask GetAsync( @@ -143,7 +143,7 @@ public interface IOpenIddictResourceManager /// The optional state. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// ValueTask GetAsync( diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictScopeManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictScopeManager.cs index 2479b67d..5400b513 100644 --- a/src/OpenIddict.Abstractions/Managers/IOpenIddictScopeManager.cs +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictScopeManager.cs @@ -28,7 +28,7 @@ public interface IOpenIddictScopeManager /// /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of scopes in the database. /// ValueTask CountAsync(CancellationToken cancellationToken = default); @@ -40,7 +40,7 @@ public interface IOpenIddictScopeManager /// The query to execute. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of scopes that match the specified query. /// ValueTask CountAsync(Func, IQueryable> query, CancellationToken cancellationToken = default); @@ -54,7 +54,7 @@ public interface IOpenIddictScopeManager /// The optional state. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of scopes that match the specified query. /// ValueTask CountAsync( @@ -67,7 +67,7 @@ public interface IOpenIddictScopeManager /// The scope descriptor. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, whose result returns the scope. + /// A that can be used to monitor the asynchronous operation, whose result returns the scope. /// ValueTask CreateAsync(OpenIddictScopeDescriptor descriptor, CancellationToken cancellationToken = default); @@ -97,7 +97,7 @@ public interface IOpenIddictScopeManager /// The unique identifier associated with the scope. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the scope corresponding to the identifier. /// ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken = default); @@ -108,7 +108,7 @@ public interface IOpenIddictScopeManager /// The name associated with the scope. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the scope corresponding to the specified name. /// ValueTask FindByNameAsync(string name, CancellationToken cancellationToken = default); @@ -136,7 +136,7 @@ public interface IOpenIddictScopeManager /// The query to execute. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// ValueTask GetAsync( @@ -151,7 +151,7 @@ public interface IOpenIddictScopeManager /// The optional state. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// ValueTask GetAsync( diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictSessionManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictSessionManager.cs new file mode 100644 index 00000000..8fb92a14 --- /dev/null +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictSessionManager.cs @@ -0,0 +1,346 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.Collections.Immutable; +using System.ComponentModel.DataAnnotations; +using System.Text.Json; + +namespace OpenIddict.Abstractions; + +/// +/// Provides methods allowing to manage the Sessions stored in the store. +/// +/// +/// Note: this interface is not meant to be implemented by custom managers, +/// that should inherit from the generic OpenIddictSessionManager class. +/// It is primarily intended to be used by services that cannot easily +/// depend on the generic session manager. The actual session entity type is +/// automatically determined at runtime based on the OpenIddict core options. +/// +public interface IOpenIddictSessionManager +{ + /// + /// Determines the number of sessions that exist in the database. + /// + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the number of sessions in the database. + /// + ValueTask CountAsync(CancellationToken cancellationToken = default); + + /// + /// Determines the number of sessions that match the specified query. + /// + /// The result type. + /// The query to execute. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the number of sessions that match the specified query. + /// + ValueTask CountAsync(Func, IQueryable> query, CancellationToken cancellationToken = default); + + /// + /// Determines the number of sessions that match the specified query. + /// + /// The state type. + /// The result type. + /// The query to execute. + /// The optional state. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the number of sessions that match the specified query. + /// + ValueTask CountAsync( + Func, TState, IQueryable> query, + TState state, CancellationToken cancellationToken = default); + + /// + /// Creates a new session based on the specified descriptor. + /// + /// The session descriptor. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, whose result returns the session. + /// + ValueTask CreateAsync(OpenIddictSessionDescriptor descriptor, CancellationToken cancellationToken = default); + + /// + /// Creates a new session. + /// + /// The session to create. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + ValueTask CreateAsync(object session, CancellationToken cancellationToken = default); + + /// + /// Removes an existing session. + /// + /// The session to delete. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + ValueTask DeleteAsync(object session, CancellationToken cancellationToken = default); + + /// + /// Retrieves the sessions matching the specified query. + /// + /// The query parameters: if a parameter is , it will not be used to filter the results. + /// 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 = default); + + /// + /// Retrieves the list of sessions corresponding to the specified application identifier. + /// + /// The application identifier associated with the sessions. + /// The that can be used to abort the operation. + /// The sessions corresponding to the specified application. + IAsyncEnumerable FindByApplicationIdAsync(string identifier, CancellationToken cancellationToken = default); + + /// + /// Retrieves the list of sessions corresponding to the specified authorization identifier. + /// + /// The authorization identifier associated with the sessions. + /// The that can be used to abort the operation. + /// The sessions corresponding to the specified authorization. + IAsyncEnumerable FindByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken = default); + + /// + /// Retrieves the list of sessions corresponding to the specified login identifier. + /// + /// The login identifier associated with the sessions. + /// The that can be used to abort the operation. + /// The sessions corresponding to the specified login identifier. + IAsyncEnumerable FindByLoginIdAsync(string identifier, CancellationToken cancellationToken = default); + + /// + /// Retrieves a session using its unique identifier. + /// + /// The unique identifier associated with the session. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the session corresponding to the identifier. + /// + ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken = default); + + /// + /// Retrieves the list of sessions corresponding to the specified subject. + /// + /// The subject associated with the sessions. + /// The that can be used to abort the operation. + /// The sessions corresponding to the specified subject. + IAsyncEnumerable FindBySubjectAsync(string subject, CancellationToken cancellationToken = default); + + /// + /// Retrieves the optional application identifier associated with a session. + /// + /// The session. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the application identifier associated with the session. + /// + ValueTask GetApplicationIdAsync(object session, CancellationToken cancellationToken = default); + + /// + /// Executes the specified query and returns the first element. + /// + /// The result type. + /// The query to execute. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the first element returned when executing the query. + /// + ValueTask GetAsync( + Func, IQueryable> query, CancellationToken cancellationToken = default); + + /// + /// Executes the specified query and returns the first element. + /// + /// The state type. + /// The result type. + /// The query to execute. + /// The optional state. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the first element returned when executing the query. + /// + ValueTask GetAsync( + Func, TState, IQueryable> query, + TState state, CancellationToken cancellationToken = default); + + /// + /// Retrieves the optional authorization identifier associated with a session. + /// + /// The session. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the authorization identifier associated with the session. + /// + ValueTask GetAuthorizationIdAsync(object session, CancellationToken cancellationToken = default); + + /// + /// Retrieves the creation date associated with a session. + /// + /// The session. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the creation date associated with the specified session. + /// + ValueTask GetCreationDateAsync(object session, CancellationToken cancellationToken = default); + + /// + /// Retrieves the unique identifier associated with a session. + /// + /// The session. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the unique identifier associated with the session. + /// + ValueTask GetIdAsync(object session, CancellationToken cancellationToken = default); + + /// + /// Retrieves the login identifier associated with a session. + /// + /// The session. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the login identifier associated with the specified session. + /// + ValueTask GetLoginIdAsync(object session, CancellationToken cancellationToken = default); + + /// + /// Retrieves the additional properties associated with a session. + /// + /// The session. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the additional properties associated with the session. + /// + ValueTask> GetPropertiesAsync(object session, CancellationToken cancellationToken = default); + + /// + /// Retrieves the status associated with a session. + /// + /// The session. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the status associated with the specified session. + /// + ValueTask GetStatusAsync(object session, CancellationToken cancellationToken = default); + + /// + /// Retrieves the subject associated with a session. + /// + /// The session. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the subject associated with the specified session. + /// + ValueTask GetSubjectAsync(object session, CancellationToken cancellationToken = default); + + /// + /// Executes the specified query and returns all the corresponding elements. + /// + /// The number of results to return. + /// The number of results to skip. + /// The that can be used to abort the operation. + /// All the elements returned when executing the specified query. + IAsyncEnumerable ListAsync( + int? count = null, int? offset = null, CancellationToken cancellationToken = default); + + /// + /// Executes the specified query and returns all the corresponding elements. + /// + /// The result type. + /// The query to execute. + /// The that can be used to abort the operation. + /// All the elements returned when executing the specified query. + IAsyncEnumerable ListAsync( + Func, IQueryable> query, CancellationToken cancellationToken = default); + + /// + /// Executes the specified query and returns all the corresponding elements. + /// + /// The state type. + /// The result type. + /// The query to execute. + /// The optional state. + /// The that can be used to abort the operation. + /// All the elements returned when executing the specified query. + IAsyncEnumerable ListAsync( + Func, TState, IQueryable> query, + TState state, CancellationToken cancellationToken = default); + + /// + /// Populates the specified descriptor using the properties exposed by the session. + /// + /// The descriptor. + /// The session. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + ValueTask PopulateAsync(OpenIddictSessionDescriptor descriptor, object session, CancellationToken cancellationToken = default); + + /// + /// Populates the session using the specified descriptor. + /// + /// The session. + /// The descriptor. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + ValueTask PopulateAsync(object session, OpenIddictSessionDescriptor descriptor, CancellationToken cancellationToken = default); + + /// + /// Updates an existing session. + /// + /// The session to update. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + ValueTask UpdateAsync(object session, CancellationToken cancellationToken = default); + + /// + /// Updates an existing session. + /// + /// The session to update. + /// The descriptor used to update the session. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + ValueTask UpdateAsync(object session, OpenIddictSessionDescriptor descriptor, CancellationToken cancellationToken = default); + + /// + /// Validates the session to ensure it's in a consistent state. + /// + /// The session. + /// The that can be used to abort the operation. + /// The validation error encountered when validating the session. + IAsyncEnumerable ValidateAsync(object session, CancellationToken cancellationToken = default); +} diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs index 0f72aa1d..256e1830 100644 --- a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs @@ -27,7 +27,7 @@ public interface IOpenIddictTokenManager /// /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of tokens in the database. /// ValueTask CountAsync(CancellationToken cancellationToken = default); @@ -39,7 +39,7 @@ public interface IOpenIddictTokenManager /// The query to execute. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of tokens that match the specified query. /// ValueTask CountAsync(Func, IQueryable> query, CancellationToken cancellationToken = default); @@ -53,7 +53,7 @@ public interface IOpenIddictTokenManager /// The optional state. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of tokens that match the specified query. /// ValueTask CountAsync( @@ -66,7 +66,7 @@ public interface IOpenIddictTokenManager /// The token descriptor. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, whose result returns the token. + /// A that can be used to monitor the asynchronous operation, whose result returns the token. /// ValueTask CreateAsync(OpenIddictTokenDescriptor descriptor, CancellationToken cancellationToken = default); @@ -91,17 +91,14 @@ public interface IOpenIddictTokenManager ValueTask DeleteAsync(object token, CancellationToken cancellationToken = default); /// - /// Retrieves the tokens matching the specified parameters. + /// Retrieves the tokens matching the specified query. /// - /// The subject associated with the token, or not to filter out specific subjects. - /// The client associated with the token, or not to filter out specific clients. - /// The token status, or not to filter out specific token statuses. - /// The token type, or not to filter out specific token types. + /// The query parameters: if a parameter is , it will not be used to filter the results. /// The that can be used to abort the operation. /// The tokens corresponding to the criteria. IAsyncEnumerable FindAsync( - string? subject, string? client, - string? status, string? type, CancellationToken cancellationToken = default); + (string? Subject, string? ApplicationId, string? Status, string? Type) query, + CancellationToken cancellationToken = default); /// /// Retrieves the list of tokens corresponding to the specified application identifier. @@ -125,19 +122,21 @@ public interface IOpenIddictTokenManager /// The unique identifier associated with the token. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the token corresponding to the unique identifier. /// ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken = default); /// - /// Retrieves the list of tokens corresponding to the specified reference identifier. - /// Note: the reference identifier may be hashed or encrypted for security reasons. + /// Retrieves a token using its unique reference identifier. /// + /// + /// Note: the reference identifier may be hashed or encrypted for security reasons. + /// /// The reference identifier associated with the tokens. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the tokens corresponding to the specified reference identifier. /// ValueTask FindByReferenceIdAsync(string identifier, CancellationToken cancellationToken = default); @@ -168,7 +167,7 @@ public interface IOpenIddictTokenManager /// The query to execute. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// ValueTask GetAsync( @@ -183,7 +182,7 @@ public interface IOpenIddictTokenManager /// The optional state. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// ValueTask GetAsync( diff --git a/src/OpenIddict.Abstractions/OpenIddictConstants.cs b/src/OpenIddict.Abstractions/OpenIddictConstants.cs index a0c9c076..cbc3369e 100644 --- a/src/OpenIddict.Abstractions/OpenIddictConstants.cs +++ b/src/OpenIddict.Abstractions/OpenIddictConstants.cs @@ -106,6 +106,7 @@ public static class OpenIddictConstants public const string RequestForgeryProtection = "rfp"; public const string Role = "role"; public const string Scope = "scope"; + public const string SessionId = "sid"; public const string StreetAddress = "street_address"; public const string Subject = "sub"; public const string TargetLinkUri = "target_link_uri"; @@ -154,6 +155,7 @@ public static class OpenIddictConstants public const string RequestTokenType = "oi_reqt_typ"; public const string Resource = "oi_rsrc"; public const string ResponseType = "oi_rsp_typ"; + public const string SessionId = "oi_ses_id"; public const string SigningAlgorithm = "oi_sign_alg"; public const string Scope = "oi_scp"; public const string StateTokenLifetime = "oi_stet_lft"; diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index 76d81d5b..b0cc79ed 100644 --- a/src/OpenIddict.Abstractions/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx @@ -867,16 +867,13 @@ Make sure that the entity is not abstract and has a public parameterless constru An error occurred while pruning authorizations. - The application associated with the authorization cannot be found. + The application matching the specified identifier cannot be found in the change tracker or in the database. An error occurred while pruning tokens. - - The application associated with the token cannot be found. - - The authorization associated with the token cannot be found. + The authorization matching the specified identifier cannot be found in the change tracker or in the database. No Entity Framework Core context was configured to be used with OpenIddict. @@ -2448,6 +2445,9 @@ To use a custom policy relying on the system store, set 'OpenIddictServerOptions A resource with the same name already exists. + + The login identifier cannot be null or empty and must match the value used to represent the user session. + The '{0}' parameter shouldn't be null or empty at this point. diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs index 42e3643e..55645638 100644 --- a/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs +++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs @@ -2692,6 +2692,22 @@ public static class OpenIddictExtensions public static string? GetAuthorizationId(this ClaimsPrincipal principal) => principal.GetClaim(Claims.Private.AuthorizationId); + /// + /// Gets the internal session identifier associated with the claims identity. + /// + /// The claims identity. + /// The unique identifier or if the claim cannot be found. + public static string? GetSessionId(this ClaimsIdentity identity) + => identity.GetClaim(Claims.Private.SessionId); + + /// + /// Gets the internal session identifier associated with the claims principal. + /// + /// The claims principal. + /// The unique identifier or if the claim cannot be found. + public static string? GetSessionId(this ClaimsPrincipal principal) + => principal.GetClaim(Claims.Private.SessionId); + /// /// Gets the internal token identifier associated with the claims identity. /// @@ -3344,6 +3360,24 @@ public static class OpenIddictExtensions public static ClaimsPrincipal SetAuthorizationId(this ClaimsPrincipal principal, string? identifier) => principal.SetClaim(Claims.Private.AuthorizationId, identifier); + /// + /// Sets the internal session identifier associated with the claims identity. + /// + /// The claims identity. + /// The unique identifier to store. + /// The claims identity. + public static ClaimsIdentity SetSessionId(this ClaimsIdentity identity, string? identifier) + => identity.SetClaim(Claims.Private.SessionId, identifier); + + /// + /// Sets the internal session identifier associated with the claims principal. + /// + /// The claims principal. + /// The unique identifier to store. + /// The claims principal. + public static ClaimsPrincipal SetSessionId(this ClaimsPrincipal principal, string? identifier) + => principal.SetClaim(Claims.Private.SessionId, identifier); + /// /// Sets the internal token identifier associated with the claims identity. /// diff --git a/src/OpenIddict.Abstractions/Stores/IOpenIddictApplicationStore.cs b/src/OpenIddict.Abstractions/Stores/IOpenIddictApplicationStore.cs index dabe347e..a433f549 100644 --- a/src/OpenIddict.Abstractions/Stores/IOpenIddictApplicationStore.cs +++ b/src/OpenIddict.Abstractions/Stores/IOpenIddictApplicationStore.cs @@ -23,7 +23,7 @@ public interface IOpenIddictApplicationStore where TApplication : /// /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of applications in the database. /// ValueTask CountAsync(CancellationToken cancellationToken); @@ -37,7 +37,7 @@ public interface IOpenIddictApplicationStore where TApplication : /// The optional state. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of applications that match the specified query. /// ValueTask CountAsync( @@ -66,7 +66,7 @@ public interface IOpenIddictApplicationStore where TApplication : /// The unique identifier associated with the application. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the client application corresponding to the identifier. /// ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken); @@ -77,7 +77,7 @@ public interface IOpenIddictApplicationStore where TApplication : /// The client identifier associated with the application. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the client application corresponding to the identifier. /// ValueTask FindByClientIdAsync(string identifier, CancellationToken cancellationToken); @@ -120,7 +120,7 @@ public interface IOpenIddictApplicationStore where TApplication : /// The optional state. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// ValueTask GetAsync( diff --git a/src/OpenIddict.Abstractions/Stores/IOpenIddictAuthorizationStore.cs b/src/OpenIddict.Abstractions/Stores/IOpenIddictAuthorizationStore.cs index fa178d59..650b45e9 100644 --- a/src/OpenIddict.Abstractions/Stores/IOpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.Abstractions/Stores/IOpenIddictAuthorizationStore.cs @@ -20,7 +20,7 @@ public interface IOpenIddictAuthorizationStore where TAuthorizat /// /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of authorizations in the database. /// ValueTask CountAsync(CancellationToken cancellationToken); @@ -34,7 +34,7 @@ public interface IOpenIddictAuthorizationStore where TAuthorizat /// The optional state. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of authorizations that match the specified query. /// ValueTask CountAsync( @@ -58,19 +58,14 @@ public interface IOpenIddictAuthorizationStore where TAuthorizat ValueTask DeleteAsync(TAuthorization authorization, CancellationToken cancellationToken); /// - /// Retrieves the authorizations matching the specified parameters. + /// Retrieves the authorizations matching the specified query. /// - /// The subject associated with the authorization, or not to filter out specific subjects. - /// The client associated with the authorization, or not to filter out specific clients. - /// The authorization status, or not to filter out specific authorization statuses. - /// The authorization type, or not to filter out specific authorization types. - /// The minimal scopes associated with the authorization, or not to filter out scopes. + /// The query parameters: if a parameter is , it will not be used to filter the results. /// The that can be used to abort the operation. /// The authorizations corresponding to the criteria. IAsyncEnumerable FindAsync( - string? subject, string? client, - string? status, string? type, - ImmutableArray? scopes, CancellationToken cancellationToken); + (string? Subject, string? ApplicationId, string? Status, + string? Type, ImmutableArray? RequiredScopes) query, CancellationToken cancellationToken); /// /// Retrieves the list of authorizations corresponding to the specified application identifier. @@ -86,7 +81,7 @@ public interface IOpenIddictAuthorizationStore where TAuthorizat /// The unique identifier associated with the authorization. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the authorization corresponding to the identifier. /// ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken); @@ -119,7 +114,7 @@ public interface IOpenIddictAuthorizationStore where TAuthorizat /// The optional state. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// ValueTask GetAsync( diff --git a/src/OpenIddict.Abstractions/Stores/IOpenIddictResourceStore.cs b/src/OpenIddict.Abstractions/Stores/IOpenIddictResourceStore.cs index ff7fd1ea..8327e2ad 100644 --- a/src/OpenIddict.Abstractions/Stores/IOpenIddictResourceStore.cs +++ b/src/OpenIddict.Abstractions/Stores/IOpenIddictResourceStore.cs @@ -21,7 +21,7 @@ public interface IOpenIddictResourceStore where TResource : class /// /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of resources in the database. /// ValueTask CountAsync(CancellationToken cancellationToken); @@ -35,7 +35,7 @@ public interface IOpenIddictResourceStore where TResource : class /// The optional state. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of resources that match the specified query. /// ValueTask CountAsync( @@ -64,7 +64,7 @@ public interface IOpenIddictResourceStore where TResource : class /// The unique identifier associated with the resource. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the resource corresponding to the identifier. /// ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken); @@ -75,7 +75,7 @@ public interface IOpenIddictResourceStore where TResource : class /// The name associated with the resource. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the resource corresponding to the specified name. /// ValueTask FindByNameAsync(string name, CancellationToken cancellationToken); @@ -97,7 +97,7 @@ public interface IOpenIddictResourceStore where TResource : class /// The optional state. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// ValueTask GetAsync( diff --git a/src/OpenIddict.Abstractions/Stores/IOpenIddictScopeStore.cs b/src/OpenIddict.Abstractions/Stores/IOpenIddictScopeStore.cs index 1a263b26..4122bece 100644 --- a/src/OpenIddict.Abstractions/Stores/IOpenIddictScopeStore.cs +++ b/src/OpenIddict.Abstractions/Stores/IOpenIddictScopeStore.cs @@ -21,7 +21,7 @@ public interface IOpenIddictScopeStore where TScope : class /// /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of scopes in the database. /// ValueTask CountAsync(CancellationToken cancellationToken); @@ -35,7 +35,7 @@ public interface IOpenIddictScopeStore where TScope : class /// The optional state. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of scopes that match the specified query. /// ValueTask CountAsync( @@ -64,7 +64,7 @@ public interface IOpenIddictScopeStore where TScope : class /// The unique identifier associated with the scope. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the scope corresponding to the identifier. /// ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken); @@ -75,7 +75,7 @@ public interface IOpenIddictScopeStore where TScope : class /// The name associated with the scope. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the scope corresponding to the specified name. /// ValueTask FindByNameAsync(string name, CancellationToken cancellationToken); @@ -105,7 +105,7 @@ public interface IOpenIddictScopeStore where TScope : class /// The optional state. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// ValueTask GetAsync( diff --git a/src/OpenIddict.Abstractions/Stores/IOpenIddictSessionStore.cs b/src/OpenIddict.Abstractions/Stores/IOpenIddictSessionStore.cs new file mode 100644 index 00000000..e1af87ef --- /dev/null +++ b/src/OpenIddict.Abstractions/Stores/IOpenIddictSessionStore.cs @@ -0,0 +1,319 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.Collections.Immutable; +using System.Text.Json; + +namespace OpenIddict.Abstractions; + +/// +/// Provides methods allowing to manage the sessions stored in a database. +/// +/// The type of the session entity. +public interface IOpenIddictSessionStore where TSession : class +{ + /// + /// Determines the number of sessions that exist in the database. + /// + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the number of sessions in the database. + /// + ValueTask CountAsync(CancellationToken cancellationToken); + + /// + /// Determines the number of sessions that match the specified query. + /// + /// The state type. + /// The result type. + /// The query to execute. + /// The optional state. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the number of sessions that match the specified query. + /// + ValueTask CountAsync( + Func, TState, IQueryable> query, + TState state, CancellationToken cancellationToken); + + /// + /// Creates a new session. + /// + /// The session to create. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + ValueTask CreateAsync(TSession session, CancellationToken cancellationToken); + + /// + /// Removes an existing session. + /// + /// The session to delete. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + ValueTask DeleteAsync(TSession session, CancellationToken cancellationToken); + + /// + /// Retrieves the sessions matching the specified query. + /// + /// The query parameters: if a parameter is , it will not be used to filter the results. + /// 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); + + /// + /// Retrieves the list of sessions corresponding to the specified application identifier. + /// + /// The application identifier associated with the sessions. + /// The that can be used to abort the operation. + /// The sessions corresponding to the specified application. + IAsyncEnumerable FindByApplicationIdAsync(string identifier, CancellationToken cancellationToken); + + /// + /// Retrieves the list of sessions corresponding to the specified authorization identifier. + /// + /// The authorization identifier associated with the sessions. + /// The that can be used to abort the operation. + /// The sessions corresponding to the specified authorization. + IAsyncEnumerable FindByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken); + + /// + /// Retrieves the list of sessions corresponding to the specified login identifier. + /// + /// The login identifier associated with the sessions. + /// The that can be used to abort the operation. + /// The sessions corresponding to the specified login identifier. + IAsyncEnumerable FindByLoginIdAsync(string identifier, CancellationToken cancellationToken); + + /// + /// Retrieves a session using its unique identifier. + /// + /// The unique identifier associated with the session. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the session corresponding to the identifier. + /// + ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken); + + /// + /// Retrieves the list of sessions corresponding to the specified subject. + /// + /// The subject associated with the sessions. + /// The that can be used to abort the operation. + /// The sessions corresponding to the specified subject. + IAsyncEnumerable FindBySubjectAsync(string subject, CancellationToken cancellationToken); + + /// + /// Retrieves the optional application identifier associated with a session. + /// + /// The session. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the application identifier associated with the session. + /// + ValueTask GetApplicationIdAsync(TSession session, CancellationToken cancellationToken); + + /// + /// Executes the specified query and returns the first element. + /// + /// The state type. + /// The result type. + /// The query to execute. + /// The optional state. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the first element returned when executing the query. + /// + ValueTask GetAsync( + Func, TState, IQueryable> query, + TState state, CancellationToken cancellationToken); + + /// + /// Retrieves the optional authorization identifier associated with a session. + /// + /// The session. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the authorization identifier associated with the session. + /// + ValueTask GetAuthorizationIdAsync(TSession session, CancellationToken cancellationToken); + + /// + /// Retrieves the creation date associated with a session. + /// + /// The session. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the creation date associated with the specified session. + /// + ValueTask GetCreationDateAsync(TSession session, CancellationToken cancellationToken); + + /// + /// Retrieves the unique identifier associated with a session. + /// + /// The session. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the unique identifier associated with the session. + /// + ValueTask GetIdAsync(TSession session, CancellationToken cancellationToken); + + /// + /// Retrieves the login identifier associated with a session. + /// + /// The session. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the login identifier associated with the specified session. + /// + ValueTask GetLoginIdAsync(TSession session, CancellationToken cancellationToken); + + /// + /// Retrieves the additional properties associated with a session. + /// + /// The session. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, whose + /// result returns all the additional properties associated with the session. + /// + ValueTask> GetPropertiesAsync(TSession session, CancellationToken cancellationToken); + + /// + /// Retrieves the status associated with a session. + /// + /// The session. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the status associated with the specified session. + /// + ValueTask GetStatusAsync(TSession session, CancellationToken cancellationToken); + + /// + /// Retrieves the subject associated with a session. + /// + /// The session. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the subject associated with the specified session. + /// + ValueTask GetSubjectAsync(TSession session, CancellationToken cancellationToken); + + /// + /// Instantiates a new session. + /// + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the instantiated session, that can be persisted in the database. + /// + ValueTask InstantiateAsync(CancellationToken cancellationToken); + + /// + /// Executes the specified query and returns all the corresponding elements. + /// + /// The number of results to return. + /// The number of results to skip. + /// The that can be used to abort the operation. + /// All the elements returned when executing the specified query. + IAsyncEnumerable ListAsync(int? count, int? offset, CancellationToken cancellationToken); + + /// + /// Executes the specified query and returns all the corresponding elements. + /// + /// The state type. + /// The result type. + /// The query to execute. + /// The optional state. + /// The that can be used to abort the operation. + /// All the elements returned when executing the specified query. + IAsyncEnumerable ListAsync( + Func, TState, IQueryable> query, + TState state, CancellationToken cancellationToken); + + /// + /// Sets the application identifier associated with a session. + /// + /// The session. + /// The unique identifier associated with the session. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + ValueTask SetApplicationIdAsync(TSession session, string? identifier, CancellationToken cancellationToken); + + /// + /// Sets the authorization identifier associated with a session. + /// + /// The session. + /// The unique identifier associated with the session. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + ValueTask SetAuthorizationIdAsync(TSession session, string? identifier, CancellationToken cancellationToken); + + /// + /// Sets the creation date associated with a session. + /// + /// The session. + /// The creation date. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + ValueTask SetCreationDateAsync(TSession session, DateTimeOffset? date, CancellationToken cancellationToken); + + /// + /// Sets the login identifier associated with a session. + /// + /// The session. + /// The login identifier associated with the session. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + ValueTask SetLoginIdAsync(TSession session, string? identifier, CancellationToken cancellationToken); + + /// + /// Sets the additional properties associated with a session. + /// + /// The session. + /// The additional properties associated with the session. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + ValueTask SetPropertiesAsync(TSession session, + ImmutableDictionary properties, CancellationToken cancellationToken); + + /// + /// Sets the status associated with a session. + /// + /// The session. + /// The status associated with the session. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + ValueTask SetStatusAsync(TSession session, string? status, CancellationToken cancellationToken); + + /// + /// Sets the subject associated with a session. + /// + /// The session. + /// The subject associated with the session. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + ValueTask SetSubjectAsync(TSession session, string? subject, CancellationToken cancellationToken); + + /// + /// Updates an existing session. + /// + /// The session to update. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + ValueTask UpdateAsync(TSession session, CancellationToken cancellationToken); +} diff --git a/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs b/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs index 49892a4f..784e85da 100644 --- a/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs +++ b/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs @@ -20,7 +20,7 @@ public interface IOpenIddictTokenStore where TToken : class /// /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of applications in the database. /// ValueTask CountAsync(CancellationToken cancellationToken); @@ -34,7 +34,7 @@ public interface IOpenIddictTokenStore where TToken : class /// The optional state. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of tokens that match the specified query. /// ValueTask CountAsync( @@ -58,17 +58,13 @@ public interface IOpenIddictTokenStore where TToken : class ValueTask DeleteAsync(TToken token, CancellationToken cancellationToken); /// - /// Retrieves the tokens matching the specified parameters. + /// Retrieves the tokens matching the specified query. /// - /// The subject associated with the token, or not to filter out specific subjects. - /// The client associated with the token, or not to filter out specific clients. - /// The token status, or not to filter out specific token statuses. - /// The token type, or not to filter out specific token types. + /// The query parameters: if a parameter is , it will not be used to filter the results. /// The that can be used to abort the operation. /// The tokens corresponding to the criteria. IAsyncEnumerable FindAsync( - string? subject, string? client, - string? status, string? type, CancellationToken cancellationToken); + (string? Subject, string? ApplicationId, string? Status, string? Type) query, CancellationToken cancellationToken); /// /// Retrieves the list of tokens corresponding to the specified application identifier. @@ -92,19 +88,21 @@ public interface IOpenIddictTokenStore where TToken : class /// The unique identifier associated with the token. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the token corresponding to the unique identifier. /// ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken); /// - /// Retrieves the list of tokens corresponding to the specified reference identifier. - /// Note: the reference identifier may be hashed or encrypted for security reasons. + /// Retrieves a token using its unique reference identifier. /// + /// + /// Note: the reference identifier may be hashed or encrypted for security reasons. + /// /// The reference identifier associated with the tokens. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the tokens corresponding to the specified reference identifier. /// ValueTask FindByReferenceIdAsync(string identifier, CancellationToken cancellationToken); @@ -137,7 +135,7 @@ public interface IOpenIddictTokenStore where TToken : class /// The optional state. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// ValueTask GetAsync( diff --git a/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionFormatter.cs b/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionFormatter.cs index b5f72af2..ddf22736 100644 --- a/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionFormatter.cs +++ b/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionFormatter.cs @@ -206,7 +206,7 @@ public sealed class OpenIddictClientDataProtectionFormatter : IOpenIddictClientD SetArrayProperty(properties, Properties.Scopes, principal.GetScopes()); // Copy the principal and exclude the claim that were mapped to authentication properties. - principal = principal.Clone(claim => claim.Type is not ( + principal = principal.Clone(static claim => claim.Type is not ( Claims.Private.Audience or Claims.Private.CodeVerifier or Claims.Private.CreationDate or diff --git a/src/OpenIddict.Client/IOpenIddictClientHandlerFilter.cs b/src/OpenIddict.Client/IOpenIddictClientHandlerFilter.cs index f11b828f..c006c2db 100644 --- a/src/OpenIddict.Client/IOpenIddictClientHandlerFilter.cs +++ b/src/OpenIddict.Client/IOpenIddictClientHandlerFilter.cs @@ -19,7 +19,7 @@ public interface IOpenIddictClientHandlerFilter where TContext : Ba /// /// The context associated with the event to process. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose boolean result determines whether the handler will be invoked or not. /// ValueTask IsActiveAsync(TContext context); diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.cs index 9fbb27fd..76dd0a4e 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.cs @@ -1752,7 +1752,8 @@ public static partial class OpenIddictClientHandlers { // The following claims MUST be represented as unique strings. Claims.AuthenticationContextReference or Claims.AuthorizedParty or - Claims.Issuer or Claims.Nonce or Claims.Subject + Claims.Issuer or Claims.Nonce or + Claims.SessionId or Claims.Subject => values is [{ ValueType: ClaimValueTypes.String }], // The following claims MUST be represented as unique strings or array of strings. @@ -3488,7 +3489,8 @@ public static partial class OpenIddictClientHandlers { // The following claims MUST be represented as unique strings. Claims.AuthenticationContextReference or Claims.AuthorizedParty or - Claims.Issuer or Claims.Nonce or Claims.Subject + Claims.Issuer or Claims.Nonce or + Claims.SessionId or Claims.Subject => values is [{ ValueType: ClaimValueTypes.String }], // The following claims MUST be represented as unique strings or array of strings. diff --git a/src/OpenIddict.Core/Caches/OpenIddictApplicationCache.cs b/src/OpenIddict.Core/Caches/OpenIddictApplicationCache.cs index 47fb8aea..979b0f80 100644 --- a/src/OpenIddict.Core/Caches/OpenIddictApplicationCache.cs +++ b/src/OpenIddict.Core/Caches/OpenIddictApplicationCache.cs @@ -312,7 +312,7 @@ public sealed class OpenIddictApplicationCache : IOpenIddictApplic /// The application associated with the expiration signal. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns an expiration signal for the specified application. /// private async ValueTask CreateExpirationSignalAsync(TApplication application, CancellationToken cancellationToken) diff --git a/src/OpenIddict.Core/Caches/OpenIddictAuthorizationCache.cs b/src/OpenIddict.Core/Caches/OpenIddictAuthorizationCache.cs index 99bdf6f8..2d8a43c1 100644 --- a/src/OpenIddict.Core/Caches/OpenIddictAuthorizationCache.cs +++ b/src/OpenIddict.Core/Caches/OpenIddictAuthorizationCache.cs @@ -84,13 +84,12 @@ public sealed class OpenIddictAuthorizationCache : IOpenIddictAu /// public async IAsyncEnumerable FindAsync( - string? subject, string? client, - string? status, string? type, - ImmutableArray? scopes, [EnumeratorCancellation] CancellationToken cancellationToken) + (string? Subject, string? ApplicationId, string? Status, + string? Type, ImmutableArray? RequiredScopes) query, [EnumeratorCancellation] CancellationToken cancellationToken) { // Note: this method is only partially cached. - await foreach (var authorization in _store.FindAsync(subject, client, status, type, scopes, cancellationToken)) + await foreach (var authorization in _store.FindAsync(query, cancellationToken)) { await AddAsync(authorization, cancellationToken); @@ -276,7 +275,7 @@ public sealed class OpenIddictAuthorizationCache : IOpenIddictAu /// The authorization associated with the expiration signal. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns an expiration signal for the specified authorization. /// private async ValueTask CreateExpirationSignalAsync(TAuthorization authorization, CancellationToken cancellationToken) diff --git a/src/OpenIddict.Core/Caches/OpenIddictResourceCache.cs b/src/OpenIddict.Core/Caches/OpenIddictResourceCache.cs index 51d7bd4a..f13de8e1 100644 --- a/src/OpenIddict.Core/Caches/OpenIddictResourceCache.cs +++ b/src/OpenIddict.Core/Caches/OpenIddictResourceCache.cs @@ -238,7 +238,7 @@ public sealed class OpenIddictResourceCache : IOpenIddictResourceCach /// The resource associated with the expiration signal. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns an expiration signal for the specified resource. /// private async ValueTask CreateExpirationSignalAsync(TResource resource, CancellationToken cancellationToken) diff --git a/src/OpenIddict.Core/Caches/OpenIddictScopeCache.cs b/src/OpenIddict.Core/Caches/OpenIddictScopeCache.cs index b271eb60..69c0a099 100644 --- a/src/OpenIddict.Core/Caches/OpenIddictScopeCache.cs +++ b/src/OpenIddict.Core/Caches/OpenIddictScopeCache.cs @@ -285,7 +285,7 @@ public sealed class OpenIddictScopeCache : IOpenIddictScopeCache /// The scope associated with the expiration signal. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns an expiration signal for the specified scope. /// private async ValueTask CreateExpirationSignalAsync(TScope scope, CancellationToken cancellationToken) diff --git a/src/OpenIddict.Core/Caches/OpenIddictSessionCache.cs b/src/OpenIddict.Core/Caches/OpenIddictSessionCache.cs new file mode 100644 index 00000000..0ea33997 --- /dev/null +++ b/src/OpenIddict.Core/Caches/OpenIddictSessionCache.cs @@ -0,0 +1,383 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.Collections.Concurrent; +using System.Collections.Immutable; +using System.Runtime.CompilerServices; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; + +namespace OpenIddict.Core; + +/// +/// Provides methods allowing to cache sessions after retrieving them from the store. +/// +/// The type of the Session entity. +public sealed class OpenIddictSessionCache : IOpenIddictSessionCache, IDisposable where TSession : class +{ + private readonly MemoryCache _cache; + private readonly ConcurrentDictionary _signals; + private readonly IOpenIddictSessionStore _store; + + /// + /// Creates a new instance of the class. + /// + /// The options. + /// The store. + public OpenIddictSessionCache( + IOptionsMonitor options, + IOpenIddictSessionStore store) + { + _cache = new MemoryCache(new MemoryCacheOptions + { + SizeLimit = (options ?? throw new ArgumentNullException(nameof(options))).CurrentValue.EntityCacheLimit + }); + + _signals = new ConcurrentDictionary(StringComparer.Ordinal); + _store = store ?? throw new ArgumentNullException(nameof(store)); + } + + /// + public async ValueTask AddAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + _cache.Remove(new + { + Method = nameof(FindByApplicationIdAsync), + Identifier = await _store.GetApplicationIdAsync(session, cancellationToken) + }); + + _cache.Remove(new + { + Method = nameof(FindByAuthorizationIdAsync), + Identifier = await _store.GetAuthorizationIdAsync(session, cancellationToken) + }); + + _cache.Remove(new + { + Method = nameof(FindByIdAsync), + Identifier = await _store.GetIdAsync(session, cancellationToken) + }); + + _cache.Remove(new + { + Method = nameof(FindByLoginIdAsync), + Identifier = await _store.GetLoginIdAsync(session, cancellationToken) + }); + + _cache.Remove(new + { + Method = nameof(FindBySubjectAsync), + Subject = await _store.GetSubjectAsync(session, cancellationToken) + }); + + await CreateEntryAsync(new + { + Method = nameof(FindByIdAsync), + Identifier = await _store.GetIdAsync(session, cancellationToken) + }, session, cancellationToken); + } + + /// + public void Dispose() + { + foreach (var signal in _signals) + { + signal.Value.Dispose(); + } + + _cache.Dispose(); + } + + /// + public async IAsyncEnumerable FindAsync( + (string? Subject, string? LoginId, string? ApplicationId, string? Status) query, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + // Note: this method is only partially cached. + + await foreach (var session in _store.FindAsync(query, cancellationToken)) + { + await AddAsync(session, cancellationToken); + + yield return session; + } + } + + /// + public IAsyncEnumerable FindByApplicationIdAsync(string identifier, CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrEmpty(identifier); + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var parameters = new + { + Method = nameof(FindByApplicationIdAsync), + Identifier = identifier + }; + + if (!_cache.TryGetValue(parameters, out ImmutableArray sessions)) + { + var builder = ImmutableArray.CreateBuilder(); + + await foreach (var session in _store.FindByApplicationIdAsync(identifier, cancellationToken)) + { + builder.Add(session); + + await AddAsync(session, cancellationToken); + } + + sessions = builder.ToImmutable(); + + await CreateEntryAsync(parameters, sessions, cancellationToken); + } + + foreach (var session in sessions) + { + yield return session; + } + } + } + + /// + public IAsyncEnumerable FindByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrEmpty(identifier); + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var parameters = new + { + Method = nameof(FindByAuthorizationIdAsync), + Identifier = identifier + }; + + if (!_cache.TryGetValue(parameters, out ImmutableArray sessions)) + { + var builder = ImmutableArray.CreateBuilder(); + + await foreach (var session in _store.FindByAuthorizationIdAsync(identifier, cancellationToken)) + { + builder.Add(session); + + await AddAsync(session, cancellationToken); + } + + sessions = builder.ToImmutable(); + + await CreateEntryAsync(parameters, sessions, cancellationToken); + } + + foreach (var session in sessions) + { + yield return session; + } + } + } + + /// + public ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrEmpty(identifier); + + var parameters = new + { + Method = nameof(FindByIdAsync), + Identifier = identifier + }; + + if (_cache.TryGetValue(parameters, out TSession? session)) + { + return new(session); + } + + return new(ExecuteAsync()); + + async Task ExecuteAsync() + { + if ((session = await _store.FindByIdAsync(identifier, cancellationToken)) is not null) + { + await AddAsync(session, cancellationToken); + } + + await CreateEntryAsync(parameters, session, cancellationToken); + + return session; + } + } + + /// + public IAsyncEnumerable FindByLoginIdAsync(string identifier, CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrEmpty(identifier); + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var parameters = new + { + Method = nameof(FindByLoginIdAsync), + Identifier = identifier + }; + + if (!_cache.TryGetValue(parameters, out ImmutableArray sessions)) + { + var builder = ImmutableArray.CreateBuilder(); + + await foreach (var session in _store.FindByLoginIdAsync(identifier, cancellationToken)) + { + builder.Add(session); + + await AddAsync(session, cancellationToken); + } + + sessions = builder.ToImmutable(); + + await CreateEntryAsync(parameters, sessions, cancellationToken); + } + + foreach (var session in sessions) + { + yield return session; + } + } + } + + /// + public IAsyncEnumerable FindBySubjectAsync(string subject, CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrEmpty(subject); + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var parameters = new + { + Method = nameof(FindBySubjectAsync), + Identifier = subject + }; + + if (!_cache.TryGetValue(parameters, out ImmutableArray sessions)) + { + var builder = ImmutableArray.CreateBuilder(); + + await foreach (var session in _store.FindBySubjectAsync(subject, cancellationToken)) + { + builder.Add(session); + + await AddAsync(session, cancellationToken); + } + + sessions = builder.ToImmutable(); + + await CreateEntryAsync(parameters, sessions, cancellationToken); + } + + foreach (var session in sessions) + { + yield return session; + } + } + } + + /// + public async ValueTask RemoveAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + var identifier = await _store.GetIdAsync(session, cancellationToken); + if (string.IsNullOrEmpty(identifier)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0196)); + } + + if (_signals.TryRemove(identifier, out CancellationTokenSource? signal)) + { + signal.Cancel(); + signal.Dispose(); + } + } + + /// + /// Creates a cache entry for the specified key. + /// + /// The cache key. + /// The session to store in the cache entry, if applicable. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + private async ValueTask CreateEntryAsync(object key, TSession? session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(key); + + using var entry = _cache.CreateEntry(key); + + if (session is not null) + { + entry.AddExpirationToken(await CreateExpirationSignalAsync(session, cancellationToken) + ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0197))); + } + + entry.Size = 1L; + entry.Value = session; + } + + /// + /// Creates a cache entry for the specified key. + /// + /// The cache key. + /// The sessions to store in the cache entry. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + private async ValueTask CreateEntryAsync(object key, ImmutableArray sessions, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(key); + + using var entry = _cache.CreateEntry(key); + + foreach (var session in sessions) + { + entry.AddExpirationToken(await CreateExpirationSignalAsync(session, cancellationToken) + ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0197))); + } + + entry.Size = sessions.Length; + entry.Value = sessions; + } + + /// + /// Creates an expiration signal allowing to invalidate all the + /// cache entries associated with the specified session. + /// + /// The session associated with the expiration signal. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns an expiration signal for the specified session. + /// + private async ValueTask CreateExpirationSignalAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + var identifier = await _store.GetIdAsync(session, cancellationToken); + if (string.IsNullOrEmpty(identifier)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0204)); + } + + var signal = _signals.GetOrAdd(identifier, _ => new CancellationTokenSource()); + + return new CancellationChangeToken(signal.Token); + } +} diff --git a/src/OpenIddict.Core/Caches/OpenIddictTokenCache.cs b/src/OpenIddict.Core/Caches/OpenIddictTokenCache.cs index f03706fa..43a7b97f 100644 --- a/src/OpenIddict.Core/Caches/OpenIddictTokenCache.cs +++ b/src/OpenIddict.Core/Caches/OpenIddictTokenCache.cs @@ -102,12 +102,12 @@ public sealed class OpenIddictTokenCache : IOpenIddictTokenCache /// public async IAsyncEnumerable FindAsync( - string? subject, string? client, - string? status, string? type, [EnumeratorCancellation] CancellationToken cancellationToken) + (string? Subject, string? ApplicationId, string? Status, string? Type) query, + [EnumeratorCancellation] CancellationToken cancellationToken) { // Note: this method is only partially cached. - await foreach (var token in _store.FindAsync(subject, client, status, type, cancellationToken)) + await foreach (var token in _store.FindAsync(query, cancellationToken)) { await AddAsync(token, cancellationToken); @@ -362,7 +362,7 @@ public sealed class OpenIddictTokenCache : IOpenIddictTokenCache /// The token associated with the expiration signal. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns an expiration signal for the specified token. /// private async ValueTask CreateExpirationSignalAsync(TToken token, CancellationToken cancellationToken) diff --git a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs index ac32c439..fe1b58c3 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs @@ -77,7 +77,7 @@ public class OpenIddictApplicationManager : IOpenIddictApplication /// /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of applications in the database. /// public virtual ValueTask CountAsync(CancellationToken cancellationToken = default) @@ -90,7 +90,7 @@ public class OpenIddictApplicationManager : IOpenIddictApplication /// The query to execute. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of applications that match the specified query. /// public virtual ValueTask CountAsync( @@ -110,7 +110,7 @@ public class OpenIddictApplicationManager : IOpenIddictApplication /// The optional state. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of applications that match the specified query. /// public virtual ValueTask CountAsync( @@ -238,7 +238,7 @@ public class OpenIddictApplicationManager : IOpenIddictApplication /// The application descriptor. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the unique identifier associated with the application. /// public virtual async ValueTask CreateAsync( @@ -291,7 +291,7 @@ public class OpenIddictApplicationManager : IOpenIddictApplication /// The client identifier associated with the application. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the client application corresponding to the identifier. /// public virtual async ValueTask FindByClientIdAsync( @@ -326,7 +326,7 @@ public class OpenIddictApplicationManager : IOpenIddictApplication /// The unique identifier associated with the application. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the client application corresponding to the identifier. /// public virtual async ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken = default) @@ -462,7 +462,7 @@ public class OpenIddictApplicationManager : IOpenIddictApplication /// The query to execute. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// public virtual ValueTask GetAsync( @@ -482,7 +482,7 @@ public class OpenIddictApplicationManager : IOpenIddictApplication /// The optional state. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// public virtual ValueTask GetAsync( @@ -1373,7 +1373,7 @@ public class OpenIddictApplicationManager : IOpenIddictApplication /// The that can be used to abort the operation. /// A that can be used to monitor the asynchronous operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns a boolean indicating whether the client secret was valid. /// public virtual async ValueTask ValidateClientSecretAsync( @@ -1437,7 +1437,7 @@ public class OpenIddictApplicationManager : IOpenIddictApplication /// The that can be used to abort the operation. /// Note: if no client_id parameter is specified in end session requests, this method may not be called. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns a boolean indicating whether the post_logout_redirect_uri was valid. /// public virtual async ValueTask ValidatePostLogoutRedirectUriAsync(TApplication application, @@ -1503,7 +1503,7 @@ public class OpenIddictApplicationManager : IOpenIddictApplication /// The that can be used to abort the operation. /// A that can be used to monitor the asynchronous operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns a boolean indicating whether the client certificate was valid. /// public virtual async ValueTask ValidatePublicKeyInfrastructureTlsClientCertificateAsync( @@ -1602,7 +1602,7 @@ public class OpenIddictApplicationManager : IOpenIddictApplication /// The URI that should be compared to one of the redirect_uri stored in the database. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns a boolean indicating whether the redirect_uri was valid. /// public virtual async ValueTask ValidateRedirectUriAsync(TApplication application, @@ -1670,7 +1670,7 @@ public class OpenIddictApplicationManager : IOpenIddictApplication /// The that can be used to abort the operation. /// A that can be used to monitor the asynchronous operation. /// - /// A that can be used to monitor the asynchronous operation, whose + /// A that can be used to monitor the asynchronous operation, whose /// result returns a boolean indicating whether the self-signed client certificate was valid. /// public virtual async ValueTask ValidateSelfSignedTlsClientCertificateAsync( @@ -1823,7 +1823,7 @@ public class OpenIddictApplicationManager : IOpenIddictApplication /// The value stored in the database, which is usually a hashed representation of the secret. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, whose result returns + /// A that can be used to monitor the asynchronous operation, whose result returns /// a tuple indicating whether the client secret was valid and whether the client secret should be re-hashed. /// protected virtual ValueTask<(bool IsValid, bool IsRehashRequired)> ValidateClientSecretAsync( diff --git a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs index ba57c90d..901a18d1 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs @@ -7,7 +7,6 @@ using System.Collections.Immutable; using System.ComponentModel.DataAnnotations; using System.Runtime.CompilerServices; -using System.Security.Claims; using System.Text; using System.Text.Json; using Microsoft.Extensions.Logging; @@ -71,7 +70,7 @@ public class OpenIddictAuthorizationManager : IOpenIddictAuthori /// /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of authorizations in the database. /// public virtual ValueTask CountAsync(CancellationToken cancellationToken = default) @@ -84,7 +83,7 @@ public class OpenIddictAuthorizationManager : IOpenIddictAuthori /// The query to execute. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of authorizations that match the specified query. /// public virtual ValueTask CountAsync( @@ -104,7 +103,7 @@ public class OpenIddictAuthorizationManager : IOpenIddictAuthori /// The optional state. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of authorizations that match the specified query. /// public virtual ValueTask CountAsync( @@ -176,7 +175,7 @@ public class OpenIddictAuthorizationManager : IOpenIddictAuthori /// The authorization descriptor. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, whose result returns the authorization. + /// A that can be used to monitor the asynchronous operation, whose result returns the authorization. /// public virtual async ValueTask CreateAsync( OpenIddictAuthorizationDescriptor descriptor, CancellationToken cancellationToken = default) @@ -192,60 +191,6 @@ public class OpenIddictAuthorizationManager : IOpenIddictAuthori return authorization; } - /// - /// Creates a new permanent authorization based on the specified parameters. - /// - /// The identity associated with the authorization. - /// The subject associated with the authorization. - /// The client associated with the authorization. - /// The authorization type. - /// The minimal scopes associated with the authorization. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, whose result returns the authorization. - /// - public virtual ValueTask CreateAsync( - ClaimsIdentity identity, string subject, string client, - string type, ImmutableArray scopes, CancellationToken cancellationToken = default) - => CreateAsync(new ClaimsPrincipal(identity ?? throw new ArgumentNullException(nameof(identity))), - subject, client, type, scopes, cancellationToken); - - /// - /// Creates a new permanent authorization based on the specified parameters. - /// - /// The principal associated with the authorization. - /// The subject associated with the authorization. - /// The client associated with the authorization. - /// The authorization type. - /// The minimal scopes associated with the authorization. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, whose result returns the authorization. - /// - public virtual ValueTask CreateAsync( - ClaimsPrincipal principal, string subject, string client, - string type, ImmutableArray scopes, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(principal); - ArgumentException.ThrowIfNullOrEmpty(subject); - ArgumentException.ThrowIfNullOrEmpty(client); - ArgumentException.ThrowIfNullOrEmpty(type); - - var descriptor = new OpenIddictAuthorizationDescriptor - { - ApplicationId = client, - CreationDate = Options.CurrentValue.TimeProvider.GetUtcNow(), - Principal = principal, - Status = Statuses.Valid, - Subject = subject, - Type = type - }; - - descriptor.Scopes.UnionWith(scopes); - - return CreateAsync(descriptor, cancellationToken); - } - /// /// Removes an existing authorization. /// @@ -267,23 +212,18 @@ public class OpenIddictAuthorizationManager : IOpenIddictAuthori } /// - /// Retrieves the authorizations matching the specified parameters. + /// Retrieves the authorizations matching the specified query. /// - /// The subject associated with the authorization, or not to filter out specific subjects. - /// The client associated with the authorization, or not to filter out specific clients. - /// The authorization status, or not to filter out specific authorization statuses. - /// The authorization type, or not to filter out specific authorization types. - /// The minimal scopes associated with the authorization, or not to filter out scopes. + /// The query parameters: if a parameter is , it will not be used to filter the results. /// The that can be used to abort the operation. /// The authorizations corresponding to the criteria. public virtual IAsyncEnumerable FindAsync( - string? subject, string? client, - string? status, string? type, - ImmutableArray? scopes, CancellationToken cancellationToken = default) + (string? Subject, string? ApplicationId, string? Status, + string? Type, ImmutableArray? RequiredScopes) query, CancellationToken cancellationToken = default) { var authorizations = Options.CurrentValue.DisableEntityCaching - ? Store.FindAsync(subject, client, status, type, scopes, cancellationToken) - : Cache.FindAsync(subject, client, status, type, scopes, cancellationToken); + ? Store.FindAsync(query, cancellationToken) + : Cache.FindAsync(query, cancellationToken); if (Options.CurrentValue.DisableAdditionalFiltering) { @@ -300,13 +240,14 @@ public class OpenIddictAuthorizationManager : IOpenIddictAuthori { await foreach (var authorization in authorizations) { - if (!string.IsNullOrEmpty(subject) && - !string.Equals(await Store.GetSubjectAsync(authorization, cancellationToken), subject, StringComparison.Ordinal)) + if (!string.IsNullOrEmpty(query.Subject) && + !string.Equals(await Store.GetSubjectAsync(authorization, cancellationToken), query.Subject, StringComparison.Ordinal)) { continue; } - if (scopes is not null && !await HasScopesAsync(authorization, scopes.Value, cancellationToken)) + if (query.RequiredScopes is { IsDefaultOrEmpty: false } scopes && + !await HasScopesAsync(authorization, scopes, cancellationToken)) { continue; } @@ -360,7 +301,7 @@ public class OpenIddictAuthorizationManager : IOpenIddictAuthori /// The unique identifier associated with the authorization. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the authorization corresponding to the identifier. /// public virtual async ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken = default) @@ -450,7 +391,7 @@ public class OpenIddictAuthorizationManager : IOpenIddictAuthori /// The query to execute. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// public virtual ValueTask GetAsync( @@ -470,7 +411,7 @@ public class OpenIddictAuthorizationManager : IOpenIddictAuthori /// The optional state. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// public virtual ValueTask GetAsync( @@ -977,14 +918,6 @@ public class OpenIddictAuthorizationManager : IOpenIddictAuthori ValueTask IOpenIddictAuthorizationManager.CountAsync(Func, TState, IQueryable> query, TState state, CancellationToken cancellationToken) => CountAsync(query, state, cancellationToken); - /// - async ValueTask IOpenIddictAuthorizationManager.CreateAsync(ClaimsIdentity identity, string subject, string client, string type, ImmutableArray scopes, CancellationToken cancellationToken) - => await CreateAsync(identity, subject, client, type, scopes, cancellationToken); - - /// - async ValueTask IOpenIddictAuthorizationManager.CreateAsync(ClaimsPrincipal principal, string subject, string client, string type, ImmutableArray scopes, CancellationToken cancellationToken) - => await CreateAsync(principal, subject, client, type, scopes, cancellationToken); - /// async ValueTask IOpenIddictAuthorizationManager.CreateAsync(OpenIddictAuthorizationDescriptor descriptor, CancellationToken cancellationToken) => await CreateAsync(descriptor, cancellationToken); @@ -998,8 +931,8 @@ public class OpenIddictAuthorizationManager : IOpenIddictAuthori => DeleteAsync((TAuthorization) authorization, cancellationToken); /// - IAsyncEnumerable IOpenIddictAuthorizationManager.FindAsync(string? subject, string? client, string? status, string? type, ImmutableArray? scopes, CancellationToken cancellationToken) - => FindAsync(subject, client, status, type, scopes, cancellationToken); + IAsyncEnumerable IOpenIddictAuthorizationManager.FindAsync((string? Subject, string? ApplicationId, string? Status, string? Type, ImmutableArray? RequiredScopes) query, CancellationToken cancellationToken) + => FindAsync(query, cancellationToken); /// IAsyncEnumerable IOpenIddictAuthorizationManager.FindByApplicationIdAsync(string identifier, CancellationToken cancellationToken) diff --git a/src/OpenIddict.Core/Managers/OpenIddictResourceManager.cs b/src/OpenIddict.Core/Managers/OpenIddictResourceManager.cs index 2e6b4686..c4cc73a2 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictResourceManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictResourceManager.cs @@ -70,7 +70,7 @@ public class OpenIddictResourceManager : IOpenIddictResourceManager w /// /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of resources in the database. /// public virtual ValueTask CountAsync(CancellationToken cancellationToken = default) @@ -83,7 +83,7 @@ public class OpenIddictResourceManager : IOpenIddictResourceManager w /// The query to execute. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of resources that match the specified query. /// public virtual ValueTask CountAsync( @@ -103,7 +103,7 @@ public class OpenIddictResourceManager : IOpenIddictResourceManager w /// The optional state. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of resources that match the specified query. /// public virtual ValueTask CountAsync( @@ -169,7 +169,7 @@ public class OpenIddictResourceManager : IOpenIddictResourceManager w /// The resource descriptor. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, whose result returns the resource. + /// A that can be used to monitor the asynchronous operation, whose result returns the resource. /// public virtual async ValueTask CreateAsync( OpenIddictResourceDescriptor descriptor, CancellationToken cancellationToken = default) @@ -211,7 +211,7 @@ public class OpenIddictResourceManager : IOpenIddictResourceManager w /// The unique identifier associated with the resource. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the resource corresponding to the identifier. /// public virtual async ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken = default) @@ -245,7 +245,7 @@ public class OpenIddictResourceManager : IOpenIddictResourceManager w /// The name associated with the resource. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the resource corresponding to the specified name. /// public virtual async ValueTask FindByNameAsync(string name, CancellationToken cancellationToken = default) @@ -323,7 +323,7 @@ public class OpenIddictResourceManager : IOpenIddictResourceManager w /// The query to execute. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// public virtual ValueTask GetAsync( @@ -343,7 +343,7 @@ public class OpenIddictResourceManager : IOpenIddictResourceManager w /// The optional state. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// public virtual ValueTask GetAsync( diff --git a/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs b/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs index cb70ea22..bf541486 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs @@ -70,7 +70,7 @@ public class OpenIddictScopeManager : IOpenIddictScopeManager where TSco /// /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of scopes in the database. /// public virtual ValueTask CountAsync(CancellationToken cancellationToken = default) @@ -83,7 +83,7 @@ public class OpenIddictScopeManager : IOpenIddictScopeManager where TSco /// The query to execute. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of scopes that match the specified query. /// public virtual ValueTask CountAsync( @@ -103,7 +103,7 @@ public class OpenIddictScopeManager : IOpenIddictScopeManager where TSco /// The optional state. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of scopes that match the specified query. /// public virtual ValueTask CountAsync( @@ -169,7 +169,7 @@ public class OpenIddictScopeManager : IOpenIddictScopeManager where TSco /// The scope descriptor. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, whose result returns the scope. + /// A that can be used to monitor the asynchronous operation, whose result returns the scope. /// public virtual async ValueTask CreateAsync( OpenIddictScopeDescriptor descriptor, CancellationToken cancellationToken = default) @@ -211,7 +211,7 @@ public class OpenIddictScopeManager : IOpenIddictScopeManager where TSco /// The unique identifier associated with the scope. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the scope corresponding to the identifier. /// public virtual async ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken = default) @@ -245,7 +245,7 @@ public class OpenIddictScopeManager : IOpenIddictScopeManager where TSco /// The name associated with the scope. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the scope corresponding to the specified name. /// public virtual async ValueTask FindByNameAsync(string name, CancellationToken cancellationToken = default) @@ -362,7 +362,7 @@ public class OpenIddictScopeManager : IOpenIddictScopeManager where TSco /// The query to execute. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// public virtual ValueTask GetAsync( @@ -382,7 +382,7 @@ public class OpenIddictScopeManager : IOpenIddictScopeManager where TSco /// The optional state. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// public virtual ValueTask GetAsync( diff --git a/src/OpenIddict.Core/Managers/OpenIddictSessionManager.cs b/src/OpenIddict.Core/Managers/OpenIddictSessionManager.cs new file mode 100644 index 00000000..f8d02ef5 --- /dev/null +++ b/src/OpenIddict.Core/Managers/OpenIddictSessionManager.cs @@ -0,0 +1,919 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.Collections.Immutable; +using System.ComponentModel.DataAnnotations; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using ValidationException = OpenIddict.Abstractions.OpenIddictExceptions.ValidationException; + +namespace OpenIddict.Core; + +/// +/// Provides methods allowing to manage the sessions stored in the store. +/// +/// +/// Applications that do not want to depend on a specific entity type can use the non-generic +/// instead, for which the actual entity type is resolved at runtime. +/// +/// The type of the session entity. +public class OpenIddictSessionManager : IOpenIddictSessionManager where TSession : class +{ + /// + /// Creates a new instance of the class. + /// + /// The cache. + /// The logger. + /// The options. + /// The store. + public OpenIddictSessionManager( + IOpenIddictSessionCache cache, + ILogger> logger, + IOptionsMonitor options, + IOpenIddictSessionStore store) + { + Cache = cache ?? throw new ArgumentNullException(nameof(cache)); + Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + Options = options ?? throw new ArgumentNullException(nameof(options)); + Store = store ?? throw new ArgumentNullException(nameof(store)); + } + + /// + /// Gets the cache associated with the current manager. + /// + protected IOpenIddictSessionCache Cache { get; } + + /// + /// Gets the logger associated with the current manager. + /// + protected ILogger Logger { get; } + + /// + /// Gets the options associated with the current manager. + /// + protected IOptionsMonitor Options { get; } + + /// + /// Gets the store associated with the current manager. + /// + protected IOpenIddictSessionStore Store { get; } + + /// + /// Determines the number of sessions that exist in the database. + /// + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the number of sessions in the database. + /// + public virtual ValueTask CountAsync(CancellationToken cancellationToken = default) + => Store.CountAsync(cancellationToken); + + /// + /// Determines the number of sessions that match the specified query. + /// + /// The result type. + /// The query to execute. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the number of sessions that match the specified query. + /// + public virtual ValueTask CountAsync( + Func, IQueryable> query, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(query); + + return CountAsync(static (sessions, query) => query(sessions), query, cancellationToken); + } + + /// + /// Determines the number of sessions that match the specified query. + /// + /// The state type. + /// The result type. + /// The query to execute. + /// The optional state. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the number of sessions that match the specified query. + /// + public virtual ValueTask CountAsync( + Func, TState, IQueryable> query, + TState state, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(query); + + return Store.CountAsync(query, state, cancellationToken); + } + + /// + /// Creates a new session. + /// + /// The session to create. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual async ValueTask CreateAsync(TSession session, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(session); + + // If no status was explicitly specified, assume that the session is valid. + if (string.IsNullOrEmpty(await Store.GetStatusAsync(session, cancellationToken))) + { + await Store.SetStatusAsync(session, Statuses.Valid, cancellationToken); + } + + var results = await GetValidationResultsAsync(session, cancellationToken); + if (results.Any(result => result != ValidationResult.Success)) + { + var builder = new StringBuilder(); + builder.AppendLine(SR.GetResourceString(SR.ID0207)); + builder.AppendLine(); + + foreach (var result in results) + { + builder.AppendLine(result.ErrorMessage); + } + + throw new ValidationException(builder.ToString(), results); + } + + await Store.CreateAsync(session, cancellationToken); + + if (!Options.CurrentValue.DisableEntityCaching) + { + await Cache.AddAsync(session, cancellationToken); + } + + async Task> GetValidationResultsAsync( + TSession session, CancellationToken cancellationToken) + { + var builder = ImmutableArray.CreateBuilder(); + + await foreach (var result in ValidateAsync(session, cancellationToken)) + { + builder.Add(result); + } + + return builder.ToImmutable(); + } + } + + /// + /// Creates a new session based on the specified descriptor. + /// + /// The session descriptor. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, whose result returns the session. + /// + public virtual async ValueTask CreateAsync( + OpenIddictSessionDescriptor descriptor, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(descriptor); + + var session = await Store.InstantiateAsync(cancellationToken) + ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0208)); + + await PopulateAsync(session, descriptor, cancellationToken); + await CreateAsync(session, cancellationToken); + + return session; + } + + /// + /// Removes an existing session. + /// + /// The session to delete. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual async ValueTask DeleteAsync(TSession session, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(session); + + if (!Options.CurrentValue.DisableEntityCaching) + { + await Cache.RemoveAsync(session, cancellationToken); + } + + await Store.DeleteAsync(session, cancellationToken); + } + + /// + /// Retrieves the sessions matching the specified query. + /// + /// The query parameters: if a parameter is , it will not be used to filter the results. + /// 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, + CancellationToken cancellationToken = default) + { + var sessions = Options.CurrentValue.DisableEntityCaching + ? Store.FindAsync(query, cancellationToken) + : Cache.FindAsync(query, cancellationToken); + + if (Options.CurrentValue.DisableAdditionalFiltering) + { + return sessions; + } + + // SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default. + // To ensure a case-sensitive comparison is enforced independently of the database/table/query collation + // used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here. + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var session in sessions) + { + if (string.IsNullOrEmpty(query.Subject) || + string.Equals(await Store.GetSubjectAsync(session, cancellationToken), query.Subject, StringComparison.Ordinal)) + { + yield return session; + } + } + } + } + + /// + /// Retrieves the list of sessions corresponding to the specified application identifier. + /// + /// The application identifier associated with the sessions. + /// The that can be used to abort the operation. + /// The sessions corresponding to the specified application. + public virtual IAsyncEnumerable FindByApplicationIdAsync( + string identifier, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(identifier); + + var sessions = Options.CurrentValue.DisableEntityCaching + ? Store.FindByApplicationIdAsync(identifier, cancellationToken) + : Cache.FindByApplicationIdAsync(identifier, cancellationToken); + + if (Options.CurrentValue.DisableAdditionalFiltering) + { + return sessions; + } + + // SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default. + // To ensure a case-sensitive comparison is enforced independently of the database/table/query collation + // used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here. + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var session in sessions) + { + if (string.Equals(await Store.GetApplicationIdAsync(session, cancellationToken), identifier, StringComparison.Ordinal)) + { + yield return session; + } + } + } + } + + /// + /// Retrieves the list of sessions corresponding to the specified authorization identifier. + /// + /// The authorization identifier associated with the sessions. + /// The that can be used to abort the operation. + /// The sessions corresponding to the specified authorization. + public virtual IAsyncEnumerable FindByAuthorizationIdAsync( + string identifier, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(identifier); + + var sessions = Options.CurrentValue.DisableEntityCaching + ? Store.FindByAuthorizationIdAsync(identifier, cancellationToken) + : Cache.FindByAuthorizationIdAsync(identifier, cancellationToken); + + if (Options.CurrentValue.DisableAdditionalFiltering) + { + return sessions; + } + + // SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default. + // To ensure a case-sensitive comparison is enforced independently of the database/table/query collation + // used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here. + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var session in sessions) + { + if (string.Equals(await Store.GetAuthorizationIdAsync(session, cancellationToken), identifier, StringComparison.Ordinal)) + { + yield return session; + } + } + } + } + + /// + /// Retrieves a session using its unique identifier. + /// + /// The unique identifier associated with the session. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the session corresponding to the identifier. + /// + public virtual async ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(identifier); + + var session = Options.CurrentValue.DisableEntityCaching + ? await Store.FindByIdAsync(identifier, cancellationToken) + : await Cache.FindByIdAsync(identifier, cancellationToken); + + if (session is null) + { + return null; + } + + // SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default. + // To ensure a case-sensitive comparison is enforced independently of the database/table/query collation + // used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here. + if (!Options.CurrentValue.DisableAdditionalFiltering && + !string.Equals(await Store.GetIdAsync(session, cancellationToken), identifier, StringComparison.Ordinal)) + { + return null; + } + + return session; + } + + /// + /// Retrieves the list of sessions corresponding to the specified login identifier. + /// + /// The login identifier associated with the sessions. + /// The that can be used to abort the operation. + /// The sessions corresponding to the specified login identifier. + public virtual IAsyncEnumerable FindByLoginIdAsync(string identifier, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(identifier); + + var sessions = Options.CurrentValue.DisableEntityCaching + ? Store.FindByLoginIdAsync(identifier, cancellationToken) + : Cache.FindByLoginIdAsync(identifier, cancellationToken); + + if (Options.CurrentValue.DisableAdditionalFiltering) + { + return sessions; + } + + // SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default. + // To ensure a case-sensitive comparison is enforced independently of the database/table/query collation + // used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here. + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var session in sessions) + { + if (string.Equals(await Store.GetLoginIdAsync(session, cancellationToken), identifier, StringComparison.Ordinal)) + { + yield return session; + } + } + } + } + + /// + /// Retrieves the list of sessions corresponding to the specified subject. + /// + /// The subject associated with the sessions. + /// The that can be used to abort the operation. + /// The sessions corresponding to the specified subject. + public virtual IAsyncEnumerable FindBySubjectAsync( + string subject, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(subject); + + var sessions = Options.CurrentValue.DisableEntityCaching + ? Store.FindBySubjectAsync(subject, cancellationToken) + : Cache.FindBySubjectAsync(subject, cancellationToken); + + if (Options.CurrentValue.DisableAdditionalFiltering) + { + return sessions; + } + + // SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default. + // To ensure a case-sensitive comparison is enforced independently of the database/table/query collation + // used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here. + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var session in sessions) + { + if (string.Equals(await Store.GetSubjectAsync(session, cancellationToken), subject, StringComparison.Ordinal)) + { + yield return session; + } + } + } + } + + /// + /// Retrieves the optional application identifier associated with a session. + /// + /// The session. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the application identifier associated with the session. + /// + public virtual ValueTask GetApplicationIdAsync(TSession session, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(session); + + return Store.GetApplicationIdAsync(session, cancellationToken); + } + + /// + /// Executes the specified query and returns the first element. + /// + /// The result type. + /// The query to execute. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the first element returned when executing the query. + /// + public virtual ValueTask GetAsync( + Func, IQueryable> query, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(query); + + return GetAsync(static (sessions, query) => query(sessions), query, cancellationToken); + } + + /// + /// Executes the specified query and returns the first element. + /// + /// The state type. + /// The result type. + /// The query to execute. + /// The optional state. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the first element returned when executing the query. + /// + public virtual ValueTask GetAsync( + Func, TState, IQueryable> query, + TState state, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(query); + + return Store.GetAsync(query, state, cancellationToken); + } + + /// + /// Retrieves the optional authorization identifier associated with a session. + /// + /// The session. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the authorization identifier associated with the session. + /// + public virtual ValueTask GetAuthorizationIdAsync(TSession session, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(session); + + return Store.GetAuthorizationIdAsync(session, cancellationToken); + } + + /// + /// Retrieves the creation date associated with a session. + /// + /// The session. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the creation date associated with the specified session. + /// + public virtual ValueTask GetCreationDateAsync(TSession session, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(session); + + return Store.GetCreationDateAsync(session, cancellationToken); + } + + /// + /// Retrieves the unique identifier associated with a session. + /// + /// The session. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the unique identifier associated with the session. + /// + public virtual ValueTask GetIdAsync(TSession session, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(session); + + return Store.GetIdAsync(session, cancellationToken); + } + + /// + /// Retrieves the login identifier associated with a session. + /// + /// The session. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the login identifier associated with the specified session. + /// + public virtual ValueTask GetLoginIdAsync(TSession session, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(session); + + return Store.GetLoginIdAsync(session, cancellationToken); + } + + /// + /// Retrieves the additional properties associated with a session. + /// + /// The session. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the additional properties associated with the session. + /// + public virtual ValueTask> GetPropertiesAsync( + TSession session, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(session); + + return Store.GetPropertiesAsync(session, cancellationToken); + } + + /// + /// Retrieves the status associated with a session. + /// + /// The session. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the status associated with the specified session. + /// + public virtual ValueTask GetStatusAsync(TSession session, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(session); + + return Store.GetStatusAsync(session, cancellationToken); + } + + /// + /// Retrieves the subject associated with a session. + /// + /// The session. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the subject associated with the specified session. + /// + public virtual ValueTask GetSubjectAsync(TSession session, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(session); + + return Store.GetSubjectAsync(session, cancellationToken); + } + + /// + /// Executes the specified query and returns all the corresponding elements. + /// + /// The number of results to return. + /// The number of results to skip. + /// The that can be used to abort the operation. + /// All the elements returned when executing the specified query. + public virtual IAsyncEnumerable ListAsync( + int? count = null, int? offset = null, CancellationToken cancellationToken = default) + => Store.ListAsync(count, offset, cancellationToken); + + /// + /// Executes the specified query and returns all the corresponding elements. + /// + /// The result type. + /// The query to execute. + /// The that can be used to abort the operation. + /// All the elements returned when executing the specified query. + public virtual IAsyncEnumerable ListAsync( + Func, IQueryable> query, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(query); + + return ListAsync(static (sessions, query) => query(sessions), query, cancellationToken); + } + + /// + /// Executes the specified query and returns all the corresponding elements. + /// + /// The state type. + /// The result type. + /// The query to execute. + /// The optional state. + /// The that can be used to abort the operation. + /// All the elements returned when executing the specified query. + public virtual IAsyncEnumerable ListAsync( + Func, TState, IQueryable> query, + TState state, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(query); + + return Store.ListAsync(query, state, cancellationToken); + } + + /// + /// Populates the session using the specified descriptor. + /// + /// The session. + /// The descriptor. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual async ValueTask PopulateAsync(TSession session, + OpenIddictSessionDescriptor descriptor, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(session); + ArgumentNullException.ThrowIfNull(descriptor); + + await Store.SetApplicationIdAsync(session, descriptor.ApplicationId, cancellationToken); + await Store.SetAuthorizationIdAsync(session, descriptor.AuthorizationId, cancellationToken); + await Store.SetCreationDateAsync(session, descriptor.CreationDate, cancellationToken); + await Store.SetLoginIdAsync(session, descriptor.LoginId, cancellationToken); + await Store.SetPropertiesAsync(session, [.. descriptor.Properties], cancellationToken); + await Store.SetStatusAsync(session, descriptor.Status, cancellationToken); + await Store.SetSubjectAsync(session, descriptor.Subject, cancellationToken); + } + + /// + /// Populates the specified descriptor using the properties exposed by the session. + /// + /// The descriptor. + /// The session. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual async ValueTask PopulateAsync( + OpenIddictSessionDescriptor descriptor, + TSession session, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(descriptor); + ArgumentNullException.ThrowIfNull(session); + + descriptor.ApplicationId = await Store.GetApplicationIdAsync(session, cancellationToken); + descriptor.AuthorizationId = await Store.GetAuthorizationIdAsync(session, cancellationToken); + descriptor.CreationDate = await Store.GetCreationDateAsync(session, cancellationToken); + descriptor.LoginId = await Store.GetLoginIdAsync(session, cancellationToken); + descriptor.Status = await Store.GetStatusAsync(session, cancellationToken); + descriptor.Subject = await Store.GetSubjectAsync(session, cancellationToken); + + descriptor.Properties.Clear(); + foreach (var pair in await Store.GetPropertiesAsync(session, cancellationToken)) + { + descriptor.Properties.Add(pair.Key, pair.Value); + } + } + + /// + /// Updates an existing session. + /// + /// The session to update. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual async ValueTask UpdateAsync(TSession session, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(session); + + var results = await GetValidationResultsAsync(session, cancellationToken); + if (results.Any(result => result != ValidationResult.Success)) + { + var builder = new StringBuilder(); + builder.AppendLine(SR.GetResourceString(SR.ID0215)); + builder.AppendLine(); + + foreach (var result in results) + { + builder.AppendLine(result.ErrorMessage); + } + + throw new ValidationException(builder.ToString(), results); + } + + if (!Options.CurrentValue.DisableEntityCaching) + { + await Cache.RemoveAsync(session, cancellationToken); + } + + await Store.UpdateAsync(session, cancellationToken); + + if (!Options.CurrentValue.DisableEntityCaching) + { + await Cache.AddAsync(session, cancellationToken); + } + + async Task> GetValidationResultsAsync( + TSession session, CancellationToken cancellationToken) + { + var builder = ImmutableArray.CreateBuilder(); + + await foreach (var result in ValidateAsync(session, cancellationToken)) + { + builder.Add(result); + } + + return builder.ToImmutable(); + } + } + + /// + /// Updates an existing session. + /// + /// The session to update. + /// The descriptor used to update the session. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual async ValueTask UpdateAsync(TSession session, + OpenIddictSessionDescriptor descriptor, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(session); + ArgumentNullException.ThrowIfNull(descriptor); + + await PopulateAsync(session, descriptor, cancellationToken); + await UpdateAsync(session, cancellationToken); + } + + /// + /// Validates the session to ensure it's in a consistent state. + /// + /// The session. + /// The that can be used to abort the operation. + /// The validation error encountered when validating the session. + public virtual IAsyncEnumerable ValidateAsync(TSession session, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(session); + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(await Store.GetStatusAsync(session, cancellationToken))) + { + yield return new ValidationResult(SR.GetResourceString(SR.ID2038)); + } + + if (string.IsNullOrEmpty(await Store.GetLoginIdAsync(session, cancellationToken))) + { + yield return new ValidationResult(SR.GetResourceString(SR.ID2209)); + } + } + } + + /// + ValueTask IOpenIddictSessionManager.CountAsync(CancellationToken cancellationToken) + => CountAsync(cancellationToken); + + /// + ValueTask IOpenIddictSessionManager.CountAsync(Func, IQueryable> query, CancellationToken cancellationToken) + => CountAsync(query, cancellationToken); + + /// + ValueTask IOpenIddictSessionManager.CountAsync(Func, TState, IQueryable> query, TState state, CancellationToken cancellationToken) + => CountAsync(query, state, cancellationToken); + + /// + async ValueTask IOpenIddictSessionManager.CreateAsync(OpenIddictSessionDescriptor descriptor, CancellationToken cancellationToken) + => await CreateAsync(descriptor, cancellationToken); + + /// + ValueTask IOpenIddictSessionManager.CreateAsync(object session, CancellationToken cancellationToken) + => CreateAsync((TSession) session, cancellationToken); + + /// + ValueTask IOpenIddictSessionManager.DeleteAsync(object session, CancellationToken cancellationToken) + => DeleteAsync((TSession) session, cancellationToken); + + /// + IAsyncEnumerable IOpenIddictSessionManager.FindAsync((string? Subject, string? LoginId, string? ApplicationId, string? Status) query, CancellationToken cancellationToken) + => FindAsync(query, cancellationToken); + + /// + IAsyncEnumerable IOpenIddictSessionManager.FindByApplicationIdAsync(string identifier, CancellationToken cancellationToken) + => FindByApplicationIdAsync(identifier, cancellationToken); + + /// + IAsyncEnumerable IOpenIddictSessionManager.FindByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken) + => FindByAuthorizationIdAsync(identifier, cancellationToken); + + /// + async ValueTask IOpenIddictSessionManager.FindByIdAsync(string identifier, CancellationToken cancellationToken) + => await FindByIdAsync(identifier, cancellationToken); + + /// + IAsyncEnumerable IOpenIddictSessionManager.FindByLoginIdAsync(string identifier, CancellationToken cancellationToken) + => FindByLoginIdAsync(identifier, cancellationToken); + + /// + IAsyncEnumerable IOpenIddictSessionManager.FindBySubjectAsync(string subject, CancellationToken cancellationToken) + => FindBySubjectAsync(subject, cancellationToken); + + /// + ValueTask IOpenIddictSessionManager.GetApplicationIdAsync(object session, CancellationToken cancellationToken) + => GetApplicationIdAsync((TSession) session, cancellationToken); + + /// + ValueTask IOpenIddictSessionManager.GetAsync(Func, IQueryable> query, CancellationToken cancellationToken) where TResult : default + => GetAsync(query, cancellationToken); + + /// + ValueTask IOpenIddictSessionManager.GetAsync(Func, TState, IQueryable> query, TState state, CancellationToken cancellationToken) where TResult : default + => GetAsync(query, state, cancellationToken); + + /// + ValueTask IOpenIddictSessionManager.GetAuthorizationIdAsync(object session, CancellationToken cancellationToken) + => GetAuthorizationIdAsync((TSession) session, cancellationToken); + + /// + ValueTask IOpenIddictSessionManager.GetCreationDateAsync(object session, CancellationToken cancellationToken) + => GetCreationDateAsync((TSession) session, cancellationToken); + + /// + ValueTask IOpenIddictSessionManager.GetIdAsync(object session, CancellationToken cancellationToken) + => GetIdAsync((TSession) session, cancellationToken); + + /// + ValueTask IOpenIddictSessionManager.GetLoginIdAsync(object session, CancellationToken cancellationToken) + => GetLoginIdAsync((TSession) session, cancellationToken); + + /// + ValueTask> IOpenIddictSessionManager.GetPropertiesAsync(object session, CancellationToken cancellationToken) + => GetPropertiesAsync((TSession) session, cancellationToken); + + /// + ValueTask IOpenIddictSessionManager.GetStatusAsync(object session, CancellationToken cancellationToken) + => GetStatusAsync((TSession) session, cancellationToken); + + /// + ValueTask IOpenIddictSessionManager.GetSubjectAsync(object session, CancellationToken cancellationToken) + => GetSubjectAsync((TSession) session, cancellationToken); + + /// + IAsyncEnumerable IOpenIddictSessionManager.ListAsync(int? count, int? offset, CancellationToken cancellationToken) + => ListAsync(count, offset, cancellationToken); + + /// + IAsyncEnumerable IOpenIddictSessionManager.ListAsync(Func, IQueryable> query, CancellationToken cancellationToken) + => ListAsync(query, cancellationToken); + + /// + IAsyncEnumerable IOpenIddictSessionManager.ListAsync(Func, TState, IQueryable> query, TState state, CancellationToken cancellationToken) + => ListAsync(query, state, cancellationToken); + + /// + ValueTask IOpenIddictSessionManager.PopulateAsync(OpenIddictSessionDescriptor descriptor, object session, CancellationToken cancellationToken) + => PopulateAsync(descriptor, (TSession) session, cancellationToken); + + /// + ValueTask IOpenIddictSessionManager.PopulateAsync(object session, OpenIddictSessionDescriptor descriptor, CancellationToken cancellationToken) + => PopulateAsync((TSession) session, descriptor, cancellationToken); + + /// + ValueTask IOpenIddictSessionManager.UpdateAsync(object session, CancellationToken cancellationToken) + => UpdateAsync((TSession) session, cancellationToken); + + /// + ValueTask IOpenIddictSessionManager.UpdateAsync(object session, OpenIddictSessionDescriptor descriptor, CancellationToken cancellationToken) + => UpdateAsync((TSession) session, descriptor, cancellationToken); + + /// + IAsyncEnumerable IOpenIddictSessionManager.ValidateAsync(object session, CancellationToken cancellationToken) + => ValidateAsync((TSession) session, cancellationToken); +} diff --git a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs index 150c0927..eec8b69b 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs @@ -71,7 +71,7 @@ public class OpenIddictTokenManager : IOpenIddictTokenManager where TTok /// /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of tokens in the database. /// public virtual ValueTask CountAsync(CancellationToken cancellationToken = default) @@ -84,7 +84,7 @@ public class OpenIddictTokenManager : IOpenIddictTokenManager where TTok /// The query to execute. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of tokens that match the specified query. /// public virtual ValueTask CountAsync( @@ -104,7 +104,7 @@ public class OpenIddictTokenManager : IOpenIddictTokenManager where TTok /// The optional state. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of tokens that match the specified query. /// public virtual ValueTask CountAsync( @@ -184,7 +184,7 @@ public class OpenIddictTokenManager : IOpenIddictTokenManager where TTok /// The token descriptor. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, whose result returns the token. + /// A that can be used to monitor the asynchronous operation, whose result returns the token. /// public virtual async ValueTask CreateAsync( OpenIddictTokenDescriptor descriptor, CancellationToken cancellationToken = default) @@ -221,21 +221,18 @@ public class OpenIddictTokenManager : IOpenIddictTokenManager where TTok } /// - /// Retrieves the tokens matching the specified parameters. + /// Retrieves the tokens matching the specified query. /// - /// The subject associated with the token, or not to filter out specific subjects. - /// The client associated with the token, or not to filter out specific clients. - /// The token status, or not to filter out specific token statuses. - /// The token type, or not to filter out specific token types. + /// The query parameters: if a parameter is , it will not be used to filter the results. /// The that can be used to abort the operation. - /// Tokens corresponding to the criteria. + /// The tokens corresponding to the criteria. public virtual IAsyncEnumerable FindAsync( - string? subject, string? client, - string? status, string? type, CancellationToken cancellationToken = default) + (string? Subject, string? ApplicationId, string? Status, string? Type) query, + CancellationToken cancellationToken = default) { var tokens = Options.CurrentValue.DisableEntityCaching - ? Store.FindAsync(subject, client, status, type, cancellationToken) - : Cache.FindAsync(subject, client, status, type, cancellationToken); + ? Store.FindAsync(query, cancellationToken) + : Cache.FindAsync(query, cancellationToken); if (Options.CurrentValue.DisableAdditionalFiltering) { @@ -252,8 +249,8 @@ public class OpenIddictTokenManager : IOpenIddictTokenManager where TTok { await foreach (var token in tokens) { - if (string.IsNullOrEmpty(subject) || - string.Equals(await Store.GetSubjectAsync(token, cancellationToken), subject, StringComparison.Ordinal)) + if (string.IsNullOrEmpty(query.Subject) || + string.Equals(await Store.GetSubjectAsync(token, cancellationToken), query.Subject, StringComparison.Ordinal)) { yield return token; } @@ -343,7 +340,7 @@ public class OpenIddictTokenManager : IOpenIddictTokenManager where TTok /// The unique identifier associated with the token. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the token corresponding to the unique identifier. /// public virtual async ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken = default) @@ -372,13 +369,15 @@ public class OpenIddictTokenManager : IOpenIddictTokenManager where TTok } /// - /// Retrieves the list of tokens corresponding to the specified reference identifier. - /// Note: the reference identifier may be hashed or encrypted for security reasons. + /// Retrieves a token using its unique reference identifier. /// + /// + /// Note: the reference identifier may be hashed or encrypted for security reasons. + /// /// The reference identifier associated with the tokens. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the tokens corresponding to the specified reference identifier. /// public virtual async ValueTask FindByReferenceIdAsync(string identifier, CancellationToken cancellationToken = default) @@ -470,7 +469,7 @@ public class OpenIddictTokenManager : IOpenIddictTokenManager where TTok /// The query to execute. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// public virtual ValueTask GetAsync( @@ -490,7 +489,7 @@ public class OpenIddictTokenManager : IOpenIddictTokenManager where TTok /// The optional state. /// The that can be used to abort the operation. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// public virtual ValueTask GetAsync( @@ -1151,8 +1150,7 @@ public class OpenIddictTokenManager : IOpenIddictTokenManager where TTok } } - var type = await Store.GetTypeAsync(token, cancellationToken); - if (string.IsNullOrEmpty(type)) + if (string.IsNullOrEmpty(await Store.GetTypeAsync(token, cancellationToken))) { yield return new ValidationResult(SR.GetResourceString(SR.ID2086)); } @@ -1207,8 +1205,8 @@ public class OpenIddictTokenManager : IOpenIddictTokenManager where TTok => DeleteAsync((TToken) token, cancellationToken); /// - IAsyncEnumerable IOpenIddictTokenManager.FindAsync(string? subject, string? client, string? status, string? type, CancellationToken cancellationToken) - => FindAsync(subject, client, status, type, cancellationToken); + IAsyncEnumerable IOpenIddictTokenManager.FindAsync((string? Subject, string? ApplicationId, string? Status, string? Type) query, CancellationToken cancellationToken) + => FindAsync(query, cancellationToken); /// IAsyncEnumerable IOpenIddictTokenManager.FindByApplicationIdAsync(string identifier, CancellationToken cancellationToken) diff --git a/src/OpenIddict.Core/OpenIddictCoreBuilder.cs b/src/OpenIddict.Core/OpenIddictCoreBuilder.cs index 0d9f2896..c0b49cf6 100644 --- a/src/OpenIddict.Core/OpenIddictCoreBuilder.cs +++ b/src/OpenIddict.Core/OpenIddictCoreBuilder.cs @@ -377,6 +377,86 @@ public sealed class OpenIddictCoreBuilder return this; } + /// + /// Replaces the session manager by the specified type. + /// + /// The type of the entity. + /// The type of the manager. + /// The instance. + public OpenIddictCoreBuilder ReplaceSessionManager< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TManager>() + where TSession : class + where TManager : OpenIddictSessionManager + { + Services.Replace(ServiceDescriptor.Scoped, TManager>()); + + return this; + } + + /// + /// Replaces the session manager by the specified type. + /// + /// + /// Note: the specified type MUST be an open generic type definition containing exactly one generic argument. + /// + /// The type of the manager. + /// The instance. + public OpenIddictCoreBuilder ReplaceSessionManager( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type) + { + if (!type.IsGenericTypeDefinition || type.GetGenericArguments() is not { Length: 1 }) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0232), nameof(type)); + } + + Services.Replace(ServiceDescriptor.Scoped(typeof(OpenIddictSessionManager<>), type)); + + return this; + } + + /// + /// Replaces the session store by the specified type. + /// + /// The type of the entity. + /// The type of the store. + /// The lifetime of the store. + /// The instance. + public OpenIddictCoreBuilder ReplaceSessionStore< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TStore>( + ServiceLifetime lifetime = ServiceLifetime.Scoped) + where TSession : class + where TStore : IOpenIddictSessionStore + { + Services.Replace(ServiceDescriptor.Describe(typeof(IOpenIddictSessionStore), typeof(TStore), lifetime)); + + return this; + } + + /// + /// Replaces the session store by the specified type. + /// + /// + /// Note: the specified type MUST be an open generic type definition containing exactly one generic argument. + /// + /// The type of the store. + /// The lifetime of the store. + /// The instance. + public OpenIddictCoreBuilder ReplaceSessionStore( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type, + ServiceLifetime lifetime = ServiceLifetime.Scoped) + { + if (!type.IsGenericTypeDefinition || type.GetGenericArguments() is not { Length: 1 }) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0232), nameof(type)); + } + + Services.Replace(ServiceDescriptor.Describe(typeof(IOpenIddictSessionStore<>), type, lifetime)); + + return this; + } + /// /// Replaces the token manager by the specified type. /// @@ -600,6 +680,19 @@ public sealed class OpenIddictCoreBuilder return this; } + /// + /// Configures OpenIddict to use the specified entity as the default session entity. + /// + /// The instance. + public OpenIddictCoreBuilder SetDefaultSessionEntity< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession>() where TSession : class + { + Services.Replace(ServiceDescriptor.Scoped(static provider => + provider.GetRequiredService>())); + + return this; + } + /// /// Configures OpenIddict to use the specified entity as the default token entity. /// diff --git a/src/OpenIddict.Core/OpenIddictCoreExtensions.cs b/src/OpenIddict.Core/OpenIddictCoreExtensions.cs index 6ae119d0..2dc8aff1 100644 --- a/src/OpenIddict.Core/OpenIddictCoreExtensions.cs +++ b/src/OpenIddict.Core/OpenIddictCoreExtensions.cs @@ -33,12 +33,14 @@ public static class OpenIddictCoreExtensions builder.Services.TryAddScoped(typeof(IOpenIddictAuthorizationCache<>), typeof(OpenIddictAuthorizationCache<>)); builder.Services.TryAddScoped(typeof(IOpenIddictResourceCache<>), typeof(OpenIddictResourceCache<>)); builder.Services.TryAddScoped(typeof(IOpenIddictScopeCache<>), typeof(OpenIddictScopeCache<>)); + builder.Services.TryAddScoped(typeof(IOpenIddictSessionCache<>), typeof(OpenIddictSessionCache<>)); builder.Services.TryAddScoped(typeof(IOpenIddictTokenCache<>), typeof(OpenIddictTokenCache<>)); builder.Services.TryAddScoped(typeof(OpenIddictApplicationManager<>)); builder.Services.TryAddScoped(typeof(OpenIddictAuthorizationManager<>)); builder.Services.TryAddScoped(typeof(OpenIddictResourceManager<>)); builder.Services.TryAddScoped(typeof(OpenIddictScopeManager<>)); + builder.Services.TryAddScoped(typeof(OpenIddictSessionManager<>)); builder.Services.TryAddScoped(typeof(OpenIddictTokenManager<>)); // Note: default factories for the untyped managers are always registered to make debugging @@ -52,6 +54,8 @@ public static class OpenIddictCoreExtensions throw new InvalidOperationException(SR.GetResourceString(SR.ID0472))); builder.Services.TryAddScoped(static provider => throw new InvalidOperationException(SR.GetResourceString(SR.ID0472))); + builder.Services.TryAddScoped(static provider => + throw new InvalidOperationException(SR.GetResourceString(SR.ID0472))); builder.Services.TryAddScoped(static provider => throw new InvalidOperationException(SR.GetResourceString(SR.ID0472))); diff --git a/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkApplication.cs b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkApplication.cs index cfa52908..a2447606 100644 --- a/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkApplication.cs +++ b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkApplication.cs @@ -12,7 +12,10 @@ namespace OpenIddict.EntityFramework.Models; /// /// Represents an OpenIddict application. /// -public class OpenIddictEntityFrameworkApplication : OpenIddictEntityFrameworkApplication +public class OpenIddictEntityFrameworkApplication : + OpenIddictEntityFrameworkApplication { public OpenIddictEntityFrameworkApplication() => Id = Guid.NewGuid().ToString(); } diff --git a/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkAuthorization.cs b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkAuthorization.cs index 382248ae..93b415d1 100644 --- a/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkAuthorization.cs +++ b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkAuthorization.cs @@ -12,7 +12,10 @@ namespace OpenIddict.EntityFramework.Models; /// /// Represents an OpenIddict authorization. /// -public class OpenIddictEntityFrameworkAuthorization : OpenIddictEntityFrameworkAuthorization +public class OpenIddictEntityFrameworkAuthorization : + OpenIddictEntityFrameworkAuthorization { public OpenIddictEntityFrameworkAuthorization() => Id = Guid.NewGuid().ToString(); } diff --git a/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkResource.cs b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkResource.cs index c67e750c..9e8241df 100644 --- a/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkResource.cs +++ b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkResource.cs @@ -12,7 +12,8 @@ namespace OpenIddict.EntityFramework.Models; /// /// Represents an OpenIddict resource. /// -public class OpenIddictEntityFrameworkResource : OpenIddictEntityFrameworkResource +public class OpenIddictEntityFrameworkResource : + OpenIddictEntityFrameworkResource { public OpenIddictEntityFrameworkResource() => Id = Guid.NewGuid().ToString(); } diff --git a/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkScope.cs b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkScope.cs index 183304b6..4ed5c7c9 100644 --- a/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkScope.cs +++ b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkScope.cs @@ -12,7 +12,8 @@ namespace OpenIddict.EntityFramework.Models; /// /// Represents an OpenIddict scope. /// -public class OpenIddictEntityFrameworkScope : OpenIddictEntityFrameworkScope +public class OpenIddictEntityFrameworkScope : + OpenIddictEntityFrameworkScope { public OpenIddictEntityFrameworkScope() => Id = Guid.NewGuid().ToString(); } diff --git a/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkSession.cs b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkSession.cs new file mode 100644 index 00000000..4457d008 --- /dev/null +++ b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkSession.cs @@ -0,0 +1,77 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace OpenIddict.EntityFramework.Models; + +/// +/// Represents an OpenIddict session. +/// +public class OpenIddictEntityFrameworkSession : + OpenIddictEntityFrameworkSession +{ + public OpenIddictEntityFrameworkSession() => Id = Guid.NewGuid().ToString(); +} + +/// +/// Represents an OpenIddict session. +/// +[DebuggerDisplay("Id = {Id.ToString(),nq} ; Subject = {Subject,nq} ; LoginId = {LoginId,nq} ; Status = {Status,nq}")] +public class OpenIddictEntityFrameworkSession + where TKey : notnull, IEquatable + where TApplication : class + where TAuthorization : class +{ + /// + /// Gets or sets the application associated with the session. + /// + public virtual TApplication? Application { get; set; } + + /// + /// Gets or sets the authorization associated with the session. + /// + public virtual TAuthorization? Authorization { get; set; } + + /// + /// Gets or sets the concurrency token of the session. + /// + public virtual string? ConcurrencyToken { get; set; } = Guid.NewGuid().ToString(); + + /// + /// Gets or sets the UTC creation date of the session. + /// + public virtual DateTime? CreationDate { get; set; } + + /// + /// Gets or sets the unique identifier of the session. + /// + public virtual TKey? Id { get; set; } + + /// + /// Gets or sets the login identifier of the session. + /// + public virtual string? LoginId { get; set; } + + /// + /// Gets or sets the additional properties of the session, serialized as a JSON object. + /// + [StringSyntax(StringSyntaxAttribute.Json)] + public virtual string? Properties { get; set; } + + /// + /// Gets or sets the status of the session. + /// + public virtual string? Status { get; set; } + + /// + /// Gets or sets the subject of the session. + /// + public virtual string? Subject { get; set; } +} diff --git a/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkToken.cs b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkToken.cs index 9aa210ca..6e9ab164 100644 --- a/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkToken.cs +++ b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkToken.cs @@ -12,7 +12,10 @@ namespace OpenIddict.EntityFramework.Models; /// /// Represents an OpenIddict token. /// -public class OpenIddictEntityFrameworkToken : OpenIddictEntityFrameworkToken +public class OpenIddictEntityFrameworkToken : + OpenIddictEntityFrameworkToken { public OpenIddictEntityFrameworkToken() => Id = Guid.NewGuid().ToString(); } @@ -91,7 +94,7 @@ public class OpenIddictEntityFrameworkToken public virtual string? Status { get; set; } /// - /// Gets or sets the subject associated with the token. + /// Gets or sets the subject of the token. /// public virtual string? Subject { get; set; } diff --git a/src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkSessionConfiguration.cs b/src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkSessionConfiguration.cs new file mode 100644 index 00000000..01924a1d --- /dev/null +++ b/src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkSessionConfiguration.cs @@ -0,0 +1,74 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.ComponentModel; +using System.Data.Entity.ModelConfiguration; +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; +using OpenIddict.EntityFramework.Models; + +namespace OpenIddict.EntityFramework; + +/// +/// Defines a relational mapping for the session entity. +/// +/// The type of the session entity. +/// The type of the application entity. +/// The type of the authorization entity. +/// The type of the token entity. +/// The type of the primary key. +[EditorBrowsable(EditorBrowsableState.Never)] +public sealed class OpenIddictEntityFrameworkSessionConfiguration< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApplication, + [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 TKey : notnull, IEquatable +{ + public OpenIddictEntityFrameworkSessionConfiguration() + { + // Warning: optional foreign keys MUST NOT be added as CLR properties because + // Entity Framework would throw an exception due to the TKey generic parameter + // being non-nullable when using value types like short, int, long or Guid. + + Property(static session => session.ConcurrencyToken) + .HasMaxLength(50) + .IsConcurrencyToken(); + + HasKey(static session => session.Id); + + if (typeof(TKey) == typeof(string)) + { + var parameter = Expression.Parameter(typeof(TSession), "session"); + var property = Expression.Property(parameter, + typeof(TSession).GetProperty(nameof(OpenIddictEntityFrameworkSession.Id))!); + var lambda = Expression.Lambda>(property, parameter); + + Property(lambda).HasMaxLength(100); + } + + Property(static session => session.LoginId) + .HasMaxLength(100); + + // Warning: the index on the LoginId property MUST NOT be declared as + // a unique index, as Entity Framework 6.x doesn't support creating indexes + // with null-friendly WHERE conditions, unlike Entity Framework Core. + HasIndex(static session => session.LoginId); + + Property(static session => session.Status) + .HasMaxLength(50); + + Property(static session => session.Subject) + .HasMaxLength(400); + + ToTable("OpenIddictSessions"); + } +} diff --git a/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkBuilder.cs b/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkBuilder.cs index bf70dd08..a585cf67 100644 --- a/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkBuilder.cs +++ b/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkBuilder.cs @@ -68,12 +68,14 @@ public sealed class OpenIddictEntityFrameworkBuilder [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResource, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TScope, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey>() where TApplication : OpenIddictEntityFrameworkApplication where TAuthorization : OpenIddictEntityFrameworkAuthorization where TResource : OpenIddictEntityFrameworkResource where TScope : OpenIddictEntityFrameworkScope + where TSession : OpenIddictEntityFrameworkSession where TToken : OpenIddictEntityFrameworkToken where TKey : notnull, IEquatable { @@ -82,7 +84,8 @@ public sealed class OpenIddictEntityFrameworkBuilder // // To ensure a better exception is thrown, a manual check is made here. if (typeof(TApplication).IsGenericType || typeof(TAuthorization).IsGenericType || - typeof(TResource).IsGenericType || typeof(TScope).IsGenericType || typeof(TToken).IsGenericType) + typeof(TResource).IsGenericType || typeof(TScope).IsGenericType || + typeof(TSession).IsGenericType || typeof(TToken).IsGenericType) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0277)); } @@ -104,6 +107,8 @@ public sealed class OpenIddictEntityFrameworkBuilder provider.GetRequiredService>())); Services.Replace(ServiceDescriptor.Scoped(static provider => provider.GetRequiredService>())); + Services.Replace(ServiceDescriptor.Scoped(static provider => + provider.GetRequiredService>())); Services.Replace(ServiceDescriptor.Scoped(static provider => provider.GetRequiredService>())); @@ -115,6 +120,8 @@ public sealed class OpenIddictEntityFrameworkBuilder OpenIddictEntityFrameworkResourceStore>()); Services.Replace(ServiceDescriptor.Scoped, OpenIddictEntityFrameworkScopeStore>()); + Services.Replace(ServiceDescriptor.Scoped, + OpenIddictEntityFrameworkSessionStore>()); Services.Replace(ServiceDescriptor.Scoped, OpenIddictEntityFrameworkTokenStore>()); diff --git a/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkExtensions.cs b/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkExtensions.cs index a2edf857..10375ff2 100644 --- a/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkExtensions.cs +++ b/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkExtensions.cs @@ -35,12 +35,14 @@ public static class OpenIddictEntityFrameworkExtensions .SetDefaultAuthorizationEntity() .SetDefaultResourceEntity() .SetDefaultScopeEntity() + .SetDefaultSessionEntity() .SetDefaultTokenEntity(); builder.ReplaceApplicationStore() .ReplaceAuthorizationStore() .ReplaceResourceStore() .ReplaceScopeStore() + .ReplaceSessionStore() .ReplaceTokenStore(); // Note: a default context factory is always registered to make debugging easier when diff --git a/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkHelpers.cs b/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkHelpers.cs index 3161370c..339eb36e 100644 --- a/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkHelpers.cs +++ b/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkHelpers.cs @@ -29,6 +29,7 @@ public static class OpenIddictEntityFrameworkHelpers OpenIddictEntityFrameworkAuthorization, OpenIddictEntityFrameworkResource, OpenIddictEntityFrameworkScope, + OpenIddictEntityFrameworkSession, OpenIddictEntityFrameworkToken, string>(); /// @@ -37,7 +38,7 @@ public static class OpenIddictEntityFrameworkHelpers /// /// /// Note: when using custom entities, the new entities MUST be registered by calling - /// . + /// . /// /// The builder used to configure the Entity Framework context. /// The Entity Framework context builder. @@ -46,12 +47,14 @@ public static class OpenIddictEntityFrameworkHelpers [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResource, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TScope, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey>(this DbModelBuilder builder) where TApplication : OpenIddictEntityFrameworkApplication where TAuthorization : OpenIddictEntityFrameworkAuthorization where TResource : OpenIddictEntityFrameworkResource where TScope : OpenIddictEntityFrameworkScope + where TSession : OpenIddictEntityFrameworkSession where TToken : OpenIddictEntityFrameworkToken where TKey : notnull, IEquatable { @@ -62,6 +65,7 @@ public static class OpenIddictEntityFrameworkHelpers .Add(new OpenIddictEntityFrameworkAuthorizationConfiguration()) .Add(new OpenIddictEntityFrameworkResourceConfiguration()) .Add(new OpenIddictEntityFrameworkScopeConfiguration()) + .Add(new OpenIddictEntityFrameworkSessionConfiguration()) .Add(new OpenIddictEntityFrameworkTokenConfiguration()); return builder; @@ -82,7 +86,7 @@ public static class OpenIddictEntityFrameworkHelpers static async IAsyncEnumerable ExecuteAsync(IQueryable source, [EnumeratorCancellation] CancellationToken cancellationToken) { - using var enumerator = ((IDbAsyncEnumerable)source).GetAsyncEnumerator(); + using var enumerator = ((IDbAsyncEnumerable) source).GetAsyncEnumerator(); while (await enumerator.MoveNextAsync(cancellationToken)) { diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkAuthorizationStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkAuthorizationStore.cs index 022abac0..b6c60f8c 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkAuthorizationStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkAuthorizationStore.cs @@ -159,44 +159,47 @@ public class OpenIddictEntityFrameworkAuthorizationStore< /// public virtual async IAsyncEnumerable FindAsync( - string? subject, string? client, - string? status, string? type, - ImmutableArray? scopes, [EnumeratorCancellation] CancellationToken cancellationToken) + (string? Subject, string? ApplicationId, string? Status, + string? Type, ImmutableArray? RequiredScopes) query, + [EnumeratorCancellation] CancellationToken cancellationToken) { var context = await Context.GetDbContextAsync(cancellationToken); - IQueryable query = context.Set().Include(authorization => authorization.Application); + IQueryable authorizations = context.Set().Include(authorization => authorization.Application); - if (!string.IsNullOrEmpty(subject)) + if (!string.IsNullOrEmpty(query.Subject)) { - query = query.Where(authorization => authorization.Subject == subject); + authorizations = authorizations.Where(authorization => authorization.Subject == query.Subject); } - if (!string.IsNullOrEmpty(client)) + if (!string.IsNullOrEmpty(query.ApplicationId)) { - var key = ConvertIdentifierFromString(client); - - query = query.Where(authorization => authorization.Application!.Id!.Equals(key)); + var key = ConvertIdentifierFromString(query.ApplicationId); + authorizations = authorizations.Where(authorization => authorization.Application!.Id!.Equals(key)); } - if (!string.IsNullOrEmpty(status)) + if (!string.IsNullOrEmpty(query.Status)) { - query = query.Where(authorization => authorization.Status == status); + authorizations = authorizations.Where(authorization => authorization.Status == query.Status); } - if (!string.IsNullOrEmpty(type)) + if (!string.IsNullOrEmpty(query.Type)) { - query = query.Where(authorization => authorization.Type == type); + authorizations = authorizations.Where(authorization => authorization.Type == query.Type); } - await foreach (var authorization in query.AsAsyncEnumerable(cancellationToken)) + // Note: Entity Framework Core cannot translate the logic used to filter authorizations by scopes in a + // SQL query so the filtering is done manually after the results have been retrieved from the database. + await foreach (var authorization in authorizations.AsAsyncEnumerable(cancellationToken)) { - if (scopes is null || (await GetScopesAsync(authorization, cancellationToken)) + if (query.RequiredScopes is { IsDefaultOrEmpty: false } scopes && !(await GetScopesAsync(authorization, cancellationToken)) .ToHashSet(StringComparer.Ordinal) .IsSupersetOf(scopes)) { - yield return authorization; + continue; } + + yield return authorization; } } @@ -259,12 +262,12 @@ public class OpenIddictEntityFrameworkAuthorizationStore< { ArgumentNullException.ThrowIfNull(authorization); - var context = await Context.GetDbContextAsync(cancellationToken); - // If the application is not attached to the authorization, try to load it manually. if (authorization.Application is null) { - var reference = context.Entry(authorization).Reference(entry => entry.Application); + var context = await Context.GetDbContextAsync(cancellationToken); + + var reference = context.Entry(authorization).Reference(static entry => entry.Application); if (reference.EntityEntry.State is EntityState.Detached) { return null; @@ -703,10 +706,10 @@ public class OpenIddictEntityFrameworkAuthorizationStore< { ArgumentNullException.ThrowIfNull(authorization); - var context = await Context.GetDbContextAsync(cancellationToken); - if (!string.IsNullOrEmpty(identifier)) { + var context = await Context.GetDbContextAsync(cancellationToken); + authorization.Application = await context.Set().FindAsync( cancellationToken, ConvertIdentifierFromString(identifier)) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0244)); @@ -717,7 +720,9 @@ public class OpenIddictEntityFrameworkAuthorizationStore< // If the application is not attached to the authorization, try to load it manually. if (authorization.Application is null) { - var reference = context.Entry(authorization).Reference(entry => entry.Application); + var context = await Context.GetDbContextAsync(cancellationToken); + + var reference = context.Entry(authorization).Reference(static entry => entry.Application); if (reference.EntityEntry.State is EntityState.Detached) { return; diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkSessionStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkSessionStore.cs new file mode 100644 index 00000000..9e063995 --- /dev/null +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkSessionStore.cs @@ -0,0 +1,706 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.Collections.Immutable; +using System.ComponentModel; +using System.Data.Entity.Infrastructure; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using OpenIddict.EntityFramework.Models; +using static OpenIddict.Abstractions.OpenIddictExceptions; + +namespace OpenIddict.EntityFramework; + +/// +/// Provides methods allowing to manage the sessions stored in a database. +/// +public class OpenIddictEntityFrameworkSessionStore : + OpenIddictEntityFrameworkSessionStore +{ + public OpenIddictEntityFrameworkSessionStore( + IMemoryCache cache, + IOpenIddictEntityFrameworkContext context, + IOptionsMonitor options) + : base(cache, context, options) + { + } +} + +/// +/// Provides methods allowing to manage the sessions stored in a database. +/// +/// The type of the session entity. +/// The type of the application entity. +/// The type of the authorization entity. +/// The type of the token entity. +/// The type of the entity primary keys. +public class OpenIddictEntityFrameworkSessionStore< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApplication, + [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 TKey : notnull, IEquatable +{ + public OpenIddictEntityFrameworkSessionStore( + IMemoryCache cache, + IOpenIddictEntityFrameworkContext context, + IOptionsMonitor options) + { + Cache = cache ?? throw new ArgumentNullException(nameof(cache)); + Context = context ?? throw new ArgumentNullException(nameof(context)); + Options = options ?? throw new ArgumentNullException(nameof(options)); + } + + /// + /// Gets the memory cache associated with the current store. + /// + protected IMemoryCache Cache { get; } + + /// + /// Gets the database context associated with the current store. + /// + protected IOpenIddictEntityFrameworkContext Context { get; } + + /// + /// Gets the options associated with the current store. + /// + protected IOptionsMonitor Options { get; } + + /// + public virtual async ValueTask CountAsync(CancellationToken cancellationToken) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + return await context.Set().LongCountAsync(cancellationToken); + } + + /// + public virtual async ValueTask CountAsync( + Func, TState, IQueryable> query, + TState state, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(query); + + var context = await Context.GetDbContextAsync(cancellationToken); + + return await query(context.Set(), state).LongCountAsync(cancellationToken); + } + + /// + public virtual async ValueTask CreateAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + var context = await Context.GetDbContextAsync(cancellationToken); + + context.Set().Add(session); + + await context.SaveChangesAsync(cancellationToken); + } + + /// + public virtual async ValueTask DeleteAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + var context = await Context.GetDbContextAsync(cancellationToken); + + context.Set().Remove(session); + + try + { + await context.SaveChangesAsync(cancellationToken); + } + + catch (DbUpdateConcurrencyException exception) + { + // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + context.Entry(session).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, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + IQueryable sessions = context.Set() + .Include(session => session.Application) + .Include(session => session.Authorization); + + if (!string.IsNullOrEmpty(query.Subject)) + { + sessions = sessions.Where(session => session.Subject == query.Subject); + } + + if (!string.IsNullOrEmpty(query.ApplicationId)) + { + var key = ConvertIdentifierFromString(query.ApplicationId); + sessions = sessions.Where(session => session.Application!.Id!.Equals(key)); + } + + if (!string.IsNullOrEmpty(query.LoginId)) + { + sessions = sessions.Where(session => session.LoginId == query.LoginId); + } + + if (!string.IsNullOrEmpty(query.Status)) + { + sessions = sessions.Where(session => session.Status == query.Status); + } + + await foreach (var session in sessions.AsAsyncEnumerable(cancellationToken)) + { + yield return session; + } + } + + /// + public virtual IAsyncEnumerable FindByApplicationIdAsync(string identifier, CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrEmpty(identifier); + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var context = await Context.GetDbContextAsync(cancellationToken); + var key = ConvertIdentifierFromString(identifier); + + await foreach (var session in + (from session in context.Set().Include(session => session.Application).Include(session => session.Authorization) + where session.Application!.Id!.Equals(key) + select session).AsAsyncEnumerable(cancellationToken)) + { + yield return session; + } + } + } + + /// + public virtual IAsyncEnumerable FindByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrEmpty(identifier); + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var context = await Context.GetDbContextAsync(cancellationToken); + var key = ConvertIdentifierFromString(identifier); + + await foreach (var session in + (from session in context.Set().Include(session => session.Application).Include(session => session.Authorization) + where session.Authorization!.Id!.Equals(key) + select session).AsAsyncEnumerable(cancellationToken)) + { + yield return session; + } + } + } + + /// + public virtual async ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrEmpty(identifier); + + var context = await Context.GetDbContextAsync(cancellationToken); + var key = ConvertIdentifierFromString(identifier); + + return await context.Set().FindAsync(cancellationToken, [key]); + } + + /// + public virtual IAsyncEnumerable FindByLoginIdAsync(string identifier, CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrEmpty(identifier); + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + await foreach (var session in + (from session in context.Set().Include(session => session.Application).Include(session => session.Authorization) + where session.LoginId == identifier + select session).AsAsyncEnumerable(cancellationToken)) + { + yield return session; + } + } + } + + /// + public virtual IAsyncEnumerable FindBySubjectAsync(string subject, CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrEmpty(subject); + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + await foreach (var session in + (from session in context.Set().Include(session => session.Application).Include(session => session.Authorization) + where session.Subject == subject + select session).AsAsyncEnumerable(cancellationToken)) + { + yield return session; + } + } + } + + /// + public virtual async ValueTask GetApplicationIdAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + // If the application is not attached to the session, try to load it manually. + if (session.Application is null) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + var reference = context.Entry(session).Reference(static entry => entry.Application); + if (reference.EntityEntry.State is EntityState.Detached) + { + return null; + } + + await reference.LoadAsync(cancellationToken); + } + + if (session.Application is null) + { + return null; + } + + return ConvertIdentifierToString(session.Application.Id); + } + + /// + public virtual async ValueTask GetAsync( + Func, TState, IQueryable> query, + TState state, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(query); + + var context = await Context.GetDbContextAsync(cancellationToken); + + return await query(context.Set(), state).FirstOrDefaultAsync(cancellationToken); + } + + /// + public virtual async ValueTask GetAuthorizationIdAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + // If the application is not attached to the session, try to load it manually. + if (session.Application is null) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + var reference = context.Entry(session).Reference(static entry => entry.Application); + if (reference.EntityEntry.State is EntityState.Detached) + { + return null; + } + + await reference.LoadAsync(cancellationToken); + } + + if (session.Application is null) + { + return null; + } + + return ConvertIdentifierToString(session.Application.Id); + } + + /// + public virtual ValueTask GetCreationDateAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + return new(session.CreationDate is DateTime date ? DateTime.SpecifyKind(date, DateTimeKind.Utc) : null); + } + + /// + public virtual ValueTask GetIdAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + return new(ConvertIdentifierToString(session.Id)); + } + + /// + public virtual ValueTask GetLoginIdAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + return new(session.LoginId); + } + + /// + public virtual ValueTask> GetPropertiesAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + if (string.IsNullOrEmpty(session.Properties)) + { + return new(ImmutableDictionary.Create()); + } + + // Note: parsing the stringified properties is an expensive operation. + // To mitigate that, the resulting object is stored in the memory cache. + var key = string.Concat("7c674699-92a2-4607-a11b-a4d4edf9df46", "\x1e", session.Properties); + var properties = Cache.GetOrCreate(key, entry => + { + entry.SetPriority(CacheItemPriority.High) + .SetSlidingExpiration(TimeSpan.FromMinutes(1)); + + using var document = JsonDocument.Parse(session.Properties); + var builder = ImmutableDictionary.CreateBuilder(); + + foreach (var property in document.RootElement.EnumerateObject()) + { + builder[property.Name] = property.Value.Clone(); + } + + return builder.ToImmutable(); + })!; + + return new(properties); + } + + /// + public virtual ValueTask GetStatusAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + return new(session.Status); + } + + /// + public virtual ValueTask GetSubjectAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + return new(session.Subject); + } + + /// + public virtual ValueTask InstantiateAsync(CancellationToken cancellationToken) + { + try + { + return new(Activator.CreateInstance()); + } + + catch (MemberAccessException exception) + { + return new(Task.FromException( + new InvalidOperationException(SR.GetResourceString(SR.ID0240), exception))); + } + } + + /// + public virtual async IAsyncEnumerable ListAsync(int? count, int? offset, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + IQueryable query = context.Set().OrderBy(session => session.Id!); + + if (offset.HasValue) + { + query = query.Skip(offset.Value); + } + + if (count.HasValue) + { + query = query.Take(count.Value); + } + + await foreach (var session in query.AsAsyncEnumerable(cancellationToken)) + { + yield return session; + } + } + + /// + public virtual IAsyncEnumerable ListAsync( + Func, TState, IQueryable> query, + TState state, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(query); + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + await foreach (var session in query(context.Set(), state).AsAsyncEnumerable(cancellationToken)) + { + yield return session; + } + } + } + + /// + public virtual async ValueTask SetApplicationIdAsync(TSession session, string? identifier, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + if (!string.IsNullOrEmpty(identifier)) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + session.Application = await context.Set().FindAsync( + cancellationToken, ConvertIdentifierFromString(identifier)) + ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0244)); + } + + else + { + // If the application is not attached to the session, try to load it manually. + if (session.Application is null) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + var reference = context.Entry(session).Reference(static entry => entry.Application); + if (reference.EntityEntry.State is EntityState.Detached) + { + return; + } + + await reference.LoadAsync(cancellationToken); + } + + session.Application = null; + } + } + + /// + public virtual async ValueTask SetAuthorizationIdAsync(TSession session, string? identifier, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + if (!string.IsNullOrEmpty(identifier)) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + session.Authorization = await context.Set().FindAsync( + cancellationToken, ConvertIdentifierFromString(identifier)) + ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0251)); + } + + else + { + // If the authorization is not attached to the session, try to load it manually. + if (session.Authorization is null) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + var reference = context.Entry(session).Reference(static entry => entry.Authorization); + if (reference.EntityEntry.State is EntityState.Detached) + { + return; + } + + await reference.LoadAsync(cancellationToken); + } + + session.Authorization = null; + } + } + + /// + public virtual ValueTask SetCreationDateAsync(TSession session, DateTimeOffset? date, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + session.CreationDate = date?.UtcDateTime; + + return ValueTask.CompletedTask; + } + + /// + public virtual ValueTask SetPropertiesAsync(TSession session, + ImmutableDictionary properties, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + if (properties is not { IsEmpty: false }) + { + session.Properties = null; + + return ValueTask.CompletedTask; + } + + using var stream = new MemoryStream(); + using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + Indented = false + }); + + writer.WriteStartObject(); + + foreach (var property in properties) + { + writer.WritePropertyName(property.Key); + property.Value.WriteTo(writer); + } + + writer.WriteEndObject(); + writer.Flush(); + + session.Properties = Encoding.UTF8.GetString(stream.ToArray()); + + return ValueTask.CompletedTask; + } + + /// + public virtual ValueTask SetLoginIdAsync(TSession session, string? identifier, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + session.LoginId = identifier; + + return ValueTask.CompletedTask; + } + + /// + public virtual ValueTask SetStatusAsync(TSession session, string? status, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + session.Status = status; + + return ValueTask.CompletedTask; + } + + /// + public virtual ValueTask SetSubjectAsync(TSession session, string? subject, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + session.Subject = subject; + + return ValueTask.CompletedTask; + } + + /// + public virtual async ValueTask UpdateAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + var context = await Context.GetDbContextAsync(cancellationToken); + + context.Set().Attach(session); + + // Generate a new concurrency token and attach it + // to the session before persisting the changes. + session.ConcurrencyToken = Guid.NewGuid().ToString(); + + context.Entry(session).State = EntityState.Modified; + + try + { + await context.SaveChangesAsync(cancellationToken); + } + + catch (DbUpdateConcurrencyException exception) + { + // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + context.Entry(session).State = EntityState.Unchanged; + + throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception); + } + } + + /// + /// Converts the provided identifier to a strongly typed key object. + /// + /// The identifier to convert. + /// An instance of representing the provided identifier. + public virtual TKey? ConvertIdentifierFromString(string? identifier) + { + if (string.IsNullOrEmpty(identifier)) + { + return default; + } + + // Optimization: if the key is a string, directly return it as-is. + if (typeof(TKey) == typeof(string)) + { + return (TKey?) (object?) identifier; + } + + else + { + var converter = +#if NET + TypeDescriptor.GetConverterFromRegisteredType(typeof(TKey)); +#else + TypeDescriptor.GetConverter(typeof(TKey)); +#endif + + return (TKey?) converter.ConvertFromInvariantString(identifier); + } + } + + /// + /// Converts the provided identifier to its string representation. + /// + /// The identifier to convert. + /// A representation of the provided identifier. + public virtual string? ConvertIdentifierToString(TKey? identifier) + { + if (Equals(identifier, default(TKey))) + { + return null; + } + + // Optimization: if the key is a string, directly return it as-is. + if (identifier is string value) + { + return value; + } + + else + { + var converter = +#if NET + TypeDescriptor.GetConverterFromRegisteredType(typeof(TKey)); +#else + TypeDescriptor.GetConverter(typeof(TKey)); +#endif + + return converter.ConvertToInvariantString(identifier); + } + } +} diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs index 79a07412..3cb832e0 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs @@ -136,38 +136,37 @@ public class OpenIddictEntityFrameworkTokenStore< /// public virtual async IAsyncEnumerable FindAsync( - string? subject, string? client, - string? status, string? type, [EnumeratorCancellation] CancellationToken cancellationToken) + (string? Subject, string? ApplicationId, string? Status, string? Type) query, + [EnumeratorCancellation] CancellationToken cancellationToken) { var context = await Context.GetDbContextAsync(cancellationToken); - IQueryable query = context.Set() - .Include(token => token.Application) - .Include(token => token.Authorization); + IQueryable tokens = context.Set() + .Include(token => token.Application) + .Include(token => token.Authorization); - if (!string.IsNullOrEmpty(subject)) + if (!string.IsNullOrEmpty(query.Subject)) { - query = query.Where(token => token.Subject == subject); + tokens = tokens.Where(token => token.Subject == query.Subject); } - if (!string.IsNullOrEmpty(client)) + if (!string.IsNullOrEmpty(query.ApplicationId)) { - var key = ConvertIdentifierFromString(client); - - query = query.Where(token => token.Application!.Id!.Equals(key)); + var key = ConvertIdentifierFromString(query.ApplicationId); + tokens = tokens.Where(token => token.Application!.Id!.Equals(key)); } - if (!string.IsNullOrEmpty(status)) + if (!string.IsNullOrEmpty(query.Status)) { - query = query.Where(token => token.Status == status); + tokens = tokens.Where(token => token.Status == query.Status); } - if (!string.IsNullOrEmpty(type)) + if (!string.IsNullOrEmpty(query.Type)) { - query = query.Where(token => token.Type == type); + tokens = tokens.Where(token => token.Type == query.Type); } - await foreach (var token in query.AsAsyncEnumerable(cancellationToken)) + await foreach (var token in tokens.AsAsyncEnumerable(cancellationToken)) { yield return token; } @@ -274,12 +273,12 @@ public class OpenIddictEntityFrameworkTokenStore< { ArgumentNullException.ThrowIfNull(token); - var context = await Context.GetDbContextAsync(cancellationToken); - // If the application is not attached to the token, try to load it manually. if (token.Application is null) { - var reference = context.Entry(token).Reference(entry => entry.Application); + var context = await Context.GetDbContextAsync(cancellationToken); + + var reference = context.Entry(token).Reference(static entry => entry.Application); if (reference.EntityEntry.State is EntityState.Detached) { return null; @@ -316,12 +315,12 @@ public class OpenIddictEntityFrameworkTokenStore< { ArgumentNullException.ThrowIfNull(token); - var context = await Context.GetDbContextAsync(cancellationToken); - // If the authorization is not attached to the token, try to load it manually. if (token.Authorization is null) { - var reference = context.Entry(token).Reference(entry => entry.Authorization); + var context = await Context.GetDbContextAsync(cancellationToken); + + var reference = context.Entry(token).Reference(static entry => entry.Authorization); if (reference.EntityEntry.State is EntityState.Detached) { return null; @@ -791,13 +790,13 @@ public class OpenIddictEntityFrameworkTokenStore< { ArgumentNullException.ThrowIfNull(token); - var context = await Context.GetDbContextAsync(cancellationToken); - if (!string.IsNullOrEmpty(identifier)) { + var context = await Context.GetDbContextAsync(cancellationToken); + token.Application = await context.Set().FindAsync( cancellationToken, ConvertIdentifierFromString(identifier)) - ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0250)); + ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0244)); } else @@ -805,7 +804,9 @@ public class OpenIddictEntityFrameworkTokenStore< // If the application is not attached to the token, try to load it manually. if (token.Application is null) { - var reference = context.Entry(token).Reference(entry => entry.Application); + var context = await Context.GetDbContextAsync(cancellationToken); + + var reference = context.Entry(token).Reference(static entry => entry.Application); if (reference.EntityEntry.State is EntityState.Detached) { return; @@ -823,10 +824,10 @@ public class OpenIddictEntityFrameworkTokenStore< { ArgumentNullException.ThrowIfNull(token); - var context = await Context.GetDbContextAsync(cancellationToken); - if (!string.IsNullOrEmpty(identifier)) { + var context = await Context.GetDbContextAsync(cancellationToken); + token.Authorization = await context.Set().FindAsync( cancellationToken, ConvertIdentifierFromString(identifier)) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0251)); @@ -837,7 +838,9 @@ public class OpenIddictEntityFrameworkTokenStore< // If the authorization is not attached to the token, try to load it manually. if (token.Authorization is null) { - var reference = context.Entry(token).Reference(entry => entry.Authorization); + var context = await Context.GetDbContextAsync(cancellationToken); + + var reference = context.Entry(token).Reference(static entry => entry.Authorization); if (reference.EntityEntry.State is EntityState.Detached) { return; diff --git a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreApplication.cs b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreApplication.cs index 5c436636..5c3b985e 100644 --- a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreApplication.cs +++ b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreApplication.cs @@ -13,7 +13,10 @@ namespace OpenIddict.EntityFrameworkCore.Models; /// /// Represents an OpenIddict application. /// -public class OpenIddictEntityFrameworkCoreApplication : OpenIddictEntityFrameworkCoreApplication +public class OpenIddictEntityFrameworkCoreApplication : + OpenIddictEntityFrameworkCoreApplication { public OpenIddictEntityFrameworkCoreApplication() => Id = Guid.NewGuid().ToString(); } @@ -21,7 +24,10 @@ public class OpenIddictEntityFrameworkCoreApplication : OpenIddictEntityFramewor /// /// Represents an OpenIddict application. /// -public class OpenIddictEntityFrameworkCoreApplication : OpenIddictEntityFrameworkCoreApplication, OpenIddictEntityFrameworkCoreToken> +public class OpenIddictEntityFrameworkCoreApplication : + OpenIddictEntityFrameworkCoreApplication, + OpenIddictEntityFrameworkCoreToken> where TKey : notnull, IEquatable; /// diff --git a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreAuthorization.cs b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreAuthorization.cs index f6d883b9..ae84951b 100644 --- a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreAuthorization.cs +++ b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreAuthorization.cs @@ -12,7 +12,10 @@ namespace OpenIddict.EntityFrameworkCore.Models; /// /// Represents an OpenIddict authorization. /// -public class OpenIddictEntityFrameworkCoreAuthorization : OpenIddictEntityFrameworkCoreAuthorization +public class OpenIddictEntityFrameworkCoreAuthorization : + OpenIddictEntityFrameworkCoreAuthorization { public OpenIddictEntityFrameworkCoreAuthorization() => Id = Guid.NewGuid().ToString(); } @@ -20,7 +23,10 @@ public class OpenIddictEntityFrameworkCoreAuthorization : OpenIddictEntityFramew /// /// Represents an OpenIddict authorization. /// -public class OpenIddictEntityFrameworkCoreAuthorization : OpenIddictEntityFrameworkCoreAuthorization, OpenIddictEntityFrameworkCoreToken> +public class OpenIddictEntityFrameworkCoreAuthorization : + OpenIddictEntityFrameworkCoreAuthorization, + OpenIddictEntityFrameworkCoreToken> where TKey : notnull, IEquatable; /// diff --git a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreResource.cs b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreResource.cs index 9a0b4acb..f885ad62 100644 --- a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreResource.cs +++ b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreResource.cs @@ -12,7 +12,8 @@ namespace OpenIddict.EntityFrameworkCore.Models; /// /// Represents an OpenIddict resource. /// -public class OpenIddictEntityFrameworkCoreResource : OpenIddictEntityFrameworkCoreResource +public class OpenIddictEntityFrameworkCoreResource : + OpenIddictEntityFrameworkCoreResource { public OpenIddictEntityFrameworkCoreResource() => Id = Guid.NewGuid().ToString(); } diff --git a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreScope.cs b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreScope.cs index 1e1a3073..b9077aaa 100644 --- a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreScope.cs +++ b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreScope.cs @@ -12,7 +12,8 @@ namespace OpenIddict.EntityFrameworkCore.Models; /// /// Represents an OpenIddict scope. /// -public class OpenIddictEntityFrameworkCoreScope : OpenIddictEntityFrameworkCoreScope +public class OpenIddictEntityFrameworkCoreScope : + OpenIddictEntityFrameworkCoreScope { public OpenIddictEntityFrameworkCoreScope() => Id = Guid.NewGuid().ToString(); } diff --git a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreSession.cs b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreSession.cs new file mode 100644 index 00000000..6e89959f --- /dev/null +++ b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreSession.cs @@ -0,0 +1,85 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.Diagnostics; +using System.Text.Json; + +namespace OpenIddict.EntityFrameworkCore.Models; + +/// +/// Represents an OpenIddict session. +/// +public class OpenIddictEntityFrameworkCoreSession : + OpenIddictEntityFrameworkCoreSession +{ + public OpenIddictEntityFrameworkCoreSession() => Id = Guid.NewGuid().ToString(); +} + +/// +/// Represents an OpenIddict session. +/// +public class OpenIddictEntityFrameworkCoreSession : + OpenIddictEntityFrameworkCoreSession, + OpenIddictEntityFrameworkCoreAuthorization> + 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 + where TKey : notnull, IEquatable + where TApplication : class + where TAuthorization : class +{ + /// + /// Gets or sets the application associated with the session. + /// + public virtual TApplication? Application { get; set; } + + /// + /// Gets or sets the authorization associated with the session. + /// + public virtual TAuthorization? Authorization { get; set; } + + /// + /// Gets or sets the concurrency token of the session. + /// + public virtual string? ConcurrencyToken { get; set; } = Guid.NewGuid().ToString(); + + /// + /// Gets or sets the UTC creation date of the session. + /// + public virtual DateTime? CreationDate { get; set; } + + /// + /// Gets or sets the unique identifier of the session. + /// + public virtual TKey? Id { get; set; } + + /// + /// Gets or sets the login identifier of the session. + /// + public virtual string? LoginId { get; set; } + + /// + /// Gets or sets the additional properties of the session. + /// + public virtual IDictionary? Properties { get; set; } + + /// + /// Gets or sets the status of the session. + /// + public virtual string? Status { get; set; } + + /// + /// Gets or sets the subject of the session. + /// + public virtual string? Subject { get; set; } +} diff --git a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreToken.cs b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreToken.cs index 56b52235..5bf3b1f5 100644 --- a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreToken.cs +++ b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreToken.cs @@ -12,7 +12,10 @@ namespace OpenIddict.EntityFrameworkCore.Models; /// /// Represents an OpenIddict token. /// -public class OpenIddictEntityFrameworkCoreToken : OpenIddictEntityFrameworkCoreToken +public class OpenIddictEntityFrameworkCoreToken : + OpenIddictEntityFrameworkCoreToken { public OpenIddictEntityFrameworkCoreToken() => Id = Guid.NewGuid().ToString(); } @@ -20,7 +23,10 @@ public class OpenIddictEntityFrameworkCoreToken : OpenIddictEntityFrameworkCoreT /// /// Represents an OpenIddict token. /// -public class OpenIddictEntityFrameworkCoreToken : OpenIddictEntityFrameworkCoreToken, OpenIddictEntityFrameworkCoreAuthorization> +public class OpenIddictEntityFrameworkCoreToken : + OpenIddictEntityFrameworkCoreToken, + OpenIddictEntityFrameworkCoreAuthorization> where TKey : notnull, IEquatable; /// @@ -33,12 +39,12 @@ public class OpenIddictEntityFrameworkCoreToken - /// Gets or sets the application associated with the current token. + /// Gets or sets the application associated with the token. /// public virtual TApplication? Application { get; set; } /// - /// Gets or sets the authorization associated with the current token. + /// Gets or sets the authorization associated with the token. /// public virtual TAuthorization? Authorization { get; set; } diff --git a/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreSessionConfiguration.cs b/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreSessionConfiguration.cs new file mode 100644 index 00000000..e5e5e785 --- /dev/null +++ b/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreSessionConfiguration.cs @@ -0,0 +1,85 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using OpenIddict.EntityFrameworkCore.Models; + +namespace OpenIddict.EntityFrameworkCore; + +/// +/// Defines a relational mapping for the session entity. +/// +/// The type of the session entity. +/// The type of the application entity. +/// The type of the authorization entity. +/// The type of the token entity. +/// The type of the primary key. +[EditorBrowsable(EditorBrowsableState.Never)] +public sealed class OpenIddictEntityFrameworkCoreSessionConfiguration< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApplication, + [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 TKey : notnull, IEquatable +{ + public void Configure(EntityTypeBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); + + // Warning: optional foreign keys MUST NOT be added as CLR properties because + // Entity Framework would throw an exception due to the TKey generic parameter + // being non-nullable when using value types like short, int, long or Guid. + + builder.Property(static session => session.ConcurrencyToken) + .HasMaxLength(50) + .IsConcurrencyToken(); + + builder.HasKey(static session => session.Id); + + builder.Property(static session => session.Id) + .ValueGeneratedOnAdd(); + + if (typeof(TKey) == typeof(string)) + { + builder.Property(static session => session.Id) + .HasMaxLength(100); + } + + builder.Property(static session => session.LoginId) + .HasMaxLength(100); + + builder.HasIndex(static session => session.LoginId) + .IsUnique(); + + builder.Property(static session => session.Status) + .HasMaxLength(50); + + builder.Property(static session => session.Subject) + .HasMaxLength(400); + + builder.Property(static session => session.Properties) + .HasConversion( + static value => JsonSerializer.Serialize(value, OpenIddictSerializer.Default.IDictionaryStringJsonElement), + static value => JsonSerializer.Deserialize(value, OpenIddictSerializer.Default.IDictionaryStringJsonElement), + CreateDictionaryComparer()); + + builder.ToTable("OpenIddictSessions"); + + static ValueComparer CreateDictionaryComparer() => new ValueComparer>( + static (left, right) => ReferenceEquals(left, right) || (left != null && right != null && left.SequenceEqual(right)), + static value => value.Aggregate(0, static (hash, value) => HashCode.Combine(hash, value)), + static value => value.ToDictionary()); + } +} diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs index a2c15de3..08d57259 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs @@ -77,6 +77,7 @@ public sealed class OpenIddictEntityFrameworkCoreBuilder OpenIddictEntityFrameworkCoreAuthorization, OpenIddictEntityFrameworkCoreResource, OpenIddictEntityFrameworkCoreScope, + OpenIddictEntityFrameworkCoreSession, OpenIddictEntityFrameworkCoreToken, TKey>(); /// @@ -89,12 +90,14 @@ public sealed class OpenIddictEntityFrameworkCoreBuilder [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResource, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TScope, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey>() where TApplication : OpenIddictEntityFrameworkCoreApplication where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization where TResource : OpenIddictEntityFrameworkCoreResource where TScope : OpenIddictEntityFrameworkCoreScope + where TSession : OpenIddictEntityFrameworkCoreSession where TToken : OpenIddictEntityFrameworkCoreToken where TKey : notnull, IEquatable { @@ -114,6 +117,8 @@ public sealed class OpenIddictEntityFrameworkCoreBuilder provider.GetRequiredService>())); Services.Replace(ServiceDescriptor.Scoped(static provider => provider.GetRequiredService>())); + Services.Replace(ServiceDescriptor.Scoped(static provider => + provider.GetRequiredService>())); Services.Replace(ServiceDescriptor.Scoped(static provider => provider.GetRequiredService>())); @@ -125,6 +130,8 @@ public sealed class OpenIddictEntityFrameworkCoreBuilder OpenIddictEntityFrameworkCoreResourceStore>()); Services.Replace(ServiceDescriptor.Scoped, OpenIddictEntityFrameworkCoreScopeStore>()); + Services.Replace(ServiceDescriptor.Scoped, + OpenIddictEntityFrameworkCoreSessionStore>()); Services.Replace(ServiceDescriptor.Scoped, OpenIddictEntityFrameworkCoreTokenStore>()); diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreCustomizer.cs b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreCustomizer.cs index 4df783c3..45f77e77 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreCustomizer.cs +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreCustomizer.cs @@ -20,12 +20,14 @@ public sealed class OpenIddictEntityFrameworkCoreCustomizer< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResource, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TScope, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : RelationalModelCustomizer where TApplication : OpenIddictEntityFrameworkCoreApplication where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization where TResource : OpenIddictEntityFrameworkCoreResource where TScope : OpenIddictEntityFrameworkCoreScope + where TSession : OpenIddictEntityFrameworkCoreSession where TToken : OpenIddictEntityFrameworkCoreToken where TKey : notnull, IEquatable { @@ -41,7 +43,7 @@ public sealed class OpenIddictEntityFrameworkCoreCustomizer< ArgumentNullException.ThrowIfNull(context); // Register the OpenIddict entity sets. - modelBuilder.UseOpenIddict(); + modelBuilder.UseOpenIddict(); base.Customize(modelBuilder, context); } diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreExtensions.cs b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreExtensions.cs index 6d252d18..27efa0d4 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreExtensions.cs +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreExtensions.cs @@ -35,12 +35,14 @@ public static class OpenIddictEntityFrameworkCoreExtensions .SetDefaultAuthorizationEntity() .SetDefaultResourceEntity() .SetDefaultScopeEntity() + .SetDefaultSessionEntity() .SetDefaultTokenEntity(); builder.ReplaceApplicationStore() .ReplaceAuthorizationStore() .ReplaceResourceStore() .ReplaceScopeStore() + .ReplaceSessionStore() .ReplaceTokenStore(); // Note: a default context factory is always registered to make debugging easier when diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs index afa60cca..00a4ac2a 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs @@ -27,6 +27,7 @@ public static class OpenIddictEntityFrameworkCoreHelpers OpenIddictEntityFrameworkCoreAuthorization, OpenIddictEntityFrameworkCoreResource, OpenIddictEntityFrameworkCoreScope, + OpenIddictEntityFrameworkCoreSession, OpenIddictEntityFrameworkCoreToken, string>(); /// @@ -59,6 +60,7 @@ public static class OpenIddictEntityFrameworkCoreHelpers OpenIddictEntityFrameworkCoreAuthorization, OpenIddictEntityFrameworkCoreResource, OpenIddictEntityFrameworkCoreScope, + OpenIddictEntityFrameworkCoreSession, OpenIddictEntityFrameworkCoreToken, TKey>(); /// @@ -87,7 +89,7 @@ public static class OpenIddictEntityFrameworkCoreHelpers /// /// /// Note: when using custom entities, the new entities MUST be registered by calling - /// . + /// . /// /// The builder used to configure the Entity Framework Core context. /// The Entity Framework Core context builder. @@ -96,6 +98,7 @@ public static class OpenIddictEntityFrameworkCoreHelpers [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResource, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TScope, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey>( this DbContextOptionsBuilder builder) @@ -103,13 +106,14 @@ public static class OpenIddictEntityFrameworkCoreHelpers where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization where TResource : OpenIddictEntityFrameworkCoreResource where TScope : OpenIddictEntityFrameworkCoreScope + where TSession : OpenIddictEntityFrameworkCoreSession where TToken : OpenIddictEntityFrameworkCoreToken where TKey : notnull, IEquatable { ArgumentNullException.ThrowIfNull(builder); return ReplaceService>(builder); + OpenIddictEntityFrameworkCoreCustomizer>(builder); static DbContextOptionsBuilder ReplaceService< TService, @@ -124,7 +128,7 @@ public static class OpenIddictEntityFrameworkCoreHelpers /// /// /// Note: when using custom entities, the new entities MUST be registered by calling - /// . + /// . /// /// The builder used to configure the Entity Framework Core context. /// The Entity Framework Core context builder. @@ -133,6 +137,7 @@ public static class OpenIddictEntityFrameworkCoreHelpers [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResource, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TScope, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey, TContext>( @@ -141,11 +146,12 @@ public static class OpenIddictEntityFrameworkCoreHelpers where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization where TResource : OpenIddictEntityFrameworkCoreResource where TScope : OpenIddictEntityFrameworkCoreScope + where TSession : OpenIddictEntityFrameworkCoreSession where TToken : OpenIddictEntityFrameworkCoreToken where TKey : notnull, IEquatable where TContext : DbContext { - builder.UseOpenIddict(); + builder.UseOpenIddict(); return builder; } @@ -160,6 +166,7 @@ public static class OpenIddictEntityFrameworkCoreHelpers OpenIddictEntityFrameworkCoreAuthorization, OpenIddictEntityFrameworkCoreResource, OpenIddictEntityFrameworkCoreScope, + OpenIddictEntityFrameworkCoreSession, OpenIddictEntityFrameworkCoreToken, string>(); /// @@ -178,6 +185,7 @@ public static class OpenIddictEntityFrameworkCoreHelpers OpenIddictEntityFrameworkCoreAuthorization, OpenIddictEntityFrameworkCoreResource, OpenIddictEntityFrameworkCoreScope, + OpenIddictEntityFrameworkCoreSession, OpenIddictEntityFrameworkCoreToken, TKey>(); /// @@ -186,7 +194,7 @@ public static class OpenIddictEntityFrameworkCoreHelpers /// /// /// Note: when using custom entities, the new entities MUST be registered by calling - /// . + /// . /// /// The builder used to configure the Entity Framework Core context. /// The Entity Framework Core context builder. @@ -195,12 +203,14 @@ public static class OpenIddictEntityFrameworkCoreHelpers [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResource, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TScope, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey>(this ModelBuilder builder) where TApplication : OpenIddictEntityFrameworkCoreApplication where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization where TResource : OpenIddictEntityFrameworkCoreResource where TScope : OpenIddictEntityFrameworkCoreScope + where TSession : OpenIddictEntityFrameworkCoreSession where TToken : OpenIddictEntityFrameworkCoreToken where TKey : notnull, IEquatable { @@ -211,6 +221,7 @@ public static class OpenIddictEntityFrameworkCoreHelpers .ApplyConfiguration(new OpenIddictEntityFrameworkCoreAuthorizationConfiguration()) .ApplyConfiguration(new OpenIddictEntityFrameworkCoreResourceConfiguration()) .ApplyConfiguration(new OpenIddictEntityFrameworkCoreScopeConfiguration()) + .ApplyConfiguration(new OpenIddictEntityFrameworkCoreSessionConfiguration()) .ApplyConfiguration(new OpenIddictEntityFrameworkCoreTokenConfiguration()); } } diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs index 60ccc566..a5e4303d 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs @@ -217,44 +217,47 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore< /// public virtual async IAsyncEnumerable FindAsync( - string? subject, string? client, - string? status, string? type, - ImmutableArray? scopes, [EnumeratorCancellation] CancellationToken cancellationToken) + (string? Subject, string? ApplicationId, string? Status, + string? Type, ImmutableArray? RequiredScopes) query, + [EnumeratorCancellation] CancellationToken cancellationToken) { var context = await Context.GetDbContextAsync(cancellationToken); - IQueryable query = context.Set().Include(authorization => authorization.Application).AsTracking(); + IQueryable authorizations = context.Set().Include(authorization => authorization.Application).AsTracking(); - if (!string.IsNullOrEmpty(subject)) + if (!string.IsNullOrEmpty(query.Subject)) { - query = query.Where(authorization => authorization.Subject == subject); + authorizations = authorizations.Where(authorization => authorization.Subject == query.Subject); } - if (!string.IsNullOrEmpty(client)) + if (!string.IsNullOrEmpty(query.ApplicationId)) { - var key = ConvertIdentifierFromString(client); - - query = query.Where(authorization => authorization.Application!.Id!.Equals(key)); + var key = ConvertIdentifierFromString(query.ApplicationId); + authorizations = authorizations.Where(authorization => authorization.Application!.Id!.Equals(key)); } - if (!string.IsNullOrEmpty(status)) + if (!string.IsNullOrEmpty(query.Status)) { - query = query.Where(authorization => authorization.Status == status); + authorizations = authorizations.Where(authorization => authorization.Status == query.Status); } - if (!string.IsNullOrEmpty(type)) + if (!string.IsNullOrEmpty(query.Type)) { - query = query.Where(authorization => authorization.Type == type); + authorizations = authorizations.Where(authorization => authorization.Type == query.Type); } - await foreach (var authorization in query.AsAsyncEnumerable().WithCancellation(cancellationToken)) + // Note: Entity Framework 6.x cannot translate the logic used to filter authorizations by scopes in a + // SQL query so the filtering is done manually after the results have been retrieved from the database. + await foreach (var authorization in authorizations.AsAsyncEnumerable().WithCancellation(cancellationToken)) { - if (scopes is null || (await GetScopesAsync(authorization, cancellationToken)) + if (query.RequiredScopes is { IsDefaultOrEmpty: false } scopes && !(await GetScopesAsync(authorization, cancellationToken)) .ToHashSet(StringComparer.Ordinal) .IsSupersetOf(scopes)) { - yield return authorization; + continue; } + + yield return authorization; } } @@ -317,12 +320,12 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore< { ArgumentNullException.ThrowIfNull(authorization); - var context = await Context.GetDbContextAsync(cancellationToken); - // If the application is not attached to the authorization, try to load it manually. if (authorization.Application is null) { - var reference = context.Entry(authorization).Reference(entry => entry.Application); + var context = await Context.GetDbContextAsync(cancellationToken); + + var reference = context.Entry(authorization).Reference(static entry => entry.Application); if (reference.EntityEntry.State is EntityState.Detached) { return null; @@ -786,12 +789,13 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore< { ArgumentNullException.ThrowIfNull(authorization); - var context = await Context.GetDbContextAsync(cancellationToken); - if (!string.IsNullOrEmpty(identifier)) { + var context = await Context.GetDbContextAsync(cancellationToken); + authorization.Application = await context.Set() - .FindAsync([ConvertIdentifierFromString(identifier)], cancellationToken); + .FindAsync([ConvertIdentifierFromString(identifier)], cancellationToken) + ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0244)); } else @@ -799,7 +803,9 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore< // If the application is not attached to the authorization, try to load it manually. if (authorization.Application is null) { - var reference = context.Entry(authorization).Reference(entry => entry.Application); + var context = await Context.GetDbContextAsync(cancellationToken); + + var reference = context.Entry(authorization).Reference(static entry => entry.Application); if (reference.EntityEntry.State is EntityState.Detached) { return; diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreResourceStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreResourceStore.cs index cc34c918..8a6a8338 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreResourceStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreResourceStore.cs @@ -19,7 +19,8 @@ namespace OpenIddict.EntityFrameworkCore; /// /// Provides methods allowing to manage the resources stored in a database. /// -public class OpenIddictEntityFrameworkCoreResourceStore : OpenIddictEntityFrameworkCoreResourceStore +public class OpenIddictEntityFrameworkCoreResourceStore : + OpenIddictEntityFrameworkCoreResourceStore { public OpenIddictEntityFrameworkCoreResourceStore( IOpenIddictEntityFrameworkCoreContext context, @@ -34,7 +35,8 @@ public class OpenIddictEntityFrameworkCoreResourceStore : OpenIddictEntityFramew /// /// The type of the entity primary keys. public class OpenIddictEntityFrameworkCoreResourceStore< - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : OpenIddictEntityFrameworkCoreResourceStore, TKey> + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : + OpenIddictEntityFrameworkCoreResourceStore, TKey> where TKey : notnull, IEquatable { public OpenIddictEntityFrameworkCoreResourceStore( diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreScopeStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreScopeStore.cs index d2dab74c..c02bc707 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreScopeStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreScopeStore.cs @@ -19,7 +19,8 @@ namespace OpenIddict.EntityFrameworkCore; /// /// Provides methods allowing to manage the scopes stored in a database. /// -public class OpenIddictEntityFrameworkCoreScopeStore : OpenIddictEntityFrameworkCoreScopeStore +public class OpenIddictEntityFrameworkCoreScopeStore : + OpenIddictEntityFrameworkCoreScopeStore { public OpenIddictEntityFrameworkCoreScopeStore( IOpenIddictEntityFrameworkCoreContext context, @@ -34,7 +35,8 @@ public class OpenIddictEntityFrameworkCoreScopeStore : OpenIddictEntityFramework /// /// The type of the entity primary keys. public class OpenIddictEntityFrameworkCoreScopeStore< - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : OpenIddictEntityFrameworkCoreScopeStore, TKey> + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : + OpenIddictEntityFrameworkCoreScopeStore, TKey> where TKey : notnull, IEquatable { public OpenIddictEntityFrameworkCoreScopeStore( diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreSessionStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreSessionStore.cs new file mode 100644 index 00000000..51e9a9e0 --- /dev/null +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreSessionStore.cs @@ -0,0 +1,668 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.Collections.Immutable; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Text.Json; +using Microsoft.Extensions.Options; +using OpenIddict.EntityFrameworkCore.Models; +using static OpenIddict.Abstractions.OpenIddictExceptions; + +namespace OpenIddict.EntityFrameworkCore; + +/// +/// Provides methods allowing to manage the sessions stored in a database. +/// +public class OpenIddictEntityFrameworkCoreSessionStore : + OpenIddictEntityFrameworkCoreSessionStore +{ + public OpenIddictEntityFrameworkCoreSessionStore( + IOpenIddictEntityFrameworkCoreContext context, + IOptionsMonitor options) + : base(context, options) + { + } +} + +/// +/// Provides methods allowing to manage the sessions stored in a database. +/// +/// The type of the entity primary keys. +public class OpenIddictEntityFrameworkCoreSessionStore< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : + OpenIddictEntityFrameworkCoreSessionStore, + OpenIddictEntityFrameworkCoreApplication, + OpenIddictEntityFrameworkCoreAuthorization, + OpenIddictEntityFrameworkCoreToken, TKey> + where TKey : notnull, IEquatable +{ + public OpenIddictEntityFrameworkCoreSessionStore( + IOpenIddictEntityFrameworkCoreContext context, + IOptionsMonitor options) + : base(context, options) + { + } +} + +/// +/// Provides methods allowing to manage the sessions stored in a database. +/// +/// The type of the session entity. +/// The type of the application entity. +/// The type of the authorization entity. +/// The type of the token entity. +/// The type of the entity primary keys. +public class OpenIddictEntityFrameworkCoreSessionStore< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApplication, + [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 TKey : notnull, IEquatable +{ + public OpenIddictEntityFrameworkCoreSessionStore( + IOpenIddictEntityFrameworkCoreContext context, + IOptionsMonitor options) + { + Context = context ?? throw new ArgumentNullException(nameof(context)); + Options = options ?? throw new ArgumentNullException(nameof(options)); + } + + /// + /// Gets the database context associated with the current store. + /// + protected IOpenIddictEntityFrameworkCoreContext Context { get; } + + /// + /// Gets the options associated with the current store. + /// + protected IOptionsMonitor Options { get; } + + /// + public virtual async ValueTask CountAsync(CancellationToken cancellationToken) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + return await context.Set().LongCountAsync(cancellationToken); + } + + /// + public virtual async ValueTask CountAsync( + Func, TState, IQueryable> query, + TState state, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(query); + + var context = await Context.GetDbContextAsync(cancellationToken); + + return await query(context.Set(), state).LongCountAsync(cancellationToken); + } + + /// + public virtual async ValueTask CreateAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + var context = await Context.GetDbContextAsync(cancellationToken); + + context.Add(session); + + await context.SaveChangesAsync(cancellationToken); + } + + /// + public virtual async ValueTask DeleteAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + var context = await Context.GetDbContextAsync(cancellationToken); + + context.Remove(session); + + try + { + await context.SaveChangesAsync(cancellationToken); + } + + catch (DbUpdateConcurrencyException exception) + { + // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + context.Entry(session).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, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + IQueryable sessions = context.Set() + .Include(session => session.Application) + .Include(session => session.Authorization) + .AsTracking(); + + if (!string.IsNullOrEmpty(query.Subject)) + { + sessions = sessions.Where(session => session.Subject == query.Subject); + } + + if (!string.IsNullOrEmpty(query.ApplicationId)) + { + var key = ConvertIdentifierFromString(query.ApplicationId); + sessions = sessions.Where(session => session.Application!.Id!.Equals(key)); + } + + if (!string.IsNullOrEmpty(query.LoginId)) + { + sessions = sessions.Where(session => session.LoginId == query.LoginId); + } + + if (!string.IsNullOrEmpty(query.Status)) + { + sessions = sessions.Where(session => session.Status == query.Status); + } + + await foreach (var session in sessions.AsAsyncEnumerable().WithCancellation(cancellationToken)) + { + yield return session; + } + } + + /// + public virtual IAsyncEnumerable FindByApplicationIdAsync(string identifier, CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrEmpty(identifier); + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var context = await Context.GetDbContextAsync(cancellationToken); + var key = ConvertIdentifierFromString(identifier); + + await foreach (var session in + (from session in context.Set() + .Include(session => session.Application) + .Include(session => session.Authorization) + .AsTracking() + where session.Application!.Id!.Equals(key) + select session).AsAsyncEnumerable().WithCancellation(cancellationToken)) + { + yield return session; + } + } + } + + /// + public virtual IAsyncEnumerable FindByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrEmpty(identifier); + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var context = await Context.GetDbContextAsync(cancellationToken); + var key = ConvertIdentifierFromString(identifier); + + await foreach (var session in + (from session in context.Set() + .Include(session => session.Application) + .Include(session => session.Authorization) + .AsTracking() + where session.Authorization!.Id!.Equals(key) + select session).AsAsyncEnumerable().WithCancellation(cancellationToken)) + { + yield return session; + } + } + } + + /// + public virtual async ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrEmpty(identifier); + + var context = await Context.GetDbContextAsync(cancellationToken); + var key = ConvertIdentifierFromString(identifier); + + return await context.Set().FindAsync([key], cancellationToken); + } + + /// + public virtual IAsyncEnumerable FindByLoginIdAsync(string identifier, CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrEmpty(identifier); + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + await foreach (var session in + (from session in context.Set() + .Include(session => session.Application) + .Include(session => session.Authorization) + .AsTracking() + where session.LoginId == identifier + select session).AsAsyncEnumerable().WithCancellation(cancellationToken)) + { + yield return session; + } + } + } + + /// + public virtual IAsyncEnumerable FindBySubjectAsync(string subject, CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrEmpty(subject); + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + await foreach (var session in + (from session in context.Set() + .Include(session => session.Application) + .Include(session => session.Authorization) + .AsTracking() + where session.Subject == subject + select session).AsAsyncEnumerable().WithCancellation(cancellationToken)) + { + yield return session; + } + } + } + + /// + public virtual async ValueTask GetApplicationIdAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + // If the application is not attached to the session, try to load it manually. + if (session.Application is null) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + var reference = context.Entry(session).Reference(static entry => entry.Application); + if (reference.EntityEntry.State is EntityState.Detached) + { + return null; + } + + await reference.LoadAsync(cancellationToken); + } + + if (session.Application is null) + { + return null; + } + + return ConvertIdentifierToString(session.Application.Id); + } + + /// + public virtual async ValueTask GetAsync( + Func, TState, IQueryable> query, + TState state, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(query); + + var context = await Context.GetDbContextAsync(cancellationToken); + + return await query(context.Set().AsTracking(), state).FirstOrDefaultAsync(cancellationToken); + } + + /// + public virtual async ValueTask GetAuthorizationIdAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + // If the authorization is not attached to the session, try to load it manually. + if (session.Authorization is null) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + var reference = context.Entry(session).Reference(static entry => entry.Authorization); + if (reference.EntityEntry.State is EntityState.Detached) + { + return null; + } + + await reference.LoadAsync(cancellationToken); + } + + if (session.Authorization is null) + { + return null; + } + + return ConvertIdentifierToString(session.Authorization.Id); + } + + /// + public virtual ValueTask GetCreationDateAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + return new(session.CreationDate is DateTime date ? DateTime.SpecifyKind(date, DateTimeKind.Utc) : null); + } + + /// + public virtual ValueTask GetIdAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + return new(ConvertIdentifierToString(session.Id)); + } + + /// + public virtual ValueTask GetLoginIdAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + return new(session.LoginId); + } + + /// + public virtual ValueTask> GetPropertiesAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + return new(session.Properties is { Count: > 0 } properties ? [.. properties] : []); + } + + /// + public virtual ValueTask GetStatusAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + return new(session.Status); + } + + /// + public virtual ValueTask GetSubjectAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + return new(session.Subject); + } + + /// + public virtual ValueTask InstantiateAsync(CancellationToken cancellationToken) + { + try + { + return new(Activator.CreateInstance()); + } + + catch (MemberAccessException exception) + { + return new(Task.FromException( + new InvalidOperationException(SR.GetResourceString(SR.ID0240), exception))); + } + } + + /// + public virtual async IAsyncEnumerable ListAsync(int? count, int? offset, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + var query = context.Set().OrderBy(session => session.Id!).AsTracking(); + + if (offset.HasValue) + { + query = query.Skip(offset.Value); + } + + if (count.HasValue) + { + query = query.Take(count.Value); + } + + await foreach (var session in query.AsAsyncEnumerable().WithCancellation(cancellationToken)) + { + yield return session; + } + } + + /// + public virtual IAsyncEnumerable ListAsync( + Func, TState, IQueryable> query, + TState state, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(query); + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + await foreach (var session in query(context.Set().AsTracking(), state).AsAsyncEnumerable().WithCancellation(cancellationToken)) + { + yield return session; + } + } + } + + /// + public virtual async ValueTask SetApplicationIdAsync(TSession session, string? identifier, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + if (!string.IsNullOrEmpty(identifier)) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + session.Application = await context.Set() + .FindAsync([ConvertIdentifierFromString(identifier)], cancellationToken) + ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0244)); + } + + else + { + // If the application is not attached to the session, try to load it manually. + if (session.Application is null) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + var reference = context.Entry(session).Reference(static entry => entry.Application); + if (reference.EntityEntry.State is EntityState.Detached) + { + return; + } + + await reference.LoadAsync(cancellationToken); + } + + session.Application = null; + } + } + + /// + public virtual async ValueTask SetAuthorizationIdAsync(TSession session, string? identifier, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + if (!string.IsNullOrEmpty(identifier)) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + session.Authorization = await context.Set() + .FindAsync([ConvertIdentifierFromString(identifier)], cancellationToken) + ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0251)); + } + + else + { + // If the authorization is not attached to the session, try to load it manually. + if (session.Authorization is null) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + var reference = context.Entry(session).Reference(static entry => entry.Authorization); + if (reference.EntityEntry.State is EntityState.Detached) + { + return; + } + + await reference.LoadAsync(cancellationToken); + } + + session.Authorization = null; + } + } + + /// + public virtual ValueTask SetCreationDateAsync(TSession session, DateTimeOffset? date, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + session.CreationDate = date?.UtcDateTime; + + return ValueTask.CompletedTask; + } + + /// + public virtual ValueTask SetPropertiesAsync(TSession session, + ImmutableDictionary properties, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + session.Properties = properties; + + return ValueTask.CompletedTask; + } + + /// + public virtual ValueTask SetLoginIdAsync(TSession session, string? identifier, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + session.LoginId = identifier; + + return ValueTask.CompletedTask; + } + + /// + public virtual ValueTask SetStatusAsync(TSession session, string? status, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + session.Status = status; + + return ValueTask.CompletedTask; + } + + /// + public virtual ValueTask SetSubjectAsync(TSession session, string? subject, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + session.Subject = subject; + + return ValueTask.CompletedTask; + } + + /// + public virtual async ValueTask UpdateAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + var context = await Context.GetDbContextAsync(cancellationToken); + + context.Attach(session); + + // Generate a new concurrency token and attach it + // to the session before persisting the changes. + session.ConcurrencyToken = Guid.NewGuid().ToString(); + + context.Update(session); + + try + { + await context.SaveChangesAsync(cancellationToken); + } + + catch (DbUpdateConcurrencyException exception) + { + // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + context.Entry(session).State = EntityState.Unchanged; + + throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception); + } + } + + /// + /// Converts the provided identifier to a strongly typed key object. + /// + /// The identifier to convert. + /// An instance of representing the provided identifier. + public virtual TKey? ConvertIdentifierFromString(string? identifier) + { + if (string.IsNullOrEmpty(identifier)) + { + return default; + } + + // Optimization: if the key is a string, directly return it as-is. + if (typeof(TKey) == typeof(string)) + { + return (TKey?) (object?) identifier; + } + + else + { + var converter = TypeDescriptor.GetConverterFromRegisteredType(typeof(TKey)); + + return (TKey?) converter.ConvertFromInvariantString(identifier); + } + } + + /// + /// Converts the provided identifier to its string representation. + /// + /// The identifier to convert. + /// A representation of the provided identifier. + public virtual string? ConvertIdentifierToString(TKey? identifier) + { + if (Equals(identifier, default(TKey))) + { + return null; + } + + // Optimization: if the key is a string, directly return it as-is. + if (identifier is string value) + { + return value; + } + + else + { + var converter = TypeDescriptor.GetConverterFromRegisteredType(typeof(TKey)); + + return converter.ConvertToInvariantString(identifier); + } + } +} diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs index f98a9848..aed21f5a 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs @@ -143,39 +143,38 @@ public class OpenIddictEntityFrameworkCoreTokenStore< /// public virtual async IAsyncEnumerable FindAsync( - string? subject, string? client, - string? status, string? type, [EnumeratorCancellation] CancellationToken cancellationToken) + (string? Subject, string? ApplicationId, string? Status, string? Type) query, + [EnumeratorCancellation] CancellationToken cancellationToken) { var context = await Context.GetDbContextAsync(cancellationToken); - IQueryable query = context.Set() - .Include(token => token.Application) - .Include(token => token.Authorization) - .AsTracking(); + IQueryable tokens = context.Set() + .Include(token => token.Application) + .Include(token => token.Authorization) + .AsTracking(); - if (!string.IsNullOrEmpty(subject)) + if (!string.IsNullOrEmpty(query.Subject)) { - query = query.Where(token => token.Subject == subject); + tokens = tokens.Where(token => token.Subject == query.Subject); } - if (!string.IsNullOrEmpty(client)) + if (!string.IsNullOrEmpty(query.ApplicationId)) { - var key = ConvertIdentifierFromString(client); - - query = query.Where(token => token.Application!.Id!.Equals(key)); + var key = ConvertIdentifierFromString(query.ApplicationId); + tokens = tokens.Where(token => token.Application!.Id!.Equals(key)); } - if (!string.IsNullOrEmpty(status)) + if (!string.IsNullOrEmpty(query.Status)) { - query = query.Where(token => token.Status == status); + tokens = tokens.Where(token => token.Status == query.Status); } - if (!string.IsNullOrEmpty(type)) + if (!string.IsNullOrEmpty(query.Type)) { - query = query.Where(token => token.Type == type); + tokens = tokens.Where(token => token.Type == query.Type); } - await foreach (var token in query.AsAsyncEnumerable().WithCancellation(cancellationToken)) + await foreach (var token in tokens.AsAsyncEnumerable().WithCancellation(cancellationToken)) { yield return token; } @@ -274,7 +273,10 @@ public class OpenIddictEntityFrameworkCoreTokenStore< var context = await Context.GetDbContextAsync(cancellationToken); await foreach (var token in - (from token in context.Set().Include(token => token.Application).Include(token => token.Authorization).AsTracking() + (from token in context.Set() + .Include(token => token.Application) + .Include(token => token.Authorization) + .AsTracking() where token.Subject == subject select token).AsAsyncEnumerable().WithCancellation(cancellationToken)) { @@ -288,12 +290,12 @@ public class OpenIddictEntityFrameworkCoreTokenStore< { ArgumentNullException.ThrowIfNull(token); - var context = await Context.GetDbContextAsync(cancellationToken); - // If the application is not attached to the token, try to load it manually. if (token.Application is null) { - var reference = context.Entry(token).Reference(entry => entry.Application); + var context = await Context.GetDbContextAsync(cancellationToken); + + var reference = context.Entry(token).Reference(static entry => entry.Application); if (reference.EntityEntry.State is EntityState.Detached) { return null; @@ -329,12 +331,12 @@ public class OpenIddictEntityFrameworkCoreTokenStore< { ArgumentNullException.ThrowIfNull(token); - var context = await Context.GetDbContextAsync(cancellationToken); - // If the authorization is not attached to the token, try to load it manually. if (token.Authorization is null) { - var reference = context.Entry(token).Reference(entry => entry.Authorization); + var context = await Context.GetDbContextAsync(cancellationToken); + + var reference = context.Entry(token).Reference(static entry => entry.Authorization); if (reference.EntityEntry.State is EntityState.Detached) { return null; @@ -877,12 +879,13 @@ public class OpenIddictEntityFrameworkCoreTokenStore< { ArgumentNullException.ThrowIfNull(token); - var context = await Context.GetDbContextAsync(cancellationToken); - if (!string.IsNullOrEmpty(identifier)) { + var context = await Context.GetDbContextAsync(cancellationToken); + token.Application = await context.Set() - .FindAsync([ConvertIdentifierFromString(identifier)], cancellationToken); + .FindAsync([ConvertIdentifierFromString(identifier)], cancellationToken) + ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0244)); } else @@ -890,7 +893,9 @@ public class OpenIddictEntityFrameworkCoreTokenStore< // If the application is not attached to the token, try to load it manually. if (token.Application is null) { - var reference = context.Entry(token).Reference(entry => entry.Application); + var context = await Context.GetDbContextAsync(cancellationToken); + + var reference = context.Entry(token).Reference(static entry => entry.Application); if (reference.EntityEntry.State is EntityState.Detached) { return; @@ -908,12 +913,13 @@ public class OpenIddictEntityFrameworkCoreTokenStore< { ArgumentNullException.ThrowIfNull(token); - var context = await Context.GetDbContextAsync(cancellationToken); - if (!string.IsNullOrEmpty(identifier)) { + var context = await Context.GetDbContextAsync(cancellationToken); + token.Authorization = await context.Set() - .FindAsync([ConvertIdentifierFromString(identifier)], cancellationToken); + .FindAsync([ConvertIdentifierFromString(identifier)], cancellationToken) + ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0251)); } else @@ -921,7 +927,9 @@ public class OpenIddictEntityFrameworkCoreTokenStore< // If the authorization is not attached to the token, try to load it manually. if (token.Authorization is null) { - var reference = context.Entry(token).Reference(entry => entry.Authorization); + var context = await Context.GetDbContextAsync(cancellationToken); + + var reference = context.Entry(token).Reference(static entry => entry.Authorization); if (reference.EntityEntry.State is EntityState.Detached) { return; diff --git a/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbAuthorization.cs b/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbAuthorization.cs index cd71d00f..8b88341b 100644 --- a/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbAuthorization.cs +++ b/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbAuthorization.cs @@ -58,7 +58,7 @@ public class OpenIddictMongoDbAuthorization public virtual string? Status { get; set; } /// - /// Gets or sets the subject associated with the authorization. + /// Gets or sets the subject of the authorization. /// [BsonElement("subject"), BsonIgnoreIfNull] public virtual string? Subject { get; set; } diff --git a/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbSession.cs b/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbSession.cs new file mode 100644 index 00000000..be3b8dfa --- /dev/null +++ b/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbSession.cs @@ -0,0 +1,70 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.Diagnostics; + +namespace OpenIddict.MongoDb.Models; + +/// +/// Represents an OpenIddict session. +/// +[DebuggerDisplay("Id = {Id.ToString(),nq} ; Name = {Name,nq}")] +public class OpenIddictMongoDbSession +{ + /// + /// Gets or sets the identifier of the application associated with the session. + /// + [BsonElement("application_id"), BsonIgnoreIfDefault] + public virtual ObjectId ApplicationId { get; set; } + + /// + /// Gets or sets the identifier of the authorization associated with the session. + /// + [BsonElement("authorization_id"), BsonIgnoreIfDefault] + public virtual ObjectId AuthorizationId { get; set; } + + /// + /// Gets or sets the concurrency token of the session. + /// + [BsonElement("concurrency_token"), BsonIgnoreIfNull] + public virtual string? ConcurrencyToken { get; set; } = Guid.NewGuid().ToString(); + + /// + /// Gets or sets the UTC creation date of the session. + /// + [BsonElement("creation_date"), BsonIgnoreIfNull] + public virtual DateTime? CreationDate { get; set; } + + /// + /// Gets or sets the unique identifier of the session. + /// + [BsonId, BsonRequired] + public virtual ObjectId Id { get; set; } + + /// + /// Gets or sets the login identifier of the session. + /// + [BsonElement("login_id"), BsonIgnoreIfNull] + public virtual string? LoginId { get; set; } + + /// + /// Gets or sets the additional properties of the session. + /// + [BsonElement("properties"), BsonIgnoreIfNull] + public virtual BsonDocument? Properties { get; set; } + + /// + /// Gets or sets the status of the session. + /// + [BsonElement("status"), BsonIgnoreIfNull] + public virtual string? Status { get; set; } + + /// + /// Gets or sets the subject of the session. + /// + [BsonElement("subject"), BsonIgnoreIfNull] + public virtual string? Subject { get; set; } +} diff --git a/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbToken.cs b/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbToken.cs index 063759a6..4e1f8027 100644 --- a/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbToken.cs +++ b/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbToken.cs @@ -89,7 +89,7 @@ public class OpenIddictMongoDbToken public virtual string? Status { get; set; } /// - /// Gets or sets the subject associated with the token. + /// Gets or sets the subject of the token. /// [BsonElement("subject"), BsonIgnoreIfNull] public virtual string? Subject { get; set; } diff --git a/src/OpenIddict.MongoDb/OpenIddictMongoDbBuilder.cs b/src/OpenIddict.MongoDb/OpenIddictMongoDbBuilder.cs index a0a726da..afe8b8b6 100644 --- a/src/OpenIddict.MongoDb/OpenIddictMongoDbBuilder.cs +++ b/src/OpenIddict.MongoDb/OpenIddictMongoDbBuilder.cs @@ -126,6 +126,23 @@ public sealed class OpenIddictMongoDbBuilder return this; } + /// + /// Configures OpenIddict to use the specified entity as the default session entity. + /// + /// The instance. + public OpenIddictMongoDbBuilder ReplaceDefaultSessionEntity< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession>() + where TSession : OpenIddictMongoDbSession + { + Services.Replace(ServiceDescriptor.Scoped(static provider => + provider.GetRequiredService>())); + + Services.Replace(ServiceDescriptor.Scoped< + IOpenIddictSessionStore, OpenIddictMongoDbSessionStore>()); + + return this; + } + /// /// Configures OpenIddict to use the specified entity as the default token entity. /// @@ -191,6 +208,18 @@ public sealed class OpenIddictMongoDbBuilder return Configure(options => options.ScopesCollectionName = name); } + /// + /// Replaces the default sessions collection name (by default, openiddict.sessions). + /// + /// The collection name + /// The instance. + public OpenIddictMongoDbBuilder SetSessionsCollectionName(string name) + { + ArgumentException.ThrowIfNullOrEmpty(name); + + return Configure(options => options.SessionsCollectionName = name); + } + /// /// Replaces the default tokens collection name (by default, openiddict.tokens). /// diff --git a/src/OpenIddict.MongoDb/OpenIddictMongoDbExtensions.cs b/src/OpenIddict.MongoDb/OpenIddictMongoDbExtensions.cs index ff2187cd..569d42d7 100644 --- a/src/OpenIddict.MongoDb/OpenIddictMongoDbExtensions.cs +++ b/src/OpenIddict.MongoDb/OpenIddictMongoDbExtensions.cs @@ -34,6 +34,7 @@ public static class OpenIddictMongoDbExtensions .SetDefaultAuthorizationEntity() .SetDefaultResourceEntity() .SetDefaultScopeEntity() + .SetDefaultSessionEntity() .SetDefaultTokenEntity(); // Note: the Mongo stores don't depend on scoped/transient services and thus can @@ -42,6 +43,7 @@ public static class OpenIddictMongoDbExtensions .ReplaceAuthorizationStore(ServiceLifetime.Singleton) .ReplaceResourceStore(ServiceLifetime.Singleton) .ReplaceScopeStore(ServiceLifetime.Singleton) + .ReplaceSessionStore(ServiceLifetime.Singleton) .ReplaceTokenStore(ServiceLifetime.Singleton); builder.Services.TryAddSingleton(); diff --git a/src/OpenIddict.MongoDb/OpenIddictMongoDbOptions.cs b/src/OpenIddict.MongoDb/OpenIddictMongoDbOptions.cs index 799f10c2..eb09add0 100644 --- a/src/OpenIddict.MongoDb/OpenIddictMongoDbOptions.cs +++ b/src/OpenIddict.MongoDb/OpenIddictMongoDbOptions.cs @@ -37,6 +37,11 @@ public sealed class OpenIddictMongoDbOptions /// public string ScopesCollectionName { get; set; } = "openiddict.scopes"; + /// + /// Gets or sets the name of the sessions collection (by default, openiddict.sessions). + /// + public string SessionsCollectionName { get; set; } = "openiddict.sessions"; + /// /// Gets or sets the name of the tokens collection (by default, openiddict.tokens). /// diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbAuthorizationStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbAuthorizationStore.cs index 511ed63c..44b857ee 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbAuthorizationStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbAuthorizationStore.cs @@ -110,43 +110,40 @@ public class OpenIddictMongoDbAuthorizationStore< /// public virtual async IAsyncEnumerable FindAsync( - string? subject, string? client, - string? status, string? type, - ImmutableArray? scopes, [EnumeratorCancellation] CancellationToken cancellationToken) + (string? Subject, string? ApplicationId, string? Status, + string? Type, ImmutableArray? RequiredScopes) query, [EnumeratorCancellation] CancellationToken cancellationToken) { var database = await Context.GetDatabaseAsync(cancellationToken); var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); - IQueryable query = collection.AsQueryable(); + IQueryable authorizations = collection.AsQueryable(); - if (!string.IsNullOrEmpty(subject)) + if (!string.IsNullOrEmpty(query.Subject)) { - query = query.Where(authorization => authorization.Subject == subject); + authorizations = authorizations.Where(authorization => authorization.Subject == query.Subject); } - if (!string.IsNullOrEmpty(client)) + if (!string.IsNullOrEmpty(query.ApplicationId)) { - query = query.Where(authorization => authorization.ApplicationId == ObjectId.Parse(client)); + authorizations = authorizations.Where(authorization => authorization.ApplicationId == ObjectId.Parse(query.ApplicationId)); } - if (!string.IsNullOrEmpty(status)) + if (!string.IsNullOrEmpty(query.Status)) { - query = query.Where(authorization => authorization.Status == status); + authorizations = authorizations.Where(authorization => authorization.Status == query.Status); } - if (!string.IsNullOrEmpty(type)) + if (!string.IsNullOrEmpty(query.Type)) { - query = query.Where(authorization => authorization.Type == type); + authorizations = authorizations.Where(authorization => authorization.Type == query.Type); } - if (scopes is ImmutableArray values) + if (query.RequiredScopes is { IsDefaultOrEmpty: false } scopes) { - // Note: Enumerable.All() is deliberately used without the extension method syntax to ensure - // ImmutableArrayExtensions.All() (which is not supported by MongoDB) is not used instead. - query = query.Where(authorization => Enumerable.All(values, scope => authorization.Scopes!.Contains(scope))); + authorizations = authorizations.Where(authorization => scopes.All(scope => authorization.Scopes!.Contains(scope))); } - await foreach (var authorization in query.ToAsyncEnumerable().WithCancellation(cancellationToken)) + await foreach (var authorization in authorizations.ToAsyncEnumerable().WithCancellation(cancellationToken)) { yield return authorization; } @@ -459,15 +456,7 @@ public class OpenIddictMongoDbAuthorizationStore< { ArgumentNullException.ThrowIfNull(authorization); - if (!string.IsNullOrEmpty(identifier)) - { - authorization.ApplicationId = ObjectId.Parse(identifier); - } - - else - { - authorization.ApplicationId = ObjectId.Empty; - } + authorization.ApplicationId = !string.IsNullOrEmpty(identifier) ? ObjectId.Parse(identifier) : ObjectId.Empty; return ValueTask.CompletedTask; } diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbSessionStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbSessionStore.cs new file mode 100644 index 00000000..33c43c91 --- /dev/null +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbSessionStore.cs @@ -0,0 +1,499 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; +using Microsoft.Extensions.Options; +using OpenIddict.MongoDb.Models; +using static OpenIddict.Abstractions.OpenIddictExceptions; + +namespace OpenIddict.MongoDb; + +/// +/// Provides methods allowing to manage the sessions stored in a database. +/// +public class OpenIddictMongoDbSessionStore : OpenIddictMongoDbSessionStore +{ + public OpenIddictMongoDbSessionStore( + IOpenIddictMongoDbContext context, + IOptionsMonitor options) + : base(context, options) + { + } +} + +/// +/// Provides methods allowing to manage the sessions stored in a database. +/// +/// The type of the session entity. +public class OpenIddictMongoDbSessionStore< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession> : IOpenIddictSessionStore + where TSession : OpenIddictMongoDbSession +{ + public OpenIddictMongoDbSessionStore( + IOpenIddictMongoDbContext context, + IOptionsMonitor options) + { + Context = context ?? throw new ArgumentNullException(nameof(context)); + Options = options ?? throw new ArgumentNullException(nameof(options)); + } + + /// + /// Gets the database context associated with the current store. + /// + protected IOpenIddictMongoDbContext Context { get; } + + /// + /// Gets the options associated with the current store. + /// + protected IOptionsMonitor Options { get; } + + /// + public virtual async ValueTask CountAsync(CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.SessionsCollectionName); + + return await collection.CountDocumentsAsync(FilterDefinition.Empty, null, cancellationToken); + } + + /// + public virtual async ValueTask CountAsync( + Func, TState, IQueryable> query, + TState state, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(query); + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.SessionsCollectionName); + + return await query(collection.AsQueryable(), state).LongCountAsync(cancellationToken); + } + + /// + public virtual async ValueTask CreateAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.SessionsCollectionName); + + await collection.InsertOneAsync(session, null, cancellationToken); + } + + /// + public virtual async ValueTask DeleteAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.SessionsCollectionName); + + if ((await collection.DeleteOneAsync(entity => + entity.Id == session.Id && + entity.ConcurrencyToken == session.ConcurrencyToken, cancellationToken)).DeletedCount is 0) + { + throw new ConcurrencyException(SR.GetResourceString(SR.ID0239)); + } + } + + /// + public virtual async IAsyncEnumerable FindAsync( + (string? Subject, string? LoginId, string? ApplicationId, string? Status) query, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.SessionsCollectionName); + + IQueryable sessions = collection.AsQueryable(); + + if (!string.IsNullOrEmpty(query.Subject)) + { + sessions = sessions.Where(session => session.Subject == query.Subject); + } + + if (!string.IsNullOrEmpty(query.ApplicationId)) + { + sessions = sessions.Where(session => session.ApplicationId == ObjectId.Parse(query.ApplicationId)); + } + + if (!string.IsNullOrEmpty(query.LoginId)) + { + sessions = sessions.Where(session => session.LoginId == query.LoginId); + } + + if (!string.IsNullOrEmpty(query.Status)) + { + sessions = sessions.Where(session => session.Status == query.Status); + } + + await foreach (var session in sessions.ToAsyncEnumerable().WithCancellation(cancellationToken)) + { + yield return session; + } + } + + /// + public virtual IAsyncEnumerable FindByApplicationIdAsync(string identifier, CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrEmpty(identifier); + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.SessionsCollectionName); + + await foreach (var session in collection.Find(session => + session.ApplicationId == ObjectId.Parse(identifier)).ToAsyncEnumerable().WithCancellation(cancellationToken)) + { + yield return session; + } + } + } + + /// + public virtual IAsyncEnumerable FindByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrEmpty(identifier); + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.SessionsCollectionName); + + await foreach (var session in collection.Find(session => + session.AuthorizationId == ObjectId.Parse(identifier)).ToAsyncEnumerable().WithCancellation(cancellationToken)) + { + yield return session; + } + } + } + + /// + public virtual async ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrEmpty(identifier); + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.SessionsCollectionName); + + return await collection.Find(session => session.Id == ObjectId.Parse(identifier)).FirstOrDefaultAsync(cancellationToken); + } + + /// + public virtual IAsyncEnumerable FindByLoginIdAsync(string identifier, CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrEmpty(identifier); + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.SessionsCollectionName); + + await foreach (var session in collection.Find(session => session.LoginId == identifier).ToAsyncEnumerable().WithCancellation(cancellationToken)) + { + yield return session; + } + } + } + + /// + public virtual IAsyncEnumerable FindBySubjectAsync(string subject, CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrEmpty(subject); + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.SessionsCollectionName); + + await foreach (var session in collection.Find(session => session.Subject == subject).ToAsyncEnumerable().WithCancellation(cancellationToken)) + { + yield return session; + } + } + } + + /// + public virtual ValueTask GetApplicationIdAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + return new(session.ApplicationId != ObjectId.Empty ? session.ApplicationId.ToString() : null); + } + + /// + public virtual async ValueTask GetAsync( + Func, TState, IQueryable> query, + TState state, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(query); + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.SessionsCollectionName); + + return await query(collection.AsQueryable(), state).FirstOrDefaultAsync(cancellationToken); + } + + /// + public virtual ValueTask GetAuthorizationIdAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + return new(session.AuthorizationId != ObjectId.Empty ? session.AuthorizationId.ToString() : null); + } + + /// + public virtual ValueTask GetCreationDateAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + return new(session.CreationDate is DateTime date ? DateTime.SpecifyKind(date, DateTimeKind.Utc) : null); + } + + /// + public virtual ValueTask GetIdAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + return new(session.Id.ToString()); + } + + /// + public virtual ValueTask GetLoginIdAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + return new(session.LoginId); + } + + /// + public virtual ValueTask> GetPropertiesAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + if (session.Properties is null) + { + return new(ImmutableDictionary.Create()); + } + + using var document = JsonDocument.Parse(session.Properties.ToJson()); + var builder = ImmutableDictionary.CreateBuilder(); + + foreach (var property in document.RootElement.EnumerateObject()) + { + builder[property.Name] = property.Value.Clone(); + } + + return new(builder.ToImmutable()); + } + + /// + public virtual ValueTask GetStatusAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + return new(session.Status); + } + + /// + public virtual ValueTask GetSubjectAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + return new(session.Subject); + } + + /// + public virtual ValueTask InstantiateAsync(CancellationToken cancellationToken) + { + try + { + return new(Activator.CreateInstance()); + } + + catch (MemberAccessException exception) + { + return new(Task.FromException( + new InvalidOperationException(SR.GetResourceString(SR.ID0240), exception))); + } + } + + /// + public virtual async IAsyncEnumerable ListAsync( + int? count, int? offset, [EnumeratorCancellation] CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.SessionsCollectionName); + + var query = (IQueryable) collection.AsQueryable().OrderBy(session => session.Id); + + if (offset.HasValue) + { + query = query.Skip(offset.Value); + } + + if (count.HasValue) + { + query = query.Take(count.Value); + } + + await foreach (var session in ((IAsyncCursorSource) query).ToAsyncEnumerable().WithCancellation(cancellationToken)) + { + yield return session; + } + } + + /// + public virtual IAsyncEnumerable ListAsync( + Func, TState, IQueryable> query, + TState state, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(query); + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.SessionsCollectionName); + + await foreach (var element in query(collection.AsQueryable(), state).ToAsyncEnumerable().WithCancellation(cancellationToken)) + { + yield return element; + } + } + } + + /// + public virtual ValueTask SetApplicationIdAsync(TSession session, string? identifier, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + session.ApplicationId = !string.IsNullOrEmpty(identifier) ? ObjectId.Parse(identifier) : ObjectId.Empty; + + return ValueTask.CompletedTask; + } + + /// + public virtual ValueTask SetAuthorizationIdAsync(TSession session, string? identifier, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + session.AuthorizationId = !string.IsNullOrEmpty(identifier) ? ObjectId.Parse(identifier) : ObjectId.Empty; + + return ValueTask.CompletedTask; + } + + /// + public virtual ValueTask SetCreationDateAsync(TSession session, DateTimeOffset? date, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + session.CreationDate = date?.UtcDateTime; + + return ValueTask.CompletedTask; + } + + /// + public virtual ValueTask SetPropertiesAsync(TSession session, + ImmutableDictionary properties, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + if (properties is not { IsEmpty: false }) + { + session.Properties = null; + + return ValueTask.CompletedTask; + } + + using var stream = new MemoryStream(); + using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + Indented = false + }); + + writer.WriteStartObject(); + + foreach (var property in properties) + { + writer.WritePropertyName(property.Key); + property.Value.WriteTo(writer); + } + + writer.WriteEndObject(); + writer.Flush(); + + session.Properties = BsonDocument.Parse(Encoding.UTF8.GetString(stream.ToArray())); + + return ValueTask.CompletedTask; + } + + /// + public virtual ValueTask SetLoginIdAsync(TSession session, string? identifier, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + session.LoginId = identifier; + + return ValueTask.CompletedTask; + } + + /// + public virtual ValueTask SetStatusAsync(TSession session, string? status, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + session.Status = status; + + return ValueTask.CompletedTask; + } + + /// + public virtual ValueTask SetSubjectAsync(TSession session, string? subject, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + session.Subject = subject; + + return ValueTask.CompletedTask; + } + + /// + public virtual async ValueTask UpdateAsync(TSession session, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(session); + + // Generate a new concurrency token and attach it + // to the session before persisting the changes. + var timestamp = session.ConcurrencyToken; + session.ConcurrencyToken = Guid.NewGuid().ToString(); + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.SessionsCollectionName); + + if ((await collection.ReplaceOneAsync(entity => + entity.Id == session.Id && + entity.ConcurrencyToken == timestamp, session, null as ReplaceOptions, cancellationToken)).MatchedCount is 0) + { + throw new ConcurrencyException(SR.GetResourceString(SR.ID0239)); + } + } +} diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs index 59b6c74d..00d0f25f 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs @@ -106,35 +106,35 @@ public class OpenIddictMongoDbTokenStore< /// public virtual async IAsyncEnumerable FindAsync( - string? subject, string? client, - string? status, string? type, [EnumeratorCancellation] CancellationToken cancellationToken) + (string? Subject, string? ApplicationId, string? Status, string? Type) query, + [EnumeratorCancellation] CancellationToken cancellationToken) { var database = await Context.GetDatabaseAsync(cancellationToken); var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); - IQueryable query = collection.AsQueryable(); + IQueryable tokens = collection.AsQueryable(); - if (!string.IsNullOrEmpty(subject)) + if (!string.IsNullOrEmpty(query.Subject)) { - query = query.Where(token => token.Subject == subject); + tokens = tokens.Where(token => token.Subject == query.Subject); } - if (!string.IsNullOrEmpty(client)) + if (!string.IsNullOrEmpty(query.ApplicationId)) { - query = query.Where(token => token.ApplicationId == ObjectId.Parse(client)); + tokens = tokens.Where(token => token.ApplicationId == ObjectId.Parse(query.ApplicationId)); } - if (!string.IsNullOrEmpty(status)) + if (!string.IsNullOrEmpty(query.Status)) { - query = query.Where(token => token.Status == status); + tokens = tokens.Where(token => token.Status == query.Status); } - if (!string.IsNullOrEmpty(type)) + if (!string.IsNullOrEmpty(query.Type)) { - query = query.Where(token => token.Type == type); + tokens = tokens.Where(token => token.Type == query.Type); } - await foreach (var token in query.ToAsyncEnumerable().WithCancellation(cancellationToken)) + await foreach (var token in tokens.ToAsyncEnumerable().WithCancellation(cancellationToken)) { yield return token; } @@ -521,15 +521,7 @@ public class OpenIddictMongoDbTokenStore< { ArgumentNullException.ThrowIfNull(token); - if (!string.IsNullOrEmpty(identifier)) - { - token.ApplicationId = ObjectId.Parse(identifier); - } - - else - { - token.ApplicationId = ObjectId.Empty; - } + token.ApplicationId = !string.IsNullOrEmpty(identifier) ? ObjectId.Parse(identifier) : ObjectId.Empty; return ValueTask.CompletedTask; } @@ -539,15 +531,7 @@ public class OpenIddictMongoDbTokenStore< { ArgumentNullException.ThrowIfNull(token); - if (!string.IsNullOrEmpty(identifier)) - { - token.AuthorizationId = ObjectId.Parse(identifier); - } - - else - { - token.AuthorizationId = ObjectId.Empty; - } + token.AuthorizationId = !string.IsNullOrEmpty(identifier) ? ObjectId.Parse(identifier) : ObjectId.Empty; return ValueTask.CompletedTask; } diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConstants.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConstants.cs index 7fe42b02..85c6b459 100644 --- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConstants.cs +++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConstants.cs @@ -21,6 +21,7 @@ public static class OpenIddictServerDataProtectionConstants public const string Expires = ".expires"; public const string IdentityTokenLifetime = ".identity_token_lifetime"; public const string InternalAuthorizationId = ".internal_authorization_id"; + public const string InternalSessionId = ".internal_session_id"; public const string InternalTokenId = ".internal_token_id"; public const string Issued = ".issued"; public const string Nonce = ".nonce"; diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs index 5ce6913a..d792d6b7 100644 --- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs +++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs @@ -52,6 +52,7 @@ public sealed class OpenIddictServerDataProtectionFormatter : IOpenIddictServerD .SetClaim(Claims.Private.ExpirationDate, GetStringProperty(properties, Properties.Expires)) .SetClaim(Claims.Private.Nonce, GetStringProperty(properties, Properties.Nonce)) .SetClaim(Claims.Private.RedirectUri, GetStringProperty(properties, Properties.OriginalRedirectUri)) + .SetClaim(Claims.Private.SessionId, GetStringProperty(properties, Properties.InternalSessionId)) .SetClaim(Claims.Private.TokenId, GetStringProperty(properties, Properties.InternalTokenId)); static (ClaimsPrincipal principal, IReadOnlyDictionary properties) Read(BinaryReader reader) @@ -213,6 +214,7 @@ public sealed class OpenIddictServerDataProtectionFormatter : IOpenIddictServerD SetProperty(properties, Properties.HostProperties, principal.GetClaim(Claims.Private.HostProperties)); SetProperty(properties, Properties.InternalAuthorizationId, principal.GetAuthorizationId()); + SetProperty(properties, Properties.InternalSessionId, principal.GetSessionId()); SetProperty(properties, Properties.InternalTokenId, principal.GetTokenId()); SetProperty(properties, Properties.DeviceCodeId, principal.GetClaim(Claims.Private.DeviceCodeId)); @@ -225,7 +227,7 @@ public sealed class OpenIddictServerDataProtectionFormatter : IOpenIddictServerD SetArrayProperty(properties, Properties.Scopes, principal.GetScopes()); // Copy the principal and exclude the claim that were mapped to authentication properties. - principal = principal.Clone(claim => claim.Type is not ( + principal = principal.Clone(static claim => claim.Type is not ( Claims.Private.AccessTokenLifetime or Claims.Private.Audience or Claims.Private.AuthorizationCodeLifetime or @@ -244,6 +246,7 @@ public sealed class OpenIddictServerDataProtectionFormatter : IOpenIddictServerD Claims.Private.RequestTokenLifetime or Claims.Private.Resource or Claims.Private.Scope or + Claims.Private.SessionId or Claims.Private.TokenId or Claims.Private.UserCodeLifetime)); diff --git a/src/OpenIddict.Server/IOpenIddictServerHandlerFilter.cs b/src/OpenIddict.Server/IOpenIddictServerHandlerFilter.cs index fd59118a..6f13e1e9 100644 --- a/src/OpenIddict.Server/IOpenIddictServerHandlerFilter.cs +++ b/src/OpenIddict.Server/IOpenIddictServerHandlerFilter.cs @@ -19,7 +19,7 @@ public interface IOpenIddictServerHandlerFilter where TContext : Ba /// /// The context associated with the event to process. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose boolean result determines whether the handler will be invoked or not. /// ValueTask IsActiveAsync(TContext context); diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index 44a1a598..39be5ef8 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -2855,7 +2855,7 @@ public static partial class OpenIddictServerHandlers Claims.AuthenticationContextReference or Claims.Subject or Claims.Private.AuthorizationId or Claims.Private.CreationDate or Claims.Private.DeviceCodeId or Claims.Private.ExpirationDate or - Claims.Private.TokenId + Claims.Private.SessionId or Claims.Private.TokenId => values is [{ ValueType: ClaimValueTypes.String }], // The following claims MUST be represented as unique strings or array of strings. @@ -3557,7 +3557,8 @@ public static partial class OpenIddictServerHandlers if (string.Equals(claim.Type, Claims.Subject, StringComparison.OrdinalIgnoreCase) || string.Equals(claim.Type, Claims.Private.AuthorizationId, StringComparison.OrdinalIgnoreCase) || string.Equals(claim.Type, Claims.Private.Presenter, StringComparison.OrdinalIgnoreCase) || - string.Equals(claim.Type, Claims.Private.Scope, StringComparison.OrdinalIgnoreCase)) + string.Equals(claim.Type, Claims.Private.Scope, StringComparison.OrdinalIgnoreCase) || + string.Equals(claim.Type, Claims.Private.SessionId, StringComparison.OrdinalIgnoreCase)) { return true; } @@ -3963,7 +3964,8 @@ public static partial class OpenIddictServerHandlers if (string.Equals(claim.Type, Claims.Subject, StringComparison.OrdinalIgnoreCase) || string.Equals(claim.Type, Claims.Private.AuthorizationId, StringComparison.OrdinalIgnoreCase) || string.Equals(claim.Type, Claims.Private.Presenter, StringComparison.OrdinalIgnoreCase) || - string.Equals(claim.Type, Claims.Private.Scope, StringComparison.OrdinalIgnoreCase)) + string.Equals(claim.Type, Claims.Private.Scope, StringComparison.OrdinalIgnoreCase) || + string.Equals(claim.Type, Claims.Private.SessionId, StringComparison.OrdinalIgnoreCase)) { return true; } @@ -4020,7 +4022,8 @@ public static partial class OpenIddictServerHandlers if (string.Equals(claim.Type, Claims.Subject, StringComparison.OrdinalIgnoreCase) || string.Equals(claim.Type, Claims.Private.AuthorizationId, StringComparison.OrdinalIgnoreCase) || string.Equals(claim.Type, Claims.Private.Presenter, StringComparison.OrdinalIgnoreCase) || - string.Equals(claim.Type, Claims.Private.Scope, StringComparison.OrdinalIgnoreCase)) + string.Equals(claim.Type, Claims.Private.Scope, StringComparison.OrdinalIgnoreCase) || + string.Equals(claim.Type, Claims.Private.SessionId, StringComparison.OrdinalIgnoreCase)) { return true; } @@ -4541,7 +4544,8 @@ public static partial class OpenIddictServerHandlers { // Always include the following claims: if (string.Equals(claim.Type, Claims.Subject, StringComparison.OrdinalIgnoreCase) || - string.Equals(claim.Type, Claims.Private.AuthorizationId, StringComparison.OrdinalIgnoreCase)) + string.Equals(claim.Type, Claims.Private.AuthorizationId, StringComparison.OrdinalIgnoreCase) || + string.Equals(claim.Type, Claims.Private.SessionId, StringComparison.OrdinalIgnoreCase)) { return true; } @@ -4643,6 +4647,9 @@ public static partial class OpenIddictServerHandlers _ => null }); + // If available, use the internal session identifier for the standard "sid" claim. + principal.SetClaim(Claims.SessionId, context.Principal.GetClaim(Claims.Private.SessionId)); + context.IdentityTokenPrincipal = principal; } } diff --git a/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionConstants.cs b/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionConstants.cs index 9d55277a..a3d51321 100644 --- a/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionConstants.cs +++ b/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionConstants.cs @@ -21,6 +21,7 @@ public static class OpenIddictValidationDataProtectionConstants public const string HostProperties = ".host_properties"; public const string IdentityTokenLifetime = ".identity_token_lifetime"; public const string InternalAuthorizationId = ".internal_authorization_id"; + public const string InternalSessionId = ".internal_session_id"; public const string InternalTokenId = ".internal_token_id"; public const string Issued = ".issued"; public const string Nonce = ".nonce"; diff --git a/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionFormatter.cs b/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionFormatter.cs index fdca201b..1fe35d51 100644 --- a/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionFormatter.cs +++ b/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionFormatter.cs @@ -35,6 +35,7 @@ public sealed class OpenIddictValidationDataProtectionFormatter : IOpenIddictVal .SetClaim(Claims.Private.AuthorizationId, GetStringProperty(properties, Properties.InternalAuthorizationId)) .SetClaim(Claims.Private.CreationDate, GetStringProperty(properties, Properties.Issued)) .SetClaim(Claims.Private.ExpirationDate, GetStringProperty(properties, Properties.Expires)) + .SetClaim(Claims.Private.SessionId, GetStringProperty(properties, Properties.InternalSessionId)) .SetClaim(Claims.Private.TokenId, GetStringProperty(properties, Properties.InternalTokenId)); static (ClaimsPrincipal principal, IReadOnlyDictionary properties) Read(BinaryReader reader) diff --git a/src/OpenIddict.Validation/IOpenIddictValidationHandlerFilter.cs b/src/OpenIddict.Validation/IOpenIddictValidationHandlerFilter.cs index 19f3f0df..c6390bdd 100644 --- a/src/OpenIddict.Validation/IOpenIddictValidationHandlerFilter.cs +++ b/src/OpenIddict.Validation/IOpenIddictValidationHandlerFilter.cs @@ -18,7 +18,7 @@ public interface IOpenIddictValidationHandlerFilter where TContext /// /// The context associated with the event to process. /// - /// A that can be used to monitor the asynchronous operation, + /// A that can be used to monitor the asynchronous operation, /// whose boolean result determines whether the handler will be invoked or not. /// ValueTask IsActiveAsync(TContext context); diff --git a/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs b/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs index d6b5785a..2f4271d0 100644 --- a/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs +++ b/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs @@ -5780,6 +5780,72 @@ public class OpenIddictExtensionsTests Assert.Equal("42", principal.GetAuthorizationId()); } + [Fact] + public void ClaimsIdentity_GetSessionId_ThrowsAnExceptionForNullIdentity() + { + // Arrange + var identity = (ClaimsIdentity) null!; + + // Act and assert + var exception = Assert.Throws(identity.GetSessionId); + + Assert.Equal("identity", exception.ParamName); + } + + [Fact] + public void ClaimsPrincipal_GetSessionId_ThrowsAnExceptionForNullPrincipal() + { + // Arrange + var principal = (ClaimsPrincipal) null!; + + // Act and assert + var exception = Assert.Throws(principal.GetSessionId); + + Assert.Equal("principal", exception.ParamName); + } + + [Fact] + public void ClaimsIdentity_GetSessionId_ReturnsNullForMissingClaim() + { + // Arrange + var identity = new ClaimsIdentity(); + + // Act and assert + Assert.Null(identity.GetSessionId()); + } + + [Fact] + public void ClaimsPrincipal_GetSessionId_ReturnsNullForMissingClaim() + { + // Arrange + var principal = new ClaimsPrincipal(new ClaimsIdentity()); + + // Act and assert + Assert.Null(principal.GetSessionId()); + } + + [Fact] + public void ClaimsIdentity_GetSessionId_ReturnsExpectedResult() + { + // Arrange + var identity = new ClaimsIdentity(); + identity.SetClaim(Claims.Private.SessionId, "42"); + + // Act and assert + Assert.Equal("42", identity.GetSessionId()); + } + + [Fact] + public void ClaimsPrincipal_GetSessionId_ReturnsExpectedResult() + { + // Arrange + var principal = new ClaimsPrincipal(new ClaimsIdentity()); + principal.SetClaim(Claims.Private.SessionId, "42"); + + // Act and assert + Assert.Equal("42", principal.GetSessionId()); + } + [Fact] public void ClaimsIdentity_HasAudience_ThrowsAnExceptionForNullIdentity() { @@ -7278,6 +7344,88 @@ public class OpenIddictExtensionsTests Assert.Equal("42", principal.GetClaim(Claims.Private.AuthorizationId)); } + [Fact] + public void ClaimsIdentity_SetSessionId_ThrowsAnExceptionForNullIdentity() + { + // Arrange + var identity = (ClaimsIdentity) null!; + + // Act and assert + var exception = Assert.Throws(() => identity.SetSessionId(null)); + + Assert.Equal("identity", exception.ParamName); + } + + [Fact] + public void ClaimsPrincipal_SetSessionId_ThrowsAnExceptionForNullPrincipal() + { + // Arrange + var principal = (ClaimsPrincipal) null!; + + // Act and assert + var exception = Assert.Throws(() => principal.SetSessionId(null)); + + Assert.Equal("principal", exception.ParamName); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void ClaimsIdentity_SetSessionId_RemovesClaimForNullOrEmptyValue(string? value) + { + // Arrange + var identity = new ClaimsIdentity(); + identity.AddClaim(Claims.Private.SessionId, 2520); + + // Act + identity.SetSessionId(value); + + // Assert + Assert.Empty(identity.Claims); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void ClaimsPrincipal_SetSessionId_RemovesClaimForNullOrEmptyValue(string? value) + { + // Arrange + var principal = new ClaimsPrincipal(new ClaimsIdentity()); + principal.AddClaim(Claims.Private.SessionId, 2520); + + // Act + principal.SetSessionId(value); + + // Assert + Assert.Empty(principal.Claims); + } + + [Fact] + public void ClaimsIdentity_SetSessionId_AddsClaim() + { + // Arrange + var identity = new ClaimsIdentity(); + + // Act + identity.SetSessionId("42"); + + // Assert + Assert.Equal("42", identity.GetClaim(Claims.Private.SessionId)); + } + + [Fact] + public void ClaimsPrincipal_SetSessionId_AddsClaim() + { + // Arrange + var principal = new ClaimsPrincipal(new ClaimsIdentity()); + + // Act + principal.SetSessionId("42"); + + // Assert + Assert.Equal("42", principal.GetClaim(Claims.Private.SessionId)); + } + [Fact] public void ClaimsIdentity_SetTokenId_ThrowsAnExceptionForNullIdentity() { diff --git a/test/OpenIddict.Core.Tests/Caches/OpenIddictSessionCacheTests.cs b/test/OpenIddict.Core.Tests/Caches/OpenIddictSessionCacheTests.cs new file mode 100644 index 00000000..04940422 --- /dev/null +++ b/test/OpenIddict.Core.Tests/Caches/OpenIddictSessionCacheTests.cs @@ -0,0 +1,549 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace OpenIddict.Core.Tests; + +public class OpenIddictSessionCacheTests +{ + [Fact] + public void Constructor_ThrowsAnExceptionForNullOptions() + { + // Arrange + var options = (IOptionsMonitor) null!; + var store = Mock.Of>(); + + // Act and assert + var exception = Assert.Throws(() => new OpenIddictSessionCache(options, store)); + + Assert.Equal("options", exception.ParamName); + } + + [Fact] + public void Constructor_ThrowsAnExceptionForNullStore() + { + // Arrange + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions()); + var store = (IOpenIddictSessionStore) null!; + + // Act and assert + var exception = Assert.Throws(() => new OpenIddictSessionCache(options, store)); + + Assert.Equal("store", exception.ParamName); + } + + [Fact] + public async Task AddAsync_ThrowsAnExceptionForNullsession() + { + // Arrange + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions()); + var store = Mock.Of>(); + var cache = new OpenIddictSessionCache(options, store); + + // Act and assert + var exception = await Assert.ThrowsAsync( + () => cache.AddAsync(session: null!, CancellationToken.None).AsTask()); + + Assert.Equal("session", exception.ParamName); + } + + [Fact] + public void Dispose_CanBeCalledMultipleTimes() + { + // Arrange + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions()); + var store = Mock.Of>(); + var cache = new OpenIddictSessionCache(options, store); + + // Act and assert + cache.Dispose(); + cache.Dispose(); + } + + [Fact] + public async Task FindByApplicationIdAsync_QueriesStoreOnCacheMiss() + { + // Arrange + var sessions = new[] + { + new OpenIddictSession(), + new OpenIddictSession() + }; + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions()); + var store = new Mock>(); + + store.Setup(store => store.GetIdAsync(sessions[0], It.IsAny())) + .ReturnsAsync("session-id-1"); + store.Setup(store => store.GetApplicationIdAsync(sessions[0], It.IsAny())) + .ReturnsAsync("application-id"); + store.Setup(store => store.GetAuthorizationIdAsync(sessions[0], It.IsAny())) + .ReturnsAsync((string?) null); + store.Setup(store => store.GetSubjectAsync(sessions[0], It.IsAny())) + .ReturnsAsync((string?) null); + + store.Setup(store => store.GetIdAsync(sessions[1], It.IsAny())) + .ReturnsAsync("session-id-2"); + store.Setup(store => store.GetApplicationIdAsync(sessions[1], It.IsAny())) + .ReturnsAsync("application-id"); + store.Setup(store => store.GetAuthorizationIdAsync(sessions[1], It.IsAny())) + .ReturnsAsync((string?) null); + store.Setup(store => store.GetSubjectAsync(sessions[1], It.IsAny())) + .ReturnsAsync((string?) null); + + store.Setup(store => store.FindByApplicationIdAsync("application-id", It.IsAny())) + .Returns(sessions.ToAsyncEnumerable()); + + var cache = new OpenIddictSessionCache(options, store.Object); + + // Act + var results = await cache.FindByApplicationIdAsync("application-id", CancellationToken.None).ToListAsync(); + + // Assert + Assert.Equal(2, results.Count); + Assert.Contains(sessions[0], results); + Assert.Contains(sessions[1], results); + store.Verify(store => store.FindByApplicationIdAsync("application-id", It.IsAny()), Times.Once()); + } + + [Fact] + public async Task FindByApplicationIdAsync_ThrowsAnExceptionForNullIdentifier() + { + // Arrange + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions()); + var store = Mock.Of>(); + var cache = new OpenIddictSessionCache(options, store); + + // Act and assert + var exception = Assert.Throws( + () => cache.FindByApplicationIdAsync(identifier: null!, CancellationToken.None)); + + Assert.Equal("identifier", exception.ParamName); + } + + [Fact] + public async Task FindByApplicationIdAsync_ThrowsAnExceptionForEmptyIdentifier() + { + // Arrange + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions()); + var store = Mock.Of>(); + var cache = new OpenIddictSessionCache(options, store); + + // Act and assert + var exception = Assert.Throws( + () => cache.FindByApplicationIdAsync(identifier: string.Empty, CancellationToken.None)); + + Assert.Equal("identifier", exception.ParamName); + } + + [Fact] + public async Task FindByAuthorizationIdAsync_QueriesStoreOnCacheMiss() + { + // Arrange + var sessions = new[] + { + new OpenIddictSession(), + new OpenIddictSession() + }; + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions()); + var store = new Mock>(); + + store.Setup(store => store.GetIdAsync(sessions[0], It.IsAny())) + .ReturnsAsync("session-id-1"); + store.Setup(store => store.GetApplicationIdAsync(sessions[0], It.IsAny())) + .ReturnsAsync((string?) null); + store.Setup(store => store.GetAuthorizationIdAsync(sessions[0], It.IsAny())) + .ReturnsAsync("authorization-id"); + store.Setup(store => store.GetSubjectAsync(sessions[0], It.IsAny())) + .ReturnsAsync((string?) null); + + store.Setup(store => store.GetIdAsync(sessions[1], It.IsAny())) + .ReturnsAsync("session-id-2"); + store.Setup(store => store.GetApplicationIdAsync(sessions[1], It.IsAny())) + .ReturnsAsync((string?) null); + store.Setup(store => store.GetAuthorizationIdAsync(sessions[1], It.IsAny())) + .ReturnsAsync("authorization-id"); + store.Setup(store => store.GetSubjectAsync(sessions[1], It.IsAny())) + .ReturnsAsync((string?) null); + + store.Setup(store => store.FindByAuthorizationIdAsync("authorization-id", It.IsAny())) + .Returns(sessions.ToAsyncEnumerable()); + + var cache = new OpenIddictSessionCache(options, store.Object); + + // Act + var results = await cache.FindByAuthorizationIdAsync("authorization-id", CancellationToken.None).ToListAsync(); + + // Assert + Assert.Equal(2, results.Count); + Assert.Contains(sessions[0], results); + Assert.Contains(sessions[1], results); + store.Verify(store => store.FindByAuthorizationIdAsync("authorization-id", It.IsAny()), Times.Once()); + } + + [Fact] + public async Task FindByAuthorizationIdAsync_ThrowsAnExceptionForNullIdentifier() + { + // Arrange + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions()); + var store = Mock.Of>(); + var cache = new OpenIddictSessionCache(options, store); + + // Act and assert + var exception = Assert.Throws( + () => cache.FindByAuthorizationIdAsync(identifier: null!, CancellationToken.None)); + + Assert.Equal("identifier", exception.ParamName); + } + + [Fact] + public async Task FindByAuthorizationIdAsync_ThrowsAnExceptionForEmptyIdentifier() + { + // Arrange + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions()); + var store = Mock.Of>(); + var cache = new OpenIddictSessionCache(options, store); + + // Act and assert + var exception = Assert.Throws( + () => cache.FindByAuthorizationIdAsync(identifier: string.Empty, CancellationToken.None)); + + Assert.Equal("identifier", exception.ParamName); + } + + [Fact] + public async Task FindByIdAsync_ThrowsAnExceptionForNullIdentifier() + { + // Arrange + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions()); + var store = Mock.Of>(); + var cache = new OpenIddictSessionCache(options, store); + + // Act and assert + var exception = await Assert.ThrowsAsync( + () => cache.FindByIdAsync(identifier: null!, CancellationToken.None).AsTask()); + + Assert.Equal("identifier", exception.ParamName); + } + + [Fact] + public async Task FindByIdAsync_ThrowsAnExceptionForEmptyIdentifier() + { + // Arrange + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions()); + var store = Mock.Of>(); + var cache = new OpenIddictSessionCache(options, store); + + // Act and assert + var exception = await Assert.ThrowsAsync( + () => cache.FindByIdAsync(identifier: string.Empty, CancellationToken.None).AsTask()); + + Assert.Equal("identifier", exception.ParamName); + } + + [Fact] + public async Task FindByIdAsync_ReturnsCachedsessionOnCacheHit() + { + // Arrange + var session = new OpenIddictSession(); + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions()); + var store = new Mock>(); + + store.Setup(store => store.GetIdAsync(session, It.IsAny())) + .ReturnsAsync("session-id"); + + var cache = new OpenIddictSessionCache(options, store.Object); + + await cache.AddAsync(session, CancellationToken.None); + + // Act + var result = await cache.FindByIdAsync("session-id", CancellationToken.None); + + // Assert + Assert.Same(session, result); + store.Verify(store => store.FindByIdAsync("session-id", It.IsAny()), Times.Never()); + } + + [Fact] + public async Task FindByIdAsync_QueriesStoreOnCacheMiss() + { + // Arrange + var session = new OpenIddictSession(); + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions()); + var store = new Mock>(); + + store.Setup(store => store.FindByIdAsync("session-id", It.IsAny())) + .ReturnsAsync(session); + store.Setup(store => store.GetIdAsync(session, It.IsAny())) + .ReturnsAsync("session-id"); + + var cache = new OpenIddictSessionCache(options, store.Object); + + // Act + var result = await cache.FindByIdAsync("session-id", CancellationToken.None); + + // Assert + Assert.Same(session, result); + store.Verify(store => store.FindByIdAsync("session-id", It.IsAny()), Times.Once()); + } + + [Fact] + public async Task FindByIdAsync_ReturnsNullWhensessionNotFound() + { + // Arrange + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions()); + var store = new Mock>(); + + store.Setup(store => store.FindByIdAsync("session-id", It.IsAny())) + .ReturnsAsync((OpenIddictSession?) null); + + var cache = new OpenIddictSessionCache(options, store.Object); + + // Act + var result = await cache.FindByIdAsync("session-id", CancellationToken.None); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task FindByLoginIdAsync_QueriesStoreOnCacheMiss() + { + // Arrange + var sessions = new[] + { + new OpenIddictSession(), + new OpenIddictSession() + }; + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions()); + var store = new Mock>(); + + store.Setup(store => store.GetIdAsync(sessions[0], It.IsAny())) + .ReturnsAsync("session-id-1"); + store.Setup(store => store.GetApplicationIdAsync(sessions[0], It.IsAny())) + .ReturnsAsync((string?) null); + store.Setup(store => store.GetAuthorizationIdAsync(sessions[0], It.IsAny())) + .ReturnsAsync((string?) null); + store.Setup(store => store.GetLoginIdAsync(sessions[0], It.IsAny())) + .ReturnsAsync("login-id"); + store.Setup(store => store.GetSubjectAsync(sessions[0], It.IsAny())) + .ReturnsAsync("subject"); + + store.Setup(store => store.GetIdAsync(sessions[1], It.IsAny())) + .ReturnsAsync("session-id-2"); + store.Setup(store => store.GetApplicationIdAsync(sessions[1], It.IsAny())) + .ReturnsAsync((string?) null); + store.Setup(store => store.GetAuthorizationIdAsync(sessions[1], It.IsAny())) + .ReturnsAsync((string?) null); + store.Setup(store => store.GetLoginIdAsync(sessions[1], It.IsAny())) + .ReturnsAsync("login-id"); + store.Setup(store => store.GetSubjectAsync(sessions[1], It.IsAny())) + .ReturnsAsync("subject"); + + store.Setup(store => store.FindByLoginIdAsync("login-id", It.IsAny())) + .Returns(sessions.ToAsyncEnumerable()); + + var cache = new OpenIddictSessionCache(options, store.Object); + + // Act + var results = await cache.FindByLoginIdAsync("login-id", CancellationToken.None).ToListAsync(); + + // Assert + Assert.Equal(2, results.Count); + Assert.Contains(sessions[0], results); + Assert.Contains(sessions[1], results); + store.Verify(store => store.FindByLoginIdAsync("login-id", It.IsAny()), Times.Once()); + } + + [Fact] + public async Task FindByLoginIdAsync_ThrowsAnExceptionForNullIdentifier() + { + // Arrange + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions()); + var store = Mock.Of>(); + var cache = new OpenIddictSessionCache(options, store); + + // Act and assert + var exception = Assert.Throws( + () => cache.FindByLoginIdAsync(identifier: null!, CancellationToken.None)); + + Assert.Equal("identifier", exception.ParamName); + } + + [Fact] + public async Task FindByLoginIdAsync_ThrowsAnExceptionForEmptyIdentifier() + { + // Arrange + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions()); + var store = Mock.Of>(); + var cache = new OpenIddictSessionCache(options, store); + + // Act and assert + var exception = Assert.Throws( + () => cache.FindByLoginIdAsync(identifier: string.Empty, CancellationToken.None)); + + Assert.Equal("identifier", exception.ParamName); + } + + [Fact] + public async Task FindBySubjectAsync_QueriesStoreOnCacheMiss() + { + // Arrange + var sessions = new[] + { + new OpenIddictSession(), + new OpenIddictSession() + }; + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions()); + var store = new Mock>(); + + store.Setup(store => store.GetIdAsync(sessions[0], It.IsAny())) + .ReturnsAsync("session-id-1"); + store.Setup(store => store.GetApplicationIdAsync(sessions[0], It.IsAny())) + .ReturnsAsync((string?) null); + store.Setup(store => store.GetAuthorizationIdAsync(sessions[0], It.IsAny())) + .ReturnsAsync((string?) null); + store.Setup(store => store.GetSubjectAsync(sessions[0], It.IsAny())) + .ReturnsAsync("subject"); + + store.Setup(store => store.GetIdAsync(sessions[1], It.IsAny())) + .ReturnsAsync("session-id-2"); + store.Setup(store => store.GetApplicationIdAsync(sessions[1], It.IsAny())) + .ReturnsAsync((string?) null); + store.Setup(store => store.GetAuthorizationIdAsync(sessions[1], It.IsAny())) + .ReturnsAsync((string?) null); + store.Setup(store => store.GetSubjectAsync(sessions[1], It.IsAny())) + .ReturnsAsync("subject"); + + store.Setup(store => store.FindBySubjectAsync("subject", It.IsAny())) + .Returns(sessions.ToAsyncEnumerable()); + + var cache = new OpenIddictSessionCache(options, store.Object); + + // Act + var results = await cache.FindBySubjectAsync("subject", CancellationToken.None).ToListAsync(); + + // Assert + Assert.Equal(2, results.Count); + Assert.Contains(sessions[0], results); + Assert.Contains(sessions[1], results); + store.Verify(store => store.FindBySubjectAsync("subject", It.IsAny()), Times.Once()); + } + + [Fact] + public async Task FindBySubjectAsync_ThrowsAnExceptionForNullSubject() + { + // Arrange + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions()); + var store = Mock.Of>(); + var cache = new OpenIddictSessionCache(options, store); + + // Act and assert + var exception = Assert.Throws( + () => cache.FindBySubjectAsync(subject: null!, CancellationToken.None)); + + Assert.Equal("subject", exception.ParamName); + } + + [Fact] + public async Task FindBySubjectAsync_ThrowsAnExceptionForEmptySubject() + { + // Arrange + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions()); + var store = Mock.Of>(); + var cache = new OpenIddictSessionCache(options, store); + + // Act and assert + var exception = Assert.Throws( + () => cache.FindBySubjectAsync(subject: string.Empty, CancellationToken.None)); + + Assert.Equal("subject", exception.ParamName); + } + + [Fact] + public async Task RemoveAsync_ThrowsAnExceptionForNullsession() + { + // Arrange + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions()); + var store = Mock.Of>(); + var cache = new OpenIddictSessionCache(options, store); + + // Act and assert + var exception = await Assert.ThrowsAsync( + () => cache.RemoveAsync(session: null!, CancellationToken.None).AsTask()); + + Assert.Equal("session", exception.ParamName); + } + + [Fact] + public async Task RemoveAsync_InvalidatesCachedEntries() + { + // Arrange + var session = new OpenIddictSession(); + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions()); + var store = new Mock>(); + + store.Setup(store => store.GetIdAsync(session, It.IsAny())) + .ReturnsAsync("session-id"); + + var cache = new OpenIddictSessionCache(options, store.Object); + + await cache.AddAsync(session, CancellationToken.None); + + // Act + await cache.RemoveAsync(session, CancellationToken.None); + + var result = await cache.FindByIdAsync("session-id", CancellationToken.None); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task RemoveAsync_ThrowsForsessionWithoutId() + { + // Arrange + var session = new OpenIddictSession(); + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions()); + var store = new Mock>(); + + store.Setup(store => store.GetIdAsync(session, It.IsAny())) + .ReturnsAsync((string?) null); + + var cache = new OpenIddictSessionCache(options, store.Object); + + // Act and assert + await Assert.ThrowsAsync( + () => cache.RemoveAsync(session, CancellationToken.None).AsTask()); + } + + public sealed class OpenIddictSession; +} diff --git a/test/OpenIddict.Core.Tests/Managers/OpenIddictAuthorizationManagerTests.cs b/test/OpenIddict.Core.Tests/Managers/OpenIddictAuthorizationManagerTests.cs index 4bf63f1d..9cdfaa92 100644 --- a/test/OpenIddict.Core.Tests/Managers/OpenIddictAuthorizationManagerTests.cs +++ b/test/OpenIddict.Core.Tests/Managers/OpenIddictAuthorizationManagerTests.cs @@ -211,7 +211,7 @@ public class OpenIddictAuthorizationManagerTests mock => mock.CurrentValue == new OpenIddictCoreOptions { DisableEntityCaching = true }); var store = new Mock>(); - store.Setup(store => store.FindAsync("alice", null, null, null, null, It.IsAny())) + store.Setup(store => store.FindAsync(It.Is<(string?, string?, string?, string?, ImmutableArray?)>(query => query.Item1 == "alice"), It.IsAny())) .Returns(authorizations.ToAsyncEnumerable()); store.Setup(store => store.GetSubjectAsync(authorizations[0], It.IsAny())) @@ -223,7 +223,7 @@ public class OpenIddictAuthorizationManagerTests var manager = new OpenIddictAuthorizationManager(cache, logger, options, store.Object); // Act - var results = await manager.FindAsync("alice", null, null, null, null).ToListAsync(); + var results = await manager.FindAsync(("alice", null, null, null, null)).ToListAsync(); // Assert Assert.Single(results); @@ -240,8 +240,9 @@ public class OpenIddictAuthorizationManagerTests var options = Mock.Of>( mock => mock.CurrentValue == new OpenIddictCoreOptions { DisableEntityCaching = true }); var store = new Mock>(); + var scopes = ImmutableArray.Create("openid"); - store.Setup(store => store.FindAsync(null, null, null, null, ImmutableArray.Create("openid"), It.IsAny())) + store.Setup(store => store.FindAsync(It.Is<(string?, string?, string?, string?, ImmutableArray?)>(query => query.Item5 == scopes), It.IsAny())) .Returns(authorizations.ToAsyncEnumerable()); store.Setup(store => store.GetSubjectAsync(It.IsAny(), It.IsAny())) @@ -256,7 +257,7 @@ public class OpenIddictAuthorizationManagerTests var manager = new OpenIddictAuthorizationManager(cache, logger, options, store.Object); // Act - var results = await manager.FindAsync(null, null, null, null, ImmutableArray.Create("openid")).ToListAsync(); + var results = await manager.FindAsync((null, null, null, null, scopes)).ToListAsync(); // Assert Assert.Single(results); @@ -896,16 +897,9 @@ public class OpenIddictAuthorizationManagerTests var store = new Mock>(); store.Setup(store => store.FindAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny?>(), + It.IsAny<(string?, string?, string?, string?, ImmutableArray?)>(), It.IsAny())) - .Returns((string? subject, string? client, string? status, string? type, ImmutableArray? scopes, CancellationToken cancellationToken) => - { - return Enumerable.Empty().ToAsyncEnumerable(); - }); + .Returns(Enumerable.Empty().ToAsyncEnumerable()); var manager = new OpenIddictAuthorizationManager(cache, logger, options, store.Object); diff --git a/test/OpenIddict.Core.Tests/Managers/OpenIddictSessionManagerTests.cs b/test/OpenIddict.Core.Tests/Managers/OpenIddictSessionManagerTests.cs new file mode 100644 index 00000000..27b3951e --- /dev/null +++ b/test/OpenIddict.Core.Tests/Managers/OpenIddictSessionManagerTests.cs @@ -0,0 +1,861 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.Collections.Immutable; +using System.ComponentModel.DataAnnotations; +using System.Text.Json; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace OpenIddict.Core.Tests; + +public class OpenIddictSessionManagerTests +{ + [Fact] + public void Constructor_ThrowsAnExceptionForNullCache() + { + // Arrange + var cache = (IOpenIddictSessionCache) null!; + var logger = Mock.Of>>(); + var options = Mock.Of>(); + var store = Mock.Of>(); + + // Act and assert + var exception = Assert.Throws( + () => new OpenIddictSessionManager(cache, logger, options, store)); + + Assert.Equal("cache", exception.ParamName); + } + + [Fact] + public void Constructor_ThrowsAnExceptionForNullLogger() + { + // Arrange + var cache = Mock.Of>(); + var logger = (ILogger>) null!; + var options = Mock.Of>(); + var store = Mock.Of>(); + + // Act and assert + var exception = Assert.Throws( + () => new OpenIddictSessionManager(cache, logger, options, store)); + + Assert.Equal("logger", exception.ParamName); + } + + [Fact] + public void Constructor_ThrowsAnExceptionForNullOptions() + { + // Arrange + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = (IOptionsMonitor) null!; + var store = Mock.Of>(); + + // Act and assert + var exception = Assert.Throws( + () => new OpenIddictSessionManager(cache, logger, options, store)); + + Assert.Equal("options", exception.ParamName); + } + + [Fact] + public void Constructor_ThrowsAnExceptionForNullStore() + { + // Arrange + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>(); + var store = (IOpenIddictSessionStore) null!; + + // Act and assert + var exception = Assert.Throws( + () => new OpenIddictSessionManager(cache, logger, options, store)); + + Assert.Equal("store", exception.ParamName); + } + + [Fact] + public async Task CountAsync_CallsStoreMethod() + { + // Arrange + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions()); + var store = new Mock>(); + + store.Setup(store => store.CountAsync(It.IsAny())) + .ReturnsAsync(42); + + var manager = new OpenIddictSessionManager(cache, logger, options, store.Object); + + // Act + var count = await manager.CountAsync(); + + // Assert + Assert.Equal(42, count); + store.Verify(store => store.CountAsync(It.IsAny()), Times.Once()); + } + + [Fact] + public async Task CountAsync_WithQuery_ThrowsAnExceptionForNullQuery() + { + // Arrange + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>(); + var store = Mock.Of>(); + + var manager = new OpenIddictSessionManager(cache, logger, options, store); + + // Act and assert + var exception = await Assert.ThrowsAsync( + () => manager.CountAsync(query: null!).AsTask()); + + Assert.Equal("query", exception.ParamName); + } + + [Fact] + public async Task CreateAsync_ThrowsAnExceptionForNullSession() + { + // Arrange + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions()); + var store = Mock.Of>(); + + var manager = new OpenIddictSessionManager(cache, logger, options, store); + + // Act and assert + var exception = await Assert.ThrowsAsync( + () => manager.CreateAsync(session: null!).AsTask()); + + Assert.Equal("session", exception.ParamName); + } + + [Fact] + public async Task DeleteAsync_ThrowsAnExceptionForNullSession() + { + // Arrange + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions()); + var store = Mock.Of>(); + + var manager = new OpenIddictSessionManager(cache, logger, options, store); + + // Act and assert + var exception = await Assert.ThrowsAsync( + () => manager.DeleteAsync(session: null!).AsTask()); + + Assert.Equal("session", exception.ParamName); + } + + [Fact] + public async Task DeleteAsync_RemovessessionFromCache_WhenCachingIsEnabled() + { + // Arrange + var cache = new Mock>(); + var logger = Mock.Of>>(); + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions { DisableEntityCaching = false }); + var store = new Mock>(); + var session = new CustomSession(); + + var manager = new OpenIddictSessionManager(cache.Object, logger, options, store.Object); + + // Act + await manager.DeleteAsync(session); + + // Assert + cache.Verify(cache => cache.RemoveAsync(session, It.IsAny()), Times.Once()); + store.Verify(store => store.DeleteAsync(session, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task DeleteAsync_DoesNotRemoveFromCache_WhenCachingIsDisabled() + { + // Arrange + var cache = new Mock>(); + var logger = Mock.Of>>(); + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions { DisableEntityCaching = true }); + var store = new Mock>(); + var session = new CustomSession(); + + var manager = new OpenIddictSessionManager(cache.Object, logger, options, store.Object); + + // Act + await manager.DeleteAsync(session); + + // Assert + cache.Verify(cache => cache.RemoveAsync(It.IsAny(), It.IsAny()), Times.Never()); + store.Verify(store => store.DeleteAsync(session, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task FindByIdAsync_ThrowsAnExceptionForNullIdentifier() + { + // Arrange + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>(); + var store = Mock.Of>(); + + var manager = new OpenIddictSessionManager(cache, logger, options, store); + + // Act and assert + var exception = await Assert.ThrowsAsync( + () => manager.FindByIdAsync(identifier: null!).AsTask()); + + Assert.Equal("identifier", exception.ParamName); + } + + [Fact] + public async Task FindByIdAsync_ThrowsAnExceptionForEmptyIdentifier() + { + // Arrange + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>(); + var store = Mock.Of>(); + + var manager = new OpenIddictSessionManager(cache, logger, options, store); + + // Act and assert + var exception = await Assert.ThrowsAsync( + () => manager.FindByIdAsync(identifier: string.Empty).AsTask()); + + Assert.Equal("identifier", exception.ParamName); + } + + [Fact] + public async Task FindByIdAsync_UsesCache_WhenCachingIsEnabled() + { + // Arrange + var session = new CustomSession(); + var cache = new Mock>(); + var logger = Mock.Of>>(); + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions { DisableEntityCaching = false }); + var store = new Mock>(); + + cache.Setup(cache => cache.FindByIdAsync("id", It.IsAny())) + .ReturnsAsync(session); + + store.Setup(store => store.GetIdAsync(session, It.IsAny())) + .ReturnsAsync("id"); + + var manager = new OpenIddictSessionManager(cache.Object, logger, options, store.Object); + + // Act + var result = await manager.FindByIdAsync("id"); + + // Assert + Assert.Same(session, result); + cache.Verify(cache => cache.FindByIdAsync("id", It.IsAny()), Times.Once()); + store.Verify(store => store.FindByIdAsync(It.IsAny(), It.IsAny()), Times.Never()); + } + + [Fact] + public async Task FindByIdAsync_UsesStore_WhenCachingIsDisabled() + { + // Arrange + var session = new CustomSession(); + var cache = new Mock>(); + var logger = Mock.Of>>(); + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions { DisableEntityCaching = true }); + var store = new Mock>(); + + store.Setup(store => store.FindByIdAsync("id", It.IsAny())) + .ReturnsAsync(session); + + store.Setup(store => store.GetIdAsync(session, It.IsAny())) + .ReturnsAsync("id"); + + var manager = new OpenIddictSessionManager(cache.Object, logger, options, store.Object); + + // Act + var result = await manager.FindByIdAsync("id"); + + // Assert + Assert.Same(session, result); + cache.Verify(cache => cache.FindByIdAsync(It.IsAny(), It.IsAny()), Times.Never()); + store.Verify(store => store.FindByIdAsync("id", It.IsAny()), Times.Once()); + } + + [Fact] + public async Task GetAsync_WithQuery_ThrowsAnExceptionForNullQuery() + { + // Arrange + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>(); + var store = Mock.Of>(); + + var manager = new OpenIddictSessionManager(cache, logger, options, store); + + // Act and assert + var exception = await Assert.ThrowsAsync( + () => manager.GetAsync(query: null!).AsTask()); + + Assert.Equal("query", exception.ParamName); + } + + [Fact] + public async Task GetAsync_WithQueryAndState_ThrowsAnExceptionForNullQuery() + { + // Arrange + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>(); + var store = Mock.Of>(); + + var manager = new OpenIddictSessionManager(cache, logger, options, store); + + // Act and assert + var exception = await Assert.ThrowsAsync( + () => manager.GetAsync(query: null!, state: null!).AsTask()); + + Assert.Equal("query", exception.ParamName); + } + + [Fact] + public async Task GetIdAsync_ThrowsAnExceptionForNullSession() + { + // Arrange + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>(); + var store = Mock.Of>(); + + var manager = new OpenIddictSessionManager(cache, logger, options, store); + + // Act and assert + var exception = await Assert.ThrowsAsync( + () => manager.GetIdAsync(session: null!).AsTask()); + + Assert.Equal("session", exception.ParamName); + } + + [Fact] + public async Task GetIdAsync_ReturnsIdentifierFromStore() + { + // Arrange + var session = new CustomSession(); + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>(); + var store = new Mock>(); + + store.Setup(store => store.GetIdAsync(session, It.IsAny())) + .ReturnsAsync("unique-session-id"); + + var manager = new OpenIddictSessionManager(cache, logger, options, store.Object); + + // Act + var id = await manager.GetIdAsync(session); + + // Assert + Assert.Equal("unique-session-id", id); + store.Verify(store => store.GetIdAsync(session, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task GetApplicationIdAsync_ThrowsAnExceptionForNullSession() + { + // Arrange + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>(); + var store = Mock.Of>(); + + var manager = new OpenIddictSessionManager(cache, logger, options, store); + + // Act and assert + var exception = await Assert.ThrowsAsync( + () => manager.GetApplicationIdAsync(session: null!).AsTask()); + + Assert.Equal("session", exception.ParamName); + } + + [Fact] + public async Task GetApplicationIdAsync_ReturnsIdentifierFromStore() + { + // Arrange + var session = new CustomSession(); + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>(); + var store = new Mock>(); + + store.Setup(store => store.GetApplicationIdAsync(session, It.IsAny())) + .ReturnsAsync("application-id"); + + var manager = new OpenIddictSessionManager(cache, logger, options, store.Object); + + // Act + var applicationId = await manager.GetApplicationIdAsync(session); + + // Assert + Assert.Equal("application-id", applicationId); + store.Verify(store => store.GetApplicationIdAsync(session, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task GetAuthorizationIdAsync_ThrowsAnExceptionForNullSession() + { + // Arrange + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>(); + var store = Mock.Of>(); + + var manager = new OpenIddictSessionManager(cache, logger, options, store); + + // Act and assert + var exception = await Assert.ThrowsAsync( + () => manager.GetAuthorizationIdAsync(session: null!).AsTask()); + + Assert.Equal("session", exception.ParamName); + } + + [Fact] + public async Task GetAuthorizationIdAsync_ReturnsIdentifierFromStore() + { + // Arrange + var session = new CustomSession(); + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>(); + var store = new Mock>(); + + store.Setup(store => store.GetAuthorizationIdAsync(session, It.IsAny())) + .ReturnsAsync("authorization-id"); + + var manager = new OpenIddictSessionManager(cache, logger, options, store.Object); + + // Act + var authorizationId = await manager.GetAuthorizationIdAsync(session); + + // Assert + Assert.Equal("authorization-id", authorizationId); + store.Verify(store => store.GetAuthorizationIdAsync(session, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task GetCreationDateAsync_ThrowsAnExceptionForNullSession() + { + // Arrange + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>(); + var store = Mock.Of>(); + + var manager = new OpenIddictSessionManager(cache, logger, options, store); + + // Act and assert + var exception = await Assert.ThrowsAsync( + () => manager.GetCreationDateAsync(session: null!).AsTask()); + + Assert.Equal("session", exception.ParamName); + } + + [Fact] + public async Task GetCreationDateAsync_ReturnsCreationDateFromStore() + { + // Arrange + var session = new CustomSession(); + var creationDate = new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero); + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>(); + var store = new Mock>(); + + store.Setup(store => store.GetCreationDateAsync(session, It.IsAny())) + .ReturnsAsync(creationDate); + + var manager = new OpenIddictSessionManager(cache, logger, options, store.Object); + + // Act + var result = await manager.GetCreationDateAsync(session); + + // Assert + Assert.Equal(creationDate, result); + store.Verify(store => store.GetCreationDateAsync(session, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task GetLoginIdAsync_ThrowsAnExceptionForNullSession() + { + // Arrange + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>(); + var store = Mock.Of>(); + + var manager = new OpenIddictSessionManager(cache, logger, options, store); + + // Act and assert + var exception = await Assert.ThrowsAsync( + () => manager.GetLoginIdAsync(session: null!).AsTask()); + + Assert.Equal("session", exception.ParamName); + } + + [Fact] + public async Task GetLoginIdAsync_ReturnsIdentifierFromStore() + { + // Arrange + var session = new CustomSession(); + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>(); + var store = new Mock>(); + + store.Setup(store => store.GetLoginIdAsync(session, It.IsAny())) + .ReturnsAsync("login-id"); + + var manager = new OpenIddictSessionManager(cache, logger, options, store.Object); + + // Act + var loginId = await manager.GetLoginIdAsync(session); + + // Assert + Assert.Equal("login-id", loginId); + store.Verify(store => store.GetLoginIdAsync(session, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task GetPropertiesAsync_ThrowsAnExceptionForNullSession() + { + // Arrange + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>(); + var store = Mock.Of>(); + + var manager = new OpenIddictSessionManager(cache, logger, options, store); + + // Act and assert + var exception = await Assert.ThrowsAsync( + () => manager.GetPropertiesAsync(session: null!).AsTask()); + + Assert.Equal("session", exception.ParamName); + } + + [Fact] + public async Task GetPropertiesAsync_ReturnsPropertiesFromStore() + { + // Arrange + var session = new CustomSession(); + var properties = ImmutableDictionary.Empty; + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>(); + var store = new Mock>(); + + store.Setup(store => store.GetPropertiesAsync(session, It.IsAny())) + .ReturnsAsync(properties); + + var manager = new OpenIddictSessionManager(cache, logger, options, store.Object); + + // Act + var result = await manager.GetPropertiesAsync(session); + + // Assert + Assert.Same(properties, result); + store.Verify(store => store.GetPropertiesAsync(session, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task GetStatusAsync_ThrowsAnExceptionForNullSession() + { + // Arrange + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>(); + var store = Mock.Of>(); + + var manager = new OpenIddictSessionManager(cache, logger, options, store); + + // Act and assert + var exception = await Assert.ThrowsAsync( + () => manager.GetStatusAsync(session: null!).AsTask()); + + Assert.Equal("session", exception.ParamName); + } + + [Fact] + public async Task GetStatusAsync_ReturnsStatusFromStore() + { + // Arrange + var session = new CustomSession(); + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>(); + var store = new Mock>(); + + store.Setup(store => store.GetStatusAsync(session, It.IsAny())) + .ReturnsAsync(Statuses.Valid); + + var manager = new OpenIddictSessionManager(cache, logger, options, store.Object); + + // Act + var status = await manager.GetStatusAsync(session); + + // Assert + Assert.Equal(Statuses.Valid, status); + store.Verify(store => store.GetStatusAsync(session, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task GetSubjectAsync_ThrowsAnExceptionForNullSession() + { + // Arrange + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>(); + var store = Mock.Of>(); + + var manager = new OpenIddictSessionManager(cache, logger, options, store); + + // Act and assert + var exception = await Assert.ThrowsAsync( + () => manager.GetSubjectAsync(session: null!).AsTask()); + + Assert.Equal("session", exception.ParamName); + } + + [Fact] + public async Task GetSubjectAsync_ReturnsSubjectFromStore() + { + // Arrange + var session = new CustomSession(); + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>(); + var store = new Mock>(); + + store.Setup(store => store.GetSubjectAsync(session, It.IsAny())) + .ReturnsAsync("subject"); + + var manager = new OpenIddictSessionManager(cache, logger, options, store.Object); + + // Act + var subject = await manager.GetSubjectAsync(session); + + // Assert + Assert.Equal("subject", subject); + store.Verify(store => store.GetSubjectAsync(session, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ListAsync_ReturnsAllSessions() + { + // Arrange + var sessions = new[] { new CustomSession(), new CustomSession() }; + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions()); + var store = new Mock>(); + + store.Setup(store => store.ListAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(sessions.ToAsyncEnumerable()); + + var manager = new OpenIddictSessionManager(cache, logger, options, store.Object); + + // Act + var results = new List(); + await foreach (var scp in manager.ListAsync()) + { + results.Add(scp); + } + + // Assert + Assert.Equal(2, results.Count); + store.Verify(store => store.ListAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + } + + [Fact] + public async Task PopulateAsync_ThrowsAnExceptionForNullSession() + { + // Arrange + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>(); + var store = Mock.Of>(); + + var manager = new OpenIddictSessionManager(cache, logger, options, store); + var descriptor = new OpenIddictSessionDescriptor(); + + // Act and assert + var exception = await Assert.ThrowsAsync( + () => manager.PopulateAsync(session: null!, descriptor).AsTask()); + + Assert.Equal("session", exception.ParamName); + } + + [Fact] + public async Task PopulateAsync_ThrowsAnExceptionForNullDescriptor() + { + // Arrange + var session = new CustomSession(); + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>(); + var store = Mock.Of>(); + + var manager = new OpenIddictSessionManager(cache, logger, options, store); + + // Act and assert + var exception = await Assert.ThrowsAsync( + () => manager.PopulateAsync(session, descriptor: null!).AsTask()); + + Assert.Equal("descriptor", exception.ParamName); + } + + [Fact] + public async Task UpdateAsync_ThrowsAnExceptionForNullSession() + { + // Arrange + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions()); + var store = Mock.Of>(); + + var manager = new OpenIddictSessionManager(cache, logger, options, store); + + // Act and assert + var exception = await Assert.ThrowsAsync( + () => manager.UpdateAsync(session: null!).AsTask()); + + Assert.Equal("session", exception.ParamName); + } + + [Fact] + public async Task UpdateAsync_WithDescriptor_ThrowsAnExceptionForNullSession() + { + // Arrange + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions()); + var store = Mock.Of>(); + + var manager = new OpenIddictSessionManager(cache, logger, options, store); + var descriptor = new OpenIddictSessionDescriptor(); + + // Act and assert + var exception = await Assert.ThrowsAsync( + () => manager.UpdateAsync(session: null!, descriptor).AsTask()); + + Assert.Equal("session", exception.ParamName); + } + + [Fact] + public void ValidateAsync_ThrowsAnExceptionForNullSession() + { + // Arrange + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>(); + var store = Mock.Of>(); + + var manager = new OpenIddictSessionManager(cache, logger, options, store); + + // Act and assert + var exception = Assert.Throws( + () => manager.ValidateAsync(session: null!)); + + Assert.Equal("session", exception.ParamName); + } + + [Fact] + public async Task ValidateAsync_ReturnsErrorWhenStatusIsEmpty() + { + // Arrange + var session = new CustomSession(); + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions()); + var store = new Mock>(); + + store.Setup(store => store.GetStatusAsync(session, It.IsAny())) + .ReturnsAsync(string.Empty); + + var manager = new OpenIddictSessionManager(cache, logger, options, store.Object); + + // Act + var results = await manager.ValidateAsync(session).ToListAsync(); + + // Assert + Assert.Contains(results, result => result.ErrorMessage == SR.GetResourceString(SR.ID2038)); + } + + [Fact] + public async Task ValidateAsync_ReturnsErrorWhenLoginIdIsEmpty() + { + // Arrange + var session = new CustomSession(); + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions()); + var store = new Mock>(); + + store.Setup(store => store.GetStatusAsync(session, It.IsAny())) + .ReturnsAsync(Statuses.Valid); + + store.Setup(store => store.GetLoginIdAsync(session, It.IsAny())) + .ReturnsAsync(string.Empty); + + var manager = new OpenIddictSessionManager(cache, logger, options, store.Object); + + // Act + var results = await manager.ValidateAsync(session).ToListAsync(); + + // Assert + Assert.Contains(results, result => result.ErrorMessage == SR.GetResourceString(SR.ID2209)); + } + + [Fact] + public async Task ValidateAsync_ReturnsNoErrorsForValidSession() + { + // Arrange + var session = new CustomSession(); + var cache = Mock.Of>(); + var logger = Mock.Of>>(); + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictCoreOptions()); + var store = new Mock>(); + + store.Setup(store => store.GetStatusAsync(session, It.IsAny())) + .ReturnsAsync(Statuses.Valid); + + store.Setup(store => store.GetLoginIdAsync(session, It.IsAny())) + .ReturnsAsync("login-id"); + + var manager = new OpenIddictSessionManager(cache, logger, options, store.Object); + + // Act + var results = await manager.ValidateAsync(session).ToListAsync(); + + // Assert + Assert.DoesNotContain(results, static result => result != ValidationResult.Success); + } + + public class CustomSession; +} diff --git a/test/OpenIddict.Core.Tests/OpenIddictCoreBuilderTests.cs b/test/OpenIddict.Core.Tests/OpenIddictCoreBuilderTests.cs index dc89ed40..e2044ee6 100644 --- a/test/OpenIddict.Core.Tests/OpenIddictCoreBuilderTests.cs +++ b/test/OpenIddict.Core.Tests/OpenIddictCoreBuilderTests.cs @@ -205,6 +205,51 @@ public class OpenIddictCoreBuilderTests Assert.Equal(typeof(OpenGenericScopeManager<>), descriptor.ImplementationType); } + [Fact] + public void ReplaceSessionManager_ThrowsAnExceptionForClosedSourceManager() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act and assert + var exception = Assert.Throws(() => builder.ReplaceSessionManager(typeof(ClosedGenericSessionManager))); + + Assert.Equal("type", exception.ParamName); + Assert.StartsWith(SR.GetResourceString(SR.ID0232), exception.Message); + } + + [Fact] + public void ReplaceSessionManager_ThrowsAnExceptionForInvalidManager() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act and assert + var exception = Assert.Throws(() => builder.ReplaceSessionManager(typeof(object))); + + Assert.Equal("type", exception.ParamName); + Assert.StartsWith(SR.GetResourceString(SR.ID0232), exception.Message); + } + + [Fact] + public void ReplaceSessionManager_OverridesDefaultOpenGenericManager() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.ReplaceSessionManager(typeof(OpenGenericSessionManager<>)); + + // Assert + var descriptor = Assert.Single(services, service => + service.Lifetime == ServiceLifetime.Scoped && + service.ServiceType == typeof(OpenIddictSessionManager<>)); + Assert.Equal(typeof(OpenGenericSessionManager<>), descriptor.ImplementationType); + } + [Fact] public void ReplaceTokenManager_ThrowsAnExceptionForClosedSourceManager() { @@ -551,6 +596,23 @@ public class OpenIddictCoreBuilderTests service.ImplementationFactory is not null); } + [Fact] + public void SetDefaultSessionEntity_ReplacesUntypedManager() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.SetDefaultSessionEntity(); + + // Assert + Assert.Contains(services, service => + service.Lifetime == ServiceLifetime.Scoped && + service.ServiceType == typeof(IOpenIddictSessionManager) && + service.ImplementationFactory is not null); + } + [Fact] public void SetDefaultTokenEntity_ReplacesUntypedManager() { @@ -582,6 +644,7 @@ private static OpenIddictCoreBuilder CreateBuilder(IServiceCollection services) private class CustomAuthorization; private class CustomResource; private class CustomScope; + private class CustomSession; private class CustomToken; private class ClosedGenericApplicationManager : OpenIddictApplicationManager @@ -684,6 +747,31 @@ private static OpenIddictCoreBuilder CreateBuilder(IServiceCollection services) } } + private class ClosedGenericSessionManager : OpenIddictSessionManager + { + public ClosedGenericSessionManager( + IOpenIddictSessionCache cache, + ILogger> logger, + IOptionsMonitor options, + IOpenIddictSessionStore store) + : base(cache, logger, options, store) + { + } + } + + private class OpenGenericSessionManager : OpenIddictSessionManager + where TSession : class + { + public OpenGenericSessionManager( + IOpenIddictSessionCache cache, + ILogger> logger, + IOptionsMonitor options, + IOpenIddictSessionStore store) + : base(cache, logger, options, store) + { + } + } + private class ClosedGenericTokenManager : OpenIddictTokenManager { public ClosedGenericTokenManager( diff --git a/test/OpenIddict.Core.Tests/OpenIddictCoreExtensionsTests.cs b/test/OpenIddict.Core.Tests/OpenIddictCoreExtensionsTests.cs index ffac8d39..3a2c3ac4 100644 --- a/test/OpenIddict.Core.Tests/OpenIddictCoreExtensionsTests.cs +++ b/test/OpenIddict.Core.Tests/OpenIddictCoreExtensionsTests.cs @@ -71,6 +71,7 @@ public class OpenIddictCoreExtensionsTests [InlineData(typeof(OpenIddictAuthorizationManager<>))] [InlineData(typeof(OpenIddictResourceManager<>))] [InlineData(typeof(OpenIddictScopeManager<>))] + [InlineData(typeof(OpenIddictSessionManager<>))] [InlineData(typeof(OpenIddictTokenManager<>))] public void AddCore_RegistersDefaultManagers(Type type) { @@ -90,6 +91,7 @@ public class OpenIddictCoreExtensionsTests [InlineData(typeof(IOpenIddictAuthorizationManager))] [InlineData(typeof(IOpenIddictResourceManager))] [InlineData(typeof(IOpenIddictScopeManager))] + [InlineData(typeof(IOpenIddictSessionManager))] [InlineData(typeof(IOpenIddictTokenManager))] public void AddCore_RegistersUntypedProxies(Type type) { @@ -176,6 +178,24 @@ public class OpenIddictCoreExtensionsTests Assert.Equal(SR.GetResourceString(SR.ID0472), exception.Message); } + [Fact] + public void AddCore_ResolvingUntypedSessionManagerThrowsAnException() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenIddictBuilder(services); + + // Act + builder.AddCore(); + + // Assert + var provider = services.BuildServiceProvider(); + + var exception = Assert.Throws(provider.GetRequiredService); + + Assert.Equal(SR.GetResourceString(SR.ID0472), exception.Message); + } + [Fact] public void AddCore_ResolvingUntypedTokenManagerThrowsAnException() { diff --git a/test/OpenIddict.EntityFramework.Tests/OpenIddictEntityFrameworkBuilderTests.cs b/test/OpenIddict.EntityFramework.Tests/OpenIddictEntityFrameworkBuilderTests.cs index 649572a5..2c76a8fd 100644 --- a/test/OpenIddict.EntityFramework.Tests/OpenIddictEntityFrameworkBuilderTests.cs +++ b/test/OpenIddict.EntityFramework.Tests/OpenIddictEntityFrameworkBuilderTests.cs @@ -33,7 +33,7 @@ public class OpenIddictEntityFrameworkBuilderTests var builder = CreateBuilder(services); // Act - builder.ReplaceDefaultEntities(); + builder.ReplaceDefaultEntities(); // Assert Assert.Contains(services, service => @@ -52,6 +52,10 @@ public class OpenIddictEntityFrameworkBuilderTests service.Lifetime == ServiceLifetime.Scoped && service.ServiceType == typeof(IOpenIddictScopeStore) && service.ImplementationType == typeof(OpenIddictEntityFrameworkScopeStore)); + Assert.Contains(services, service => + service.Lifetime == ServiceLifetime.Scoped && + service.ServiceType == typeof(IOpenIddictSessionStore) && + service.ImplementationType == typeof(OpenIddictEntityFrameworkSessionStore)); Assert.Contains(services, service => service.Lifetime == ServiceLifetime.Scoped && service.ServiceType == typeof(IOpenIddictTokenStore) && @@ -90,6 +94,7 @@ public class OpenIddictEntityFrameworkBuilderTests public class CustomAuthorization : OpenIddictEntityFrameworkAuthorization; public class CustomResource : OpenIddictEntityFrameworkResource; public class CustomScope : OpenIddictEntityFrameworkScope; + 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 b4fb5c7d..01f66986 100644 --- a/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreBuilderTests.cs +++ b/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreBuilderTests.cs @@ -33,7 +33,7 @@ public class OpenIddictEntityFrameworkCoreBuilderTests var builder = CreateBuilder(services); // Act - builder.ReplaceDefaultEntities(); + builder.ReplaceDefaultEntities(); // Assert Assert.Contains(services, service => @@ -52,6 +52,10 @@ public class OpenIddictEntityFrameworkCoreBuilderTests service.Lifetime == ServiceLifetime.Scoped && service.ServiceType == typeof(IOpenIddictScopeStore) && service.ImplementationType == typeof(OpenIddictEntityFrameworkCoreScopeStore)); + Assert.Contains(services, service => + service.Lifetime == ServiceLifetime.Scoped && + service.ServiceType == typeof(IOpenIddictSessionStore) && + service.ImplementationType == typeof(OpenIddictEntityFrameworkCoreSessionStore)); Assert.Contains(services, service => service.Lifetime == ServiceLifetime.Scoped && service.ServiceType == typeof(IOpenIddictTokenStore) && @@ -90,6 +94,7 @@ public class OpenIddictEntityFrameworkCoreBuilderTests public class CustomAuthorization : OpenIddictEntityFrameworkCoreAuthorization; public class CustomResource : OpenIddictEntityFrameworkCoreResource; public class CustomScope : OpenIddictEntityFrameworkCoreScope; + 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 dabd1924..2470f88f 100644 --- a/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreHelpersTests.cs +++ b/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreHelpersTests.cs @@ -27,6 +27,8 @@ public class OpenIddictEntityFrameworkCoreHelpersTests .Returns(builder.Object); builder.Setup(mock => mock.ApplyConfiguration(It.IsAny>())) .Returns(builder.Object); + builder.Setup(mock => mock.ApplyConfiguration(It.IsAny>())) + .Returns(builder.Object); builder.Setup(mock => mock.ApplyConfiguration(It.IsAny>())) .Returns(builder.Object); @@ -42,6 +44,8 @@ public class OpenIddictEntityFrameworkCoreHelpersTests It.IsAny>()), Times.Once()); builder.Verify(mock => mock.ApplyConfiguration( It.IsAny>()), Times.Once()); + builder.Verify(mock => mock.ApplyConfiguration( + It.IsAny>()), Times.Once()); builder.Verify(mock => mock.ApplyConfiguration( It.IsAny>()), Times.Once()); } @@ -59,6 +63,8 @@ public class OpenIddictEntityFrameworkCoreHelpersTests .Returns(builder.Object); builder.Setup(mock => mock.ApplyConfiguration(It.IsAny>>())) .Returns(builder.Object); + builder.Setup(mock => mock.ApplyConfiguration(It.IsAny>>() )) + .Returns(builder.Object); builder.Setup(mock => mock.ApplyConfiguration(It.IsAny>>())) .Returns(builder.Object); @@ -74,6 +80,8 @@ public class OpenIddictEntityFrameworkCoreHelpersTests It.IsAny, long>>()), Times.Once()); builder.Verify(mock => mock.ApplyConfiguration( It.IsAny, long>>()), Times.Once()); + 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()); } @@ -91,11 +99,13 @@ public class OpenIddictEntityFrameworkCoreHelpersTests .Returns(builder.Object); builder.Setup(mock => mock.ApplyConfiguration(It.IsAny>())) .Returns(builder.Object); + builder.Setup(mock => mock.ApplyConfiguration(It.IsAny>())) + .Returns(builder.Object); builder.Setup(mock => mock.ApplyConfiguration(It.IsAny>())) .Returns(builder.Object); // Act - builder.Object.UseOpenIddict(); + builder.Object.UseOpenIddict(); // Assert builder.Verify(mock => mock.ApplyConfiguration( @@ -106,6 +116,8 @@ public class OpenIddictEntityFrameworkCoreHelpersTests It.IsAny>()), Times.Once()); builder.Verify(mock => mock.ApplyConfiguration( It.IsAny>()), Times.Once()); + builder.Verify(mock => mock.ApplyConfiguration( + It.IsAny>()), Times.Once()); builder.Verify(mock => mock.ApplyConfiguration( It.IsAny>()), Times.Once()); } @@ -114,5 +126,6 @@ public class OpenIddictEntityFrameworkCoreHelpersTests public class CustomAuthorization : OpenIddictEntityFrameworkCoreAuthorization; public class CustomResource : OpenIddictEntityFrameworkCoreResource; public class CustomScope : OpenIddictEntityFrameworkCoreScope; + public class CustomSession : OpenIddictEntityFrameworkCoreSession; public class CustomToken : OpenIddictEntityFrameworkCoreToken; } diff --git a/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbBuilderTests.cs b/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbBuilderTests.cs index 051e9be6..7ca3cde7 100644 --- a/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbBuilderTests.cs +++ b/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbBuilderTests.cs @@ -95,6 +95,23 @@ public class OpenIddictMongoDbBuilderTests service.ImplementationType == typeof(OpenIddictMongoDbScopeStore)); } + [Fact] + public void ReplaceDefaultSessionEntity_StoreIsCorrectlyReplaced() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.ReplaceDefaultSessionEntity(); + + // Assert + Assert.Contains(services, service => + service.Lifetime == ServiceLifetime.Scoped && + service.ServiceType == typeof(IOpenIddictSessionStore) && + service.ImplementationType == typeof(OpenIddictMongoDbSessionStore)); + } + [Fact] public void ReplaceDefaultTokenEntity_StoreIsCorrectlyReplaced() { @@ -240,6 +257,38 @@ public class OpenIddictMongoDbBuilderTests Assert.Equal("custom_collection", options.ScopesCollectionName); } + [Theory] + [InlineData(null)] + [InlineData("")] + public void SetSessionsCollectionName_ThrowsAnExceptionForNullOrEmptyCollectionName(string? name) + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act and assert + var exception = Assert.ThrowsAny(() => builder.SetSessionsCollectionName(name!)); + + Assert.Equal("name", exception.ParamName); + } + + [Fact] + public void SetSessionsCollectionName_CollectionNameIsCorrectlySet() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.SetSessionsCollectionName("custom_collection"); + + // Assert + var provider = services.BuildServiceProvider(); + var options = provider.GetRequiredService>().CurrentValue; + + Assert.Equal("custom_collection", options.SessionsCollectionName); + } + [Theory] [InlineData(null)] [InlineData("")] @@ -321,5 +370,6 @@ public class OpenIddictMongoDbBuilderTests public class CustomAuthorization : OpenIddictMongoDbAuthorization; public class CustomResource : OpenIddictMongoDbResource; public class CustomScope : OpenIddictMongoDbScope; + public class CustomSession : OpenIddictMongoDbSession; public class CustomToken : OpenIddictMongoDbToken; } diff --git a/test/OpenIddict.Server.DataProtection.Tests/OpenIddictServerBuilderTests.cs b/test/OpenIddict.Server.DataProtection.Tests/OpenIddictServerDataProtectionFormatterTests.cs similarity index 100% rename from test/OpenIddict.Server.DataProtection.Tests/OpenIddictServerBuilderTests.cs rename to test/OpenIddict.Server.DataProtection.Tests/OpenIddictServerDataProtectionFormatterTests.cs