Browse Source

Introduce a new session entity and store the session identifier in the tokens

pull/2521/head
Kévin Chalet 2 months ago
parent
commit
4f6882bc0a
  1. 118
      sandbox/OpenIddict.Sandbox.AspNet.Server/App_Start/IdentityConfig.cs
  2. 123
      sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs
  3. 21
      sandbox/OpenIddict.Sandbox.AspNet.Server/Models/IdentityModels.cs
  4. 5
      sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs
  5. 136
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs
  6. 16
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Program.cs
  7. 36
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Services/UserClaimsPrincipalFactory.cs
  8. 12
      src/OpenIddict.Abstractions/Caches/IOpenIddictAuthorizationCache.cs
  9. 82
      src/OpenIddict.Abstractions/Caches/IOpenIddictSessionCache.cs
  10. 16
      src/OpenIddict.Abstractions/Caches/IOpenIddictTokenCache.cs
  11. 36
      src/OpenIddict.Abstractions/Descriptors/OpenIddictApplicationDescriptor.cs
  12. 22
      src/OpenIddict.Abstractions/Descriptors/OpenIddictAuthorizationDescriptor.cs
  13. 12
      src/OpenIddict.Abstractions/Descriptors/OpenIddictResourceDescriptor.cs
  14. 14
      src/OpenIddict.Abstractions/Descriptors/OpenIddictScopeDescriptor.cs
  15. 53
      src/OpenIddict.Abstractions/Descriptors/OpenIddictSessionDescriptor.cs
  16. 32
      src/OpenIddict.Abstractions/Descriptors/OpenIddictTokenDescriptor.cs
  17. 26
      src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs
  18. 59
      src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs
  19. 16
      src/OpenIddict.Abstractions/Managers/IOpenIddictResourceManager.cs
  20. 16
      src/OpenIddict.Abstractions/Managers/IOpenIddictScopeManager.cs
  21. 346
      src/OpenIddict.Abstractions/Managers/IOpenIddictSessionManager.cs
  22. 33
      src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs
  23. 2
      src/OpenIddict.Abstractions/OpenIddictConstants.cs
  24. 10
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  25. 34
      src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs
  26. 10
      src/OpenIddict.Abstractions/Stores/IOpenIddictApplicationStore.cs
  27. 21
      src/OpenIddict.Abstractions/Stores/IOpenIddictAuthorizationStore.cs
  28. 10
      src/OpenIddict.Abstractions/Stores/IOpenIddictResourceStore.cs
  29. 10
      src/OpenIddict.Abstractions/Stores/IOpenIddictScopeStore.cs
  30. 319
      src/OpenIddict.Abstractions/Stores/IOpenIddictSessionStore.cs
  31. 26
      src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs
  32. 2
      src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionFormatter.cs
  33. 2
      src/OpenIddict.Client/IOpenIddictClientHandlerFilter.cs
  34. 6
      src/OpenIddict.Client/OpenIddictClientHandlers.cs
  35. 2
      src/OpenIddict.Core/Caches/OpenIddictApplicationCache.cs
  36. 9
      src/OpenIddict.Core/Caches/OpenIddictAuthorizationCache.cs
  37. 2
      src/OpenIddict.Core/Caches/OpenIddictResourceCache.cs
  38. 2
      src/OpenIddict.Core/Caches/OpenIddictScopeCache.cs
  39. 383
      src/OpenIddict.Core/Caches/OpenIddictSessionCache.cs
  40. 8
      src/OpenIddict.Core/Caches/OpenIddictTokenCache.cs
  41. 28
      src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
  42. 105
      src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
  43. 16
      src/OpenIddict.Core/Managers/OpenIddictResourceManager.cs
  44. 16
      src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs
  45. 919
      src/OpenIddict.Core/Managers/OpenIddictSessionManager.cs
  46. 50
      src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
  47. 93
      src/OpenIddict.Core/OpenIddictCoreBuilder.cs
  48. 4
      src/OpenIddict.Core/OpenIddictCoreExtensions.cs
  49. 5
      src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkApplication.cs
  50. 5
      src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkAuthorization.cs
  51. 3
      src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkResource.cs
  52. 3
      src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkScope.cs
  53. 77
      src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkSession.cs
  54. 7
      src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkToken.cs
  55. 74
      src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkSessionConfiguration.cs
  56. 9
      src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkBuilder.cs
  57. 2
      src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkExtensions.cs
  58. 8
      src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkHelpers.cs
  59. 51
      src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkAuthorizationStore.cs
  60. 706
      src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkSessionStore.cs
  61. 61
      src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs
  62. 10
      src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreApplication.cs
  63. 10
      src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreAuthorization.cs
  64. 3
      src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreResource.cs
  65. 3
      src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreScope.cs
  66. 85
      src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreSession.cs
  67. 14
      src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreToken.cs
  68. 85
      src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreSessionConfiguration.cs
  69. 7
      src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs
  70. 4
      src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreCustomizer.cs
  71. 2
      src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreExtensions.cs
  72. 21
      src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs
  73. 54
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs
  74. 6
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreResourceStore.cs
  75. 6
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreScopeStore.cs
  76. 668
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreSessionStore.cs
  77. 72
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs
  78. 2
      src/OpenIddict.MongoDb.Models/OpenIddictMongoDbAuthorization.cs
  79. 70
      src/OpenIddict.MongoDb.Models/OpenIddictMongoDbSession.cs
  80. 2
      src/OpenIddict.MongoDb.Models/OpenIddictMongoDbToken.cs
  81. 29
      src/OpenIddict.MongoDb/OpenIddictMongoDbBuilder.cs
  82. 2
      src/OpenIddict.MongoDb/OpenIddictMongoDbExtensions.cs
  83. 5
      src/OpenIddict.MongoDb/OpenIddictMongoDbOptions.cs
  84. 41
      src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbAuthorizationStore.cs
  85. 499
      src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbSessionStore.cs
  86. 44
      src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs
  87. 1
      src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConstants.cs
  88. 5
      src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs
  89. 2
      src/OpenIddict.Server/IOpenIddictServerHandlerFilter.cs
  90. 17
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  91. 1
      src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionConstants.cs
  92. 1
      src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionFormatter.cs
  93. 2
      src/OpenIddict.Validation/IOpenIddictValidationHandlerFilter.cs
  94. 148
      test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs
  95. 549
      test/OpenIddict.Core.Tests/Caches/OpenIddictSessionCacheTests.cs
  96. 20
      test/OpenIddict.Core.Tests/Managers/OpenIddictAuthorizationManagerTests.cs
  97. 861
      test/OpenIddict.Core.Tests/Managers/OpenIddictSessionManagerTests.cs
  98. 88
      test/OpenIddict.Core.Tests/OpenIddictCoreBuilderTests.cs
  99. 20
      test/OpenIddict.Core.Tests/OpenIddictCoreExtensionsTests.cs
  100. 7
      test/OpenIddict.EntityFramework.Tests/OpenIddictEntityFrameworkBuilderTests.cs

118
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<ApplicationUser>
{
public ApplicationUserManager(IUserStore<ApplicationUser> store)
@ -39,14 +34,13 @@ public class ApplicationUserManager : UserManager<ApplicationUser>
public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
{
var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(context.Get<ApplicationDbContext>()));
// Configurer la logique de validation pour les noms d'utilisateur
manager.UserValidator = new UserValidator<ApplicationUser>(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<ApplicationUser>
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<ApplicationUser>
{
MessageFormat = "Votre code de sécurité est {0}"
@ -84,7 +75,6 @@ public class ApplicationUserManager : UserManager<ApplicationUser>
}
}
// Configurer le gestionnaire de connexion d'application qui est utilisé dans cette application.
public class ApplicationSignInManager : SignInManager<ApplicationUser, string>
{
public ApplicationSignInManager(ApplicationUserManager userManager, IAuthenticationManager authenticationManager)
@ -101,4 +91,98 @@ public class ApplicationSignInManager : SignInManager<ApplicationUser, string>
{
return new ApplicationSignInManager(context.GetUserManager<ApplicationUserManager>(), 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<ApplicationUserManager>()
?? 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<char>(value),
right: MemoryMarshal.AsBytes<char>(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<byte> left, ReadOnlySpan<byte> 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;
}
}
}

123
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<ApplicationUserManager>().FindByIdAsync(result.Identity.GetUserId())
?? throw new InvalidOperationException("The user details cannot be retrieved.");
var user = await context.GetUserManager<ApplicationUserManager>().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<ApplicationUserManager>().FindByIdAsync(result.Identity.GetUserId())
?? throw new InvalidOperationException("The user details cannot be retrieved.");
var user = await context.GetUserManager<ApplicationUserManager>().FindByIdAsync(result.Identity.GetUserId());
if (user is null)
{
context.Authentication.Challenge(
authenticationTypes: OpenIddictServerOwinDefaults.AuthenticationType,
properties: new AuthenticationProperties(new Dictionary<string, string?>
{
[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);

21
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<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> 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;
}
}

5
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<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
OnValidateIdentity = ApplicationSignInManager.OnValidateIdentity
}
});

136
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<ApplicationUser> _signInManager;
private readonly UserManager<ApplicationUser> _userManager;
@ -37,6 +38,7 @@ public class AuthorizationController : Controller
IOpenIddictAuthorizationManager authorizationManager,
OpenIddictClientService clientService,
IOpenIddictScopeManager scopeManager,
IOpenIddictSessionManager sessionManager,
SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> 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<string, string?>
{
[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<IActionResult> 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<string, string?>
{
[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);

16
sandbox/OpenIddict.Sandbox.AspNetCore.Server/Program.cs

@ -31,9 +31,25 @@ builder.Services.AddDbContext<ApplicationDbContext>(options =>
// Register the Identity builder.Services.
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
.AddClaimsPrincipalFactory<UserClaimsPrincipalFactory>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Note: ASP.NET Core Identity doesn't store a unique identifier representing user sessions.
//
// To work around that, a custom IUserClaimsPrincipalFactory<TUser> 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<SecurityStampValidatorOptions>(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 =>

36
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<ApplicationUser>
{
public UserClaimsPrincipalFactory(
UserManager<ApplicationUser> userManager,
IOptions<IdentityOptions> optionsAccessor)
: base(userManager, optionsAccessor)
{
}
protected override async Task<ClaimsIdentity> 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;
}
}

12
src/OpenIddict.Abstractions/Caches/IOpenIddictAuthorizationCache.cs

@ -23,18 +23,14 @@ public interface IOpenIddictAuthorizationCache<TAuthorization> where TAuthorizat
ValueTask AddAsync(TAuthorization authorization, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the authorizations matching the specified parameters.
/// Retrieves the authorizations matching the specified query.
/// </summary>
/// <param name="subject">The subject associated with the authorization, or <see langword="null"/> not to filter out specific subjects.</param>
/// <param name="client">The client associated with the authorization, or <see langword="null"/> not to filter out specific clients.</param>
/// <param name="status">The authorization status, or <see langword="null"/> not to filter out specific authorization statuses.</param>
/// <param name="type">The authorization type, or <see langword="null"/> not to filter out specific authorization types.</param>
/// <param name="scopes">The minimal scopes associated with the authorization, or <see langword="null"/> not to filter out scopes.</param>
/// <param name="query">The query parameters: if a parameter is <see langword="null"/>, it will not be used to filter the results.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The authorizations corresponding to the criteria.</returns>
IAsyncEnumerable<TAuthorization> FindAsync(
string? subject, string? client, string? status,
string? type, ImmutableArray<string>? scopes, CancellationToken cancellationToken);
(string? Subject, string? ApplicationId, string? Status,
string? Type, ImmutableArray<string>? RequiredScopes) query, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the list of authorizations corresponding to the specified application identifier.

82
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;
/// <summary>
/// Provides methods allowing to cache sessions after retrieving them from the store.
/// </summary>
/// <typeparam name="TSession">The type of the session entity.</typeparam>
public interface IOpenIddictSessionCache<TSession> where TSession : class
{
/// <summary>
/// Add the specified session to the cache.
/// </summary>
/// <param name="session">The session to add to the cache.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
ValueTask AddAsync(TSession session, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the sessions matching the specified query.
/// </summary>
/// <param name="query">The query parameters: if a parameter is <see langword="null"/>, it will not be used to filter the results.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The sessions corresponding to the criteria.</returns>
IAsyncEnumerable<TSession> FindAsync(
(string? Subject, string? LoginId, string? ApplicationId, string? Status) query, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the list of sessions corresponding to the specified application identifier.
/// </summary>
/// <param name="identifier">The application identifier associated with the sessions.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The sessions corresponding to the specified application.</returns>
IAsyncEnumerable<TSession> FindByApplicationIdAsync(string identifier, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the list of sessions corresponding to the specified authorization identifier.
/// </summary>
/// <param name="identifier">The authorization identifier associated with the sessions.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The sessions corresponding to the specified authorization.</returns>
IAsyncEnumerable<TSession> FindByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the list of sessions corresponding to the specified login identifier.
/// </summary>
/// <param name="identifier">The login identifier associated with the sessions.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The sessions corresponding to the specified login identifier.</returns>
IAsyncEnumerable<TSession> FindByLoginIdAsync(string identifier, CancellationToken cancellationToken);
/// <summary>
/// Retrieves a session using its unique identifier.
/// </summary>
/// <param name="identifier">The unique identifier associated with the session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the session corresponding to the identifier.
/// </returns>
ValueTask<TSession?> FindByIdAsync(string identifier, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the list of sessions corresponding to the specified subject.
/// </summary>
/// <param name="subject">The subject associated with the sessions.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The sessions corresponding to the specified subject.</returns>
IAsyncEnumerable<TSession> FindBySubjectAsync(string subject, CancellationToken cancellationToken);
/// <summary>
/// Removes the specified session from the cache.
/// </summary>
/// <param name="session">The session to remove from the cache.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
ValueTask RemoveAsync(TSession session, CancellationToken cancellationToken);
}

16
src/OpenIddict.Abstractions/Caches/IOpenIddictTokenCache.cs

@ -21,17 +21,13 @@ public interface IOpenIddictTokenCache<TToken> where TToken : class
ValueTask AddAsync(TToken token, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the tokens matching the specified parameters.
/// Retrieves the tokens matching the specified query.
/// </summary>
/// <param name="subject">The subject associated with the token, or <see langword="null"/> not to filter out specific subjects.</param>
/// <param name="client">The client associated with the token, or <see langword="null"/> not to filter out specific clients.</param>
/// <param name="status">The token status, or <see langword="null"/> not to filter out specific token statuses.</param>
/// <param name="type">The token type, or <see langword="null"/> not to filter out specific token types.</param>
/// <param name="query">The query parameters: if a parameter is <see langword="null"/>, it will not be used to filter the results.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The tokens corresponding to the criteria.</returns>
IAsyncEnumerable<TToken> FindAsync(
string? subject, string? client,
string? status, string? type, CancellationToken cancellationToken);
(string? Subject, string? ApplicationId, string? Status, string? Type) query, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the list of tokens corresponding to the specified application identifier.
@ -61,9 +57,11 @@ public interface IOpenIddictTokenCache<TToken> where TToken : class
ValueTask<TToken?> FindByIdAsync(string identifier, CancellationToken cancellationToken);
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// Note: the reference identifier may be hashed or encrypted for security reasons.
/// </remarks>
/// <param name="identifier">The reference identifier associated with the tokens.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>

36
src/OpenIddict.Abstractions/Descriptors/OpenIddictApplicationDescriptor.cs

@ -10,80 +10,84 @@ namespace OpenIddict.Abstractions;
public class OpenIddictApplicationDescriptor
{
/// <summary>
/// Gets or sets the application type associated with the application.
/// Gets or sets the application type of the application.
/// </summary>
public string? ApplicationType { get; set; }
/// <summary>
/// Gets or sets the client identifier associated with the application.
/// Gets or sets the client identifier of the application.
/// </summary>
public string? ClientId { get; set; }
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// <para>
/// Note: depending on the application manager used to create this instance,
/// this property may be hashed or encrypted for security reasons.
/// </para>
/// <para>
/// 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.
/// </para>
/// </remarks>
public string? ClientSecret { get; set; }
/// <summary>
/// Gets or sets the client type associated with the application.
/// Gets or sets the client type of the application.
/// </summary>
public string? ClientType { get; set; }
/// <summary>
/// Gets or sets the consent type associated with the application.
/// Gets or sets the consent type of the application.
/// </summary>
public string? ConsentType { get; set; }
/// <summary>
/// Gets or sets the display name associated with the application.
/// Gets or sets the display name of the application.
/// </summary>
public string? DisplayName { get; set; }
/// <summary>
/// Gets the localized display names associated with the application.
/// Gets the localized display names of the application.
/// </summary>
public Dictionary<CultureInfo, string> DisplayNames { get; } = [];
/// <summary>
/// Gets or sets the JSON Web Key Set associated with the application.
/// Gets or sets the JSON Web Key Set of the application.
/// </summary>
public JsonWebKeySet? JsonWebKeySet { get; set; }
/// <summary>
/// Gets the permissions associated with the application.
/// Gets the permissions of the application.
/// </summary>
public HashSet<string> Permissions { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets the post-logout redirect URIs associated with the application.
/// Gets the post-logout redirect URIs of the application.
/// </summary>
public HashSet<Uri> PostLogoutRedirectUris { get; } = [];
/// <summary>
/// Gets the additional properties associated with the application.
/// Gets the additional properties of the application.
/// </summary>
public Dictionary<string, JsonElement> Properties { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets the redirect URIs associated with the application.
/// Gets the redirect URIs of the application.
/// </summary>
public HashSet<Uri> RedirectUris { get; } = [];
/// <summary>
/// Gets the requirements associated with the application.
/// Gets the requirements of the application.
/// </summary>
public HashSet<string> Requirements { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets the settings associated with the application.
/// Gets the settings of the application.
/// </summary>
public Dictionary<string, string> Settings { get; } = new(StringComparer.Ordinal);

22
src/OpenIddict.Abstractions/Descriptors/OpenIddictAuthorizationDescriptor.cs

@ -9,38 +9,40 @@ namespace OpenIddict.Abstractions;
public class OpenIddictAuthorizationDescriptor
{
/// <summary>
/// Gets or sets the application identifier associated with the authorization.
/// Gets or sets the identifier of the application associated with the authorization.
/// </summary>
public string? ApplicationId { get; set; }
/// <summary>
/// Gets or sets the creation date associated with the authorization.
/// Gets or sets the creation date of the authorization.
/// </summary>
public DateTimeOffset? CreationDate { get; set; }
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// Note: this property is not stored by the default stores.
/// </remarks>
public ClaimsPrincipal? Principal { get; set; }
/// <summary>
/// Gets the additional properties associated with the authorization.
/// Gets or sets the additional properties of the authorization.
/// </summary>
public Dictionary<string, JsonElement> Properties { get; } = new(StringComparer.Ordinal);
public Dictionary<string, JsonElement> Properties { get; set; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets the scopes associated with the authorization.
/// Gets or sets the scopes of the authorization.
/// </summary>
public HashSet<string> Scopes { get; } = new(StringComparer.Ordinal);
public HashSet<string> Scopes { get; set; } = [];
/// <summary>
/// Gets or sets the status associated with the authorization.
/// Gets or sets the status of the authorization.
/// </summary>
public string? Status { get; set; }
/// <summary>
/// Gets or sets the subject associated with the authorization.
/// Gets or sets the subject of the authorization.
/// </summary>
public string? Subject { get; set; }

12
src/OpenIddict.Abstractions/Descriptors/OpenIddictResourceDescriptor.cs

@ -9,32 +9,32 @@ namespace OpenIddict.Abstractions;
public class OpenIddictResourceDescriptor
{
/// <summary>
/// Gets or sets the description associated with the resource.
/// Gets or sets the description of the resource.
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Gets the localized descriptions associated with the resource.
/// Gets the localized descriptions of the resource.
/// </summary>
public Dictionary<CultureInfo, string> Descriptions { get; } = [];
/// <summary>
/// Gets or sets the display name associated with the resource.
/// Gets or sets the display name of the resource.
/// </summary>
public string? DisplayName { get; set; }
/// <summary>
/// Gets the localized display names associated with the resource.
/// Gets the localized display names of the resource.
/// </summary>
public Dictionary<CultureInfo, string> DisplayNames { get; } = [];
/// <summary>
/// Gets or sets the unique name associated with the resource.
/// Gets or sets the unique name of the resource.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Gets the additional properties associated with the resource.
/// Gets the additional properties of the resource.
/// </summary>
public Dictionary<string, JsonElement> Properties { get; } = new(StringComparer.Ordinal);
}

14
src/OpenIddict.Abstractions/Descriptors/OpenIddictScopeDescriptor.cs

@ -9,37 +9,37 @@ namespace OpenIddict.Abstractions;
public class OpenIddictScopeDescriptor
{
/// <summary>
/// Gets or sets the description associated with the scope.
/// Gets or sets the description of the scope.
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Gets the localized descriptions associated with the scope.
/// Gets the localized descriptions of the scope.
/// </summary>
public Dictionary<CultureInfo, string> Descriptions { get; } = [];
/// <summary>
/// Gets or sets the display name associated with the scope.
/// Gets or sets the display name of the scope.
/// </summary>
public string? DisplayName { get; set; }
/// <summary>
/// Gets the localized display names associated with the scope.
/// Gets the localized display names of the scope.
/// </summary>
public Dictionary<CultureInfo, string> DisplayNames { get; } = [];
/// <summary>
/// Gets or sets the unique name associated with the scope.
/// Gets or sets the unique name of the scope.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Gets the additional properties associated with the scope.
/// Gets the additional properties of the scope.
/// </summary>
public Dictionary<string, JsonElement> Properties { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets the resources associated with the scope.
/// Gets the resources of the scope.
/// </summary>
public HashSet<string> Resources { get; } = new(StringComparer.Ordinal);
}

53
src/OpenIddict.Abstractions/Descriptors/OpenIddictSessionDescriptor.cs

@ -0,0 +1,53 @@
using System.Security.Claims;
using System.Text.Json;
namespace OpenIddict.Abstractions;
/// <summary>
/// Represents an OpenIddict session descriptor.
/// </summary>
public class OpenIddictSessionDescriptor
{
/// <summary>
/// Gets or sets the identifier of the application associated with the session.
/// </summary>
public string? ApplicationId { get; set; }
/// <summary>
/// Gets or sets the identifier of the authorization associated with the session.
/// </summary>
public string? AuthorizationId { get; set; }
/// <summary>
/// Gets or sets the creation date of the session.
/// </summary>
public DateTimeOffset? CreationDate { get; set; }
/// <summary>
/// Gets or sets the login identifier of the session.
/// </summary>
public string? LoginId { get; set; }
/// <summary>
/// Gets or sets the optional principal specified by the caller.
/// </summary>
/// <remarks>
/// Note: this property is not stored by the default stores.
/// </remarks>
public ClaimsPrincipal? Principal { get; set; }
/// <summary>
/// Gets the additional properties of the session.
/// </summary>
public Dictionary<string, JsonElement> Properties { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets or sets the status of the session.
/// </summary>
public string? Status { get; set; }
/// <summary>
/// Gets or sets the subject of the session.
/// </summary>
public string? Subject { get; set; }
}

32
src/OpenIddict.Abstractions/Descriptors/OpenIddictTokenDescriptor.cs

@ -9,65 +9,69 @@ namespace OpenIddict.Abstractions;
public class OpenIddictTokenDescriptor
{
/// <summary>
/// Gets or sets the application identifier associated with the token.
/// Gets or sets the identifier of the application associated with the token.
/// </summary>
public string? ApplicationId { get; set; }
/// <summary>
/// Gets or sets the authorization identifier associated with the token.
/// Gets or sets the identifier of the authorization associated with the token.
/// </summary>
public string? AuthorizationId { get; set; }
/// <summary>
/// Gets or sets the creation date associated with the token.
/// Gets or sets the creation date of the token.
/// </summary>
public DateTimeOffset? CreationDate { get; set; }
/// <summary>
/// Gets or sets the expiration date associated with the token.
/// Gets or sets the expiration date of the token.
/// </summary>
public DateTimeOffset? ExpirationDate { get; set; }
/// <summary>
/// Gets or sets the payload associated with the token.
/// Gets or sets the payload of the token.
/// </summary>
public string? Payload { get; set; }
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// Note: this property is not stored by the default stores.
/// </remarks>
public ClaimsPrincipal? Principal { get; set; }
/// <summary>
/// Gets the additional properties associated with the token.
/// Gets the additional properties of the token.
/// </summary>
public Dictionary<string, JsonElement> Properties { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets or sets the redemption date associated with the token.
/// Gets or sets the redemption date of the token.
/// </summary>
public DateTimeOffset? RedemptionDate { get; set; }
/// <summary>
/// Gets or sets the reference identifier associated with the token.
/// Gets or sets the reference identifier of the token.
/// </summary>
/// <remarks>
/// Note: depending on the application manager used when creating it,
/// this property may be hashed or encrypted for security reasons.
/// </summary>
/// </remarks>
public string? ReferenceId { get; set; }
/// <summary>
/// Gets or sets the status associated with the token.
/// Gets or sets the status of the token.
/// </summary>
public string? Status { get; set; }
/// <summary>
/// Gets or sets the subject associated with the token.
/// Gets or sets the subject of the token.
/// </summary>
public string? Subject { get; set; }
/// <summary>
/// Gets or sets the token type.
/// Gets or sets the type of the token.
/// </summary>
public string? Type { get; set; }
}

26
src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs

@ -31,7 +31,7 @@ public interface IOpenIddictApplicationManager
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of applications in the database.
/// </returns>
ValueTask<long> CountAsync(CancellationToken cancellationToken = default);
@ -43,7 +43,7 @@ public interface IOpenIddictApplicationManager
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of applications that match the specified query.
/// </returns>
ValueTask<long> CountAsync<TResult>(Func<IQueryable<object>, IQueryable<TResult>> query, CancellationToken cancellationToken = default);
@ -57,7 +57,7 @@ public interface IOpenIddictApplicationManager
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of applications that match the specified query.
/// </returns>
ValueTask<long> CountAsync<TState, TResult>(
@ -72,7 +72,7 @@ public interface IOpenIddictApplicationManager
/// <param name="descriptor">The application descriptor.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the unique identifier associated with the application.
/// </returns>
ValueTask<object> CreateAsync(OpenIddictApplicationDescriptor descriptor, CancellationToken cancellationToken = default);
@ -122,7 +122,7 @@ public interface IOpenIddictApplicationManager
/// <param name="identifier">The client identifier associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
/// </returns>
ValueTask<object?> FindByClientIdAsync(string identifier, CancellationToken cancellationToken = default);
@ -133,7 +133,7 @@ public interface IOpenIddictApplicationManager
/// <param name="identifier">The unique identifier associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
/// </returns>
ValueTask<object?> FindByIdAsync(string identifier, CancellationToken cancellationToken = default);
@ -174,7 +174,7 @@ public interface IOpenIddictApplicationManager
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
ValueTask<TResult?> GetAsync<TResult>(
@ -189,7 +189,7 @@ public interface IOpenIddictApplicationManager
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
ValueTask<TResult?> GetAsync<TState, TResult>(
@ -554,7 +554,7 @@ public interface IOpenIddictApplicationManager
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns a boolean indicating whether the client secret was valid.
/// </returns>
ValueTask<bool> ValidateClientSecretAsync(object application, string secret, CancellationToken cancellationToken = default);
@ -567,7 +567,7 @@ public interface IOpenIddictApplicationManager
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <remarks>Note: if no client_id parameter is specified in end session requests, this method may not be called.</remarks>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns a boolean indicating whether the post_logout_redirect_uri was valid.
/// </returns>
ValueTask<bool> ValidatePostLogoutRedirectUriAsync(object application,
@ -582,7 +582,7 @@ public interface IOpenIddictApplicationManager
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns a boolean indicating whether the client certificate was valid.
/// </returns>
ValueTask<bool> ValidatePublicKeyInfrastructureTlsClientCertificateAsync(object application,
@ -595,7 +595,7 @@ public interface IOpenIddictApplicationManager
/// <param name="uri">The URI that should be compared to one of the redirect_uri stored in the database.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns a boolean indicating whether the redirect_uri was valid.
/// </returns>
ValueTask<bool> ValidateRedirectUriAsync(object application,
@ -610,7 +610,7 @@ public interface IOpenIddictApplicationManager
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation, whose
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, whose
/// result returns a boolean indicating whether the self-signed client certificate was valid.
/// </returns>
ValueTask<bool> ValidateSelfSignedTlsClientCertificateAsync(

59
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
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of authorizations in the database.
/// </returns>
ValueTask<long> CountAsync(CancellationToken cancellationToken = default);
@ -40,7 +39,7 @@ public interface IOpenIddictAuthorizationManager
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of authorizations that match the specified query.
/// </returns>
ValueTask<long> CountAsync<TResult>(
@ -55,52 +54,20 @@ public interface IOpenIddictAuthorizationManager
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of authorizations that match the specified query.
/// </returns>
ValueTask<long> CountAsync<TState, TResult>(
Func<IQueryable<object>, TState, IQueryable<TResult>> query,
TState state, CancellationToken cancellationToken = default);
/// <summary>
/// Creates a new permanent authorization based on the specified parameters.
/// </summary>
/// <param name="identity">The identity associated with the authorization.</param>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="client">The client associated with the authorization.</param>
/// <param name="type">The authorization type.</param>
/// <param name="scopes">The minimal scopes associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation, whose result returns the authorization.
/// </returns>
ValueTask<object> CreateAsync(
ClaimsIdentity identity, string subject, string client,
string type, ImmutableArray<string> scopes, CancellationToken cancellationToken = default);
/// <summary>
/// Creates a new permanent authorization based on the specified parameters.
/// </summary>
/// <param name="principal">The principal associated with the authorization.</param>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="client">The client associated with the authorization.</param>
/// <param name="type">The authorization type.</param>
/// <param name="scopes">The minimal scopes associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation, whose result returns the authorization.
/// </returns>
ValueTask<object> CreateAsync(
ClaimsPrincipal principal, string subject, string client,
string type, ImmutableArray<string> scopes, CancellationToken cancellationToken = default);
/// <summary>
/// Creates a new authorization based on the specified descriptor.
/// </summary>
/// <param name="descriptor">The authorization descriptor.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation, whose result returns the authorization.
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, whose result returns the authorization.
/// </returns>
ValueTask<object> CreateAsync(OpenIddictAuthorizationDescriptor descriptor, CancellationToken cancellationToken = default);
@ -125,18 +92,14 @@ public interface IOpenIddictAuthorizationManager
ValueTask DeleteAsync(object authorization, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the authorizations matching the specified parameters.
/// Retrieves the authorizations matching the specified query.
/// </summary>
/// <param name="subject">The subject associated with the authorization, or <see langword="null"/> not to filter out specific subjects.</param>
/// <param name="client">The client associated with the authorization, or <see langword="null"/> not to filter out specific clients.</param>
/// <param name="status">The authorization status, or <see langword="null"/> not to filter out specific authorization statuses.</param>
/// <param name="type">The authorization type, or <see langword="null"/> not to filter out specific authorization types.</param>
/// <param name="scopes">The minimal scopes associated with the authorization, or <see langword="null"/> not to filter out scopes.</param>
/// <param name="query">The query parameters: if a parameter is <see langword="null"/>, it will not be used to filter the results.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The authorizations corresponding to the criteria.</returns>
IAsyncEnumerable<object> FindAsync(
string? subject, string? client, string? status,
string? type, ImmutableArray<string>? scopes, CancellationToken cancellationToken = default);
(string? Subject, string? ApplicationId, string? Status,
string? Type, ImmutableArray<string>? RequiredScopes) query, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the list of authorizations corresponding to the specified application identifier.
@ -152,7 +115,7 @@ public interface IOpenIddictAuthorizationManager
/// <param name="identifier">The unique identifier associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorization corresponding to the identifier.
/// </returns>
ValueTask<object?> FindByIdAsync(string identifier, CancellationToken cancellationToken = default);
@ -183,7 +146,7 @@ public interface IOpenIddictAuthorizationManager
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
ValueTask<TResult?> GetAsync<TResult>(
@ -198,7 +161,7 @@ public interface IOpenIddictAuthorizationManager
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
ValueTask<TResult?> GetAsync<TState, TResult>(

16
src/OpenIddict.Abstractions/Managers/IOpenIddictResourceManager.cs

@ -28,7 +28,7 @@ public interface IOpenIddictResourceManager
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of resources in the database.
/// </returns>
ValueTask<long> CountAsync(CancellationToken cancellationToken = default);
@ -40,7 +40,7 @@ public interface IOpenIddictResourceManager
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of resources that match the specified query.
/// </returns>
ValueTask<long> CountAsync<TResult>(Func<IQueryable<object>, IQueryable<TResult>> query, CancellationToken cancellationToken = default);
@ -54,7 +54,7 @@ public interface IOpenIddictResourceManager
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of resources that match the specified query.
/// </returns>
ValueTask<long> CountAsync<TState, TResult>(
@ -67,7 +67,7 @@ public interface IOpenIddictResourceManager
/// <param name="descriptor">The resource descriptor.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation, whose result returns the resource.
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, whose result returns the resource.
/// </returns>
ValueTask<object> CreateAsync(OpenIddictResourceDescriptor descriptor, CancellationToken cancellationToken = default);
@ -97,7 +97,7 @@ public interface IOpenIddictResourceManager
/// <param name="identifier">The unique identifier associated with the resource.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the resource corresponding to the identifier.
/// </returns>
ValueTask<object?> FindByIdAsync(string identifier, CancellationToken cancellationToken = default);
@ -108,7 +108,7 @@ public interface IOpenIddictResourceManager
/// <param name="name">The name associated with the resource.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the resource corresponding to the specified name.
/// </returns>
ValueTask<object?> FindByNameAsync(string name, CancellationToken cancellationToken = default);
@ -128,7 +128,7 @@ public interface IOpenIddictResourceManager
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
ValueTask<TResult?> GetAsync<TResult>(
@ -143,7 +143,7 @@ public interface IOpenIddictResourceManager
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
ValueTask<TResult?> GetAsync<TState, TResult>(

16
src/OpenIddict.Abstractions/Managers/IOpenIddictScopeManager.cs

@ -28,7 +28,7 @@ public interface IOpenIddictScopeManager
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of scopes in the database.
/// </returns>
ValueTask<long> CountAsync(CancellationToken cancellationToken = default);
@ -40,7 +40,7 @@ public interface IOpenIddictScopeManager
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of scopes that match the specified query.
/// </returns>
ValueTask<long> CountAsync<TResult>(Func<IQueryable<object>, IQueryable<TResult>> query, CancellationToken cancellationToken = default);
@ -54,7 +54,7 @@ public interface IOpenIddictScopeManager
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of scopes that match the specified query.
/// </returns>
ValueTask<long> CountAsync<TState, TResult>(
@ -67,7 +67,7 @@ public interface IOpenIddictScopeManager
/// <param name="descriptor">The scope descriptor.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation, whose result returns the scope.
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, whose result returns the scope.
/// </returns>
ValueTask<object> CreateAsync(OpenIddictScopeDescriptor descriptor, CancellationToken cancellationToken = default);
@ -97,7 +97,7 @@ public interface IOpenIddictScopeManager
/// <param name="identifier">The unique identifier associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scope corresponding to the identifier.
/// </returns>
ValueTask<object?> FindByIdAsync(string identifier, CancellationToken cancellationToken = default);
@ -108,7 +108,7 @@ public interface IOpenIddictScopeManager
/// <param name="name">The name associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scope corresponding to the specified name.
/// </returns>
ValueTask<object?> FindByNameAsync(string name, CancellationToken cancellationToken = default);
@ -136,7 +136,7 @@ public interface IOpenIddictScopeManager
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
ValueTask<TResult?> GetAsync<TResult>(
@ -151,7 +151,7 @@ public interface IOpenIddictScopeManager
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
ValueTask<TResult?> GetAsync<TState, TResult>(

346
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;
/// <summary>
/// Provides methods allowing to manage the Sessions stored in the store.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public interface IOpenIddictSessionManager
{
/// <summary>
/// Determines the number of sessions that exist in the database.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of sessions in the database.
/// </returns>
ValueTask<long> CountAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Determines the number of sessions that match the specified query.
/// </summary>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of sessions that match the specified query.
/// </returns>
ValueTask<long> CountAsync<TResult>(Func<IQueryable<object>, IQueryable<TResult>> query, CancellationToken cancellationToken = default);
/// <summary>
/// Determines the number of sessions that match the specified query.
/// </summary>
/// <typeparam name="TState">The state type.</typeparam>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of sessions that match the specified query.
/// </returns>
ValueTask<long> CountAsync<TState, TResult>(
Func<IQueryable<object>, TState, IQueryable<TResult>> query,
TState state, CancellationToken cancellationToken = default);
/// <summary>
/// Creates a new session based on the specified descriptor.
/// </summary>
/// <param name="descriptor">The session descriptor.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, whose result returns the session.
/// </returns>
ValueTask<object> CreateAsync(OpenIddictSessionDescriptor descriptor, CancellationToken cancellationToken = default);
/// <summary>
/// Creates a new session.
/// </summary>
/// <param name="session">The session to create.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
ValueTask CreateAsync(object session, CancellationToken cancellationToken = default);
/// <summary>
/// Removes an existing session.
/// </summary>
/// <param name="session">The session to delete.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
ValueTask DeleteAsync(object session, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the sessions matching the specified query.
/// </summary>
/// <param name="query">The query parameters: if a parameter is <see langword="null"/>, it will not be used to filter the results.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The sessions corresponding to the criteria.</returns>
IAsyncEnumerable<object> FindAsync(
(string? Subject, string? LoginId, string? ApplicationId, string? Status) query,
CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the list of sessions corresponding to the specified application identifier.
/// </summary>
/// <param name="identifier">The application identifier associated with the sessions.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The sessions corresponding to the specified application.</returns>
IAsyncEnumerable<object> FindByApplicationIdAsync(string identifier, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the list of sessions corresponding to the specified authorization identifier.
/// </summary>
/// <param name="identifier">The authorization identifier associated with the sessions.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The sessions corresponding to the specified authorization.</returns>
IAsyncEnumerable<object> FindByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the list of sessions corresponding to the specified login identifier.
/// </summary>
/// <param name="identifier">The login identifier associated with the sessions.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The sessions corresponding to the specified login identifier.</returns>
IAsyncEnumerable<object> FindByLoginIdAsync(string identifier, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves a session using its unique identifier.
/// </summary>
/// <param name="identifier">The unique identifier associated with the session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the session corresponding to the identifier.
/// </returns>
ValueTask<object?> FindByIdAsync(string identifier, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the list of sessions corresponding to the specified subject.
/// </summary>
/// <param name="subject">The subject associated with the sessions.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The sessions corresponding to the specified subject.</returns>
IAsyncEnumerable<object> FindBySubjectAsync(string subject, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the optional application identifier associated with a session.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the application identifier associated with the session.
/// </returns>
ValueTask<string?> GetApplicationIdAsync(object session, CancellationToken cancellationToken = default);
/// <summary>
/// Executes the specified query and returns the first element.
/// </summary>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
ValueTask<TResult?> GetAsync<TResult>(
Func<IQueryable<object>, IQueryable<TResult>> query, CancellationToken cancellationToken = default);
/// <summary>
/// Executes the specified query and returns the first element.
/// </summary>
/// <typeparam name="TState">The state type.</typeparam>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
ValueTask<TResult?> GetAsync<TState, TResult>(
Func<IQueryable<object>, TState, IQueryable<TResult>> query,
TState state, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the optional authorization identifier associated with a session.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorization identifier associated with the session.
/// </returns>
ValueTask<string?> GetAuthorizationIdAsync(object session, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the creation date associated with a session.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the creation date associated with the specified session.
/// </returns>
ValueTask<DateTimeOffset?> GetCreationDateAsync(object session, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the unique identifier associated with a session.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the unique identifier associated with the session.
/// </returns>
ValueTask<string?> GetIdAsync(object session, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the login identifier associated with a session.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the login identifier associated with the specified session.
/// </returns>
ValueTask<string?> GetLoginIdAsync(object session, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the additional properties associated with a session.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the additional properties associated with the session.
/// </returns>
ValueTask<ImmutableDictionary<string, JsonElement>> GetPropertiesAsync(object session, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the status associated with a session.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the status associated with the specified session.
/// </returns>
ValueTask<string?> GetStatusAsync(object session, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the subject associated with a session.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the subject associated with the specified session.
/// </returns>
ValueTask<string?> GetSubjectAsync(object session, CancellationToken cancellationToken = default);
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <param name="count">The number of results to return.</param>
/// <param name="offset">The number of results to skip.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>All the elements returned when executing the specified query.</returns>
IAsyncEnumerable<object> ListAsync(
int? count = null, int? offset = null, CancellationToken cancellationToken = default);
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>All the elements returned when executing the specified query.</returns>
IAsyncEnumerable<TResult> ListAsync<TResult>(
Func<IQueryable<object>, IQueryable<TResult>> query, CancellationToken cancellationToken = default);
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <typeparam name="TState">The state type.</typeparam>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>All the elements returned when executing the specified query.</returns>
IAsyncEnumerable<TResult> ListAsync<TState, TResult>(
Func<IQueryable<object>, TState, IQueryable<TResult>> query,
TState state, CancellationToken cancellationToken = default);
/// <summary>
/// Populates the specified descriptor using the properties exposed by the session.
/// </summary>
/// <param name="descriptor">The descriptor.</param>
/// <param name="session">The session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
ValueTask PopulateAsync(OpenIddictSessionDescriptor descriptor, object session, CancellationToken cancellationToken = default);
/// <summary>
/// Populates the session using the specified descriptor.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="descriptor">The descriptor.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
ValueTask PopulateAsync(object session, OpenIddictSessionDescriptor descriptor, CancellationToken cancellationToken = default);
/// <summary>
/// Updates an existing session.
/// </summary>
/// <param name="session">The session to update.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
ValueTask UpdateAsync(object session, CancellationToken cancellationToken = default);
/// <summary>
/// Updates an existing session.
/// </summary>
/// <param name="session">The session to update.</param>
/// <param name="descriptor">The descriptor used to update the session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
ValueTask UpdateAsync(object session, OpenIddictSessionDescriptor descriptor, CancellationToken cancellationToken = default);
/// <summary>
/// Validates the session to ensure it's in a consistent state.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The validation error encountered when validating the session.</returns>
IAsyncEnumerable<ValidationResult> ValidateAsync(object session, CancellationToken cancellationToken = default);
}

33
src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs

@ -27,7 +27,7 @@ public interface IOpenIddictTokenManager
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of tokens in the database.
/// </returns>
ValueTask<long> CountAsync(CancellationToken cancellationToken = default);
@ -39,7 +39,7 @@ public interface IOpenIddictTokenManager
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of tokens that match the specified query.
/// </returns>
ValueTask<long> CountAsync<TResult>(Func<IQueryable<object>, IQueryable<TResult>> query, CancellationToken cancellationToken = default);
@ -53,7 +53,7 @@ public interface IOpenIddictTokenManager
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of tokens that match the specified query.
/// </returns>
ValueTask<long> CountAsync<TState, TResult>(
@ -66,7 +66,7 @@ public interface IOpenIddictTokenManager
/// <param name="descriptor">The token descriptor.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation, whose result returns the token.
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, whose result returns the token.
/// </returns>
ValueTask<object> CreateAsync(OpenIddictTokenDescriptor descriptor, CancellationToken cancellationToken = default);
@ -91,17 +91,14 @@ public interface IOpenIddictTokenManager
ValueTask DeleteAsync(object token, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the tokens matching the specified parameters.
/// Retrieves the tokens matching the specified query.
/// </summary>
/// <param name="subject">The subject associated with the token, or <see langword="null"/> not to filter out specific subjects.</param>
/// <param name="client">The client associated with the token, or <see langword="null"/> not to filter out specific clients.</param>
/// <param name="status">The token status, or <see langword="null"/> not to filter out specific token statuses.</param>
/// <param name="type">The token type, or <see langword="null"/> not to filter out specific token types.</param>
/// <param name="query">The query parameters: if a parameter is <see langword="null"/>, it will not be used to filter the results.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The tokens corresponding to the criteria.</returns>
IAsyncEnumerable<object> FindAsync(
string? subject, string? client,
string? status, string? type, CancellationToken cancellationToken = default);
(string? Subject, string? ApplicationId, string? Status, string? Type) query,
CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the list of tokens corresponding to the specified application identifier.
@ -125,19 +122,21 @@ public interface IOpenIddictTokenManager
/// <param name="identifier">The unique identifier associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the token corresponding to the unique identifier.
/// </returns>
ValueTask<object?> FindByIdAsync(string identifier, CancellationToken cancellationToken = default);
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// Note: the reference identifier may be hashed or encrypted for security reasons.
/// </remarks>
/// <param name="identifier">The reference identifier associated with the tokens.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified reference identifier.
/// </returns>
ValueTask<object?> FindByReferenceIdAsync(string identifier, CancellationToken cancellationToken = default);
@ -168,7 +167,7 @@ public interface IOpenIddictTokenManager
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
ValueTask<TResult?> GetAsync<TResult>(
@ -183,7 +182,7 @@ public interface IOpenIddictTokenManager
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
ValueTask<TResult?> GetAsync<TState, TResult>(

2
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";

10
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -867,16 +867,13 @@ Make sure that the entity is not abstract and has a public parameterless constru
<value>An error occurred while pruning authorizations.</value>
</data>
<data name="ID0244" xml:space="preserve">
<value>The application associated with the authorization cannot be found.</value>
<value>The application matching the specified identifier cannot be found in the change tracker or in the database.</value>
</data>
<data name="ID0249" xml:space="preserve">
<value>An error occurred while pruning tokens.</value>
</data>
<data name="ID0250" xml:space="preserve">
<value>The application associated with the token cannot be found.</value>
</data>
<data name="ID0251" xml:space="preserve">
<value>The authorization associated with the token cannot be found.</value>
<value>The authorization matching the specified identifier cannot be found in the change tracker or in the database.</value>
</data>
<data name="ID0253" xml:space="preserve">
<value>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
<data name="ID2208" xml:space="preserve">
<value>A resource with the same name already exists.</value>
</data>
<data name="ID2209" xml:space="preserve">
<value>The login identifier cannot be null or empty and must match the value used to represent the user session.</value>
</data>
<data name="ID4000" xml:space="preserve">
<value>The '{0}' parameter shouldn't be null or empty at this point.</value>
</data>

34
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);
/// <summary>
/// Gets the internal session identifier associated with the claims identity.
/// </summary>
/// <param name="identity">The claims identity.</param>
/// <returns>The unique identifier or <see langword="null"/> if the claim cannot be found.</returns>
public static string? GetSessionId(this ClaimsIdentity identity)
=> identity.GetClaim(Claims.Private.SessionId);
/// <summary>
/// Gets the internal session identifier associated with the claims principal.
/// </summary>
/// <param name="principal">The claims principal.</param>
/// <returns>The unique identifier or <see langword="null"/> if the claim cannot be found.</returns>
public static string? GetSessionId(this ClaimsPrincipal principal)
=> principal.GetClaim(Claims.Private.SessionId);
/// <summary>
/// Gets the internal token identifier associated with the claims identity.
/// </summary>
@ -3344,6 +3360,24 @@ public static class OpenIddictExtensions
public static ClaimsPrincipal SetAuthorizationId(this ClaimsPrincipal principal, string? identifier)
=> principal.SetClaim(Claims.Private.AuthorizationId, identifier);
/// <summary>
/// Sets the internal session identifier associated with the claims identity.
/// </summary>
/// <param name="identity">The claims identity.</param>
/// <param name="identifier">The unique identifier to store.</param>
/// <returns>The claims identity.</returns>
public static ClaimsIdentity SetSessionId(this ClaimsIdentity identity, string? identifier)
=> identity.SetClaim(Claims.Private.SessionId, identifier);
/// <summary>
/// Sets the internal session identifier associated with the claims principal.
/// </summary>
/// <param name="principal">The claims principal.</param>
/// <param name="identifier">The unique identifier to store.</param>
/// <returns>The claims principal.</returns>
public static ClaimsPrincipal SetSessionId(this ClaimsPrincipal principal, string? identifier)
=> principal.SetClaim(Claims.Private.SessionId, identifier);
/// <summary>
/// Sets the internal token identifier associated with the claims identity.
/// </summary>

10
src/OpenIddict.Abstractions/Stores/IOpenIddictApplicationStore.cs

@ -23,7 +23,7 @@ public interface IOpenIddictApplicationStore<TApplication> where TApplication :
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of applications in the database.
/// </returns>
ValueTask<long> CountAsync(CancellationToken cancellationToken);
@ -37,7 +37,7 @@ public interface IOpenIddictApplicationStore<TApplication> where TApplication :
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of applications that match the specified query.
/// </returns>
ValueTask<long> CountAsync<TState, TResult>(
@ -66,7 +66,7 @@ public interface IOpenIddictApplicationStore<TApplication> where TApplication :
/// <param name="identifier">The unique identifier associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
/// </returns>
ValueTask<TApplication?> FindByIdAsync(string identifier, CancellationToken cancellationToken);
@ -77,7 +77,7 @@ public interface IOpenIddictApplicationStore<TApplication> where TApplication :
/// <param name="identifier">The client identifier associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
/// </returns>
ValueTask<TApplication?> FindByClientIdAsync(string identifier, CancellationToken cancellationToken);
@ -120,7 +120,7 @@ public interface IOpenIddictApplicationStore<TApplication> where TApplication :
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
ValueTask<TResult?> GetAsync<TState, TResult>(

21
src/OpenIddict.Abstractions/Stores/IOpenIddictAuthorizationStore.cs

@ -20,7 +20,7 @@ public interface IOpenIddictAuthorizationStore<TAuthorization> where TAuthorizat
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of authorizations in the database.
/// </returns>
ValueTask<long> CountAsync(CancellationToken cancellationToken);
@ -34,7 +34,7 @@ public interface IOpenIddictAuthorizationStore<TAuthorization> where TAuthorizat
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of authorizations that match the specified query.
/// </returns>
ValueTask<long> CountAsync<TState, TResult>(
@ -58,19 +58,14 @@ public interface IOpenIddictAuthorizationStore<TAuthorization> where TAuthorizat
ValueTask DeleteAsync(TAuthorization authorization, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the authorizations matching the specified parameters.
/// Retrieves the authorizations matching the specified query.
/// </summary>
/// <param name="subject">The subject associated with the authorization, or <see langword="null"/> not to filter out specific subjects.</param>
/// <param name="client">The client associated with the authorization, or <see langword="null"/> not to filter out specific clients.</param>
/// <param name="status">The authorization status, or <see langword="null"/> not to filter out specific authorization statuses.</param>
/// <param name="type">The authorization type, or <see langword="null"/> not to filter out specific authorization types.</param>
/// <param name="scopes">The minimal scopes associated with the authorization, or <see langword="null"/> not to filter out scopes.</param>
/// <param name="query">The query parameters: if a parameter is <see langword="null"/>, it will not be used to filter the results.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The authorizations corresponding to the criteria.</returns>
IAsyncEnumerable<TAuthorization> FindAsync(
string? subject, string? client,
string? status, string? type,
ImmutableArray<string>? scopes, CancellationToken cancellationToken);
(string? Subject, string? ApplicationId, string? Status,
string? Type, ImmutableArray<string>? RequiredScopes) query, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the list of authorizations corresponding to the specified application identifier.
@ -86,7 +81,7 @@ public interface IOpenIddictAuthorizationStore<TAuthorization> where TAuthorizat
/// <param name="identifier">The unique identifier associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorization corresponding to the identifier.
/// </returns>
ValueTask<TAuthorization?> FindByIdAsync(string identifier, CancellationToken cancellationToken);
@ -119,7 +114,7 @@ public interface IOpenIddictAuthorizationStore<TAuthorization> where TAuthorizat
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
ValueTask<TResult?> GetAsync<TState, TResult>(

10
src/OpenIddict.Abstractions/Stores/IOpenIddictResourceStore.cs

@ -21,7 +21,7 @@ public interface IOpenIddictResourceStore<TResource> where TResource : class
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of resources in the database.
/// </returns>
ValueTask<long> CountAsync(CancellationToken cancellationToken);
@ -35,7 +35,7 @@ public interface IOpenIddictResourceStore<TResource> where TResource : class
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of resources that match the specified query.
/// </returns>
ValueTask<long> CountAsync<TState, TResult>(
@ -64,7 +64,7 @@ public interface IOpenIddictResourceStore<TResource> where TResource : class
/// <param name="identifier">The unique identifier associated with the resource.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the resource corresponding to the identifier.
/// </returns>
ValueTask<TResource?> FindByIdAsync(string identifier, CancellationToken cancellationToken);
@ -75,7 +75,7 @@ public interface IOpenIddictResourceStore<TResource> where TResource : class
/// <param name="name">The name associated with the resource.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the resource corresponding to the specified name.
/// </returns>
ValueTask<TResource?> FindByNameAsync(string name, CancellationToken cancellationToken);
@ -97,7 +97,7 @@ public interface IOpenIddictResourceStore<TResource> where TResource : class
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
ValueTask<TResult?> GetAsync<TState, TResult>(

10
src/OpenIddict.Abstractions/Stores/IOpenIddictScopeStore.cs

@ -21,7 +21,7 @@ public interface IOpenIddictScopeStore<TScope> where TScope : class
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of scopes in the database.
/// </returns>
ValueTask<long> CountAsync(CancellationToken cancellationToken);
@ -35,7 +35,7 @@ public interface IOpenIddictScopeStore<TScope> where TScope : class
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of scopes that match the specified query.
/// </returns>
ValueTask<long> CountAsync<TState, TResult>(
@ -64,7 +64,7 @@ public interface IOpenIddictScopeStore<TScope> where TScope : class
/// <param name="identifier">The unique identifier associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scope corresponding to the identifier.
/// </returns>
ValueTask<TScope?> FindByIdAsync(string identifier, CancellationToken cancellationToken);
@ -75,7 +75,7 @@ public interface IOpenIddictScopeStore<TScope> where TScope : class
/// <param name="name">The name associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scope corresponding to the specified name.
/// </returns>
ValueTask<TScope?> FindByNameAsync(string name, CancellationToken cancellationToken);
@ -105,7 +105,7 @@ public interface IOpenIddictScopeStore<TScope> where TScope : class
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
ValueTask<TResult?> GetAsync<TState, TResult>(

319
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;
/// <summary>
/// Provides methods allowing to manage the sessions stored in a database.
/// </summary>
/// <typeparam name="TSession">The type of the session entity.</typeparam>
public interface IOpenIddictSessionStore<TSession> where TSession : class
{
/// <summary>
/// Determines the number of sessions that exist in the database.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of sessions in the database.
/// </returns>
ValueTask<long> CountAsync(CancellationToken cancellationToken);
/// <summary>
/// Determines the number of sessions that match the specified query.
/// </summary>
/// <typeparam name="TState">The state type.</typeparam>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of sessions that match the specified query.
/// </returns>
ValueTask<long> CountAsync<TState, TResult>(
Func<IQueryable<TSession>, TState, IQueryable<TResult>> query,
TState state, CancellationToken cancellationToken);
/// <summary>
/// Creates a new session.
/// </summary>
/// <param name="session">The session to create.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
ValueTask CreateAsync(TSession session, CancellationToken cancellationToken);
/// <summary>
/// Removes an existing session.
/// </summary>
/// <param name="session">The session to delete.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
ValueTask DeleteAsync(TSession session, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the sessions matching the specified query.
/// </summary>
/// <param name="query">The query parameters: if a parameter is <see langword="null"/>, it will not be used to filter the results.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The sessions corresponding to the criteria.</returns>
IAsyncEnumerable<TSession> FindAsync(
(string? Subject, string? LoginId, string? ApplicationId, string? Status) query, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the list of sessions corresponding to the specified application identifier.
/// </summary>
/// <param name="identifier">The application identifier associated with the sessions.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The sessions corresponding to the specified application.</returns>
IAsyncEnumerable<TSession> FindByApplicationIdAsync(string identifier, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the list of sessions corresponding to the specified authorization identifier.
/// </summary>
/// <param name="identifier">The authorization identifier associated with the sessions.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The sessions corresponding to the specified authorization.</returns>
IAsyncEnumerable<TSession> FindByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the list of sessions corresponding to the specified login identifier.
/// </summary>
/// <param name="identifier">The login identifier associated with the sessions.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The sessions corresponding to the specified login identifier.</returns>
IAsyncEnumerable<TSession> FindByLoginIdAsync(string identifier, CancellationToken cancellationToken);
/// <summary>
/// Retrieves a session using its unique identifier.
/// </summary>
/// <param name="identifier">The unique identifier associated with the session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the session corresponding to the identifier.
/// </returns>
ValueTask<TSession?> FindByIdAsync(string identifier, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the list of sessions corresponding to the specified subject.
/// </summary>
/// <param name="subject">The subject associated with the sessions.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The sessions corresponding to the specified subject.</returns>
IAsyncEnumerable<TSession> FindBySubjectAsync(string subject, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the optional application identifier associated with a session.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the application identifier associated with the session.
/// </returns>
ValueTask<string?> GetApplicationIdAsync(TSession session, CancellationToken cancellationToken);
/// <summary>
/// Executes the specified query and returns the first element.
/// </summary>
/// <typeparam name="TState">The state type.</typeparam>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
ValueTask<TResult?> GetAsync<TState, TResult>(
Func<IQueryable<TSession>, TState, IQueryable<TResult>> query,
TState state, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the optional authorization identifier associated with a session.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorization identifier associated with the session.
/// </returns>
ValueTask<string?> GetAuthorizationIdAsync(TSession session, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the creation date associated with a session.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the creation date associated with the specified session.
/// </returns>
ValueTask<DateTimeOffset?> GetCreationDateAsync(TSession session, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the unique identifier associated with a session.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the unique identifier associated with the session.
/// </returns>
ValueTask<string?> GetIdAsync(TSession session, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the login identifier associated with a session.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the login identifier associated with the specified session.
/// </returns>
ValueTask<string?> GetLoginIdAsync(TSession session, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the additional properties associated with a session.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, whose
/// result returns all the additional properties associated with the session.
/// </returns>
ValueTask<ImmutableDictionary<string, JsonElement>> GetPropertiesAsync(TSession session, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the status associated with a session.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the status associated with the specified session.
/// </returns>
ValueTask<string?> GetStatusAsync(TSession session, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the subject associated with a session.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the subject associated with the specified session.
/// </returns>
ValueTask<string?> GetSubjectAsync(TSession session, CancellationToken cancellationToken);
/// <summary>
/// Instantiates a new session.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the instantiated session, that can be persisted in the database.
/// </returns>
ValueTask<TSession> InstantiateAsync(CancellationToken cancellationToken);
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <param name="count">The number of results to return.</param>
/// <param name="offset">The number of results to skip.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>All the elements returned when executing the specified query.</returns>
IAsyncEnumerable<TSession> ListAsync(int? count, int? offset, CancellationToken cancellationToken);
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <typeparam name="TState">The state type.</typeparam>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>All the elements returned when executing the specified query.</returns>
IAsyncEnumerable<TResult> ListAsync<TState, TResult>(
Func<IQueryable<TSession>, TState, IQueryable<TResult>> query,
TState state, CancellationToken cancellationToken);
/// <summary>
/// Sets the application identifier associated with a session.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="identifier">The unique identifier associated with the session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
ValueTask SetApplicationIdAsync(TSession session, string? identifier, CancellationToken cancellationToken);
/// <summary>
/// Sets the authorization identifier associated with a session.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="identifier">The unique identifier associated with the session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
ValueTask SetAuthorizationIdAsync(TSession session, string? identifier, CancellationToken cancellationToken);
/// <summary>
/// Sets the creation date associated with a session.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="date">The creation date.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
ValueTask SetCreationDateAsync(TSession session, DateTimeOffset? date, CancellationToken cancellationToken);
/// <summary>
/// Sets the login identifier associated with a session.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="identifier">The login identifier associated with the session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
ValueTask SetLoginIdAsync(TSession session, string? identifier, CancellationToken cancellationToken);
/// <summary>
/// Sets the additional properties associated with a session.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="properties">The additional properties associated with the session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
ValueTask SetPropertiesAsync(TSession session,
ImmutableDictionary<string, JsonElement> properties, CancellationToken cancellationToken);
/// <summary>
/// Sets the status associated with a session.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="status">The status associated with the session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
ValueTask SetStatusAsync(TSession session, string? status, CancellationToken cancellationToken);
/// <summary>
/// Sets the subject associated with a session.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="subject">The subject associated with the session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
ValueTask SetSubjectAsync(TSession session, string? subject, CancellationToken cancellationToken);
/// <summary>
/// Updates an existing session.
/// </summary>
/// <param name="session">The session to update.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
ValueTask UpdateAsync(TSession session, CancellationToken cancellationToken);
}

26
src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs

@ -20,7 +20,7 @@ public interface IOpenIddictTokenStore<TToken> where TToken : class
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of applications in the database.
/// </returns>
ValueTask<long> CountAsync(CancellationToken cancellationToken);
@ -34,7 +34,7 @@ public interface IOpenIddictTokenStore<TToken> where TToken : class
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of tokens that match the specified query.
/// </returns>
ValueTask<long> CountAsync<TState, TResult>(
@ -58,17 +58,13 @@ public interface IOpenIddictTokenStore<TToken> where TToken : class
ValueTask DeleteAsync(TToken token, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the tokens matching the specified parameters.
/// Retrieves the tokens matching the specified query.
/// </summary>
/// <param name="subject">The subject associated with the token, or <see langword="null"/> not to filter out specific subjects.</param>
/// <param name="client">The client associated with the token, or <see langword="null"/> not to filter out specific clients.</param>
/// <param name="status">The token status, or <see langword="null"/> not to filter out specific token statuses.</param>
/// <param name="type">The token type, or <see langword="null"/> not to filter out specific token types.</param>
/// <param name="query">The query parameters: if a parameter is <see langword="null"/>, it will not be used to filter the results.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The tokens corresponding to the criteria.</returns>
IAsyncEnumerable<TToken> FindAsync(
string? subject, string? client,
string? status, string? type, CancellationToken cancellationToken);
(string? Subject, string? ApplicationId, string? Status, string? Type) query, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the list of tokens corresponding to the specified application identifier.
@ -92,19 +88,21 @@ public interface IOpenIddictTokenStore<TToken> where TToken : class
/// <param name="identifier">The unique identifier associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the token corresponding to the unique identifier.
/// </returns>
ValueTask<TToken?> FindByIdAsync(string identifier, CancellationToken cancellationToken);
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// Note: the reference identifier may be hashed or encrypted for security reasons.
/// </remarks>
/// <param name="identifier">The reference identifier associated with the tokens.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified reference identifier.
/// </returns>
ValueTask<TToken?> FindByReferenceIdAsync(string identifier, CancellationToken cancellationToken);
@ -137,7 +135,7 @@ public interface IOpenIddictTokenStore<TToken> where TToken : class
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
ValueTask<TResult?> GetAsync<TState, TResult>(

2
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

2
src/OpenIddict.Client/IOpenIddictClientHandlerFilter.cs

@ -19,7 +19,7 @@ public interface IOpenIddictClientHandlerFilter<in TContext> where TContext : Ba
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose boolean result determines whether the handler will be invoked or not.
/// </returns>
ValueTask<bool> IsActiveAsync(TContext context);

6
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.

2
src/OpenIddict.Core/Caches/OpenIddictApplicationCache.cs

@ -312,7 +312,7 @@ public sealed class OpenIddictApplicationCache<TApplication> : IOpenIddictApplic
/// <param name="application">The application associated with the expiration signal.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns an expiration signal for the specified application.
/// </returns>
private async ValueTask<IChangeToken> CreateExpirationSignalAsync(TApplication application, CancellationToken cancellationToken)

9
src/OpenIddict.Core/Caches/OpenIddictAuthorizationCache.cs

@ -84,13 +84,12 @@ public sealed class OpenIddictAuthorizationCache<TAuthorization> : IOpenIddictAu
/// <inheritdoc/>
public async IAsyncEnumerable<TAuthorization> FindAsync(
string? subject, string? client,
string? status, string? type,
ImmutableArray<string>? scopes, [EnumeratorCancellation] CancellationToken cancellationToken)
(string? Subject, string? ApplicationId, string? Status,
string? Type, ImmutableArray<string>? 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<TAuthorization> : IOpenIddictAu
/// <param name="authorization">The authorization associated with the expiration signal.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns an expiration signal for the specified authorization.
/// </returns>
private async ValueTask<IChangeToken> CreateExpirationSignalAsync(TAuthorization authorization, CancellationToken cancellationToken)

2
src/OpenIddict.Core/Caches/OpenIddictResourceCache.cs

@ -238,7 +238,7 @@ public sealed class OpenIddictResourceCache<TResource> : IOpenIddictResourceCach
/// <param name="resource">The resource associated with the expiration signal.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns an expiration signal for the specified resource.
/// </returns>
private async ValueTask<IChangeToken> CreateExpirationSignalAsync(TResource resource, CancellationToken cancellationToken)

2
src/OpenIddict.Core/Caches/OpenIddictScopeCache.cs

@ -285,7 +285,7 @@ public sealed class OpenIddictScopeCache<TScope> : IOpenIddictScopeCache<TScope>
/// <param name="scope">The scope associated with the expiration signal.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns an expiration signal for the specified scope.
/// </returns>
private async ValueTask<IChangeToken> CreateExpirationSignalAsync(TScope scope, CancellationToken cancellationToken)

383
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;
/// <summary>
/// Provides methods allowing to cache sessions after retrieving them from the store.
/// </summary>
/// <typeparam name="TSession">The type of the Session entity.</typeparam>
public sealed class OpenIddictSessionCache<TSession> : IOpenIddictSessionCache<TSession>, IDisposable where TSession : class
{
private readonly MemoryCache _cache;
private readonly ConcurrentDictionary<string, CancellationTokenSource> _signals;
private readonly IOpenIddictSessionStore<TSession> _store;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictSessionCache{TSession}"/> class.
/// </summary>
/// <param name="options">The options.</param>
/// <param name="store">The store.</param>
public OpenIddictSessionCache(
IOptionsMonitor<OpenIddictCoreOptions> options,
IOpenIddictSessionStore<TSession> store)
{
_cache = new MemoryCache(new MemoryCacheOptions
{
SizeLimit = (options ?? throw new ArgumentNullException(nameof(options))).CurrentValue.EntityCacheLimit
});
_signals = new ConcurrentDictionary<string, CancellationTokenSource>(StringComparer.Ordinal);
_store = store ?? throw new ArgumentNullException(nameof(store));
}
/// <inheritdoc/>
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);
}
/// <inheritdoc/>
public void Dispose()
{
foreach (var signal in _signals)
{
signal.Value.Dispose();
}
_cache.Dispose();
}
/// <inheritdoc/>
public async IAsyncEnumerable<TSession> 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;
}
}
/// <inheritdoc/>
public IAsyncEnumerable<TSession> FindByApplicationIdAsync(string identifier, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(identifier);
return ExecuteAsync(cancellationToken);
async IAsyncEnumerable<TSession> ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
var parameters = new
{
Method = nameof(FindByApplicationIdAsync),
Identifier = identifier
};
if (!_cache.TryGetValue(parameters, out ImmutableArray<TSession> sessions))
{
var builder = ImmutableArray.CreateBuilder<TSession>();
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;
}
}
}
/// <inheritdoc/>
public IAsyncEnumerable<TSession> FindByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(identifier);
return ExecuteAsync(cancellationToken);
async IAsyncEnumerable<TSession> ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
var parameters = new
{
Method = nameof(FindByAuthorizationIdAsync),
Identifier = identifier
};
if (!_cache.TryGetValue(parameters, out ImmutableArray<TSession> sessions))
{
var builder = ImmutableArray.CreateBuilder<TSession>();
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;
}
}
}
/// <inheritdoc/>
public ValueTask<TSession?> 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<TSession?> ExecuteAsync()
{
if ((session = await _store.FindByIdAsync(identifier, cancellationToken)) is not null)
{
await AddAsync(session, cancellationToken);
}
await CreateEntryAsync(parameters, session, cancellationToken);
return session;
}
}
/// <inheritdoc/>
public IAsyncEnumerable<TSession> FindByLoginIdAsync(string identifier, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(identifier);
return ExecuteAsync(cancellationToken);
async IAsyncEnumerable<TSession> ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
var parameters = new
{
Method = nameof(FindByLoginIdAsync),
Identifier = identifier
};
if (!_cache.TryGetValue(parameters, out ImmutableArray<TSession> sessions))
{
var builder = ImmutableArray.CreateBuilder<TSession>();
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;
}
}
}
/// <inheritdoc/>
public IAsyncEnumerable<TSession> FindBySubjectAsync(string subject, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(subject);
return ExecuteAsync(cancellationToken);
async IAsyncEnumerable<TSession> ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
var parameters = new
{
Method = nameof(FindBySubjectAsync),
Identifier = subject
};
if (!_cache.TryGetValue(parameters, out ImmutableArray<TSession> sessions))
{
var builder = ImmutableArray.CreateBuilder<TSession>();
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;
}
}
}
/// <inheritdoc/>
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();
}
}
/// <summary>
/// Creates a cache entry for the specified key.
/// </summary>
/// <param name="key">The cache key.</param>
/// <param name="session">The session to store in the cache entry, if applicable.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
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;
}
/// <summary>
/// Creates a cache entry for the specified key.
/// </summary>
/// <param name="key">The cache key.</param>
/// <param name="sessions">The sessions to store in the cache entry.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
private async ValueTask CreateEntryAsync(object key, ImmutableArray<TSession> 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;
}
/// <summary>
/// Creates an expiration signal allowing to invalidate all the
/// cache entries associated with the specified session.
/// </summary>
/// <param name="session">The session associated with the expiration signal.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns an expiration signal for the specified session.
/// </returns>
private async ValueTask<IChangeToken> 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);
}
}

8
src/OpenIddict.Core/Caches/OpenIddictTokenCache.cs

@ -102,12 +102,12 @@ public sealed class OpenIddictTokenCache<TToken> : IOpenIddictTokenCache<TToken>
/// <inheritdoc/>
public async IAsyncEnumerable<TToken> 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<TToken> : IOpenIddictTokenCache<TToken>
/// <param name="token">The token associated with the expiration signal.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns an expiration signal for the specified token.
/// </returns>
private async ValueTask<IChangeToken> CreateExpirationSignalAsync(TToken token, CancellationToken cancellationToken)

28
src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs

@ -77,7 +77,7 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of applications in the database.
/// </returns>
public virtual ValueTask<long> CountAsync(CancellationToken cancellationToken = default)
@ -90,7 +90,7 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of applications that match the specified query.
/// </returns>
public virtual ValueTask<long> CountAsync<TResult>(
@ -110,7 +110,7 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of applications that match the specified query.
/// </returns>
public virtual ValueTask<long> CountAsync<TState, TResult>(
@ -238,7 +238,7 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
/// <param name="descriptor">The application descriptor.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the unique identifier associated with the application.
/// </returns>
public virtual async ValueTask<TApplication> CreateAsync(
@ -291,7 +291,7 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
/// <param name="identifier">The client identifier associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
/// </returns>
public virtual async ValueTask<TApplication?> FindByClientIdAsync(
@ -326,7 +326,7 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
/// <param name="identifier">The unique identifier associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
/// </returns>
public virtual async ValueTask<TApplication?> FindByIdAsync(string identifier, CancellationToken cancellationToken = default)
@ -462,7 +462,7 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
public virtual ValueTask<TResult?> GetAsync<TResult>(
@ -482,7 +482,7 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
public virtual ValueTask<TResult?> GetAsync<TState, TResult>(
@ -1373,7 +1373,7 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns a boolean indicating whether the client secret was valid.
/// </returns>
public virtual async ValueTask<bool> ValidateClientSecretAsync(
@ -1437,7 +1437,7 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <remarks>Note: if no client_id parameter is specified in end session requests, this method may not be called.</remarks>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns a boolean indicating whether the post_logout_redirect_uri was valid.
/// </returns>
public virtual async ValueTask<bool> ValidatePostLogoutRedirectUriAsync(TApplication application,
@ -1503,7 +1503,7 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns a boolean indicating whether the client certificate was valid.
/// </returns>
public virtual async ValueTask<bool> ValidatePublicKeyInfrastructureTlsClientCertificateAsync(
@ -1602,7 +1602,7 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
/// <param name="uri">The URI that should be compared to one of the redirect_uri stored in the database.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns a boolean indicating whether the redirect_uri was valid.
/// </returns>
public virtual async ValueTask<bool> ValidateRedirectUriAsync(TApplication application,
@ -1670,7 +1670,7 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation, whose
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, whose
/// result returns a boolean indicating whether the self-signed client certificate was valid.
/// </returns>
public virtual async ValueTask<bool> ValidateSelfSignedTlsClientCertificateAsync(
@ -1823,7 +1823,7 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
/// <param name="comparand">The value stored in the database, which is usually a hashed representation of the secret.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation, whose result returns
/// A <see cref="ValueTask{TResult}"/> 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.
/// </returns>
protected virtual ValueTask<(bool IsValid, bool IsRehashRequired)> ValidateClientSecretAsync(

105
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<TAuthorization> : IOpenIddictAuthori
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of authorizations in the database.
/// </returns>
public virtual ValueTask<long> CountAsync(CancellationToken cancellationToken = default)
@ -84,7 +83,7 @@ public class OpenIddictAuthorizationManager<TAuthorization> : IOpenIddictAuthori
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of authorizations that match the specified query.
/// </returns>
public virtual ValueTask<long> CountAsync<TResult>(
@ -104,7 +103,7 @@ public class OpenIddictAuthorizationManager<TAuthorization> : IOpenIddictAuthori
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of authorizations that match the specified query.
/// </returns>
public virtual ValueTask<long> CountAsync<TState, TResult>(
@ -176,7 +175,7 @@ public class OpenIddictAuthorizationManager<TAuthorization> : IOpenIddictAuthori
/// <param name="descriptor">The authorization descriptor.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation, whose result returns the authorization.
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, whose result returns the authorization.
/// </returns>
public virtual async ValueTask<TAuthorization> CreateAsync(
OpenIddictAuthorizationDescriptor descriptor, CancellationToken cancellationToken = default)
@ -192,60 +191,6 @@ public class OpenIddictAuthorizationManager<TAuthorization> : IOpenIddictAuthori
return authorization;
}
/// <summary>
/// Creates a new permanent authorization based on the specified parameters.
/// </summary>
/// <param name="identity">The identity associated with the authorization.</param>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="client">The client associated with the authorization.</param>
/// <param name="type">The authorization type.</param>
/// <param name="scopes">The minimal scopes associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation, whose result returns the authorization.
/// </returns>
public virtual ValueTask<TAuthorization> CreateAsync(
ClaimsIdentity identity, string subject, string client,
string type, ImmutableArray<string> scopes, CancellationToken cancellationToken = default)
=> CreateAsync(new ClaimsPrincipal(identity ?? throw new ArgumentNullException(nameof(identity))),
subject, client, type, scopes, cancellationToken);
/// <summary>
/// Creates a new permanent authorization based on the specified parameters.
/// </summary>
/// <param name="principal">The principal associated with the authorization.</param>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="client">The client associated with the authorization.</param>
/// <param name="type">The authorization type.</param>
/// <param name="scopes">The minimal scopes associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation, whose result returns the authorization.
/// </returns>
public virtual ValueTask<TAuthorization> CreateAsync(
ClaimsPrincipal principal, string subject, string client,
string type, ImmutableArray<string> 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);
}
/// <summary>
/// Removes an existing authorization.
/// </summary>
@ -267,23 +212,18 @@ public class OpenIddictAuthorizationManager<TAuthorization> : IOpenIddictAuthori
}
/// <summary>
/// Retrieves the authorizations matching the specified parameters.
/// Retrieves the authorizations matching the specified query.
/// </summary>
/// <param name="subject">The subject associated with the authorization, or <see langword="null"/> not to filter out specific subjects.</param>
/// <param name="client">The client associated with the authorization, or <see langword="null"/> not to filter out specific clients.</param>
/// <param name="status">The authorization status, or <see langword="null"/> not to filter out specific authorization statuses.</param>
/// <param name="type">The authorization type, or <see langword="null"/> not to filter out specific authorization types.</param>
/// <param name="scopes">The minimal scopes associated with the authorization, or <see langword="null"/> not to filter out scopes.</param>
/// <param name="query">The query parameters: if a parameter is <see langword="null"/>, it will not be used to filter the results.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The authorizations corresponding to the criteria.</returns>
public virtual IAsyncEnumerable<TAuthorization> FindAsync(
string? subject, string? client,
string? status, string? type,
ImmutableArray<string>? scopes, CancellationToken cancellationToken = default)
(string? Subject, string? ApplicationId, string? Status,
string? Type, ImmutableArray<string>? 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<TAuthorization> : 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<TAuthorization> : IOpenIddictAuthori
/// <param name="identifier">The unique identifier associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorization corresponding to the identifier.
/// </returns>
public virtual async ValueTask<TAuthorization?> FindByIdAsync(string identifier, CancellationToken cancellationToken = default)
@ -450,7 +391,7 @@ public class OpenIddictAuthorizationManager<TAuthorization> : IOpenIddictAuthori
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
public virtual ValueTask<TResult?> GetAsync<TResult>(
@ -470,7 +411,7 @@ public class OpenIddictAuthorizationManager<TAuthorization> : IOpenIddictAuthori
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
public virtual ValueTask<TResult?> GetAsync<TState, TResult>(
@ -977,14 +918,6 @@ public class OpenIddictAuthorizationManager<TAuthorization> : IOpenIddictAuthori
ValueTask<long> IOpenIddictAuthorizationManager.CountAsync<TState, TResult>(Func<IQueryable<object>, TState, IQueryable<TResult>> query, TState state, CancellationToken cancellationToken)
=> CountAsync(query, state, cancellationToken);
/// <inheritdoc/>
async ValueTask<object> IOpenIddictAuthorizationManager.CreateAsync(ClaimsIdentity identity, string subject, string client, string type, ImmutableArray<string> scopes, CancellationToken cancellationToken)
=> await CreateAsync(identity, subject, client, type, scopes, cancellationToken);
/// <inheritdoc/>
async ValueTask<object> IOpenIddictAuthorizationManager.CreateAsync(ClaimsPrincipal principal, string subject, string client, string type, ImmutableArray<string> scopes, CancellationToken cancellationToken)
=> await CreateAsync(principal, subject, client, type, scopes, cancellationToken);
/// <inheritdoc/>
async ValueTask<object> IOpenIddictAuthorizationManager.CreateAsync(OpenIddictAuthorizationDescriptor descriptor, CancellationToken cancellationToken)
=> await CreateAsync(descriptor, cancellationToken);
@ -998,8 +931,8 @@ public class OpenIddictAuthorizationManager<TAuthorization> : IOpenIddictAuthori
=> DeleteAsync((TAuthorization) authorization, cancellationToken);
/// <inheritdoc/>
IAsyncEnumerable<object> IOpenIddictAuthorizationManager.FindAsync(string? subject, string? client, string? status, string? type, ImmutableArray<string>? scopes, CancellationToken cancellationToken)
=> FindAsync(subject, client, status, type, scopes, cancellationToken);
IAsyncEnumerable<object> IOpenIddictAuthorizationManager.FindAsync((string? Subject, string? ApplicationId, string? Status, string? Type, ImmutableArray<string>? RequiredScopes) query, CancellationToken cancellationToken)
=> FindAsync(query, cancellationToken);
/// <inheritdoc/>
IAsyncEnumerable<object> IOpenIddictAuthorizationManager.FindByApplicationIdAsync(string identifier, CancellationToken cancellationToken)

16
src/OpenIddict.Core/Managers/OpenIddictResourceManager.cs

@ -70,7 +70,7 @@ public class OpenIddictResourceManager<TResource> : IOpenIddictResourceManager w
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of resources in the database.
/// </returns>
public virtual ValueTask<long> CountAsync(CancellationToken cancellationToken = default)
@ -83,7 +83,7 @@ public class OpenIddictResourceManager<TResource> : IOpenIddictResourceManager w
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of resources that match the specified query.
/// </returns>
public virtual ValueTask<long> CountAsync<TResult>(
@ -103,7 +103,7 @@ public class OpenIddictResourceManager<TResource> : IOpenIddictResourceManager w
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of resources that match the specified query.
/// </returns>
public virtual ValueTask<long> CountAsync<TState, TResult>(
@ -169,7 +169,7 @@ public class OpenIddictResourceManager<TResource> : IOpenIddictResourceManager w
/// <param name="descriptor">The resource descriptor.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation, whose result returns the resource.
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, whose result returns the resource.
/// </returns>
public virtual async ValueTask<TResource> CreateAsync(
OpenIddictResourceDescriptor descriptor, CancellationToken cancellationToken = default)
@ -211,7 +211,7 @@ public class OpenIddictResourceManager<TResource> : IOpenIddictResourceManager w
/// <param name="identifier">The unique identifier associated with the resource.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the resource corresponding to the identifier.
/// </returns>
public virtual async ValueTask<TResource?> FindByIdAsync(string identifier, CancellationToken cancellationToken = default)
@ -245,7 +245,7 @@ public class OpenIddictResourceManager<TResource> : IOpenIddictResourceManager w
/// <param name="name">The name associated with the resource.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the resource corresponding to the specified name.
/// </returns>
public virtual async ValueTask<TResource?> FindByNameAsync(string name, CancellationToken cancellationToken = default)
@ -323,7 +323,7 @@ public class OpenIddictResourceManager<TResource> : IOpenIddictResourceManager w
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
public virtual ValueTask<TResult?> GetAsync<TResult>(
@ -343,7 +343,7 @@ public class OpenIddictResourceManager<TResource> : IOpenIddictResourceManager w
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
public virtual ValueTask<TResult?> GetAsync<TState, TResult>(

16
src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs

@ -70,7 +70,7 @@ public class OpenIddictScopeManager<TScope> : IOpenIddictScopeManager where TSco
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of scopes in the database.
/// </returns>
public virtual ValueTask<long> CountAsync(CancellationToken cancellationToken = default)
@ -83,7 +83,7 @@ public class OpenIddictScopeManager<TScope> : IOpenIddictScopeManager where TSco
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of scopes that match the specified query.
/// </returns>
public virtual ValueTask<long> CountAsync<TResult>(
@ -103,7 +103,7 @@ public class OpenIddictScopeManager<TScope> : IOpenIddictScopeManager where TSco
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of scopes that match the specified query.
/// </returns>
public virtual ValueTask<long> CountAsync<TState, TResult>(
@ -169,7 +169,7 @@ public class OpenIddictScopeManager<TScope> : IOpenIddictScopeManager where TSco
/// <param name="descriptor">The scope descriptor.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation, whose result returns the scope.
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, whose result returns the scope.
/// </returns>
public virtual async ValueTask<TScope> CreateAsync(
OpenIddictScopeDescriptor descriptor, CancellationToken cancellationToken = default)
@ -211,7 +211,7 @@ public class OpenIddictScopeManager<TScope> : IOpenIddictScopeManager where TSco
/// <param name="identifier">The unique identifier associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scope corresponding to the identifier.
/// </returns>
public virtual async ValueTask<TScope?> FindByIdAsync(string identifier, CancellationToken cancellationToken = default)
@ -245,7 +245,7 @@ public class OpenIddictScopeManager<TScope> : IOpenIddictScopeManager where TSco
/// <param name="name">The name associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scope corresponding to the specified name.
/// </returns>
public virtual async ValueTask<TScope?> FindByNameAsync(string name, CancellationToken cancellationToken = default)
@ -362,7 +362,7 @@ public class OpenIddictScopeManager<TScope> : IOpenIddictScopeManager where TSco
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
public virtual ValueTask<TResult?> GetAsync<TResult>(
@ -382,7 +382,7 @@ public class OpenIddictScopeManager<TScope> : IOpenIddictScopeManager where TSco
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
public virtual ValueTask<TResult?> GetAsync<TState, TResult>(

919
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;
/// <summary>
/// Provides methods allowing to manage the sessions stored in the store.
/// </summary>
/// <remarks>
/// Applications that do not want to depend on a specific entity type can use the non-generic
/// <see cref="IOpenIddictSessionManager"/> instead, for which the actual entity type is resolved at runtime.
/// </remarks>
/// <typeparam name="TSession">The type of the session entity.</typeparam>
public class OpenIddictSessionManager<TSession> : IOpenIddictSessionManager where TSession : class
{
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictSessionManager{TSession}"/> class.
/// </summary>
/// <param name="cache">The cache.</param>
/// <param name="logger">The logger.</param>
/// <param name="options">The options.</param>
/// <param name="store">The store.</param>
public OpenIddictSessionManager(
IOpenIddictSessionCache<TSession> cache,
ILogger<OpenIddictSessionManager<TSession>> logger,
IOptionsMonitor<OpenIddictCoreOptions> options,
IOpenIddictSessionStore<TSession> 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));
}
/// <summary>
/// Gets the cache associated with the current manager.
/// </summary>
protected IOpenIddictSessionCache<TSession> Cache { get; }
/// <summary>
/// Gets the logger associated with the current manager.
/// </summary>
protected ILogger Logger { get; }
/// <summary>
/// Gets the options associated with the current manager.
/// </summary>
protected IOptionsMonitor<OpenIddictCoreOptions> Options { get; }
/// <summary>
/// Gets the store associated with the current manager.
/// </summary>
protected IOpenIddictSessionStore<TSession> Store { get; }
/// <summary>
/// Determines the number of sessions that exist in the database.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of sessions in the database.
/// </returns>
public virtual ValueTask<long> CountAsync(CancellationToken cancellationToken = default)
=> Store.CountAsync(cancellationToken);
/// <summary>
/// Determines the number of sessions that match the specified query.
/// </summary>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of sessions that match the specified query.
/// </returns>
public virtual ValueTask<long> CountAsync<TResult>(
Func<IQueryable<TSession>, IQueryable<TResult>> query, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(query);
return CountAsync(static (sessions, query) => query(sessions), query, cancellationToken);
}
/// <summary>
/// Determines the number of sessions that match the specified query.
/// </summary>
/// <typeparam name="TState">The state type.</typeparam>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of sessions that match the specified query.
/// </returns>
public virtual ValueTask<long> CountAsync<TState, TResult>(
Func<IQueryable<TSession>, TState, IQueryable<TResult>> query,
TState state, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(query);
return Store.CountAsync(query, state, cancellationToken);
}
/// <summary>
/// Creates a new session.
/// </summary>
/// <param name="session">The session to create.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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<ImmutableArray<ValidationResult>> GetValidationResultsAsync(
TSession session, CancellationToken cancellationToken)
{
var builder = ImmutableArray.CreateBuilder<ValidationResult>();
await foreach (var result in ValidateAsync(session, cancellationToken))
{
builder.Add(result);
}
return builder.ToImmutable();
}
}
/// <summary>
/// Creates a new session based on the specified descriptor.
/// </summary>
/// <param name="descriptor">The session descriptor.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, whose result returns the session.
/// </returns>
public virtual async ValueTask<TSession> 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;
}
/// <summary>
/// Removes an existing session.
/// </summary>
/// <param name="session">The session to delete.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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);
}
/// <summary>
/// Retrieves the sessions matching the specified query.
/// </summary>
/// <param name="query">The query parameters: if a parameter is <see langword="null"/>, it will not be used to filter the results.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The sessions corresponding to the criteria.</returns>
public virtual IAsyncEnumerable<TSession> 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<TSession> 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;
}
}
}
}
/// <summary>
/// Retrieves the list of sessions corresponding to the specified application identifier.
/// </summary>
/// <param name="identifier">The application identifier associated with the sessions.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The sessions corresponding to the specified application.</returns>
public virtual IAsyncEnumerable<TSession> 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<TSession> ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var session in sessions)
{
if (string.Equals(await Store.GetApplicationIdAsync(session, cancellationToken), identifier, StringComparison.Ordinal))
{
yield return session;
}
}
}
}
/// <summary>
/// Retrieves the list of sessions corresponding to the specified authorization identifier.
/// </summary>
/// <param name="identifier">The authorization identifier associated with the sessions.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The sessions corresponding to the specified authorization.</returns>
public virtual IAsyncEnumerable<TSession> 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<TSession> ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var session in sessions)
{
if (string.Equals(await Store.GetAuthorizationIdAsync(session, cancellationToken), identifier, StringComparison.Ordinal))
{
yield return session;
}
}
}
}
/// <summary>
/// Retrieves a session using its unique identifier.
/// </summary>
/// <param name="identifier">The unique identifier associated with the session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the session corresponding to the identifier.
/// </returns>
public virtual async ValueTask<TSession?> 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;
}
/// <summary>
/// Retrieves the list of sessions corresponding to the specified login identifier.
/// </summary>
/// <param name="identifier">The login identifier associated with the sessions.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The sessions corresponding to the specified login identifier.</returns>
public virtual IAsyncEnumerable<TSession> 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<TSession> ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var session in sessions)
{
if (string.Equals(await Store.GetLoginIdAsync(session, cancellationToken), identifier, StringComparison.Ordinal))
{
yield return session;
}
}
}
}
/// <summary>
/// Retrieves the list of sessions corresponding to the specified subject.
/// </summary>
/// <param name="subject">The subject associated with the sessions.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The sessions corresponding to the specified subject.</returns>
public virtual IAsyncEnumerable<TSession> 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<TSession> ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var session in sessions)
{
if (string.Equals(await Store.GetSubjectAsync(session, cancellationToken), subject, StringComparison.Ordinal))
{
yield return session;
}
}
}
}
/// <summary>
/// Retrieves the optional application identifier associated with a session.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the application identifier associated with the session.
/// </returns>
public virtual ValueTask<string?> GetApplicationIdAsync(TSession session, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(session);
return Store.GetApplicationIdAsync(session, cancellationToken);
}
/// <summary>
/// Executes the specified query and returns the first element.
/// </summary>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
public virtual ValueTask<TResult?> GetAsync<TResult>(
Func<IQueryable<TSession>, IQueryable<TResult>> query, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(query);
return GetAsync(static (sessions, query) => query(sessions), query, cancellationToken);
}
/// <summary>
/// Executes the specified query and returns the first element.
/// </summary>
/// <typeparam name="TState">The state type.</typeparam>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
public virtual ValueTask<TResult?> GetAsync<TState, TResult>(
Func<IQueryable<TSession>, TState, IQueryable<TResult>> query,
TState state, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(query);
return Store.GetAsync(query, state, cancellationToken);
}
/// <summary>
/// Retrieves the optional authorization identifier associated with a session.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorization identifier associated with the session.
/// </returns>
public virtual ValueTask<string?> GetAuthorizationIdAsync(TSession session, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(session);
return Store.GetAuthorizationIdAsync(session, cancellationToken);
}
/// <summary>
/// Retrieves the creation date associated with a session.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the creation date associated with the specified session.
/// </returns>
public virtual ValueTask<DateTimeOffset?> GetCreationDateAsync(TSession session, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(session);
return Store.GetCreationDateAsync(session, cancellationToken);
}
/// <summary>
/// Retrieves the unique identifier associated with a session.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the unique identifier associated with the session.
/// </returns>
public virtual ValueTask<string?> GetIdAsync(TSession session, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(session);
return Store.GetIdAsync(session, cancellationToken);
}
/// <summary>
/// Retrieves the login identifier associated with a session.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the login identifier associated with the specified session.
/// </returns>
public virtual ValueTask<string?> GetLoginIdAsync(TSession session, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(session);
return Store.GetLoginIdAsync(session, cancellationToken);
}
/// <summary>
/// Retrieves the additional properties associated with a session.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the additional properties associated with the session.
/// </returns>
public virtual ValueTask<ImmutableDictionary<string, JsonElement>> GetPropertiesAsync(
TSession session, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(session);
return Store.GetPropertiesAsync(session, cancellationToken);
}
/// <summary>
/// Retrieves the status associated with a session.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the status associated with the specified session.
/// </returns>
public virtual ValueTask<string?> GetStatusAsync(TSession session, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(session);
return Store.GetStatusAsync(session, cancellationToken);
}
/// <summary>
/// Retrieves the subject associated with a session.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the subject associated with the specified session.
/// </returns>
public virtual ValueTask<string?> GetSubjectAsync(TSession session, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(session);
return Store.GetSubjectAsync(session, cancellationToken);
}
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <param name="count">The number of results to return.</param>
/// <param name="offset">The number of results to skip.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>All the elements returned when executing the specified query.</returns>
public virtual IAsyncEnumerable<TSession> ListAsync(
int? count = null, int? offset = null, CancellationToken cancellationToken = default)
=> Store.ListAsync(count, offset, cancellationToken);
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>All the elements returned when executing the specified query.</returns>
public virtual IAsyncEnumerable<TResult> ListAsync<TResult>(
Func<IQueryable<TSession>, IQueryable<TResult>> query, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(query);
return ListAsync(static (sessions, query) => query(sessions), query, cancellationToken);
}
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <typeparam name="TState">The state type.</typeparam>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>All the elements returned when executing the specified query.</returns>
public virtual IAsyncEnumerable<TResult> ListAsync<TState, TResult>(
Func<IQueryable<TSession>, TState, IQueryable<TResult>> query,
TState state, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(query);
return Store.ListAsync(query, state, cancellationToken);
}
/// <summary>
/// Populates the session using the specified descriptor.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="descriptor">The descriptor.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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);
}
/// <summary>
/// Populates the specified descriptor using the properties exposed by the session.
/// </summary>
/// <param name="descriptor">The descriptor.</param>
/// <param name="session">The session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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);
}
}
/// <summary>
/// Updates an existing session.
/// </summary>
/// <param name="session">The session to update.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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<ImmutableArray<ValidationResult>> GetValidationResultsAsync(
TSession session, CancellationToken cancellationToken)
{
var builder = ImmutableArray.CreateBuilder<ValidationResult>();
await foreach (var result in ValidateAsync(session, cancellationToken))
{
builder.Add(result);
}
return builder.ToImmutable();
}
}
/// <summary>
/// Updates an existing session.
/// </summary>
/// <param name="session">The session to update.</param>
/// <param name="descriptor">The descriptor used to update the session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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);
}
/// <summary>
/// Validates the session to ensure it's in a consistent state.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The validation error encountered when validating the session.</returns>
public virtual IAsyncEnumerable<ValidationResult> ValidateAsync(TSession session, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(session);
return ExecuteAsync(cancellationToken);
async IAsyncEnumerable<ValidationResult> 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));
}
}
}
/// <inheritdoc/>
ValueTask<long> IOpenIddictSessionManager.CountAsync(CancellationToken cancellationToken)
=> CountAsync(cancellationToken);
/// <inheritdoc/>
ValueTask<long> IOpenIddictSessionManager.CountAsync<TResult>(Func<IQueryable<object>, IQueryable<TResult>> query, CancellationToken cancellationToken)
=> CountAsync(query, cancellationToken);
/// <inheritdoc/>
ValueTask<long> IOpenIddictSessionManager.CountAsync<TState, TResult>(Func<IQueryable<object>, TState, IQueryable<TResult>> query, TState state, CancellationToken cancellationToken)
=> CountAsync(query, state, cancellationToken);
/// <inheritdoc/>
async ValueTask<object> IOpenIddictSessionManager.CreateAsync(OpenIddictSessionDescriptor descriptor, CancellationToken cancellationToken)
=> await CreateAsync(descriptor, cancellationToken);
/// <inheritdoc/>
ValueTask IOpenIddictSessionManager.CreateAsync(object session, CancellationToken cancellationToken)
=> CreateAsync((TSession) session, cancellationToken);
/// <inheritdoc/>
ValueTask IOpenIddictSessionManager.DeleteAsync(object session, CancellationToken cancellationToken)
=> DeleteAsync((TSession) session, cancellationToken);
/// <inheritdoc/>
IAsyncEnumerable<object> IOpenIddictSessionManager.FindAsync((string? Subject, string? LoginId, string? ApplicationId, string? Status) query, CancellationToken cancellationToken)
=> FindAsync(query, cancellationToken);
/// <inheritdoc/>
IAsyncEnumerable<object> IOpenIddictSessionManager.FindByApplicationIdAsync(string identifier, CancellationToken cancellationToken)
=> FindByApplicationIdAsync(identifier, cancellationToken);
/// <inheritdoc/>
IAsyncEnumerable<object> IOpenIddictSessionManager.FindByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken)
=> FindByAuthorizationIdAsync(identifier, cancellationToken);
/// <inheritdoc/>
async ValueTask<object?> IOpenIddictSessionManager.FindByIdAsync(string identifier, CancellationToken cancellationToken)
=> await FindByIdAsync(identifier, cancellationToken);
/// <inheritdoc/>
IAsyncEnumerable<object> IOpenIddictSessionManager.FindByLoginIdAsync(string identifier, CancellationToken cancellationToken)
=> FindByLoginIdAsync(identifier, cancellationToken);
/// <inheritdoc/>
IAsyncEnumerable<object> IOpenIddictSessionManager.FindBySubjectAsync(string subject, CancellationToken cancellationToken)
=> FindBySubjectAsync(subject, cancellationToken);
/// <inheritdoc/>
ValueTask<string?> IOpenIddictSessionManager.GetApplicationIdAsync(object session, CancellationToken cancellationToken)
=> GetApplicationIdAsync((TSession) session, cancellationToken);
/// <inheritdoc/>
ValueTask<TResult?> IOpenIddictSessionManager.GetAsync<TResult>(Func<IQueryable<object>, IQueryable<TResult>> query, CancellationToken cancellationToken) where TResult : default
=> GetAsync(query, cancellationToken);
/// <inheritdoc/>
ValueTask<TResult?> IOpenIddictSessionManager.GetAsync<TState, TResult>(Func<IQueryable<object>, TState, IQueryable<TResult>> query, TState state, CancellationToken cancellationToken) where TResult : default
=> GetAsync(query, state, cancellationToken);
/// <inheritdoc/>
ValueTask<string?> IOpenIddictSessionManager.GetAuthorizationIdAsync(object session, CancellationToken cancellationToken)
=> GetAuthorizationIdAsync((TSession) session, cancellationToken);
/// <inheritdoc/>
ValueTask<DateTimeOffset?> IOpenIddictSessionManager.GetCreationDateAsync(object session, CancellationToken cancellationToken)
=> GetCreationDateAsync((TSession) session, cancellationToken);
/// <inheritdoc/>
ValueTask<string?> IOpenIddictSessionManager.GetIdAsync(object session, CancellationToken cancellationToken)
=> GetIdAsync((TSession) session, cancellationToken);
/// <inheritdoc/>
ValueTask<string?> IOpenIddictSessionManager.GetLoginIdAsync(object session, CancellationToken cancellationToken)
=> GetLoginIdAsync((TSession) session, cancellationToken);
/// <inheritdoc/>
ValueTask<ImmutableDictionary<string, JsonElement>> IOpenIddictSessionManager.GetPropertiesAsync(object session, CancellationToken cancellationToken)
=> GetPropertiesAsync((TSession) session, cancellationToken);
/// <inheritdoc/>
ValueTask<string?> IOpenIddictSessionManager.GetStatusAsync(object session, CancellationToken cancellationToken)
=> GetStatusAsync((TSession) session, cancellationToken);
/// <inheritdoc/>
ValueTask<string?> IOpenIddictSessionManager.GetSubjectAsync(object session, CancellationToken cancellationToken)
=> GetSubjectAsync((TSession) session, cancellationToken);
/// <inheritdoc/>
IAsyncEnumerable<object> IOpenIddictSessionManager.ListAsync(int? count, int? offset, CancellationToken cancellationToken)
=> ListAsync(count, offset, cancellationToken);
/// <inheritdoc/>
IAsyncEnumerable<TResult> IOpenIddictSessionManager.ListAsync<TResult>(Func<IQueryable<object>, IQueryable<TResult>> query, CancellationToken cancellationToken)
=> ListAsync(query, cancellationToken);
/// <inheritdoc/>
IAsyncEnumerable<TResult> IOpenIddictSessionManager.ListAsync<TState, TResult>(Func<IQueryable<object>, TState, IQueryable<TResult>> query, TState state, CancellationToken cancellationToken)
=> ListAsync(query, state, cancellationToken);
/// <inheritdoc/>
ValueTask IOpenIddictSessionManager.PopulateAsync(OpenIddictSessionDescriptor descriptor, object session, CancellationToken cancellationToken)
=> PopulateAsync(descriptor, (TSession) session, cancellationToken);
/// <inheritdoc/>
ValueTask IOpenIddictSessionManager.PopulateAsync(object session, OpenIddictSessionDescriptor descriptor, CancellationToken cancellationToken)
=> PopulateAsync((TSession) session, descriptor, cancellationToken);
/// <inheritdoc/>
ValueTask IOpenIddictSessionManager.UpdateAsync(object session, CancellationToken cancellationToken)
=> UpdateAsync((TSession) session, cancellationToken);
/// <inheritdoc/>
ValueTask IOpenIddictSessionManager.UpdateAsync(object session, OpenIddictSessionDescriptor descriptor, CancellationToken cancellationToken)
=> UpdateAsync((TSession) session, descriptor, cancellationToken);
/// <inheritdoc/>
IAsyncEnumerable<ValidationResult> IOpenIddictSessionManager.ValidateAsync(object session, CancellationToken cancellationToken)
=> ValidateAsync((TSession) session, cancellationToken);
}

50
src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs

@ -71,7 +71,7 @@ public class OpenIddictTokenManager<TToken> : IOpenIddictTokenManager where TTok
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of tokens in the database.
/// </returns>
public virtual ValueTask<long> CountAsync(CancellationToken cancellationToken = default)
@ -84,7 +84,7 @@ public class OpenIddictTokenManager<TToken> : IOpenIddictTokenManager where TTok
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of tokens that match the specified query.
/// </returns>
public virtual ValueTask<long> CountAsync<TResult>(
@ -104,7 +104,7 @@ public class OpenIddictTokenManager<TToken> : IOpenIddictTokenManager where TTok
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of tokens that match the specified query.
/// </returns>
public virtual ValueTask<long> CountAsync<TState, TResult>(
@ -184,7 +184,7 @@ public class OpenIddictTokenManager<TToken> : IOpenIddictTokenManager where TTok
/// <param name="descriptor">The token descriptor.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation, whose result returns the token.
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, whose result returns the token.
/// </returns>
public virtual async ValueTask<TToken> CreateAsync(
OpenIddictTokenDescriptor descriptor, CancellationToken cancellationToken = default)
@ -221,21 +221,18 @@ public class OpenIddictTokenManager<TToken> : IOpenIddictTokenManager where TTok
}
/// <summary>
/// Retrieves the tokens matching the specified parameters.
/// Retrieves the tokens matching the specified query.
/// </summary>
/// <param name="subject">The subject associated with the token, or <see langword="null"/> not to filter out specific subjects.</param>
/// <param name="client">The client associated with the token, or <see langword="null"/> not to filter out specific clients.</param>
/// <param name="status">The token status, or <see langword="null"/> not to filter out specific token statuses.</param>
/// <param name="type">The token type, or <see langword="null"/> not to filter out specific token types.</param>
/// <param name="query">The query parameters: if a parameter is <see langword="null"/>, it will not be used to filter the results.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>Tokens corresponding to the criteria.</returns>
/// <returns>The tokens corresponding to the criteria.</returns>
public virtual IAsyncEnumerable<TToken> 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<TToken> : 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<TToken> : IOpenIddictTokenManager where TTok
/// <param name="identifier">The unique identifier associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the token corresponding to the unique identifier.
/// </returns>
public virtual async ValueTask<TToken?> FindByIdAsync(string identifier, CancellationToken cancellationToken = default)
@ -372,13 +369,15 @@ public class OpenIddictTokenManager<TToken> : IOpenIddictTokenManager where TTok
}
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// Note: the reference identifier may be hashed or encrypted for security reasons.
/// </remarks>
/// <param name="identifier">The reference identifier associated with the tokens.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified reference identifier.
/// </returns>
public virtual async ValueTask<TToken?> FindByReferenceIdAsync(string identifier, CancellationToken cancellationToken = default)
@ -470,7 +469,7 @@ public class OpenIddictTokenManager<TToken> : IOpenIddictTokenManager where TTok
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
public virtual ValueTask<TResult?> GetAsync<TResult>(
@ -490,7 +489,7 @@ public class OpenIddictTokenManager<TToken> : IOpenIddictTokenManager where TTok
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
public virtual ValueTask<TResult?> GetAsync<TState, TResult>(
@ -1151,8 +1150,7 @@ public class OpenIddictTokenManager<TToken> : 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<TToken> : IOpenIddictTokenManager where TTok
=> DeleteAsync((TToken) token, cancellationToken);
/// <inheritdoc/>
IAsyncEnumerable<object> IOpenIddictTokenManager.FindAsync(string? subject, string? client, string? status, string? type, CancellationToken cancellationToken)
=> FindAsync(subject, client, status, type, cancellationToken);
IAsyncEnumerable<object> IOpenIddictTokenManager.FindAsync((string? Subject, string? ApplicationId, string? Status, string? Type) query, CancellationToken cancellationToken)
=> FindAsync(query, cancellationToken);
/// <inheritdoc/>
IAsyncEnumerable<object> IOpenIddictTokenManager.FindByApplicationIdAsync(string identifier, CancellationToken cancellationToken)

93
src/OpenIddict.Core/OpenIddictCoreBuilder.cs

@ -377,6 +377,86 @@ public sealed class OpenIddictCoreBuilder
return this;
}
/// <summary>
/// Replaces the session manager by the specified type.
/// </summary>
/// <typeparam name="TSession">The type of the entity.</typeparam>
/// <typeparam name="TManager">The type of the manager.</typeparam>
/// <returns>The <see cref="OpenIddictCoreBuilder"/> instance.</returns>
public OpenIddictCoreBuilder ReplaceSessionManager<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TManager>()
where TSession : class
where TManager : OpenIddictSessionManager<TSession>
{
Services.Replace(ServiceDescriptor.Scoped<OpenIddictSessionManager<TSession>, TManager>());
return this;
}
/// <summary>
/// Replaces the session manager by the specified type.
/// </summary>
/// <remarks>
/// Note: the specified type MUST be an open generic type definition containing exactly one generic argument.
/// </remarks>
/// <param name="type">The type of the manager.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/> instance.</returns>
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;
}
/// <summary>
/// Replaces the session store by the specified type.
/// </summary>
/// <typeparam name="TSession">The type of the entity.</typeparam>
/// <typeparam name="TStore">The type of the store.</typeparam>
/// <param name="lifetime">The lifetime of the store.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/> instance.</returns>
public OpenIddictCoreBuilder ReplaceSessionStore<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TStore>(
ServiceLifetime lifetime = ServiceLifetime.Scoped)
where TSession : class
where TStore : IOpenIddictSessionStore<TSession>
{
Services.Replace(ServiceDescriptor.Describe(typeof(IOpenIddictSessionStore<TSession>), typeof(TStore), lifetime));
return this;
}
/// <summary>
/// Replaces the session store by the specified type.
/// </summary>
/// <remarks>
/// Note: the specified type MUST be an open generic type definition containing exactly one generic argument.
/// </remarks>
/// <param name="type">The type of the store.</param>
/// <param name="lifetime">The lifetime of the store.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/> instance.</returns>
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;
}
/// <summary>
/// Replaces the token manager by the specified type.
/// </summary>
@ -600,6 +680,19 @@ public sealed class OpenIddictCoreBuilder
return this;
}
/// <summary>
/// Configures OpenIddict to use the specified entity as the default session entity.
/// </summary>
/// <returns>The <see cref="OpenIddictCoreBuilder"/> instance.</returns>
public OpenIddictCoreBuilder SetDefaultSessionEntity<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession>() where TSession : class
{
Services.Replace(ServiceDescriptor.Scoped<IOpenIddictSessionManager>(static provider =>
provider.GetRequiredService<OpenIddictSessionManager<TSession>>()));
return this;
}
/// <summary>
/// Configures OpenIddict to use the specified entity as the default token entity.
/// </summary>

4
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<IOpenIddictScopeManager>(static provider =>
throw new InvalidOperationException(SR.GetResourceString(SR.ID0472)));
builder.Services.TryAddScoped<IOpenIddictSessionManager>(static provider =>
throw new InvalidOperationException(SR.GetResourceString(SR.ID0472)));
builder.Services.TryAddScoped<IOpenIddictTokenManager>(static provider =>
throw new InvalidOperationException(SR.GetResourceString(SR.ID0472)));

5
src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkApplication.cs

@ -12,7 +12,10 @@ namespace OpenIddict.EntityFramework.Models;
/// <summary>
/// Represents an OpenIddict application.
/// </summary>
public class OpenIddictEntityFrameworkApplication : OpenIddictEntityFrameworkApplication<string, OpenIddictEntityFrameworkAuthorization, OpenIddictEntityFrameworkToken>
public class OpenIddictEntityFrameworkApplication :
OpenIddictEntityFrameworkApplication<string,
OpenIddictEntityFrameworkAuthorization,
OpenIddictEntityFrameworkToken>
{
public OpenIddictEntityFrameworkApplication() => Id = Guid.NewGuid().ToString();
}

5
src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkAuthorization.cs

@ -12,7 +12,10 @@ namespace OpenIddict.EntityFramework.Models;
/// <summary>
/// Represents an OpenIddict authorization.
/// </summary>
public class OpenIddictEntityFrameworkAuthorization : OpenIddictEntityFrameworkAuthorization<string, OpenIddictEntityFrameworkApplication, OpenIddictEntityFrameworkToken>
public class OpenIddictEntityFrameworkAuthorization :
OpenIddictEntityFrameworkAuthorization<string,
OpenIddictEntityFrameworkApplication,
OpenIddictEntityFrameworkToken>
{
public OpenIddictEntityFrameworkAuthorization() => Id = Guid.NewGuid().ToString();
}

3
src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkResource.cs

@ -12,7 +12,8 @@ namespace OpenIddict.EntityFramework.Models;
/// <summary>
/// Represents an OpenIddict resource.
/// </summary>
public class OpenIddictEntityFrameworkResource : OpenIddictEntityFrameworkResource<string>
public class OpenIddictEntityFrameworkResource :
OpenIddictEntityFrameworkResource<string>
{
public OpenIddictEntityFrameworkResource() => Id = Guid.NewGuid().ToString();
}

3
src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkScope.cs

@ -12,7 +12,8 @@ namespace OpenIddict.EntityFramework.Models;
/// <summary>
/// Represents an OpenIddict scope.
/// </summary>
public class OpenIddictEntityFrameworkScope : OpenIddictEntityFrameworkScope<string>
public class OpenIddictEntityFrameworkScope :
OpenIddictEntityFrameworkScope<string>
{
public OpenIddictEntityFrameworkScope() => Id = Guid.NewGuid().ToString();
}

77
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;
/// <summary>
/// Represents an OpenIddict session.
/// </summary>
public class OpenIddictEntityFrameworkSession :
OpenIddictEntityFrameworkSession<string,
OpenIddictEntityFrameworkApplication,
OpenIddictEntityFrameworkAuthorization>
{
public OpenIddictEntityFrameworkSession() => Id = Guid.NewGuid().ToString();
}
/// <summary>
/// Represents an OpenIddict session.
/// </summary>
[DebuggerDisplay("Id = {Id.ToString(),nq} ; Subject = {Subject,nq} ; LoginId = {LoginId,nq} ; Status = {Status,nq}")]
public class OpenIddictEntityFrameworkSession<TKey, TApplication, TAuthorization>
where TKey : notnull, IEquatable<TKey>
where TApplication : class
where TAuthorization : class
{
/// <summary>
/// Gets or sets the application associated with the session.
/// </summary>
public virtual TApplication? Application { get; set; }
/// <summary>
/// Gets or sets the authorization associated with the session.
/// </summary>
public virtual TAuthorization? Authorization { get; set; }
/// <summary>
/// Gets or sets the concurrency token of the session.
/// </summary>
public virtual string? ConcurrencyToken { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// Gets or sets the UTC creation date of the session.
/// </summary>
public virtual DateTime? CreationDate { get; set; }
/// <summary>
/// Gets or sets the unique identifier of the session.
/// </summary>
public virtual TKey? Id { get; set; }
/// <summary>
/// Gets or sets the login identifier of the session.
/// </summary>
public virtual string? LoginId { get; set; }
/// <summary>
/// Gets or sets the additional properties of the session, serialized as a JSON object.
/// </summary>
[StringSyntax(StringSyntaxAttribute.Json)]
public virtual string? Properties { get; set; }
/// <summary>
/// Gets or sets the status of the session.
/// </summary>
public virtual string? Status { get; set; }
/// <summary>
/// Gets or sets the subject of the session.
/// </summary>
public virtual string? Subject { get; set; }
}

7
src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkToken.cs

@ -12,7 +12,10 @@ namespace OpenIddict.EntityFramework.Models;
/// <summary>
/// Represents an OpenIddict token.
/// </summary>
public class OpenIddictEntityFrameworkToken : OpenIddictEntityFrameworkToken<string, OpenIddictEntityFrameworkApplication, OpenIddictEntityFrameworkAuthorization>
public class OpenIddictEntityFrameworkToken :
OpenIddictEntityFrameworkToken<string,
OpenIddictEntityFrameworkApplication,
OpenIddictEntityFrameworkAuthorization>
{
public OpenIddictEntityFrameworkToken() => Id = Guid.NewGuid().ToString();
}
@ -91,7 +94,7 @@ public class OpenIddictEntityFrameworkToken<TKey, TApplication, TAuthorization>
public virtual string? Status { get; set; }
/// <summary>
/// Gets or sets the subject associated with the token.
/// Gets or sets the subject of the token.
/// </summary>
public virtual string? Subject { get; set; }

74
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;
/// <summary>
/// Defines a relational mapping for the session entity.
/// </summary>
/// <typeparam name="TSession">The type of the session entity.</typeparam>
/// <typeparam name="TApplication">The type of the application entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the authorization entity.</typeparam>
/// <typeparam name="TToken">The type of the token entity.</typeparam>
/// <typeparam name="TKey">The type of the primary key.</typeparam>
[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<TSession>
where TSession : OpenIddictEntityFrameworkSession<TKey, TApplication, TAuthorization>
where TApplication : OpenIddictEntityFrameworkApplication<TKey, TAuthorization, TToken>
where TAuthorization : OpenIddictEntityFrameworkAuthorization<TKey, TApplication, TToken>
where TToken : OpenIddictEntityFrameworkToken<TKey, TApplication, TAuthorization>
where TKey : notnull, IEquatable<TKey>
{
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<Func<TSession, string>>(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");
}
}

9
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<TKey, TAuthorization, TToken>
where TAuthorization : OpenIddictEntityFrameworkAuthorization<TKey, TApplication, TToken>
where TResource : OpenIddictEntityFrameworkResource<TKey>
where TScope : OpenIddictEntityFrameworkScope<TKey>
where TSession : OpenIddictEntityFrameworkSession<TKey, TApplication, TAuthorization>
where TToken : OpenIddictEntityFrameworkToken<TKey, TApplication, TAuthorization>
where TKey : notnull, IEquatable<TKey>
{
@ -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<OpenIddictResourceManager<TResource>>()));
Services.Replace(ServiceDescriptor.Scoped<IOpenIddictScopeManager>(static provider =>
provider.GetRequiredService<OpenIddictScopeManager<TScope>>()));
Services.Replace(ServiceDescriptor.Scoped<IOpenIddictSessionManager>(static provider =>
provider.GetRequiredService<OpenIddictSessionManager<TSession>>()));
Services.Replace(ServiceDescriptor.Scoped<IOpenIddictTokenManager>(static provider =>
provider.GetRequiredService<OpenIddictTokenManager<TToken>>()));
@ -115,6 +120,8 @@ public sealed class OpenIddictEntityFrameworkBuilder
OpenIddictEntityFrameworkResourceStore<TResource, TKey>>());
Services.Replace(ServiceDescriptor.Scoped<IOpenIddictScopeStore<TScope>,
OpenIddictEntityFrameworkScopeStore<TScope, TKey>>());
Services.Replace(ServiceDescriptor.Scoped<IOpenIddictSessionStore<TSession>,
OpenIddictEntityFrameworkSessionStore<TSession, TApplication, TAuthorization, TToken, TKey>>());
Services.Replace(ServiceDescriptor.Scoped<IOpenIddictTokenStore<TToken>,
OpenIddictEntityFrameworkTokenStore<TToken, TApplication, TAuthorization, TKey>>());

2
src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkExtensions.cs

@ -35,12 +35,14 @@ public static class OpenIddictEntityFrameworkExtensions
.SetDefaultAuthorizationEntity<OpenIddictEntityFrameworkAuthorization>()
.SetDefaultResourceEntity<OpenIddictEntityFrameworkResource>()
.SetDefaultScopeEntity<OpenIddictEntityFrameworkScope>()
.SetDefaultSessionEntity<OpenIddictEntityFrameworkSession>()
.SetDefaultTokenEntity<OpenIddictEntityFrameworkToken>();
builder.ReplaceApplicationStore<OpenIddictEntityFrameworkApplication, OpenIddictEntityFrameworkApplicationStore>()
.ReplaceAuthorizationStore<OpenIddictEntityFrameworkAuthorization, OpenIddictEntityFrameworkAuthorizationStore>()
.ReplaceResourceStore<OpenIddictEntityFrameworkResource, OpenIddictEntityFrameworkResourceStore>()
.ReplaceScopeStore<OpenIddictEntityFrameworkScope, OpenIddictEntityFrameworkScopeStore>()
.ReplaceSessionStore<OpenIddictEntityFrameworkSession, OpenIddictEntityFrameworkSessionStore>()
.ReplaceTokenStore<OpenIddictEntityFrameworkToken, OpenIddictEntityFrameworkTokenStore>();
// Note: a default context factory is always registered to make debugging easier when

8
src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkHelpers.cs

@ -29,6 +29,7 @@ public static class OpenIddictEntityFrameworkHelpers
OpenIddictEntityFrameworkAuthorization,
OpenIddictEntityFrameworkResource,
OpenIddictEntityFrameworkScope,
OpenIddictEntityFrameworkSession,
OpenIddictEntityFrameworkToken, string>();
/// <summary>
@ -37,7 +38,7 @@ public static class OpenIddictEntityFrameworkHelpers
/// </summary>
/// <remarks>
/// Note: when using custom entities, the new entities MUST be registered by calling
/// <see cref="OpenIddictEntityFrameworkBuilder.ReplaceDefaultEntities{TApplication, TAuthorization, TResource, TScope, TToken, TKey}"/>.
/// <see cref="OpenIddictEntityFrameworkBuilder.ReplaceDefaultEntities{TApplication, TAuthorization, TResource, TScope, TSession, TToken, TKey}"/>.
/// </remarks>
/// <param name="builder">The builder used to configure the Entity Framework context.</param>
/// <returns>The Entity Framework context builder.</returns>
@ -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<TKey, TAuthorization, TToken>
where TAuthorization : OpenIddictEntityFrameworkAuthorization<TKey, TApplication, TToken>
where TResource : OpenIddictEntityFrameworkResource<TKey>
where TScope : OpenIddictEntityFrameworkScope<TKey>
where TSession : OpenIddictEntityFrameworkSession<TKey, TApplication, TAuthorization>
where TToken : OpenIddictEntityFrameworkToken<TKey, TApplication, TAuthorization>
where TKey : notnull, IEquatable<TKey>
{
@ -62,6 +65,7 @@ public static class OpenIddictEntityFrameworkHelpers
.Add(new OpenIddictEntityFrameworkAuthorizationConfiguration<TAuthorization, TApplication, TToken, TKey>())
.Add(new OpenIddictEntityFrameworkResourceConfiguration<TResource, TKey>())
.Add(new OpenIddictEntityFrameworkScopeConfiguration<TScope, TKey>())
.Add(new OpenIddictEntityFrameworkSessionConfiguration<TSession, TApplication, TAuthorization, TToken, TKey>())
.Add(new OpenIddictEntityFrameworkTokenConfiguration<TToken, TApplication, TAuthorization, TKey>());
return builder;
@ -82,7 +86,7 @@ public static class OpenIddictEntityFrameworkHelpers
static async IAsyncEnumerable<T> ExecuteAsync(IQueryable<T> source, [EnumeratorCancellation] CancellationToken cancellationToken)
{
using var enumerator = ((IDbAsyncEnumerable<T>)source).GetAsyncEnumerator();
using var enumerator = ((IDbAsyncEnumerable<T>) source).GetAsyncEnumerator();
while (await enumerator.MoveNextAsync(cancellationToken))
{

51
src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkAuthorizationStore.cs

@ -159,44 +159,47 @@ public class OpenIddictEntityFrameworkAuthorizationStore<
/// <inheritdoc/>
public virtual async IAsyncEnumerable<TAuthorization> FindAsync(
string? subject, string? client,
string? status, string? type,
ImmutableArray<string>? scopes, [EnumeratorCancellation] CancellationToken cancellationToken)
(string? Subject, string? ApplicationId, string? Status,
string? Type, ImmutableArray<string>? RequiredScopes) query,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
var context = await Context.GetDbContextAsync(cancellationToken);
IQueryable<TAuthorization> query = context.Set<TAuthorization>().Include(authorization => authorization.Application);
IQueryable<TAuthorization> authorizations = context.Set<TAuthorization>().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<TApplication>().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;

706
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;
/// <summary>
/// Provides methods allowing to manage the sessions stored in a database.
/// </summary>
public class OpenIddictEntityFrameworkSessionStore :
OpenIddictEntityFrameworkSessionStore<OpenIddictEntityFrameworkSession,
OpenIddictEntityFrameworkApplication,
OpenIddictEntityFrameworkAuthorization,
OpenIddictEntityFrameworkToken, string>
{
public OpenIddictEntityFrameworkSessionStore(
IMemoryCache cache,
IOpenIddictEntityFrameworkContext context,
IOptionsMonitor<OpenIddictEntityFrameworkOptions> options)
: base(cache, context, options)
{
}
}
/// <summary>
/// Provides methods allowing to manage the sessions stored in a database.
/// </summary>
/// <typeparam name="TSession">The type of the session entity.</typeparam>
/// <typeparam name="TApplication">The type of the application entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the authorization entity.</typeparam>
/// <typeparam name="TToken">The type of the token entity.</typeparam>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
public class OpenIddictEntityFrameworkSessionStore<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApplication,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : IOpenIddictSessionStore<TSession>
where TSession : OpenIddictEntityFrameworkSession<TKey, TApplication, TAuthorization>
where TApplication : OpenIddictEntityFrameworkApplication<TKey, TAuthorization, TToken>
where TAuthorization : OpenIddictEntityFrameworkAuthorization<TKey, TApplication, TToken>
where TToken : OpenIddictEntityFrameworkToken<TKey, TApplication, TAuthorization>
where TKey : notnull, IEquatable<TKey>
{
public OpenIddictEntityFrameworkSessionStore(
IMemoryCache cache,
IOpenIddictEntityFrameworkContext context,
IOptionsMonitor<OpenIddictEntityFrameworkOptions> options)
{
Cache = cache ?? throw new ArgumentNullException(nameof(cache));
Context = context ?? throw new ArgumentNullException(nameof(context));
Options = options ?? throw new ArgumentNullException(nameof(options));
}
/// <summary>
/// Gets the memory cache associated with the current store.
/// </summary>
protected IMemoryCache Cache { get; }
/// <summary>
/// Gets the database context associated with the current store.
/// </summary>
protected IOpenIddictEntityFrameworkContext Context { get; }
/// <summary>
/// Gets the options associated with the current store.
/// </summary>
protected IOptionsMonitor<OpenIddictEntityFrameworkOptions> Options { get; }
/// <inheritdoc/>
public virtual async ValueTask<long> CountAsync(CancellationToken cancellationToken)
{
var context = await Context.GetDbContextAsync(cancellationToken);
return await context.Set<TSession>().LongCountAsync(cancellationToken);
}
/// <inheritdoc/>
public virtual async ValueTask<long> CountAsync<TState, TResult>(
Func<IQueryable<TSession>, TState, IQueryable<TResult>> query,
TState state, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(query);
var context = await Context.GetDbContextAsync(cancellationToken);
return await query(context.Set<TSession>(), state).LongCountAsync(cancellationToken);
}
/// <inheritdoc/>
public virtual async ValueTask CreateAsync(TSession session, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
var context = await Context.GetDbContextAsync(cancellationToken);
context.Set<TSession>().Add(session);
await context.SaveChangesAsync(cancellationToken);
}
/// <inheritdoc/>
public virtual async ValueTask DeleteAsync(TSession session, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
var context = await Context.GetDbContextAsync(cancellationToken);
context.Set<TSession>().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);
}
}
/// <inheritdoc/>
public virtual async IAsyncEnumerable<TSession> FindAsync(
(string? Subject, string? LoginId, string? ApplicationId, string? Status) query,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
var context = await Context.GetDbContextAsync(cancellationToken);
IQueryable<TSession> sessions = context.Set<TSession>()
.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;
}
}
/// <inheritdoc/>
public virtual IAsyncEnumerable<TSession> FindByApplicationIdAsync(string identifier, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(identifier);
return ExecuteAsync(cancellationToken);
async IAsyncEnumerable<TSession> ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
var context = await Context.GetDbContextAsync(cancellationToken);
var key = ConvertIdentifierFromString(identifier);
await foreach (var session in
(from session in context.Set<TSession>().Include(session => session.Application).Include(session => session.Authorization)
where session.Application!.Id!.Equals(key)
select session).AsAsyncEnumerable(cancellationToken))
{
yield return session;
}
}
}
/// <inheritdoc/>
public virtual IAsyncEnumerable<TSession> FindByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(identifier);
return ExecuteAsync(cancellationToken);
async IAsyncEnumerable<TSession> ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
var context = await Context.GetDbContextAsync(cancellationToken);
var key = ConvertIdentifierFromString(identifier);
await foreach (var session in
(from session in context.Set<TSession>().Include(session => session.Application).Include(session => session.Authorization)
where session.Authorization!.Id!.Equals(key)
select session).AsAsyncEnumerable(cancellationToken))
{
yield return session;
}
}
}
/// <inheritdoc/>
public virtual async ValueTask<TSession?> FindByIdAsync(string identifier, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(identifier);
var context = await Context.GetDbContextAsync(cancellationToken);
var key = ConvertIdentifierFromString(identifier);
return await context.Set<TSession>().FindAsync(cancellationToken, [key]);
}
/// <inheritdoc/>
public virtual IAsyncEnumerable<TSession> FindByLoginIdAsync(string identifier, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(identifier);
return ExecuteAsync(cancellationToken);
async IAsyncEnumerable<TSession> ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
var context = await Context.GetDbContextAsync(cancellationToken);
await foreach (var session in
(from session in context.Set<TSession>().Include(session => session.Application).Include(session => session.Authorization)
where session.LoginId == identifier
select session).AsAsyncEnumerable(cancellationToken))
{
yield return session;
}
}
}
/// <inheritdoc/>
public virtual IAsyncEnumerable<TSession> FindBySubjectAsync(string subject, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(subject);
return ExecuteAsync(cancellationToken);
async IAsyncEnumerable<TSession> ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
var context = await Context.GetDbContextAsync(cancellationToken);
await foreach (var session in
(from session in context.Set<TSession>().Include(session => session.Application).Include(session => session.Authorization)
where session.Subject == subject
select session).AsAsyncEnumerable(cancellationToken))
{
yield return session;
}
}
}
/// <inheritdoc/>
public virtual async ValueTask<string?> 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);
}
/// <inheritdoc/>
public virtual async ValueTask<TResult?> GetAsync<TState, TResult>(
Func<IQueryable<TSession>, TState, IQueryable<TResult>> query,
TState state, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(query);
var context = await Context.GetDbContextAsync(cancellationToken);
return await query(context.Set<TSession>(), state).FirstOrDefaultAsync(cancellationToken);
}
/// <inheritdoc/>
public virtual async ValueTask<string?> 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);
}
/// <inheritdoc/>
public virtual ValueTask<DateTimeOffset?> GetCreationDateAsync(TSession session, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
return new(session.CreationDate is DateTime date ? DateTime.SpecifyKind(date, DateTimeKind.Utc) : null);
}
/// <inheritdoc/>
public virtual ValueTask<string?> GetIdAsync(TSession session, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
return new(ConvertIdentifierToString(session.Id));
}
/// <inheritdoc/>
public virtual ValueTask<string?> GetLoginIdAsync(TSession session, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
return new(session.LoginId);
}
/// <inheritdoc/>
public virtual ValueTask<ImmutableDictionary<string, JsonElement>> GetPropertiesAsync(TSession session, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
if (string.IsNullOrEmpty(session.Properties))
{
return new(ImmutableDictionary.Create<string, JsonElement>());
}
// 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<string, JsonElement>();
foreach (var property in document.RootElement.EnumerateObject())
{
builder[property.Name] = property.Value.Clone();
}
return builder.ToImmutable();
})!;
return new(properties);
}
/// <inheritdoc/>
public virtual ValueTask<string?> GetStatusAsync(TSession session, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
return new(session.Status);
}
/// <inheritdoc/>
public virtual ValueTask<string?> GetSubjectAsync(TSession session, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
return new(session.Subject);
}
/// <inheritdoc/>
public virtual ValueTask<TSession> InstantiateAsync(CancellationToken cancellationToken)
{
try
{
return new(Activator.CreateInstance<TSession>());
}
catch (MemberAccessException exception)
{
return new(Task.FromException<TSession>(
new InvalidOperationException(SR.GetResourceString(SR.ID0240), exception)));
}
}
/// <inheritdoc/>
public virtual async IAsyncEnumerable<TSession> ListAsync(int? count, int? offset,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
var context = await Context.GetDbContextAsync(cancellationToken);
IQueryable<TSession> query = context.Set<TSession>().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;
}
}
/// <inheritdoc/>
public virtual IAsyncEnumerable<TResult> ListAsync<TState, TResult>(
Func<IQueryable<TSession>, TState, IQueryable<TResult>> query,
TState state, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(query);
return ExecuteAsync(cancellationToken);
async IAsyncEnumerable<TResult> ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
var context = await Context.GetDbContextAsync(cancellationToken);
await foreach (var session in query(context.Set<TSession>(), state).AsAsyncEnumerable(cancellationToken))
{
yield return session;
}
}
}
/// <inheritdoc/>
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<TApplication>().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;
}
}
/// <inheritdoc/>
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<TAuthorization>().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;
}
}
/// <inheritdoc/>
public virtual ValueTask SetCreationDateAsync(TSession session, DateTimeOffset? date, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
session.CreationDate = date?.UtcDateTime;
return ValueTask.CompletedTask;
}
/// <inheritdoc/>
public virtual ValueTask SetPropertiesAsync(TSession session,
ImmutableDictionary<string, JsonElement> 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;
}
/// <inheritdoc/>
public virtual ValueTask SetLoginIdAsync(TSession session, string? identifier, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
session.LoginId = identifier;
return ValueTask.CompletedTask;
}
/// <inheritdoc/>
public virtual ValueTask SetStatusAsync(TSession session, string? status, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
session.Status = status;
return ValueTask.CompletedTask;
}
/// <inheritdoc/>
public virtual ValueTask SetSubjectAsync(TSession session, string? subject, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
session.Subject = subject;
return ValueTask.CompletedTask;
}
/// <inheritdoc/>
public virtual async ValueTask UpdateAsync(TSession session, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
var context = await Context.GetDbContextAsync(cancellationToken);
context.Set<TSession>().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);
}
}
/// <summary>
/// Converts the provided identifier to a strongly typed key object.
/// </summary>
/// <param name="identifier">The identifier to convert.</param>
/// <returns>An instance of <typeparamref name="TKey"/> representing the provided identifier.</returns>
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);
}
}
/// <summary>
/// Converts the provided identifier to its string representation.
/// </summary>
/// <param name="identifier">The identifier to convert.</param>
/// <returns>A <see cref="string"/> representation of the provided identifier.</returns>
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);
}
}
}

61
src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs

@ -136,38 +136,37 @@ public class OpenIddictEntityFrameworkTokenStore<
/// <inheritdoc/>
public virtual async IAsyncEnumerable<TToken> 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<TToken> query = context.Set<TToken>()
.Include(token => token.Application)
.Include(token => token.Authorization);
IQueryable<TToken> tokens = context.Set<TToken>()
.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<TApplication>().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<TAuthorization>().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;

10
src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreApplication.cs

@ -13,7 +13,10 @@ namespace OpenIddict.EntityFrameworkCore.Models;
/// <summary>
/// Represents an OpenIddict application.
/// </summary>
public class OpenIddictEntityFrameworkCoreApplication : OpenIddictEntityFrameworkCoreApplication<string, OpenIddictEntityFrameworkCoreAuthorization, OpenIddictEntityFrameworkCoreToken>
public class OpenIddictEntityFrameworkCoreApplication :
OpenIddictEntityFrameworkCoreApplication<string,
OpenIddictEntityFrameworkCoreAuthorization,
OpenIddictEntityFrameworkCoreToken>
{
public OpenIddictEntityFrameworkCoreApplication() => Id = Guid.NewGuid().ToString();
}
@ -21,7 +24,10 @@ public class OpenIddictEntityFrameworkCoreApplication : OpenIddictEntityFramewor
/// <summary>
/// Represents an OpenIddict application.
/// </summary>
public class OpenIddictEntityFrameworkCoreApplication<TKey> : OpenIddictEntityFrameworkCoreApplication<TKey, OpenIddictEntityFrameworkCoreAuthorization<TKey>, OpenIddictEntityFrameworkCoreToken<TKey>>
public class OpenIddictEntityFrameworkCoreApplication<TKey> :
OpenIddictEntityFrameworkCoreApplication<TKey,
OpenIddictEntityFrameworkCoreAuthorization<TKey>,
OpenIddictEntityFrameworkCoreToken<TKey>>
where TKey : notnull, IEquatable<TKey>;
/// <summary>

10
src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreAuthorization.cs

@ -12,7 +12,10 @@ namespace OpenIddict.EntityFrameworkCore.Models;
/// <summary>
/// Represents an OpenIddict authorization.
/// </summary>
public class OpenIddictEntityFrameworkCoreAuthorization : OpenIddictEntityFrameworkCoreAuthorization<string, OpenIddictEntityFrameworkCoreApplication, OpenIddictEntityFrameworkCoreToken>
public class OpenIddictEntityFrameworkCoreAuthorization :
OpenIddictEntityFrameworkCoreAuthorization<string,
OpenIddictEntityFrameworkCoreApplication,
OpenIddictEntityFrameworkCoreToken>
{
public OpenIddictEntityFrameworkCoreAuthorization() => Id = Guid.NewGuid().ToString();
}
@ -20,7 +23,10 @@ public class OpenIddictEntityFrameworkCoreAuthorization : OpenIddictEntityFramew
/// <summary>
/// Represents an OpenIddict authorization.
/// </summary>
public class OpenIddictEntityFrameworkCoreAuthorization<TKey> : OpenIddictEntityFrameworkCoreAuthorization<TKey, OpenIddictEntityFrameworkCoreApplication<TKey>, OpenIddictEntityFrameworkCoreToken<TKey>>
public class OpenIddictEntityFrameworkCoreAuthorization<TKey> :
OpenIddictEntityFrameworkCoreAuthorization<TKey,
OpenIddictEntityFrameworkCoreApplication<TKey>,
OpenIddictEntityFrameworkCoreToken<TKey>>
where TKey : notnull, IEquatable<TKey>;
/// <summary>

3
src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreResource.cs

@ -12,7 +12,8 @@ namespace OpenIddict.EntityFrameworkCore.Models;
/// <summary>
/// Represents an OpenIddict resource.
/// </summary>
public class OpenIddictEntityFrameworkCoreResource : OpenIddictEntityFrameworkCoreResource<string>
public class OpenIddictEntityFrameworkCoreResource :
OpenIddictEntityFrameworkCoreResource<string>
{
public OpenIddictEntityFrameworkCoreResource() => Id = Guid.NewGuid().ToString();
}

3
src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreScope.cs

@ -12,7 +12,8 @@ namespace OpenIddict.EntityFrameworkCore.Models;
/// <summary>
/// Represents an OpenIddict scope.
/// </summary>
public class OpenIddictEntityFrameworkCoreScope : OpenIddictEntityFrameworkCoreScope<string>
public class OpenIddictEntityFrameworkCoreScope :
OpenIddictEntityFrameworkCoreScope<string>
{
public OpenIddictEntityFrameworkCoreScope() => Id = Guid.NewGuid().ToString();
}

85
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;
/// <summary>
/// Represents an OpenIddict session.
/// </summary>
public class OpenIddictEntityFrameworkCoreSession :
OpenIddictEntityFrameworkCoreSession<string,
OpenIddictEntityFrameworkCoreApplication,
OpenIddictEntityFrameworkCoreAuthorization>
{
public OpenIddictEntityFrameworkCoreSession() => Id = Guid.NewGuid().ToString();
}
/// <summary>
/// Represents an OpenIddict session.
/// </summary>
public class OpenIddictEntityFrameworkCoreSession<TKey> :
OpenIddictEntityFrameworkCoreSession<TKey,
OpenIddictEntityFrameworkCoreApplication<TKey>,
OpenIddictEntityFrameworkCoreAuthorization<TKey>>
where TKey : notnull, IEquatable<TKey>;
/// <summary>
/// Represents an OpenIddict session.
/// </summary>
[DebuggerDisplay("Id = {Id.ToString(),nq} ; Subject = {Subject,nq} ; LoginId = {LoginId,nq} ; Status = {Status,nq}")]
public class OpenIddictEntityFrameworkCoreSession<TKey, TApplication, TAuthorization>
where TKey : notnull, IEquatable<TKey>
where TApplication : class
where TAuthorization : class
{
/// <summary>
/// Gets or sets the application associated with the session.
/// </summary>
public virtual TApplication? Application { get; set; }
/// <summary>
/// Gets or sets the authorization associated with the session.
/// </summary>
public virtual TAuthorization? Authorization { get; set; }
/// <summary>
/// Gets or sets the concurrency token of the session.
/// </summary>
public virtual string? ConcurrencyToken { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// Gets or sets the UTC creation date of the session.
/// </summary>
public virtual DateTime? CreationDate { get; set; }
/// <summary>
/// Gets or sets the unique identifier of the session.
/// </summary>
public virtual TKey? Id { get; set; }
/// <summary>
/// Gets or sets the login identifier of the session.
/// </summary>
public virtual string? LoginId { get; set; }
/// <summary>
/// Gets or sets the additional properties of the session.
/// </summary>
public virtual IDictionary<string, JsonElement>? Properties { get; set; }
/// <summary>
/// Gets or sets the status of the session.
/// </summary>
public virtual string? Status { get; set; }
/// <summary>
/// Gets or sets the subject of the session.
/// </summary>
public virtual string? Subject { get; set; }
}

14
src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreToken.cs

@ -12,7 +12,10 @@ namespace OpenIddict.EntityFrameworkCore.Models;
/// <summary>
/// Represents an OpenIddict token.
/// </summary>
public class OpenIddictEntityFrameworkCoreToken : OpenIddictEntityFrameworkCoreToken<string, OpenIddictEntityFrameworkCoreApplication, OpenIddictEntityFrameworkCoreAuthorization>
public class OpenIddictEntityFrameworkCoreToken :
OpenIddictEntityFrameworkCoreToken<string,
OpenIddictEntityFrameworkCoreApplication,
OpenIddictEntityFrameworkCoreAuthorization>
{
public OpenIddictEntityFrameworkCoreToken() => Id = Guid.NewGuid().ToString();
}
@ -20,7 +23,10 @@ public class OpenIddictEntityFrameworkCoreToken : OpenIddictEntityFrameworkCoreT
/// <summary>
/// Represents an OpenIddict token.
/// </summary>
public class OpenIddictEntityFrameworkCoreToken<TKey> : OpenIddictEntityFrameworkCoreToken<TKey, OpenIddictEntityFrameworkCoreApplication<TKey>, OpenIddictEntityFrameworkCoreAuthorization<TKey>>
public class OpenIddictEntityFrameworkCoreToken<TKey> :
OpenIddictEntityFrameworkCoreToken<TKey,
OpenIddictEntityFrameworkCoreApplication<TKey>,
OpenIddictEntityFrameworkCoreAuthorization<TKey>>
where TKey : notnull, IEquatable<TKey>;
/// <summary>
@ -33,12 +39,12 @@ public class OpenIddictEntityFrameworkCoreToken<TKey, TApplication, TAuthorizati
where TAuthorization : class
{
/// <summary>
/// Gets or sets the application associated with the current token.
/// Gets or sets the application associated with the token.
/// </summary>
public virtual TApplication? Application { get; set; }
/// <summary>
/// Gets or sets the authorization associated with the current token.
/// Gets or sets the authorization associated with the token.
/// </summary>
public virtual TAuthorization? Authorization { get; set; }

85
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;
/// <summary>
/// Defines a relational mapping for the session entity.
/// </summary>
/// <typeparam name="TSession">The type of the session entity.</typeparam>
/// <typeparam name="TApplication">The type of the application entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the authorization entity.</typeparam>
/// <typeparam name="TToken">The type of the token entity.</typeparam>
/// <typeparam name="TKey">The type of the primary key.</typeparam>
[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<TSession>
where TSession : OpenIddictEntityFrameworkCoreSession<TKey, TApplication, TAuthorization>
where TApplication : OpenIddictEntityFrameworkCoreApplication<TKey, TAuthorization, TToken>
where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization<TKey, TApplication, TToken>
where TToken : OpenIddictEntityFrameworkCoreToken<TKey, TApplication, TAuthorization>
where TKey : notnull, IEquatable<TKey>
{
public void Configure(EntityTypeBuilder<TSession> 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<JsonElement>());
builder.ToTable("OpenIddictSessions");
static ValueComparer CreateDictionaryComparer<TValue>() => new ValueComparer<IDictionary<string, TValue>>(
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());
}
}

7
src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs

@ -77,6 +77,7 @@ public sealed class OpenIddictEntityFrameworkCoreBuilder
OpenIddictEntityFrameworkCoreAuthorization<TKey>,
OpenIddictEntityFrameworkCoreResource<TKey>,
OpenIddictEntityFrameworkCoreScope<TKey>,
OpenIddictEntityFrameworkCoreSession<TKey>,
OpenIddictEntityFrameworkCoreToken<TKey>, TKey>();
/// <summary>
@ -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<TKey, TAuthorization, TToken>
where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization<TKey, TApplication, TToken>
where TResource : OpenIddictEntityFrameworkCoreResource<TKey>
where TScope : OpenIddictEntityFrameworkCoreScope<TKey>
where TSession : OpenIddictEntityFrameworkCoreSession<TKey, TApplication, TAuthorization>
where TToken : OpenIddictEntityFrameworkCoreToken<TKey, TApplication, TAuthorization>
where TKey : notnull, IEquatable<TKey>
{
@ -114,6 +117,8 @@ public sealed class OpenIddictEntityFrameworkCoreBuilder
provider.GetRequiredService<OpenIddictResourceManager<TResource>>()));
Services.Replace(ServiceDescriptor.Scoped<IOpenIddictScopeManager>(static provider =>
provider.GetRequiredService<OpenIddictScopeManager<TScope>>()));
Services.Replace(ServiceDescriptor.Scoped<IOpenIddictSessionManager>(static provider =>
provider.GetRequiredService<OpenIddictSessionManager<TSession>>()));
Services.Replace(ServiceDescriptor.Scoped<IOpenIddictTokenManager>(static provider =>
provider.GetRequiredService<OpenIddictTokenManager<TToken>>()));
@ -125,6 +130,8 @@ public sealed class OpenIddictEntityFrameworkCoreBuilder
OpenIddictEntityFrameworkCoreResourceStore<TResource, TKey>>());
Services.Replace(ServiceDescriptor.Scoped<IOpenIddictScopeStore<TScope>,
OpenIddictEntityFrameworkCoreScopeStore<TScope, TKey>>());
Services.Replace(ServiceDescriptor.Scoped<IOpenIddictSessionStore<TSession>,
OpenIddictEntityFrameworkCoreSessionStore<TSession, TApplication, TAuthorization, TToken, TKey>>());
Services.Replace(ServiceDescriptor.Scoped<IOpenIddictTokenStore<TToken>,
OpenIddictEntityFrameworkCoreTokenStore<TToken, TApplication, TAuthorization, TKey>>());

4
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<TKey, TAuthorization, TToken>
where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization<TKey, TApplication, TToken>
where TResource : OpenIddictEntityFrameworkCoreResource<TKey>
where TScope : OpenIddictEntityFrameworkCoreScope<TKey>
where TSession : OpenIddictEntityFrameworkCoreSession<TKey, TApplication, TAuthorization>
where TToken : OpenIddictEntityFrameworkCoreToken<TKey, TApplication, TAuthorization>
where TKey : notnull, IEquatable<TKey>
{
@ -41,7 +43,7 @@ public sealed class OpenIddictEntityFrameworkCoreCustomizer<
ArgumentNullException.ThrowIfNull(context);
// Register the OpenIddict entity sets.
modelBuilder.UseOpenIddict<TApplication, TAuthorization, TResource, TScope, TToken, TKey>();
modelBuilder.UseOpenIddict<TApplication, TAuthorization, TResource, TScope, TSession, TToken, TKey>();
base.Customize(modelBuilder, context);
}

2
src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreExtensions.cs

@ -35,12 +35,14 @@ public static class OpenIddictEntityFrameworkCoreExtensions
.SetDefaultAuthorizationEntity<OpenIddictEntityFrameworkCoreAuthorization>()
.SetDefaultResourceEntity<OpenIddictEntityFrameworkCoreResource>()
.SetDefaultScopeEntity<OpenIddictEntityFrameworkCoreScope>()
.SetDefaultSessionEntity<OpenIddictEntityFrameworkCoreSession>()
.SetDefaultTokenEntity<OpenIddictEntityFrameworkCoreToken>();
builder.ReplaceApplicationStore<OpenIddictEntityFrameworkCoreApplication, OpenIddictEntityFrameworkCoreApplicationStore>()
.ReplaceAuthorizationStore<OpenIddictEntityFrameworkCoreAuthorization, OpenIddictEntityFrameworkCoreAuthorizationStore>()
.ReplaceResourceStore<OpenIddictEntityFrameworkCoreResource, OpenIddictEntityFrameworkCoreResourceStore>()
.ReplaceScopeStore<OpenIddictEntityFrameworkCoreScope, OpenIddictEntityFrameworkCoreScopeStore>()
.ReplaceSessionStore<OpenIddictEntityFrameworkCoreSession, OpenIddictEntityFrameworkCoreSessionStore>()
.ReplaceTokenStore<OpenIddictEntityFrameworkCoreToken, OpenIddictEntityFrameworkCoreTokenStore>();
// Note: a default context factory is always registered to make debugging easier when

21
src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs

@ -27,6 +27,7 @@ public static class OpenIddictEntityFrameworkCoreHelpers
OpenIddictEntityFrameworkCoreAuthorization,
OpenIddictEntityFrameworkCoreResource,
OpenIddictEntityFrameworkCoreScope,
OpenIddictEntityFrameworkCoreSession,
OpenIddictEntityFrameworkCoreToken, string>();
/// <summary>
@ -59,6 +60,7 @@ public static class OpenIddictEntityFrameworkCoreHelpers
OpenIddictEntityFrameworkCoreAuthorization<TKey>,
OpenIddictEntityFrameworkCoreResource<TKey>,
OpenIddictEntityFrameworkCoreScope<TKey>,
OpenIddictEntityFrameworkCoreSession<TKey>,
OpenIddictEntityFrameworkCoreToken<TKey>, TKey>();
/// <summary>
@ -87,7 +89,7 @@ public static class OpenIddictEntityFrameworkCoreHelpers
/// </summary>
/// <remarks>
/// Note: when using custom entities, the new entities MUST be registered by calling
/// <see cref="OpenIddictEntityFrameworkCoreBuilder.ReplaceDefaultEntities{TApplication, TAuthorization, TResource, TScope, TToken, TKey}"/>.
/// <see cref="OpenIddictEntityFrameworkCoreBuilder.ReplaceDefaultEntities{TApplication, TAuthorization, TResource, TScope, TSession, TToken, TKey}"/>.
/// </remarks>
/// <param name="builder">The builder used to configure the Entity Framework Core context.</param>
/// <returns>The Entity Framework Core context builder.</returns>
@ -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<TKey, TApplication, TToken>
where TResource : OpenIddictEntityFrameworkCoreResource<TKey>
where TScope : OpenIddictEntityFrameworkCoreScope<TKey>
where TSession : OpenIddictEntityFrameworkCoreSession<TKey, TApplication, TAuthorization>
where TToken : OpenIddictEntityFrameworkCoreToken<TKey, TApplication, TAuthorization>
where TKey : notnull, IEquatable<TKey>
{
ArgumentNullException.ThrowIfNull(builder);
return ReplaceService<IModelCustomizer,
OpenIddictEntityFrameworkCoreCustomizer<TApplication, TAuthorization, TResource, TScope, TToken, TKey>>(builder);
OpenIddictEntityFrameworkCoreCustomizer<TApplication, TAuthorization, TResource, TScope, TSession, TToken, TKey>>(builder);
static DbContextOptionsBuilder ReplaceService<
TService,
@ -124,7 +128,7 @@ public static class OpenIddictEntityFrameworkCoreHelpers
/// </summary>
/// <remarks>
/// Note: when using custom entities, the new entities MUST be registered by calling
/// <see cref="OpenIddictEntityFrameworkCoreBuilder.ReplaceDefaultEntities{TApplication, TAuthorization, TResource, TScope, TToken, TKey}"/>.
/// <see cref="OpenIddictEntityFrameworkCoreBuilder.ReplaceDefaultEntities{TApplication, TAuthorization, TResource, TScope, TSession, TToken, TKey}"/>.
/// </remarks>
/// <param name="builder">The builder used to configure the Entity Framework Core context.</param>
/// <returns>The Entity Framework Core context builder.</returns>
@ -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<TKey, TApplication, TToken>
where TResource : OpenIddictEntityFrameworkCoreResource<TKey>
where TScope : OpenIddictEntityFrameworkCoreScope<TKey>
where TSession : OpenIddictEntityFrameworkCoreSession<TKey, TApplication, TAuthorization>
where TToken : OpenIddictEntityFrameworkCoreToken<TKey, TApplication, TAuthorization>
where TKey : notnull, IEquatable<TKey>
where TContext : DbContext
{
builder.UseOpenIddict<TApplication, TAuthorization, TResource, TScope, TToken, TKey>();
builder.UseOpenIddict<TApplication, TAuthorization, TResource, TScope, TSession, TToken, TKey>();
return builder;
}
@ -160,6 +166,7 @@ public static class OpenIddictEntityFrameworkCoreHelpers
OpenIddictEntityFrameworkCoreAuthorization,
OpenIddictEntityFrameworkCoreResource,
OpenIddictEntityFrameworkCoreScope,
OpenIddictEntityFrameworkCoreSession,
OpenIddictEntityFrameworkCoreToken, string>();
/// <summary>
@ -178,6 +185,7 @@ public static class OpenIddictEntityFrameworkCoreHelpers
OpenIddictEntityFrameworkCoreAuthorization<TKey>,
OpenIddictEntityFrameworkCoreResource<TKey>,
OpenIddictEntityFrameworkCoreScope<TKey>,
OpenIddictEntityFrameworkCoreSession<TKey>,
OpenIddictEntityFrameworkCoreToken<TKey>, TKey>();
/// <summary>
@ -186,7 +194,7 @@ public static class OpenIddictEntityFrameworkCoreHelpers
/// </summary>
/// <remarks>
/// Note: when using custom entities, the new entities MUST be registered by calling
/// <see cref="OpenIddictEntityFrameworkCoreBuilder.ReplaceDefaultEntities{TApplication, TAuthorization, TResource, TScope, TToken, TKey}"/>.
/// <see cref="OpenIddictEntityFrameworkCoreBuilder.ReplaceDefaultEntities{TApplication, TAuthorization, TResource, TScope, TSession, TToken, TKey}"/>.
/// </remarks>
/// <param name="builder">The builder used to configure the Entity Framework Core context.</param>
/// <returns>The Entity Framework Core context builder.</returns>
@ -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<TKey, TAuthorization, TToken>
where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization<TKey, TApplication, TToken>
where TResource : OpenIddictEntityFrameworkCoreResource<TKey>
where TScope : OpenIddictEntityFrameworkCoreScope<TKey>
where TSession : OpenIddictEntityFrameworkCoreSession<TKey, TApplication, TAuthorization>
where TToken : OpenIddictEntityFrameworkCoreToken<TKey, TApplication, TAuthorization>
where TKey : notnull, IEquatable<TKey>
{
@ -211,6 +221,7 @@ public static class OpenIddictEntityFrameworkCoreHelpers
.ApplyConfiguration(new OpenIddictEntityFrameworkCoreAuthorizationConfiguration<TAuthorization, TApplication, TToken, TKey>())
.ApplyConfiguration(new OpenIddictEntityFrameworkCoreResourceConfiguration<TResource, TKey>())
.ApplyConfiguration(new OpenIddictEntityFrameworkCoreScopeConfiguration<TScope, TKey>())
.ApplyConfiguration(new OpenIddictEntityFrameworkCoreSessionConfiguration<TSession, TApplication, TAuthorization, TToken, TKey>())
.ApplyConfiguration(new OpenIddictEntityFrameworkCoreTokenConfiguration<TToken, TApplication, TAuthorization, TKey>());
}
}

54
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs

@ -217,44 +217,47 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore<
/// <inheritdoc/>
public virtual async IAsyncEnumerable<TAuthorization> FindAsync(
string? subject, string? client,
string? status, string? type,
ImmutableArray<string>? scopes, [EnumeratorCancellation] CancellationToken cancellationToken)
(string? Subject, string? ApplicationId, string? Status,
string? Type, ImmutableArray<string>? RequiredScopes) query,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
var context = await Context.GetDbContextAsync(cancellationToken);
IQueryable<TAuthorization> query = context.Set<TAuthorization>().Include(authorization => authorization.Application).AsTracking();
IQueryable<TAuthorization> authorizations = context.Set<TAuthorization>().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<TApplication>()
.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;

6
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreResourceStore.cs

@ -19,7 +19,8 @@ namespace OpenIddict.EntityFrameworkCore;
/// <summary>
/// Provides methods allowing to manage the resources stored in a database.
/// </summary>
public class OpenIddictEntityFrameworkCoreResourceStore : OpenIddictEntityFrameworkCoreResourceStore<OpenIddictEntityFrameworkCoreResource, string>
public class OpenIddictEntityFrameworkCoreResourceStore :
OpenIddictEntityFrameworkCoreResourceStore<OpenIddictEntityFrameworkCoreResource, string>
{
public OpenIddictEntityFrameworkCoreResourceStore(
IOpenIddictEntityFrameworkCoreContext context,
@ -34,7 +35,8 @@ public class OpenIddictEntityFrameworkCoreResourceStore : OpenIddictEntityFramew
/// </summary>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
public class OpenIddictEntityFrameworkCoreResourceStore<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : OpenIddictEntityFrameworkCoreResourceStore<OpenIddictEntityFrameworkCoreResource<TKey>, TKey>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> :
OpenIddictEntityFrameworkCoreResourceStore<OpenIddictEntityFrameworkCoreResource<TKey>, TKey>
where TKey : notnull, IEquatable<TKey>
{
public OpenIddictEntityFrameworkCoreResourceStore(

6
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreScopeStore.cs

@ -19,7 +19,8 @@ namespace OpenIddict.EntityFrameworkCore;
/// <summary>
/// Provides methods allowing to manage the scopes stored in a database.
/// </summary>
public class OpenIddictEntityFrameworkCoreScopeStore : OpenIddictEntityFrameworkCoreScopeStore<OpenIddictEntityFrameworkCoreScope, string>
public class OpenIddictEntityFrameworkCoreScopeStore :
OpenIddictEntityFrameworkCoreScopeStore<OpenIddictEntityFrameworkCoreScope, string>
{
public OpenIddictEntityFrameworkCoreScopeStore(
IOpenIddictEntityFrameworkCoreContext context,
@ -34,7 +35,8 @@ public class OpenIddictEntityFrameworkCoreScopeStore : OpenIddictEntityFramework
/// </summary>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
public class OpenIddictEntityFrameworkCoreScopeStore<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : OpenIddictEntityFrameworkCoreScopeStore<OpenIddictEntityFrameworkCoreScope<TKey>, TKey>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> :
OpenIddictEntityFrameworkCoreScopeStore<OpenIddictEntityFrameworkCoreScope<TKey>, TKey>
where TKey : notnull, IEquatable<TKey>
{
public OpenIddictEntityFrameworkCoreScopeStore(

668
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;
/// <summary>
/// Provides methods allowing to manage the sessions stored in a database.
/// </summary>
public class OpenIddictEntityFrameworkCoreSessionStore :
OpenIddictEntityFrameworkCoreSessionStore<OpenIddictEntityFrameworkCoreSession,
OpenIddictEntityFrameworkCoreApplication,
OpenIddictEntityFrameworkCoreAuthorization,
OpenIddictEntityFrameworkCoreToken, string>
{
public OpenIddictEntityFrameworkCoreSessionStore(
IOpenIddictEntityFrameworkCoreContext context,
IOptionsMonitor<OpenIddictEntityFrameworkCoreOptions> options)
: base(context, options)
{
}
}
/// <summary>
/// Provides methods allowing to manage the sessions stored in a database.
/// </summary>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
public class OpenIddictEntityFrameworkCoreSessionStore<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> :
OpenIddictEntityFrameworkCoreSessionStore<OpenIddictEntityFrameworkCoreSession<TKey>,
OpenIddictEntityFrameworkCoreApplication<TKey>,
OpenIddictEntityFrameworkCoreAuthorization<TKey>,
OpenIddictEntityFrameworkCoreToken<TKey>, TKey>
where TKey : notnull, IEquatable<TKey>
{
public OpenIddictEntityFrameworkCoreSessionStore(
IOpenIddictEntityFrameworkCoreContext context,
IOptionsMonitor<OpenIddictEntityFrameworkCoreOptions> options)
: base(context, options)
{
}
}
/// <summary>
/// Provides methods allowing to manage the sessions stored in a database.
/// </summary>
/// <typeparam name="TSession">The type of the session entity.</typeparam>
/// <typeparam name="TApplication">The type of the application entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the authorization entity.</typeparam>
/// <typeparam name="TToken">The type of the token entity.</typeparam>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
public class OpenIddictEntityFrameworkCoreSessionStore<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApplication,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : IOpenIddictSessionStore<TSession>
where TSession : OpenIddictEntityFrameworkCoreSession<TKey, TApplication, TAuthorization>
where TApplication : OpenIddictEntityFrameworkCoreApplication<TKey, TAuthorization, TToken>
where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization<TKey, TApplication, TToken>
where TToken : OpenIddictEntityFrameworkCoreToken<TKey, TApplication, TAuthorization>
where TKey : notnull, IEquatable<TKey>
{
public OpenIddictEntityFrameworkCoreSessionStore(
IOpenIddictEntityFrameworkCoreContext context,
IOptionsMonitor<OpenIddictEntityFrameworkCoreOptions> options)
{
Context = context ?? throw new ArgumentNullException(nameof(context));
Options = options ?? throw new ArgumentNullException(nameof(options));
}
/// <summary>
/// Gets the database context associated with the current store.
/// </summary>
protected IOpenIddictEntityFrameworkCoreContext Context { get; }
/// <summary>
/// Gets the options associated with the current store.
/// </summary>
protected IOptionsMonitor<OpenIddictEntityFrameworkCoreOptions> Options { get; }
/// <inheritdoc/>
public virtual async ValueTask<long> CountAsync(CancellationToken cancellationToken)
{
var context = await Context.GetDbContextAsync(cancellationToken);
return await context.Set<TSession>().LongCountAsync(cancellationToken);
}
/// <inheritdoc/>
public virtual async ValueTask<long> CountAsync<TState, TResult>(
Func<IQueryable<TSession>, TState, IQueryable<TResult>> query,
TState state, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(query);
var context = await Context.GetDbContextAsync(cancellationToken);
return await query(context.Set<TSession>(), state).LongCountAsync(cancellationToken);
}
/// <inheritdoc/>
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);
}
/// <inheritdoc/>
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);
}
}
/// <inheritdoc/>
public virtual async IAsyncEnumerable<TSession> FindAsync(
(string? Subject, string? LoginId, string? ApplicationId, string? Status) query,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
var context = await Context.GetDbContextAsync(cancellationToken);
IQueryable<TSession> sessions = context.Set<TSession>()
.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;
}
}
/// <inheritdoc/>
public virtual IAsyncEnumerable<TSession> FindByApplicationIdAsync(string identifier, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(identifier);
return ExecuteAsync(cancellationToken);
async IAsyncEnumerable<TSession> ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
var context = await Context.GetDbContextAsync(cancellationToken);
var key = ConvertIdentifierFromString(identifier);
await foreach (var session in
(from session in context.Set<TSession>()
.Include(session => session.Application)
.Include(session => session.Authorization)
.AsTracking()
where session.Application!.Id!.Equals(key)
select session).AsAsyncEnumerable().WithCancellation(cancellationToken))
{
yield return session;
}
}
}
/// <inheritdoc/>
public virtual IAsyncEnumerable<TSession> FindByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(identifier);
return ExecuteAsync(cancellationToken);
async IAsyncEnumerable<TSession> ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
var context = await Context.GetDbContextAsync(cancellationToken);
var key = ConvertIdentifierFromString(identifier);
await foreach (var session in
(from session in context.Set<TSession>()
.Include(session => session.Application)
.Include(session => session.Authorization)
.AsTracking()
where session.Authorization!.Id!.Equals(key)
select session).AsAsyncEnumerable().WithCancellation(cancellationToken))
{
yield return session;
}
}
}
/// <inheritdoc/>
public virtual async ValueTask<TSession?> FindByIdAsync(string identifier, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(identifier);
var context = await Context.GetDbContextAsync(cancellationToken);
var key = ConvertIdentifierFromString(identifier);
return await context.Set<TSession>().FindAsync([key], cancellationToken);
}
/// <inheritdoc/>
public virtual IAsyncEnumerable<TSession> FindByLoginIdAsync(string identifier, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(identifier);
return ExecuteAsync(cancellationToken);
async IAsyncEnumerable<TSession> ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
var context = await Context.GetDbContextAsync(cancellationToken);
await foreach (var session in
(from session in context.Set<TSession>()
.Include(session => session.Application)
.Include(session => session.Authorization)
.AsTracking()
where session.LoginId == identifier
select session).AsAsyncEnumerable().WithCancellation(cancellationToken))
{
yield return session;
}
}
}
/// <inheritdoc/>
public virtual IAsyncEnumerable<TSession> FindBySubjectAsync(string subject, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(subject);
return ExecuteAsync(cancellationToken);
async IAsyncEnumerable<TSession> ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
var context = await Context.GetDbContextAsync(cancellationToken);
await foreach (var session in
(from session in context.Set<TSession>()
.Include(session => session.Application)
.Include(session => session.Authorization)
.AsTracking()
where session.Subject == subject
select session).AsAsyncEnumerable().WithCancellation(cancellationToken))
{
yield return session;
}
}
}
/// <inheritdoc/>
public virtual async ValueTask<string?> 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);
}
/// <inheritdoc/>
public virtual async ValueTask<TResult?> GetAsync<TState, TResult>(
Func<IQueryable<TSession>, TState, IQueryable<TResult>> query,
TState state, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(query);
var context = await Context.GetDbContextAsync(cancellationToken);
return await query(context.Set<TSession>().AsTracking(), state).FirstOrDefaultAsync(cancellationToken);
}
/// <inheritdoc/>
public virtual async ValueTask<string?> 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);
}
/// <inheritdoc/>
public virtual ValueTask<DateTimeOffset?> GetCreationDateAsync(TSession session, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
return new(session.CreationDate is DateTime date ? DateTime.SpecifyKind(date, DateTimeKind.Utc) : null);
}
/// <inheritdoc/>
public virtual ValueTask<string?> GetIdAsync(TSession session, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
return new(ConvertIdentifierToString(session.Id));
}
/// <inheritdoc/>
public virtual ValueTask<string?> GetLoginIdAsync(TSession session, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
return new(session.LoginId);
}
/// <inheritdoc/>
public virtual ValueTask<ImmutableDictionary<string, JsonElement>> GetPropertiesAsync(TSession session, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
return new(session.Properties is { Count: > 0 } properties ? [.. properties] : []);
}
/// <inheritdoc/>
public virtual ValueTask<string?> GetStatusAsync(TSession session, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
return new(session.Status);
}
/// <inheritdoc/>
public virtual ValueTask<string?> GetSubjectAsync(TSession session, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
return new(session.Subject);
}
/// <inheritdoc/>
public virtual ValueTask<TSession> InstantiateAsync(CancellationToken cancellationToken)
{
try
{
return new(Activator.CreateInstance<TSession>());
}
catch (MemberAccessException exception)
{
return new(Task.FromException<TSession>(
new InvalidOperationException(SR.GetResourceString(SR.ID0240), exception)));
}
}
/// <inheritdoc/>
public virtual async IAsyncEnumerable<TSession> ListAsync(int? count, int? offset,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
var context = await Context.GetDbContextAsync(cancellationToken);
var query = context.Set<TSession>().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;
}
}
/// <inheritdoc/>
public virtual IAsyncEnumerable<TResult> ListAsync<TState, TResult>(
Func<IQueryable<TSession>, TState, IQueryable<TResult>> query,
TState state, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(query);
return ExecuteAsync(cancellationToken);
async IAsyncEnumerable<TResult> ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
var context = await Context.GetDbContextAsync(cancellationToken);
await foreach (var session in query(context.Set<TSession>().AsTracking(), state).AsAsyncEnumerable().WithCancellation(cancellationToken))
{
yield return session;
}
}
}
/// <inheritdoc/>
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<TApplication>()
.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;
}
}
/// <inheritdoc/>
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<TAuthorization>()
.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;
}
}
/// <inheritdoc/>
public virtual ValueTask SetCreationDateAsync(TSession session, DateTimeOffset? date, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
session.CreationDate = date?.UtcDateTime;
return ValueTask.CompletedTask;
}
/// <inheritdoc/>
public virtual ValueTask SetPropertiesAsync(TSession session,
ImmutableDictionary<string, JsonElement> properties, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
session.Properties = properties;
return ValueTask.CompletedTask;
}
/// <inheritdoc/>
public virtual ValueTask SetLoginIdAsync(TSession session, string? identifier, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
session.LoginId = identifier;
return ValueTask.CompletedTask;
}
/// <inheritdoc/>
public virtual ValueTask SetStatusAsync(TSession session, string? status, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
session.Status = status;
return ValueTask.CompletedTask;
}
/// <inheritdoc/>
public virtual ValueTask SetSubjectAsync(TSession session, string? subject, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
session.Subject = subject;
return ValueTask.CompletedTask;
}
/// <inheritdoc/>
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);
}
}
/// <summary>
/// Converts the provided identifier to a strongly typed key object.
/// </summary>
/// <param name="identifier">The identifier to convert.</param>
/// <returns>An instance of <typeparamref name="TKey"/> representing the provided identifier.</returns>
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);
}
}
/// <summary>
/// Converts the provided identifier to its string representation.
/// </summary>
/// <param name="identifier">The identifier to convert.</param>
/// <returns>A <see cref="string"/> representation of the provided identifier.</returns>
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);
}
}
}

72
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs

@ -143,39 +143,38 @@ public class OpenIddictEntityFrameworkCoreTokenStore<
/// <inheritdoc/>
public virtual async IAsyncEnumerable<TToken> 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<TToken> query = context.Set<TToken>()
.Include(token => token.Application)
.Include(token => token.Authorization)
.AsTracking();
IQueryable<TToken> tokens = context.Set<TToken>()
.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<TToken>().Include(token => token.Application).Include(token => token.Authorization).AsTracking()
(from token in context.Set<TToken>()
.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<TApplication>()
.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<TAuthorization>()
.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;

2
src/OpenIddict.MongoDb.Models/OpenIddictMongoDbAuthorization.cs

@ -58,7 +58,7 @@ public class OpenIddictMongoDbAuthorization
public virtual string? Status { get; set; }
/// <summary>
/// Gets or sets the subject associated with the authorization.
/// Gets or sets the subject of the authorization.
/// </summary>
[BsonElement("subject"), BsonIgnoreIfNull]
public virtual string? Subject { get; set; }

70
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;
/// <summary>
/// Represents an OpenIddict session.
/// </summary>
[DebuggerDisplay("Id = {Id.ToString(),nq} ; Name = {Name,nq}")]
public class OpenIddictMongoDbSession
{
/// <summary>
/// Gets or sets the identifier of the application associated with the session.
/// </summary>
[BsonElement("application_id"), BsonIgnoreIfDefault]
public virtual ObjectId ApplicationId { get; set; }
/// <summary>
/// Gets or sets the identifier of the authorization associated with the session.
/// </summary>
[BsonElement("authorization_id"), BsonIgnoreIfDefault]
public virtual ObjectId AuthorizationId { get; set; }
/// <summary>
/// Gets or sets the concurrency token of the session.
/// </summary>
[BsonElement("concurrency_token"), BsonIgnoreIfNull]
public virtual string? ConcurrencyToken { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// Gets or sets the UTC creation date of the session.
/// </summary>
[BsonElement("creation_date"), BsonIgnoreIfNull]
public virtual DateTime? CreationDate { get; set; }
/// <summary>
/// Gets or sets the unique identifier of the session.
/// </summary>
[BsonId, BsonRequired]
public virtual ObjectId Id { get; set; }
/// <summary>
/// Gets or sets the login identifier of the session.
/// </summary>
[BsonElement("login_id"), BsonIgnoreIfNull]
public virtual string? LoginId { get; set; }
/// <summary>
/// Gets or sets the additional properties of the session.
/// </summary>
[BsonElement("properties"), BsonIgnoreIfNull]
public virtual BsonDocument? Properties { get; set; }
/// <summary>
/// Gets or sets the status of the session.
/// </summary>
[BsonElement("status"), BsonIgnoreIfNull]
public virtual string? Status { get; set; }
/// <summary>
/// Gets or sets the subject of the session.
/// </summary>
[BsonElement("subject"), BsonIgnoreIfNull]
public virtual string? Subject { get; set; }
}

2
src/OpenIddict.MongoDb.Models/OpenIddictMongoDbToken.cs

@ -89,7 +89,7 @@ public class OpenIddictMongoDbToken
public virtual string? Status { get; set; }
/// <summary>
/// Gets or sets the subject associated with the token.
/// Gets or sets the subject of the token.
/// </summary>
[BsonElement("subject"), BsonIgnoreIfNull]
public virtual string? Subject { get; set; }

29
src/OpenIddict.MongoDb/OpenIddictMongoDbBuilder.cs

@ -126,6 +126,23 @@ public sealed class OpenIddictMongoDbBuilder
return this;
}
/// <summary>
/// Configures OpenIddict to use the specified entity as the default session entity.
/// </summary>
/// <returns>The <see cref="OpenIddictMongoDbBuilder"/> instance.</returns>
public OpenIddictMongoDbBuilder ReplaceDefaultSessionEntity<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession>()
where TSession : OpenIddictMongoDbSession
{
Services.Replace(ServiceDescriptor.Scoped<IOpenIddictSessionManager>(static provider =>
provider.GetRequiredService<OpenIddictSessionManager<TSession>>()));
Services.Replace(ServiceDescriptor.Scoped<
IOpenIddictSessionStore<TSession>, OpenIddictMongoDbSessionStore<TSession>>());
return this;
}
/// <summary>
/// Configures OpenIddict to use the specified entity as the default token entity.
/// </summary>
@ -191,6 +208,18 @@ public sealed class OpenIddictMongoDbBuilder
return Configure(options => options.ScopesCollectionName = name);
}
/// <summary>
/// Replaces the default sessions collection name (by default, openiddict.sessions).
/// </summary>
/// <param name="name">The collection name</param>
/// <returns>The <see cref="OpenIddictMongoDbBuilder"/> instance.</returns>
public OpenIddictMongoDbBuilder SetSessionsCollectionName(string name)
{
ArgumentException.ThrowIfNullOrEmpty(name);
return Configure(options => options.SessionsCollectionName = name);
}
/// <summary>
/// Replaces the default tokens collection name (by default, openiddict.tokens).
/// </summary>

2
src/OpenIddict.MongoDb/OpenIddictMongoDbExtensions.cs

@ -34,6 +34,7 @@ public static class OpenIddictMongoDbExtensions
.SetDefaultAuthorizationEntity<OpenIddictMongoDbAuthorization>()
.SetDefaultResourceEntity<OpenIddictMongoDbResource>()
.SetDefaultScopeEntity<OpenIddictMongoDbScope>()
.SetDefaultSessionEntity<OpenIddictMongoDbSession>()
.SetDefaultTokenEntity<OpenIddictMongoDbToken>();
// Note: the Mongo stores don't depend on scoped/transient services and thus can
@ -42,6 +43,7 @@ public static class OpenIddictMongoDbExtensions
.ReplaceAuthorizationStore<OpenIddictMongoDbAuthorization, OpenIddictMongoDbAuthorizationStore>(ServiceLifetime.Singleton)
.ReplaceResourceStore<OpenIddictMongoDbResource, OpenIddictMongoDbResourceStore>(ServiceLifetime.Singleton)
.ReplaceScopeStore<OpenIddictMongoDbScope, OpenIddictMongoDbScopeStore>(ServiceLifetime.Singleton)
.ReplaceSessionStore<OpenIddictMongoDbSession, OpenIddictMongoDbSessionStore>(ServiceLifetime.Singleton)
.ReplaceTokenStore<OpenIddictMongoDbToken, OpenIddictMongoDbTokenStore>(ServiceLifetime.Singleton);
builder.Services.TryAddSingleton<IOpenIddictMongoDbContext, OpenIddictMongoDbContext>();

5
src/OpenIddict.MongoDb/OpenIddictMongoDbOptions.cs

@ -37,6 +37,11 @@ public sealed class OpenIddictMongoDbOptions
/// </summary>
public string ScopesCollectionName { get; set; } = "openiddict.scopes";
/// <summary>
/// Gets or sets the name of the sessions collection (by default, openiddict.sessions).
/// </summary>
public string SessionsCollectionName { get; set; } = "openiddict.sessions";
/// <summary>
/// Gets or sets the name of the tokens collection (by default, openiddict.tokens).
/// </summary>

41
src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbAuthorizationStore.cs

@ -110,43 +110,40 @@ public class OpenIddictMongoDbAuthorizationStore<
/// <inheritdoc/>
public virtual async IAsyncEnumerable<TAuthorization> FindAsync(
string? subject, string? client,
string? status, string? type,
ImmutableArray<string>? scopes, [EnumeratorCancellation] CancellationToken cancellationToken)
(string? Subject, string? ApplicationId, string? Status,
string? Type, ImmutableArray<string>? RequiredScopes) query, [EnumeratorCancellation] CancellationToken cancellationToken)
{
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TAuthorization>(Options.CurrentValue.AuthorizationsCollectionName);
IQueryable<TAuthorization> query = collection.AsQueryable();
IQueryable<TAuthorization> 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<string> 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;
}

499
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;
/// <summary>
/// Provides methods allowing to manage the sessions stored in a database.
/// </summary>
public class OpenIddictMongoDbSessionStore : OpenIddictMongoDbSessionStore<OpenIddictMongoDbSession>
{
public OpenIddictMongoDbSessionStore(
IOpenIddictMongoDbContext context,
IOptionsMonitor<OpenIddictMongoDbOptions> options)
: base(context, options)
{
}
}
/// <summary>
/// Provides methods allowing to manage the sessions stored in a database.
/// </summary>
/// <typeparam name="TSession">The type of the session entity.</typeparam>
public class OpenIddictMongoDbSessionStore<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TSession> : IOpenIddictSessionStore<TSession>
where TSession : OpenIddictMongoDbSession
{
public OpenIddictMongoDbSessionStore(
IOpenIddictMongoDbContext context,
IOptionsMonitor<OpenIddictMongoDbOptions> options)
{
Context = context ?? throw new ArgumentNullException(nameof(context));
Options = options ?? throw new ArgumentNullException(nameof(options));
}
/// <summary>
/// Gets the database context associated with the current store.
/// </summary>
protected IOpenIddictMongoDbContext Context { get; }
/// <summary>
/// Gets the options associated with the current store.
/// </summary>
protected IOptionsMonitor<OpenIddictMongoDbOptions> Options { get; }
/// <inheritdoc/>
public virtual async ValueTask<long> CountAsync(CancellationToken cancellationToken)
{
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TSession>(Options.CurrentValue.SessionsCollectionName);
return await collection.CountDocumentsAsync(FilterDefinition<TSession>.Empty, null, cancellationToken);
}
/// <inheritdoc/>
public virtual async ValueTask<long> CountAsync<TState, TResult>(
Func<IQueryable<TSession>, TState, IQueryable<TResult>> query,
TState state, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(query);
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TSession>(Options.CurrentValue.SessionsCollectionName);
return await query(collection.AsQueryable(), state).LongCountAsync(cancellationToken);
}
/// <inheritdoc/>
public virtual async ValueTask CreateAsync(TSession session, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TSession>(Options.CurrentValue.SessionsCollectionName);
await collection.InsertOneAsync(session, null, cancellationToken);
}
/// <inheritdoc/>
public virtual async ValueTask DeleteAsync(TSession session, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TSession>(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));
}
}
/// <inheritdoc/>
public virtual async IAsyncEnumerable<TSession> FindAsync(
(string? Subject, string? LoginId, string? ApplicationId, string? Status) query,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TSession>(Options.CurrentValue.SessionsCollectionName);
IQueryable<TSession> 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;
}
}
/// <inheritdoc/>
public virtual IAsyncEnumerable<TSession> FindByApplicationIdAsync(string identifier, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(identifier);
return ExecuteAsync(cancellationToken);
async IAsyncEnumerable<TSession> ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TSession>(Options.CurrentValue.SessionsCollectionName);
await foreach (var session in collection.Find(session =>
session.ApplicationId == ObjectId.Parse(identifier)).ToAsyncEnumerable().WithCancellation(cancellationToken))
{
yield return session;
}
}
}
/// <inheritdoc/>
public virtual IAsyncEnumerable<TSession> FindByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(identifier);
return ExecuteAsync(cancellationToken);
async IAsyncEnumerable<TSession> ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TSession>(Options.CurrentValue.SessionsCollectionName);
await foreach (var session in collection.Find(session =>
session.AuthorizationId == ObjectId.Parse(identifier)).ToAsyncEnumerable().WithCancellation(cancellationToken))
{
yield return session;
}
}
}
/// <inheritdoc/>
public virtual async ValueTask<TSession?> FindByIdAsync(string identifier, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(identifier);
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TSession>(Options.CurrentValue.SessionsCollectionName);
return await collection.Find(session => session.Id == ObjectId.Parse(identifier)).FirstOrDefaultAsync(cancellationToken);
}
/// <inheritdoc/>
public virtual IAsyncEnumerable<TSession> FindByLoginIdAsync(string identifier, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(identifier);
return ExecuteAsync(cancellationToken);
async IAsyncEnumerable<TSession> ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TSession>(Options.CurrentValue.SessionsCollectionName);
await foreach (var session in collection.Find(session => session.LoginId == identifier).ToAsyncEnumerable().WithCancellation(cancellationToken))
{
yield return session;
}
}
}
/// <inheritdoc/>
public virtual IAsyncEnumerable<TSession> FindBySubjectAsync(string subject, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(subject);
return ExecuteAsync(cancellationToken);
async IAsyncEnumerable<TSession> ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TSession>(Options.CurrentValue.SessionsCollectionName);
await foreach (var session in collection.Find(session => session.Subject == subject).ToAsyncEnumerable().WithCancellation(cancellationToken))
{
yield return session;
}
}
}
/// <inheritdoc/>
public virtual ValueTask<string?> GetApplicationIdAsync(TSession session, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
return new(session.ApplicationId != ObjectId.Empty ? session.ApplicationId.ToString() : null);
}
/// <inheritdoc/>
public virtual async ValueTask<TResult?> GetAsync<TState, TResult>(
Func<IQueryable<TSession>, TState, IQueryable<TResult>> query,
TState state, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(query);
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TSession>(Options.CurrentValue.SessionsCollectionName);
return await query(collection.AsQueryable(), state).FirstOrDefaultAsync(cancellationToken);
}
/// <inheritdoc/>
public virtual ValueTask<string?> GetAuthorizationIdAsync(TSession session, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
return new(session.AuthorizationId != ObjectId.Empty ? session.AuthorizationId.ToString() : null);
}
/// <inheritdoc/>
public virtual ValueTask<DateTimeOffset?> GetCreationDateAsync(TSession session, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
return new(session.CreationDate is DateTime date ? DateTime.SpecifyKind(date, DateTimeKind.Utc) : null);
}
/// <inheritdoc/>
public virtual ValueTask<string?> GetIdAsync(TSession session, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
return new(session.Id.ToString());
}
/// <inheritdoc/>
public virtual ValueTask<string?> GetLoginIdAsync(TSession session, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
return new(session.LoginId);
}
/// <inheritdoc/>
public virtual ValueTask<ImmutableDictionary<string, JsonElement>> GetPropertiesAsync(TSession session, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
if (session.Properties is null)
{
return new(ImmutableDictionary.Create<string, JsonElement>());
}
using var document = JsonDocument.Parse(session.Properties.ToJson());
var builder = ImmutableDictionary.CreateBuilder<string, JsonElement>();
foreach (var property in document.RootElement.EnumerateObject())
{
builder[property.Name] = property.Value.Clone();
}
return new(builder.ToImmutable());
}
/// <inheritdoc/>
public virtual ValueTask<string?> GetStatusAsync(TSession session, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
return new(session.Status);
}
/// <inheritdoc/>
public virtual ValueTask<string?> GetSubjectAsync(TSession session, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
return new(session.Subject);
}
/// <inheritdoc/>
public virtual ValueTask<TSession> InstantiateAsync(CancellationToken cancellationToken)
{
try
{
return new(Activator.CreateInstance<TSession>());
}
catch (MemberAccessException exception)
{
return new(Task.FromException<TSession>(
new InvalidOperationException(SR.GetResourceString(SR.ID0240), exception)));
}
}
/// <inheritdoc/>
public virtual async IAsyncEnumerable<TSession> ListAsync(
int? count, int? offset, [EnumeratorCancellation] CancellationToken cancellationToken)
{
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TSession>(Options.CurrentValue.SessionsCollectionName);
var query = (IQueryable<TSession>) 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<TSession>) query).ToAsyncEnumerable().WithCancellation(cancellationToken))
{
yield return session;
}
}
/// <inheritdoc/>
public virtual IAsyncEnumerable<TResult> ListAsync<TState, TResult>(
Func<IQueryable<TSession>, TState, IQueryable<TResult>> query,
TState state, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(query);
return ExecuteAsync(cancellationToken);
async IAsyncEnumerable<TResult> ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TSession>(Options.CurrentValue.SessionsCollectionName);
await foreach (var element in query(collection.AsQueryable(), state).ToAsyncEnumerable().WithCancellation(cancellationToken))
{
yield return element;
}
}
}
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
public virtual ValueTask SetCreationDateAsync(TSession session, DateTimeOffset? date, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
session.CreationDate = date?.UtcDateTime;
return ValueTask.CompletedTask;
}
/// <inheritdoc/>
public virtual ValueTask SetPropertiesAsync(TSession session,
ImmutableDictionary<string, JsonElement> 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;
}
/// <inheritdoc/>
public virtual ValueTask SetLoginIdAsync(TSession session, string? identifier, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
session.LoginId = identifier;
return ValueTask.CompletedTask;
}
/// <inheritdoc/>
public virtual ValueTask SetStatusAsync(TSession session, string? status, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
session.Status = status;
return ValueTask.CompletedTask;
}
/// <inheritdoc/>
public virtual ValueTask SetSubjectAsync(TSession session, string? subject, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
session.Subject = subject;
return ValueTask.CompletedTask;
}
/// <inheritdoc/>
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<TSession>(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));
}
}
}

44
src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs

@ -106,35 +106,35 @@ public class OpenIddictMongoDbTokenStore<
/// <inheritdoc/>
public virtual async IAsyncEnumerable<TToken> 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<TToken>(Options.CurrentValue.TokensCollectionName);
IQueryable<TToken> query = collection.AsQueryable();
IQueryable<TToken> 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;
}

1
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";

5
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<string, string> 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));

2
src/OpenIddict.Server/IOpenIddictServerHandlerFilter.cs

@ -19,7 +19,7 @@ public interface IOpenIddictServerHandlerFilter<in TContext> where TContext : Ba
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose boolean result determines whether the handler will be invoked or not.
/// </returns>
ValueTask<bool> IsActiveAsync(TContext context);

17
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;
}
}

1
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";

1
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<string, string> properties) Read(BinaryReader reader)

2
src/OpenIddict.Validation/IOpenIddictValidationHandlerFilter.cs

@ -18,7 +18,7 @@ public interface IOpenIddictValidationHandlerFilter<in TContext> where TContext
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose boolean result determines whether the handler will be invoked or not.
/// </returns>
ValueTask<bool> IsActiveAsync(TContext context);

148
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<ArgumentNullException>(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<ArgumentNullException>(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<ArgumentNullException>(() => 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<ArgumentNullException>(() => 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()
{

549
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<OpenIddictCoreOptions>) null!;
var store = Mock.Of<IOpenIddictSessionStore<object>>();
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => new OpenIddictSessionCache<object>(options, store));
Assert.Equal("options", exception.ParamName);
}
[Fact]
public void Constructor_ThrowsAnExceptionForNullStore()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = (IOpenIddictSessionStore<object>) null!;
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => new OpenIddictSessionCache<object>(options, store));
Assert.Equal("store", exception.ParamName);
}
[Fact]
public async Task AddAsync_ThrowsAnExceptionForNullsession()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictSessionStore<OpenIddictSession>>();
var cache = new OpenIddictSessionCache<OpenIddictSession>(options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => cache.AddAsync(session: null!, CancellationToken.None).AsTask());
Assert.Equal("session", exception.ParamName);
}
[Fact]
public void Dispose_CanBeCalledMultipleTimes()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictSessionStore<OpenIddictSession>>();
var cache = new OpenIddictSessionCache<OpenIddictSession>(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<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictSessionStore<OpenIddictSession>>();
store.Setup(store => store.GetIdAsync(sessions[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("session-id-1");
store.Setup(store => store.GetApplicationIdAsync(sessions[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("application-id");
store.Setup(store => store.GetAuthorizationIdAsync(sessions[0], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetSubjectAsync(sessions[0], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetIdAsync(sessions[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("session-id-2");
store.Setup(store => store.GetApplicationIdAsync(sessions[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("application-id");
store.Setup(store => store.GetAuthorizationIdAsync(sessions[1], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetSubjectAsync(sessions[1], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.FindByApplicationIdAsync("application-id", It.IsAny<CancellationToken>()))
.Returns(sessions.ToAsyncEnumerable());
var cache = new OpenIddictSessionCache<OpenIddictSession>(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<CancellationToken>()), Times.Once());
}
[Fact]
public async Task FindByApplicationIdAsync_ThrowsAnExceptionForNullIdentifier()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictSessionStore<OpenIddictSession>>();
var cache = new OpenIddictSessionCache<OpenIddictSession>(options, store);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(
() => cache.FindByApplicationIdAsync(identifier: null!, CancellationToken.None));
Assert.Equal("identifier", exception.ParamName);
}
[Fact]
public async Task FindByApplicationIdAsync_ThrowsAnExceptionForEmptyIdentifier()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictSessionStore<OpenIddictSession>>();
var cache = new OpenIddictSessionCache<OpenIddictSession>(options, store);
// Act and assert
var exception = Assert.Throws<ArgumentException>(
() => 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<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictSessionStore<OpenIddictSession>>();
store.Setup(store => store.GetIdAsync(sessions[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("session-id-1");
store.Setup(store => store.GetApplicationIdAsync(sessions[0], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetAuthorizationIdAsync(sessions[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("authorization-id");
store.Setup(store => store.GetSubjectAsync(sessions[0], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetIdAsync(sessions[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("session-id-2");
store.Setup(store => store.GetApplicationIdAsync(sessions[1], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetAuthorizationIdAsync(sessions[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("authorization-id");
store.Setup(store => store.GetSubjectAsync(sessions[1], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.FindByAuthorizationIdAsync("authorization-id", It.IsAny<CancellationToken>()))
.Returns(sessions.ToAsyncEnumerable());
var cache = new OpenIddictSessionCache<OpenIddictSession>(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<CancellationToken>()), Times.Once());
}
[Fact]
public async Task FindByAuthorizationIdAsync_ThrowsAnExceptionForNullIdentifier()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictSessionStore<OpenIddictSession>>();
var cache = new OpenIddictSessionCache<OpenIddictSession>(options, store);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(
() => cache.FindByAuthorizationIdAsync(identifier: null!, CancellationToken.None));
Assert.Equal("identifier", exception.ParamName);
}
[Fact]
public async Task FindByAuthorizationIdAsync_ThrowsAnExceptionForEmptyIdentifier()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictSessionStore<OpenIddictSession>>();
var cache = new OpenIddictSessionCache<OpenIddictSession>(options, store);
// Act and assert
var exception = Assert.Throws<ArgumentException>(
() => cache.FindByAuthorizationIdAsync(identifier: string.Empty, CancellationToken.None));
Assert.Equal("identifier", exception.ParamName);
}
[Fact]
public async Task FindByIdAsync_ThrowsAnExceptionForNullIdentifier()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictSessionStore<OpenIddictSession>>();
var cache = new OpenIddictSessionCache<OpenIddictSession>(options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => cache.FindByIdAsync(identifier: null!, CancellationToken.None).AsTask());
Assert.Equal("identifier", exception.ParamName);
}
[Fact]
public async Task FindByIdAsync_ThrowsAnExceptionForEmptyIdentifier()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictSessionStore<OpenIddictSession>>();
var cache = new OpenIddictSessionCache<OpenIddictSession>(options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentException>(
() => 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<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictSessionStore<OpenIddictSession>>();
store.Setup(store => store.GetIdAsync(session, It.IsAny<CancellationToken>()))
.ReturnsAsync("session-id");
var cache = new OpenIddictSessionCache<OpenIddictSession>(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<CancellationToken>()), Times.Never());
}
[Fact]
public async Task FindByIdAsync_QueriesStoreOnCacheMiss()
{
// Arrange
var session = new OpenIddictSession();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictSessionStore<OpenIddictSession>>();
store.Setup(store => store.FindByIdAsync("session-id", It.IsAny<CancellationToken>()))
.ReturnsAsync(session);
store.Setup(store => store.GetIdAsync(session, It.IsAny<CancellationToken>()))
.ReturnsAsync("session-id");
var cache = new OpenIddictSessionCache<OpenIddictSession>(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<CancellationToken>()), Times.Once());
}
[Fact]
public async Task FindByIdAsync_ReturnsNullWhensessionNotFound()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictSessionStore<OpenIddictSession>>();
store.Setup(store => store.FindByIdAsync("session-id", It.IsAny<CancellationToken>()))
.ReturnsAsync((OpenIddictSession?) null);
var cache = new OpenIddictSessionCache<OpenIddictSession>(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<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictSessionStore<OpenIddictSession>>();
store.Setup(store => store.GetIdAsync(sessions[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("session-id-1");
store.Setup(store => store.GetApplicationIdAsync(sessions[0], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetAuthorizationIdAsync(sessions[0], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetLoginIdAsync(sessions[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("login-id");
store.Setup(store => store.GetSubjectAsync(sessions[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("subject");
store.Setup(store => store.GetIdAsync(sessions[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("session-id-2");
store.Setup(store => store.GetApplicationIdAsync(sessions[1], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetAuthorizationIdAsync(sessions[1], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetLoginIdAsync(sessions[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("login-id");
store.Setup(store => store.GetSubjectAsync(sessions[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("subject");
store.Setup(store => store.FindByLoginIdAsync("login-id", It.IsAny<CancellationToken>()))
.Returns(sessions.ToAsyncEnumerable());
var cache = new OpenIddictSessionCache<OpenIddictSession>(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<CancellationToken>()), Times.Once());
}
[Fact]
public async Task FindByLoginIdAsync_ThrowsAnExceptionForNullIdentifier()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictSessionStore<OpenIddictSession>>();
var cache = new OpenIddictSessionCache<OpenIddictSession>(options, store);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(
() => cache.FindByLoginIdAsync(identifier: null!, CancellationToken.None));
Assert.Equal("identifier", exception.ParamName);
}
[Fact]
public async Task FindByLoginIdAsync_ThrowsAnExceptionForEmptyIdentifier()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictSessionStore<OpenIddictSession>>();
var cache = new OpenIddictSessionCache<OpenIddictSession>(options, store);
// Act and assert
var exception = Assert.Throws<ArgumentException>(
() => 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<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictSessionStore<OpenIddictSession>>();
store.Setup(store => store.GetIdAsync(sessions[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("session-id-1");
store.Setup(store => store.GetApplicationIdAsync(sessions[0], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetAuthorizationIdAsync(sessions[0], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetSubjectAsync(sessions[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("subject");
store.Setup(store => store.GetIdAsync(sessions[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("session-id-2");
store.Setup(store => store.GetApplicationIdAsync(sessions[1], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetAuthorizationIdAsync(sessions[1], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetSubjectAsync(sessions[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("subject");
store.Setup(store => store.FindBySubjectAsync("subject", It.IsAny<CancellationToken>()))
.Returns(sessions.ToAsyncEnumerable());
var cache = new OpenIddictSessionCache<OpenIddictSession>(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<CancellationToken>()), Times.Once());
}
[Fact]
public async Task FindBySubjectAsync_ThrowsAnExceptionForNullSubject()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictSessionStore<OpenIddictSession>>();
var cache = new OpenIddictSessionCache<OpenIddictSession>(options, store);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(
() => cache.FindBySubjectAsync(subject: null!, CancellationToken.None));
Assert.Equal("subject", exception.ParamName);
}
[Fact]
public async Task FindBySubjectAsync_ThrowsAnExceptionForEmptySubject()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictSessionStore<OpenIddictSession>>();
var cache = new OpenIddictSessionCache<OpenIddictSession>(options, store);
// Act and assert
var exception = Assert.Throws<ArgumentException>(
() => cache.FindBySubjectAsync(subject: string.Empty, CancellationToken.None));
Assert.Equal("subject", exception.ParamName);
}
[Fact]
public async Task RemoveAsync_ThrowsAnExceptionForNullsession()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictSessionStore<OpenIddictSession>>();
var cache = new OpenIddictSessionCache<OpenIddictSession>(options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => 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<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictSessionStore<OpenIddictSession>>();
store.Setup(store => store.GetIdAsync(session, It.IsAny<CancellationToken>()))
.ReturnsAsync("session-id");
var cache = new OpenIddictSessionCache<OpenIddictSession>(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<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictSessionStore<OpenIddictSession>>();
store.Setup(store => store.GetIdAsync(session, It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
var cache = new OpenIddictSessionCache<OpenIddictSession>(options, store.Object);
// Act and assert
await Assert.ThrowsAsync<InvalidOperationException>(
() => cache.RemoveAsync(session, CancellationToken.None).AsTask());
}
public sealed class OpenIddictSession;
}

20
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<IOpenIddictAuthorizationStore<CustomAuthorization>>();
store.Setup(store => store.FindAsync("alice", null, null, null, null, It.IsAny<CancellationToken>()))
store.Setup(store => store.FindAsync(It.Is<(string?, string?, string?, string?, ImmutableArray<string>?)>(query => query.Item1 == "alice"), It.IsAny<CancellationToken>()))
.Returns(authorizations.ToAsyncEnumerable());
store.Setup(store => store.GetSubjectAsync(authorizations[0], It.IsAny<CancellationToken>()))
@ -223,7 +223,7 @@ public class OpenIddictAuthorizationManagerTests
var manager = new OpenIddictAuthorizationManager<CustomAuthorization>(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<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions { DisableEntityCaching = true });
var store = new Mock<IOpenIddictAuthorizationStore<CustomAuthorization>>();
var scopes = ImmutableArray.Create("openid");
store.Setup(store => store.FindAsync(null, null, null, null, ImmutableArray.Create("openid"), It.IsAny<CancellationToken>()))
store.Setup(store => store.FindAsync(It.Is<(string?, string?, string?, string?, ImmutableArray<string>?)>(query => query.Item5 == scopes), It.IsAny<CancellationToken>()))
.Returns(authorizations.ToAsyncEnumerable());
store.Setup(store => store.GetSubjectAsync(It.IsAny<CustomAuthorization>(), It.IsAny<CancellationToken>()))
@ -256,7 +257,7 @@ public class OpenIddictAuthorizationManagerTests
var manager = new OpenIddictAuthorizationManager<CustomAuthorization>(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<IOpenIddictAuthorizationStore<CustomAuthorization>>();
store.Setup(store => store.FindAsync(
It.IsAny<string?>(),
It.IsAny<string?>(),
It.IsAny<string?>(),
It.IsAny<string?>(),
It.IsAny<ImmutableArray<string>?>(),
It.IsAny<(string?, string?, string?, string?, ImmutableArray<string>?)>(),
It.IsAny<CancellationToken>()))
.Returns((string? subject, string? client, string? status, string? type, ImmutableArray<string>? scopes, CancellationToken cancellationToken) =>
{
return Enumerable.Empty<CustomAuthorization>().ToAsyncEnumerable();
});
.Returns(Enumerable.Empty<CustomAuthorization>().ToAsyncEnumerable());
var manager = new OpenIddictAuthorizationManager<CustomAuthorization>(cache, logger, options, store.Object);

861
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<CustomSession>) null!;
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictSessionStore<CustomSession>>();
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(
() => new OpenIddictSessionManager<CustomSession>(cache, logger, options, store));
Assert.Equal("cache", exception.ParamName);
}
[Fact]
public void Constructor_ThrowsAnExceptionForNullLogger()
{
// Arrange
var cache = Mock.Of<IOpenIddictSessionCache<CustomSession>>();
var logger = (ILogger<OpenIddictSessionManager<CustomSession>>) null!;
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictSessionStore<CustomSession>>();
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(
() => new OpenIddictSessionManager<CustomSession>(cache, logger, options, store));
Assert.Equal("logger", exception.ParamName);
}
[Fact]
public void Constructor_ThrowsAnExceptionForNullOptions()
{
// Arrange
var cache = Mock.Of<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = (IOptionsMonitor<OpenIddictCoreOptions>) null!;
var store = Mock.Of<IOpenIddictSessionStore<CustomSession>>();
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(
() => new OpenIddictSessionManager<CustomSession>(cache, logger, options, store));
Assert.Equal("options", exception.ParamName);
}
[Fact]
public void Constructor_ThrowsAnExceptionForNullStore()
{
// Arrange
var cache = Mock.Of<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = (IOpenIddictSessionStore<CustomSession>) null!;
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(
() => new OpenIddictSessionManager<CustomSession>(cache, logger, options, store));
Assert.Equal("store", exception.ParamName);
}
[Fact]
public async Task CountAsync_CallsStoreMethod()
{
// Arrange
var cache = Mock.Of<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictSessionStore<CustomSession>>();
store.Setup(store => store.CountAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(42);
var manager = new OpenIddictSessionManager<CustomSession>(cache, logger, options, store.Object);
// Act
var count = await manager.CountAsync();
// Assert
Assert.Equal(42, count);
store.Verify(store => store.CountAsync(It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task CountAsync_WithQuery_ThrowsAnExceptionForNullQuery()
{
// Arrange
var cache = Mock.Of<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictSessionStore<CustomSession>>();
var manager = new OpenIddictSessionManager<CustomSession>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => manager.CountAsync<CustomSession>(query: null!).AsTask());
Assert.Equal("query", exception.ParamName);
}
[Fact]
public async Task CreateAsync_ThrowsAnExceptionForNullSession()
{
// Arrange
var cache = Mock.Of<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictSessionStore<CustomSession>>();
var manager = new OpenIddictSessionManager<CustomSession>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => manager.CreateAsync(session: null!).AsTask());
Assert.Equal("session", exception.ParamName);
}
[Fact]
public async Task DeleteAsync_ThrowsAnExceptionForNullSession()
{
// Arrange
var cache = Mock.Of<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictSessionStore<CustomSession>>();
var manager = new OpenIddictSessionManager<CustomSession>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => manager.DeleteAsync(session: null!).AsTask());
Assert.Equal("session", exception.ParamName);
}
[Fact]
public async Task DeleteAsync_RemovessessionFromCache_WhenCachingIsEnabled()
{
// Arrange
var cache = new Mock<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions { DisableEntityCaching = false });
var store = new Mock<IOpenIddictSessionStore<CustomSession>>();
var session = new CustomSession();
var manager = new OpenIddictSessionManager<CustomSession>(cache.Object, logger, options, store.Object);
// Act
await manager.DeleteAsync(session);
// Assert
cache.Verify(cache => cache.RemoveAsync(session, It.IsAny<CancellationToken>()), Times.Once());
store.Verify(store => store.DeleteAsync(session, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task DeleteAsync_DoesNotRemoveFromCache_WhenCachingIsDisabled()
{
// Arrange
var cache = new Mock<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions { DisableEntityCaching = true });
var store = new Mock<IOpenIddictSessionStore<CustomSession>>();
var session = new CustomSession();
var manager = new OpenIddictSessionManager<CustomSession>(cache.Object, logger, options, store.Object);
// Act
await manager.DeleteAsync(session);
// Assert
cache.Verify(cache => cache.RemoveAsync(It.IsAny<CustomSession>(), It.IsAny<CancellationToken>()), Times.Never());
store.Verify(store => store.DeleteAsync(session, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task FindByIdAsync_ThrowsAnExceptionForNullIdentifier()
{
// Arrange
var cache = Mock.Of<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictSessionStore<CustomSession>>();
var manager = new OpenIddictSessionManager<CustomSession>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => manager.FindByIdAsync(identifier: null!).AsTask());
Assert.Equal("identifier", exception.ParamName);
}
[Fact]
public async Task FindByIdAsync_ThrowsAnExceptionForEmptyIdentifier()
{
// Arrange
var cache = Mock.Of<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictSessionStore<CustomSession>>();
var manager = new OpenIddictSessionManager<CustomSession>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentException>(
() => 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<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions { DisableEntityCaching = false });
var store = new Mock<IOpenIddictSessionStore<CustomSession>>();
cache.Setup(cache => cache.FindByIdAsync("id", It.IsAny<CancellationToken>()))
.ReturnsAsync(session);
store.Setup(store => store.GetIdAsync(session, It.IsAny<CancellationToken>()))
.ReturnsAsync("id");
var manager = new OpenIddictSessionManager<CustomSession>(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<CancellationToken>()), Times.Once());
store.Verify(store => store.FindByIdAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never());
}
[Fact]
public async Task FindByIdAsync_UsesStore_WhenCachingIsDisabled()
{
// Arrange
var session = new CustomSession();
var cache = new Mock<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions { DisableEntityCaching = true });
var store = new Mock<IOpenIddictSessionStore<CustomSession>>();
store.Setup(store => store.FindByIdAsync("id", It.IsAny<CancellationToken>()))
.ReturnsAsync(session);
store.Setup(store => store.GetIdAsync(session, It.IsAny<CancellationToken>()))
.ReturnsAsync("id");
var manager = new OpenIddictSessionManager<CustomSession>(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<string>(), It.IsAny<CancellationToken>()), Times.Never());
store.Verify(store => store.FindByIdAsync("id", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task GetAsync_WithQuery_ThrowsAnExceptionForNullQuery()
{
// Arrange
var cache = Mock.Of<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictSessionStore<CustomSession>>();
var manager = new OpenIddictSessionManager<CustomSession>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => manager.GetAsync<CustomSession>(query: null!).AsTask());
Assert.Equal("query", exception.ParamName);
}
[Fact]
public async Task GetAsync_WithQueryAndState_ThrowsAnExceptionForNullQuery()
{
// Arrange
var cache = Mock.Of<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictSessionStore<CustomSession>>();
var manager = new OpenIddictSessionManager<CustomSession>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => manager.GetAsync<object, CustomSession>(query: null!, state: null!).AsTask());
Assert.Equal("query", exception.ParamName);
}
[Fact]
public async Task GetIdAsync_ThrowsAnExceptionForNullSession()
{
// Arrange
var cache = Mock.Of<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictSessionStore<CustomSession>>();
var manager = new OpenIddictSessionManager<CustomSession>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => 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<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = new Mock<IOpenIddictSessionStore<CustomSession>>();
store.Setup(store => store.GetIdAsync(session, It.IsAny<CancellationToken>()))
.ReturnsAsync("unique-session-id");
var manager = new OpenIddictSessionManager<CustomSession>(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<CancellationToken>()), Times.Once());
}
[Fact]
public async Task GetApplicationIdAsync_ThrowsAnExceptionForNullSession()
{
// Arrange
var cache = Mock.Of<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictSessionStore<CustomSession>>();
var manager = new OpenIddictSessionManager<CustomSession>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => 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<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = new Mock<IOpenIddictSessionStore<CustomSession>>();
store.Setup(store => store.GetApplicationIdAsync(session, It.IsAny<CancellationToken>()))
.ReturnsAsync("application-id");
var manager = new OpenIddictSessionManager<CustomSession>(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<CancellationToken>()), Times.Once());
}
[Fact]
public async Task GetAuthorizationIdAsync_ThrowsAnExceptionForNullSession()
{
// Arrange
var cache = Mock.Of<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictSessionStore<CustomSession>>();
var manager = new OpenIddictSessionManager<CustomSession>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => 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<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = new Mock<IOpenIddictSessionStore<CustomSession>>();
store.Setup(store => store.GetAuthorizationIdAsync(session, It.IsAny<CancellationToken>()))
.ReturnsAsync("authorization-id");
var manager = new OpenIddictSessionManager<CustomSession>(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<CancellationToken>()), Times.Once());
}
[Fact]
public async Task GetCreationDateAsync_ThrowsAnExceptionForNullSession()
{
// Arrange
var cache = Mock.Of<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictSessionStore<CustomSession>>();
var manager = new OpenIddictSessionManager<CustomSession>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => 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<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = new Mock<IOpenIddictSessionStore<CustomSession>>();
store.Setup(store => store.GetCreationDateAsync(session, It.IsAny<CancellationToken>()))
.ReturnsAsync(creationDate);
var manager = new OpenIddictSessionManager<CustomSession>(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<CancellationToken>()), Times.Once());
}
[Fact]
public async Task GetLoginIdAsync_ThrowsAnExceptionForNullSession()
{
// Arrange
var cache = Mock.Of<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictSessionStore<CustomSession>>();
var manager = new OpenIddictSessionManager<CustomSession>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => 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<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = new Mock<IOpenIddictSessionStore<CustomSession>>();
store.Setup(store => store.GetLoginIdAsync(session, It.IsAny<CancellationToken>()))
.ReturnsAsync("login-id");
var manager = new OpenIddictSessionManager<CustomSession>(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<CancellationToken>()), Times.Once());
}
[Fact]
public async Task GetPropertiesAsync_ThrowsAnExceptionForNullSession()
{
// Arrange
var cache = Mock.Of<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictSessionStore<CustomSession>>();
var manager = new OpenIddictSessionManager<CustomSession>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => manager.GetPropertiesAsync(session: null!).AsTask());
Assert.Equal("session", exception.ParamName);
}
[Fact]
public async Task GetPropertiesAsync_ReturnsPropertiesFromStore()
{
// Arrange
var session = new CustomSession();
var properties = ImmutableDictionary<string, JsonElement>.Empty;
var cache = Mock.Of<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = new Mock<IOpenIddictSessionStore<CustomSession>>();
store.Setup(store => store.GetPropertiesAsync(session, It.IsAny<CancellationToken>()))
.ReturnsAsync(properties);
var manager = new OpenIddictSessionManager<CustomSession>(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<CancellationToken>()), Times.Once());
}
[Fact]
public async Task GetStatusAsync_ThrowsAnExceptionForNullSession()
{
// Arrange
var cache = Mock.Of<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictSessionStore<CustomSession>>();
var manager = new OpenIddictSessionManager<CustomSession>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => 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<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = new Mock<IOpenIddictSessionStore<CustomSession>>();
store.Setup(store => store.GetStatusAsync(session, It.IsAny<CancellationToken>()))
.ReturnsAsync(Statuses.Valid);
var manager = new OpenIddictSessionManager<CustomSession>(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<CancellationToken>()), Times.Once());
}
[Fact]
public async Task GetSubjectAsync_ThrowsAnExceptionForNullSession()
{
// Arrange
var cache = Mock.Of<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictSessionStore<CustomSession>>();
var manager = new OpenIddictSessionManager<CustomSession>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => 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<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = new Mock<IOpenIddictSessionStore<CustomSession>>();
store.Setup(store => store.GetSubjectAsync(session, It.IsAny<CancellationToken>()))
.ReturnsAsync("subject");
var manager = new OpenIddictSessionManager<CustomSession>(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<CancellationToken>()), Times.Once());
}
[Fact]
public async Task ListAsync_ReturnsAllSessions()
{
// Arrange
var sessions = new[] { new CustomSession(), new CustomSession() };
var cache = Mock.Of<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictSessionStore<CustomSession>>();
store.Setup(store => store.ListAsync(It.IsAny<int?>(), It.IsAny<int?>(), It.IsAny<CancellationToken>()))
.Returns(sessions.ToAsyncEnumerable());
var manager = new OpenIddictSessionManager<CustomSession>(cache, logger, options, store.Object);
// Act
var results = new List<CustomSession>();
await foreach (var scp in manager.ListAsync())
{
results.Add(scp);
}
// Assert
Assert.Equal(2, results.Count);
store.Verify(store => store.ListAsync(It.IsAny<int?>(), It.IsAny<int?>(), It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task PopulateAsync_ThrowsAnExceptionForNullSession()
{
// Arrange
var cache = Mock.Of<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictSessionStore<CustomSession>>();
var manager = new OpenIddictSessionManager<CustomSession>(cache, logger, options, store);
var descriptor = new OpenIddictSessionDescriptor();
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => 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<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictSessionStore<CustomSession>>();
var manager = new OpenIddictSessionManager<CustomSession>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => manager.PopulateAsync(session, descriptor: null!).AsTask());
Assert.Equal("descriptor", exception.ParamName);
}
[Fact]
public async Task UpdateAsync_ThrowsAnExceptionForNullSession()
{
// Arrange
var cache = Mock.Of<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictSessionStore<CustomSession>>();
var manager = new OpenIddictSessionManager<CustomSession>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => manager.UpdateAsync(session: null!).AsTask());
Assert.Equal("session", exception.ParamName);
}
[Fact]
public async Task UpdateAsync_WithDescriptor_ThrowsAnExceptionForNullSession()
{
// Arrange
var cache = Mock.Of<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictSessionStore<CustomSession>>();
var manager = new OpenIddictSessionManager<CustomSession>(cache, logger, options, store);
var descriptor = new OpenIddictSessionDescriptor();
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => manager.UpdateAsync(session: null!, descriptor).AsTask());
Assert.Equal("session", exception.ParamName);
}
[Fact]
public void ValidateAsync_ThrowsAnExceptionForNullSession()
{
// Arrange
var cache = Mock.Of<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictSessionStore<CustomSession>>();
var manager = new OpenIddictSessionManager<CustomSession>(cache, logger, options, store);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(
() => manager.ValidateAsync(session: null!));
Assert.Equal("session", exception.ParamName);
}
[Fact]
public async Task ValidateAsync_ReturnsErrorWhenStatusIsEmpty()
{
// Arrange
var session = new CustomSession();
var cache = Mock.Of<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictSessionStore<CustomSession>>();
store.Setup(store => store.GetStatusAsync(session, It.IsAny<CancellationToken>()))
.ReturnsAsync(string.Empty);
var manager = new OpenIddictSessionManager<CustomSession>(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<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictSessionStore<CustomSession>>();
store.Setup(store => store.GetStatusAsync(session, It.IsAny<CancellationToken>()))
.ReturnsAsync(Statuses.Valid);
store.Setup(store => store.GetLoginIdAsync(session, It.IsAny<CancellationToken>()))
.ReturnsAsync(string.Empty);
var manager = new OpenIddictSessionManager<CustomSession>(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<IOpenIddictSessionCache<CustomSession>>();
var logger = Mock.Of<ILogger<OpenIddictSessionManager<CustomSession>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictSessionStore<CustomSession>>();
store.Setup(store => store.GetStatusAsync(session, It.IsAny<CancellationToken>()))
.ReturnsAsync(Statuses.Valid);
store.Setup(store => store.GetLoginIdAsync(session, It.IsAny<CancellationToken>()))
.ReturnsAsync("login-id");
var manager = new OpenIddictSessionManager<CustomSession>(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;
}

88
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<ArgumentException>(() => 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<ArgumentException>(() => 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<CustomSession>();
// 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<CustomApplication>
@ -684,6 +747,31 @@ private static OpenIddictCoreBuilder CreateBuilder(IServiceCollection services)
}
}
private class ClosedGenericSessionManager : OpenIddictSessionManager<CustomSession>
{
public ClosedGenericSessionManager(
IOpenIddictSessionCache<CustomSession> cache,
ILogger<OpenIddictSessionManager<CustomSession>> logger,
IOptionsMonitor<OpenIddictCoreOptions> options,
IOpenIddictSessionStore<CustomSession> store)
: base(cache, logger, options, store)
{
}
}
private class OpenGenericSessionManager<TSession> : OpenIddictSessionManager<TSession>
where TSession : class
{
public OpenGenericSessionManager(
IOpenIddictSessionCache<TSession> cache,
ILogger<OpenIddictSessionManager<TSession>> logger,
IOptionsMonitor<OpenIddictCoreOptions> options,
IOpenIddictSessionStore<TSession> store)
: base(cache, logger, options, store)
{
}
}
private class ClosedGenericTokenManager : OpenIddictTokenManager<CustomToken>
{
public ClosedGenericTokenManager(

20
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<InvalidOperationException>(provider.GetRequiredService<IOpenIddictSessionManager>);
Assert.Equal(SR.GetResourceString(SR.ID0472), exception.Message);
}
[Fact]
public void AddCore_ResolvingUntypedTokenManagerThrowsAnException()
{

7
test/OpenIddict.EntityFramework.Tests/OpenIddictEntityFrameworkBuilderTests.cs

@ -33,7 +33,7 @@ public class OpenIddictEntityFrameworkBuilderTests
var builder = CreateBuilder(services);
// Act
builder.ReplaceDefaultEntities<CustomApplication, CustomAuthorization, CustomResource, CustomScope, CustomToken, long>();
builder.ReplaceDefaultEntities<CustomApplication, CustomAuthorization, CustomResource, CustomScope, CustomSession, CustomToken, long>();
// Assert
Assert.Contains(services, service =>
@ -52,6 +52,10 @@ public class OpenIddictEntityFrameworkBuilderTests
service.Lifetime == ServiceLifetime.Scoped &&
service.ServiceType == typeof(IOpenIddictScopeStore<CustomScope>) &&
service.ImplementationType == typeof(OpenIddictEntityFrameworkScopeStore<CustomScope, long>));
Assert.Contains(services, service =>
service.Lifetime == ServiceLifetime.Scoped &&
service.ServiceType == typeof(IOpenIddictSessionStore<CustomSession>) &&
service.ImplementationType == typeof(OpenIddictEntityFrameworkSessionStore<CustomSession, CustomApplication, CustomAuthorization, CustomToken, long>));
Assert.Contains(services, service =>
service.Lifetime == ServiceLifetime.Scoped &&
service.ServiceType == typeof(IOpenIddictTokenStore<CustomToken>) &&
@ -90,6 +94,7 @@ public class OpenIddictEntityFrameworkBuilderTests
public class CustomAuthorization : OpenIddictEntityFrameworkAuthorization<long, CustomApplication, CustomToken>;
public class CustomResource : OpenIddictEntityFrameworkResource<long>;
public class CustomScope : OpenIddictEntityFrameworkScope<long>;
public class CustomSession : OpenIddictEntityFrameworkSession<long, CustomApplication, CustomAuthorization>;
public class CustomToken : OpenIddictEntityFrameworkToken<long, CustomApplication, CustomAuthorization>;
public class CustomDbContext : DbContext

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save