diff --git a/UltimateAuth.slnx b/UltimateAuth.slnx index 7423241a..c0568374 100644 --- a/UltimateAuth.slnx +++ b/UltimateAuth.slnx @@ -1,4 +1,8 @@ + + + + @@ -12,14 +16,16 @@ + + - + @@ -28,6 +34,7 @@ + diff --git a/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/CodeBeam.UltimateAuth.EntityFrameworkCore.csproj b/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/CodeBeam.UltimateAuth.EntityFrameworkCore.csproj new file mode 100644 index 00000000..45529114 --- /dev/null +++ b/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/CodeBeam.UltimateAuth.EntityFrameworkCore.csproj @@ -0,0 +1,24 @@ + + + + net8.0;net9.0;net10.0 + enable + enable + 0.1.0-preview.1 + 0.1.0-preview.1 + true + + + + + + + + + + + + + + + diff --git a/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/UAuthEfCoreOptions.cs b/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/UAuthEfCoreOptions.cs new file mode 100644 index 00000000..464cf4a5 --- /dev/null +++ b/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/UAuthEfCoreOptions.cs @@ -0,0 +1,83 @@ +using Microsoft.EntityFrameworkCore; + +namespace CodeBeam.UltimateAuth.EntityFrameworkCore +{ + /// + /// Provides configuration options for setting up Entity Framework Core database contexts used by the UltimateAuth + /// system. + /// + /// Use this class to specify delegates that configure the options for various DbContext + /// instances, such as Users, Credentials, Authorization, Sessions, Tokens, and Authentication. Each property allows + /// customization of the corresponding context's setup, including database provider and connection details. If a + /// specific configuration delegate is not set for a context, the default configuration is applied. This class is + /// typically configured during application startup to ensure consistent and flexible database context + /// initialization. + public sealed class UAuthEfCoreOptions + { + /// + /// Gets or sets the default action to configure the database context options builder. + /// + /// Use this property to specify a delegate that applies default configuration to a + /// DbContextOptionsBuilder instance. This action is typically invoked when setting up a new database context to + /// ensure consistent configuration across the application. + public Action? Default { get; set; } + + + /// + /// Gets or sets the delegate used to configure options for the Users DbContext. + /// If not set, default option will implement. + /// + /// Assign a delegate to customize the configuration of the Users DbContext, such as + /// specifying the database provider or connection string. This property is typically used during application + /// startup to control how the Users context is set up. + public Action? Users { get; set; } + + /// + /// Gets or sets the delegate used to configure options for the Credentials DbContext. + /// If not set, default option will implement. + /// + /// Assign a delegate to customize the configuration of the Credentials DbContext, such as + /// specifying the database provider or connection string. This property is typically used during application + /// startup to control how the Credentials context is set up. + public Action? Credentials { get; set; } + + /// + /// Gets or sets the delegate used to configure options for the Authorization DbContext. + /// If not set, default option will implement. + /// + /// Assign a delegate to customize the configuration of the Authorization DbContext, such as + /// specifying the database provider or connection string. This property is typically used during application + /// startup to control how the Authorization context is set up. + public Action? Authorization { get; set; } + + /// + /// Gets or sets the delegate used to configure options for the Sessions DbContext. + /// If not set, default option will implement. + /// + /// Assign a delegate to customize the configuration of the Sessions DbContext, such as + /// specifying the database provider or connection string. This property is typically used during application + /// startup to control how the Sessions context is set up. + public Action? Sessions { get; set; } + + /// + /// Gets or sets the delegate used to configure options for the Tokens DbContext. + /// If not set, default option will implement. + /// + /// Assign a delegate to customize the configuration of the Tokens DbContext, such as + /// specifying the database provider or connection string. This property is typically used during application + /// startup to control how the Tokens context is set up. + public Action? Tokens { get; set; } + + /// + /// Gets or sets the delegate used to configure options for the Authentication DbContext. + /// If not set, default option will implement. + /// + /// Assign a delegate to customize the configuration of the Authentication DbContext, such as + /// specifying the database provider or connection string. This property is typically used during application + /// startup to control how the Authentication context is set up. + public Action? Authentication { get; set; } + + internal Action Resolve(Action? specific) + => specific ?? Default ?? throw new InvalidOperationException("No database configuration provided for UltimateAuth EFCore. Use options.Default or configure specific DbContext options."); + } +} diff --git a/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/UltimateAuthEntityFrameworkCoreExtensions.cs b/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/UltimateAuthEntityFrameworkCoreExtensions.cs new file mode 100644 index 00000000..cb69b02d --- /dev/null +++ b/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/UltimateAuthEntityFrameworkCoreExtensions.cs @@ -0,0 +1,95 @@ +using CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.Extensions; +using CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.Extensions; +using CodeBeam.UltimateAuth.Authorization.Reference.Extensions; +using CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore.Extensions; +using CodeBeam.UltimateAuth.Credentials.Reference.Extensions; +using CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.Extensions; +using CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore.Extensions; +using CodeBeam.UltimateAuth.Users.EntityFrameworkCore.Extensions; +using CodeBeam.UltimateAuth.Users.Reference.Extensions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace CodeBeam.UltimateAuth.EntityFrameworkCore; + +/// +/// Provides extension methods for registering UltimateAuth with Entity Framework Core-based persistence using reference +/// domain implementations. +/// +public static class UltimateAuthEntityFrameworkCoreExtensions +{ + /// + /// Registers UltimateAuth with Entity Framework Core based persistence using reference domain implementations. + /// + /// The service collection. + /// + /// A delegate used to configure the for all UltimateAuth DbContexts. + /// + /// This is required and must specify a database provider such as: + /// + /// UseSqlServer + /// UseNpgsql + /// UseSqlite + /// + /// + /// The updated . + /// + /// This method wires up all Entity Framework Core stores along with reference domain implementations. + /// + /// Example: + /// + /// services.AddUltimateAuthServer() + /// .AddEntityFrameworkReference(options => + /// { + /// options.UseSqlServer("connection-string"); + /// }); + /// + /// + /// Note: + /// This method does not configure migrations automatically. You are responsible for managing migrations. + /// + public static IServiceCollection AddEntityFrameworkReference(this IServiceCollection services, Action configureDb) + { + services + .AddUltimateAuthUsersEntityFrameworkCore(configureDb) + .AddUltimateAuthUsersReference() + .AddUltimateAuthCredentialsEntityFrameworkCore(configureDb) + .AddUltimateAuthCredentialsReference() + .AddUltimateAuthAuthorizationEntityFrameworkCore(configureDb) + .AddUltimateAuthAuthorizationReference() + .AddUltimateAuthSessionsEntityFrameworkCore(configureDb) + .AddUltimateAuthTokensEntityFrameworkCore(configureDb) + .AddUltimateAuthAuthenticationEntityFrameworkCore(configureDb); + + return services; + } + + /// + /// Adds and configures Entity Framework Core-based UltimateAuth services and related references to the specified + /// service collection. + /// + /// This method registers all required UltimateAuth services for users, credentials, + /// authorization, sessions, tokens, and authentication using Entity Framework Core. It should be called during + /// application startup as part of service configuration. + /// The service collection to which the UltimateAuth Entity Framework Core services and references will be added. + /// A delegate that configures the options for UltimateAuth Entity Framework Core integration. + /// The same service collection instance, enabling method chaining. + public static IServiceCollection AddEntityFrameworkReference(this IServiceCollection services, Action configure) + { + var options = new UAuthEfCoreOptions(); + configure(options); + + services + .AddUltimateAuthUsersEntityFrameworkCore(options.Resolve(options.Users)) + .AddUltimateAuthUsersReference() + .AddUltimateAuthCredentialsEntityFrameworkCore(options.Resolve(options.Credentials)) + .AddUltimateAuthCredentialsReference() + .AddUltimateAuthAuthorizationEntityFrameworkCore(options.Resolve(options.Authorization)) + .AddUltimateAuthAuthorizationReference() + .AddUltimateAuthSessionsEntityFrameworkCore(options.Resolve(options.Sessions)) + .AddUltimateAuthTokensEntityFrameworkCore(options.Resolve(options.Tokens)) + .AddUltimateAuthAuthenticationEntityFrameworkCore(options.Resolve(options.Authentication)); + + return services; + } +} diff --git a/nuget/CodeBeam.UltimateAuth.InMemory/CodeBeam.UltimateAuth.InMemory.csproj b/nuget/CodeBeam.UltimateAuth.InMemory/CodeBeam.UltimateAuth.InMemory.csproj new file mode 100644 index 00000000..2d115715 --- /dev/null +++ b/nuget/CodeBeam.UltimateAuth.InMemory/CodeBeam.UltimateAuth.InMemory.csproj @@ -0,0 +1,24 @@ + + + + net8.0;net9.0;net10.0 + enable + enable + 0.1.0-preview.1 + 0.1.0-preview.1 + true + + + + + + + + + + + + + + + diff --git a/nuget/CodeBeam.UltimateAuth.InMemory/UltimateAuthInMemoryExtensions.cs b/nuget/CodeBeam.UltimateAuth.InMemory/UltimateAuthInMemoryExtensions.cs new file mode 100644 index 00000000..13ab0a3f --- /dev/null +++ b/nuget/CodeBeam.UltimateAuth.InMemory/UltimateAuthInMemoryExtensions.cs @@ -0,0 +1,46 @@ +using Microsoft.Extensions.DependencyInjection; +using CodeBeam.UltimateAuth.Credentials.InMemory.Extensions; +using CodeBeam.UltimateAuth.Credentials.Reference.Extensions; +using CodeBeam.UltimateAuth.Users.InMemory.Extensions; +using CodeBeam.UltimateAuth.Users.Reference.Extensions; +using CodeBeam.UltimateAuth.Authorization.InMemory.Extensions; +using CodeBeam.UltimateAuth.Authorization.Reference.Extensions; +using CodeBeam.UltimateAuth.Sessions.InMemory.Extensions; +using CodeBeam.UltimateAuth.Tokens.InMemory.Extensions; +using CodeBeam.UltimateAuth.Authentication.InMemory.Extensions; + +namespace CodeBeam.UltimateAuth.InMemory; + +/// +/// Provides extension methods for registering in-memory implementations of UltimateAuth user, credential, +/// authorization, session, token, and authentication services, along with their reference services, in the dependency +/// injection container. +/// +/// These methods are intended for scenarios such as testing or development where in-memory storage is +/// sufficient. For production environments, consider using persistent storage implementations. +public static class UltimateAuthInMemoryExtensions +{ + /// + /// Registers in-memory implementations of UltimateAuth user, credential, authorization, session, token, and + /// authentication services, along with their reference services, in the dependency injection container. + /// + /// This method is intended for scenarios such as testing or development where in-memory storage + /// is sufficient. For production environments, consider using persistent storage implementations. + /// The service collection to which the in-memory UltimateAuth services will be added. + /// The same instance of that was provided, to support method chaining. + public static IServiceCollection AddInMemoryReference(this IServiceCollection services) + { + services + .AddUltimateAuthUsersInMemory() + .AddUltimateAuthUsersReference() + .AddUltimateAuthCredentialsInMemory() + .AddUltimateAuthCredentialsReference() + .AddUltimateAuthAuthorizationInMemory() + .AddUltimateAuthAuthorizationReference() + .AddUltimateAuthSessionsInMemory() + .AddUltimateAuthTokensInMemory() + .AddUltimateAuthAuthenticationInMemory(); + + return services; + } +} \ No newline at end of file diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.csproj b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.csproj index fd8d083e..7a0b255e 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.csproj +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.csproj @@ -16,21 +16,12 @@ - - - - - + + - - - - - - diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/App.razor b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/App.razor index f9989cb9..71805ad6 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/App.razor +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/App.razor @@ -24,7 +24,7 @@ - + diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/Home.razor b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/Home.razor index d8673b17..219617bf 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/Home.razor +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/Home.razor @@ -16,7 +16,7 @@ @inject IHubFlowReader HubFlowReader @inject IHubCredentialResolver HubCredentialResolver @inject IAuthStore AuthStore -@inject IBrowserStorage BrowserStorage +@inject IClientStorage BrowserStorage @inject IUAuthFlowService Flow @inject ISnackbar Snackbar @inject IFlowCredentialResolver CredentialResolver diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Routes.razor b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Routes.razor index 9e918850..22f83d10 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Routes.razor +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Routes.razor @@ -3,14 +3,28 @@ @inject ISnackbar Snackbar @inject DarkModeManager DarkModeManager - - + + + + + + + + + + + + + + + @* Advanced: you can fully control routing by providing your own Router *@ + @* - + @@ -20,8 +34,8 @@ - - + *@ + @code { private async Task HandleReauth() diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/_Imports.razor b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/_Imports.razor index 09765c2c..aada4df3 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/_Imports.razor +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/_Imports.razor @@ -11,6 +11,7 @@ @using CodeBeam.UltimateAuth.Sample.UAuthHub.Components @using CodeBeam.UltimateAuth.Sample.UAuthHub.Components.Layout @using CodeBeam.UltimateAuth.Client +@using CodeBeam.UltimateAuth.Client.Blazor @using MudBlazor @using MudExtensions diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Infrastructure/DarkModeManager.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Infrastructure/DarkModeManager.cs index f8f05cb4..95b63347 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Infrastructure/DarkModeManager.cs +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Infrastructure/DarkModeManager.cs @@ -7,9 +7,9 @@ public sealed class DarkModeManager { private const string StorageKey = "uauth:theme:dark"; - private readonly IBrowserStorage _storage; + private readonly IClientStorage _storage; - public DarkModeManager(IBrowserStorage storage) + public DarkModeManager(IClientStorage storage) { _storage = storage; } diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs index b0412644..20cf0a8e 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs @@ -1,21 +1,13 @@ -using CodeBeam.UltimateAuth.Authentication.InMemory; -using CodeBeam.UltimateAuth.Authorization.InMemory.Extensions; -using CodeBeam.UltimateAuth.Authorization.Reference.Extensions; -using CodeBeam.UltimateAuth.Client; -using CodeBeam.UltimateAuth.Client.Extensions; +using CodeBeam.UltimateAuth.Client.Blazor; +using CodeBeam.UltimateAuth.Client.Blazor.Extensions; using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.Infrastructure; using CodeBeam.UltimateAuth.Core.Runtime; -using CodeBeam.UltimateAuth.Credentials.InMemory.Extensions; -using CodeBeam.UltimateAuth.Credentials.Reference; +using CodeBeam.UltimateAuth.InMemory; using CodeBeam.UltimateAuth.Sample.UAuthHub.Components; using CodeBeam.UltimateAuth.Sample.UAuthHub.Infrastructure; using CodeBeam.UltimateAuth.Security.Argon2; using CodeBeam.UltimateAuth.Server.Extensions; -using CodeBeam.UltimateAuth.Sessions.InMemory; -using CodeBeam.UltimateAuth.Tokens.InMemory; -using CodeBeam.UltimateAuth.Users.InMemory.Extensions; -using CodeBeam.UltimateAuth.Users.Reference.Extensions; using MudBlazor.Services; using MudExtensions.Services; using Scalar.AspNetCore; @@ -43,18 +35,10 @@ //o.Session.TouchInterval = TimeSpan.FromSeconds(9); //o.Session.IdleTimeout = TimeSpan.FromSeconds(15); }) - .AddUltimateAuthUsersInMemory() - .AddUltimateAuthUsersReference() - .AddUltimateAuthCredentialsInMemory() - .AddUltimateAuthCredentialsReference() - .AddUltimateAuthAuthorizationInMemory() - .AddUltimateAuthAuthorizationReference() - .AddUltimateAuthInMemorySessions() - .AddUltimateAuthInMemoryTokens() - .AddUltimateAuthInMemoryAuthenticationSecurity() + .AddInMemoryReference() .AddUltimateAuthArgon2(); -builder.Services.AddUltimateAuthClient(o => +builder.Services.AddUltimateAuthClientBlazor(o => { //o.Refresh.Interval = TimeSpan.FromSeconds(5); o.Reauth.Behavior = ReauthBehavior.RaiseEvent; @@ -106,7 +90,7 @@ app.MapControllers(); app.MapRazorComponents() .AddInteractiveServerRenderMode() - .AddUltimateAuthClientRoutes(typeof(UAuthClientMarker).Assembly); + .AddUltimateAuthRoutes(UAuthAssemblies.BlazorClient()); app.MapGet("/health", () => { diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/CodeBeam.UltimateAuth.Sample.BlazorServer.csproj b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/CodeBeam.UltimateAuth.Sample.BlazorServer.csproj index 7cc56f81..78daa14d 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/CodeBeam.UltimateAuth.Sample.BlazorServer.csproj +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/CodeBeam.UltimateAuth.Sample.BlazorServer.csproj @@ -4,32 +4,23 @@ net10.0 enable enable - 0.0.1 + 0.1.0 - + - - - - - + + - - - - - - diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/App.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/App.razor index 24f946d8..087e4279 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/App.razor +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/App.razor @@ -19,7 +19,7 @@ - + diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor.cs index 844f6483..b25944b1 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor.cs +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor.cs @@ -1,4 +1,4 @@ -using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Client.Blazor; using CodeBeam.UltimateAuth.Client.Errors; using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Login.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Login.razor.cs index f4747658..a1a72472 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Login.razor.cs +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Login.razor.cs @@ -1,4 +1,4 @@ -using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Client.Blazor; using CodeBeam.UltimateAuth.Client.Runtime; using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Routes.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Routes.razor index 03c4e497..6586c3fc 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Routes.razor +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Routes.razor @@ -3,14 +3,28 @@ @inject ISnackbar Snackbar @inject DarkModeManager DarkModeManager - - + + + + + + + + + + + + + + + @* Advanced: you can fully control routing by providing your own Router *@ + @* - + @@ -20,8 +34,8 @@ - - + *@ + @code { private async Task HandleReauth() diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/_Imports.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/_Imports.razor index 877928bb..2c3cb6dd 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/_Imports.razor +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/_Imports.razor @@ -16,6 +16,7 @@ @using CodeBeam.UltimateAuth.Client @using CodeBeam.UltimateAuth.Client.Runtime @using CodeBeam.UltimateAuth.Client.Diagnostics +@using CodeBeam.UltimateAuth.Client.Blazor @using MudBlazor @using MudExtensions diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Infrastructure/DarkModeManager.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Infrastructure/DarkModeManager.cs index 7b2c1990..9afcf32f 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Infrastructure/DarkModeManager.cs +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Infrastructure/DarkModeManager.cs @@ -7,9 +7,9 @@ public sealed class DarkModeManager { private const string StorageKey = "uauth:theme:dark"; - private readonly IBrowserStorage _storage; + private readonly IClientStorage _storage; - public DarkModeManager(IBrowserStorage storage) + public DarkModeManager(IClientStorage storage) { _storage = storage; } diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs index dee4c413..551ff1d3 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs @@ -1,24 +1,16 @@ -using CodeBeam.UltimateAuth.Authentication.InMemory; -using CodeBeam.UltimateAuth.Authorization.InMemory.Extensions; -using CodeBeam.UltimateAuth.Authorization.Reference.Extensions; -using CodeBeam.UltimateAuth.Client; -using CodeBeam.UltimateAuth.Client.Extensions; +using CodeBeam.UltimateAuth.Client.Blazor.Extensions; using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.Infrastructure; -using CodeBeam.UltimateAuth.Credentials.InMemory.Extensions; -using CodeBeam.UltimateAuth.Credentials.Reference; +using CodeBeam.UltimateAuth.InMemory; using CodeBeam.UltimateAuth.Sample.BlazorServer.Components; using CodeBeam.UltimateAuth.Sample.BlazorServer.Infrastructure; using CodeBeam.UltimateAuth.Security.Argon2; using CodeBeam.UltimateAuth.Server.Extensions; -using CodeBeam.UltimateAuth.Sessions.InMemory; -using CodeBeam.UltimateAuth.Tokens.InMemory; -using CodeBeam.UltimateAuth.Users.InMemory.Extensions; -using CodeBeam.UltimateAuth.Users.Reference.Extensions; using Microsoft.AspNetCore.HttpOverrides; using MudBlazor.Services; using MudExtensions.Services; using Scalar.AspNetCore; +using CodeBeam.UltimateAuth.Client.Blazor; var builder = WebApplication.CreateBuilder(args); @@ -49,18 +41,10 @@ o.Login.LockoutDuration = TimeSpan.FromSeconds(10); o.Identifiers.AllowMultipleUsernames = true; }) - .AddUltimateAuthUsersInMemory() - .AddUltimateAuthUsersReference() - .AddUltimateAuthCredentialsInMemory() - .AddUltimateAuthCredentialsReference() - .AddUltimateAuthAuthorizationInMemory() - .AddUltimateAuthAuthorizationReference() - .AddUltimateAuthInMemorySessions() - .AddUltimateAuthInMemoryTokens() - .AddUltimateAuthInMemoryAuthenticationSecurity() + .AddInMemoryReference() .AddUltimateAuthArgon2(); -builder.Services.AddUltimateAuthClient(o => +builder.Services.AddUltimateAuthClientBlazor(o => { //o.AutoRefresh.Interval = TimeSpan.FromSeconds(5); o.Reauth.Behavior = ReauthBehavior.RaiseEvent; @@ -76,6 +60,7 @@ ForwardedHeaders.XForwardedProto; }); + var app = builder.Build(); if (!app.Environment.IsDevelopment()) @@ -104,6 +89,6 @@ app.MapUltimateAuthEndpoints(); app.MapRazorComponents() .AddInteractiveServerRenderMode() - .AddUltimateAuthClientRoutes(typeof(UAuthClientMarker).Assembly); + .AddUltimateAuthRoutes(UAuthAssemblies.BlazorClient()); app.Run(); diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/App.razor b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/App.razor index 7d8ad8a5..783b707b 100644 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/App.razor +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/App.razor @@ -1,16 +1,30 @@ -@using CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.Infrastructure -@using CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.Pages +@using CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.Pages +@using CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.Infrastructure @inject ISnackbar Snackbar @inject DarkModeManager DarkModeManager - - + + + + + + + + + + + + + + + @* Advanced: you can fully control routing by providing your own Router *@ + @* - + @@ -20,8 +34,8 @@ - - + *@ + @code { private async Task HandleReauth() diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.csproj b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.csproj index 55885729..d6206681 100644 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.csproj +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.csproj @@ -4,21 +4,21 @@ net10.0 enable enable - 0.0.1 + 0.1.0 - - - - + + + + - + diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Infrastructure/DarkModeManager.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Infrastructure/DarkModeManager.cs index bd8900e4..de933317 100644 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Infrastructure/DarkModeManager.cs +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Infrastructure/DarkModeManager.cs @@ -7,9 +7,9 @@ public sealed class DarkModeManager { private const string StorageKey = "uauth:theme:dark"; - private readonly IBrowserStorage _storage; + private readonly IClientStorage _storage; - public DarkModeManager(IBrowserStorage storage) + public DarkModeManager(IClientStorage storage) { _storage = storage; } diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Home.razor.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Home.razor.cs index 0a44f4fd..f734b4b8 100644 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Home.razor.cs +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Home.razor.cs @@ -1,4 +1,4 @@ -using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Client.Blazor; using CodeBeam.UltimateAuth.Client.Errors; using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Login.razor.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Login.razor.cs index 015076b6..459081f1 100644 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Login.razor.cs +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Login.razor.cs @@ -1,4 +1,5 @@ using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Client.Blazor; using CodeBeam.UltimateAuth.Client.Runtime; using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Program.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Program.cs index 0412ee70..53a18e6b 100644 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Program.cs +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Program.cs @@ -1,4 +1,4 @@ -using CodeBeam.UltimateAuth.Client.Extensions; +using CodeBeam.UltimateAuth.Client.Blazor.Extensions; using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.Extensions; using CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm; @@ -15,7 +15,7 @@ builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); builder.Services.AddUltimateAuth(); -builder.Services.AddUltimateAuthClient(o => +builder.Services.AddUltimateAuthClientBlazor(o => { o.Endpoints.BasePath = "https://localhost:6110/auth"; o.Reauth.Behavior = ReauthBehavior.RaiseEvent; diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/_Imports.razor b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/_Imports.razor index 7fe2bd88..9eaccb01 100644 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/_Imports.razor +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/_Imports.razor @@ -16,6 +16,7 @@ @using CodeBeam.UltimateAuth.Client @using CodeBeam.UltimateAuth.Client.Runtime @using CodeBeam.UltimateAuth.Client.Diagnostics +@using CodeBeam.UltimateAuth.Client.Blazor @using MudBlazor @using MudExtensions diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/wwwroot/index.html b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/wwwroot/index.html index 879067ed..6499fa41 100644 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/wwwroot/index.html +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/wwwroot/index.html @@ -32,7 +32,7 @@ - + diff --git a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi/CodeBeam.UltimateAuth.Sample.ResourceApi.csproj b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi/CodeBeam.UltimateAuth.Sample.ResourceApi.csproj index 4f62ba6c..c74a7da3 100644 --- a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi/CodeBeam.UltimateAuth.Sample.ResourceApi.csproj +++ b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi/CodeBeam.UltimateAuth.Sample.ResourceApi.csproj @@ -7,11 +7,11 @@ - + - + diff --git a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi/Program.cs b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi/Program.cs index e5da6274..ebb830f7 100644 --- a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi/Program.cs +++ b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi/Program.cs @@ -1,5 +1,6 @@ using System.Security.Claims; using CodeBeam.UltimateAuth.Core.Extensions; +using CodeBeam.UltimateAuth.Server.Extensions; var builder = WebApplication.CreateBuilder(args); @@ -9,17 +10,7 @@ // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); -builder.Services.AddUltimateAuth(); - -builder.Services.AddAuthorization(options => -{ - options.AddPolicy("ApiUser", policy => - { - policy.RequireAuthenticatedUser(); - policy.RequireClaim("scope", "api"); - // veya role, veya custom claim - }); -}); +builder.Services.AddUltimateAuthResourceApi(); builder.Services.AddCors(options => { diff --git a/src/CodeBeam.UltimateAuth.Client/AuthState/UAuthAuthenticationStateProvider.cs b/src/CodeBeam.UltimateAuth.Client/AuthState/UAuthAuthenticationStateProvider.cs index 5339f0f4..2f47278b 100644 --- a/src/CodeBeam.UltimateAuth.Client/AuthState/UAuthAuthenticationStateProvider.cs +++ b/src/CodeBeam.UltimateAuth.Client/AuthState/UAuthAuthenticationStateProvider.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Components.Authorization; -namespace CodeBeam.UltimateAuth.Client; +namespace CodeBeam.UltimateAuth.Client.Blazor; internal sealed class UAuthAuthenticationStateProvider : AuthenticationStateProvider { diff --git a/src/CodeBeam.UltimateAuth.Client/AuthState/UAuthCascadingStateProvider.cs b/src/CodeBeam.UltimateAuth.Client/AuthState/UAuthCascadingStateProvider.cs deleted file mode 100644 index ed025e7c..00000000 --- a/src/CodeBeam.UltimateAuth.Client/AuthState/UAuthCascadingStateProvider.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Microsoft.AspNetCore.Components; - -namespace CodeBeam.UltimateAuth.Client; - -internal sealed class UAuthCascadingStateProvider : CascadingValueSource, IDisposable -{ - private readonly IUAuthStateManager _stateManager; - - public UAuthCascadingStateProvider(IUAuthStateManager stateManager) : base(() => stateManager.State, isFixed: false) - { - _stateManager = stateManager; - _stateManager.State.Changed += OnStateChanged; - } - - private void OnStateChanged(UAuthStateChangeReason _) - { - NotifyChangedAsync(); - } - - public void Dispose() - { - _stateManager.State.Changed -= OnStateChanged; - } -} diff --git a/src/CodeBeam.UltimateAuth.Client/CodeBeam.UltimateAuth.Client.csproj b/src/CodeBeam.UltimateAuth.Client/CodeBeam.UltimateAuth.Client.Blazor.csproj similarity index 60% rename from src/CodeBeam.UltimateAuth.Client/CodeBeam.UltimateAuth.Client.csproj rename to src/CodeBeam.UltimateAuth.Client/CodeBeam.UltimateAuth.Client.Blazor.csproj index b9203488..fb40a9ef 100644 --- a/src/CodeBeam.UltimateAuth.Client/CodeBeam.UltimateAuth.Client.csproj +++ b/src/CodeBeam.UltimateAuth.Client/CodeBeam.UltimateAuth.Client.Blazor.csproj @@ -4,29 +4,49 @@ net8.0;net9.0;net10.0 enable enable - 0.0.1 - 0.0.1 - true $(NoWarn);1591 + + CodeBeam.UltimateAuth.Client.Blazor + 0.1.0-preview.1 + + + Blazor client integration for UltimateAuth. + Provides authentication state management, token handling and UI integration. + + + authentication;blazor;client;identity;auth-framework + + README.md + https://github.com/CodeBeamOrg/UltimateAuth + https://github.com/CodeBeamOrg/UltimateAuth + Apache-2.0 + + true + true + snupkg + + true + + - - - - + + + + - - - - + + + + - - - - + + + + @@ -42,14 +62,16 @@ - - - - + + + + + + diff --git a/src/CodeBeam.UltimateAuth.Client/Components/UALoginDispatch.razor b/src/CodeBeam.UltimateAuth.Client/Components/UALoginDispatch.razor index d4942ba9..af62ef65 100644 --- a/src/CodeBeam.UltimateAuth.Client/Components/UALoginDispatch.razor +++ b/src/CodeBeam.UltimateAuth.Client/Components/UALoginDispatch.razor @@ -1,6 +1,6 @@ @page "/__uauth/login-redirect" -@namespace CodeBeam.UltimateAuth.Client +@namespace CodeBeam.UltimateAuth.Client.Blazor @using CodeBeam.UltimateAuth.Core.Defaults @using Microsoft.AspNetCore.WebUtilities @inject NavigationManager Nav diff --git a/src/CodeBeam.UltimateAuth.Client/Components/UAuthApp.razor b/src/CodeBeam.UltimateAuth.Client/Components/UAuthApp.razor index 47a45bad..d0b1a1ac 100644 --- a/src/CodeBeam.UltimateAuth.Client/Components/UAuthApp.razor +++ b/src/CodeBeam.UltimateAuth.Client/Components/UAuthApp.razor @@ -1,7 +1,8 @@ -@namespace CodeBeam.UltimateAuth.Client +@namespace CodeBeam.UltimateAuth.Client.Blazor @using CodeBeam.UltimateAuth.Client.Contracts @using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Routing @inject IUAuthStateManager StateManager @inject IUAuthClientBootstrapper Bootstrapper @inject ISessionCoordinator Coordinator @@ -12,11 +13,39 @@ { @ChildContent + @if (UseBuiltInRouter) + { + @RenderRouter() + } } else { @ChildContent + @if (UseBuiltInRouter) + { + @RenderRouter() + } } + +@code { + private RenderFragment RenderRouter() => @ + + + + @if (NotAuthorized is not null) + { + @NotAuthorized + } + else + { +

Not authorized

+ } +
+
+ +
+
; +} diff --git a/src/CodeBeam.UltimateAuth.Client/Components/UAuthApp.razor.cs b/src/CodeBeam.UltimateAuth.Client/Components/UAuthApp.razor.cs index cbddf34c..ef31c4b2 100644 --- a/src/CodeBeam.UltimateAuth.Client/Components/UAuthApp.razor.cs +++ b/src/CodeBeam.UltimateAuth.Client/Components/UAuthApp.razor.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Components; +using System.Reflection; -namespace CodeBeam.UltimateAuth.Client; +namespace CodeBeam.UltimateAuth.Client.Blazor; public partial class UAuthApp { @@ -8,7 +9,28 @@ public partial class UAuthApp private bool _coordinatorStarted; [Parameter] - public RenderFragment ChildContent { get; set; } = default!; + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public RenderFragment? NotAuthorized { get; set; } + + [Parameter] + public bool UseBuiltInRouter { get; set; } + + [Parameter] + public bool UseUAuthClientRoutes { get; set; } = true; + + [Parameter] + public Assembly? AppAssembly { get; set; } + + [Parameter] + public IEnumerable? AdditionalAssemblies { get; set; } + + [Parameter] + public Type? DefaultLayout { get; set; } + + [Parameter] + public string? FocusSelector { get; set; } = "h1"; [Parameter] public UAuthRenderMode RenderMode { get; set; } = UAuthRenderMode.Manual; @@ -83,6 +105,17 @@ private async void HandleReauthRequired() await OnReauthRequired.InvokeAsync(); } + private IEnumerable GetAdditionalAssemblies() + { + if (AdditionalAssemblies is null && UseUAuthClientRoutes) + return UAuthAssemblies.BlazorClient(); + + if (UseUAuthClientRoutes) + return AdditionalAssemblies.WithUltimateAuth(); + + return Enumerable.Empty(); + } + public async ValueTask DisposeAsync() { StateManager.State.Changed -= OnStateChanged; diff --git a/src/CodeBeam.UltimateAuth.Client/Components/UAuthFlowPageBase.cs b/src/CodeBeam.UltimateAuth.Client/Components/UAuthFlowPageBase.cs index f4723276..8f805211 100644 --- a/src/CodeBeam.UltimateAuth.Client/Components/UAuthFlowPageBase.cs +++ b/src/CodeBeam.UltimateAuth.Client/Components/UAuthFlowPageBase.cs @@ -4,7 +4,7 @@ using System.Text; using System.Text.Json; -namespace CodeBeam.UltimateAuth.Client; +namespace CodeBeam.UltimateAuth.Client.Blazor; public abstract class UAuthFlowPageBase : UAuthReactiveComponentBase { diff --git a/src/CodeBeam.UltimateAuth.Client/Components/UAuthLoginForm.razor b/src/CodeBeam.UltimateAuth.Client/Components/UAuthLoginForm.razor index d8d4dddf..75ebab7b 100644 --- a/src/CodeBeam.UltimateAuth.Client/Components/UAuthLoginForm.razor +++ b/src/CodeBeam.UltimateAuth.Client/Components/UAuthLoginForm.razor @@ -1,5 +1,5 @@ @* TODO: Optional double-submit prevention for native form submit *@ -@namespace CodeBeam.UltimateAuth.Client +@namespace CodeBeam.UltimateAuth.Client.Blazor @using CodeBeam.UltimateAuth.Client.Device @using CodeBeam.UltimateAuth.Client.Options diff --git a/src/CodeBeam.UltimateAuth.Client/Components/UAuthLoginForm.razor.cs b/src/CodeBeam.UltimateAuth.Client/Components/UAuthLoginForm.razor.cs index cd7de6dc..bb730360 100644 --- a/src/CodeBeam.UltimateAuth.Client/Components/UAuthLoginForm.razor.cs +++ b/src/CodeBeam.UltimateAuth.Client/Components/UAuthLoginForm.razor.cs @@ -7,7 +7,7 @@ using Microsoft.AspNetCore.WebUtilities; using Microsoft.JSInterop; -namespace CodeBeam.UltimateAuth.Client; +namespace CodeBeam.UltimateAuth.Client.Blazor; public partial class UAuthLoginForm { diff --git a/src/CodeBeam.UltimateAuth.Client/Components/UAuthReactiveComponentBase.cs b/src/CodeBeam.UltimateAuth.Client/Components/UAuthReactiveComponentBase.cs index aed94959..fb67afe8 100644 --- a/src/CodeBeam.UltimateAuth.Client/Components/UAuthReactiveComponentBase.cs +++ b/src/CodeBeam.UltimateAuth.Client/Components/UAuthReactiveComponentBase.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Components; -namespace CodeBeam.UltimateAuth.Client; +namespace CodeBeam.UltimateAuth.Client.Blazor; public abstract class UAuthReactiveComponentBase : ComponentBase, IDisposable { diff --git a/src/CodeBeam.UltimateAuth.Client/Components/UAuthScope.razor b/src/CodeBeam.UltimateAuth.Client/Components/UAuthScope.razor index deb5e819..49e15f69 100644 --- a/src/CodeBeam.UltimateAuth.Client/Components/UAuthScope.razor +++ b/src/CodeBeam.UltimateAuth.Client/Components/UAuthScope.razor @@ -1,4 +1,4 @@ -@namespace CodeBeam.UltimateAuth.Client +@namespace CodeBeam.UltimateAuth.Client.Blazor @inherits UAuthReactiveComponentBase @ChildContent diff --git a/src/CodeBeam.UltimateAuth.Client/Components/UAuthScope.razor.cs b/src/CodeBeam.UltimateAuth.Client/Components/UAuthScope.razor.cs index 86bf75e5..c3c4e2dd 100644 --- a/src/CodeBeam.UltimateAuth.Client/Components/UAuthScope.razor.cs +++ b/src/CodeBeam.UltimateAuth.Client/Components/UAuthScope.razor.cs @@ -1,7 +1,6 @@ using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Rendering; -namespace CodeBeam.UltimateAuth.Client; +namespace CodeBeam.UltimateAuth.Client.Blazor; public partial class UAuthScope : UAuthReactiveComponentBase { diff --git a/src/CodeBeam.UltimateAuth.Client/Components/UAuthStateView.razor b/src/CodeBeam.UltimateAuth.Client/Components/UAuthStateView.razor index bbcbab52..fca197bb 100644 --- a/src/CodeBeam.UltimateAuth.Client/Components/UAuthStateView.razor +++ b/src/CodeBeam.UltimateAuth.Client/Components/UAuthStateView.razor @@ -1,4 +1,4 @@ -@namespace CodeBeam.UltimateAuth.Client +@namespace CodeBeam.UltimateAuth.Client.Blazor @inherits UAuthReactiveComponentBase @using CodeBeam.UltimateAuth.Core.Domain diff --git a/src/CodeBeam.UltimateAuth.Client/Components/UAuthStateView.razor.cs b/src/CodeBeam.UltimateAuth.Client/Components/UAuthStateView.razor.cs index 23dc02e9..753f75e9 100644 --- a/src/CodeBeam.UltimateAuth.Client/Components/UAuthStateView.razor.cs +++ b/src/CodeBeam.UltimateAuth.Client/Components/UAuthStateView.razor.cs @@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Components; -namespace CodeBeam.UltimateAuth.Client; +namespace CodeBeam.UltimateAuth.Client.Blazor; public partial class UAuthStateView : UAuthReactiveComponentBase { diff --git a/src/CodeBeam.UltimateAuth.Client/Device/BrowserDeviceIdStorage.cs b/src/CodeBeam.UltimateAuth.Client/Device/BrowserDeviceIdStorage.cs index e2173571..b64b221b 100644 --- a/src/CodeBeam.UltimateAuth.Client/Device/BrowserDeviceIdStorage.cs +++ b/src/CodeBeam.UltimateAuth.Client/Device/BrowserDeviceIdStorage.cs @@ -1,14 +1,15 @@ using CodeBeam.UltimateAuth.Client.Contracts; +using CodeBeam.UltimateAuth.Client.Device; using CodeBeam.UltimateAuth.Client.Infrastructure; -namespace CodeBeam.UltimateAuth.Client.Device; +namespace CodeBeam.UltimateAuth.Client.Blazor.Device; public sealed class BrowserDeviceIdStorage : IDeviceIdStorage { private const string Key = "udid"; - private readonly IBrowserStorage _storage; + private readonly IClientStorage _storage; - public BrowserDeviceIdStorage(IBrowserStorage storage) + public BrowserDeviceIdStorage(IClientStorage storage) { _storage = storage; } diff --git a/src/CodeBeam.UltimateAuth.Client/Extensions/AssemblyExtensions.cs b/src/CodeBeam.UltimateAuth.Client/Extensions/AssemblyExtensions.cs new file mode 100644 index 00000000..3c0cb7aa --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Client/Extensions/AssemblyExtensions.cs @@ -0,0 +1,21 @@ +using System.Reflection; + +namespace CodeBeam.UltimateAuth.Client.Blazor; + +public static class UAuthAssemblies +{ + public static Assembly[] WithUltimateAuth(this IEnumerable? assemblies) + { + var authAssembly = typeof(UAuthBlazorClientMarker).Assembly; + + if (assemblies is null) + return new[] { authAssembly }; + + return assemblies.Append(authAssembly).DistinctBy(a => a.FullName).ToArray(); + } + + public static Assembly[] BlazorClient() + { + return new[] { typeof(UAuthBlazorClientMarker).Assembly }; + } +} \ No newline at end of file diff --git a/src/CodeBeam.UltimateAuth.Client/Extensions/ServiceCollectionExtensions.cs b/src/CodeBeam.UltimateAuth.Client/Extensions/ServiceCollectionExtensions.cs index 19cb5c77..1d3b84de 100644 --- a/src/CodeBeam.UltimateAuth.Client/Extensions/ServiceCollectionExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Client/Extensions/ServiceCollectionExtensions.cs @@ -1,103 +1,55 @@ using CodeBeam.UltimateAuth.Client.Abstractions; -using CodeBeam.UltimateAuth.Client.Authentication; +using CodeBeam.UltimateAuth.Client.Blazor.Device; +using CodeBeam.UltimateAuth.Client.Blazor.Infrastructure; using CodeBeam.UltimateAuth.Client.Device; -using CodeBeam.UltimateAuth.Client.Devices; using CodeBeam.UltimateAuth.Client.Diagnostics; -using CodeBeam.UltimateAuth.Client.Events; +using CodeBeam.UltimateAuth.Client.Extensions; using CodeBeam.UltimateAuth.Client.Infrastructure; using CodeBeam.UltimateAuth.Client.Options; -using CodeBeam.UltimateAuth.Client.Runtime; -using CodeBeam.UltimateAuth.Client.Services; -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Options; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options; -namespace CodeBeam.UltimateAuth.Client.Extensions; +namespace CodeBeam.UltimateAuth.Client.Blazor.Extensions; /// -/// Provides extension methods for registering UltimateAuth client services. -/// -/// This layer is responsible for: -/// - Client-side authentication actions (login, logout, refresh, reauth) -/// - Browser-based POST infrastructure (JS form submit) -/// - Endpoint configuration for auth mutations -/// -/// This extension can safely be used together with AddUltimateAuthServer() -/// in Blazor Server applications. +/// Provides extension methods for registering UltimateAuth client blazor services. +/// This layer method included core client capabilities with blazor components and storage. +/// This extension can safely be used together with AddUltimateAuthServer() in Blazor Server applications. /// public static class ServiceCollectionExtensions { - public static IServiceCollection AddUltimateAuthClient(this IServiceCollection services, Action? configure = null) + /// + /// Registers UltimateAuth client services with Blazor adapter services. + /// This package depends on CodeBeam.UltimateAuth.Client and completes it by supplying platform-specific implementations. + /// This ensures that all required abstractions from the core client are properly wired for Blazor applications. + /// So, do not use "AddUltimateAuthClient" when "AddUltimateAuthClientBlazor" already specified. + /// + public static IServiceCollection AddUltimateAuthClientBlazor(this IServiceCollection services, Action? configure = null) { ArgumentNullException.ThrowIfNull(services); + services.AddUltimateAuthClient(configure); - services.AddOptions() - // Program.cs configuration (lowest precedence) - .Configure(options => - { - configure?.Invoke(options); - }) - // appsettings.json (highest precedence) - .BindConfiguration("UltimateAuth:Client"); - - return services.AddUltimateAuthClientInternal(); + return services.AddUltimateAuthClientBlazorInternal(); } /// /// Internal shared registration pipeline for UltimateAuth client services. - /// - /// This method registers: - /// - Client infrastructure - /// - Public client abstractions - /// - /// NOTE: /// This method does NOT register any server-side services. /// - private static IServiceCollection AddUltimateAuthClientInternal(this IServiceCollection services) + private static IServiceCollection AddUltimateAuthClientBlazorInternal(this IServiceCollection services) { - services.AddScoped(); - - services.AddOptions(); - services.AddSingleton, UAuthClientOptionsValidator>(); - services.AddSingleton, UAuthClientEndpointOptionsValidator>(); - - services.AddSingleton(); - services.AddSingleton, UAuthClientOptionsPostConfigure>(); - services.TryAddSingleton(); - services.AddSingleton(); - - services.PostConfigure(o => - { - o.AutoRefresh.Interval ??= TimeSpan.FromMinutes(5); - }); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); services.TryAddScoped(); - services.TryAddScoped(); services.AddScoped(); - services.TryAddScoped(); + services.AddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); + services.TryAddScoped(); services.TryAddScoped(); - services.AddScoped(); services.AddScoped(); - - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); + services.AddScoped(); services.AddAuthorizationCore(); diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/BlazorReturnUrlProvider.cs b/src/CodeBeam.UltimateAuth.Client/Infrastructure/BlazorReturnUrlProvider.cs new file mode 100644 index 00000000..1e95cee8 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Client/Infrastructure/BlazorReturnUrlProvider.cs @@ -0,0 +1,16 @@ +using CodeBeam.UltimateAuth.Client.Abstractions; +using Microsoft.AspNetCore.Components; + +namespace CodeBeam.UltimateAuth.Client.Blazor.Infrastructure; + +internal sealed class BlazorReturnUrlProvider : IReturnUrlProvider +{ + private readonly NavigationManager _nav; + + public BlazorReturnUrlProvider(NavigationManager nav) + { + _nav = nav; + } + + public string GetCurrentUrl() => _nav.Uri; +} diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/BrowserStorage.cs b/src/CodeBeam.UltimateAuth.Client/Infrastructure/BrowserClientStorage.cs similarity index 80% rename from src/CodeBeam.UltimateAuth.Client/Infrastructure/BrowserStorage.cs rename to src/CodeBeam.UltimateAuth.Client/Infrastructure/BrowserClientStorage.cs index 6c3dff3e..017c43a5 100644 --- a/src/CodeBeam.UltimateAuth.Client/Infrastructure/BrowserStorage.cs +++ b/src/CodeBeam.UltimateAuth.Client/Infrastructure/BrowserClientStorage.cs @@ -1,13 +1,14 @@ using CodeBeam.UltimateAuth.Client.Contracts; +using CodeBeam.UltimateAuth.Client.Infrastructure; using Microsoft.JSInterop; -namespace CodeBeam.UltimateAuth.Client.Infrastructure; +namespace CodeBeam.UltimateAuth.Client.Blazor.Infrastructure; -public sealed class BrowserStorage : IBrowserStorage +public sealed class BrowserClientStorage : IClientStorage { private readonly IJSRuntime _js; - public BrowserStorage(IJSRuntime js) + public BrowserClientStorage(IJSRuntime js) { _js = js; } diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/BrowserUAuthBridge.cs b/src/CodeBeam.UltimateAuth.Client/Infrastructure/BrowserUAuthBridge.cs index 2fa8642c..31d28d09 100644 --- a/src/CodeBeam.UltimateAuth.Client/Infrastructure/BrowserUAuthBridge.cs +++ b/src/CodeBeam.UltimateAuth.Client/Infrastructure/BrowserUAuthBridge.cs @@ -1,6 +1,7 @@ -using Microsoft.JSInterop; +using CodeBeam.UltimateAuth.Client.Infrastructure; +using Microsoft.JSInterop; -namespace CodeBeam.UltimateAuth.Client.Infrastructure; +namespace CodeBeam.UltimateAuth.Client.Blazor.Infrastructure; internal sealed class BrowserUAuthBridge : IBrowserUAuthBridge { diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/SessionCoordinator.cs b/src/CodeBeam.UltimateAuth.Client/Infrastructure/SessionCoordinator.cs index a6de47b9..fb90ef57 100644 --- a/src/CodeBeam.UltimateAuth.Client/Infrastructure/SessionCoordinator.cs +++ b/src/CodeBeam.UltimateAuth.Client/Infrastructure/SessionCoordinator.cs @@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Components; using Microsoft.Extensions.Options; -namespace CodeBeam.UltimateAuth.Client.Infrastructure; +namespace CodeBeam.UltimateAuth.Client.Blazor.Infrastructure; // TODO: Add multi tab single refresh support internal sealed class SessionCoordinator : ISessionCoordinator diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/Login/UAuthLoginPageDiscovery.cs b/src/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthLoginPageDiscovery.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Infrastructure/Login/UAuthLoginPageDiscovery.cs rename to src/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthLoginPageDiscovery.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthRequestClient.cs b/src/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthRequestClient.cs index 7cb88931..b0a2a020 100644 --- a/src/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthRequestClient.cs +++ b/src/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthRequestClient.cs @@ -1,12 +1,13 @@ using CodeBeam.UltimateAuth.Client.Contracts; using CodeBeam.UltimateAuth.Client.Errors; +using CodeBeam.UltimateAuth.Client.Infrastructure; using CodeBeam.UltimateAuth.Client.Options; using Microsoft.Extensions.Options; using Microsoft.JSInterop; using System.Net; // TODO: Add fluent helper API like RequiredOk -namespace CodeBeam.UltimateAuth.Client.Infrastructure; +namespace CodeBeam.UltimateAuth.Client.Blazor.Infrastructure; internal sealed class UAuthRequestClient : IUAuthRequestClient { diff --git a/src/CodeBeam.UltimateAuth.Client/README.md b/src/CodeBeam.UltimateAuth.Client/README.md new file mode 100644 index 00000000..62c39699 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Client/README.md @@ -0,0 +1,11 @@ +# UltimateAuth Blazor Client + +Client integration for UltimateAuth. + +## Features + +- Authentication state integration +- Session handling +- Token flows +- PKCE support +- Blazor components diff --git a/src/CodeBeam.UltimateAuth.Client/Runtime/UAuthBlazorClientMarker.cs b/src/CodeBeam.UltimateAuth.Client/Runtime/UAuthBlazorClientMarker.cs new file mode 100644 index 00000000..d7174ce1 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Client/Runtime/UAuthBlazorClientMarker.cs @@ -0,0 +1,5 @@ +namespace CodeBeam.UltimateAuth.Client.Blazor; + +public class UAuthBlazorClientMarker +{ +} diff --git a/src/CodeBeam.UltimateAuth.Client/Runtime/UAuthClientMarker.cs b/src/CodeBeam.UltimateAuth.Client/Runtime/UAuthClientMarker.cs deleted file mode 100644 index 1c9c2333..00000000 --- a/src/CodeBeam.UltimateAuth.Client/Runtime/UAuthClientMarker.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace CodeBeam.UltimateAuth.Client; - -public class UAuthClientMarker -{ -} diff --git a/src/CodeBeam.UltimateAuth.Client/_Imports.razor b/src/CodeBeam.UltimateAuth.Client/_Imports.razor index 34f03595..d2138d49 100644 --- a/src/CodeBeam.UltimateAuth.Client/_Imports.razor +++ b/src/CodeBeam.UltimateAuth.Client/_Imports.razor @@ -3,3 +3,4 @@ @using CodeBeam.UltimateAuth.Client @using CodeBeam.UltimateAuth.Client.Abstractions @using CodeBeam.UltimateAuth.Client.Infrastructure +@using CodeBeam.UltimateAuth.Client.Blazor diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Security/IAuthenticationSecurityManager.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authentication/IAuthenticationSecurityManager.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Core/Abstractions/Security/IAuthenticationSecurityManager.cs rename to src/CodeBeam.UltimateAuth.Core/Abstractions/Authentication/IAuthenticationSecurityManager.cs diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Security/IAuthenticationSecurityStateStore.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authentication/IAuthenticationSecurityStateStore.cs similarity index 50% rename from src/CodeBeam.UltimateAuth.Core/Abstractions/Security/IAuthenticationSecurityStateStore.cs rename to src/CodeBeam.UltimateAuth.Core/Abstractions/Authentication/IAuthenticationSecurityStateStore.cs index fef7743c..e02e6a66 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Security/IAuthenticationSecurityStateStore.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authentication/IAuthenticationSecurityStateStore.cs @@ -1,16 +1,15 @@ using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Core.Security; namespace CodeBeam.UltimateAuth.Core.Abstractions; public interface IAuthenticationSecurityStateStore { - Task GetAsync(TenantKey tenant, UserKey userKey, AuthenticationSecurityScope scope, CredentialType? credentialType, CancellationToken ct = default); + Task GetAsync(UserKey userKey, AuthenticationSecurityScope scope, CredentialType? credentialType, CancellationToken ct = default); Task AddAsync(AuthenticationSecurityState state, CancellationToken ct = default); Task UpdateAsync(AuthenticationSecurityState state, long expectedVersion, CancellationToken ct = default); - Task DeleteAsync(TenantKey tenant, UserKey userKey, AuthenticationSecurityScope scope, CredentialType? credentialType, CancellationToken ct = default); + Task DeleteAsync(UserKey userKey, AuthenticationSecurityScope scope, CredentialType? credentialType, CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Authentication/IAuthenticationSecurityStateStoreFactory.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authentication/IAuthenticationSecurityStateStoreFactory.cs new file mode 100644 index 00000000..7a5f7aa9 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authentication/IAuthenticationSecurityStateStoreFactory.cs @@ -0,0 +1,9 @@ +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Core.Abstractions +{ + public interface IAuthenticationSecurityStateStoreFactory + { + IAuthenticationSecurityStateStore Create(TenantKey tenant); + } +} diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Entity/ITenantEntity.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Entity/ITenantEntity.cs new file mode 100644 index 00000000..c5f57319 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Entity/ITenantEntity.cs @@ -0,0 +1,8 @@ +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Core.Abstractions; + +public interface ITenantEntity +{ + TenantKey Tenant { get; } +} diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IRefreshTokenStore.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IRefreshTokenStore.cs index fb8693c1..095beb5e 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IRefreshTokenStore.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IRefreshTokenStore.cs @@ -5,7 +5,6 @@ namespace CodeBeam.UltimateAuth.Core.Abstractions; public interface IRefreshTokenStore { Task ExecuteAsync(Func action, CancellationToken ct = default); - Task ExecuteAsync(Func> action, CancellationToken ct = default); Task StoreAsync(RefreshToken token, CancellationToken ct = default); diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStore.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStore.cs index 53fd7b31..fc29ea73 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStore.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStore.cs @@ -20,8 +20,8 @@ public interface ISessionStore Task SaveChainAsync(UAuthSessionChain chain, long expectedVersion, CancellationToken ct = default); Task CreateChainAsync(UAuthSessionChain chain, CancellationToken ct = default); Task RevokeChainAsync(SessionChainId chainId, DateTimeOffset at, CancellationToken ct = default); - Task RevokeOtherChainsAsync(TenantKey tenant, UserKey user, SessionChainId keepChain, DateTimeOffset at, CancellationToken ct = default); - Task RevokeAllChainsAsync(TenantKey tenant, UserKey user, DateTimeOffset at, CancellationToken ct = default); + Task RevokeOtherChainsAsync(UserKey user, SessionChainId keepChain, DateTimeOffset at, CancellationToken ct = default); + Task RevokeAllChainsAsync(UserKey user, DateTimeOffset at, CancellationToken ct = default); Task RevokeChainCascadeAsync(SessionChainId chainId, DateTimeOffset at, CancellationToken ct = default); Task LogoutChainAsync(SessionChainId chainId, DateTimeOffset at, CancellationToken ct = default); @@ -34,7 +34,7 @@ public interface ISessionStore Task GetChainIdBySessionAsync(AuthSessionId sessionId, CancellationToken ct = default); Task> GetChainsByUserAsync(UserKey userKey, bool includeHistoricalRoots = false, CancellationToken ct = default); - Task GetChainByDeviceAsync(TenantKey tenant, UserKey userKey, DeviceId deviceId, CancellationToken ct = default); + Task GetChainByDeviceAsync(UserKey userKey, DeviceId deviceId, CancellationToken ct = default); Task> GetChainsByRootAsync(SessionRootId rootId, CancellationToken ct = default); Task> GetSessionsByChainAsync(SessionChainId chainId, CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Core/AssemblyVisibility.cs b/src/CodeBeam.UltimateAuth.Core/AssemblyVisibility.cs index 2f9b3da7..a7a9e51d 100644 --- a/src/CodeBeam.UltimateAuth.Core/AssemblyVisibility.cs +++ b/src/CodeBeam.UltimateAuth.Core/AssemblyVisibility.cs @@ -6,4 +6,5 @@ [assembly: InternalsVisibleTo("CodeBeam.UltimateAuth.Users.EntityFrameworkCore")] [assembly: InternalsVisibleTo("CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore")] [assembly: InternalsVisibleTo("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore")] +[assembly: InternalsVisibleTo("CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore")] [assembly: InternalsVisibleTo("CodeBeam.UltimateAuth.Tests.Unit")] diff --git a/src/CodeBeam.UltimateAuth.Core/CodeBeam.UltimateAuth.Core.csproj b/src/CodeBeam.UltimateAuth.Core/CodeBeam.UltimateAuth.Core.csproj index 4e15f94d..56516403 100644 --- a/src/CodeBeam.UltimateAuth.Core/CodeBeam.UltimateAuth.Core.csproj +++ b/src/CodeBeam.UltimateAuth.Core/CodeBeam.UltimateAuth.Core.csproj @@ -4,16 +4,35 @@ net8.0;net9.0;net10.0 enable enable - 0.0.1 - 0.0.1 - true $(NoWarn);1591 + + CodeBeam.UltimateAuth.Core + 0.1.0-preview.1 + + CodeBeam + CodeBeam + Core domain primitives and abstractions for UltimateAuth. This package is not intended to be installed directly in most applications. Use CodeBeam.UltimateAuth.Server or Client packages instead. + authentication;authorization;identity;security;oauth;login;session;auth;refresh-token;pkce;dotnet;aspnetcore;blazor;maui;auth-framework + README.md + https://github.com/CodeBeamOrg/UltimateAuth + https://github.com/CodeBeamOrg/UltimateAuth + Apache-2.0 + + true + true + snupkg + + + + + + + + - - - +
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Device/DeviceContext.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Device/DeviceContext.cs index f3d3049c..4c00e82c 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Device/DeviceContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Device/DeviceContext.cs @@ -1,5 +1,9 @@ -namespace CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Infrastructure; +using System.Text.Json.Serialization; +namespace CodeBeam.UltimateAuth.Core.Domain; + +[JsonConverter(typeof(DeviceContextJsonConverter))] public sealed class DeviceContext { public DeviceId? DeviceId { get; init; } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Device/DeviceId.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Device/DeviceId.cs index 7da55a17..18800f17 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Device/DeviceId.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Device/DeviceId.cs @@ -1,7 +1,10 @@ -using System.Security; +using CodeBeam.UltimateAuth.Core.Infrastructure; +using System.Security; +using System.Text.Json.Serialization; namespace CodeBeam.UltimateAuth.Core.Domain; +[JsonConverter(typeof(DeviceIdJsonConverter))] public readonly record struct DeviceId { public const int MinLength = 16; diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Security/AuthenticationSecurityState.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Security/AuthenticationSecurityState.cs index 501db9ae..b3cf195e 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Security/AuthenticationSecurityState.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Security/AuthenticationSecurityState.cs @@ -1,11 +1,12 @@ -using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.Errors; using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Core.Security; // TODO: Do not store reset token hash in db. -public sealed class AuthenticationSecurityState +public sealed class AuthenticationSecurityState : ITenantEntity, IVersionedEntity, IEntitySnapshot { public Guid Id { get; } public TenantKey Tenant { get; } @@ -29,6 +30,13 @@ public sealed class AuthenticationSecurityState public bool IsLocked(DateTimeOffset now) => LockedUntil.HasValue && LockedUntil > now; public bool HasResetRequest => ResetRequestedAt is not null; + + long IVersionedEntity.Version + { + get => SecurityVersion; + set => throw new NotSupportedException("AuthenticationSecurityState uses SecurityVersion."); + } + private AuthenticationSecurityState( Guid id, TenantKey tenant, @@ -425,4 +433,60 @@ public AuthenticationSecurityState ClearReset() 0, securityVersion: SecurityVersion + 1); } + + public AuthenticationSecurityState Snapshot() + { + return new AuthenticationSecurityState( + id: Id, + tenant: Tenant, + userKey: UserKey, + scope: Scope, + credentialType: CredentialType, + failedAttempts: FailedAttempts, + lastFailedAt: LastFailedAt, + lockedUntil: LockedUntil, + requiresReauthentication: RequiresReauthentication, + resetRequestedAt: ResetRequestedAt, + resetExpiresAt: ResetExpiresAt, + resetConsumedAt: ResetConsumedAt, + resetTokenHash: ResetTokenHash, + resetAttempts: ResetAttempts, + securityVersion: SecurityVersion + ); + } + + public static AuthenticationSecurityState FromProjection( + Guid id, + TenantKey tenant, + UserKey userKey, + AuthenticationSecurityScope scope, + CredentialType? credentialType, + int failedAttempts, + DateTimeOffset? lastFailedAt, + DateTimeOffset? lockedUntil, + bool requiresReauthentication, + DateTimeOffset? resetRequestedAt, + DateTimeOffset? resetExpiresAt, + DateTimeOffset? resetConsumedAt, + string? resetTokenHash, + int resetAttempts, + long securityVersion) + { + return new AuthenticationSecurityState( + id, + tenant, + userKey, + scope, + credentialType, + failedAttempts, + lastFailedAt, + lockedUntil, + requiresReauthentication, + resetRequestedAt, + resetExpiresAt, + resetConsumedAt, + resetTokenHash, + resetAttempts, + securityVersion); + } } diff --git a/src/CodeBeam.UltimateAuth.Core/Extensions/ServiceCollectionExtensions.cs b/src/CodeBeam.UltimateAuth.Core/Extensions/ServiceCollectionExtensions.cs index ea8e2fea..779222b7 100644 --- a/src/CodeBeam.UltimateAuth.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Core/Extensions/ServiceCollectionExtensions.cs @@ -74,7 +74,6 @@ private static IServiceCollection AddUltimateAuthInternal(this IServiceCollectio services.AddSingleton(); services.TryAddSingleton(); - services.TryAddSingleton(); return services; } diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Converters/DeviceContextJsonConverter.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Converters/DeviceContextJsonConverter.cs new file mode 100644 index 00000000..e0e8b013 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Converters/DeviceContextJsonConverter.cs @@ -0,0 +1,65 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using CodeBeam.UltimateAuth.Core.Domain; + +namespace CodeBeam.UltimateAuth.Core.Infrastructure; + +public sealed class DeviceContextJsonConverter : JsonConverter +{ + public override DeviceContext Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + throw new JsonException("DeviceContext must be an object."); + + using var doc = JsonDocument.ParseValue(ref reader); + var root = doc.RootElement; + + // DeviceId + DeviceId? deviceId = null; + if (root.TryGetProperty("deviceId", out var deviceIdProp)) + { + var raw = deviceIdProp.GetString(); + + if (!string.IsNullOrWhiteSpace(raw)) + { + if (!DeviceId.TryCreate(raw, out var parsed)) + throw new JsonException("Invalid DeviceId"); + + deviceId = parsed; + } + } + + string? deviceType = root.TryGetProperty("deviceType", out var dt) ? dt.GetString() : null; + string? platform = root.TryGetProperty("platform", out var pf) ? pf.GetString() : null; + string? os = root.TryGetProperty("operatingSystem", out var osProp) ? osProp.GetString() : null; + string? browser = root.TryGetProperty("browser", out var br) ? br.GetString() : null; + string? ip = root.TryGetProperty("ipAddress", out var ipProp) ? ipProp.GetString() : null; + + if (deviceId is not DeviceId resolvedDeviceId) + return DeviceContext.Anonymous(); + + return DeviceContext.Create( + resolvedDeviceId, + deviceType, + platform, + os, + browser, + ip); + } + + public override void Write(Utf8JsonWriter writer, DeviceContext value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + if (value.DeviceId is not null) + writer.WriteString("deviceId", (string)value.DeviceId); + + writer.WriteString("deviceType", value.DeviceType); + writer.WriteString("platform", value.Platform); + writer.WriteString("operatingSystem", value.OperatingSystem); + writer.WriteString("browser", value.Browser); + writer.WriteString("ipAddress", value.IpAddress); + + writer.WriteEndObject(); + } +} \ No newline at end of file diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Converters/DeviceIdJsonConverter.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Converters/DeviceIdJsonConverter.cs new file mode 100644 index 00000000..95299f6c --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Converters/DeviceIdJsonConverter.cs @@ -0,0 +1,23 @@ +using CodeBeam.UltimateAuth.Core.Domain; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace CodeBeam.UltimateAuth.Core.Infrastructure; + +public sealed class DeviceIdJsonConverter : JsonConverter +{ + public override DeviceId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + + if (!DeviceId.TryCreate(value, out var id)) + throw new JsonException("Invalid DeviceId"); + + return id; + } + + public override void Write(Utf8JsonWriter writer, DeviceId value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.Value); + } +} diff --git a/src/CodeBeam.UltimateAuth.Core/README.md b/src/CodeBeam.UltimateAuth.Core/README.md new file mode 100644 index 00000000..47472f1f --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Core/README.md @@ -0,0 +1,14 @@ +# UltimateAuth Core + +Core domain primitives and abstractions for UltimateAuth. + +⚠️ This package is not intended to be installed directly in most applications. + +## Use instead + +- CodeBeam.UltimateAuth.Server (Main backend package) +- CodeBeam.UltimateAuth.Client (Main client package) + +This package is included transitively by higher-level UltimateAuth packages. + +###### Look at https://github.com/CodeBeamOrg/UltimateAuth for installation details. \ No newline at end of file diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AccessContextFactory.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AccessContextFactory.cs index df660fde..762e02b1 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AccessContextFactory.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AccessContextFactory.cs @@ -9,12 +9,12 @@ namespace CodeBeam.UltimateAuth.Server.Auth; internal sealed class AccessContextFactory : IAccessContextFactory { - private readonly IUserRoleStore _roleStore; + private readonly IUserRoleStoreFactory _userRoleFactory; private readonly IUserIdConverterResolver _converterResolver; - public AccessContextFactory(IUserRoleStore roleStore, IUserIdConverterResolver converterResolver) + public AccessContextFactory(IUserRoleStoreFactory userRoleFactory, IUserIdConverterResolver converterResolver) { - _roleStore = roleStore; + _userRoleFactory = userRoleFactory; _converterResolver = converterResolver; } @@ -45,7 +45,8 @@ private async Task CreateInternalAsync(AuthFlowContext authFlow, if (authFlow.IsAuthenticated && authFlow.UserKey is not null) { - var assignments = await _roleStore.GetAssignmentsAsync(authFlow.Tenant, authFlow.UserKey.Value, ct); + var userRoleStore = _userRoleFactory.Create(authFlow.Tenant); + var assignments = await userRoleStore.GetAssignmentsAsync(authFlow.UserKey.Value, ct); var roleIds = assignments.Select(x => x.RoleId).ToArray(); attrs["roles"] = roleIds; diff --git a/src/CodeBeam.UltimateAuth.Server/Authentication/AuthenticationSecurityManager.cs b/src/CodeBeam.UltimateAuth.Server/Authentication/AuthenticationSecurityManager.cs index 5c531ec4..59de49b3 100644 --- a/src/CodeBeam.UltimateAuth.Server/Authentication/AuthenticationSecurityManager.cs +++ b/src/CodeBeam.UltimateAuth.Server/Authentication/AuthenticationSecurityManager.cs @@ -1,6 +1,5 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.Errors; using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Core.Security; using CodeBeam.UltimateAuth.Server.Options; @@ -10,12 +9,12 @@ namespace CodeBeam.UltimateAuth.Server.Security; internal sealed class AuthenticationSecurityManager : IAuthenticationSecurityManager { - private readonly IAuthenticationSecurityStateStore _store; + private readonly IAuthenticationSecurityStateStoreFactory _storeFactory; private readonly UAuthServerOptions _options; - public AuthenticationSecurityManager(IAuthenticationSecurityStateStore store, IOptions options) + public AuthenticationSecurityManager(IAuthenticationSecurityStateStoreFactory storeFactory, IOptions options) { - _store = store; + _storeFactory = storeFactory; _options = options.Value; } @@ -23,13 +22,14 @@ public async Task GetOrCreateAccountAsync(TenantKey { ct.ThrowIfCancellationRequested(); - var state = await _store.GetAsync(tenant, userKey, AuthenticationSecurityScope.Account, credentialType: null, ct); + var store = _storeFactory.Create(tenant); + var state = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, credentialType: null, ct); if (state is not null) return state; var created = AuthenticationSecurityState.CreateAccount(tenant, userKey); - await _store.AddAsync(created, ct); + await store.AddAsync(created, ct); return created; } @@ -37,25 +37,28 @@ public async Task GetOrCreateFactorAsync(TenantKey { ct.ThrowIfCancellationRequested(); - var state = await _store.GetAsync(tenant, userKey, AuthenticationSecurityScope.Factor, type, ct); + var store = _storeFactory.Create(tenant); + var state = await store.GetAsync(userKey, AuthenticationSecurityScope.Factor, type, ct); if (state is not null) return state; var created = AuthenticationSecurityState.CreateFactor(tenant, userKey, type); - await _store.AddAsync(created, ct); + await store.AddAsync(created, ct); return created; } public Task UpdateAsync(AuthenticationSecurityState updated, long expectedVersion, CancellationToken ct = default) { - return _store.UpdateAsync(updated, expectedVersion, ct); + var store = _storeFactory.Create(updated.Tenant); + return store.UpdateAsync(updated, expectedVersion, ct); } public Task DeleteAsync(TenantKey tenant, UserKey userKey, AuthenticationSecurityScope scope, CredentialType? credentialType, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - return _store.DeleteAsync(tenant, userKey, scope, credentialType, ct); + var store = _storeFactory.Create(tenant); + return store.DeleteAsync(userKey, scope, credentialType, ct); } } diff --git a/src/CodeBeam.UltimateAuth.Server/CodeBeam.UltimateAuth.Server.csproj b/src/CodeBeam.UltimateAuth.Server/CodeBeam.UltimateAuth.Server.csproj index 6f06bab2..df25028f 100644 --- a/src/CodeBeam.UltimateAuth.Server/CodeBeam.UltimateAuth.Server.csproj +++ b/src/CodeBeam.UltimateAuth.Server/CodeBeam.UltimateAuth.Server.csproj @@ -4,12 +4,36 @@ net8.0;net9.0;net10.0 enable enable - 0.0.1 - 0.0.1 - true $(NoWarn);1591 + + CodeBeam.UltimateAuth.Server + 0.1.0-preview.1 + + CodeBeam + CodeBeam + + + Main server package for UltimateAuth. + Includes authentication, authorization, users, credentials, sessions, tokens and policy modules with dependency injection setup. + This is the primary package to install for backend applications. + + + authentication;authorization;identity;security;server;oauth;pkce;jwt;aspnetcore;auth-framework + + README.md + https://github.com/CodeBeamOrg/UltimateAuth + https://github.com/CodeBeamOrg/UltimateAuth + Apache-2.0 + + true + true + snupkg + + true + + @@ -19,14 +43,15 @@ - - - + - - + + + + +
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs index fd21d395..4387daee 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs @@ -1,5 +1,4 @@ using CodeBeam.UltimateAuth.Authorization.Contracts; -using CodeBeam.UltimateAuth.Authorization.Domain; using CodeBeam.UltimateAuth.Core.Defaults; using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Server.Auth; diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs index b4646a45..4778a004 100644 --- a/src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs @@ -33,6 +33,7 @@ using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using CodeBeam.UltimateAuth.Users; +using CodeBeam.UltimateAuth.Server.ResourceApi; namespace CodeBeam.UltimateAuth.Server.Extensions; @@ -66,6 +67,32 @@ public static IServiceCollection AddUltimateAuthServer(this IServiceCollection s return services; } + public static IServiceCollection AddUltimateAuthResourceApi(this IServiceCollection services, Action? configure = null) + { + ArgumentNullException.ThrowIfNull(services); + services.AddUltimateAuth(); + + //AddUsersInternal(services); + //AddCredentialsInternal(services); + //AddAuthorizationInternal(services); + //AddUltimateAuthPolicies(services); + + services.AddOptions() + .Configure(options => + { + configure?.Invoke(options); + }) + .BindConfiguration("UltimateAuth:Server") + .PostConfigure(options => + { + options.Endpoints.Authentication = false; + }); + + services.AddUltimateAuthResourceInternal(); + + return services; + } + private static IServiceCollection AddUltimateAuthServerInternal(this IServiceCollection services) { services.AddSingleton(); @@ -215,6 +242,8 @@ private static IServiceCollection AddUltimateAuthServerInternal(this IServiceCol services.TryAddScoped(); services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddScoped(); // ----------------------------- @@ -321,6 +350,49 @@ internal static IServiceCollection AddAuthorizationInternal(IServiceCollection s services.TryAddScoped(typeof(IUserClaimsProvider), typeof(AuthorizationClaimsProvider)); return services; } + + // TODO: This is not true, need to build true pipeline for ResourceApi. + private static IServiceCollection AddUltimateAuthResourceInternal(this IServiceCollection services) + { + services.AddSingleton(); + + services.TryAddScoped(); + services.TryAddScoped(); + + services.TryAddScoped(typeof(IUserAccessor), typeof(UAuthUserAccessor)); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + + services.TryAddScoped(); + services.TryAddScoped(); + + services.TryAddScoped(); + + services.TryAddSingleton(); + + services.AddHttpContextAccessor(); + services.AddAuthentication(); + + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddScoped(); + services.TryAddSingleton(); + + services.Replace(ServiceDescriptor.Scoped()); + services.Replace(ServiceDescriptor.Scoped()); + + services.PostConfigureAll(options => + { + options.DefaultAuthenticateScheme ??= UAuthConstants.SchemeDefaults.GlobalScheme; + }); + + return services; + } } internal sealed class NullTenantResolver : ITenantIdResolver diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthRazorExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthRazorExtensions.cs index 22d91d86..d837ebfa 100644 --- a/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthRazorExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthRazorExtensions.cs @@ -5,7 +5,7 @@ namespace CodeBeam.UltimateAuth.Server.Extensions; public static class UAuthRazorExtensions { - public static RazorComponentsEndpointConventionBuilder AddUltimateAuthClientRoutes(this RazorComponentsEndpointConventionBuilder builder,Assembly clientAssembly) + public static RazorComponentsEndpointConventionBuilder AddUltimateAuthRoutes(this RazorComponentsEndpointConventionBuilder builder, Assembly[] clientAssembly) { return builder.AddAdditionalAssemblies(clientAssembly); } diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginOrchestrator.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginOrchestrator.cs index e3b1ba17..c34d264b 100644 --- a/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginOrchestrator.cs +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginOrchestrator.cs @@ -119,12 +119,12 @@ public async Task LoginAsync(AuthFlowContext flow, LoginRequest req } // TODO: Add create-time uniqueness guard for chain id for concurrency - var kernel = _storeFactory.Create(request.Tenant); + var sessionStore = _storeFactory.Create(request.Tenant); SessionChainId? chainId = null; if (userKey is not null) { - var chain = await kernel.GetChainByDeviceAsync(request.Tenant, userKey.Value, deviceId, ct); + var chain = await sessionStore.GetChainByDeviceAsync(userKey.Value, deviceId, ct); if (chain is not null && !chain.IsRevoked) chainId = chain.ChainId; diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UserAccessorBridge.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UserAccessorBridge.cs index 2bacd92e..17f95cb2 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UserAccessorBridge.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UserAccessorBridge.cs @@ -18,5 +18,4 @@ public async Task ResolveAsync(HttpContext context) var accessor = _services.GetRequiredService>(); await accessor.ResolveAsync(context); } - } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserCreateValidator.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserCreateValidator.cs index fdbc5c37..b3441fbe 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserCreateValidator.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserCreateValidator.cs @@ -1,5 +1,6 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Users.Contracts; +using CodeBeam.UltimateAuth.Users; namespace CodeBeam.UltimateAuth.Server.Infrastructure; diff --git a/src/CodeBeam.UltimateAuth.Server/README.md b/src/CodeBeam.UltimateAuth.Server/README.md new file mode 100644 index 00000000..6b7d5314 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/README.md @@ -0,0 +1,25 @@ +# UltimateAuth Server + +The main backend package for UltimateAuth. + +## What this package includes + +- Authentication core +- Users module +- Credentials module +- Authorization (roles & permissions) +- Policies (authorization logic) + +## Notes + +This package automatically includes all required core modules. + +You do NOT need to install individual packages like: + +- Core +- Users +- Credentials +- Authorization +- Policies + +unless you are building custom integrations. (But still need reference and persistence packages) \ No newline at end of file diff --git a/src/CodeBeam.UltimateAuth.Server/ResourceApi/AllowAllAccessPolicyProvider.cs b/src/CodeBeam.UltimateAuth.Server/ResourceApi/AllowAllAccessPolicyProvider.cs new file mode 100644 index 00000000..b6f55185 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/ResourceApi/AllowAllAccessPolicyProvider.cs @@ -0,0 +1,33 @@ +using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Policies.Abstractions; + +namespace CodeBeam.UltimateAuth.Server.ResourceApi +{ + internal sealed class AllowAllAccessPolicyProvider : IAccessPolicyProvider + { + public IReadOnlyCollection GetPolicies(AccessContext context) + { + throw new NotSupportedException(); + } + + public Task ResolveAsync(string name, CancellationToken ct = default) + => Task.FromResult(new AllowAllPolicy()); + } + + internal sealed class AllowAllPolicy : IAccessPolicy + { + public bool AppliesTo(AccessContext context) + { + throw new NotImplementedException(); + } + + public AccessDecision Decide(AccessContext context) + { + throw new NotImplementedException(); + } + + public Task EvaluateAsync(AccessContext context, CancellationToken ct = default) + => Task.FromResult(true); + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/ResourceApi/NoOpIdentifierValidator.cs b/src/CodeBeam.UltimateAuth.Server/ResourceApi/NoOpIdentifierValidator.cs new file mode 100644 index 00000000..de7ff5df --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/ResourceApi/NoOpIdentifierValidator.cs @@ -0,0 +1,13 @@ +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Server.Infrastructure; +using CodeBeam.UltimateAuth.Users.Contracts; + +namespace CodeBeam.UltimateAuth.Server.ResourceApi; + +internal class NoOpIdentifierValidator : IIdentifierValidator +{ + public Task ValidateAsync(AccessContext context, UserIdentifierInfo identifier, CancellationToken ct = default) + { + throw new NotImplementedException(); + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/ResourceApi/NoOpRefreshTokenValidator.cs b/src/CodeBeam.UltimateAuth.Server/ResourceApi/NoOpRefreshTokenValidator.cs new file mode 100644 index 00000000..9b173787 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/ResourceApi/NoOpRefreshTokenValidator.cs @@ -0,0 +1,15 @@ +using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.Contracts; + +namespace CodeBeam.UltimateAuth.Server.ResourceApi; + +internal sealed class NoOpRefreshTokenValidator : IRefreshTokenValidator +{ + public Task ValidateAsync(RefreshTokenValidationContext context, CancellationToken ct = default) + => Task.CompletedTask; + + Task IRefreshTokenValidator.ValidateAsync(RefreshTokenValidationContext context, CancellationToken ct) + { + throw new NotImplementedException(); + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/ResourceApi/NoOpSessionValidator.cs b/src/CodeBeam.UltimateAuth.Server/ResourceApi/NoOpSessionValidator.cs new file mode 100644 index 00000000..a6b54a4a --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/ResourceApi/NoOpSessionValidator.cs @@ -0,0 +1,14 @@ +using CodeBeam.UltimateAuth.Core.Contracts; + +namespace CodeBeam.UltimateAuth.Server.ResourceApi; + +internal sealed class NoOpSessionValidator : ISessionValidator +{ + public Task ValidateSesAsync(SessionValidationContext context, CancellationToken ct = default) + => Task.CompletedTask; + + public Task ValidateSessionAsync(SessionValidationContext context, CancellationToken ct = default) + { + throw new NotSupportedException(); + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/ResourceApi/NoOpTokenHasher.cs b/src/CodeBeam.UltimateAuth.Server/ResourceApi/NoOpTokenHasher.cs new file mode 100644 index 00000000..a1f51a9f --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/ResourceApi/NoOpTokenHasher.cs @@ -0,0 +1,9 @@ +using CodeBeam.UltimateAuth.Core.Abstractions; + +namespace CodeBeam.UltimateAuth.Server.ResourceApi; + +internal sealed class NoOpTokenHasher : ITokenHasher +{ + public string Hash(string input) => input; + public bool Verify(string input, string hash) => input == hash; +} diff --git a/src/CodeBeam.UltimateAuth.Server/ResourceApi/NoOpUserClaimsProvider.cs b/src/CodeBeam.UltimateAuth.Server/ResourceApi/NoOpUserClaimsProvider.cs new file mode 100644 index 00000000..4d7e0d92 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/ResourceApi/NoOpUserClaimsProvider.cs @@ -0,0 +1,17 @@ +using CodeBeam.UltimateAuth.Core; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using System.Security.Claims; + +namespace CodeBeam.UltimateAuth.Server.ResourceApi; + +internal sealed class NoOpUserClaimsProvider : IUserClaimsProvider +{ + public Task> GetClaimsAsync(TenantKey tenant, UserKey user, CancellationToken ct = default) + => Task.FromResult>(Array.Empty()); + + Task IUserClaimsProvider.GetClaimsAsync(TenantKey tenant, UserKey userKey, CancellationToken ct) + { + return Task.FromResult(ClaimsSnapshot.Empty); + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/ResourceApi/NotSupportedPasswordHasher.cs b/src/CodeBeam.UltimateAuth.Server/ResourceApi/NotSupportedPasswordHasher.cs new file mode 100644 index 00000000..b2f94991 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/ResourceApi/NotSupportedPasswordHasher.cs @@ -0,0 +1,16 @@ +using CodeBeam.UltimateAuth.Core.Abstractions; + +namespace CodeBeam.UltimateAuth.Server.ResourceApi; + +internal class NotSupportedPasswordHasher : IUAuthPasswordHasher +{ + public string Hash(string password) + { + throw new NotSupportedException(); + } + + public bool Verify(string hash, string secret) + { + throw new NotSupportedException(); + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/ResourceApi/NotSupportedRefreshTokenStoreFactory.cs b/src/CodeBeam.UltimateAuth.Server/ResourceApi/NotSupportedRefreshTokenStoreFactory.cs new file mode 100644 index 00000000..1de3a14d --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/ResourceApi/NotSupportedRefreshTokenStoreFactory.cs @@ -0,0 +1,12 @@ +using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Server.ResourceApi; + +internal class NotSupportedRefreshTokenStoreFactory : IRefreshTokenStoreFactory +{ + public IRefreshTokenStore Create(TenantKey tenant) + { + throw new NotSupportedException(); + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/ResourceApi/NotSupportedSessionStoreFactory.cs b/src/CodeBeam.UltimateAuth.Server/ResourceApi/NotSupportedSessionStoreFactory.cs new file mode 100644 index 00000000..3f5db1ca --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/ResourceApi/NotSupportedSessionStoreFactory.cs @@ -0,0 +1,12 @@ +using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Server.ResourceApi; + +internal class NotSupportedSessionStoreFactory : ISessionStoreFactory +{ + public ISessionStore Create(TenantKey tenant) + { + throw new NotSupportedException(); + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/ResourceApi/NotSupportedUserRoleStoreFactory.cs b/src/CodeBeam.UltimateAuth.Server/ResourceApi/NotSupportedUserRoleStoreFactory.cs new file mode 100644 index 00000000..33b75cdb --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/ResourceApi/NotSupportedUserRoleStoreFactory.cs @@ -0,0 +1,12 @@ +using CodeBeam.UltimateAuth.Authorization; +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Server.ResourceApi; + +internal class NotSupportedUserRoleStoreFactory : IUserRoleStoreFactory +{ + public IUserRoleStore Create(TenantKey tenant) + { + throw new NotSupportedException(); + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Runtime/ResourceRuntimeMarker.cs b/src/CodeBeam.UltimateAuth.Server/Runtime/ResourceRuntimeMarker.cs new file mode 100644 index 00000000..c6ba922d --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Runtime/ResourceRuntimeMarker.cs @@ -0,0 +1,7 @@ +using CodeBeam.UltimateAuth.Core.Runtime; + +namespace CodeBeam.UltimateAuth.Server.Runtime; + +internal class ResourceRuntimeMarker : IUAuthRuntimeMarker +{ +} diff --git a/src/CodeBeam.UltimateAuth.Server/Stores/AspNetIdentityUserStore.cs b/src/CodeBeam.UltimateAuth.Server/Stores/AspNetIdentityUserStore.cs deleted file mode 100644 index 51b261f7..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Stores/AspNetIdentityUserStore.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace CodeBeam.UltimateAuth.Server.Stores; - -public sealed class AspNetIdentityUserStore // : IUAuthUserStore -{ - //private readonly UserManager _users; - - //public AspNetIdentityUserStore(UserManager users) - //{ - // _users = users; - //} - - //public async Task?> FindByUsernameAsync( - // string? tenantId, - // string username, - // CancellationToken cancellationToken = default) - //{ - // var user = await _users.FindByNameAsync(username); - // if (user is null) - // return null; - - // var claims = await _users.GetClaimsAsync(user); - - // return new UAuthUserRecord - // { - // UserId = user.Id, - // Username = user.UserName!, - // PasswordHash = user.PasswordHash!, - // Claims = ClaimsSnapshot.From( - // claims.Select(c => (c.Type, c.Value)).ToArray()) - // }; - //} -} diff --git a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/AssemblyVisibility.cs b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/AssemblyVisibility.cs new file mode 100644 index 00000000..ed166fcc --- /dev/null +++ b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/AssemblyVisibility.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("CodeBeam.UltimateAuth.Tests.Unit")] diff --git a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.csproj b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.csproj new file mode 100644 index 00000000..475e5660 --- /dev/null +++ b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.csproj @@ -0,0 +1,16 @@ + + + + net8.0;net9.0;net10.0 + enable + enable + true + $(NoWarn);1591 + + + + + + + + diff --git a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Data/UAuthAuthenticationDbContext.cs b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Data/UAuthAuthenticationDbContext.cs new file mode 100644 index 00000000..c2ff82b3 --- /dev/null +++ b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Data/UAuthAuthenticationDbContext.cs @@ -0,0 +1,78 @@ +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using Microsoft.EntityFrameworkCore; + +namespace CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore; + +internal sealed class UAuthAuthenticationDbContext : DbContext +{ + public DbSet AuthenticationSecurityStates => Set(); + + + public UAuthAuthenticationDbContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder b) + { + ConfigureAuthenticationSecurityState(b); + } + + private void ConfigureAuthenticationSecurityState(ModelBuilder b) + { + b.Entity(e => + { + e.ToTable("UAuth_Authentication"); + e.HasKey(x => x.Id); + + e.Property(x => x.SecurityVersion).IsConcurrencyToken(); + + e.Property(x => x.Tenant) + .HasConversion( + v => v.Value, + v => TenantKey.FromInternal(v)) + .HasMaxLength(128) + .IsRequired(); + + e.Property(x => x.UserKey) + .HasConversion( + v => v.Value, + v => UserKey.FromString(v)) + .HasMaxLength(128) + .IsRequired(); + + e.Property(x => x.Scope) + .IsRequired(); + + e.Property(x => x.CredentialType); + + e.Property(x => x.FailedAttempts) + .IsRequired(); + + e.Property(x => x.LastFailedAt); + + e.Property(x => x.LockedUntil); + + e.Property(x => x.RequiresReauthentication) + .IsRequired(); + + e.Property(x => x.ResetRequestedAt); + e.Property(x => x.ResetExpiresAt); + e.Property(x => x.ResetConsumedAt); + + e.Property(x => x.ResetTokenHash) + .HasMaxLength(512); + + e.Property(x => x.ResetAttempts) + .IsRequired(); + + e.HasIndex(x => new { x.Tenant, x.UserKey, x.Scope, x.CredentialType }).IsUnique(); + e.HasIndex(x => new { x.Tenant, x.UserKey }); + e.HasIndex(x => new { x.Tenant, x.LockedUntil }); + e.HasIndex(x => new { x.Tenant, x.ResetRequestedAt }); + e.HasIndex(x => new { x.Tenant, x.UserKey, x.Scope }); + + }); + } +} diff --git a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..956212fa --- /dev/null +++ b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,15 @@ +using CodeBeam.UltimateAuth.Core.Abstractions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddUltimateAuthAuthenticationEntityFrameworkCore(this IServiceCollection services, Action configureDb) + { + services.AddDbContext(configureDb); + services.AddScoped(); + return services; + } +} diff --git a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Mappers/AuthenticationSecurityStateMapper.cs b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Mappers/AuthenticationSecurityStateMapper.cs new file mode 100644 index 00000000..1de60842 --- /dev/null +++ b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Mappers/AuthenticationSecurityStateMapper.cs @@ -0,0 +1,65 @@ +using CodeBeam.UltimateAuth.Core.Security; + +namespace CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore; + +internal static class AuthenticationSecurityStateMapper +{ + public static AuthenticationSecurityState ToDomain(AuthenticationSecurityStateProjection p) + { + return AuthenticationSecurityState.FromProjection( + p.Id, + p.Tenant, + p.UserKey, + p.Scope, + p.CredentialType, + p.FailedAttempts, + p.LastFailedAt, + p.LockedUntil, + p.RequiresReauthentication, + p.ResetRequestedAt, + p.ResetExpiresAt, + p.ResetConsumedAt, + p.ResetTokenHash, + p.ResetAttempts, + p.SecurityVersion); + } + + public static AuthenticationSecurityStateProjection ToProjection(AuthenticationSecurityState d) + { + return new AuthenticationSecurityStateProjection + { + Id = d.Id, + Tenant = d.Tenant, + UserKey = d.UserKey, + Scope = d.Scope, + CredentialType = d.CredentialType, + FailedAttempts = d.FailedAttempts, + LastFailedAt = d.LastFailedAt, + LockedUntil = d.LockedUntil, + RequiresReauthentication = d.RequiresReauthentication, + ResetRequestedAt = d.ResetRequestedAt, + ResetExpiresAt = d.ResetExpiresAt, + ResetConsumedAt = d.ResetConsumedAt, + ResetTokenHash = d.ResetTokenHash, + ResetAttempts = d.ResetAttempts, + SecurityVersion = d.SecurityVersion + }; + } + + public static void UpdateProjection(AuthenticationSecurityState d, AuthenticationSecurityStateProjection p) + { + p.FailedAttempts = d.FailedAttempts; + p.LastFailedAt = d.LastFailedAt; + p.LockedUntil = d.LockedUntil; + p.RequiresReauthentication = d.RequiresReauthentication; + + p.ResetRequestedAt = d.ResetRequestedAt; + p.ResetExpiresAt = d.ResetExpiresAt; + p.ResetConsumedAt = d.ResetConsumedAt; + p.ResetTokenHash = d.ResetTokenHash; + p.ResetAttempts = d.ResetAttempts; + + p.SecurityVersion = d.SecurityVersion; + } +} + diff --git a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Projections/AuthenticationSecutiryStateProjection.cs b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Projections/AuthenticationSecutiryStateProjection.cs new file mode 100644 index 00000000..fc9740ad --- /dev/null +++ b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Projections/AuthenticationSecutiryStateProjection.cs @@ -0,0 +1,28 @@ +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore; + +internal sealed class AuthenticationSecurityStateProjection +{ + public Guid Id { get; set; } + + public TenantKey Tenant { get; set; } + public UserKey UserKey { get; set; } + + public AuthenticationSecurityScope Scope { get; set; } + public CredentialType? CredentialType { get; set; } + + public int FailedAttempts { get; set; } + public DateTimeOffset? LastFailedAt { get; set; } + public DateTimeOffset? LockedUntil { get; set; } + public bool RequiresReauthentication { get; set; } + + public DateTimeOffset? ResetRequestedAt { get; set; } + public DateTimeOffset? ResetExpiresAt { get; set; } + public DateTimeOffset? ResetConsumedAt { get; set; } + public string? ResetTokenHash { get; set; } + public int ResetAttempts { get; set; } + + public long SecurityVersion { get; set; } +} diff --git a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Stores/EfCoreAuthenticationSecurityStateStore.cs b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Stores/EfCoreAuthenticationSecurityStateStore.cs new file mode 100644 index 00000000..ac247165 --- /dev/null +++ b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Stores/EfCoreAuthenticationSecurityStateStore.cs @@ -0,0 +1,82 @@ +using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Errors; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Core.Security; +using Microsoft.EntityFrameworkCore; + +namespace CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore; + +internal sealed class EfCoreAuthenticationSecurityStateStore : IAuthenticationSecurityStateStore +{ + private readonly UAuthAuthenticationDbContext _db; + private readonly TenantKey _tenant; + + public EfCoreAuthenticationSecurityStateStore(UAuthAuthenticationDbContext db, TenantContext tenant) + { + _db = db; + _tenant = tenant.Tenant; + } + + public async Task GetAsync(UserKey userKey, AuthenticationSecurityScope scope, CredentialType? credentialType, CancellationToken ct = default) + { + var entity = await _db.AuthenticationSecurityStates + .AsNoTracking() + .SingleOrDefaultAsync(x => + x.Tenant == _tenant && + x.UserKey == userKey && + x.Scope == scope && + x.CredentialType == credentialType, + ct); + + return entity is null + ? null + : AuthenticationSecurityStateMapper.ToDomain(entity); + } + + public async Task AddAsync(AuthenticationSecurityState state, CancellationToken ct = default) + { + var entity = AuthenticationSecurityStateMapper.ToProjection(state); + + _db.AuthenticationSecurityStates.Add(entity); + + await _db.SaveChangesAsync(ct); + } + + public async Task UpdateAsync(AuthenticationSecurityState state, long expectedVersion, CancellationToken ct = default) + { + var entity = await _db.AuthenticationSecurityStates + .SingleOrDefaultAsync(x => + x.Tenant == _tenant && + x.Id == state.Id, + ct); + + if (entity is null) + throw new UAuthNotFoundException("security_state_not_found"); + + if (entity.SecurityVersion != expectedVersion) + throw new UAuthConflictException("security_state_version_conflict"); + + AuthenticationSecurityStateMapper.UpdateProjection(state, entity); + + await _db.SaveChangesAsync(ct); + } + + public async Task DeleteAsync(UserKey userKey, AuthenticationSecurityScope scope, CredentialType? credentialType, CancellationToken ct = default) + { + var entity = await _db.AuthenticationSecurityStates + .SingleOrDefaultAsync(x => + x.Tenant == _tenant && + x.UserKey == userKey && + x.Scope == scope && + x.CredentialType == credentialType, + ct); + + if (entity is null) + return; + + _db.AuthenticationSecurityStates.Remove(entity); + + await _db.SaveChangesAsync(ct); + } +} diff --git a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Stores/EfCoreAuthenticationSecurityStateStoreFactory.cs b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Stores/EfCoreAuthenticationSecurityStateStoreFactory.cs new file mode 100644 index 00000000..74ab7382 --- /dev/null +++ b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Stores/EfCoreAuthenticationSecurityStateStoreFactory.cs @@ -0,0 +1,19 @@ +using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore; + +internal sealed class EfCoreAuthenticationSecurityStateStoreFactory : IAuthenticationSecurityStateStoreFactory +{ + private readonly UAuthAuthenticationDbContext _db; + + public EfCoreAuthenticationSecurityStateStoreFactory(UAuthAuthenticationDbContext db) + { + _db = db; + } + + public IAuthenticationSecurityStateStore Create(TenantKey tenant) + { + return new EfCoreAuthenticationSecurityStateStore(_db, new TenantContext(tenant)); + } +} diff --git a/src/authentication/CodeBeam.UltimateAuth.Authentication.InMemory/CodeBeam.UltimateAuth.Authentication.InMemory.csproj b/src/authentication/CodeBeam.UltimateAuth.Authentication.InMemory/CodeBeam.UltimateAuth.Authentication.InMemory.csproj index 0ad38403..50cb8637 100644 --- a/src/authentication/CodeBeam.UltimateAuth.Authentication.InMemory/CodeBeam.UltimateAuth.Authentication.InMemory.csproj +++ b/src/authentication/CodeBeam.UltimateAuth.Authentication.InMemory/CodeBeam.UltimateAuth.Authentication.InMemory.csproj @@ -11,6 +11,7 @@ +
\ No newline at end of file diff --git a/src/authentication/CodeBeam.UltimateAuth.Authentication.InMemory/InMemoryAuthenticationSecurityStateStore.cs b/src/authentication/CodeBeam.UltimateAuth.Authentication.InMemory/InMemoryAuthenticationSecurityStateStore.cs index 8c7e25ab..6a191aeb 100644 --- a/src/authentication/CodeBeam.UltimateAuth.Authentication.InMemory/InMemoryAuthenticationSecurityStateStore.cs +++ b/src/authentication/CodeBeam.UltimateAuth.Authentication.InMemory/InMemoryAuthenticationSecurityStateStore.cs @@ -9,16 +9,30 @@ namespace CodeBeam.UltimateAuth.Authentication.InMemory; internal sealed class InMemoryAuthenticationSecurityStateStore : IAuthenticationSecurityStateStore { + private readonly TenantKey _tenant; + private readonly ConcurrentDictionary _byId = new(); - private readonly ConcurrentDictionary<(TenantKey, UserKey, AuthenticationSecurityScope, CredentialType?), Guid> _index = new(); + private readonly ConcurrentDictionary<(UserKey, AuthenticationSecurityScope, CredentialType?), Guid> _index = new(); - public Task GetAsync(TenantKey tenant, UserKey userKey, AuthenticationSecurityScope scope, CredentialType? credentialType, CancellationToken ct = default) + public InMemoryAuthenticationSecurityStateStore(TenantContext tenant) + { + _tenant = tenant.Tenant; + } + + public Task GetAsync( + UserKey userKey, + AuthenticationSecurityScope scope, + CredentialType? credentialType, + CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - if (_index.TryGetValue((tenant, userKey, scope, credentialType), out var id) && _byId.TryGetValue(id, out var state)) + var key = (userKey, scope, credentialType); + + if (_index.TryGetValue(key, out var id) && + _byId.TryGetValue(id, out var state)) { - return Task.FromResult(state); + return Task.FromResult(state.Snapshot()); } return Task.FromResult(null); @@ -28,12 +42,17 @@ public Task AddAsync(AuthenticationSecurityState state, CancellationToken ct = d { ct.ThrowIfCancellationRequested(); - var key = (state.Tenant, state.UserKey, state.Scope, state.CredentialType); + if (state.Tenant != _tenant) + throw new InvalidOperationException("Tenant mismatch."); + + var key = (state.UserKey, state.Scope, state.CredentialType); if (!_index.TryAdd(key, state.Id)) throw new UAuthConflictException("security_state_already_exists"); - if (!_byId.TryAdd(state.Id, state)) + var snapshot = state.Snapshot(); + + if (!_byId.TryAdd(state.Id, snapshot)) { _index.TryRemove(key, out _); throw new UAuthConflictException("security_state_add_failed"); @@ -46,7 +65,10 @@ public Task UpdateAsync(AuthenticationSecurityState state, long expectedVersion, { ct.ThrowIfCancellationRequested(); - var key = (state.Tenant, state.UserKey, state.Scope, state.CredentialType); + if (state.Tenant != _tenant) + throw new InvalidOperationException("Tenant mismatch."); + + var key = (state.UserKey, state.Scope, state.CredentialType); if (!_index.TryGetValue(key, out var id) || id != state.Id) throw new UAuthConflictException("security_state_index_corrupted"); @@ -57,17 +79,23 @@ public Task UpdateAsync(AuthenticationSecurityState state, long expectedVersion, if (current.SecurityVersion != expectedVersion) throw new UAuthConflictException("security_state_version_conflict"); - if (!_byId.TryUpdate(state.Id, state, current)) + var next = state.Snapshot(); + + if (!_byId.TryUpdate(state.Id, next, current)) throw new UAuthConflictException("security_state_update_conflict"); return Task.CompletedTask; } - public Task DeleteAsync(TenantKey tenant, UserKey userKey, AuthenticationSecurityScope scope, CredentialType? credentialType, CancellationToken ct = default) + public Task DeleteAsync( + UserKey userKey, + AuthenticationSecurityScope scope, + CredentialType? credentialType, + CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - var key = (tenant, userKey, scope, credentialType); + var key = (userKey, scope, credentialType); if (!_index.TryRemove(key, out var id)) return Task.CompletedTask; diff --git a/src/authentication/CodeBeam.UltimateAuth.Authentication.InMemory/InMemoryAuthenticationSecurityStateStoreFactory.cs b/src/authentication/CodeBeam.UltimateAuth.Authentication.InMemory/InMemoryAuthenticationSecurityStateStoreFactory.cs new file mode 100644 index 00000000..dfc34430 --- /dev/null +++ b/src/authentication/CodeBeam.UltimateAuth.Authentication.InMemory/InMemoryAuthenticationSecurityStateStoreFactory.cs @@ -0,0 +1,15 @@ +using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using System.Collections.Concurrent; + +namespace CodeBeam.UltimateAuth.Authentication.InMemory; + +internal sealed class InMemoryAuthenticationSecurityStateStoreFactory : IAuthenticationSecurityStateStoreFactory +{ + private readonly ConcurrentDictionary _stores = new(); + + public IAuthenticationSecurityStateStore Create(TenantKey tenant) + { + return _stores.GetOrAdd(tenant, t => new InMemoryAuthenticationSecurityStateStore(new TenantContext(t))); + } +} diff --git a/src/authentication/CodeBeam.UltimateAuth.Authentication.InMemory/ServiceCollectionExtensions.cs b/src/authentication/CodeBeam.UltimateAuth.Authentication.InMemory/ServiceCollectionExtensions.cs index ab406e99..cdab2eec 100644 --- a/src/authentication/CodeBeam.UltimateAuth.Authentication.InMemory/ServiceCollectionExtensions.cs +++ b/src/authentication/CodeBeam.UltimateAuth.Authentication.InMemory/ServiceCollectionExtensions.cs @@ -1,14 +1,13 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using Microsoft.Extensions.DependencyInjection; -namespace CodeBeam.UltimateAuth.Authentication.InMemory; +namespace CodeBeam.UltimateAuth.Authentication.InMemory.Extensions; public static class ServiceCollectionExtensions { - public static IServiceCollection AddUltimateAuthInMemoryAuthenticationSecurity(this IServiceCollection services) + public static IServiceCollection AddUltimateAuthAuthenticationInMemory(this IServiceCollection services) { - services.AddSingleton(); - + services.AddSingleton(); return services; } } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/CodeBeam.UltimateAuth.Authorization.Contracts.csproj b/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/CodeBeam.UltimateAuth.Authorization.Contracts.csproj index ce41f1eb..adc62681 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/CodeBeam.UltimateAuth.Authorization.Contracts.csproj +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/CodeBeam.UltimateAuth.Authorization.Contracts.csproj @@ -4,14 +4,40 @@ net8.0;net9.0;net10.0 enable enable - 0.0.1 - 0.0.1 - true $(NoWarn);1591 + + CodeBeam.UltimateAuth.Authorization.Contracts + 0.1.0-preview.1 + + CodeBeam + CodeBeam + + + Shared contracts and cross-boundary types for UltimateAuth Authorization module. + Includes role identifiers, permission models and shared authorization data structures. + Does NOT include domain logic or persistence. + + + authentication;authorization;roles;permissions;contracts;shared;auth-framework + + README.md + https://github.com/CodeBeamOrg/UltimateAuth + https://github.com/CodeBeamOrg/UltimateAuth + Apache-2.0 + + true + true + snupkg + + + + + + - + -
+
\ No newline at end of file diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/README.md b/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/README.md new file mode 100644 index 00000000..9ed9d4ca --- /dev/null +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/README.md @@ -0,0 +1,32 @@ +# UltimateAuth Authorization Contracts + +Shared contracts and cross-boundary models for the Authorization module. + +## Purpose + +This package contains: + +- Role identifiers +- Permission models +- Authorization-related DTOs + +## Does NOT include + +- Domain logic +- Persistence +- Policy enforcement logic + +## Usage + +Used by: + +- Server implementations +- Client SDKs +- Custom authorization providers + +⚠️ Usually installed transitively via: + +- CodeBeam.UltimateAuth.Server +- CodeBeam.UltimateAuth.Client + +No need to install it directly in most scenarios. \ No newline at end of file diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.csproj b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.csproj index 8b6785ef..da14ab27 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.csproj +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.csproj @@ -11,8 +11,7 @@ - - +
diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Data/UAuthAuthorizationDbContext.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Data/UAuthAuthorizationDbContext.cs index b2f9780c..7783f457 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Data/UAuthAuthorizationDbContext.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Data/UAuthAuthorizationDbContext.cs @@ -11,12 +11,9 @@ internal sealed class UAuthAuthorizationDbContext : DbContext public DbSet RolePermissions => Set(); public DbSet UserRoles => Set(); - private readonly TenantContext _tenant; - - public UAuthAuthorizationDbContext(DbContextOptions options, TenantContext tenant) + public UAuthAuthorizationDbContext(DbContextOptions options) : base(options) { - _tenant = tenant; } protected override void OnModelCreating(ModelBuilder b) @@ -30,6 +27,7 @@ private void ConfigureRole(ModelBuilder b) { b.Entity(e => { + e.ToTable("UAuth_Roles"); e.HasKey(x => x.Id); e.Property(x => x.Version) @@ -57,12 +55,12 @@ private void ConfigureRole(ModelBuilder b) .IsRequired(); e.Property(x => x.CreatedAt) - .IsRequired(); + .HasConversion( + v => v.UtcDateTime, + v => new DateTimeOffset(v, TimeSpan.Zero)); e.HasIndex(x => new { x.Tenant, x.Id }).IsUnique(); e.HasIndex(x => new { x.Tenant, x.NormalizedName }).IsUnique(); - - e.HasQueryFilter(x => _tenant.IsGlobal || x.Tenant == _tenant.Tenant); }); } @@ -70,6 +68,7 @@ private void ConfigureRolePermission(ModelBuilder b) { b.Entity(e => { + e.ToTable("UAuth_RolePermissions"); e.HasKey(x => new { x.Tenant, x.RoleId, x.Permission }); e.Property(x => x.Tenant) @@ -91,8 +90,6 @@ private void ConfigureRolePermission(ModelBuilder b) e.HasIndex(x => new { x.Tenant, x.RoleId }); e.HasIndex(x => new { x.Tenant, x.Permission }); - - e.HasQueryFilter(x => _tenant.IsGlobal || x.Tenant == _tenant.Tenant); }); } @@ -100,6 +97,7 @@ private void ConfigureUserRole(ModelBuilder b) { b.Entity(e => { + e.ToTable("UAuth_UserRoles"); e.HasKey(x => new { x.Tenant, x.UserKey, x.RoleId }); e.Property(x => x.Tenant) @@ -127,8 +125,6 @@ private void ConfigureUserRole(ModelBuilder b) e.HasIndex(x => new { x.Tenant, x.UserKey }); e.HasIndex(x => new { x.Tenant, x.RoleId }); - - e.HasQueryFilter(x => _tenant.IsGlobal || x.Tenant == _tenant.Tenant); }); } } \ No newline at end of file diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs index 8a294a7b..b4554cd4 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs @@ -5,11 +5,11 @@ namespace CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.Extensions; public static class ServiceCollectionExtensions { - public static IServiceCollection AddUltimateAuthEntityFrameworkCoreAuthorization(this IServiceCollection services, Action configureDb) + public static IServiceCollection AddUltimateAuthAuthorizationEntityFrameworkCore(this IServiceCollection services, Action configureDb) { - services.AddDbContextPool(configureDb); - services.AddScoped(); - services.AddScoped(); + services.AddDbContext(configureDb); + services.AddScoped(); + services.AddScoped(); return services; } } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Mappers/RolePermissionMapper.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Mappers/RolePermissionMapper.cs index b4805a6a..9c640976 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Mappers/RolePermissionMapper.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Mappers/RolePermissionMapper.cs @@ -1,5 +1,4 @@ using CodeBeam.UltimateAuth.Authorization.Contracts; -using CodeBeam.UltimateAuth.Authorization.Domain; using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore; diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreRoleStore.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreRoleStore.cs index 4af2dcac..8c58de33 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreRoleStore.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreRoleStore.cs @@ -1,5 +1,4 @@ using CodeBeam.UltimateAuth.Authorization.Contracts; -using CodeBeam.UltimateAuth.Authorization.Domain; using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Errors; using CodeBeam.UltimateAuth.Core.MultiTenancy; @@ -10,17 +9,19 @@ namespace CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore; internal sealed class EfCoreRoleStore : IRoleStore { private readonly UAuthAuthorizationDbContext _db; + private readonly TenantKey _tenant; - public EfCoreRoleStore(UAuthAuthorizationDbContext db) + public EfCoreRoleStore(UAuthAuthorizationDbContext db, TenantContext tenant) { _db = db; + _tenant = tenant.Tenant; } public async Task ExistsAsync(RoleKey key, CancellationToken ct = default) { return await _db.Roles .AnyAsync(x => - x.Tenant == key.Tenant && + x.Tenant == _tenant && x.Id == key.RoleId, ct); } @@ -29,7 +30,7 @@ public async Task AddAsync(Role role, CancellationToken ct = default) { var exists = await _db.Roles .AnyAsync(x => - x.Tenant == role.Tenant && + x.Tenant == _tenant && x.NormalizedName == role.NormalizedName && x.DeletedAt == null, ct); @@ -42,7 +43,7 @@ public async Task AddAsync(Role role, CancellationToken ct = default) _db.Roles.Add(entity); var permissionEntities = role.Permissions - .Select(p => RolePermissionMapper.ToProjection(role.Tenant, role.Id, p)); + .Select(p => RolePermissionMapper.ToProjection(_tenant, role.Id, p)); _db.RolePermissions.AddRange(permissionEntities); @@ -54,28 +55,28 @@ public async Task AddAsync(Role role, CancellationToken ct = default) var entity = await _db.Roles .AsNoTracking() .SingleOrDefaultAsync(x => - x.Tenant == key.Tenant && + x.Tenant == _tenant && x.Id == key.RoleId, ct); if (entity is null) return null; - var permissionEntities = await _db.RolePermissions + var permissions = await _db.RolePermissions .AsNoTracking() .Where(x => - x.Tenant == key.Tenant && + x.Tenant == _tenant && x.RoleId == key.RoleId) .ToListAsync(ct); - return RoleMapper.ToDomain(entity, permissionEntities); + return RoleMapper.ToDomain(entity, permissions); } public async Task SaveAsync(Role role, long expectedVersion, CancellationToken ct = default) { var entity = await _db.Roles .SingleOrDefaultAsync(x => - x.Tenant == role.Tenant && + x.Tenant == _tenant && x.Id == role.Id, ct); @@ -89,7 +90,7 @@ public async Task SaveAsync(Role role, long expectedVersion, CancellationToken c { var exists = await _db.Roles .AnyAsync(x => - x.Tenant == role.Tenant && + x.Tenant == _tenant && x.NormalizedName == role.NormalizedName && x.Id != role.Id && x.DeletedAt == null, @@ -100,18 +101,21 @@ public async Task SaveAsync(Role role, long expectedVersion, CancellationToken c } RoleMapper.UpdateProjection(role, entity); - entity.Version++; var existingPermissions = await _db.RolePermissions .Where(x => - x.Tenant == role.Tenant && + x.Tenant == _tenant && x.RoleId == role.Id) .ToListAsync(ct); _db.RolePermissions.RemoveRange(existingPermissions); - var newPermissions = role.Permissions.Select(p => RolePermissionMapper.ToProjection(role.Tenant, role.Id, p)); + + var newPermissions = role.Permissions + .Select(p => RolePermissionMapper.ToProjection(_tenant, role.Id, p)); + _db.RolePermissions.AddRange(newPermissions); + await _db.SaveChangesAsync(ct); } @@ -119,7 +123,7 @@ public async Task DeleteAsync(RoleKey key, long expectedVersion, DeleteMode mode { var entity = await _db.Roles .SingleOrDefaultAsync(x => - x.Tenant == key.Tenant && + x.Tenant == _tenant && x.Id == key.RoleId, ct); @@ -131,13 +135,12 @@ public async Task DeleteAsync(RoleKey key, long expectedVersion, DeleteMode mode if (mode == DeleteMode.Hard) { - var permissions = await _db.RolePermissions + await _db.RolePermissions .Where(x => - x.Tenant == key.Tenant && + x.Tenant == _tenant && x.RoleId == key.RoleId) - .ToListAsync(ct); + .ExecuteDeleteAsync(ct); - _db.RolePermissions.RemoveRange(permissions); _db.Roles.Remove(entity); } else @@ -149,12 +152,12 @@ public async Task DeleteAsync(RoleKey key, long expectedVersion, DeleteMode mode await _db.SaveChangesAsync(ct); } - public async Task GetByNameAsync(TenantKey tenant, string normalizedName, CancellationToken ct = default) + public async Task GetByNameAsync(string normalizedName, CancellationToken ct = default) { var entity = await _db.Roles .AsNoTracking() .SingleOrDefaultAsync(x => - x.Tenant == tenant && + x.Tenant == _tenant && x.NormalizedName == normalizedName && x.DeletedAt == null, ct); @@ -162,25 +165,24 @@ public async Task DeleteAsync(RoleKey key, long expectedVersion, DeleteMode mode if (entity is null) return null; - var permissionEntities = await _db.RolePermissions + var permissions = await _db.RolePermissions .AsNoTracking() .Where(x => - x.Tenant == tenant && + x.Tenant == _tenant && x.RoleId == entity.Id) .ToListAsync(ct); - return RoleMapper.ToDomain(entity, permissionEntities); + return RoleMapper.ToDomain(entity, permissions); } public async Task> GetByIdsAsync( - TenantKey tenant, IReadOnlyCollection roleIds, CancellationToken ct = default) { var entities = await _db.Roles .AsNoTracking() .Where(x => - x.Tenant == tenant && + x.Tenant == _tenant && roleIds.Contains(x.Id)) .ToListAsync(ct); @@ -189,7 +191,7 @@ public async Task> GetByIdsAsync( var permissions = await _db.RolePermissions .AsNoTracking() .Where(x => - x.Tenant == tenant && + x.Tenant == _tenant && roleIdsSet.Contains(x.RoleId)) .ToListAsync(ct); @@ -210,7 +212,6 @@ public async Task> GetByIdsAsync( } public async Task> QueryAsync( - TenantKey tenant, RoleQuery query, CancellationToken ct = default) { @@ -218,7 +219,7 @@ public async Task> QueryAsync( var baseQuery = _db.Roles .AsNoTracking() - .Where(x => x.Tenant == tenant); + .Where(x => x.Tenant == _tenant); if (!query.IncludeDeleted) baseQuery = baseQuery.Where(x => x.DeletedAt == null); @@ -232,24 +233,16 @@ public async Task> QueryAsync( baseQuery = query.SortBy switch { nameof(Role.CreatedAt) => - query.Descending - ? baseQuery.OrderByDescending(x => x.CreatedAt) - : baseQuery.OrderBy(x => x.CreatedAt), + query.Descending ? baseQuery.OrderByDescending(x => x.CreatedAt) : baseQuery.OrderBy(x => x.CreatedAt), nameof(Role.UpdatedAt) => - query.Descending - ? baseQuery.OrderByDescending(x => x.UpdatedAt) - : baseQuery.OrderBy(x => x.UpdatedAt), + query.Descending ? baseQuery.OrderByDescending(x => x.UpdatedAt) : baseQuery.OrderBy(x => x.UpdatedAt), nameof(Role.Name) => - query.Descending - ? baseQuery.OrderByDescending(x => x.Name) - : baseQuery.OrderBy(x => x.Name), + query.Descending ? baseQuery.OrderByDescending(x => x.Name) : baseQuery.OrderBy(x => x.Name), nameof(Role.NormalizedName) => - query.Descending - ? baseQuery.OrderByDescending(x => x.NormalizedName) - : baseQuery.OrderBy(x => x.NormalizedName), + query.Descending ? baseQuery.OrderByDescending(x => x.NormalizedName) : baseQuery.OrderBy(x => x.NormalizedName), _ => baseQuery.OrderBy(x => x.CreatedAt) }; diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreRoleStoreFactory.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreRoleStoreFactory.cs new file mode 100644 index 00000000..975dc443 --- /dev/null +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreRoleStoreFactory.cs @@ -0,0 +1,18 @@ +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore; + +internal sealed class EfCoreRoleStoreFactory : IRoleStoreFactory +{ + private readonly UAuthAuthorizationDbContext _db; + + public EfCoreRoleStoreFactory(UAuthAuthorizationDbContext db) + { + _db = db; + } + + public IRoleStore Create(TenantKey tenant) + { + return new EfCoreRoleStore(_db, new TenantContext(tenant)); + } +} diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreUserRoleStore.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreUserRoleStore.cs index c9db3992..24353a98 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreUserRoleStore.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreUserRoleStore.cs @@ -9,29 +9,31 @@ namespace CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore; internal sealed class EfCoreUserRoleStore : IUserRoleStore { private readonly UAuthAuthorizationDbContext _db; + private readonly TenantKey _tenant; - public EfCoreUserRoleStore(UAuthAuthorizationDbContext db) + public EfCoreUserRoleStore(UAuthAuthorizationDbContext db, TenantContext tenant) { _db = db; + _tenant = tenant.Tenant; } - public async Task> GetAssignmentsAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default) + public async Task> GetAssignmentsAsync(UserKey userKey, CancellationToken ct = default) { var entities = await _db.UserRoles .AsNoTracking() .Where(x => - x.Tenant == tenant && + x.Tenant == _tenant && x.UserKey == userKey) .ToListAsync(ct); return entities.Select(UserRoleMapper.ToDomain).ToList().AsReadOnly(); } - public async Task AssignAsync(TenantKey tenant, UserKey userKey, RoleId roleId, DateTimeOffset assignedAt, CancellationToken ct = default) + public async Task AssignAsync(UserKey userKey, RoleId roleId, DateTimeOffset assignedAt, CancellationToken ct = default) { var exists = await _db.UserRoles .AnyAsync(x => - x.Tenant == tenant && + x.Tenant == _tenant && x.UserKey == userKey && x.RoleId == roleId, ct); @@ -41,7 +43,7 @@ public async Task AssignAsync(TenantKey tenant, UserKey userKey, RoleId roleId, var entity = new UserRoleProjection { - Tenant = tenant, + Tenant = _tenant, UserKey = userKey, RoleId = roleId, AssignedAt = assignedAt @@ -51,11 +53,11 @@ public async Task AssignAsync(TenantKey tenant, UserKey userKey, RoleId roleId, await _db.SaveChangesAsync(ct); } - public async Task RemoveAsync(TenantKey tenant, UserKey userKey, RoleId roleId, CancellationToken ct = default) + public async Task RemoveAsync(UserKey userKey, RoleId roleId, CancellationToken ct = default) { var entity = await _db.UserRoles .SingleOrDefaultAsync(x => - x.Tenant == tenant && + x.Tenant == _tenant && x.UserKey == userKey && x.RoleId == roleId, ct); @@ -67,26 +69,20 @@ public async Task RemoveAsync(TenantKey tenant, UserKey userKey, RoleId roleId, await _db.SaveChangesAsync(ct); } - public async Task RemoveAssignmentsByRoleAsync(TenantKey tenant, RoleId roleId, CancellationToken ct = default) + public async Task RemoveAssignmentsByRoleAsync(RoleId roleId, CancellationToken ct = default) { - var entities = await _db.UserRoles + await _db.UserRoles .Where(x => - x.Tenant == tenant && + x.Tenant == _tenant && x.RoleId == roleId) - .ToListAsync(ct); - - if (entities.Count == 0) - return; - - _db.UserRoles.RemoveRange(entities); - await _db.SaveChangesAsync(ct); + .ExecuteDeleteAsync(ct); } - public async Task CountAssignmentsAsync(TenantKey tenant, RoleId roleId, CancellationToken ct = default) + public async Task CountAssignmentsAsync(RoleId roleId, CancellationToken ct = default) { return await _db.UserRoles .CountAsync(x => - x.Tenant == tenant && + x.Tenant == _tenant && x.RoleId == roleId, ct); } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreUserRoleStoreFactory.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreUserRoleStoreFactory.cs new file mode 100644 index 00000000..516b5e9b --- /dev/null +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreUserRoleStoreFactory.cs @@ -0,0 +1,18 @@ +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore; + +internal sealed class EfCoreUserRoleStoreFactory : IUserRoleStoreFactory +{ + private readonly UAuthAuthorizationDbContext _db; + + public EfCoreUserRoleStoreFactory(UAuthAuthorizationDbContext db) + { + _db = db; + } + + public IUserRoleStore Create(TenantKey tenant) + { + return new EfCoreUserRoleStore(_db, new TenantContext(tenant)); + } +} diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/CodeBeam.UltimateAuth.Authorization.InMemory.csproj b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/CodeBeam.UltimateAuth.Authorization.InMemory.csproj index e8e7f49f..a8bbf60f 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/CodeBeam.UltimateAuth.Authorization.InMemory.csproj +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/CodeBeam.UltimateAuth.Authorization.InMemory.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Extensions/ServiceCollectionExtensions.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Extensions/ServiceCollectionExtensions.cs index b62ad6b2..3ce68afc 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Extensions/ServiceCollectionExtensions.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Extensions/ServiceCollectionExtensions.cs @@ -1,5 +1,4 @@ -using CodeBeam.UltimateAuth.Authorization.Reference; -using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.Abstractions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -9,8 +8,8 @@ public static class ServiceCollectionExtensions { public static IServiceCollection AddUltimateAuthAuthorizationInMemory(this IServiceCollection services) { - services.TryAddSingleton(); - services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); // Never try add - seeding is enumerated and all contributors are added. services.AddSingleton(); diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/InMemoryAuthorizationSeedContributor.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/InMemoryAuthorizationSeedContributor.cs index 5cb76c78..e9e22abf 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/InMemoryAuthorizationSeedContributor.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/InMemoryAuthorizationSeedContributor.cs @@ -1,9 +1,8 @@ using CodeBeam.UltimateAuth.Authorization.Contracts; -using CodeBeam.UltimateAuth.Authorization.Domain; using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.Infrastructure; using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.InMemory; namespace CodeBeam.UltimateAuth.Authorization.InMemory; @@ -11,32 +10,76 @@ internal sealed class InMemoryAuthorizationSeedContributor : ISeedContributor { public int Order => 20; - private readonly IRoleStore _roleStore; - private readonly IUserRoleStore _roles; + private readonly IRoleStoreFactory _roleStoreFactory; + private readonly IUserRoleStoreFactory _userRoleStoreFactory; private readonly IInMemoryUserIdProvider _ids; private readonly IClock _clock; - public InMemoryAuthorizationSeedContributor(IRoleStore roleStore, IUserRoleStore roles, IInMemoryUserIdProvider ids, IClock clock) + public InMemoryAuthorizationSeedContributor( + IRoleStoreFactory roleStoreFactory, + IUserRoleStoreFactory userRoleStoreFactory, + IInMemoryUserIdProvider ids, + IClock clock) { - _roleStore = roleStore; - _roles = roles; + _roleStoreFactory = roleStoreFactory; + _userRoleStoreFactory = userRoleStoreFactory; _ids = ids; _clock = clock; } public async Task SeedAsync(TenantKey tenant, CancellationToken ct = default) { - var adminRoleId = RoleId.From(Guid.NewGuid()); - var userRoleId = RoleId.From(Guid.NewGuid()); var now = _clock.UtcNow; - await _roleStore.AddAsync(Role.Create(adminRoleId, tenant, "Admin", new HashSet() { Permission.Wildcard }, _clock.UtcNow)); - await _roleStore.AddAsync(Role.Create(userRoleId, tenant, "User", null, _clock.UtcNow)); + + var roleStore = _roleStoreFactory.Create(tenant); + var userRoleStore = _userRoleStoreFactory.Create(tenant); + + var adminRole = await roleStore.GetByNameAsync("ADMIN", ct); + if (adminRole is null) + { + adminRole = Role.Create( + RoleId.From(Guid.Parse("11111111-1111-1111-1111-111111111111")), + tenant, + "Admin", + new HashSet { Permission.Wildcard }, + now); + + await roleStore.AddAsync(adminRole, ct); + } + + var userRole = await roleStore.GetByNameAsync("USER", ct); + if (userRole is null) + { + userRole = Role.Create( + RoleId.From(Guid.Parse("22222222-2222-2222-2222-222222222222")), + tenant, + "User", + null, + now); + + await roleStore.AddAsync(userRole, ct); + } var adminKey = _ids.GetAdminUserId(); - await _roles.AssignAsync(tenant, adminKey, adminRoleId, now, ct); - await _roles.AssignAsync(tenant, adminKey, userRoleId, now, ct); + await AssignIfMissingAsync(userRoleStore, adminKey, adminRole.Id, now, ct); + await AssignIfMissingAsync(userRoleStore, adminKey, userRole.Id, now, ct); var userKey = _ids.GetUserUserId(); - await _roles.AssignAsync(tenant, userKey, userRoleId, now, ct); + await AssignIfMissingAsync(userRoleStore, userKey, userRole.Id, now, ct); + } + + private static async Task AssignIfMissingAsync( + IUserRoleStore userRoleStore, + UserKey userKey, + RoleId roleId, + DateTimeOffset assignedAt, + CancellationToken ct) + { + var assignments = await userRoleStore.GetAssignmentsAsync(userKey, ct); + + if (assignments.Any(x => x.RoleId == roleId)) + return; + + await userRoleStore.AssignAsync(userKey, roleId, assignedAt, ct); } } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryRoleStore.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryRoleStore.cs index 81843e70..1c47b32f 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryRoleStore.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryRoleStore.cs @@ -1,20 +1,22 @@ using CodeBeam.UltimateAuth.Authorization.Contracts; -using CodeBeam.UltimateAuth.Authorization.Domain; -using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Errors; using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.InMemory; namespace CodeBeam.UltimateAuth.Authorization.InMemory; -internal sealed class InMemoryRoleStore : InMemoryVersionedStore, IRoleStore +internal sealed class InMemoryRoleStore : InMemoryTenantVersionedStore, IRoleStore { protected override RoleKey GetKey(Role entity) => new(entity.Tenant, entity.Id); + public InMemoryRoleStore(TenantContext tenant) : base(tenant) + { + } + protected override void BeforeAdd(Role entity) { - if (Values().Any(r => - r.Tenant == entity.Tenant && + if (TenantValues().Any(r => r.NormalizedName == entity.NormalizedName && !r.IsDeleted)) { @@ -26,8 +28,7 @@ protected override void BeforeSave(Role entity, Role current, long expectedVersi { if (entity.NormalizedName != current.NormalizedName) { - if (Values().Any(r => - r.Tenant == entity.Tenant && + if (TenantValues().Any(r => r.NormalizedName == entity.NormalizedName && r.Id != entity.Id && !r.IsDeleted)) @@ -37,44 +38,40 @@ protected override void BeforeSave(Role entity, Role current, long expectedVersi } } - public Task GetByNameAsync(TenantKey tenant, string normalizedName, CancellationToken ct = default) + public Task GetByNameAsync(string normalizedName, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - var role = Values() + var role = TenantValues() .FirstOrDefault(r => - r.Tenant == tenant && r.NormalizedName == normalizedName && !r.IsDeleted); - return Task.FromResult(role); + return Task.FromResult(role?.Snapshot()); } - public Task> GetByIdsAsync(TenantKey tenant, IReadOnlyCollection roleIds, CancellationToken ct = default) + public Task> GetByIdsAsync(IReadOnlyCollection roleIds, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - var result = new List(roleIds.Count); + var set = roleIds.ToHashSet(); - foreach (var id in roleIds) - { - if (TryGet(new RoleKey(tenant, id), out var role) && role is not null) - { - result.Add(role.Snapshot()); - } - } + var result = TenantValues() + .Where(r => set.Contains(r.Id) && !r.IsDeleted) + .Select(r => r.Snapshot()) + .ToList() + .AsReadOnly(); return Task.FromResult>(result); } - public Task> QueryAsync(TenantKey tenant, RoleQuery query, CancellationToken ct = default) + public Task> QueryAsync(RoleQuery query, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); var normalized = query.Normalize(); - var baseQuery = Values() - .Where(r => r.Tenant == tenant); + var baseQuery = TenantValues().AsQueryable(); if (!query.IncludeDeleted) baseQuery = baseQuery.Where(r => !r.IsDeleted); @@ -113,6 +110,7 @@ public Task> QueryAsync(TenantKey tenant, RoleQuery query, Can var items = baseQuery .Skip((normalized.PageNumber - 1) * normalized.PageSize) .Take(normalized.PageSize) + .Select(x => x.Snapshot()) .ToList() .AsReadOnly(); diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryRoleStoreFactory.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryRoleStoreFactory.cs new file mode 100644 index 00000000..570d507b --- /dev/null +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryRoleStoreFactory.cs @@ -0,0 +1,14 @@ +using System.Collections.Concurrent; +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Authorization.InMemory; + +public sealed class InMemoryRoleStoreFactory : IRoleStoreFactory +{ + private readonly ConcurrentDictionary _stores = new(); + + public IRoleStore Create(TenantKey tenant) + { + return _stores.GetOrAdd(tenant, t => new InMemoryRoleStore(new TenantContext(t))); + } +} diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryUserRoleStore.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryUserRoleStore.cs index 64a4c958..8027360b 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryUserRoleStore.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryUserRoleStore.cs @@ -8,35 +8,41 @@ namespace CodeBeam.UltimateAuth.Authorization.InMemory; internal sealed class InMemoryUserRoleStore : IUserRoleStore { - private readonly ConcurrentDictionary<(TenantKey, UserKey), List> _assignments = new(); + private readonly TenantKey _tenant; + private readonly ConcurrentDictionary> _assignments = new(); - public Task> GetAssignmentsAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default) + public InMemoryUserRoleStore(TenantContext tenant) + { + _tenant = tenant.Tenant; + } + + public Task> GetAssignmentsAsync(UserKey userKey, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - if (_assignments.TryGetValue((tenant, userKey), out var list)) + if (_assignments.TryGetValue(userKey, out var list)) { lock (list) - return Task.FromResult>(list.ToArray()); + return Task.FromResult>(list.Select(x => x).ToArray()); } return Task.FromResult>(Array.Empty()); } - public Task AssignAsync(TenantKey tenant, UserKey userKey, RoleId roleId, DateTimeOffset assignedAt, CancellationToken ct = default) + public Task AssignAsync(UserKey userKey, RoleId roleId, DateTimeOffset assignedAt, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - var list = _assignments.GetOrAdd((tenant, userKey), _ => new List()); + var list = _assignments.GetOrAdd(userKey, _ => new List()); lock (list) { if (list.Any(x => x.RoleId == roleId)) - throw new UAuthConflictException("Role is already assigned to the user."); + throw new UAuthConflictException("role_already_assigned"); list.Add(new UserRole { - Tenant = tenant, + Tenant = _tenant, UserKey = userKey, RoleId = roleId, AssignedAt = assignedAt @@ -46,11 +52,11 @@ public Task AssignAsync(TenantKey tenant, UserKey userKey, RoleId roleId, DateTi return Task.CompletedTask; } - public Task RemoveAsync(TenantKey tenant, UserKey userKey, RoleId roleId, CancellationToken ct = default) + public Task RemoveAsync(UserKey userKey, RoleId roleId, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - if (_assignments.TryGetValue((tenant, userKey), out var list)) + if (_assignments.TryGetValue(userKey, out var list)) { lock (list) { @@ -61,17 +67,12 @@ public Task RemoveAsync(TenantKey tenant, UserKey userKey, RoleId roleId, Cancel return Task.CompletedTask; } - public Task RemoveAssignmentsByRoleAsync(TenantKey tenant, RoleId roleId, CancellationToken ct = default) + public Task RemoveAssignmentsByRoleAsync(RoleId roleId, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - foreach (var kv in _assignments) + foreach (var list in _assignments.Values) { - if (kv.Key.Item1 != tenant) - continue; - - var list = kv.Value; - lock (list) { list.RemoveAll(x => x.RoleId == roleId); @@ -81,19 +82,14 @@ public Task RemoveAssignmentsByRoleAsync(TenantKey tenant, RoleId roleId, Cancel return Task.CompletedTask; } - public Task CountAssignmentsAsync(TenantKey tenant, RoleId roleId, CancellationToken ct = default) + public Task CountAssignmentsAsync(RoleId roleId, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); var count = 0; - foreach (var kv in _assignments) + foreach (var list in _assignments.Values) { - if (kv.Key.Item1 != tenant) - continue; - - var list = kv.Value; - lock (list) { count += list.Count(x => x.RoleId == roleId); diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryUserRoleStoreFactory.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryUserRoleStoreFactory.cs new file mode 100644 index 00000000..9e8c8723 --- /dev/null +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryUserRoleStoreFactory.cs @@ -0,0 +1,14 @@ +using System.Collections.Concurrent; +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Authorization.InMemory; + +public sealed class InMemoryUserRoleStoreFactory : IUserRoleStoreFactory +{ + private readonly ConcurrentDictionary _stores = new(); + + public IUserRoleStore Create(TenantKey tenant) + { + return _stores.GetOrAdd(tenant, t => new InMemoryUserRoleStore(new TenantContext(t))); + } +} diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/RolePermissionResolver.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/RolePermissionResolver.cs index e30a12a9..9073946d 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/RolePermissionResolver.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/RolePermissionResolver.cs @@ -1,16 +1,15 @@ using CodeBeam.UltimateAuth.Authorization.Contracts; -using CodeBeam.UltimateAuth.Authorization.Domain; using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Authorization.Reference; internal sealed class RolePermissionResolver : IRolePermissionResolver { - private readonly IRoleStore _roles; + private readonly IRoleStoreFactory _roleFactory; - public RolePermissionResolver(IRoleStore roles) + public RolePermissionResolver(IRoleStoreFactory roleFactory) { - _roles = roles; + _roleFactory = roleFactory; } public async Task> ResolveAsync(TenantKey tenant, IReadOnlyCollection roleIds, CancellationToken ct = default) @@ -18,7 +17,8 @@ public async Task> ResolveAsync(TenantKey tenant if (roleIds.Count == 0) return Array.Empty(); - var roles = await _roles.GetByIdsAsync(tenant, roleIds, ct); + var roleStore = _roleFactory.Create(tenant); + var roles = await roleStore.GetByIdsAsync(roleIds, ct); var permissions = new HashSet(); diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/UserPermissionStore.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/UserPermissionStore.cs index b7ab7070..0b804c03 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/UserPermissionStore.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/UserPermissionStore.cs @@ -6,18 +6,19 @@ namespace CodeBeam.UltimateAuth.Authorization.Reference; internal sealed class UserPermissionStore : IUserPermissionStore { - private readonly IUserRoleStore _userRoles; + private readonly IUserRoleStoreFactory _userRolesFactory; private readonly IRolePermissionResolver _resolver; - public UserPermissionStore(IUserRoleStore userRoles, IRolePermissionResolver resolver) + public UserPermissionStore(IUserRoleStoreFactory userRolesFactory, IRolePermissionResolver resolver) { - _userRoles = userRoles; + _userRolesFactory = userRolesFactory; _resolver = resolver; } public async Task> GetPermissionsAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default) { - var assignments = await _userRoles.GetAssignmentsAsync(tenant, userKey, ct); + var userRoleStore = _userRolesFactory.Create(tenant); + var assignments = await userRoleStore.GetAssignmentsAsync(userKey, ct); var roleIds = assignments.Select(x => x.RoleId).ToArray(); return await _resolver.ResolveAsync(tenant, roleIds, ct); } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/RoleService.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/RoleService.cs index c5746fad..6ffed5e2 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/RoleService.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/RoleService.cs @@ -1,5 +1,4 @@ using CodeBeam.UltimateAuth.Authorization.Contracts; -using CodeBeam.UltimateAuth.Authorization.Domain; using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Errors; @@ -10,19 +9,19 @@ namespace CodeBeam.UltimateAuth.Authorization.Reference; internal sealed class RoleService : IRoleService { private readonly IAccessOrchestrator _accessOrchestrator; - private readonly IRoleStore _roles; - private readonly IUserRoleStore _userRoles; + private readonly IRoleStoreFactory _roleFactory; + private readonly IUserRoleStoreFactory _userRoleFactory; private readonly IClock _clock; public RoleService( IAccessOrchestrator accessOrchestrator, - IRoleStore roles, - IUserRoleStore userRoles, + IRoleStoreFactory roleFactory, + IUserRoleStoreFactory userRoleFactory, IClock clock) { _accessOrchestrator = accessOrchestrator; - _roles = roles; - _userRoles = userRoles; + _roleFactory = roleFactory; + _userRoleFactory = userRoleFactory; _clock = clock; } @@ -33,7 +32,8 @@ public async Task CreateAsync(AccessContext context, string name, IEnumera var cmd = new AccessCommand(async innerCt => { var role = Role.Create(RoleId.New(), context.ResourceTenant, name, permissions, _clock.UtcNow); - await _roles.AddAsync(role, innerCt); + var roleStore = _roleFactory.Create(context.ResourceTenant); + await roleStore.AddAsync(role, innerCt); return role; }); @@ -48,7 +48,8 @@ public async Task RenameAsync(AccessContext context, RoleId roleId, string newNa var cmd = new AccessCommand(async innerCt => { var key = new RoleKey(context.ResourceTenant, roleId); - var role = await _roles.GetAsync(key, innerCt); + var roleStore = _roleFactory.Create(context.ResourceTenant); + var role = await roleStore.GetAsync(key, innerCt); if (role is null || role.IsDeleted) throw new UAuthNotFoundException("role_not_found"); @@ -56,7 +57,7 @@ public async Task RenameAsync(AccessContext context, RoleId roleId, string newNa var expected = role.Version; role.Rename(newName, _clock.UtcNow); - await _roles.SaveAsync(role, expected, innerCt); + await roleStore.SaveAsync(role, expected, innerCt); }); await _accessOrchestrator.ExecuteAsync(context, cmd, ct); @@ -69,15 +70,16 @@ public async Task DeleteAsync(AccessContext context, RoleId ro var cmd = new AccessCommand(async innerCt => { var key = new RoleKey(context.ResourceTenant, roleId); - - var role = await _roles.GetAsync(key, innerCt); + var roleStore = _roleFactory.Create(context.ResourceTenant); + var role = await roleStore.GetAsync(key, innerCt); if (role is null) throw new UAuthNotFoundException("role_not_found"); - var removed = await _userRoles.CountAssignmentsAsync(context.ResourceTenant, roleId, innerCt); - await _userRoles.RemoveAssignmentsByRoleAsync(context.ResourceTenant, roleId, innerCt); - await _roles.DeleteAsync(key, role.Version, mode, _clock.UtcNow, innerCt); + var userRoleStore = _userRoleFactory.Create(context.ResourceTenant); + var removed = await userRoleStore.CountAssignmentsAsync(roleId, innerCt); + await userRoleStore.RemoveAssignmentsByRoleAsync(roleId, innerCt); + await roleStore.DeleteAsync(key, role.Version, mode, _clock.UtcNow, innerCt); return new DeleteRoleResult { @@ -97,8 +99,9 @@ public async Task SetPermissionsAsync(AccessContext context, RoleId roleId, IEnu var cmd = new AccessCommand(async innerCt => { + var roleStore = _roleFactory.Create(context.ResourceTenant); var key = new RoleKey(context.ResourceTenant, roleId); - var role = await _roles.GetAsync(key, innerCt); + var role = await roleStore.GetAsync(key, innerCt); if (role is null || role.IsDeleted) throw new UAuthNotFoundException("role_not_found"); @@ -106,7 +109,7 @@ public async Task SetPermissionsAsync(AccessContext context, RoleId roleId, IEnu var expected = role.Version; role.SetPermissions(permissions, _clock.UtcNow); - await _roles.SaveAsync(role, expected, innerCt); + await roleStore.SaveAsync(role, expected, innerCt); }); await _accessOrchestrator.ExecuteAsync(context, cmd, ct); @@ -118,7 +121,8 @@ public async Task> QueryAsync(AccessContext context, RoleQuery var cmd = new AccessCommand>(async innerCt => { - return await _roles.QueryAsync(context.ResourceTenant, query, innerCt); + var roleStore = _roleFactory.Create(context.ResourceTenant); + return await roleStore.QueryAsync(query, innerCt); }); return await _accessOrchestrator.ExecuteAsync(context, cmd, ct); diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/UserRoleService.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/UserRoleService.cs index 21f50c7b..11b3d28b 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/UserRoleService.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/UserRoleService.cs @@ -10,15 +10,15 @@ namespace CodeBeam.UltimateAuth.Authorization.Reference; internal sealed class UserRoleService : IUserRoleService { private readonly IAccessOrchestrator _accessOrchestrator; - private readonly IUserRoleStore _userRoles; - private readonly IRoleStore _roles; + private readonly IUserRoleStoreFactory _userRoleFactory; + private readonly IRoleStoreFactory _roleFactory; private readonly IClock _clock; - public UserRoleService(IAccessOrchestrator accessOrchestrator, IUserRoleStore userRoles, IRoleStore roles, IClock clock) + public UserRoleService(IAccessOrchestrator accessOrchestrator, IUserRoleStoreFactory userRoleFactory, IRoleStoreFactory roleFactory, IClock clock) { _accessOrchestrator = accessOrchestrator; - _userRoles = userRoles; - _roles = roles; + _userRoleFactory = userRoleFactory; + _roleFactory = roleFactory; _clock = clock; } @@ -30,13 +30,15 @@ public async Task AssignAsync(AccessContext context, UserKey targetUserKey, stri var cmd = new AccessCommand(async innerCt => { + var roleStore = _roleFactory.Create(context.ResourceTenant); + var userRoleStore = _userRoleFactory.Create(context.ResourceTenant); var normalized = roleName.Trim().ToUpperInvariant(); - var role = await _roles.GetByNameAsync(context.ResourceTenant, normalized, innerCt); + var role = await roleStore.GetByNameAsync(normalized, innerCt); if (role is null || role.IsDeleted) throw new UAuthNotFoundException("role_not_found"); - await _userRoles.AssignAsync(context.ResourceTenant, targetUserKey, role.Id, now, innerCt); + await userRoleStore.AssignAsync(targetUserKey, role.Id, now, innerCt); }); await _accessOrchestrator.ExecuteAsync(context, cmd, ct); @@ -48,13 +50,15 @@ public async Task RemoveAsync(AccessContext context, UserKey targetUserKey, stri var cmd = new AccessCommand(async innerCt => { + var roleStore = _roleFactory.Create(context.ResourceTenant); + var userRoleStore = _userRoleFactory.Create(context.ResourceTenant); var normalized = roleName.Trim().ToUpperInvariant(); - var role = await _roles.GetByNameAsync(context.ResourceTenant, normalized, innerCt); + var role = await roleStore.GetByNameAsync(normalized, innerCt); if (role is null) return; - await _userRoles.RemoveAsync(context.ResourceTenant, targetUserKey, role.Id, innerCt); + await userRoleStore.RemoveAsync(targetUserKey, role.Id, innerCt); }); await _accessOrchestrator.ExecuteAsync(context, cmd, ct); @@ -68,9 +72,11 @@ public async Task> GetRolesAsync(AccessContext context { request = request.Normalize(); - var assignments = await _userRoles.GetAssignmentsAsync(context.ResourceTenant, targetUserKey, innerCt); + var roleStore = _roleFactory.Create(context.ResourceTenant); + var userRoleStore = _userRoleFactory.Create(context.ResourceTenant); + var assignments = await userRoleStore.GetAssignmentsAsync(targetUserKey, innerCt); var roleIds = assignments.Select(x => x.RoleId).ToArray(); - var roles = await _roles.GetByIdsAsync(context.ResourceTenant, roleIds, innerCt); + var roles = await roleStore.GetByIdsAsync(roleIds, innerCt); var roleMap = roles.ToDictionary(x => x.Id); diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IRolePermissionResolver.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IRolePermissionResolver.cs index 119d5002..18c59bf3 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IRolePermissionResolver.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IRolePermissionResolver.cs @@ -1,5 +1,4 @@ using CodeBeam.UltimateAuth.Authorization.Contracts; -using CodeBeam.UltimateAuth.Authorization.Domain; using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Authorization; diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IRoleService.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IRoleService.cs index bc794624..948959f6 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IRoleService.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IRoleService.cs @@ -1,5 +1,4 @@ using CodeBeam.UltimateAuth.Authorization.Contracts; -using CodeBeam.UltimateAuth.Authorization.Domain; using CodeBeam.UltimateAuth.Core.Contracts; namespace CodeBeam.UltimateAuth.Authorization; diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IRoleStore.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IRoleStore.cs index 5b263885..c771c3bd 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IRoleStore.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IRoleStore.cs @@ -1,14 +1,12 @@ using CodeBeam.UltimateAuth.Authorization.Contracts; -using CodeBeam.UltimateAuth.Authorization.Domain; using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Authorization; public interface IRoleStore : IVersionedStore { - Task GetByNameAsync(TenantKey tenant, string normalizedName, CancellationToken ct = default); - Task> GetByIdsAsync(TenantKey tenant, IReadOnlyCollection roleIds, CancellationToken ct = default); - Task> QueryAsync(TenantKey tenant, RoleQuery query, CancellationToken ct = default); + Task GetByNameAsync(string normalizedName, CancellationToken ct = default); + Task> GetByIdsAsync(IReadOnlyCollection roleIds, CancellationToken ct = default); + Task> QueryAsync(RoleQuery query, CancellationToken ct = default); } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IRoleStoreFactory.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IRoleStoreFactory.cs new file mode 100644 index 00000000..73386874 --- /dev/null +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IRoleStoreFactory.cs @@ -0,0 +1,8 @@ +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Authorization; + +public interface IRoleStoreFactory +{ + IRoleStore Create(TenantKey tenant); +} diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IUserPermissionStore.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IUserPermissionStore.cs index eb2aabaa..bcf221a8 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IUserPermissionStore.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IUserPermissionStore.cs @@ -1,5 +1,4 @@ using CodeBeam.UltimateAuth.Authorization.Contracts; -using CodeBeam.UltimateAuth.Authorization.Domain; using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.MultiTenancy; diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IUserRoleStore.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IUserRoleStore.cs index a87fc1e3..d8918a24 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IUserRoleStore.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IUserRoleStore.cs @@ -1,14 +1,13 @@ using CodeBeam.UltimateAuth.Authorization.Contracts; using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Authorization; public interface IUserRoleStore { - Task> GetAssignmentsAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default); - Task AssignAsync(TenantKey tenant, UserKey userKey, RoleId roleId, DateTimeOffset assignedAt, CancellationToken ct = default); - Task RemoveAsync(TenantKey tenant, UserKey userKey, RoleId roleId, CancellationToken ct = default); - Task RemoveAssignmentsByRoleAsync(TenantKey tenant, RoleId roleId, CancellationToken ct = default); - Task CountAssignmentsAsync(TenantKey tenant, RoleId roleId, CancellationToken ct = default); + Task> GetAssignmentsAsync(UserKey userKey, CancellationToken ct = default); + Task AssignAsync(UserKey userKey, RoleId roleId, DateTimeOffset assignedAt, CancellationToken ct = default); + Task RemoveAsync(UserKey userKey, RoleId roleId, CancellationToken ct = default); + Task RemoveAssignmentsByRoleAsync(RoleId roleId, CancellationToken ct = default); + Task CountAssignmentsAsync(RoleId roleId, CancellationToken ct = default); } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IUserRoleStoreFactory.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IUserRoleStoreFactory.cs new file mode 100644 index 00000000..3dfe72a6 --- /dev/null +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IUserRoleStoreFactory.cs @@ -0,0 +1,8 @@ +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Authorization; + +public interface IUserRoleStoreFactory +{ + IUserRoleStore Create(TenantKey tenant); +} diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization/CodeBeam.UltimateAuth.Authorization.csproj b/src/authorization/CodeBeam.UltimateAuth.Authorization/CodeBeam.UltimateAuth.Authorization.csproj index d1493e50..f4ea74b4 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization/CodeBeam.UltimateAuth.Authorization.csproj +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization/CodeBeam.UltimateAuth.Authorization.csproj @@ -4,15 +4,44 @@ net8.0;net9.0;net10.0 enable enable - 0.0.1 - 0.0.1 - true $(NoWarn);1591 + + CodeBeam.UltimateAuth.Authorization + 0.1.0-preview.1 + + CodeBeam + CodeBeam + + + Authorization module for UltimateAuth. + Provides orchestration, abstractions and dependency injection wiring for role and permission based authorization. + Use with a persistence provider such as EntityFrameworkCore or InMemory. + This package is included transitively by CodeBeam.UltimateAuth.Server and usually does not need to be installed directly. + + + authentication;authorization;roles;permissions;security;module;auth-framework + + README.md + https://github.com/CodeBeamOrg/UltimateAuth + https://github.com/CodeBeamOrg/UltimateAuth + Apache-2.0 + + true + true + snupkg + + true + + + + + + + - - + -
+
\ No newline at end of file diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization/Domain/Role.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization/Domain/Role.cs index f48a15e9..74995da2 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization/Domain/Role.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization/Domain/Role.cs @@ -1,12 +1,11 @@ using CodeBeam.UltimateAuth.Authorization.Contracts; -using CodeBeam.UltimateAuth.Authorization.Domain; using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Errors; using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Authorization; -public sealed class Role : IVersionedEntity, IEntitySnapshot, ISoftDeletable +public sealed class Role : ITenantEntity, IVersionedEntity, IEntitySnapshot, ISoftDeletable { private readonly HashSet _permissions = new(); diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization/Domain/RoleKey.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization/Domain/RoleKey.cs index f52f8cfb..004ee340 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization/Domain/RoleKey.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization/Domain/RoleKey.cs @@ -1,7 +1,7 @@ using CodeBeam.UltimateAuth.Authorization.Contracts; using CodeBeam.UltimateAuth.Core.MultiTenancy; -namespace CodeBeam.UltimateAuth.Authorization.Domain; +namespace CodeBeam.UltimateAuth.Authorization; public readonly record struct RoleKey( TenantKey Tenant, diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization/Infrastructure/AuthorizationClaimsProvider.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization/Infrastructure/AuthorizationClaimsProvider.cs index cc427cdd..7c50a600 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization/Infrastructure/AuthorizationClaimsProvider.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization/Infrastructure/AuthorizationClaimsProvider.cs @@ -8,22 +8,24 @@ namespace CodeBeam.UltimateAuth.Authorization; public sealed class AuthorizationClaimsProvider : IUserClaimsProvider { - private readonly IUserRoleStore _roles; - private readonly IRoleStore _roleStore; + private readonly IUserRoleStoreFactory _userRoleFactory; + private readonly IRoleStoreFactory _roleFactory; private readonly IUserPermissionStore _permissions; - public AuthorizationClaimsProvider(IUserRoleStore roles, IRoleStore roleStore, IUserPermissionStore permissions) + public AuthorizationClaimsProvider(IUserRoleStoreFactory userRoleFactory, IRoleStoreFactory roleFactory, IUserPermissionStore permissions) { - _roles = roles; - _roleStore = roleStore; + _userRoleFactory = userRoleFactory; + _roleFactory = roleFactory; _permissions = permissions; } public async Task GetClaimsAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default) { - var assignments = await _roles.GetAssignmentsAsync(tenant, userKey, ct); + var roleStore = _roleFactory.Create(tenant); + var userRoleStore = _userRoleFactory.Create(tenant); + var assignments = await userRoleStore.GetAssignmentsAsync(userKey, ct); var roleIds = assignments.Select(x => x.RoleId).Distinct().ToArray(); - var roles = await _roleStore.GetByIdsAsync(tenant, roleIds, ct); + var roles = await roleStore.GetByIdsAsync(roleIds, ct); var perms = await _permissions.GetPermissionsAsync(tenant, userKey, ct); var builder = ClaimsSnapshot.Create(); diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization/README.md b/src/authorization/CodeBeam.UltimateAuth.Authorization/README.md new file mode 100644 index 00000000..0d78b0b6 --- /dev/null +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization/README.md @@ -0,0 +1,23 @@ +# UltimateAuth Authorization + +Authorization module for UltimateAuth. + +## Purpose + +This package provides: + +- Dependency injection setup +- Role and permission orchestration +- Integration points for authorization providers + +## Does NOT include + +- Persistence (use EntityFrameworkCore or InMemory packages) +- Domain implementation (use Reference package if needed) +- Policy enforcement integrations + +⚠️ This package is typically installed transitively via: + +- CodeBeam.UltimateAuth.Server + +In most cases, you do not need to install it directly unless you are building custom integrations. \ No newline at end of file diff --git a/src/CodeBeam.UltimateAuth.Client/Abstractions/IBrowserStorage.cs b/src/client/CodeBeam.UltimateAuth.Client/Abstractions/IClientStorage.cs similarity index 91% rename from src/CodeBeam.UltimateAuth.Client/Abstractions/IBrowserStorage.cs rename to src/client/CodeBeam.UltimateAuth.Client/Abstractions/IClientStorage.cs index 27871be7..9f605d26 100644 --- a/src/CodeBeam.UltimateAuth.Client/Abstractions/IBrowserStorage.cs +++ b/src/client/CodeBeam.UltimateAuth.Client/Abstractions/IClientStorage.cs @@ -2,7 +2,7 @@ namespace CodeBeam.UltimateAuth.Client.Infrastructure; -public interface IBrowserStorage +public interface IClientStorage { ValueTask SetAsync(StorageScope scope, string key, string value); ValueTask GetAsync(StorageScope scope, string key); diff --git a/src/client/CodeBeam.UltimateAuth.Client/Abstractions/IReturnUrlProvider.cs b/src/client/CodeBeam.UltimateAuth.Client/Abstractions/IReturnUrlProvider.cs new file mode 100644 index 00000000..f5eddd8e --- /dev/null +++ b/src/client/CodeBeam.UltimateAuth.Client/Abstractions/IReturnUrlProvider.cs @@ -0,0 +1,6 @@ +namespace CodeBeam.UltimateAuth.Client.Abstractions; + +public interface IReturnUrlProvider +{ + string GetCurrentUrl(); +} diff --git a/src/CodeBeam.UltimateAuth.Client/Abstractions/ISessionCoordinator.cs b/src/client/CodeBeam.UltimateAuth.Client/Abstractions/ISessionCoordinator.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Abstractions/ISessionCoordinator.cs rename to src/client/CodeBeam.UltimateAuth.Client/Abstractions/ISessionCoordinator.cs diff --git a/src/client/CodeBeam.UltimateAuth.Client/AssemblyVisibility.cs b/src/client/CodeBeam.UltimateAuth.Client/AssemblyVisibility.cs new file mode 100644 index 00000000..49c98ad3 --- /dev/null +++ b/src/client/CodeBeam.UltimateAuth.Client/AssemblyVisibility.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("CodeBeam.UltimateAuth.Client.Blazor")] +[assembly: InternalsVisibleTo("CodeBeam.UltimateAuth.Tests.Unit")] diff --git a/src/CodeBeam.UltimateAuth.Client/AuthState/IUAuthStateManager.cs b/src/client/CodeBeam.UltimateAuth.Client/AuthState/IUAuthStateManager.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/AuthState/IUAuthStateManager.cs rename to src/client/CodeBeam.UltimateAuth.Client/AuthState/IUAuthStateManager.cs diff --git a/src/CodeBeam.UltimateAuth.Client/AuthState/UAuthState.cs b/src/client/CodeBeam.UltimateAuth.Client/AuthState/UAuthState.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/AuthState/UAuthState.cs rename to src/client/CodeBeam.UltimateAuth.Client/AuthState/UAuthState.cs diff --git a/src/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateChangeReason.cs b/src/client/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateChangeReason.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateChangeReason.cs rename to src/client/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateChangeReason.cs diff --git a/src/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateEvent.cs b/src/client/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateEvent.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateEvent.cs rename to src/client/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateEvent.cs diff --git a/src/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateEventArgs.cs b/src/client/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateEventArgs.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateEventArgs.cs rename to src/client/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateEventArgs.cs diff --git a/src/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateEventHandlingMode.cs b/src/client/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateEventHandlingMode.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateEventHandlingMode.cs rename to src/client/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateEventHandlingMode.cs diff --git a/src/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateManager.cs b/src/client/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateManager.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateManager.cs rename to src/client/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateManager.cs diff --git a/src/client/CodeBeam.UltimateAuth.Client/CodeBeam.UltimateAuth.Client.csproj b/src/client/CodeBeam.UltimateAuth.Client/CodeBeam.UltimateAuth.Client.csproj new file mode 100644 index 00000000..073255a6 --- /dev/null +++ b/src/client/CodeBeam.UltimateAuth.Client/CodeBeam.UltimateAuth.Client.csproj @@ -0,0 +1,18 @@ + + + + net8.0;net9.0;net10.0 + enable + enable + true + $(NoWarn);1591 + + + + + + + + + + diff --git a/src/CodeBeam.UltimateAuth.Client/Contracts/CoordinatorTerminationReason.cs b/src/client/CodeBeam.UltimateAuth.Client/Contracts/CoordinatorTerminationReason.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Contracts/CoordinatorTerminationReason.cs rename to src/client/CodeBeam.UltimateAuth.Client/Contracts/CoordinatorTerminationReason.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Contracts/PkceClientState.cs b/src/client/CodeBeam.UltimateAuth.Client/Contracts/PkceClientState.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Contracts/PkceClientState.cs rename to src/client/CodeBeam.UltimateAuth.Client/Contracts/PkceClientState.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Contracts/RefreshResult.cs b/src/client/CodeBeam.UltimateAuth.Client/Contracts/RefreshResult.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Contracts/RefreshResult.cs rename to src/client/CodeBeam.UltimateAuth.Client/Contracts/RefreshResult.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Contracts/StorageScope.cs b/src/client/CodeBeam.UltimateAuth.Client/Contracts/StorageScope.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Contracts/StorageScope.cs rename to src/client/CodeBeam.UltimateAuth.Client/Contracts/StorageScope.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Contracts/TenantTransport.cs b/src/client/CodeBeam.UltimateAuth.Client/Contracts/TenantTransport.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Contracts/TenantTransport.cs rename to src/client/CodeBeam.UltimateAuth.Client/Contracts/TenantTransport.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Contracts/UAuthRenderMode.cs b/src/client/CodeBeam.UltimateAuth.Client/Contracts/UAuthRenderMode.cs similarity index 98% rename from src/CodeBeam.UltimateAuth.Client/Contracts/UAuthRenderMode.cs rename to src/client/CodeBeam.UltimateAuth.Client/Contracts/UAuthRenderMode.cs index 76c9fba4..f7f1cbba 100644 --- a/src/CodeBeam.UltimateAuth.Client/Contracts/UAuthRenderMode.cs +++ b/src/client/CodeBeam.UltimateAuth.Client/Contracts/UAuthRenderMode.cs @@ -4,4 +4,4 @@ public enum UAuthRenderMode { Manual = 0, Reactive = 1 -} \ No newline at end of file +} diff --git a/src/CodeBeam.UltimateAuth.Client/Contracts/UAuthTransportResult.cs b/src/client/CodeBeam.UltimateAuth.Client/Contracts/UAuthTransportResult.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Contracts/UAuthTransportResult.cs rename to src/client/CodeBeam.UltimateAuth.Client/Contracts/UAuthTransportResult.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Device/IDeviceIdGenerator.cs b/src/client/CodeBeam.UltimateAuth.Client/Device/IDeviceIdGenerator.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Device/IDeviceIdGenerator.cs rename to src/client/CodeBeam.UltimateAuth.Client/Device/IDeviceIdGenerator.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Device/IDeviceIdProvider.cs b/src/client/CodeBeam.UltimateAuth.Client/Device/IDeviceIdProvider.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Device/IDeviceIdProvider.cs rename to src/client/CodeBeam.UltimateAuth.Client/Device/IDeviceIdProvider.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Device/IDeviceIdStorage.cs b/src/client/CodeBeam.UltimateAuth.Client/Device/IDeviceIdStorage.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Device/IDeviceIdStorage.cs rename to src/client/CodeBeam.UltimateAuth.Client/Device/IDeviceIdStorage.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Device/UAuthDeviceIdGenerator.cs b/src/client/CodeBeam.UltimateAuth.Client/Device/UAuthDeviceIdGenerator.cs similarity index 85% rename from src/CodeBeam.UltimateAuth.Client/Device/UAuthDeviceIdGenerator.cs rename to src/client/CodeBeam.UltimateAuth.Client/Device/UAuthDeviceIdGenerator.cs index 1cf9fb4a..8c1001b1 100644 --- a/src/CodeBeam.UltimateAuth.Client/Device/UAuthDeviceIdGenerator.cs +++ b/src/client/CodeBeam.UltimateAuth.Client/Device/UAuthDeviceIdGenerator.cs @@ -4,7 +4,7 @@ namespace CodeBeam.UltimateAuth.Client.Devices; -public sealed class UAuthDeviceIdGenerator : IDeviceIdGenerator +internal sealed class UAuthDeviceIdGenerator : IDeviceIdGenerator { public DeviceId Generate() { diff --git a/src/CodeBeam.UltimateAuth.Client/Device/UAuthDeviceIdProvider.cs b/src/client/CodeBeam.UltimateAuth.Client/Device/UAuthDeviceIdProvider.cs similarity index 94% rename from src/CodeBeam.UltimateAuth.Client/Device/UAuthDeviceIdProvider.cs rename to src/client/CodeBeam.UltimateAuth.Client/Device/UAuthDeviceIdProvider.cs index 5cadcb31..22cb4d31 100644 --- a/src/CodeBeam.UltimateAuth.Client/Device/UAuthDeviceIdProvider.cs +++ b/src/client/CodeBeam.UltimateAuth.Client/Device/UAuthDeviceIdProvider.cs @@ -3,7 +3,7 @@ namespace CodeBeam.UltimateAuth.Client; -public sealed class UAuthDeviceIdProvider : IDeviceIdProvider +internal sealed class UAuthDeviceIdProvider : IDeviceIdProvider { private readonly IDeviceIdStorage _storage; private readonly IDeviceIdGenerator _generator; diff --git a/src/CodeBeam.UltimateAuth.Client/Diagnostics/UAuthClientDiagnostics.cs b/src/client/CodeBeam.UltimateAuth.Client/Diagnostics/UAuthClientDiagnostics.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Diagnostics/UAuthClientDiagnostics.cs rename to src/client/CodeBeam.UltimateAuth.Client/Diagnostics/UAuthClientDiagnostics.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Errors/UAuthClientException.cs b/src/client/CodeBeam.UltimateAuth.Client/Errors/UAuthClientException.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Errors/UAuthClientException.cs rename to src/client/CodeBeam.UltimateAuth.Client/Errors/UAuthClientException.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Errors/UAuthProtocolException.cs b/src/client/CodeBeam.UltimateAuth.Client/Errors/UAuthProtocolException.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Errors/UAuthProtocolException.cs rename to src/client/CodeBeam.UltimateAuth.Client/Errors/UAuthProtocolException.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Errors/UAuthTransportException.cs b/src/client/CodeBeam.UltimateAuth.Client/Errors/UAuthTransportException.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Errors/UAuthTransportException.cs rename to src/client/CodeBeam.UltimateAuth.Client/Errors/UAuthTransportException.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Events/IUAuthClientEvents.cs b/src/client/CodeBeam.UltimateAuth.Client/Events/IUAuthClientEvents.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Events/IUAuthClientEvents.cs rename to src/client/CodeBeam.UltimateAuth.Client/Events/IUAuthClientEvents.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Events/UAuthClientEvents.cs b/src/client/CodeBeam.UltimateAuth.Client/Events/UAuthClientEvents.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Events/UAuthClientEvents.cs rename to src/client/CodeBeam.UltimateAuth.Client/Events/UAuthClientEvents.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Extensions/LoginRequestFormExtensions.cs b/src/client/CodeBeam.UltimateAuth.Client/Extensions/LoginRequestFormExtensions.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Extensions/LoginRequestFormExtensions.cs rename to src/client/CodeBeam.UltimateAuth.Client/Extensions/LoginRequestFormExtensions.cs diff --git a/src/client/CodeBeam.UltimateAuth.Client/Extensions/ServiceCollectionExtensions.cs b/src/client/CodeBeam.UltimateAuth.Client/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..7cc9dac0 --- /dev/null +++ b/src/client/CodeBeam.UltimateAuth.Client/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,124 @@ +using CodeBeam.UltimateAuth.Client.Authentication; +using CodeBeam.UltimateAuth.Client.Device; +using CodeBeam.UltimateAuth.Client.Devices; +using CodeBeam.UltimateAuth.Client.Events; +using CodeBeam.UltimateAuth.Client.Infrastructure; +using CodeBeam.UltimateAuth.Client.Options; +using CodeBeam.UltimateAuth.Client.Runtime; +using CodeBeam.UltimateAuth.Client.Services; +using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.Options; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace CodeBeam.UltimateAuth.Client.Extensions; + +/// +/// Provides extension methods for registering UltimateAuth client services. +/// +/// This layer is responsible for: +/// - Client-side authentication actions (login, logout, refresh, reauth) +/// - Browser-based POST infrastructure (JS form submit) +/// - Endpoint configuration for auth mutations +/// +/// This extension can safely be used together with AddUltimateAuthServer() +/// in Blazor Server applications. +/// +public static class ServiceCollectionExtensions +{ + /// + /// Registers core UltimateAuth client services. + /// + /// This package contains the platform-agnostic authentication engine: + /// - Authentication flows (login, logout, refresh, PKCE) + /// - Client state management + /// - Domain-level services (User, Session, Credential, Authorization) + /// + /// + /// IMPORTANT: + /// This package does NOT include any platform-specific implementations such as: + /// - HTTP / JS transport + /// - Storage (browser, mobile, etc.) + /// - UI integrations + /// + /// To use this in an application, you must install a client adapter package + /// such as: + /// - CodeBeam.UltimateAuth.Client.Blazor + /// - (future) CodeBeam.UltimateAuth.Client.Maui + /// + /// These adapter packages provide concrete implementations for: + /// - Request transport + /// - Storage + /// - Device identification + /// - Navigation / UI integration + /// + public static IServiceCollection AddUltimateAuthClient(this IServiceCollection services, Action? configure = null) + { + ArgumentNullException.ThrowIfNull(services); + + services.TryAddSingleton(); + + services.AddOptions() + .Configure((options, marker) => + { + if (configure != null) + { + marker.MarkConfigured(); + configure(options); + } + }) + .BindConfiguration("UltimateAuth:Client"); + + return services.AddUltimateAuthClientInternal(); + } + + /// + /// Internal shared registration pipeline for UltimateAuth client services. + /// + /// This method registers: + /// - Client infrastructure + /// - Public client abstractions + /// + /// NOTE: + /// This method does NOT register any server-side services. + /// + private static IServiceCollection AddUltimateAuthClientInternal(this IServiceCollection services) + { + services.AddScoped(); + + services.AddSingleton, UAuthClientOptionsValidator>(); + services.AddSingleton, UAuthClientEndpointOptionsValidator>(); + + services.AddSingleton(); + services.AddSingleton, UAuthClientOptionsPostConfigure>(); + services.TryAddSingleton(); + services.AddSingleton(); + + services.PostConfigure(o => + { + o.AutoRefresh.Interval ??= TimeSpan.FromMinutes(5); + }); + + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + + + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + + services.AddScoped(); + + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + + return services; + } +} diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/ClientClock.cs b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/ClientClock.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Infrastructure/ClientClock.cs rename to src/client/CodeBeam.UltimateAuth.Client/Infrastructure/ClientClock.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/Login/ClientLoginCapabilities.cs b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/ClientLoginCapabilities.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Infrastructure/Login/ClientLoginCapabilities.cs rename to src/client/CodeBeam.UltimateAuth.Client/Infrastructure/ClientLoginCapabilities.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/IBrowserUAuthBridge.cs b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/IBrowserUAuthBridge.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Infrastructure/IBrowserUAuthBridge.cs rename to src/client/CodeBeam.UltimateAuth.Client/Infrastructure/IBrowserUAuthBridge.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/IUAuthClientBootstrapper.cs b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/IUAuthClientBootstrapper.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Infrastructure/IUAuthClientBootstrapper.cs rename to src/client/CodeBeam.UltimateAuth.Client/Infrastructure/IUAuthClientBootstrapper.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/IUAuthRequestClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/IUAuthRequestClient.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Infrastructure/IUAuthRequestClient.cs rename to src/client/CodeBeam.UltimateAuth.Client/Infrastructure/IUAuthRequestClient.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpHubCapabilities.cs b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpHubCapabilities.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpHubCapabilities.cs rename to src/client/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpHubCapabilities.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpHubCredentialResolver.cs b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpHubCredentialResolver.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpHubCredentialResolver.cs rename to src/client/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpHubCredentialResolver.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpHubFlowReader.cs b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpHubFlowReader.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpHubFlowReader.cs rename to src/client/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpHubFlowReader.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpSessionCoordinator.cs b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpSessionCoordinator.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpSessionCoordinator.cs rename to src/client/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpSessionCoordinator.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/RefreshOutcomeParser.cs b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/RefreshOutcomeParser.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Infrastructure/RefreshOutcomeParser.cs rename to src/client/CodeBeam.UltimateAuth.Client/Infrastructure/RefreshOutcomeParser.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthClientBootstrapper.cs b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthClientBootstrapper.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthClientBootstrapper.cs rename to src/client/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthClientBootstrapper.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/Login/UAuthLoginPageAttribute.cs b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthLoginPageAttribute.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Infrastructure/Login/UAuthLoginPageAttribute.cs rename to src/client/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthLoginPageAttribute.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthResultMapper.cs b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthResultMapper.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthResultMapper.cs rename to src/client/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthResultMapper.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthUrlBuilder.cs b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthUrlBuilder.cs similarity index 95% rename from src/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthUrlBuilder.cs rename to src/client/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthUrlBuilder.cs index d5c03fe1..717c9378 100644 --- a/src/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthUrlBuilder.cs +++ b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthUrlBuilder.cs @@ -3,7 +3,7 @@ namespace CodeBeam.UltimateAuth.Client.Infrastructure; -internal static class UAuthUrlBuilder +public static class UAuthUrlBuilder { public static string Build(string authority, string relativePath, UAuthClientMultiTenantOptions tenant) { diff --git a/src/client/CodeBeam.UltimateAuth.Client/Options/ClientConfigurationMarker.cs b/src/client/CodeBeam.UltimateAuth.Client/Options/ClientConfigurationMarker.cs new file mode 100644 index 00000000..8d2b8f93 --- /dev/null +++ b/src/client/CodeBeam.UltimateAuth.Client/Options/ClientConfigurationMarker.cs @@ -0,0 +1,15 @@ +namespace CodeBeam.UltimateAuth.Client.Options; + +internal sealed class ClientConfigurationMarker +{ + private bool _configured; + + public void MarkConfigured() + { + if (_configured) + throw new InvalidOperationException("UltimateAuth client options were configured multiple times. " + + "Call AddUltimateAuthClient() OR AddUltimateAuthClientBlazor(), not both with configure delegates."); + + _configured = true; + } +} diff --git a/src/CodeBeam.UltimateAuth.Client/Options/UAuthClientAutoRefreshOptions.cs b/src/client/CodeBeam.UltimateAuth.Client/Options/UAuthClientAutoRefreshOptions.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Options/UAuthClientAutoRefreshOptions.cs rename to src/client/CodeBeam.UltimateAuth.Client/Options/UAuthClientAutoRefreshOptions.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Options/UAuthClientEndpointOptions.cs b/src/client/CodeBeam.UltimateAuth.Client/Options/UAuthClientEndpointOptions.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Options/UAuthClientEndpointOptions.cs rename to src/client/CodeBeam.UltimateAuth.Client/Options/UAuthClientEndpointOptions.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Options/UAuthClientLoginFlowOptions.cs b/src/client/CodeBeam.UltimateAuth.Client/Options/UAuthClientLoginFlowOptions.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Options/UAuthClientLoginFlowOptions.cs rename to src/client/CodeBeam.UltimateAuth.Client/Options/UAuthClientLoginFlowOptions.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Options/UAuthClientMultiTenantOptions.cs b/src/client/CodeBeam.UltimateAuth.Client/Options/UAuthClientMultiTenantOptions.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Options/UAuthClientMultiTenantOptions.cs rename to src/client/CodeBeam.UltimateAuth.Client/Options/UAuthClientMultiTenantOptions.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Options/UAuthClientOptions.cs b/src/client/CodeBeam.UltimateAuth.Client/Options/UAuthClientOptions.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Options/UAuthClientOptions.cs rename to src/client/CodeBeam.UltimateAuth.Client/Options/UAuthClientOptions.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Options/UAuthClientPkceLoginFlowOptions.cs b/src/client/CodeBeam.UltimateAuth.Client/Options/UAuthClientPkceLoginFlowOptions.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Options/UAuthClientPkceLoginFlowOptions.cs rename to src/client/CodeBeam.UltimateAuth.Client/Options/UAuthClientPkceLoginFlowOptions.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Options/UAuthClientProfileDetector.cs b/src/client/CodeBeam.UltimateAuth.Client/Options/UAuthClientProfileDetector.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Options/UAuthClientProfileDetector.cs rename to src/client/CodeBeam.UltimateAuth.Client/Options/UAuthClientProfileDetector.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Options/UAuthClientReauthOptions.cs b/src/client/CodeBeam.UltimateAuth.Client/Options/UAuthClientReauthOptions.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Options/UAuthClientReauthOptions.cs rename to src/client/CodeBeam.UltimateAuth.Client/Options/UAuthClientReauthOptions.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Options/UAuthOptionsPostConfigure.cs b/src/client/CodeBeam.UltimateAuth.Client/Options/UAuthOptionsPostConfigure.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Options/UAuthOptionsPostConfigure.cs rename to src/client/CodeBeam.UltimateAuth.Client/Options/UAuthOptionsPostConfigure.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Options/UAuthStateEventOptions.cs b/src/client/CodeBeam.UltimateAuth.Client/Options/UAuthStateEventOptions.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Options/UAuthStateEventOptions.cs rename to src/client/CodeBeam.UltimateAuth.Client/Options/UAuthStateEventOptions.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Options/Validators/UAuthClientEndpointOptionsValidator.cs b/src/client/CodeBeam.UltimateAuth.Client/Options/Validators/UAuthClientEndpointOptionsValidator.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Options/Validators/UAuthClientEndpointOptionsValidator.cs rename to src/client/CodeBeam.UltimateAuth.Client/Options/Validators/UAuthClientEndpointOptionsValidator.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Options/Validators/UAuthClientOptionsValidator.cs b/src/client/CodeBeam.UltimateAuth.Client/Options/Validators/UAuthClientOptionsValidator.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Options/Validators/UAuthClientOptionsValidator.cs rename to src/client/CodeBeam.UltimateAuth.Client/Options/Validators/UAuthClientOptionsValidator.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Runtime/IUAuthClientProductInfoProvider.cs b/src/client/CodeBeam.UltimateAuth.Client/Runtime/IUAuthClientProductInfoProvider.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Runtime/IUAuthClientProductInfoProvider.cs rename to src/client/CodeBeam.UltimateAuth.Client/Runtime/IUAuthClientProductInfoProvider.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Runtime/UAuthClientProductInfo.cs b/src/client/CodeBeam.UltimateAuth.Client/Runtime/UAuthClientProductInfo.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Runtime/UAuthClientProductInfo.cs rename to src/client/CodeBeam.UltimateAuth.Client/Runtime/UAuthClientProductInfo.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Runtime/UAuthClientProductInfoProvider.cs b/src/client/CodeBeam.UltimateAuth.Client/Runtime/UAuthClientProductInfoProvider.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Runtime/UAuthClientProductInfoProvider.cs rename to src/client/CodeBeam.UltimateAuth.Client/Runtime/UAuthClientProductInfoProvider.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/IAuthorizationClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IAuthorizationClient.cs similarity index 91% rename from src/CodeBeam.UltimateAuth.Client/Services/Abstractions/IAuthorizationClient.cs rename to src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IAuthorizationClient.cs index 6e45c78c..ca9ec42b 100644 --- a/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/IAuthorizationClient.cs +++ b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IAuthorizationClient.cs @@ -1,5 +1,4 @@ -using CodeBeam.UltimateAuth.Authorization; -using CodeBeam.UltimateAuth.Authorization.Contracts; +using CodeBeam.UltimateAuth.Authorization.Contracts; using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; diff --git a/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/ICredentialClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/ICredentialClient.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Services/Abstractions/ICredentialClient.cs rename to src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/ICredentialClient.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/IFlowClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IFlowClient.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Services/Abstractions/IFlowClient.cs rename to src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IFlowClient.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/ISessionClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/ISessionClient.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Services/Abstractions/ISessionClient.cs rename to src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/ISessionClient.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUAuthClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUAuthClient.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUAuthClient.cs rename to src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUAuthClient.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserClient.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserClient.cs rename to src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserClient.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserIdentifierClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserIdentifierClient.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserIdentifierClient.cs rename to src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserIdentifierClient.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Services/UAuthAuthorizationClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthAuthorizationClient.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Services/UAuthAuthorizationClient.cs rename to src/client/CodeBeam.UltimateAuth.Client/Services/UAuthAuthorizationClient.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Services/UAuthClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthClient.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Services/UAuthClient.cs rename to src/client/CodeBeam.UltimateAuth.Client/Services/UAuthClient.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Services/UAuthCredentialClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthCredentialClient.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Services/UAuthCredentialClient.cs rename to src/client/CodeBeam.UltimateAuth.Client/Services/UAuthCredentialClient.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Services/UAuthFlowClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthFlowClient.cs similarity index 96% rename from src/CodeBeam.UltimateAuth.Client/Services/UAuthFlowClient.cs rename to src/client/CodeBeam.UltimateAuth.Client/Services/UAuthFlowClient.cs index a9e68bf4..85d21ab3 100644 --- a/src/CodeBeam.UltimateAuth.Client/Services/UAuthFlowClient.cs +++ b/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthFlowClient.cs @@ -1,4 +1,5 @@ -using CodeBeam.UltimateAuth.Client.Contracts; +using CodeBeam.UltimateAuth.Client.Abstractions; +using CodeBeam.UltimateAuth.Client.Contracts; using CodeBeam.UltimateAuth.Client.Diagnostics; using CodeBeam.UltimateAuth.Client.Errors; using CodeBeam.UltimateAuth.Client.Events; @@ -9,7 +10,6 @@ using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.Infrastructure; using CodeBeam.UltimateAuth.Users.Contracts; -using Microsoft.AspNetCore.Components; using Microsoft.Extensions.Options; using System.Net; using System.Security.Cryptography; @@ -23,18 +23,18 @@ internal class UAuthFlowClient : IFlowClient private readonly IUAuthRequestClient _post; private readonly IUAuthClientEvents _events; private readonly IDeviceIdProvider _deviceIdProvider; + private readonly IReturnUrlProvider _returnUrlProvider; private readonly UAuthClientOptions _options; private readonly UAuthClientDiagnostics _diagnostics; - private readonly NavigationManager _nav; - public UAuthFlowClient(IUAuthRequestClient post, IUAuthClientEvents events, IDeviceIdProvider deviceIdProvider, IOptions options, UAuthClientDiagnostics diagnostics, NavigationManager nav) + public UAuthFlowClient(IUAuthRequestClient post, IUAuthClientEvents events, IDeviceIdProvider deviceIdProvider, IReturnUrlProvider returnUrlProvider, IOptions options, UAuthClientDiagnostics diagnostics) { _post = post; _events = events; _deviceIdProvider = deviceIdProvider; + _returnUrlProvider = returnUrlProvider; _options = options.Value; _diagnostics = diagnostics; - _nav = nav; } private string Url(string path) => UAuthUrlBuilder.Build(_options.Endpoints.BasePath, path, _options.MultiTenant); @@ -208,7 +208,7 @@ public async Task BeginPkceAsync(string? returnUrl = null) ?? pkce.ReturnUrl ?? _options.Login.ReturnUrl ?? _options.DefaultReturnUrl - ?? _nav.Uri; + ?? _returnUrlProvider.GetCurrentUrl(); if (pkce.AutoRedirect) { diff --git a/src/CodeBeam.UltimateAuth.Client/Services/UAuthSessionClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthSessionClient.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Services/UAuthSessionClient.cs rename to src/client/CodeBeam.UltimateAuth.Client/Services/UAuthSessionClient.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Services/UAuthUserClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthUserClient.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Services/UAuthUserClient.cs rename to src/client/CodeBeam.UltimateAuth.Client/Services/UAuthUserClient.cs diff --git a/src/CodeBeam.UltimateAuth.Client/Services/UAuthUserIdentifierClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthUserIdentifierClient.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Client/Services/UAuthUserIdentifierClient.cs rename to src/client/CodeBeam.UltimateAuth.Client/Services/UAuthUserIdentifierClient.cs diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/CodeBeam.UltimateAuth.Credentials.Contracts.csproj b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/CodeBeam.UltimateAuth.Credentials.Contracts.csproj index ce41f1eb..064576d3 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/CodeBeam.UltimateAuth.Credentials.Contracts.csproj +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/CodeBeam.UltimateAuth.Credentials.Contracts.csproj @@ -4,14 +4,40 @@ net8.0;net9.0;net10.0 enable enable - 0.0.1 - 0.0.1 - true $(NoWarn);1591 + + CodeBeam.UltimateAuth.Credentials.Contracts + 0.1.0-preview.1 + + CodeBeam + CodeBeam + + + Shared contracts and cross-boundary types for UltimateAuth Credentials module. + Includes credential identifiers, DTOs and shared models used between client and server. + Does NOT include domain logic or persistence. + + + authentication;credentials;identity;contracts;shared;dto;auth-framework + + README.md + https://github.com/CodeBeamOrg/UltimateAuth + https://github.com/CodeBeamOrg/UltimateAuth + Apache-2.0 + + true + true + snupkg + + + + + +
diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/README.md b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/README.md new file mode 100644 index 00000000..a1c1bd37 --- /dev/null +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/README.md @@ -0,0 +1,32 @@ +# UltimateAuth Credentials Contracts + +Shared contracts and cross-boundary models for the Credentials module. + +## Purpose + +This package contains: + +- Credential identifiers +- DTOs +- Shared models used between client and server + +## Does NOT include + +- Domain logic +- Persistence +- Security implementations + +## Usage + +Used by: + +- Server implementations +- Client SDKs +- Custom credential providers + +⚠️ Usually installed transitively via: + +- CodeBeam.UltimateAuth.Server +- CodeBeam.UltimateAuth.Client + +No need to install it directly in most scenarios. \ No newline at end of file diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Data/UAuthCredentialDbContext.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Data/UAuthCredentialDbContext.cs index ed530b36..6e40935a 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Data/UAuthCredentialDbContext.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Data/UAuthCredentialDbContext.cs @@ -1,7 +1,5 @@ using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.MultiTenancy; -using CodeBeam.UltimateAuth.Credentials.Contracts; -using CodeBeam.UltimateAuth.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; namespace CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore; @@ -10,12 +8,9 @@ internal sealed class UAuthCredentialDbContext : DbContext { public DbSet PasswordCredentials => Set(); - private readonly TenantContext _tenant; - - public UAuthCredentialDbContext(DbContextOptions options, TenantContext tenant) + public UAuthCredentialDbContext(DbContextOptions options) : base(options) { - _tenant = tenant; } protected override void OnModelCreating(ModelBuilder b) @@ -27,6 +22,7 @@ private void ConfigurePasswordCredential(ModelBuilder b) { b.Entity(e => { + e.ToTable("UAuth_PasswordCredentials"); e.HasKey(x => x.Id); e.Property(x => x.Version).IsConcurrencyToken(); @@ -63,8 +59,6 @@ private void ConfigurePasswordCredential(ModelBuilder b) e.HasIndex(x => new { x.Tenant, x.UserKey, x.DeletedAt }); e.HasIndex(x => new { x.Tenant, x.RevokedAt }); e.HasIndex(x => new { x.Tenant, x.ExpiresAt }); - - e.HasQueryFilter(x => _tenant.IsGlobal || x.Tenant == _tenant.Tenant); }); } } \ No newline at end of file diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs index e235a2a1..23f8b844 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs @@ -2,14 +2,14 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -namespace CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore; +namespace CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore.Extensions; public static class ServiceCollectionExtensions { - public static IServiceCollection AddUltimateAuthEntityFrameworkCoreCredentials(this IServiceCollection services, Action configureDb) + public static IServiceCollection AddUltimateAuthCredentialsEntityFrameworkCore(this IServiceCollection services, Action configureDb) { - services.AddDbContextPool(configureDb); - services.AddScoped(); + services.AddDbContext(configureDb); + services.AddScoped(); return services; } } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Mappers/PasswordCredentialProjectionMapper.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Mappers/PasswordCredentialProjectionMapper.cs index 8fbf3854..8d91b84b 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Mappers/PasswordCredentialProjectionMapper.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Mappers/PasswordCredentialProjectionMapper.cs @@ -18,16 +18,20 @@ public static PasswordCredential ToDomain(this PasswordCredentialProjection p) Source = p.Source }; - return PasswordCredential.Create( + return PasswordCredential.FromProjection( id: p.Id, tenant: p.Tenant, userKey: p.UserKey, secretHash: p.SecretHash, security: security, metadata: metadata, - now: p.CreatedAt + createdAt: p.CreatedAt, + updatedAt: p.UpdatedAt, + deletedAt: p.DeletedAt, + version: p.Version ); } + public static PasswordCredentialProjection ToProjection(this PasswordCredential c) { return new PasswordCredentialProjection @@ -63,5 +67,6 @@ public static void UpdateProjection(this PasswordCredential c, PasswordCredentia p.Source = c.Metadata.Source; p.UpdatedAt = c.UpdatedAt; + p.DeletedAt = c.DeletedAt; } } \ No newline at end of file diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Stores/EfCorePasswordCredentialStore.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Stores/EfCorePasswordCredentialStore.cs index ae1fe5f0..bf533435 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Stores/EfCorePasswordCredentialStore.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Stores/EfCorePasswordCredentialStore.cs @@ -11,12 +11,12 @@ namespace CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore; internal sealed class EfCorePasswordCredentialStore : IPasswordCredentialStore { private readonly UAuthCredentialDbContext _db; - private readonly TenantContext _tenant; + private readonly TenantKey _tenant; public EfCorePasswordCredentialStore(UAuthCredentialDbContext db, TenantContext tenant) { _db = db; - _tenant = tenant; + _tenant = tenant.Tenant; } public async Task ExistsAsync(CredentialKey key, CancellationToken ct = default) @@ -24,14 +24,16 @@ public async Task ExistsAsync(CredentialKey key, CancellationToken ct = de return await _db.PasswordCredentials .AnyAsync(x => x.Id == key.Id && - x.Tenant == key.Tenant, + x.Tenant == _tenant, ct); } public async Task AddAsync(PasswordCredential credential, CancellationToken ct = default) { var entity = credential.ToProjection(); + _db.PasswordCredentials.Add(entity); + await _db.SaveChangesAsync(ct); } @@ -41,7 +43,7 @@ public async Task AddAsync(PasswordCredential credential, CancellationToken ct = .AsNoTracking() .SingleOrDefaultAsync( x => x.Id == key.Id && - x.Tenant == key.Tenant, + x.Tenant == _tenant, ct); return entity?.ToDomain(); @@ -52,7 +54,7 @@ public async Task SaveAsync(PasswordCredential credential, long expectedVersion, var entity = await _db.PasswordCredentials .SingleOrDefaultAsync(x => x.Id == credential.Id && - x.Tenant == credential.Tenant, + x.Tenant == _tenant, ct); if (entity is null) @@ -63,18 +65,30 @@ public async Task SaveAsync(PasswordCredential credential, long expectedVersion, credential.UpdateProjection(entity); entity.Version++; + await _db.SaveChangesAsync(ct); } public async Task RevokeAsync(CredentialKey key, DateTimeOffset revokedAt, long expectedVersion, CancellationToken ct = default) { - var credential = await GetAsync(key, ct); + var entity = await _db.PasswordCredentials + .SingleOrDefaultAsync(x => + x.Id == key.Id && + x.Tenant == _tenant, + ct); - if (credential is null) + if (entity is null) throw new UAuthNotFoundException("credential_not_found"); - var revoked = credential.Revoke(revokedAt); - await SaveAsync(revoked, expectedVersion, ct); + if (entity.Version != expectedVersion) + throw new UAuthConcurrencyException("credential_version_conflict"); + + var domain = entity.ToDomain().Revoke(revokedAt); + domain.UpdateProjection(entity); + + entity.Version++; + + await _db.SaveChangesAsync(ct); } public async Task DeleteAsync(CredentialKey key, long expectedVersion, DeleteMode mode, DateTimeOffset now, CancellationToken ct = default) @@ -82,7 +96,7 @@ public async Task DeleteAsync(CredentialKey key, long expectedVersion, DeleteMod var entity = await _db.PasswordCredentials .SingleOrDefaultAsync(x => x.Id == key.Id && - x.Tenant == key.Tenant, + x.Tenant == _tenant, ct); if (entity is null) @@ -97,19 +111,20 @@ public async Task DeleteAsync(CredentialKey key, long expectedVersion, DeleteMod } else { - entity.DeletedAt = now; + var domain = entity.ToDomain().MarkDeleted(now); + domain.UpdateProjection(entity); entity.Version++; } await _db.SaveChangesAsync(ct); } - public async Task> GetByUserAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default) + public async Task> GetByUserAsync(UserKey userKey, CancellationToken ct = default) { var entities = await _db.PasswordCredentials .AsNoTracking() .Where(x => - x.Tenant == tenant && + x.Tenant == _tenant && x.UserKey == userKey && x.DeletedAt == null) .ToListAsync(ct); @@ -120,27 +135,28 @@ public async Task> GetByUserAsync(Tenant .AsReadOnly(); } - public async Task DeleteByUserAsync(TenantKey tenant, UserKey userKey, DeleteMode mode, DateTimeOffset now, CancellationToken ct = default) + public async Task DeleteByUserAsync(UserKey userKey, DeleteMode mode, DateTimeOffset now, CancellationToken ct = default) { - var entities = await _db.PasswordCredentials - .Where(x => - x.Tenant == tenant && - x.UserKey == userKey) - .ToListAsync(ct); - - foreach (var entity in entities) + if (mode == DeleteMode.Hard) { - if (mode == DeleteMode.Hard) - { - _db.PasswordCredentials.Remove(entity); - } - else - { - entity.DeletedAt = now; - entity.Version++; - } + await _db.PasswordCredentials + .Where(x => + x.Tenant == _tenant && + x.UserKey == userKey) + .ExecuteDeleteAsync(ct); + + return; } - await _db.SaveChangesAsync(ct); + await _db.PasswordCredentials + .Where(x => + x.Tenant == _tenant && + x.UserKey == userKey && + x.DeletedAt == null) + .ExecuteUpdateAsync(x => + x + .SetProperty(c => c.DeletedAt, now) + .SetProperty(c => c.Version, c => c.Version + 1), + ct); } -} \ No newline at end of file +} diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Stores/EfCorePasswordCredentialStoreFactory.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Stores/EfCorePasswordCredentialStoreFactory.cs new file mode 100644 index 00000000..ba037a79 --- /dev/null +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Stores/EfCorePasswordCredentialStoreFactory.cs @@ -0,0 +1,19 @@ +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Credentials.Reference; + +namespace CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore; + +internal sealed class EfCorePasswordCredentialStoreFactory : IPasswordCredentialStoreFactory +{ + private readonly UAuthCredentialDbContext _db; + + public EfCorePasswordCredentialStoreFactory(UAuthCredentialDbContext db) + { + _db = db; + } + + public IPasswordCredentialStore Create(TenantKey tenant) + { + return new EfCorePasswordCredentialStore(_db, new TenantContext(tenant)); + } +} diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/CodeBeam.UltimateAuth.Credentials.InMemory.csproj b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/CodeBeam.UltimateAuth.Credentials.InMemory.csproj index 9229218f..d49bfe49 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/CodeBeam.UltimateAuth.Credentials.InMemory.csproj +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/CodeBeam.UltimateAuth.Credentials.InMemory.csproj @@ -12,6 +12,7 @@ + diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryCredentialSeedContributor.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryCredentialSeedContributor.cs index ed3e2ade..84b01dbc 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryCredentialSeedContributor.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryCredentialSeedContributor.cs @@ -1,10 +1,10 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.Errors; -using CodeBeam.UltimateAuth.Core.Infrastructure; using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Credentials.Contracts; using CodeBeam.UltimateAuth.Credentials.Reference; +using CodeBeam.UltimateAuth.InMemory; namespace CodeBeam.UltimateAuth.Credentials.InMemory; @@ -14,14 +14,14 @@ internal sealed class InMemoryCredentialSeedContributor : ISeedContributor private static readonly Guid _userPasswordId = Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"); public int Order => 10; - private readonly IPasswordCredentialStore _credentials; + private readonly IPasswordCredentialStoreFactory _credentialFactory; private readonly IInMemoryUserIdProvider _ids; private readonly IUAuthPasswordHasher _hasher; private readonly IClock _clock; - public InMemoryCredentialSeedContributor(IPasswordCredentialStore credentials, IInMemoryUserIdProvider ids, IUAuthPasswordHasher hasher, IClock clock) + public InMemoryCredentialSeedContributor(IPasswordCredentialStoreFactory credentialFactory, IInMemoryUserIdProvider ids, IUAuthPasswordHasher hasher, IClock clock) { - _credentials = credentials; + _credentialFactory = credentialFactory; _ids = ids; _hasher = hasher; _clock = clock; @@ -37,7 +37,8 @@ private async Task SeedCredentialAsync(UserKey userKey, Guid credentialId, strin { try { - await _credentials.AddAsync( + var credentialStore = _credentialFactory.Create(tenant); + await credentialStore.AddAsync( PasswordCredential.Create( credentialId, tenant, diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryPasswordCredentialStore.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryPasswordCredentialStore.cs index 61f22f38..0d1d7981 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryPasswordCredentialStore.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryPasswordCredentialStore.cs @@ -1,21 +1,25 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.Errors; using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Credentials.Contracts; using CodeBeam.UltimateAuth.Credentials.Reference; +using CodeBeam.UltimateAuth.InMemory; namespace CodeBeam.UltimateAuth.Credentials.InMemory; -internal sealed class InMemoryPasswordCredentialStore : InMemoryVersionedStore, IPasswordCredentialStore +internal sealed class InMemoryPasswordCredentialStore : InMemoryTenantVersionedStore, IPasswordCredentialStore { protected override CredentialKey GetKey(PasswordCredential entity) => new(entity.Tenant, entity.Id); + public InMemoryPasswordCredentialStore(TenantContext tenant) : base(tenant) + { + } + protected override void BeforeAdd(PasswordCredential entity) { - var exists = Values() + var exists = TenantValues() .Any(x => x.Tenant == entity.Tenant && x.UserKey == entity.UserKey && @@ -25,13 +29,12 @@ protected override void BeforeAdd(PasswordCredential entity) throw new UAuthConflictException("password_credential_exists"); } - public Task> GetByUserAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default) + public Task> GetByUserAsync(UserKey userKey, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - var result = Values() + var result = TenantValues() .Where(x => - x.Tenant == tenant && x.UserKey == userKey && !x.IsDeleted) .Select(x => x.Snapshot()) @@ -50,15 +53,15 @@ public Task RevokeAsync(CredentialKey key, DateTimeOffset revokedAt, long expect return SaveAsync(revoked, expectedVersion, ct); } - public async Task DeleteByUserAsync(TenantKey tenant, UserKey userKey, DeleteMode mode, DateTimeOffset now, CancellationToken ct = default) + public async Task DeleteByUserAsync(UserKey userKey, DeleteMode mode, DateTimeOffset now, CancellationToken ct = default) { - var credentials = Values() - .Where(c => c.Tenant == tenant && c.UserKey == userKey) + var credentials = TenantValues() + .Where(c => c.UserKey == userKey) .ToList(); foreach (var credential in credentials) { - await DeleteAsync(new CredentialKey(tenant, credential.Id), credential.Version, mode, now, ct); + await DeleteAsync(GetKey(credential), credential.Version, mode, now, ct); } } } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryPasswordCredentialStoreFactory.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryPasswordCredentialStoreFactory.cs new file mode 100644 index 00000000..6258724a --- /dev/null +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryPasswordCredentialStoreFactory.cs @@ -0,0 +1,15 @@ +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Credentials.Reference; +using System.Collections.Concurrent; + +namespace CodeBeam.UltimateAuth.Credentials.InMemory; + +public sealed class InMemoryPasswordCredentialStoreFactory : IPasswordCredentialStoreFactory +{ + private readonly ConcurrentDictionary _stores = new(); + + public IPasswordCredentialStore Create(TenantKey tenant) + { + return _stores.GetOrAdd(tenant, t => new InMemoryPasswordCredentialStore(new TenantContext(t))); + } +} diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/ServiceCollectionExtensions.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/ServiceCollectionExtensions.cs index 86118255..ef75640c 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/ServiceCollectionExtensions.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/ServiceCollectionExtensions.cs @@ -9,8 +9,7 @@ public static class ServiceCollectionExtensions { public static IServiceCollection AddUltimateAuthCredentialsInMemory(this IServiceCollection services) { - services.TryAddScoped(); - services.TryAddSingleton(); + services.TryAddSingleton(); // Never try add seed services.AddSingleton(); diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Domain/PasswordCredential.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Domain/PasswordCredential.cs index ee5c9c64..6e24cb6c 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Domain/PasswordCredential.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Domain/PasswordCredential.cs @@ -6,7 +6,7 @@ namespace CodeBeam.UltimateAuth.Credentials.Reference; -public sealed class PasswordCredential : ISecretCredential, IVersionedEntity, IEntitySnapshot, ISoftDeletable +public sealed class PasswordCredential : ISecretCredential, ITenantEntity, IVersionedEntity, IEntitySnapshot, ISoftDeletable { public Guid Id { get; init; } public TenantKey Tenant { get; init; } @@ -151,4 +151,29 @@ public PasswordCredential MarkDeleted(DateTimeOffset now) return this; } + + public static PasswordCredential FromProjection( + Guid id, + TenantKey tenant, + UserKey userKey, + string secretHash, + CredentialSecurityState security, + CredentialMetadata metadata, + DateTimeOffset createdAt, + DateTimeOffset? updatedAt, + DateTimeOffset? deletedAt, + long version) + { + return new PasswordCredential( + id, + tenant, + userKey, + secretHash, + security, + metadata, + createdAt, + updatedAt, + deletedAt, + version); + } } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Extensions/ServiceCollectionExtensions.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Extensions/ServiceCollectionExtensions.cs index 2ee133b7..b7c8ae8a 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Extensions/ServiceCollectionExtensions.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Extensions/ServiceCollectionExtensions.cs @@ -4,18 +4,17 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -namespace CodeBeam.UltimateAuth.Credentials.Reference +namespace CodeBeam.UltimateAuth.Credentials.Reference.Extensions; + +public static class ServiceCollectionExtensions { - public static class ServiceCollectionExtensions + public static IServiceCollection AddUltimateAuthCredentialsReference(this IServiceCollection services) { - public static IServiceCollection AddUltimateAuthCredentialsReference(this IServiceCollection services) - { - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.AddScoped(); - return services; - } + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.AddScoped(); + return services; } } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Infrastructure/PasswordCredentialProvider.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Infrastructure/PasswordCredentialProvider.cs index cfe691bb..4f5dac13 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Infrastructure/PasswordCredentialProvider.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Infrastructure/PasswordCredentialProvider.cs @@ -5,20 +5,21 @@ namespace CodeBeam.UltimateAuth.Credentials.Reference; internal sealed class PasswordCredentialProvider : ICredentialProvider { - private readonly IPasswordCredentialStore _store; + private readonly IPasswordCredentialStoreFactory _storeFactory; private readonly ICredentialValidator _validator; public CredentialType Type => CredentialType.Password; - public PasswordCredentialProvider(IPasswordCredentialStore store, ICredentialValidator validator) + public PasswordCredentialProvider(IPasswordCredentialStoreFactory storeFactory, ICredentialValidator validator) { - _store = store; + _storeFactory = storeFactory; _validator = validator; } public async Task> GetByUserAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default) { - var creds = await _store.GetByUserAsync(tenant, userKey, ct); + var store = _storeFactory.Create(tenant); + var creds = await store.GetByUserAsync(userKey, ct); return creds.Cast().ToList(); } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Infrastructure/PasswordUserLifecycleIntegration.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Infrastructure/PasswordUserLifecycleIntegration.cs index 039559f7..918ea9c4 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Infrastructure/PasswordUserLifecycleIntegration.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Infrastructure/PasswordUserLifecycleIntegration.cs @@ -10,13 +10,13 @@ namespace CodeBeam.UltimateAuth.Credentials.Reference; internal sealed class PasswordUserLifecycleIntegration : IUserLifecycleIntegration { - private readonly IPasswordCredentialStore _credentialStore; + private readonly IPasswordCredentialStoreFactory _credentialStoreFactory; private readonly IUAuthPasswordHasher _passwordHasher; private readonly IClock _clock; - public PasswordUserLifecycleIntegration(IPasswordCredentialStore credentialStore, IUAuthPasswordHasher passwordHasher, IClock clock) + public PasswordUserLifecycleIntegration(IPasswordCredentialStoreFactory credentialStoreFactory, IUAuthPasswordHasher passwordHasher, IClock clock) { - _credentialStore = credentialStore; + _credentialStoreFactory = credentialStoreFactory; _passwordHasher = passwordHasher; _clock = clock; } @@ -40,11 +40,13 @@ public async Task OnUserCreatedAsync(TenantKey tenant, UserKey userKey, object r metadata: new CredentialMetadata { }, _clock.UtcNow); - await _credentialStore.AddAsync(credential, ct); + var credentialStore = _credentialStoreFactory.Create(tenant); + await credentialStore.AddAsync(credential, ct); } public async Task OnUserDeletedAsync(TenantKey tenant, UserKey userKey, DeleteMode mode, CancellationToken ct) { - await _credentialStore.DeleteByUserAsync(tenant, userKey, mode, _clock.UtcNow, ct); + var credentialStore = _credentialStoreFactory.Create(tenant); + await credentialStore.DeleteByUserAsync(userKey, mode, _clock.UtcNow, ct); } } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Services/CredentialManagementService.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Services/CredentialManagementService.cs index df782936..ad0a4ae0 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Services/CredentialManagementService.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Services/CredentialManagementService.cs @@ -8,7 +8,6 @@ using CodeBeam.UltimateAuth.Server.Infrastructure; using CodeBeam.UltimateAuth.Server.Options; using CodeBeam.UltimateAuth.Users; -using Microsoft.AspNetCore.Session; using Microsoft.Extensions.Options; namespace CodeBeam.UltimateAuth.Credentials.Reference; @@ -17,7 +16,7 @@ namespace CodeBeam.UltimateAuth.Credentials.Reference; internal sealed class CredentialManagementService : ICredentialManagementService, IUserCredentialsInternalService { private readonly IAccessOrchestrator _accessOrchestrator; - private readonly IPasswordCredentialStore _credentials; + private readonly IPasswordCredentialStoreFactory _credentialsFactory; private readonly IAuthenticationSecurityManager _authenticationSecurityManager; private readonly IOpaqueTokenGenerator _tokenGenerator; private readonly INumericCodeGenerator _numericCodeGenerator; @@ -30,7 +29,7 @@ internal sealed class CredentialManagementService : ICredentialManagementService public CredentialManagementService( IAccessOrchestrator accessOrchestrator, - IPasswordCredentialStore credentials, + IPasswordCredentialStoreFactory credentialsFactory, IAuthenticationSecurityManager authenticationSecurityManager, IOpaqueTokenGenerator tokenGenerator, INumericCodeGenerator numericCodeGenerator, @@ -42,7 +41,7 @@ public CredentialManagementService( IClock clock) { _accessOrchestrator = accessOrchestrator; - _credentials = credentials; + _credentialsFactory = credentialsFactory; _authenticationSecurityManager = authenticationSecurityManager; _tokenGenerator = tokenGenerator; _numericCodeGenerator = numericCodeGenerator; @@ -62,8 +61,8 @@ public async Task GetAllAsync(AccessContext context, Cance { var subjectUser = context.GetTargetUserKey(); var now = _clock.UtcNow; - - var credentials = await _credentials.GetByUserAsync(context.ResourceTenant, subjectUser, innerCt); + var store = _credentialsFactory.Create(context.ResourceTenant); + var credentials = await store.GetByUserAsync(subjectUser, innerCt); var dtos = credentials .Select(c => new CredentialInfo @@ -105,7 +104,8 @@ public async Task AddAsync(AccessContext context, AddCreden metadata: new CredentialMetadata(), now: now); - await _credentials.AddAsync(credential, innerCt); + var store = _credentialsFactory.Create(context.ResourceTenant); + await store.AddAsync(credential, innerCt); return AddCredentialResult.Success(credential.Id, credential.Type); }); @@ -122,8 +122,9 @@ public async Task ChangeSecretAsync(AccessContext contex var subjectUser = context.GetTargetUserKey(); var now = _clock.UtcNow; - var credentials = await _credentials.GetByUserAsync(context.ResourceTenant, subjectUser, innerCt); - var pwd = credentials.OfType().Where(c => c.Security.IsUsable(now)).SingleOrDefault(); + var store = _credentialsFactory.Create(context.ResourceTenant); + var credentials = await store.GetByUserAsync(subjectUser, innerCt); + var pwd = credentials.OfType().Where(c => c.Security.IsUsable(now)).FirstOrDefault(); if (pwd is null) throw new UAuthNotFoundException("credential_not_found"); @@ -146,16 +147,16 @@ public async Task ChangeSecretAsync(AccessContext contex var oldVersion = pwd.Version; var newHash = _hasher.Hash(request.NewSecret); var updated = pwd.ChangeSecret(newHash, now); - await _credentials.SaveAsync(updated, oldVersion, innerCt); + await store.SaveAsync(updated, oldVersion, innerCt); var sessionStore = _sessionFactory.Create(context.ResourceTenant); if (context.IsSelfAction && context.ActorChainId is SessionChainId chainId) { - await sessionStore.RevokeOtherChainsAsync(context.ResourceTenant, subjectUser, chainId, now, innerCt); + await sessionStore.RevokeOtherChainsAsync(subjectUser, chainId, now, innerCt); } else { - await sessionStore.RevokeAllChainsAsync(context.ResourceTenant, subjectUser, now, innerCt); + await sessionStore.RevokeAllChainsAsync(subjectUser, now, innerCt); } return ChangeCredentialResult.Success(pwd.Type); @@ -173,7 +174,8 @@ public async Task RevokeAsync(AccessContext context, Rev var subjectUser = context.GetTargetUserKey(); var now = _clock.UtcNow; - var credential = await _credentials.GetAsync(new CredentialKey(context.ResourceTenant, request.Id), innerCt); + var store = _credentialsFactory.Create(context.ResourceTenant); + var credential = await store.GetAsync(new CredentialKey(context.ResourceTenant, request.Id), innerCt); if (credential is not PasswordCredential pwd) return CredentialActionResult.Fail("credential_not_found"); @@ -183,7 +185,7 @@ public async Task RevokeAsync(AccessContext context, Rev var oldVersion = pwd.Version; var updated = pwd.Revoke(now); - await _credentials.SaveAsync(updated, oldVersion, innerCt); + await store.SaveAsync(updated, oldVersion, innerCt); return CredentialActionResult.Success(); }); @@ -294,7 +296,8 @@ public async Task CompleteResetAsync(AccessContext conte throw new UAuthConflictException("invalid_reset_token"); } - var credentials = await _credentials.GetByUserAsync(context.ResourceTenant, userKey, innerCt); + var store = _credentialsFactory.Create(context.ResourceTenant); + var credentials = await store.GetByUserAsync(userKey, innerCt); var pwd = credentials.OfType().FirstOrDefault(c => c.Security.IsUsable(now)); if (pwd is null) @@ -311,7 +314,7 @@ public async Task CompleteResetAsync(AccessContext conte var newHash = _hasher.Hash(request.NewSecret); var updated = pwd.ChangeSecret(newHash, now); - await _credentials.SaveAsync(updated, oldVersion, innerCt); + await store.SaveAsync(updated, oldVersion, innerCt); return CredentialActionResult.Success(); }); @@ -351,7 +354,8 @@ public async Task DeleteAsync(AccessContext context, Del var subjectUser = context.GetTargetUserKey(); var now = _clock.UtcNow; - var credential = await _credentials.GetAsync(new CredentialKey(context.ResourceTenant, request.Id), innerCt); + var store = _credentialsFactory.Create(context.ResourceTenant); + var credential = await store.GetAsync(new CredentialKey(context.ResourceTenant, request.Id), innerCt); if (credential is not PasswordCredential pwd) return CredentialActionResult.Fail("credential_not_found"); @@ -360,7 +364,7 @@ public async Task DeleteAsync(AccessContext context, Del return CredentialActionResult.Fail("credential_not_found"); var oldVersion = pwd.Version; - await _credentials.DeleteAsync(new CredentialKey(context.ResourceTenant, pwd.Id), oldVersion, request.Mode, now, innerCt); + await store.DeleteAsync(new CredentialKey(context.ResourceTenant, pwd.Id), oldVersion, request.Mode, now, innerCt); return CredentialActionResult.Success(); }); @@ -375,7 +379,8 @@ async Task IUserCredentialsInternalService.DeleteInterna { ct.ThrowIfCancellationRequested(); - await _credentials.DeleteByUserAsync(tenant, userKey, DeleteMode.Soft, _clock.UtcNow, ct); + var store = _credentialsFactory.Create(tenant); + await store.DeleteByUserAsync(userKey, DeleteMode.Soft, _clock.UtcNow, ct); return CredentialActionResult.Success(); } } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/IPasswordCredentialStore.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Stores/IPasswordCredentialStore.cs similarity index 68% rename from src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/IPasswordCredentialStore.cs rename to src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Stores/IPasswordCredentialStore.cs index 6bdb1d05..3a044eec 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/IPasswordCredentialStore.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Stores/IPasswordCredentialStore.cs @@ -1,14 +1,13 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Credentials.Contracts; namespace CodeBeam.UltimateAuth.Credentials.Reference; public interface IPasswordCredentialStore : IVersionedStore { - Task> GetByUserAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default); - Task DeleteByUserAsync(TenantKey tenant, UserKey userKey, DeleteMode mode, DateTimeOffset now, CancellationToken ct = default); + Task> GetByUserAsync(UserKey userKey, CancellationToken ct = default); + Task DeleteByUserAsync(UserKey userKey, DeleteMode mode, DateTimeOffset now, CancellationToken ct = default); Task RevokeAsync(CredentialKey key, DateTimeOffset revokedAt, long expectedVersion, CancellationToken ct = default); } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Stores/IPasswordCredentialStoreFactory.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Stores/IPasswordCredentialStoreFactory.cs new file mode 100644 index 00000000..4ab1ef83 --- /dev/null +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Stores/IPasswordCredentialStoreFactory.cs @@ -0,0 +1,8 @@ +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Credentials.Reference; + +public interface IPasswordCredentialStoreFactory +{ + IPasswordCredentialStore Create(TenantKey tenant); +} diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/ICredentialStore.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/ICredentialStore.cs deleted file mode 100644 index 36daf785..00000000 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/ICredentialStore.cs +++ /dev/null @@ -1,18 +0,0 @@ -//using CodeBeam.UltimateAuth.Core.Abstractions; -//using CodeBeam.UltimateAuth.Core.Contracts; -//using CodeBeam.UltimateAuth.Core.Domain; -//using CodeBeam.UltimateAuth.Core.MultiTenancy; -//using CodeBeam.UltimateAuth.Credentials.Contracts; - -//namespace CodeBeam.UltimateAuth.Credentials; - -//public interface ICredentialStore : IVersionedStore -//{ -// Task> GetByUserAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default); - -// Task GetByIdAsync(CredentialKey key, CancellationToken ct = default); - -// Task RevokeAsync(CredentialKey key, DateTimeOffset revokedAt, long expectedVersion, CancellationToken ct = default); - -// Task DeleteByUserAsync(TenantKey tenant, UserKey userKey, DeleteMode mode, DateTimeOffset now, CancellationToken ct = default); -//} diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials/CodeBeam.UltimateAuth.Credentials.csproj b/src/credentials/CodeBeam.UltimateAuth.Credentials/CodeBeam.UltimateAuth.Credentials.csproj index 12ee515c..c05dc2ab 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials/CodeBeam.UltimateAuth.Credentials.csproj +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials/CodeBeam.UltimateAuth.Credentials.csproj @@ -4,15 +4,44 @@ net8.0;net9.0;net10.0 enable enable - 0.0.1 - 0.0.1 - true $(NoWarn);1591 + + CodeBeam.UltimateAuth.Credentials + 0.1.0-preview.1 + + CodeBeam + CodeBeam + + + Credentials module for UltimateAuth. + Provides orchestration, abstractions and dependency injection wiring for credential management functionality. + Use with a persistence provider such as EntityFrameworkCore or InMemory. + This package is included transitively by CodeBeam.UltimateAuth.Server and usually does not need to be installed directly. + + + authentication;credentials;identity;security;module;auth-framework + + README.md + https://github.com/CodeBeamOrg/UltimateAuth + https://github.com/CodeBeamOrg/UltimateAuth + Apache-2.0 + + true + true + snupkg + + true + + + + + + + - - + -
+
\ No newline at end of file diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials/README.md b/src/credentials/CodeBeam.UltimateAuth.Credentials/README.md new file mode 100644 index 00000000..82e95677 --- /dev/null +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials/README.md @@ -0,0 +1,22 @@ +# UltimateAuth Credentials + +Credential management module for UltimateAuth. + +## Purpose + +This package provides: + +- Dependency injection setup +- Credential module orchestration +- Integration points for credential providers + +## Does NOT include + +- Persistence (use EntityFrameworkCore or InMemory packages) +- Domain implementation (use Reference package if needed) + +⚠️ This package is typically installed transitively via: + +- CodeBeam.UltimateAuth.Server + +In most cases, you do not need to install it directly unless you are building custom integrations. \ No newline at end of file diff --git a/src/persistence/CodeBeam.UltimateAuth.InMemory.Abstractions/CodeBeam.UltimateAuth.InMemory.Abstractions.csproj b/src/persistence/CodeBeam.UltimateAuth.InMemory.Abstractions/CodeBeam.UltimateAuth.InMemory.Abstractions.csproj new file mode 100644 index 00000000..1f3e2def --- /dev/null +++ b/src/persistence/CodeBeam.UltimateAuth.InMemory.Abstractions/CodeBeam.UltimateAuth.InMemory.Abstractions.csproj @@ -0,0 +1,15 @@ + + + + net8.0;net9.0;net10.0 + enable + enable + true + $(NoWarn);1591 + + + + + + + diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/IInMemoryUserIdProvider.cs b/src/persistence/CodeBeam.UltimateAuth.InMemory.Abstractions/IInMemoryUserIdProvider.cs similarity index 67% rename from src/CodeBeam.UltimateAuth.Core/Infrastructure/IInMemoryUserIdProvider.cs rename to src/persistence/CodeBeam.UltimateAuth.InMemory.Abstractions/IInMemoryUserIdProvider.cs index 57a25023..a5296452 100644 --- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/IInMemoryUserIdProvider.cs +++ b/src/persistence/CodeBeam.UltimateAuth.InMemory.Abstractions/IInMemoryUserIdProvider.cs @@ -1,4 +1,4 @@ -namespace CodeBeam.UltimateAuth.Core.Infrastructure; +namespace CodeBeam.UltimateAuth.InMemory; public interface IInMemoryUserIdProvider { diff --git a/src/persistence/CodeBeam.UltimateAuth.InMemory.Abstractions/InMemoryTenantVersionedStore.cs b/src/persistence/CodeBeam.UltimateAuth.InMemory.Abstractions/InMemoryTenantVersionedStore.cs new file mode 100644 index 00000000..c3d3d5c3 --- /dev/null +++ b/src/persistence/CodeBeam.UltimateAuth.InMemory.Abstractions/InMemoryTenantVersionedStore.cs @@ -0,0 +1,51 @@ +using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Errors; +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.InMemory; + +public abstract class InMemoryTenantVersionedStore : InMemoryVersionedStore + where TEntity : class, IVersionedEntity, IEntitySnapshot, ITenantEntity + where TKey : notnull, IEquatable +{ + private readonly TenantContext _tenant; + + protected InMemoryTenantVersionedStore(TenantContext tenant) + { + _tenant = tenant; + } + + protected override void BeforeAdd(TEntity entity) + { + EnsureTenant(entity); + base.BeforeAdd(entity); + } + + protected override void BeforeSave(TEntity entity, TEntity current, long expectedVersion) + { + EnsureTenant(entity); + base.BeforeSave(entity, current, expectedVersion); + } + + protected override void BeforeDelete(TEntity current, long expectedVersion, DeleteMode mode, DateTimeOffset now) + { + EnsureTenant(current); + base.BeforeDelete(current, expectedVersion, mode, now); + } + + protected IReadOnlyList TenantValues() + { + return InternalValues() + .Where(x => x.Tenant == _tenant.Tenant) + .Select(Snapshot) + .ToList() + .AsReadOnly(); + } + + private void EnsureTenant(TEntity entity) + { + if (!_tenant.IsGlobal && entity.Tenant != _tenant.Tenant) + throw new UAuthConflictException("tenant_mismatch"); + } +} diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/InMemoryVersionedStore.cs b/src/persistence/CodeBeam.UltimateAuth.InMemory.Abstractions/InMemoryVersionedStore.cs similarity index 96% rename from src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/InMemoryVersionedStore.cs rename to src/persistence/CodeBeam.UltimateAuth.InMemory.Abstractions/InMemoryVersionedStore.cs index db4d54f6..8d705d32 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/InMemoryVersionedStore.cs +++ b/src/persistence/CodeBeam.UltimateAuth.InMemory.Abstractions/InMemoryVersionedStore.cs @@ -1,8 +1,9 @@ -using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Errors; using System.Collections.Concurrent; -namespace CodeBeam.UltimateAuth.Core.Abstractions; +namespace CodeBeam.UltimateAuth.InMemory; public abstract class InMemoryVersionedStore : IVersionedStore where TEntity : class, IVersionedEntity, IEntitySnapshot diff --git a/src/policies/CodeBeam.UltimateAuth.Policies/CodeBeam.UltimateAuth.Policies.csproj b/src/policies/CodeBeam.UltimateAuth.Policies/CodeBeam.UltimateAuth.Policies.csproj index 6db7b9d7..e106d967 100644 --- a/src/policies/CodeBeam.UltimateAuth.Policies/CodeBeam.UltimateAuth.Policies.csproj +++ b/src/policies/CodeBeam.UltimateAuth.Policies/CodeBeam.UltimateAuth.Policies.csproj @@ -4,15 +4,44 @@ net8.0;net9.0;net10.0 enable enable - 0.0.1 - 0.0.1 - true $(NoWarn);1591 + + CodeBeam.UltimateAuth.Policies + 0.1.0-preview.1 + + CodeBeam + CodeBeam + + + Policy evaluation module for UltimateAuth. + Provides reusable authorization policy logic built on top of roles and permissions. + Can be used independently or together with the Authorization module. + This package is included transitively by CodeBeam.UltimateAuth.Server and usually does not need to be installed directly. + + + authentication;authorization;policies;permissions;security;auth-framework + + README.md + https://github.com/CodeBeamOrg/UltimateAuth + https://github.com/CodeBeamOrg/UltimateAuth + Apache-2.0 + + true + true + snupkg + + true + + + + + + + - - + -
+
\ No newline at end of file diff --git a/src/policies/CodeBeam.UltimateAuth.Policies/README.md b/src/policies/CodeBeam.UltimateAuth.Policies/README.md new file mode 100644 index 00000000..2de5de6c --- /dev/null +++ b/src/policies/CodeBeam.UltimateAuth.Policies/README.md @@ -0,0 +1,23 @@ +# UltimateAuth Policies + +Policy evaluation module for UltimateAuth. + +## Purpose + +This package provides: + +- Authorization policy evaluation logic +- Permission-based decision making +- Reusable policy helpers + +## Does NOT include + +- Persistence +- User or credential management +- ASP.NET Core integration + +⚠️ This package is typically installed transitively via: + +- CodeBeam.UltimateAuth.Server + +In most cases, you do not need to install it directly unless you are building custom policy logic. \ No newline at end of file diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/AssemblyVisibility.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/AssemblyVisibility.cs new file mode 100644 index 00000000..ed166fcc --- /dev/null +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/AssemblyVisibility.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("CodeBeam.UltimateAuth.Tests.Unit")] diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Data/UAuthSessionDbContext.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Data/UAuthSessionDbContext.cs index c78491f8..886762d5 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Data/UAuthSessionDbContext.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Data/UAuthSessionDbContext.cs @@ -5,38 +5,33 @@ namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; -internal sealed class UltimateAuthSessionDbContext : DbContext +internal sealed class UAuthSessionDbContext : DbContext { public DbSet Roots => Set(); public DbSet Chains => Set(); public DbSet Sessions => Set(); - private readonly TenantContext _tenant; - - public UltimateAuthSessionDbContext(DbContextOptions options, TenantContext tenant) : base(options) + public UAuthSessionDbContext(DbContextOptions options) : base(options) { - _tenant = tenant; } protected override void OnModelCreating(ModelBuilder b) { - b.Entity() - .HasQueryFilter(x => _tenant.IsGlobal || x.Tenant == _tenant.Tenant); - - b.Entity() - .HasQueryFilter(x => _tenant.IsGlobal || x.Tenant == _tenant.Tenant); - - b.Entity() - .HasQueryFilter(x => _tenant.IsGlobal || x.Tenant == _tenant.Tenant); - b.Entity(e => { + e.ToTable("UAuth_SessionRoots"); e.HasKey(x => x.Id); e.Property(x => x.Version).IsConcurrencyToken(); - e.Property(x => x.UserKey).IsRequired(); e.Property(x => x.CreatedAt).IsRequired(); + + e.Property(x => x.UserKey) + .HasConversion( + v => v.Value, + v => UserKey.FromString(v)) + .HasMaxLength(128) + .IsRequired(); e.Property(x => x.Tenant) .HasConversion( v => v.Value, @@ -60,11 +55,18 @@ protected override void OnModelCreating(ModelBuilder b) b.Entity(e => { + e.ToTable("UAuth_SessionChains"); e.HasKey(x => x.Id); e.Property(x => x.Version).IsConcurrencyToken(); - e.Property(x => x.UserKey).IsRequired(); e.Property(x => x.CreatedAt).IsRequired(); + + e.Property(x => x.UserKey) + .HasConversion( + v => v.Value, + v => UserKey.FromString(v)) + .HasMaxLength(128) + .IsRequired(); e.Property(x => x.Tenant) .HasConversion( v => v.Value, @@ -113,9 +115,17 @@ protected override void OnModelCreating(ModelBuilder b) b.Entity(e => { + e.ToTable("UAuth_Sessions"); e.HasKey(x => x.Id); e.Property(x => x.Version).IsConcurrencyToken(); e.Property(x => x.CreatedAt).IsRequired(); + + e.Property(x => x.UserKey) + .HasConversion( + v => v.Value, + v => UserKey.FromString(v)) + .HasMaxLength(128) + .IsRequired(); e.Property(x => x.Tenant) .HasConversion( v => v.Value, diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs index 82bb21d0..69762efe 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs @@ -2,13 +2,13 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; +namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.Extensions; public static class ServiceCollectionExtensions { - public static IServiceCollection AddUltimateAuthEntityFrameworkCoreSessions(this IServiceCollection services,Action configureDb) + public static IServiceCollection AddUltimateAuthSessionsEntityFrameworkCore(this IServiceCollection services,Action configureDb) { - services.AddDbContextPool(configureDb); + services.AddDbContext(configureDb); services.AddScoped(); return services; diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionChainProjectionMapper.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionChainProjectionMapper.cs index 271f5c75..5fd8819f 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionChainProjectionMapper.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionChainProjectionMapper.cs @@ -51,4 +51,18 @@ public static SessionChainProjection ToProjection(this UAuthSessionChain chain) }; } + public static void UpdateProjection(this UAuthSessionChain source, SessionChainProjection target) + { + DeviceId.TryCreate(source.Device.DeviceId?.Value, out var deviceId); + + target.ActiveSessionId = source.ActiveSessionId; + target.RevokedAt = source.RevokedAt; + target.DeviceId = deviceId; + target.Device = source.Device; + target.ClaimsSnapshot = source.ClaimsSnapshot; + target.SecurityVersionAtCreation = source.SecurityVersionAtCreation; + target.LastSeenAt = source.LastSeenAt; + target.AbsoluteExpiresAt = source.AbsoluteExpiresAt; + // Version store-owned + } } diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionProjectionMapper.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionProjectionMapper.cs index 6377ea28..be5f5413 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionProjectionMapper.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionProjectionMapper.cs @@ -42,4 +42,15 @@ public static SessionProjection ToProjection(this UAuthSession s) Version = s.Version }; } + + public static void UpdateProjection(this UAuthSession source, SessionProjection target) + { + target.ExpiresAt = source.ExpiresAt; + target.RevokedAt = source.RevokedAt; + target.SecurityVersionAtCreation = source.SecurityVersionAtCreation; + target.Device = source.Device; + target.Claims = source.Claims; + target.Metadata = source.Metadata; + // Version store-owned + } } diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionRootProjectionMapper.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionRootProjectionMapper.cs index d38a02a5..70a2b038 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionRootProjectionMapper.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionRootProjectionMapper.cs @@ -34,4 +34,12 @@ public static SessionRootProjection ToProjection(this UAuthSessionRoot root) Version = root.Version }; } + + public static void UpdateProjection(this UAuthSessionRoot source, SessionRootProjection target) + { + target.UpdatedAt = source.UpdatedAt; + target.RevokedAt = source.RevokedAt; + target.SecurityVersion = source.SecurityVersion; + // Version store-owned + } } diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStore.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStore.cs index d52c232f..c9a31e6e 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStore.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStore.cs @@ -9,11 +9,13 @@ namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; internal sealed class EfCoreSessionStore : ISessionStore { - private readonly UltimateAuthSessionDbContext _db; + private readonly UAuthSessionDbContext _db; + private readonly TenantKey _tenant; - public EfCoreSessionStore(UltimateAuthSessionDbContext db) + public EfCoreSessionStore(UAuthSessionDbContext db, TenantContext tenant) { _db = db; + _tenant = tenant.Tenant; } public async Task ExecuteAsync(Func action, CancellationToken ct = default) @@ -77,23 +79,32 @@ public async Task ExecuteAsync(Func x.SessionId == sessionId); + .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.SessionId == sessionId); return projection?.ToDomain(); } - public Task SaveSessionAsync(UAuthSession session, long expectedVersion, CancellationToken ct = default) + public async Task SaveSessionAsync(UAuthSession session, long expectedVersion, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - var projection = session.ToProjection(); - _db.Sessions.Attach(projection); - _db.Entry(projection).Property(x => x.Version).OriginalValue = expectedVersion; - _db.Entry(projection).State = EntityState.Modified; - return Task.CompletedTask; + var projection = await _db.Sessions + .SingleOrDefaultAsync(x => + x.Tenant == _tenant && + x.SessionId == session.SessionId, + ct); + + if (projection is null) + throw new UAuthNotFoundException("session_not_found"); + + if (projection.Version != expectedVersion) + throw new UAuthConcurrencyException("session_concurrency_conflict"); + + session.UpdateProjection(projection); + projection.Version++; } - public async Task CreateSessionAsync(UAuthSession session, CancellationToken ct = default) + public Task CreateSessionAsync(UAuthSession session, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); @@ -103,19 +114,23 @@ public async Task CreateSessionAsync(UAuthSession session, CancellationToken ct throw new InvalidOperationException("New session must have version 0."); _db.Sessions.Add(projection); + + return Task.CompletedTask; } public async Task RevokeSessionAsync(AuthSessionId sessionId, DateTimeOffset at, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - var projection = await _db.Sessions.SingleOrDefaultAsync(x => x.SessionId == sessionId, ct); + var projection = await _db.Sessions.SingleOrDefaultAsync(x => x.Tenant == _tenant && x.SessionId == sessionId, ct); - if (projection is null || projection.IsRevoked) + if (projection is null || projection.RevokedAt is not null) return false; - var revoked = projection.ToDomain().Revoke(at); - _db.Sessions.Update(revoked.ToProjection()); + var domain = projection.ToDomain().Revoke(at); + domain.UpdateProjection(projection); + projection.Version++; + return true; } @@ -123,24 +138,35 @@ public async Task RevokeAllSessionsAsync(UserKey user, DateTimeOffset at, Cancel { ct.ThrowIfCancellationRequested(); - var chains = await _db.Chains.Where(x => x.UserKey == user).ToListAsync(ct); + var chains = await _db.Chains + .Where(x => x.Tenant == _tenant && x.UserKey == user) + .ToListAsync(ct); + var chainIds = chains.Select(x => x.ChainId).ToList(); - var sessions = await _db.Sessions.Where(x => chainIds.Contains(x.ChainId)).ToListAsync(ct); + + var sessions = await _db.Sessions + .Where(x => x.Tenant == _tenant && chainIds.Contains(x.ChainId)) + .ToListAsync(ct); foreach (var sessionProjection in sessions) { - var session = sessionProjection.ToDomain(); + if (sessionProjection.RevokedAt is not null) + continue; - if (!session.IsRevoked) - _db.Sessions.Update(session.Revoke(at).ToProjection()); + var domain = sessionProjection.ToDomain().Revoke(at); + domain.UpdateProjection(sessionProjection); + sessionProjection.Version++; } foreach (var chainProjection in chains) { - var chain = chainProjection.ToDomain(); + if (chainProjection.ActiveSessionId is null) + continue; - if (chain.ActiveSessionId is not null) - _db.Chains.Update(chain.DetachSession(at).ToProjection()); + var domain = chainProjection.ToDomain().DetachSession(at); + + domain.UpdateProjection(chainProjection); + chainProjection.Version++; } } @@ -148,24 +174,35 @@ public async Task RevokeOtherSessionsAsync(UserKey user, SessionChainId keepChai { ct.ThrowIfCancellationRequested(); - var chains = await _db.Chains.Where(x => x.UserKey == user && x.ChainId != keepChain).ToListAsync(ct); + var chains = await _db.Chains + .Where(x => x.Tenant == _tenant && x.UserKey == user && x.ChainId != keepChain) + .ToListAsync(ct); + var chainIds = chains.Select(x => x.ChainId).ToList(); - var sessions = await _db.Sessions.Where(x => chainIds.Contains(x.ChainId)).ToListAsync(ct); + + var sessions = await _db.Sessions + .Where(x => x.Tenant == _tenant && chainIds.Contains(x.ChainId)) + .ToListAsync(ct); foreach (var sessionProjection in sessions) { - var session = sessionProjection.ToDomain(); + if (sessionProjection.RevokedAt is not null) + continue; - if (!session.IsRevoked) - _db.Sessions.Update(session.Revoke(at).ToProjection()); + var domain = sessionProjection.ToDomain().Revoke(at); + domain.UpdateProjection(sessionProjection); + sessionProjection.Version++; } foreach (var chainProjection in chains) { - var chain = chainProjection.ToDomain(); + if (chainProjection.ActiveSessionId is null) + continue; + + var domain = chainProjection.ToDomain().DetachSession(at); - if (chain.ActiveSessionId is not null) - _db.Chains.Update(chain.DetachSession(at).ToProjection()); + domain.UpdateProjection(chainProjection); + chainProjection.Version++; } } @@ -175,19 +212,19 @@ public async Task RevokeOtherSessionsAsync(UserKey user, SessionChainId keepChai var projection = await _db.Chains .AsNoTracking() - .SingleOrDefaultAsync(x => x.ChainId == chainId); + .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.ChainId == chainId); return projection?.ToDomain(); } - public async Task GetChainByDeviceAsync(TenantKey tenant, UserKey userKey, DeviceId deviceId, CancellationToken ct = default) + public async Task GetChainByDeviceAsync(UserKey userKey, DeviceId deviceId, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); var projection = await _db.Chains .AsNoTracking() .Where(x => - x.Tenant == tenant && + x.Tenant == _tenant && x.UserKey == userKey && x.RevokedAt == null && x.DeviceId == deviceId) @@ -196,22 +233,24 @@ public async Task RevokeOtherSessionsAsync(UserKey user, SessionChainId keepChai return projection?.ToDomain(); } - public Task SaveChainAsync(UAuthSessionChain chain, long expectedVersion, CancellationToken ct = default) + public async Task SaveChainAsync(UAuthSessionChain chain, long expectedVersion, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - var projection = chain.ToProjection(); - - if (chain.Version != expectedVersion + 1) - throw new InvalidOperationException("Chain version must be incremented by domain."); + var projection = await _db.Chains + .SingleOrDefaultAsync(x => + x.Tenant == _tenant && + x.ChainId == chain.ChainId, + ct); - _db.Entry(projection).State = EntityState.Modified; + if (projection is null) + throw new UAuthNotFoundException("chain_not_found"); - _db.Entry(projection) - .Property(x => x.Version) - .OriginalValue = expectedVersion; + if (projection.Version != expectedVersion) + throw new UAuthConcurrencyException("chain_concurrency_conflict"); - return Task.CompletedTask; + chain.UpdateProjection(projection); + projection.Version++; } public Task CreateChainAsync(UAuthSessionChain chain, CancellationToken ct = default) @@ -232,94 +271,86 @@ public async Task RevokeChainAsync(SessionChainId chainId, DateTimeOffset at, Ca { ct.ThrowIfCancellationRequested(); - var projection = await _db.Chains.SingleOrDefaultAsync(x => x.ChainId == chainId); - - if (projection is null) - return; + var projection = await _db.Chains + .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.ChainId == chainId, ct); - var chain = projection.ToDomain(); - if (chain.IsRevoked) + if (projection is null || projection.RevokedAt is not null) return; - _db.Chains.Update(chain.Revoke(at).ToProjection()); + var domain = projection.ToDomain().Revoke(at); + domain.UpdateProjection(projection); + projection.Version++; } public async Task LogoutChainAsync(SessionChainId chainId, DateTimeOffset at, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - var chainProjection = await _db.Chains.SingleOrDefaultAsync(x => x.ChainId == chainId, ct); - - if (chainProjection is null) - return; - - var chain = chainProjection.ToDomain(); + var chainProjection = await _db.Chains + .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.ChainId == chainId, ct); - if (chain.IsRevoked) + if (chainProjection is null || chainProjection.RevokedAt is not null) return; - var sessions = await _db.Sessions.Where(x => x.ChainId == chainId).ToListAsync(ct); + var sessions = await _db.Sessions + .Where(x => x.Tenant == _tenant && x.ChainId == chainId) + .ToListAsync(ct); foreach (var sessionProjection in sessions) { - var session = sessionProjection.ToDomain(); - - if (session.IsRevoked) + if (sessionProjection.RevokedAt is not null) continue; - var revoked = session.Revoke(at); - _db.Sessions.Update(revoked.ToProjection()); + var domain = sessionProjection.ToDomain().Revoke(at); + + domain.UpdateProjection(sessionProjection); + sessionProjection.Version++; } - if (chain.ActiveSessionId is not null) + if (chainProjection.ActiveSessionId is not null) { - var updatedChain = chain.DetachSession(at); - _db.Chains.Update(updatedChain.ToProjection()); + var domain = chainProjection.ToDomain().DetachSession(at); + domain.UpdateProjection(chainProjection); + chainProjection.Version++; } } - public async Task RevokeOtherChainsAsync(TenantKey tenant, UserKey userKey, SessionChainId currentChainId, DateTimeOffset at, CancellationToken ct = default) + public async Task RevokeOtherChainsAsync(UserKey userKey, SessionChainId currentChainId, DateTimeOffset at, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); var projections = await _db.Chains .Where(x => - x.Tenant == tenant && + x.Tenant == _tenant && x.UserKey == userKey && x.ChainId != currentChainId && - !x.IsRevoked) + x.RevokedAt == null) .ToListAsync(ct); foreach (var projection in projections) { - var chain = projection.ToDomain(); - - if (chain.IsRevoked) - continue; - - _db.Chains.Update(chain.Revoke(at).ToProjection()); + var domain = projection.ToDomain().Revoke(at); + domain.UpdateProjection(projection); + projection.Version++; } } - public async Task RevokeAllChainsAsync(TenantKey tenant, UserKey userKey, DateTimeOffset at, CancellationToken ct = default) + public async Task RevokeAllChainsAsync(UserKey userKey, DateTimeOffset at, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); var projections = await _db.Chains .Where(x => - x.Tenant == tenant && + x.Tenant == _tenant && x.UserKey == userKey && - !x.IsRevoked) + x.RevokedAt == null) .ToListAsync(ct); foreach (var projection in projections) { - var chain = projection.ToDomain(); - - if (chain.IsRevoked) - continue; - - _db.Chains.Update(chain.Revoke(at).ToProjection()); + var domain = projection.ToDomain().Revoke(at); + domain.UpdateProjection(projection); + projection.Version++; } } @@ -329,7 +360,7 @@ public async Task RevokeAllChainsAsync(TenantKey tenant, UserKey userKey, DateTi return await _db.Chains .AsNoTracking() - .Where(x => x.ChainId == chainId) + .Where(x => x.Tenant == _tenant && x.ChainId == chainId) .Select(x => x.ActiveSessionId) .SingleOrDefaultAsync(); } @@ -338,39 +369,48 @@ public async Task SetActiveSessionIdAsync(SessionChainId chainId, AuthSessionId { ct.ThrowIfCancellationRequested(); - var projection = await _db.Chains.SingleOrDefaultAsync(x => x.ChainId == chainId); + var projection = _db.Chains.Local + .FirstOrDefault(x => x.Tenant == _tenant && x.ChainId == chainId); + + if (projection is null) + { + projection = await _db.Chains + .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.ChainId == chainId, ct); + } if (projection is null) return; projection.ActiveSessionId = sessionId; - _db.Chains.Update(projection); + projection.Version++; } public async Task GetRootByUserAsync(UserKey userKey, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - var rootProjection = await _db.Roots.AsNoTracking().SingleOrDefaultAsync(x => x.UserKey == userKey, ct); + var rootProjection = await _db.Roots.AsNoTracking().SingleOrDefaultAsync(x => x.Tenant == _tenant && x.UserKey == userKey, ct); return rootProjection?.ToDomain(); } - public Task SaveRootAsync(UAuthSessionRoot root, long expectedVersion, CancellationToken ct = default) + public async Task SaveRootAsync(UAuthSessionRoot root, long expectedVersion, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - var projection = root.ToProjection(); - - if (root.Version != expectedVersion + 1) - throw new InvalidOperationException("Root version must be incremented by domain."); + var projection = await _db.Roots + .SingleOrDefaultAsync(x => + x.Tenant == _tenant && + x.UserKey == root.UserKey, + ct); - _db.Entry(projection).State = EntityState.Modified; + if (projection is null) + throw new UAuthNotFoundException("root_not_found"); - _db.Entry(projection) - .Property(x => x.Version) - .OriginalValue = expectedVersion; + if (projection.Version != expectedVersion) + throw new UAuthConcurrencyException("root_concurrency_conflict"); - return Task.CompletedTask; + root.UpdateProjection(projection); + projection.Version++; } public Task CreateRootAsync(UAuthSessionRoot root, CancellationToken ct = default) @@ -391,13 +431,15 @@ public async Task RevokeRootAsync(UserKey userKey, DateTimeOffset at, Cancellati { ct.ThrowIfCancellationRequested(); - var projection = await _db.Roots.SingleOrDefaultAsync(x => x.UserKey == userKey); + var projection = await _db.Roots + .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.UserKey == userKey, ct); - if (projection is null) + if (projection is null || projection.RevokedAt is not null) return; - var root = projection.ToDomain(); - _db.Roots.Update(root.Revoke(at).ToProjection()); + var domain = projection.ToDomain().Revoke(at); + domain.UpdateProjection(projection); + projection.Version++; } public async Task GetChainIdBySessionAsync(AuthSessionId sessionId, CancellationToken ct = default) @@ -406,7 +448,7 @@ public async Task RevokeRootAsync(UserKey userKey, DateTimeOffset at, Cancellati return await _db.Sessions .AsNoTracking() - .Where(x => x.SessionId == sessionId) + .Where(x => x.Tenant == _tenant && x.SessionId == sessionId) .Select(x => (SessionChainId?)x.ChainId) .SingleOrDefaultAsync(); } @@ -415,11 +457,11 @@ public async Task> GetChainsByUserAsync(UserKey { ct.ThrowIfCancellationRequested(); - var rootsQuery = _db.Roots.AsNoTracking().Where(r => r.UserKey == userKey); + var rootsQuery = _db.Roots.AsNoTracking().Where(x => x.Tenant == _tenant && x.UserKey == userKey); if (!includeHistoricalRoots) { - rootsQuery = rootsQuery.Where(r => !r.IsRevoked); + rootsQuery = rootsQuery.Where(x => x.RevokedAt == null); } var rootIds = await rootsQuery.Select(r => r.RootId).ToListAsync(); @@ -427,7 +469,7 @@ public async Task> GetChainsByUserAsync(UserKey if (rootIds.Count == 0) return Array.Empty(); - var projections = await _db.Chains.AsNoTracking().Where(c => rootIds.Contains(c.RootId)).ToListAsync(); + var projections = await _db.Chains.AsNoTracking().Where(x => x.Tenant == _tenant && rootIds.Contains(x.RootId)).ToListAsync(); return projections.Select(c => c.ToDomain()).ToList(); } @@ -437,7 +479,7 @@ public async Task> GetChainsByRootAsync(Session var projections = await _db.Chains .AsNoTracking() - .Where(x => x.RootId == rootId) + .Where(x => x.Tenant == _tenant && x.RootId == rootId) .ToListAsync(); return projections.Select(x => x.ToDomain()).ToList(); @@ -449,7 +491,7 @@ public async Task> GetSessionsByChainAsync(SessionCh var projections = await _db.Sessions .AsNoTracking() - .Where(x => x.ChainId == chainId) + .Where(x => x.Tenant == _tenant && x.ChainId == chainId) .ToListAsync(); return projections.Select(x => x.ToDomain()).ToList(); @@ -459,7 +501,7 @@ public async Task> GetSessionsByChainAsync(SessionCh { ct.ThrowIfCancellationRequested(); - var projection = await _db.Roots.AsNoTracking().SingleOrDefaultAsync(x => x.RootId == rootId, ct); + var projection = await _db.Roots.AsNoTracking().SingleOrDefaultAsync(x => x.Tenant == _tenant && x.RootId == rootId, ct); return projection?.ToDomain(); } @@ -467,7 +509,7 @@ public async Task RemoveSessionAsync(AuthSessionId sessionId, CancellationToken { ct.ThrowIfCancellationRequested(); - var projection = await _db.Sessions.SingleOrDefaultAsync(x => x.SessionId == sessionId, ct); + var projection = await _db.Sessions.SingleOrDefaultAsync(x => x.Tenant == _tenant && x.SessionId == sessionId, ct); if (projection is null) return; @@ -480,27 +522,27 @@ public async Task RevokeChainCascadeAsync(SessionChainId chainId, DateTimeOffset ct.ThrowIfCancellationRequested(); var chainProjection = await _db.Chains - .SingleOrDefaultAsync(x => x.ChainId == chainId); + .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.ChainId == chainId, ct); if (chainProjection is null) return; - var sessionProjections = await _db.Sessions.Where(x => x.ChainId == chainId && !x.IsRevoked).ToListAsync(); + var sessionProjections = await _db.Sessions + .Where(x => x.Tenant == _tenant && x.ChainId == chainId && x.RevokedAt == null) + .ToListAsync(ct); foreach (var sessionProjection in sessionProjections) { - var session = sessionProjection.ToDomain(); - var revoked = session.Revoke(at); - - _db.Sessions.Update(revoked.ToProjection()); + var revoked = sessionProjection.ToDomain().Revoke(at); + revoked.UpdateProjection(sessionProjection); + sessionProjection.Version++; } - if (!chainProjection.IsRevoked) + if (chainProjection.RevokedAt is null) { - var chain = chainProjection.ToDomain(); - var revokedChain = chain.Revoke(at); - - _db.Chains.Update(revokedChain.ToProjection()); + var revokedChain = chainProjection.ToDomain().Revoke(at); + revokedChain.UpdateProjection(chainProjection); + chainProjection.Version++; } } @@ -508,42 +550,48 @@ public async Task RevokeRootCascadeAsync(UserKey userKey, DateTimeOffset at, Can { ct.ThrowIfCancellationRequested(); - var rootProjection = await _db.Roots.SingleOrDefaultAsync(x => x.UserKey == userKey); + var rootProjection = await _db.Roots + .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.UserKey == userKey, ct); if (rootProjection is null) return; - var chainProjections = await _db.Chains.Where(x => x.UserKey == userKey).ToListAsync(); + var chainProjections = await _db.Chains + .Where(x => x.Tenant == _tenant && x.UserKey == userKey) + .ToListAsync(ct); foreach (var chainProjection in chainProjections) { - var chainId = chainProjection.ChainId; - - var sessionProjections = await _db.Sessions.Where(x => x.ChainId == chainId && !x.IsRevoked).ToListAsync(); + var sessions = await _db.Sessions + .Where(x => x.Tenant == _tenant && x.ChainId == chainProjection.ChainId) + .ToListAsync(ct); - foreach (var sessionProjection in sessionProjections) + foreach (var sessionProjection in sessions) { - var session = sessionProjection.ToDomain(); - var revokedSession = session.Revoke(at); + if (sessionProjection.RevokedAt is not null) + continue; + + var sessionDomain = sessionProjection.ToDomain().Revoke(at); - _db.Sessions.Update(revokedSession.ToProjection()); + sessionDomain.UpdateProjection(sessionProjection); + sessionProjection.Version++; } - if (!chainProjection.IsRevoked) + if (chainProjection.RevokedAt is null) { - var chain = chainProjection.ToDomain(); - var revokedChain = chain.Revoke(at); + var chainDomain = chainProjection.ToDomain().Revoke(at); - _db.Chains.Update(revokedChain.ToProjection()); + chainDomain.UpdateProjection(chainProjection); + chainProjection.Version++; } } - if (!rootProjection.IsRevoked) + if (rootProjection.RevokedAt is null) { - var root = rootProjection.ToDomain(); - var revokedRoot = root.Revoke(at); + var rootDomain = rootProjection.ToDomain().Revoke(at); - _db.Roots.Update(revokedRoot.ToProjection()); + rootDomain.UpdateProjection(rootProjection); + rootProjection.Version++; } } } diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreFactory.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreFactory.cs index 9200b8ef..b64206f1 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreFactory.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreFactory.cs @@ -1,26 +1,19 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.MultiTenancy; -using Microsoft.Extensions.DependencyInjection; namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; -public sealed class EfCoreSessionStoreFactory : ISessionStoreFactory +internal sealed class EfCoreSessionStoreFactory : ISessionStoreFactory { - private readonly IServiceProvider _sp; + private readonly UAuthSessionDbContext _db; - public EfCoreSessionStoreFactory(IServiceProvider sp) + public EfCoreSessionStoreFactory(UAuthSessionDbContext db) { - _sp = sp; + _db = db; } public ISessionStore Create(TenantKey tenant) { - return ActivatorUtilities.CreateInstance(_sp, new TenantContext(tenant)); + return new EfCoreSessionStore(_db, new TenantContext(tenant)); } - - // TODO: Implement global here - //public ISessionStoreKernel CreateGlobal() - //{ - // return ActivatorUtilities.CreateInstance(_sp, new TenantContext(null, isGlobal: true)); - //} } diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/CodeBeam.UltimateAuth.Sessions.InMemory.csproj b/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/CodeBeam.UltimateAuth.Sessions.InMemory.csproj index 9351133b..11a75fbc 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/CodeBeam.UltimateAuth.Sessions.InMemory.csproj +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/CodeBeam.UltimateAuth.Sessions.InMemory.csproj @@ -11,6 +11,7 @@ +
diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStore.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStore.cs index 73fc9452..c21a91d6 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStore.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStore.cs @@ -10,10 +10,16 @@ internal sealed class InMemorySessionStore : ISessionStore { private readonly SemaphoreSlim _tx = new(1, 1); private readonly object _lock = new(); + private readonly TenantKey _tenant; + + public InMemorySessionStore(TenantKey tenant) + { + _tenant = tenant; + } private readonly ConcurrentDictionary _sessions = new(); private readonly ConcurrentDictionary _chains = new(); - private readonly ConcurrentDictionary _roots = new(); + private readonly ConcurrentDictionary<(TenantKey, UserKey), UAuthSessionRoot> _roots = new(); public async Task ExecuteAsync(Func action, CancellationToken ct = default) { @@ -222,13 +228,13 @@ public Task RevokeChainAsync(SessionChainId chainId, DateTimeOffset at, Cancella return Task.CompletedTask; } - public Task RevokeOtherChainsAsync(TenantKey tenant, UserKey user, SessionChainId keepChain, DateTimeOffset at, CancellationToken ct = default) + public Task RevokeOtherChainsAsync(UserKey user, SessionChainId keepChain, DateTimeOffset at, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); foreach (var (id, chain) in _chains) { - if (chain.Tenant != tenant) + if (chain.Tenant != _tenant) continue; if (chain.UserKey != user) @@ -244,13 +250,13 @@ public Task RevokeOtherChainsAsync(TenantKey tenant, UserKey user, SessionChainI return Task.CompletedTask; } - public Task RevokeAllChainsAsync(TenantKey tenant, UserKey user, DateTimeOffset at, CancellationToken ct = default) + public Task RevokeAllChainsAsync(UserKey user, DateTimeOffset at, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); foreach (var (id, chain) in _chains) { - if (chain.Tenant != tenant) + if (chain.Tenant != _tenant) continue; if (chain.UserKey != user) @@ -266,7 +272,7 @@ public Task RevokeAllChainsAsync(TenantKey tenant, UserKey user, DateTimeOffset public Task GetRootByUserAsync(UserKey userKey, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - return Task.FromResult(_roots.TryGetValue(userKey, out var r) ? r : null); + return Task.FromResult(_roots.TryGetValue((_tenant, userKey), out var r) ? r : null); } public Task GetRootByIdAsync(SessionRootId rootId, CancellationToken ct = default) @@ -279,13 +285,13 @@ public Task SaveRootAsync(UAuthSessionRoot root, long expectedVersion, Cancellat { ct.ThrowIfCancellationRequested(); - if (!_roots.TryGetValue(root.UserKey, out var current)) + if (!_roots.TryGetValue((_tenant, root.UserKey), out var current)) throw new UAuthNotFoundException("root_not_found"); if (current.Version != expectedVersion) throw new UAuthConcurrencyException("root_concurrency_conflict"); - _roots[root.UserKey] = root; + _roots[(_tenant, root.UserKey)] = root; return Task.CompletedTask; } @@ -295,13 +301,13 @@ public Task CreateRootAsync(UAuthSessionRoot root, CancellationToken ct = defaul lock (_lock) { - if (_roots.ContainsKey(root.UserKey)) + if (_roots.ContainsKey((_tenant, root.UserKey))) throw new UAuthConcurrencyException("root_already_exists"); if (root.Version != 0) throw new InvalidOperationException("New root must have version 0."); - _roots[root.UserKey] = root; + _roots[(_tenant, root.UserKey)] = root; } return Task.CompletedTask; @@ -311,9 +317,9 @@ public Task RevokeRootAsync(UserKey userKey, DateTimeOffset at, CancellationToke { ct.ThrowIfCancellationRequested(); - if (_roots.TryGetValue(userKey, out var root)) + if (_roots.TryGetValue((_tenant, userKey), out var root)) { - _roots[userKey] = root.Revoke(at); + _roots[(_tenant, userKey)] = root.Revoke(at); } return Task.CompletedTask; } @@ -328,7 +334,7 @@ public Task RevokeRootAsync(UserKey userKey, DateTimeOffset at, CancellationToke return Task.FromResult(null); } - public Task> GetChainsByUserAsync(UserKey userKey,bool includeHistoricalRoots = false, CancellationToken ct = default) + public Task> GetChainsByUserAsync(UserKey userKey, bool includeHistoricalRoots = false, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); @@ -353,13 +359,13 @@ public Task> GetChainsByRootAsync(SessionRootId return Task.FromResult>(result); } - public Task GetChainByDeviceAsync(TenantKey tenant, UserKey userKey, DeviceId deviceId, CancellationToken ct = default) + public Task GetChainByDeviceAsync(UserKey userKey, DeviceId deviceId, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); var chain = _chains.Values .FirstOrDefault(c => - c.Tenant == tenant && + c.Tenant == _tenant && c.UserKey == userKey && !c.IsRevoked && c.Device.DeviceId == deviceId); @@ -468,7 +474,7 @@ public Task RevokeRootCascadeAsync(UserKey userKey, DateTimeOffset at, Cancellat lock (_lock) { - if (!_roots.TryGetValue(userKey, out var root)) + if (!_roots.TryGetValue((_tenant, userKey), out var root)) return Task.CompletedTask; var chains = _chains.Values @@ -500,7 +506,7 @@ public Task RevokeRootCascadeAsync(UserKey userKey, DateTimeOffset at, Cancellat if (!root.IsRevoked) { var revokedRoot = root.Revoke(at); - _roots[userKey] = revokedRoot; + _roots[(_tenant, userKey)] = revokedRoot; } } diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreFactory.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreFactory.cs index af6b5e99..b0fdf3c8 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreFactory.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreFactory.cs @@ -10,6 +10,6 @@ public sealed class InMemorySessionStoreFactory : ISessionStoreFactory public ISessionStore Create(TenantKey tenant) { - return _kernels.GetOrAdd(tenant, _ => new InMemorySessionStore()); + return _kernels.GetOrAdd(tenant, t => new InMemorySessionStore(t)); } } diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/ServiceCollectionExtensions.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/ServiceCollectionExtensions.cs index 054fcffc..adb0976c 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/ServiceCollectionExtensions.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/ServiceCollectionExtensions.cs @@ -1,11 +1,11 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using Microsoft.Extensions.DependencyInjection; -namespace CodeBeam.UltimateAuth.Sessions.InMemory; +namespace CodeBeam.UltimateAuth.Sessions.InMemory.Extensions; public static class ServiceCollectionExtensions { - public static IServiceCollection AddUltimateAuthInMemorySessions(this IServiceCollection services) + public static IServiceCollection AddUltimateAuthSessionsInMemory(this IServiceCollection services) { services.AddSingleton(); return services; diff --git a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Data/UAuthTokenDbContext.cs b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Data/UAuthTokenDbContext.cs index a7caf53b..304b7a74 100644 --- a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Data/UAuthTokenDbContext.cs +++ b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Data/UAuthTokenDbContext.cs @@ -5,12 +5,12 @@ namespace CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore; -internal sealed class UltimateAuthTokenDbContext : DbContext +internal sealed class UAuthTokenDbContext : DbContext { public DbSet RefreshTokens => Set(); - public DbSet RevokedTokenIds => Set(); + //public DbSet RevokedTokenIds => Set(); // TODO: Add when JWT added. - public UltimateAuthTokenDbContext(DbContextOptions options) + public UAuthTokenDbContext(DbContextOptions options) : base(options) { } @@ -19,6 +19,7 @@ protected override void OnModelCreating(ModelBuilder b) { b.Entity(e => { + e.ToTable("UAuth_RefreshTokens"); e.HasKey(x => x.Id); e.Property(x => x.Version) @@ -30,6 +31,12 @@ protected override void OnModelCreating(ModelBuilder b) v => TenantKey.FromInternal(v)) .HasMaxLength(128) .IsRequired(); + e.Property(x => x.UserKey) + .HasConversion( + v => v.Value, + v => UserKey.FromString(v)) + .HasMaxLength(128) + .IsRequired(); e.Property(x => x.TokenId) .HasConversion( diff --git a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs index c3dcc34f..66a01476 100644 --- a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs +++ b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs @@ -2,15 +2,14 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -namespace CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore; +namespace CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore.Extensions; public static class ServiceCollectionExtensions { - public static IServiceCollection AddUltimateAuthEntityFrameworkCoreTokens(this IServiceCollection services, Action configureDb) + public static IServiceCollection AddUltimateAuthTokensEntityFrameworkCore(this IServiceCollection services, Action configureDb) { - services.AddDbContextPool(configureDb); + services.AddDbContext(configureDb); services.AddScoped(); - return services; } } diff --git a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Stores/EfCoreRefreshTokenStore.cs b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Stores/EfCoreRefreshTokenStore.cs index 7347936e..6d4aad70 100644 --- a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Stores/EfCoreRefreshTokenStore.cs +++ b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Stores/EfCoreRefreshTokenStore.cs @@ -7,13 +7,14 @@ namespace CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore; internal sealed class EfCoreRefreshTokenStore : IRefreshTokenStore { - private readonly UltimateAuthTokenDbContext _db; + private readonly UAuthTokenDbContext _db; private readonly TenantKey _tenant; + private bool _inTransaction; - public EfCoreRefreshTokenStore(UltimateAuthTokenDbContext db, TenantKey tenant) + public EfCoreRefreshTokenStore(UAuthTokenDbContext db, TenantContext tenant) { _db = db; - _tenant = tenant; + _tenant = tenant.Tenant; } public async Task ExecuteAsync(Func action, CancellationToken ct = default) @@ -22,8 +23,9 @@ public async Task ExecuteAsync(Func action, Cancellatio await strategy.ExecuteAsync(async () => { - await using var tx = - await _db.Database.BeginTransactionAsync(ct); + await using var tx = await _db.Database.BeginTransactionAsync(ct); + + _inTransaction = true; try { @@ -36,6 +38,10 @@ await strategy.ExecuteAsync(async () => await tx.RollbackAsync(ct); throw; } + finally + { + _inTransaction = false; + } }); } @@ -45,8 +51,9 @@ public async Task ExecuteAsync(Func { - await using var tx = - await _db.Database.BeginTransactionAsync(ct); + await using var tx = await _db.Database.BeginTransactionAsync(ct); + + _inTransaction = true; try { @@ -60,25 +67,36 @@ public async Task ExecuteAsync(Func FindByHashAsync( - string tokenHash, - CancellationToken ct = default) + public async Task FindByHashAsync(string tokenHash, CancellationToken ct = default) { + ct.ThrowIfCancellationRequested(); + var p = await _db.RefreshTokens .AsNoTracking() .SingleOrDefaultAsync( @@ -89,12 +107,11 @@ public Task StoreAsync(RefreshToken token, CancellationToken ct = default) return p?.ToDomain(); } - public Task RevokeAsync( - string tokenHash, - DateTimeOffset revokedAt, - string? replacedByTokenHash = null, - CancellationToken ct = default) + public Task RevokeAsync(string tokenHash, DateTimeOffset revokedAt, string? replacedByTokenHash = null, CancellationToken ct = default) { + ct.ThrowIfCancellationRequested(); + EnsureTransaction(); + var query = _db.RefreshTokens .Where(x => x.Tenant == _tenant && @@ -115,11 +132,11 @@ public Task RevokeAsync( ct); } - public Task RevokeBySessionAsync( - AuthSessionId sessionId, - DateTimeOffset revokedAt, - CancellationToken ct = default) + public Task RevokeBySessionAsync(AuthSessionId sessionId, DateTimeOffset revokedAt, CancellationToken ct = default) { + ct.ThrowIfCancellationRequested(); + EnsureTransaction(); + return _db.RefreshTokens .Where(x => x.Tenant == _tenant && @@ -130,11 +147,11 @@ public Task RevokeBySessionAsync( ct); } - public Task RevokeByChainAsync( - SessionChainId chainId, - DateTimeOffset revokedAt, - CancellationToken ct = default) + public Task RevokeByChainAsync(SessionChainId chainId, DateTimeOffset revokedAt, CancellationToken ct = default) { + ct.ThrowIfCancellationRequested(); + EnsureTransaction(); + return _db.RefreshTokens .Where(x => x.Tenant == _tenant && @@ -145,11 +162,11 @@ public Task RevokeByChainAsync( ct); } - public Task RevokeAllForUserAsync( - UserKey userKey, - DateTimeOffset revokedAt, - CancellationToken ct = default) + public Task RevokeAllForUserAsync(UserKey userKey, DateTimeOffset revokedAt, CancellationToken ct = default) { + ct.ThrowIfCancellationRequested(); + EnsureTransaction(); + return _db.RefreshTokens .Where(x => x.Tenant == _tenant && diff --git a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Stores/EfCoreRefreshTokenStoreFactory.cs b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Stores/EfCoreRefreshTokenStoreFactory.cs index e329766d..f584beae 100644 --- a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Stores/EfCoreRefreshTokenStoreFactory.cs +++ b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Stores/EfCoreRefreshTokenStoreFactory.cs @@ -1,20 +1,19 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.MultiTenancy; -using Microsoft.Extensions.DependencyInjection; namespace CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore; -public sealed class EfCoreRefreshTokenStoreFactory : IRefreshTokenStoreFactory +internal sealed class EfCoreRefreshTokenStoreFactory : IRefreshTokenStoreFactory { - private readonly IServiceProvider _sp; + private readonly UAuthTokenDbContext _db; - public EfCoreRefreshTokenStoreFactory(IServiceProvider sp) + public EfCoreRefreshTokenStoreFactory(UAuthTokenDbContext db) { - _sp = sp; + _db = db; } public IRefreshTokenStore Create(TenantKey tenant) { - return ActivatorUtilities.CreateInstance(_sp, tenant); + return new EfCoreRefreshTokenStore(_db, new TenantContext(tenant)); } } diff --git a/src/tokens/CodeBeam.UltimateAuth.Tokens.InMemory/CodeBeam.UltimateAuth.Tokens.InMemory.csproj b/src/tokens/CodeBeam.UltimateAuth.Tokens.InMemory/CodeBeam.UltimateAuth.Tokens.InMemory.csproj index df84dd42..c83d2979 100644 --- a/src/tokens/CodeBeam.UltimateAuth.Tokens.InMemory/CodeBeam.UltimateAuth.Tokens.InMemory.csproj +++ b/src/tokens/CodeBeam.UltimateAuth.Tokens.InMemory/CodeBeam.UltimateAuth.Tokens.InMemory.csproj @@ -10,6 +10,7 @@ +
diff --git a/src/tokens/CodeBeam.UltimateAuth.Tokens.InMemory/InMemoryRefreshTokenStore.cs b/src/tokens/CodeBeam.UltimateAuth.Tokens.InMemory/InMemoryRefreshTokenStore.cs index 683cb61b..a5787f95 100644 --- a/src/tokens/CodeBeam.UltimateAuth.Tokens.InMemory/InMemoryRefreshTokenStore.cs +++ b/src/tokens/CodeBeam.UltimateAuth.Tokens.InMemory/InMemoryRefreshTokenStore.cs @@ -10,7 +10,7 @@ internal sealed class InMemoryRefreshTokenStore : IRefreshTokenStore private readonly TenantKey _tenant; private readonly SemaphoreSlim _tx = new(1, 1); - private readonly ConcurrentDictionary _tokens = new(); + private readonly ConcurrentDictionary<(TenantKey, string), RefreshToken> _tokens = new(); public InMemoryRefreshTokenStore(TenantKey tenant) { @@ -52,7 +52,7 @@ public Task StoreAsync(RefreshToken token, CancellationToken ct = default) if (token.Tenant != _tenant) throw new InvalidOperationException("Tenant mismatch."); - _tokens[token.TokenHash] = token; + _tokens[(_tenant, token.TokenHash)] = token; return Task.CompletedTask; } @@ -61,7 +61,7 @@ public Task StoreAsync(RefreshToken token, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - _tokens.TryGetValue(tokenHash, out var token); + _tokens.TryGetValue((_tenant, tokenHash), out var token); return Task.FromResult(token); } @@ -70,9 +70,9 @@ public Task RevokeAsync(string tokenHash, DateTimeOffset revokedAt, string? repl { ct.ThrowIfCancellationRequested(); - if (_tokens.TryGetValue(tokenHash, out var token) && !token.IsRevoked) + if (_tokens.TryGetValue((_tenant, tokenHash), out var token) && !token.IsRevoked) { - _tokens[tokenHash] = token.Revoke(revokedAt, replacedByTokenHash); + _tokens[(_tenant, tokenHash)] = token.Revoke(revokedAt, replacedByTokenHash); } return Task.CompletedTask; @@ -82,11 +82,14 @@ public Task RevokeBySessionAsync(AuthSessionId sessionId, DateTimeOffset revoked { ct.ThrowIfCancellationRequested(); - foreach (var (hash, token) in _tokens.ToArray()) + foreach (var ((tenant, hash), token) in _tokens.ToArray()) { + if (tenant != _tenant) + continue; + if (token.SessionId == sessionId && !token.IsRevoked) { - _tokens[hash] = token.Revoke(revokedAt); + _tokens[(_tenant, hash)] = token.Revoke(revokedAt); } } @@ -97,11 +100,14 @@ public Task RevokeByChainAsync(SessionChainId chainId, DateTimeOffset revokedAt, { ct.ThrowIfCancellationRequested(); - foreach (var (hash, token) in _tokens.ToArray()) + foreach (var ((tenant, hash), token) in _tokens.ToArray()) { + if (tenant != _tenant) + continue; + if (token.ChainId == chainId && !token.IsRevoked) { - _tokens[hash] = token.Revoke(revokedAt); + _tokens[(_tenant, hash)] = token.Revoke(revokedAt); } } @@ -112,14 +118,17 @@ public Task RevokeAllForUserAsync(UserKey userKey, DateTimeOffset revokedAt, Can { ct.ThrowIfCancellationRequested(); - foreach (var (hash, token) in _tokens.ToArray()) + foreach (var ((tenant, hash), token) in _tokens.ToArray()) { + if (tenant != _tenant) + continue; + if (token.UserKey == userKey && !token.IsRevoked) { - _tokens[hash] = token.Revoke(revokedAt); + _tokens[(_tenant, hash)] = token.Revoke(revokedAt); } } return Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/src/tokens/CodeBeam.UltimateAuth.Tokens.InMemory/ServiceCollectionExtensions.cs b/src/tokens/CodeBeam.UltimateAuth.Tokens.InMemory/ServiceCollectionExtensions.cs index 76b12711..3b5868d4 100644 --- a/src/tokens/CodeBeam.UltimateAuth.Tokens.InMemory/ServiceCollectionExtensions.cs +++ b/src/tokens/CodeBeam.UltimateAuth.Tokens.InMemory/ServiceCollectionExtensions.cs @@ -1,14 +1,13 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using Microsoft.Extensions.DependencyInjection; -namespace CodeBeam.UltimateAuth.Tokens.InMemory; +namespace CodeBeam.UltimateAuth.Tokens.InMemory.Extensions; public static class ServiceCollectionExtensions { - public static IServiceCollection AddUltimateAuthInMemoryTokens(this IServiceCollection services) + public static IServiceCollection AddUltimateAuthTokensInMemory(this IServiceCollection services) { services.AddSingleton(); - return services; } } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/CodeBeam.UltimateAuth.Users.Contracts.csproj b/src/users/CodeBeam.UltimateAuth.Users.Contracts/CodeBeam.UltimateAuth.Users.Contracts.csproj index 1f3e2def..176db35b 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/CodeBeam.UltimateAuth.Users.Contracts.csproj +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/CodeBeam.UltimateAuth.Users.Contracts.csproj @@ -4,12 +4,40 @@ net8.0;net9.0;net10.0 enable enable - true $(NoWarn);1591 + + CodeBeam.UltimateAuth.Users.Contracts + 0.1.0-preview.1 + + CodeBeam + CodeBeam + + + Shared contracts and cross-boundary types for UltimateAuth Users module. + This package contains identifiers, DTOs and shared models used between client and server. + It does NOT contain domain logic or persistence. + + + authentication;identity;users;contracts;shared;dto;auth-framework + + README.md + https://github.com/CodeBeamOrg/UltimateAuth + https://github.com/CodeBeamOrg/UltimateAuth + Apache-2.0 + + true + true + snupkg + + + + + +
diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/IdentifierExistenceQuery.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/IdentifierExistenceQuery.cs index 7906dd6e..e2201471 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/IdentifierExistenceQuery.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/IdentifierExistenceQuery.cs @@ -4,7 +4,6 @@ namespace CodeBeam.UltimateAuth.Users.Contracts; public sealed record IdentifierExistenceQuery( - TenantKey Tenant, UserIdentifierType Type, string NormalizedValue, IdentifierExistenceScope Scope, diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/README.md b/src/users/CodeBeam.UltimateAuth.Users.Contracts/README.md new file mode 100644 index 00000000..2e21436e --- /dev/null +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/README.md @@ -0,0 +1,27 @@ +# UltimateAuth Users Contracts + +Shared contracts and cross-boundary models for the Users module. + +## Purpose + +This package contains DTOs, shared query models etc. + +## Does NOT include + +- Domain logic +- Persistence + +## Usage + +This package is used by: + +- Server implementations +- Client SDKs +- Custom user providers + +⚠️ This package is usually installed transitively via: + +- CodeBeam.UltimateAuth.Server +- CodeBeam.UltimateAuth.Client + +No need to install it directly in most scenarios. \ No newline at end of file diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Data/UAuthUserDbContext.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Data/UAuthUserDbContext.cs index 70b601f4..f7f0d05c 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Data/UAuthUserDbContext.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Data/UAuthUserDbContext.cs @@ -11,39 +11,23 @@ internal sealed class UAuthUserDbContext : DbContext public DbSet Lifecycles => Set(); public DbSet Profiles => Set(); - private readonly TenantContext _tenant; - - public UAuthUserDbContext(DbContextOptions options, TenantContext tenant) + public UAuthUserDbContext(DbContextOptions options) : base(options) { - _tenant = tenant; } protected override void OnModelCreating(ModelBuilder b) { - ConfigureTenantFilters(b); - ConfigureIdentifiers(b); ConfigureLifecycles(b); ConfigureProfiles(b); } - private void ConfigureTenantFilters(ModelBuilder b) - { - b.Entity() - .HasQueryFilter(x => _tenant.IsGlobal || x.Tenant == _tenant.Tenant); - - b.Entity() - .HasQueryFilter(x => _tenant.IsGlobal || x.Tenant == _tenant.Tenant); - - b.Entity() - .HasQueryFilter(x => _tenant.IsGlobal || x.Tenant == _tenant.Tenant); - } - private void ConfigureIdentifiers(ModelBuilder b) { b.Entity(e => { + e.ToTable("UAuth_UserIdentifiers"); e.HasKey(x => x.Id); e.Property(x => x.Version) @@ -86,6 +70,7 @@ private void ConfigureLifecycles(ModelBuilder b) { b.Entity(e => { + e.ToTable("UAuth_UserLifecycles"); e.HasKey(x => x.Id); e.Property(x => x.Version) @@ -119,6 +104,7 @@ private void ConfigureProfiles(ModelBuilder b) { b.Entity(e => { + e.ToTable("UAuth_UserProfiles"); e.HasKey(x => x.Id); e.Property(x => x.Version) @@ -142,7 +128,7 @@ private void ConfigureProfiles(ModelBuilder b) e.Property(x => x.Metadata) .HasConversion(new NullableJsonValueConverter>()) - .Metadata.SetValueComparer(JsonValueComparers.Create()); + .Metadata.SetValueComparer(JsonValueComparers.Create>()); e.Property(x => x.CreatedAt) .IsRequired(); diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs index 5a5ed1ba..656bc78b 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs @@ -6,12 +6,12 @@ namespace CodeBeam.UltimateAuth.Users.EntityFrameworkCore.Extensions; public static class ServiceCollectionExtensions { - public static IServiceCollection AddUltimateAuthEntityFrameworkCoreUsers(this IServiceCollection services, Action configureDb) + public static IServiceCollection AddUltimateAuthUsersEntityFrameworkCore(this IServiceCollection services, Action configureDb) { - services.AddDbContextPool(configureDb); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + services.AddDbContext(configureDb); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); return services; } } diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Mappers/UserIdentifierMapper.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Mappers/UserIdentifierMapper.cs index 0f7b2e88..75a95e35 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Mappers/UserIdentifierMapper.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Mappers/UserIdentifierMapper.cs @@ -39,4 +39,17 @@ public static UserIdentifierProjection ToProjection(this UserIdentifier d) Version = d.Version }; } + + public static void UpdateProjection(this UserIdentifier source, UserIdentifierProjection target) + { + // Don't touch identity and concurrency properties + + target.Value = source.Value; + target.NormalizedValue = source.NormalizedValue; + target.IsPrimary = source.IsPrimary; + + target.VerifiedAt = source.VerifiedAt; + target.UpdatedAt = source.UpdatedAt; + target.DeletedAt = source.DeletedAt; + } } diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Mappers/UserLifecycleMapper.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Mappers/UserLifecycleMapper.cs index 654600c3..f7a35eb9 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Mappers/UserLifecycleMapper.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Mappers/UserLifecycleMapper.cs @@ -33,4 +33,12 @@ public static UserLifecycleProjection ToProjection(this UserLifecycle d) Version = d.Version }; } + + public static void UpdateProjection(this UserLifecycle source, UserLifecycleProjection target) + { + target.Status = source.Status; + target.SecurityVersion = source.SecurityVersion; + target.UpdatedAt = source.UpdatedAt; + target.DeletedAt = source.DeletedAt; + } } diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Mappers/UserProfileMapper.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Mappers/UserProfileMapper.cs index d18a1bcb..aaa2addb 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Mappers/UserProfileMapper.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Mappers/UserProfileMapper.cs @@ -49,4 +49,24 @@ public static UserProfileProjection ToProjection(this UserProfile d) Version = d.Version }; } + + public static void UpdateProjection(this UserProfile source, UserProfileProjection target) + { + target.DisplayName = source.DisplayName; + target.FirstName = source.FirstName; + target.LastName = source.LastName; + target.Metadata = source.Metadata?.ToDictionary(); + target.UpdatedAt = source.UpdatedAt; + target.DeletedAt = source.DeletedAt; + target.BirthDate = source.BirthDate; + target.Gender = source.Gender; + target.Bio = source.Bio; + target.Language = source.Language; + target.TimeZone = source.TimeZone; + target.Culture = source.Culture; + + // Version store-owned + // Id / Tenant / UserKey / CreatedAt immutable + } + } diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EFCoreUserProfileStoreFactory.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EFCoreUserProfileStoreFactory.cs new file mode 100644 index 00000000..8ac0d8c7 --- /dev/null +++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EFCoreUserProfileStoreFactory.cs @@ -0,0 +1,19 @@ +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Users.Reference; + +namespace CodeBeam.UltimateAuth.Users.EntityFrameworkCore; + +internal sealed class EfCoreUserProfileStoreFactory : IUserProfileStoreFactory +{ + private readonly UAuthUserDbContext _db; + + public EfCoreUserProfileStoreFactory(UAuthUserDbContext db) + { + _db = db; + } + + public IUserProfileStore Create(TenantKey tenant) + { + return new EfCoreUserProfileStore(_db, new TenantContext(tenant)); + } +} diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserIdentifierStore.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserIdentifierStore.cs index 86fd189f..5f85b529 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserIdentifierStore.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserIdentifierStore.cs @@ -11,10 +11,12 @@ namespace CodeBeam.UltimateAuth.Users.EntityFrameworkCore; internal sealed class EfCoreUserIdentifierStore : IUserIdentifierStore { private readonly UAuthUserDbContext _db; + private readonly TenantKey _tenant; - public EfCoreUserIdentifierStore(UAuthUserDbContext db) + public EfCoreUserIdentifierStore(UAuthUserDbContext db, TenantContext tenant) { _db = db; + _tenant = tenant.Tenant; } public async Task ExistsAsync(Guid key, CancellationToken ct = default) @@ -22,7 +24,10 @@ public async Task ExistsAsync(Guid key, CancellationToken ct = default) ct.ThrowIfCancellationRequested(); return await _db.Identifiers - .AnyAsync(x => x.Id == key, ct); + .AnyAsync(x => + x.Id == key && + x.Tenant == _tenant, + ct); } public async Task ExistsAsync(IdentifierExistenceQuery query, CancellationToken ct = default) @@ -32,7 +37,7 @@ public async Task ExistsAsync(IdentifierExistenceQuer var q = _db.Identifiers .AsNoTracking() .Where(x => - x.Tenant == query.Tenant && + x.Tenant == _tenant && x.Type == query.Type && x.NormalizedValue == query.NormalizedValue && x.DeletedAt == null); @@ -79,12 +84,15 @@ public async Task ExistsAsync(IdentifierExistenceQuer var projection = await _db.Identifiers .AsNoTracking() - .SingleOrDefaultAsync(x => x.Id == key, ct); + .SingleOrDefaultAsync(x => + x.Id == key && + x.Tenant == _tenant, + ct); return projection?.ToDomain(); } - public async Task GetAsync(TenantKey tenant, UserIdentifierType type, string normalizedValue, CancellationToken ct = default) + public async Task GetAsync(UserIdentifierType type, string normalizedValue, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); @@ -92,7 +100,7 @@ public async Task ExistsAsync(IdentifierExistenceQuer .AsNoTracking() .SingleOrDefaultAsync( x => - x.Tenant == tenant && + x.Tenant == _tenant && x.Type == type && x.NormalizedValue == normalizedValue && x.DeletedAt == null, @@ -107,19 +115,22 @@ public async Task ExistsAsync(IdentifierExistenceQuer var projection = await _db.Identifiers .AsNoTracking() - .SingleOrDefaultAsync(x => x.Id == id, ct); + .SingleOrDefaultAsync(x => + x.Id == id && + x.Tenant == _tenant, + ct); return projection?.ToDomain(); } - public async Task> GetByUserAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default) + public async Task> GetByUserAsync(UserKey userKey, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); var projections = await _db.Identifiers .AsNoTracking() .Where(x => - x.Tenant == tenant && + x.Tenant == _tenant && x.UserKey == userKey && x.DeletedAt == null) .OrderBy(x => x.CreatedAt) @@ -132,18 +143,18 @@ public async Task AddAsync(UserIdentifier entity, CancellationToken ct = default { ct.ThrowIfCancellationRequested(); - var projection = entity.ToProjection(); - if (entity.Version != 0) throw new UAuthValidationException("New identifier must have version 0."); + var projection = entity.ToProjection(); + using var tx = await _db.Database.BeginTransactionAsync(ct); if (entity.IsPrimary) { await _db.Identifiers .Where(x => - x.Tenant == entity.Tenant && + x.Tenant == _tenant && x.UserKey == entity.UserKey && x.Type == entity.Type && x.IsPrimary && @@ -163,15 +174,13 @@ public async Task SaveAsync(UserIdentifier entity, long expectedVersion, Cancell { ct.ThrowIfCancellationRequested(); - var projection = entity.ToProjection(); - using var tx = await _db.Database.BeginTransactionAsync(ct); if (entity.IsPrimary) { await _db.Identifiers .Where(x => - x.Tenant == entity.Tenant && + x.Tenant == _tenant && x.UserKey == entity.UserKey && x.Type == entity.Type && x.Id != entity.Id && @@ -182,88 +191,24 @@ await _db.Identifiers ct); } - _db.Entry(projection).State = EntityState.Modified; + var existing = await _db.Identifiers + .SingleOrDefaultAsync(x => + x.Id == entity.Id && + x.Tenant == _tenant, + ct); - _db.Entry(projection) - .Property(x => x.Version) - .OriginalValue = expectedVersion; + if (existing is null) + throw new UAuthNotFoundException("identifier_not_found"); - try - { - await _db.SaveChangesAsync(ct); - await tx.CommitAsync(ct); - } - catch (DbUpdateConcurrencyException) - { + if (existing.Version != expectedVersion) throw new UAuthConcurrencyException("identifier_concurrency_conflict"); - } - } - public async Task> QueryAsync(TenantKey tenant, UserIdentifierQuery query, CancellationToken ct = default) - { - ct.ThrowIfCancellationRequested(); + entity.UpdateProjection(existing); - if (query.UserKey is null) - throw new UAuthIdentifierValidationException("userKey_required"); - - var normalized = query.Normalize(); - - var baseQuery = _db.Identifiers - .AsNoTracking() - .Where(x => - x.Tenant == tenant && - x.UserKey == query.UserKey && - (query.IncludeDeleted || x.DeletedAt == null)); - - baseQuery = query.SortBy switch - { - nameof(UserIdentifier.Type) => - query.Descending - ? baseQuery.OrderByDescending(x => x.Type) - : baseQuery.OrderBy(x => x.Type), - - nameof(UserIdentifier.CreatedAt) => - query.Descending - ? baseQuery.OrderByDescending(x => x.CreatedAt) - : baseQuery.OrderBy(x => x.CreatedAt), - - nameof(UserIdentifier.Value) => - query.Descending - ? baseQuery.OrderByDescending(x => x.Value) - : baseQuery.OrderBy(x => x.Value), - - _ => baseQuery.OrderBy(x => x.CreatedAt) - }; - - var total = await baseQuery.CountAsync(ct); - - var items = await baseQuery - .Skip((normalized.PageNumber - 1) * normalized.PageSize) - .Take(normalized.PageSize) - .ToListAsync(ct); - - return new PagedResult( - items.Select(x => x.ToDomain()).ToList(), - total, - normalized.PageNumber, - normalized.PageSize, - query.SortBy, - query.Descending); - } - - public async Task> GetByUsersAsync(TenantKey tenant, IReadOnlyList userKeys, CancellationToken ct = default) - { - ct.ThrowIfCancellationRequested(); - - var projections = await _db.Identifiers - .AsNoTracking() - .Where(x => - x.Tenant == tenant && - userKeys.Contains(x.UserKey) && - x.DeletedAt == null) - .ToListAsync(ct); + existing.Version++; - return projections.Select(x => x.ToDomain()).ToList(); + await _db.SaveChangesAsync(ct); + await tx.CommitAsync(ct); } public async Task DeleteAsync(Guid key, long expectedVersion, DeleteMode mode, DateTimeOffset now, CancellationToken ct = default) @@ -271,7 +216,10 @@ public async Task DeleteAsync(Guid key, long expectedVersion, DeleteMode mode, D ct.ThrowIfCancellationRequested(); var projection = await _db.Identifiers - .SingleOrDefaultAsync(x => x.Id == key, ct); + .SingleOrDefaultAsync(x => + x.Id == key && + x.Tenant == _tenant, + ct); if (projection is null) throw new UAuthNotFoundException("identifier_not_found"); @@ -293,7 +241,7 @@ public async Task DeleteAsync(Guid key, long expectedVersion, DeleteMode mode, D await _db.SaveChangesAsync(ct); } - public async Task DeleteByUserAsync(TenantKey tenant, UserKey userKey, DeleteMode mode, DateTimeOffset deletedAt, CancellationToken ct = default) + public async Task DeleteByUserAsync(UserKey userKey, DeleteMode mode, DateTimeOffset deletedAt, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); @@ -301,7 +249,7 @@ public async Task DeleteByUserAsync(TenantKey tenant, UserKey userKey, DeleteMod { await _db.Identifiers .Where(x => - x.Tenant == tenant && + x.Tenant == _tenant && x.UserKey == userKey) .ExecuteDeleteAsync(ct); @@ -310,7 +258,7 @@ await _db.Identifiers await _db.Identifiers .Where(x => - x.Tenant == tenant && + x.Tenant == _tenant && x.UserKey == userKey && x.DeletedAt == null) .ExecuteUpdateAsync( @@ -319,4 +267,80 @@ await _db.Identifiers .SetProperty(i => i.IsPrimary, false), ct); } + + public async Task> GetByUsersAsync(IReadOnlyList userKeys, CancellationToken ct = default) + { + ct.ThrowIfCancellationRequested(); + + if (userKeys is null || userKeys.Count == 0) + return Array.Empty(); + + var projections = await _db.Identifiers + .AsNoTracking() + .Where(x => + x.Tenant == _tenant && + userKeys.Contains(x.UserKey) && + x.DeletedAt == null) + .OrderBy(x => x.UserKey) + .ThenBy(x => x.CreatedAt) + .ToListAsync(ct); + + return projections + .Select(x => x.ToDomain()) + .ToList(); + } + + public async Task> QueryAsync(UserIdentifierQuery query, CancellationToken ct = default) + { + ct.ThrowIfCancellationRequested(); + + if (query.UserKey is null) + throw new UAuthIdentifierValidationException("userKey_required"); + + var normalized = query.Normalize(); + + var baseQuery = _db.Identifiers + .AsNoTracking() + .Where(x => + x.Tenant == _tenant && + x.UserKey == query.UserKey); + + if (!query.IncludeDeleted) + baseQuery = baseQuery.Where(x => x.DeletedAt == null); + + baseQuery = query.SortBy switch + { + nameof(UserIdentifier.Type) => + query.Descending + ? baseQuery.OrderByDescending(x => x.Type) + : baseQuery.OrderBy(x => x.Type), + + nameof(UserIdentifier.CreatedAt) => + query.Descending + ? baseQuery.OrderByDescending(x => x.CreatedAt) + : baseQuery.OrderBy(x => x.CreatedAt), + + nameof(UserIdentifier.Value) => + query.Descending + ? baseQuery.OrderByDescending(x => x.Value) + : baseQuery.OrderBy(x => x.Value), + + _ => baseQuery.OrderBy(x => x.CreatedAt) + }; + + var total = await baseQuery.CountAsync(ct); + + var items = await baseQuery + .Skip((normalized.PageNumber - 1) * normalized.PageSize) + .Take(normalized.PageSize) + .ToListAsync(ct); + + return new PagedResult( + items.Select(x => x.ToDomain()).ToList(), + total, + normalized.PageNumber, + normalized.PageSize, + query.SortBy, + query.Descending); + } } diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserIdentifierStoreFactory.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserIdentifierStoreFactory.cs new file mode 100644 index 00000000..fe6cdbdf --- /dev/null +++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserIdentifierStoreFactory.cs @@ -0,0 +1,20 @@ +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Users.Reference; +using Microsoft.EntityFrameworkCore; + +namespace CodeBeam.UltimateAuth.Users.EntityFrameworkCore; + +internal sealed class EfCoreUserIdentifierStoreFactory : IUserIdentifierStoreFactory +{ + private readonly UAuthUserDbContext _db; + + public EfCoreUserIdentifierStoreFactory(UAuthUserDbContext db) + { + _db = db; + } + + public IUserIdentifierStore Create(TenantKey tenant) + { + return new EfCoreUserIdentifierStore(_db, new TenantContext(tenant)); + } +} diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserLifecycleStore.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserLifecycleStore.cs index c2b35917..eea3a8c8 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserLifecycleStore.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserLifecycleStore.cs @@ -9,10 +9,12 @@ namespace CodeBeam.UltimateAuth.Users.EntityFrameworkCore; internal sealed class EfCoreUserLifecycleStore : IUserLifecycleStore { private readonly UAuthUserDbContext _db; + private readonly TenantKey _tenant; - public EfCoreUserLifecycleStore(UAuthUserDbContext db) + public EfCoreUserLifecycleStore(UAuthUserDbContext db, TenantContext tenant) { _db = db; + _tenant = tenant.Tenant; } public async Task GetAsync(UserLifecycleKey key, CancellationToken ct = default) @@ -22,7 +24,7 @@ public EfCoreUserLifecycleStore(UAuthUserDbContext db) var projection = await _db.Lifecycles .AsNoTracking() .SingleOrDefaultAsync( - x => x.Tenant == key.Tenant && + x => x.Tenant == _tenant && x.UserKey == key.UserKey, ct); @@ -35,7 +37,7 @@ public async Task ExistsAsync(UserLifecycleKey key, CancellationToken ct = return await _db.Lifecycles .AnyAsync( - x => x.Tenant == key.Tenant && + x => x.Tenant == _tenant && x.UserKey == key.UserKey, ct); } @@ -44,11 +46,11 @@ public async Task AddAsync(UserLifecycle entity, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - var projection = entity.ToProjection(); - if (entity.Version != 0) throw new InvalidOperationException("New lifecycle must have version 0."); + var projection = entity.ToProjection(); + _db.Lifecycles.Add(projection); await _db.SaveChangesAsync(ct); @@ -58,22 +60,22 @@ public async Task SaveAsync(UserLifecycle entity, long expectedVersion, Cancella { ct.ThrowIfCancellationRequested(); - var projection = entity.ToProjection(); - - _db.Entry(projection).State = EntityState.Modified; + var existing = await _db.Lifecycles + .SingleOrDefaultAsync(x => + x.Tenant == _tenant && + x.UserKey == entity.UserKey, + ct); - _db.Entry(projection) - .Property(x => x.Version) - .OriginalValue = expectedVersion; + if (existing is null) + throw new UAuthNotFoundException("user_lifecycle_not_found"); - try - { - await _db.SaveChangesAsync(ct); - } - catch (DbUpdateConcurrencyException) - { + if (existing.Version != expectedVersion) throw new UAuthConcurrencyException("user_lifecycle_concurrency_conflict"); - } + + entity.UpdateProjection(existing); + existing.Version++; + + await _db.SaveChangesAsync(ct); } public async Task DeleteAsync(UserLifecycleKey key, long expectedVersion, DeleteMode mode, DateTimeOffset now, CancellationToken ct = default) @@ -82,7 +84,7 @@ public async Task DeleteAsync(UserLifecycleKey key, long expectedVersion, Delete var projection = await _db.Lifecycles .SingleOrDefaultAsync( - x => x.Tenant == key.Tenant && + x => x.Tenant == _tenant && x.UserKey == key.UserKey, ct); @@ -105,7 +107,7 @@ public async Task DeleteAsync(UserLifecycleKey key, long expectedVersion, Delete await _db.SaveChangesAsync(ct); } - public async Task> QueryAsync(TenantKey tenant, UserLifecycleQuery query, CancellationToken ct = default) + public async Task> QueryAsync(UserLifecycleQuery query, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); @@ -113,7 +115,7 @@ public async Task> QueryAsync(TenantKey tenant, UserL var baseQuery = _db.Lifecycles .AsNoTracking() - .Where(x => x.Tenant == tenant); + .Where(x => x.Tenant == _tenant); if (!query.IncludeDeleted) baseQuery = baseQuery.Where(x => x.DeletedAt == null); @@ -124,19 +126,13 @@ public async Task> QueryAsync(TenantKey tenant, UserL baseQuery = query.SortBy switch { nameof(UserLifecycle.Id) => - query.Descending - ? baseQuery.OrderByDescending(x => x.Id) - : baseQuery.OrderBy(x => x.Id), + query.Descending ? baseQuery.OrderByDescending(x => x.Id) : baseQuery.OrderBy(x => x.Id), nameof(UserLifecycle.CreatedAt) => - query.Descending - ? baseQuery.OrderByDescending(x => x.CreatedAt) - : baseQuery.OrderBy(x => x.CreatedAt), + query.Descending ? baseQuery.OrderByDescending(x => x.CreatedAt) : baseQuery.OrderBy(x => x.CreatedAt), nameof(UserLifecycle.Status) => - query.Descending - ? baseQuery.OrderByDescending(x => x.Status) - : baseQuery.OrderBy(x => x.Status), + query.Descending ? baseQuery.OrderByDescending(x => x.Status) : baseQuery.OrderBy(x => x.Status), _ => baseQuery.OrderBy(x => x.CreatedAt) }; diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserLifecycleStoreFactory.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserLifecycleStoreFactory.cs new file mode 100644 index 00000000..9a346c7a --- /dev/null +++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserLifecycleStoreFactory.cs @@ -0,0 +1,20 @@ +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Users.Reference; +using Microsoft.EntityFrameworkCore; + +namespace CodeBeam.UltimateAuth.Users.EntityFrameworkCore; + +internal sealed class EfCoreUserLifecycleStoreFactory : IUserLifecycleStoreFactory +{ + private readonly UAuthUserDbContext _db; + + public EfCoreUserLifecycleStoreFactory(UAuthUserDbContext db) + { + _db = db; + } + + public IUserLifecycleStore Create(TenantKey tenant) + { + return new EfCoreUserLifecycleStore(_db, new TenantContext(tenant)); + } +} diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserProfileStore.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserProfileStore.cs index cf7286de..a3772ddb 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserProfileStore.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserProfileStore.cs @@ -10,10 +10,12 @@ namespace CodeBeam.UltimateAuth.Users.EntityFrameworkCore; internal sealed class EfCoreUserProfileStore : IUserProfileStore { private readonly UAuthUserDbContext _db; + private readonly TenantKey _tenant; - public EfCoreUserProfileStore(UAuthUserDbContext db) + public EfCoreUserProfileStore(UAuthUserDbContext db, TenantContext tenant) { _db = db; + _tenant = tenant.Tenant; } public async Task GetAsync(UserProfileKey key, CancellationToken ct = default) @@ -23,7 +25,7 @@ public EfCoreUserProfileStore(UAuthUserDbContext db) var projection = await _db.Profiles .AsNoTracking() .SingleOrDefaultAsync(x => - x.Tenant == key.Tenant && + x.Tenant == _tenant && x.UserKey == key.UserKey, ct); @@ -36,7 +38,7 @@ public async Task ExistsAsync(UserProfileKey key, CancellationToken ct = d return await _db.Profiles .AnyAsync(x => - x.Tenant == key.Tenant && + x.Tenant == _tenant && x.UserKey == key.UserKey, ct); } @@ -46,7 +48,12 @@ public async Task AddAsync(UserProfile entity, CancellationToken ct = default) ct.ThrowIfCancellationRequested(); var projection = entity.ToProjection(); + + if (entity.Version != 0) + throw new InvalidOperationException("New profile must have version 0."); + _db.Profiles.Add(projection); + await _db.SaveChangesAsync(ct); } @@ -54,13 +61,21 @@ public async Task SaveAsync(UserProfile entity, long expectedVersion, Cancellati { ct.ThrowIfCancellationRequested(); - var projection = entity.ToProjection(); + var existing = await _db.Profiles + .SingleOrDefaultAsync(x => + x.Tenant == _tenant && + x.UserKey == entity.UserKey, + ct); + + if (existing is null) + throw new UAuthNotFoundException("user_profile_not_found"); + + if (existing.Version != expectedVersion) + throw new UAuthConcurrencyException("user_profile_concurrency_conflict"); - if (entity.Version != expectedVersion + 1) - throw new InvalidOperationException("Profile version must be incremented by domain."); + entity.UpdateProjection(existing); + existing.Version++; - _db.Entry(projection).State = EntityState.Modified; - _db.Entry(projection).Property(x => x.Version).OriginalValue = expectedVersion; await _db.SaveChangesAsync(ct); } @@ -70,7 +85,7 @@ public async Task DeleteAsync(UserProfileKey key, long expectedVersion, DeleteMo var projection = await _db.Profiles .SingleOrDefaultAsync(x => - x.Tenant == key.Tenant && + x.Tenant == _tenant && x.UserKey == key.UserKey, ct); @@ -93,7 +108,7 @@ public async Task DeleteAsync(UserProfileKey key, long expectedVersion, DeleteMo await _db.SaveChangesAsync(ct); } - public async Task> QueryAsync(TenantKey tenant, UserProfileQuery query, CancellationToken ct = default) + public async Task> QueryAsync(UserProfileQuery query, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); @@ -101,7 +116,7 @@ public async Task> QueryAsync(TenantKey tenant, UserPro var baseQuery = _db.Profiles .AsNoTracking() - .Where(x => x.Tenant == tenant); + .Where(x => x.Tenant == _tenant); if (!query.IncludeDeleted) baseQuery = baseQuery.Where(x => x.DeletedAt == null); @@ -147,13 +162,13 @@ public async Task> QueryAsync(TenantKey tenant, UserPro query.Descending); } - public async Task> GetByUsersAsync(TenantKey tenant, IReadOnlyList userKeys, CancellationToken ct = default) + public async Task> GetByUsersAsync(IReadOnlyList userKeys, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); var projections = await _db.Profiles .AsNoTracking() - .Where(x => x.Tenant == tenant) + .Where(x => x.Tenant == _tenant) .Where(x => userKeys.Contains(x.UserKey)) .Where(x => x.DeletedAt == null) .ToListAsync(ct); diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/CodeBeam.UltimateAuth.Users.InMemory.csproj b/src/users/CodeBeam.UltimateAuth.Users.InMemory/CodeBeam.UltimateAuth.Users.InMemory.csproj index fa0680d1..6c1366bb 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.InMemory/CodeBeam.UltimateAuth.Users.InMemory.csproj +++ b/src/users/CodeBeam.UltimateAuth.Users.InMemory/CodeBeam.UltimateAuth.Users.InMemory.csproj @@ -10,6 +10,7 @@ + diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Extensions/ServiceCollectionExtensions.cs b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Extensions/ServiceCollectionExtensions.cs index 9492398b..3d585691 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Extensions/ServiceCollectionExtensions.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Extensions/ServiceCollectionExtensions.cs @@ -1,6 +1,6 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.Infrastructure; +using CodeBeam.UltimateAuth.InMemory; using CodeBeam.UltimateAuth.Users.Reference; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -11,9 +11,9 @@ public static class ServiceCollectionExtensions { public static IServiceCollection AddUltimateAuthUsersInMemory(this IServiceCollection services) { - services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.TryAddSingleton, InMemoryUserIdProvider>(); // Seed never try add diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Infrastructure/InMemoryUserIdProvider.cs b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Infrastructure/InMemoryUserIdProvider.cs index 74a3a233..0d6f4ec8 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Infrastructure/InMemoryUserIdProvider.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Infrastructure/InMemoryUserIdProvider.cs @@ -1,5 +1,5 @@ using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.Infrastructure; +using CodeBeam.UltimateAuth.InMemory; namespace CodeBeam.UltimateAuth.Users.InMemory; diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Infrastructure/InMemoryUserSeedContributor.cs b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Infrastructure/InMemoryUserSeedContributor.cs index 2335b208..659f30d2 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Infrastructure/InMemoryUserSeedContributor.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Infrastructure/InMemoryUserSeedContributor.cs @@ -1,7 +1,7 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.Infrastructure; using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.InMemory; using CodeBeam.UltimateAuth.Server.Infrastructure; using CodeBeam.UltimateAuth.Users.Contracts; using CodeBeam.UltimateAuth.Users.Reference; @@ -12,25 +12,25 @@ internal sealed class InMemoryUserSeedContributor : ISeedContributor { public int Order => 0; - private readonly IUserLifecycleStore _lifecycle; - private readonly IUserProfileStore _profiles; - private readonly IUserIdentifierStore _identifiers; + private readonly IUserLifecycleStoreFactory _lifecycleFactory; + private readonly IUserIdentifierStoreFactory _identifierFactory; + private readonly IUserProfileStoreFactory _profileFactory; private readonly IInMemoryUserIdProvider _ids; private readonly IIdentifierNormalizer _identifierNormalizer; private readonly IClock _clock; public InMemoryUserSeedContributor( - IUserLifecycleStore lifecycle, - IUserProfileStore profiles, - IUserIdentifierStore identifiers, + IUserLifecycleStoreFactory lifecycleFactory, + IUserProfileStoreFactory profileFactory, + IUserIdentifierStoreFactory identifierFactory, IInMemoryUserIdProvider ids, IIdentifierNormalizer identifierNormalizer, IClock clock) { - _lifecycle = lifecycle; - _profiles = profiles; + _lifecycleFactory = lifecycleFactory; + _identifierFactory = identifierFactory; + _profileFactory = profileFactory; _ids = ids; - _identifiers = identifiers; _identifierNormalizer = identifierNormalizer; _clock = clock; } @@ -43,47 +43,60 @@ public async Task SeedAsync(TenantKey tenant, CancellationToken ct = default) private async Task SeedUserAsync(TenantKey tenant, UserKey userKey, string displayName, string username, string email, string phone, CancellationToken ct) { - var userLifecycleKey = new UserLifecycleKey(tenant, userKey); - if (await _lifecycle.ExistsAsync(userLifecycleKey, ct)) - return; - - await _lifecycle.AddAsync(UserLifecycle.Create(tenant, userKey, _clock.UtcNow), ct); - await _profiles.AddAsync(UserProfile.Create(_clock.UtcNow, tenant, userKey, displayName: displayName), ct); - - await _identifiers.AddAsync( - UserIdentifier.Create( - Guid.NewGuid(), - tenant, - userKey, - UserIdentifierType.Username, - username, - _identifierNormalizer.Normalize(UserIdentifierType.Username, username).Normalized, - _clock.UtcNow, - true, - _clock.UtcNow), ct); - - await _identifiers.AddAsync( - UserIdentifier.Create( - Guid.NewGuid(), - tenant, - userKey, - UserIdentifierType.Email, - email, - _identifierNormalizer.Normalize(UserIdentifierType.Email, email).Normalized, - _clock.UtcNow, - true, - _clock.UtcNow), ct); - - await _identifiers.AddAsync( - UserIdentifier.Create( - Guid.NewGuid(), - tenant, - userKey, - UserIdentifierType.Phone, - phone, - _identifierNormalizer.Normalize(UserIdentifierType.Phone, phone).Normalized, - _clock.UtcNow, - true, - _clock.UtcNow), ct); + var now = _clock.UtcNow; + + var lifecycleStore = _lifecycleFactory.Create(tenant); + var profileStore = _profileFactory.Create(tenant); + var identifierStore = _identifierFactory.Create(tenant); + + var lifecycleKey = new UserLifecycleKey(tenant, userKey); + + var exists = await lifecycleStore.ExistsAsync(lifecycleKey, ct); + + if (!exists) + { + await lifecycleStore.AddAsync( + UserLifecycle.Create(tenant, userKey, now), + ct); + } + + var profileKey = new UserProfileKey(tenant, userKey); + if (!await profileStore.ExistsAsync(profileKey, ct)) + { + await profileStore.AddAsync( + UserProfile.Create(Guid.NewGuid(), tenant, userKey, now, displayName: displayName), + ct); + } + + async Task EnsureIdentifier( + UserIdentifierType type, + string value, + bool isPrimary) + { + var normalized = _identifierNormalizer + .Normalize(type, value).Normalized; + + var existing = await identifierStore.GetAsync(type, normalized, ct); + + if (existing is not null) + return; + + await identifierStore.AddAsync( + UserIdentifier.Create( + Guid.NewGuid(), + tenant, + userKey, + type, + value, + normalized, + now, + isPrimary, + now), + ct); + } + + await EnsureIdentifier(UserIdentifierType.Username, username, true); + await EnsureIdentifier(UserIdentifierType.Email, email, true); + await EnsureIdentifier(UserIdentifierType.Phone, phone, true); } } diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserIdentifierStore.cs b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserIdentifierStore.cs index b99c7604..cd624dbc 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserIdentifierStore.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserIdentifierStore.cs @@ -1,25 +1,29 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.Errors; using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.InMemory; using CodeBeam.UltimateAuth.Users.Contracts; using CodeBeam.UltimateAuth.Users.Reference; namespace CodeBeam.UltimateAuth.Users.InMemory; -public sealed class InMemoryUserIdentifierStore : InMemoryVersionedStore, IUserIdentifierStore +public sealed class InMemoryUserIdentifierStore : InMemoryTenantVersionedStore, IUserIdentifierStore { protected override Guid GetKey(UserIdentifier entity) => entity.Id; private readonly object _primaryLock = new(); + public InMemoryUserIdentifierStore(TenantContext tenant) : base(tenant) + { + + } + public Task ExistsAsync(IdentifierExistenceQuery query, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - var candidates = Values() + var candidates = TenantValues() .Where(x => - x.Tenant == query.Tenant && x.Type == query.Type && x.NormalizedValue == query.NormalizedValue && !x.IsDeleted); @@ -50,18 +54,17 @@ public Task ExistsAsync(IdentifierExistenceQuery quer new IdentifierExistenceResult(true, match.UserKey, match.Id, match.IsPrimary)); } - public Task GetAsync(TenantKey tenant, UserIdentifierType type, string normalizedValue, CancellationToken ct = default) + public Task GetAsync(UserIdentifierType type, string normalizedValue, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - var identifier = Values() + var identifier = TenantValues() .FirstOrDefault(x => - x.Tenant == tenant && x.Type == type && x.NormalizedValue == normalizedValue && !x.IsDeleted); - return Task.FromResult(identifier); + return Task.FromResult(identifier?.Snapshot()); } public Task GetByIdAsync(Guid id, CancellationToken ct = default) @@ -69,12 +72,11 @@ public Task ExistsAsync(IdentifierExistenceQuery quer return GetAsync(id, ct); } - public Task> GetByUserAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default) + public Task> GetByUserAsync(UserKey userKey, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - var result = Values() - .Where(x => x.Tenant == tenant) + var result = TenantValues() .Where(x => x.UserKey == userKey) .Where(x => !x.IsDeleted) .OrderBy(x => x.CreatedAt) @@ -134,7 +136,7 @@ public override Task SaveAsync(UserIdentifier entity, long expectedVersion, Canc } } - public Task> QueryAsync(TenantKey tenant, UserIdentifierQuery query, CancellationToken ct = default) + public Task> QueryAsync(UserIdentifierQuery query, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); @@ -143,8 +145,7 @@ public Task> QueryAsync(TenantKey tenant, UserIdenti var normalized = query.Normalize(); - var baseQuery = Values() - .Where(x => x.Tenant == tenant) + var baseQuery = TenantValues() .Where(x => x.UserKey == query.UserKey.Value); if (!query.IncludeDeleted) @@ -195,14 +196,13 @@ public Task> QueryAsync(TenantKey tenant, UserIdenti } - public Task> GetByUsersAsync(TenantKey tenant, IReadOnlyList userKeys, CancellationToken ct = default) + public Task> GetByUsersAsync(IReadOnlyList userKeys, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); var set = userKeys.ToHashSet(); - var result = Values() - .Where(x => x.Tenant == tenant) + var result = TenantValues() .Where(x => set.Contains(x.UserKey)) .Where(x => !x.IsDeleted) .Select(x => x.Snapshot()) @@ -212,12 +212,12 @@ public Task> GetByUsersAsync(TenantKey tenant, IRe return Task.FromResult>(result); } - public async Task DeleteByUserAsync(TenantKey tenant, UserKey userKey, DeleteMode mode, DateTimeOffset deletedAt, CancellationToken ct = default) + public async Task DeleteByUserAsync(UserKey userKey, DeleteMode mode, DateTimeOffset deletedAt, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - var identifiers = Values() - .Where(x => x.Tenant == tenant && x.UserKey == userKey && !x.IsDeleted) + var identifiers = TenantValues() + .Where(x => x.UserKey == userKey && !x.IsDeleted) .ToList(); foreach (var identifier in identifiers) diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserIdentifierStoreFactory.cs b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserIdentifierStoreFactory.cs new file mode 100644 index 00000000..828fcc51 --- /dev/null +++ b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserIdentifierStoreFactory.cs @@ -0,0 +1,27 @@ +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Users.Reference; +using Microsoft.Extensions.DependencyInjection; +using System.Collections.Concurrent; + +namespace CodeBeam.UltimateAuth.Users.InMemory; + +internal sealed class InMemoryUserIdentifierStoreFactory : IUserIdentifierStoreFactory +{ + private readonly IServiceProvider _provider; + private readonly ConcurrentDictionary _stores = new(); + + public InMemoryUserIdentifierStoreFactory(IServiceProvider provider) + { + _provider = provider; + } + + public IUserIdentifierStore Create(TenantKey tenant) + { + return _stores.GetOrAdd(tenant, t => + { + Console.WriteLine("New Store Added"); + var tenantContext = new TenantContext(tenant); + return ActivatorUtilities.CreateInstance(_provider, tenantContext); + }); + } +} diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserLifecycleStore.cs b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserLifecycleStore.cs index 912fd90b..4546acfc 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserLifecycleStore.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserLifecycleStore.cs @@ -1,23 +1,25 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.InMemory; using CodeBeam.UltimateAuth.Users.Reference; namespace CodeBeam.UltimateAuth.Users.InMemory; -public sealed class InMemoryUserLifecycleStore : InMemoryVersionedStore, IUserLifecycleStore +public sealed class InMemoryUserLifecycleStore : InMemoryTenantVersionedStore, IUserLifecycleStore { protected override UserLifecycleKey GetKey(UserLifecycle entity) => new(entity.Tenant, entity.UserKey); - public Task> QueryAsync(TenantKey tenant, UserLifecycleQuery query, CancellationToken ct = default) + public InMemoryUserLifecycleStore(TenantContext tenant) : base(tenant) + { + } + + public Task> QueryAsync(UserLifecycleQuery query, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); var normalized = query.Normalize(); - - var baseQuery = Values() - .Where(x => x.Tenant == tenant); + var baseQuery = TenantValues().AsQueryable(); if (!query.IncludeDeleted) baseQuery = baseQuery.Where(x => !x.IsDeleted); @@ -42,11 +44,6 @@ public Task> QueryAsync(TenantKey tenant, UserLifecyc ? baseQuery.OrderByDescending(x => x.Status) : baseQuery.OrderBy(x => x.Status), - nameof(UserLifecycle.Tenant) => - query.Descending - ? baseQuery.OrderByDescending(x => x.Tenant.Value) - : baseQuery.OrderBy(x => x.Tenant.Value), - nameof(UserLifecycle.UserKey) => query.Descending ? baseQuery.OrderByDescending(x => x.UserKey.Value) @@ -61,7 +58,7 @@ public Task> QueryAsync(TenantKey tenant, UserLifecyc }; var totalCount = baseQuery.Count(); - var items = baseQuery.Skip((normalized.PageNumber - 1) * normalized.PageSize).Take(normalized.PageSize).ToList().AsReadOnly(); + var items = baseQuery.Skip((normalized.PageNumber - 1) * normalized.PageSize).Take(normalized.PageSize).Select(x => x.Snapshot()).ToList().AsReadOnly(); return Task.FromResult(new PagedResult(items, totalCount, normalized.PageNumber, normalized.PageSize, query.SortBy, query.Descending)); } diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserLifecycleStoreFactory.cs b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserLifecycleStoreFactory.cs new file mode 100644 index 00000000..f19359f7 --- /dev/null +++ b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserLifecycleStoreFactory.cs @@ -0,0 +1,26 @@ +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Users.Reference; +using Microsoft.Extensions.DependencyInjection; +using System.Collections.Concurrent; + +namespace CodeBeam.UltimateAuth.Users.InMemory; + +internal sealed class InMemoryUserLifecycleStoreFactory : IUserLifecycleStoreFactory +{ + private readonly IServiceProvider _provider; + private readonly ConcurrentDictionary _stores = new(); + + public InMemoryUserLifecycleStoreFactory(IServiceProvider provider) + { + _provider = provider; + } + + public IUserLifecycleStore Create(TenantKey tenant) + { + return _stores.GetOrAdd(tenant, t => + { + var tenantContext = new TenantContext(tenant); + return ActivatorUtilities.CreateInstance(_provider, tenantContext); + }); + } +} diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserProfileStore.cs b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserProfileStore.cs index d24ff036..38195576 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserProfileStore.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserProfileStore.cs @@ -1,24 +1,26 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.InMemory; using CodeBeam.UltimateAuth.Users.Reference; namespace CodeBeam.UltimateAuth.Users.InMemory; -public sealed class InMemoryUserProfileStore : InMemoryVersionedStore, IUserProfileStore +public sealed class InMemoryUserProfileStore : InMemoryTenantVersionedStore, IUserProfileStore { protected override UserProfileKey GetKey(UserProfile entity) => new(entity.Tenant, entity.UserKey); - public Task> QueryAsync(TenantKey tenant, UserProfileQuery query, CancellationToken ct = default) + public InMemoryUserProfileStore(TenantContext tenant) : base(tenant) + { + } + + public Task> QueryAsync(UserProfileQuery query, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); var normalized = query.Normalize(); - - var baseQuery = Values() - .Where(x => x.Tenant == tenant); + var baseQuery = TenantValues().AsQueryable(); if (!query.IncludeDeleted) baseQuery = baseQuery.Where(x => !x.IsDeleted); @@ -53,6 +55,7 @@ public Task> QueryAsync(TenantKey tenant, UserProfileQu var items = baseQuery .Skip((normalized.PageNumber - 1) * normalized.PageSize) .Take(normalized.PageSize) + .Select(x => x.Snapshot()) .ToList() .AsReadOnly(); @@ -66,14 +69,13 @@ public Task> QueryAsync(TenantKey tenant, UserProfileQu query.Descending)); } - public Task> GetByUsersAsync(TenantKey tenant, IReadOnlyList userKeys, CancellationToken ct = default) + public Task> GetByUsersAsync(IReadOnlyList userKeys, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); var set = userKeys.ToHashSet(); - var result = Values() - .Where(x => x.Tenant == tenant) + var result = TenantValues() .Where(x => set.Contains(x.UserKey)) .Where(x => !x.IsDeleted) .Select(x => x.Snapshot()) diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserProfileStoreFactory.cs b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserProfileStoreFactory.cs new file mode 100644 index 00000000..b6f49bd4 --- /dev/null +++ b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserProfileStoreFactory.cs @@ -0,0 +1,26 @@ +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Users.Reference; +using Microsoft.Extensions.DependencyInjection; +using System.Collections.Concurrent; + +namespace CodeBeam.UltimateAuth.Users.InMemory; + +internal sealed class InMemoryUserProfileStoreFactory : IUserProfileStoreFactory +{ + private readonly IServiceProvider _provider; + private readonly ConcurrentDictionary _stores = new(); + + public InMemoryUserProfileStoreFactory(IServiceProvider provider) + { + _provider = provider; + } + + public IUserProfileStore Create(TenantKey tenant) + { + return _stores.GetOrAdd(tenant, t => + { + var tenantContext = new TenantContext(t); + return ActivatorUtilities.CreateInstance(_provider, tenantContext); + }); + } +} diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserIdentifier.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserIdentifier.cs index f47dee0b..1f00216e 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserIdentifier.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserIdentifier.cs @@ -5,7 +5,8 @@ using CodeBeam.UltimateAuth.Users.Contracts; namespace CodeBeam.UltimateAuth.Users.Reference; -public sealed class UserIdentifier : IVersionedEntity, ISoftDeletable, IEntitySnapshot + +public sealed class UserIdentifier : ITenantEntity, IVersionedEntity, ISoftDeletable, IEntitySnapshot { public Guid Id { get; private set; } public TenantKey Tenant { get; private set; } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserLifecycle.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserLifecycle.cs index b0b64139..0d4d7d04 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserLifecycle.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserLifecycle.cs @@ -5,7 +5,7 @@ namespace CodeBeam.UltimateAuth.Users.Reference; -public sealed class UserLifecycle : IVersionedEntity, ISoftDeletable, IEntitySnapshot +public sealed class UserLifecycle : ITenantEntity, IVersionedEntity, ISoftDeletable, IEntitySnapshot { private UserLifecycle() { } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserProfile.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserProfile.cs index fbf58740..9bc18905 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserProfile.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserProfile.cs @@ -5,7 +5,7 @@ namespace CodeBeam.UltimateAuth.Users.Reference; // TODO: Multi profile (e.g., public profiles, private profiles, profiles per application, etc. with ProfileKey) -public sealed class UserProfile : IVersionedEntity, ISoftDeletable, IEntitySnapshot +public sealed class UserProfile : ITenantEntity, IVersionedEntity, ISoftDeletable, IEntitySnapshot { private UserProfile() { } @@ -62,10 +62,10 @@ public UserProfile Snapshot() } public static UserProfile Create( - DateTimeOffset createdAt, + Guid? id, TenantKey tenant, UserKey userKey, - Guid? id = null, + DateTimeOffset createdAt, string? firstName = null, string? lastName = null, string? displayName = null, diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Infrastructure/LoginIdentifierResolver.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Infrastructure/LoginIdentifierResolver.cs index 5a061172..9a1350e0 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Infrastructure/LoginIdentifierResolver.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Infrastructure/LoginIdentifierResolver.cs @@ -7,18 +7,18 @@ namespace CodeBeam.UltimateAuth.Users.Reference; public sealed class LoginIdentifierResolver : ILoginIdentifierResolver { - private readonly IUserIdentifierStore _store; + private readonly IUserIdentifierStoreFactory _storeFactory; private readonly IIdentifierNormalizer _normalizer; private readonly IEnumerable _customResolvers; private readonly UAuthLoginIdentifierOptions _options; public LoginIdentifierResolver( - IUserIdentifierStore store, + IUserIdentifierStoreFactory storeFactory, IIdentifierNormalizer normalizer, IEnumerable customResolvers, IOptions options) { - _store = store; + _storeFactory = storeFactory; _normalizer = normalizer; _customResolvers = customResolvers; _options = options.Value.LoginIdentifiers; @@ -58,7 +58,8 @@ public LoginIdentifierResolver( return null; } - var found = await _store.GetAsync(tenant, builtInType, normalized, ct); + var store = _storeFactory.Create(tenant); + var found = await store.GetAsync(builtInType, normalized, ct); if (found is null || !found.IsPrimary) { if (_options.EnableCustomResolvers && !_options.CustomResolversFirst) diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Infrastructure/PrimaryUserIdentifierProvider.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Infrastructure/PrimaryUserIdentifierProvider.cs index 74dc264e..1e096928 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Infrastructure/PrimaryUserIdentifierProvider.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Infrastructure/PrimaryUserIdentifierProvider.cs @@ -6,16 +6,17 @@ namespace CodeBeam.UltimateAuth.Users.Reference; internal sealed class PrimaryUserIdentifierProvider : IPrimaryUserIdentifierProvider { - private readonly IUserIdentifierStore _store; + private readonly IUserIdentifierStoreFactory _storeFactory; - public PrimaryUserIdentifierProvider(IUserIdentifierStore store) + public PrimaryUserIdentifierProvider(IUserIdentifierStoreFactory storeFactory) { - _store = store; + _storeFactory = storeFactory; } public async Task GetAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default) { - var identifiers = await _store.GetByUserAsync(tenant, userKey, ct); + var store = _storeFactory.Create(tenant); + var identifiers = await store.GetByUserAsync(userKey, ct); var primary = identifiers.Where(x => x.IsPrimary).ToList(); if (primary.Count == 0) diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Infrastructure/UserLifecycleSnaphotProvider.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Infrastructure/UserLifecycleSnaphotProvider.cs index 82350491..34a9d8cb 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Infrastructure/UserLifecycleSnaphotProvider.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Infrastructure/UserLifecycleSnaphotProvider.cs @@ -6,16 +6,17 @@ namespace CodeBeam.UltimateAuth.Users.Reference; internal sealed class UserLifecycleSnapshotProvider : IUserLifecycleSnapshotProvider { - private readonly IUserLifecycleStore _store; + private readonly IUserLifecycleStoreFactory _storeFactory; - public UserLifecycleSnapshotProvider(IUserLifecycleStore store) + public UserLifecycleSnapshotProvider(IUserLifecycleStoreFactory storeFactory) { - _store = store; + _storeFactory = storeFactory; } public async Task GetAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default) { - var profile = await _store.GetAsync(new UserLifecycleKey(tenant, userKey), ct); + var store = _storeFactory.Create(tenant); + var profile = await store.GetAsync(new UserLifecycleKey(tenant, userKey), ct); if (profile is null || profile.IsDeleted) return null; diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Infrastructure/UserProfileSnapshotProvider.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Infrastructure/UserProfileSnapshotProvider.cs index f3ccc480..72cbe134 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Infrastructure/UserProfileSnapshotProvider.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Infrastructure/UserProfileSnapshotProvider.cs @@ -6,16 +6,17 @@ namespace CodeBeam.UltimateAuth.Users.Reference; internal sealed class UserProfileSnapshotProvider : IUserProfileSnapshotProvider { - private readonly IUserProfileStore _store; + private readonly IUserProfileStoreFactory _storeFactory; - public UserProfileSnapshotProvider(IUserProfileStore store) + public UserProfileSnapshotProvider(IUserProfileStoreFactory storeFactory) { - _store = store; + _storeFactory = storeFactory; } public async Task GetAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default) { - var profile = await _store.GetAsync(new UserProfileKey(tenant, userKey), ct); + var store = _storeFactory.Create(tenant); + var profile = await store.GetAsync(new UserProfileKey(tenant, userKey), ct); if (profile is null || profile.IsDeleted) return null; diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs index 0044d49c..b122e142 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs @@ -6,6 +6,7 @@ using CodeBeam.UltimateAuth.Server.Infrastructure; using CodeBeam.UltimateAuth.Server.Options; using CodeBeam.UltimateAuth.Users.Contracts; +using CodeBeam.UltimateAuth.Users; using Microsoft.Extensions.Options; namespace CodeBeam.UltimateAuth.Users.Reference; @@ -13,9 +14,9 @@ namespace CodeBeam.UltimateAuth.Users.Reference; internal sealed class UserApplicationService : IUserApplicationService { private readonly IAccessOrchestrator _accessOrchestrator; - private readonly IUserLifecycleStore _lifecycleStore; - private readonly IUserProfileStore _profileStore; - private readonly IUserIdentifierStore _identifierStore; + private readonly IUserLifecycleStoreFactory _lifecycleStoreFactory; + private readonly IUserIdentifierStoreFactory _identifierStoreFactory; + private readonly IUserProfileStoreFactory _profileStoreFactory; private readonly IUserCreateValidator _userCreateValidator; private readonly IIdentifierValidator _identifierValidator; private readonly IEnumerable _integrations; @@ -26,9 +27,9 @@ internal sealed class UserApplicationService : IUserApplicationService public UserApplicationService( IAccessOrchestrator accessOrchestrator, - IUserLifecycleStore lifecycleStore, - IUserProfileStore profileStore, - IUserIdentifierStore identifierStore, + IUserLifecycleStoreFactory lifecycleStoreFactory, + IUserIdentifierStoreFactory identifierStoreFactory, + IUserProfileStoreFactory profileStoreFactory, IUserCreateValidator userCreateValidator, IIdentifierValidator identifierValidator, IEnumerable integrations, @@ -38,9 +39,9 @@ public UserApplicationService( IClock clock) { _accessOrchestrator = accessOrchestrator; - _lifecycleStore = lifecycleStore; - _profileStore = profileStore; - _identifierStore = identifierStore; + _lifecycleStoreFactory = lifecycleStoreFactory; + _identifierStoreFactory = identifierStoreFactory; + _profileStoreFactory = profileStoreFactory; _userCreateValidator = userCreateValidator; _identifierValidator = identifierValidator; _integrations = integrations; @@ -65,13 +66,16 @@ public async Task CreateUserAsync(AccessContext context, Creat var now = _clock.UtcNow; var userKey = UserKey.New(); - await _lifecycleStore.AddAsync(UserLifecycle.Create(context.ResourceTenant, userKey, now), innerCt); + var lifecycleStore = _lifecycleStoreFactory.Create(context.ResourceTenant); + await lifecycleStore.AddAsync(UserLifecycle.Create(context.ResourceTenant, userKey, now), innerCt); - await _profileStore.AddAsync( + var profileStore = _profileStoreFactory.Create(context.ResourceTenant); + await profileStore.AddAsync( UserProfile.Create( - now, + Guid.NewGuid(), context.ResourceTenant, userKey, + now, firstName: request.FirstName, lastName: request.LastName, displayName: request.DisplayName ?? request.UserName ?? request.Email ?? request.Phone, @@ -82,9 +86,10 @@ await _profileStore.AddAsync( timezone: request.TimeZone, culture: request.Culture), innerCt); + var identifierStore = _identifierStoreFactory.Create(context.ResourceTenant); if (!string.IsNullOrWhiteSpace(request.UserName)) { - await _identifierStore.AddAsync( + await identifierStore.AddAsync( UserIdentifier.Create( Guid.NewGuid(), context.ResourceTenant, @@ -99,7 +104,7 @@ await _identifierStore.AddAsync( if (!string.IsNullOrWhiteSpace(request.Email)) { - await _identifierStore.AddAsync( + await identifierStore.AddAsync( UserIdentifier.Create( Guid.NewGuid(), context.ResourceTenant, @@ -114,7 +119,7 @@ await _identifierStore.AddAsync( if (!string.IsNullOrWhiteSpace(request.Phone)) { - await _identifierStore.AddAsync( + await identifierStore.AddAsync( UserIdentifier.Create( Guid.NewGuid(), context.ResourceTenant, @@ -152,7 +157,8 @@ public async Task ChangeUserStatusAsync(AccessContext context, object request, C var targetUserKey = context.GetTargetUserKey(); var userLifecycleKey = new UserLifecycleKey(context.ResourceTenant, targetUserKey); - var current = await _lifecycleStore.GetAsync(userLifecycleKey, innerCt); + var lifecycleStore = _lifecycleStoreFactory.Create(context.ResourceTenant); + var current = await lifecycleStore.GetAsync(userLifecycleKey, innerCt); var now = _clock.UtcNow; if (current is null) @@ -167,7 +173,7 @@ public async Task ChangeUserStatusAsync(AccessContext context, object request, C throw new UAuthConflictException("admin_cannot_set_self_status"); } var newEntity = current.ChangeStatus(now, newStatus); - await _lifecycleStore.SaveAsync(newEntity, current.Version, innerCt); + await lifecycleStore.SaveAsync(newEntity, current.Version, innerCt); }); await _accessOrchestrator.ExecuteAsync(context, command, ct); @@ -181,20 +187,23 @@ public async Task DeleteMeAsync(AccessContext context, CancellationToken ct = de var now = _clock.UtcNow; var lifecycleKey = new UserLifecycleKey(context.ResourceTenant, userKey); - var lifecycle = await _lifecycleStore.GetAsync(lifecycleKey, innerCt); + var lifecycleStore = _lifecycleStoreFactory.Create(context.ResourceTenant); + var lifecycle = await lifecycleStore.GetAsync(lifecycleKey, innerCt); if (lifecycle is null) throw new UAuthNotFoundException(); + var profileStore = _profileStoreFactory.Create(context.ResourceTenant); + var identifierStore = _identifierStoreFactory.Create(context.ResourceTenant); var profileKey = new UserProfileKey(context.ResourceTenant, userKey); - var profile = await _profileStore.GetAsync(profileKey, innerCt); + var profile = await profileStore.GetAsync(profileKey, innerCt); - await _lifecycleStore.DeleteAsync(lifecycleKey, lifecycle.Version, DeleteMode.Soft, now, innerCt); - await _identifierStore.DeleteByUserAsync(context.ResourceTenant, userKey, DeleteMode.Soft, now, innerCt); + await lifecycleStore.DeleteAsync(lifecycleKey, lifecycle.Version, DeleteMode.Soft, now, innerCt); + await identifierStore.DeleteByUserAsync(userKey, DeleteMode.Soft, now, innerCt); if (profile is not null) { - await _profileStore.DeleteAsync(profileKey, profile.Version, DeleteMode.Soft, now, innerCt); + await profileStore.DeleteAsync(profileKey, profile.Version, DeleteMode.Soft, now, innerCt); } foreach (var integration in _integrations) @@ -203,7 +212,7 @@ public async Task DeleteMeAsync(AccessContext context, CancellationToken ct = de } var sessionStore = _sessionStoreFactory.Create(context.ResourceTenant); - await sessionStore.RevokeAllChainsAsync(context.ResourceTenant, userKey, now, innerCt); + await sessionStore.RevokeAllChainsAsync(userKey, now, innerCt); }); await _accessOrchestrator.ExecuteAsync(context, command, ct); @@ -216,20 +225,22 @@ public async Task DeleteUserAsync(AccessContext context, DeleteUserRequest reque var targetUserKey = context.GetTargetUserKey(); var now = _clock.UtcNow; var userLifecycleKey = new UserLifecycleKey(context.ResourceTenant, targetUserKey); - - var lifecycle = await _lifecycleStore.GetAsync(userLifecycleKey, innerCt); + var lifecycleStore = _lifecycleStoreFactory.Create(context.ResourceTenant); + var lifecycle = await lifecycleStore.GetAsync(userLifecycleKey, innerCt); if (lifecycle is null) throw new UAuthNotFoundException(); + var profileStore = _profileStoreFactory.Create(context.ResourceTenant); + var identifierStore = _identifierStoreFactory.Create(context.ResourceTenant); var profileKey = new UserProfileKey(context.ResourceTenant, targetUserKey); - var profile = await _profileStore.GetAsync(profileKey, innerCt); - await _lifecycleStore.DeleteAsync(userLifecycleKey, lifecycle.Version, request.Mode, now, innerCt); - await _identifierStore.DeleteByUserAsync(context.ResourceTenant, targetUserKey, request.Mode, now, innerCt); + var profile = await profileStore.GetAsync(profileKey, innerCt); + await lifecycleStore.DeleteAsync(userLifecycleKey, lifecycle.Version, request.Mode, now, innerCt); + await identifierStore.DeleteByUserAsync(targetUserKey, request.Mode, now, innerCt); if (profile is not null) { - await _profileStore.DeleteAsync(profileKey, profile.Version, request.Mode, now, innerCt); + await profileStore.DeleteAsync(profileKey, profile.Version, request.Mode, now, innerCt); } foreach (var integration in _integrations) @@ -280,8 +291,8 @@ public async Task UpdateUserProfileAsync(AccessContext context, UpdateProfileReq var now = _clock.UtcNow; var key = new UserProfileKey(tenant, userKey); - - var profile = await _profileStore.GetAsync(key, innerCt); + var profileStore = _profileStoreFactory.Create(tenant); + var profile = await profileStore.GetAsync(key, innerCt); if (profile is null) throw new UAuthNotFoundException(); @@ -294,7 +305,7 @@ public async Task UpdateUserProfileAsync(AccessContext context, UpdateProfileReq .UpdateLocalization(request.Language, request.TimeZone, request.Culture, now) .UpdateMetadata(request.Metadata, now); - await _profileStore.SaveAsync(profile, expectedVersion, innerCt); + await profileStore.SaveAsync(profile, expectedVersion, innerCt); }); await _accessOrchestrator.ExecuteAsync(context, command, ct); @@ -313,8 +324,8 @@ public async Task> GetIdentifiersByUserAsync(Acc query ??= new UserIdentifierQuery(); query.UserKey = targetUserKey; - - var result = await _identifierStore.QueryAsync(context.ResourceTenant, query, innerCt); + var identifierStore = _identifierStoreFactory.Create(context.ResourceTenant); + var result = await identifierStore.QueryAsync(query, innerCt); var dtoItems = result.Items.Select(UserIdentifierMapper.ToDto).ToList().AsReadOnly(); return new PagedResult( @@ -337,7 +348,8 @@ public async Task> GetIdentifiersByUserAsync(Acc if (!normalized.IsValid) return null; - var identifier = await _identifierStore.GetAsync(context.ResourceTenant, type, normalized.Normalized, innerCt); + var identifierStore = _identifierStoreFactory.Create(context.ResourceTenant); + var identifier = await identifierStore.GetAsync(type, normalized.Normalized, innerCt); return identifier is null ? null : UserIdentifierMapper.ToDto(identifier); }); @@ -354,7 +366,8 @@ public async Task UserIdentifierExistsAsync(AccessContext context, UserIde UserKey? userKey = scope == IdentifierExistenceScope.WithinUser ? context.GetTargetUserKey() : null; - var result = await _identifierStore.ExistsAsync(new IdentifierExistenceQuery(context.ResourceTenant, type, normalized.Normalized, scope, userKey), innerCt); + var identifierStore = _identifierStoreFactory.Create(context.ResourceTenant); + var result = await identifierStore.ExistsAsync(new IdentifierExistenceQuery(type, normalized.Normalized, scope, userKey), innerCt); return result.Exists; }); @@ -379,11 +392,12 @@ public async Task AddUserIdentifierAsync(AccessContext context, AddUserIdentifie if (!normalized.IsValid) throw new UAuthIdentifierValidationException(normalized.ErrorCode ?? "identifier_invalid"); - var existing = await _identifierStore.GetByUserAsync(context.ResourceTenant, userKey, innerCt); + var identifierStore = _identifierStoreFactory.Create(context.ResourceTenant); + var existing = await identifierStore.GetByUserAsync(userKey, innerCt); EnsureMultipleIdentifierAllowed(request.Type, existing); - var userScopeResult = await _identifierStore.ExistsAsync( - new IdentifierExistenceQuery(context.ResourceTenant, request.Type, normalized.Normalized, IdentifierExistenceScope.WithinUser, UserKey: userKey), innerCt); + var userScopeResult = await identifierStore.ExistsAsync( + new IdentifierExistenceQuery(request.Type, normalized.Normalized, IdentifierExistenceScope.WithinUser, UserKey: userKey), innerCt); if (userScopeResult.Exists) throw new UAuthIdentifierConflictException("identifier_already_exists_for_user"); @@ -397,9 +411,8 @@ public async Task AddUserIdentifierAsync(AccessContext context, AddUserIdentifie ? IdentifierExistenceScope.TenantAny : IdentifierExistenceScope.TenantPrimaryOnly; - var globalResult = await _identifierStore.ExistsAsync( + var globalResult = await identifierStore.ExistsAsync( new IdentifierExistenceQuery( - context.ResourceTenant, request.Type, normalized.Normalized, scope), @@ -416,7 +429,7 @@ public async Task AddUserIdentifierAsync(AccessContext context, AddUserIdentifie EnsureVerificationRequirements(request.Type, isVerified: false); } - await _identifierStore.AddAsync( + await identifierStore.AddAsync( UserIdentifier.Create( Guid.NewGuid(), context.ResourceTenant, @@ -437,7 +450,8 @@ public async Task UpdateUserIdentifierAsync(AccessContext context, UpdateUserIde { EnsureOverrideAllowed(context); - var identifier = await _identifierStore.GetByIdAsync(request.Id, innerCt); + var identifierStore = _identifierStoreFactory.Create(context.ResourceTenant); + var identifier = await identifierStore.GetByIdAsync(request.Id, innerCt); if (identifier is null || identifier.IsDeleted) throw new UAuthIdentifierNotFoundException("identifier_not_found"); @@ -461,9 +475,8 @@ public async Task UpdateUserIdentifierAsync(AccessContext context, UpdateUserIde if (string.Equals(identifier.NormalizedValue, normalized.Normalized, StringComparison.Ordinal)) throw new UAuthIdentifierValidationException("identifier_value_unchanged"); - var withinUserResult = await _identifierStore.ExistsAsync( + var withinUserResult = await identifierStore.ExistsAsync( new IdentifierExistenceQuery( - identifier.Tenant, identifier.Type, normalized.Normalized, IdentifierExistenceScope.WithinUser, @@ -483,9 +496,8 @@ public async Task UpdateUserIdentifierAsync(AccessContext context, UpdateUserIde ? IdentifierExistenceScope.TenantAny : IdentifierExistenceScope.TenantPrimaryOnly; - var result = await _identifierStore.ExistsAsync( + var result = await identifierStore.ExistsAsync( new IdentifierExistenceQuery( - identifier.Tenant, identifier.Type, normalized.Normalized, scope, @@ -499,7 +511,7 @@ public async Task UpdateUserIdentifierAsync(AccessContext context, UpdateUserIde var expectedVersion = identifier.Version; identifier.ChangeValue(request.NewValue, normalized.Normalized, _clock.UtcNow); - await _identifierStore.SaveAsync(identifier, expectedVersion, innerCt); + await identifierStore.SaveAsync(identifier, expectedVersion, innerCt); }); await _accessOrchestrator.ExecuteAsync(context, command, ct); @@ -511,7 +523,8 @@ public async Task SetPrimaryUserIdentifierAsync(AccessContext context, SetPrimar { EnsureOverrideAllowed(context); - var identifier = await _identifierStore.GetByIdAsync(request.IdentifierId, innerCt); + var identifierStore = _identifierStoreFactory.Create(context.ResourceTenant); + var identifier = await identifierStore.GetByIdAsync(request.IdentifierId, innerCt); if (identifier is null) throw new UAuthIdentifierNotFoundException("identifier_not_found"); @@ -520,15 +533,15 @@ public async Task SetPrimaryUserIdentifierAsync(AccessContext context, SetPrimar EnsureVerificationRequirements(identifier.Type, identifier.IsVerified); - var result = await _identifierStore.ExistsAsync( - new IdentifierExistenceQuery(identifier.Tenant, identifier.Type, identifier.NormalizedValue, IdentifierExistenceScope.TenantPrimaryOnly, ExcludeIdentifierId: identifier.Id), innerCt); + var result = await identifierStore.ExistsAsync( + new IdentifierExistenceQuery(identifier.Type, identifier.NormalizedValue, IdentifierExistenceScope.TenantPrimaryOnly, ExcludeIdentifierId: identifier.Id), innerCt); if (result.Exists) throw new UAuthIdentifierConflictException("identifier_already_exists"); var expectedVersion = identifier.Version; identifier.SetPrimary(_clock.UtcNow); - await _identifierStore.SaveAsync(identifier, expectedVersion, innerCt); + await identifierStore.SaveAsync(identifier, expectedVersion, innerCt); }); await _accessOrchestrator.ExecuteAsync(context, command, ct); @@ -540,7 +553,8 @@ public async Task UnsetPrimaryUserIdentifierAsync(AccessContext context, UnsetPr { EnsureOverrideAllowed(context); - var identifier = await _identifierStore.GetByIdAsync(request.IdentifierId, innerCt); + var identifierStore = _identifierStoreFactory.Create(context.ResourceTenant); + var identifier = await identifierStore.GetByIdAsync(request.IdentifierId, innerCt); if (identifier is null) throw new UAuthIdentifierNotFoundException("identifier_not_found"); @@ -548,7 +562,7 @@ public async Task UnsetPrimaryUserIdentifierAsync(AccessContext context, UnsetPr throw new UAuthIdentifierValidationException("identifier_already_not_primary"); var userIdentifiers = - await _identifierStore.GetByUserAsync(identifier.Tenant, identifier.UserKey, innerCt); + await identifierStore.GetByUserAsync(identifier.UserKey, innerCt); var activeLoginPrimaries = userIdentifiers .Where(i => @@ -565,7 +579,7 @@ public async Task UnsetPrimaryUserIdentifierAsync(AccessContext context, UnsetPr var expectedVersion = identifier.Version; identifier.UnsetPrimary(_clock.UtcNow); - await _identifierStore.SaveAsync(identifier, expectedVersion, innerCt); + await identifierStore.SaveAsync(identifier, expectedVersion, innerCt); }); await _accessOrchestrator.ExecuteAsync(context, command, ct); @@ -577,13 +591,14 @@ public async Task VerifyUserIdentifierAsync(AccessContext context, VerifyUserIde { EnsureOverrideAllowed(context); - var identifier = await _identifierStore.GetByIdAsync(request.IdentifierId, innerCt); + var identifierStore = _identifierStoreFactory.Create(context.ResourceTenant); + var identifier = await identifierStore.GetByIdAsync(request.IdentifierId, innerCt); if (identifier is null) throw new UAuthIdentifierNotFoundException("identifier_not_found"); var expectedVersion = identifier.Version; identifier.MarkVerified(_clock.UtcNow); - await _identifierStore.SaveAsync(identifier, expectedVersion, innerCt); + await identifierStore.SaveAsync(identifier, expectedVersion, innerCt); }); await _accessOrchestrator.ExecuteAsync(context, command, ct); @@ -595,11 +610,12 @@ public async Task DeleteUserIdentifierAsync(AccessContext context, DeleteUserIde { EnsureOverrideAllowed(context); - var identifier = await _identifierStore.GetByIdAsync(request.IdentifierId, innerCt); + var identifierStore = _identifierStoreFactory.Create(context.ResourceTenant); + var identifier = await identifierStore.GetByIdAsync(request.IdentifierId, innerCt); if (identifier is null) throw new UAuthIdentifierNotFoundException("identifier_not_found"); - var identifiers = await _identifierStore.GetByUserAsync(identifier.Tenant, identifier.UserKey, innerCt); + var identifiers = await identifierStore.GetByUserAsync(identifier.UserKey, innerCt); var loginIdentifiers = identifiers.Where(i => !i.IsDeleted && IsLoginIdentifier(i.Type)).ToList(); if (identifier.IsPrimary) @@ -622,12 +638,12 @@ public async Task DeleteUserIdentifierAsync(AccessContext context, DeleteUserIde if (request.Mode == DeleteMode.Hard) { - await _identifierStore.DeleteAsync(identifier.Id, expectedVersion, DeleteMode.Hard, _clock.UtcNow, innerCt); + await identifierStore.DeleteAsync(identifier.Id, expectedVersion, DeleteMode.Hard, _clock.UtcNow, innerCt); } else { identifier.MarkDeleted(_clock.UtcNow); - await _identifierStore.SaveAsync(identifier, expectedVersion, innerCt); + await identifierStore.SaveAsync(identifier, expectedVersion, innerCt); } }); @@ -641,8 +657,11 @@ public async Task DeleteUserIdentifierAsync(AccessContext context, DeleteUserIde private async Task BuildUserViewAsync(TenantKey tenant, UserKey userKey, CancellationToken ct) { - var lifecycle = await _lifecycleStore.GetAsync(new UserLifecycleKey(tenant, userKey)); - var profile = await _profileStore.GetAsync(new UserProfileKey(tenant, userKey), ct); + var lifecycleStore = _lifecycleStoreFactory.Create(tenant); + var identifierStore = _identifierStoreFactory.Create(tenant); + var profileStore = _profileStoreFactory.Create(tenant); + var lifecycle = await lifecycleStore.GetAsync(new UserLifecycleKey(tenant, userKey)); + var profile = await profileStore.GetAsync(new UserProfileKey(tenant, userKey), ct); if (lifecycle is null || lifecycle.IsDeleted) throw new UAuthNotFoundException("user_not_found"); @@ -650,7 +669,7 @@ private async Task BuildUserViewAsync(TenantKey tenant, UserKey userKe if (profile is null || profile.IsDeleted) throw new UAuthNotFoundException("user_profile_not_found"); - var identifiers = await _identifierStore.GetByUserAsync(tenant, userKey, ct); + var identifiers = await identifierStore.GetByUserAsync(userKey, ct); var username = identifiers.FirstOrDefault(x => x.Type == UserIdentifierType.Username && x.IsPrimary); var primaryEmail = identifiers.FirstOrDefault(x => x.Type == UserIdentifierType.Email && x.IsPrimary); @@ -738,7 +757,8 @@ public async Task> QueryUsersAsync(AccessContext contex IncludeDeleted = query.IncludeDeleted }; - var lifecycleResult = await _lifecycleStore.QueryAsync(context.ResourceTenant, lifecycleQuery, innerCt); + var lifecycleStore = _lifecycleStoreFactory.Create(context.ResourceTenant); + var lifecycleResult = await lifecycleStore.QueryAsync(lifecycleQuery, innerCt); var lifecycles = lifecycleResult.Items; if (lifecycles.Count == 0) @@ -753,8 +773,10 @@ public async Task> QueryUsersAsync(AccessContext contex } var userKeys = lifecycles.Select(x => x.UserKey).ToList(); - var profiles = await _profileStore.GetByUsersAsync(context.ResourceTenant, userKeys, innerCt); - var identifiers = await _identifierStore.GetByUsersAsync(context.ResourceTenant, userKeys, innerCt); + var profileStore = _profileStoreFactory.Create(context.ResourceTenant); + var identifierStore = _identifierStoreFactory.Create(context.ResourceTenant); + var profiles = await profileStore.GetByUsersAsync(userKeys, innerCt); + var identifiers = await identifierStore.GetByUsersAsync(userKeys, innerCt); var profileMap = profiles.ToDictionary(x => x.UserKey); var identifierGroups = identifiers.GroupBy(x => x.UserKey).ToDictionary(x => x.Key, x => x.ToList()); diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserIdentifierStore.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserIdentifierStore.cs index 407b03a3..04e004ad 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserIdentifierStore.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserIdentifierStore.cs @@ -1,7 +1,6 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Users.Contracts; namespace CodeBeam.UltimateAuth.Users.Reference; @@ -10,12 +9,12 @@ public interface IUserIdentifierStore : IVersionedStore { Task ExistsAsync(IdentifierExistenceQuery query, CancellationToken ct = default); - Task> GetByUserAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default); + Task> GetByUserAsync(UserKey userKey, CancellationToken ct = default); Task GetByIdAsync(Guid id, CancellationToken ct = default); - Task GetAsync(TenantKey tenant, UserIdentifierType type, string value, CancellationToken ct = default); + Task GetAsync(UserIdentifierType type, string value, CancellationToken ct = default); - Task> QueryAsync(TenantKey tenant, UserIdentifierQuery query, CancellationToken ct = default); + Task> QueryAsync(UserIdentifierQuery query, CancellationToken ct = default); - Task> GetByUsersAsync(TenantKey tenant, IReadOnlyList userKeys, CancellationToken ct = default); - Task DeleteByUserAsync(TenantKey tenant, UserKey userKey, DeleteMode mode, DateTimeOffset deletedAt, CancellationToken ct = default); + Task> GetByUsersAsync(IReadOnlyList userKeys, CancellationToken ct = default); + Task DeleteByUserAsync(UserKey userKey, DeleteMode mode, DateTimeOffset deletedAt, CancellationToken ct = default); } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserIdentifierStoreFactory.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserIdentifierStoreFactory.cs new file mode 100644 index 00000000..e1c0ce04 --- /dev/null +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserIdentifierStoreFactory.cs @@ -0,0 +1,8 @@ +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Users.Reference; + +public interface IUserIdentifierStoreFactory +{ + IUserIdentifierStore Create(TenantKey tenant); +} \ No newline at end of file diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserLifecycleStore.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserLifecycleStore.cs index c930be38..c687f6da 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserLifecycleStore.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserLifecycleStore.cs @@ -1,12 +1,9 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.MultiTenancy; -using CodeBeam.UltimateAuth.Users.Contracts; namespace CodeBeam.UltimateAuth.Users.Reference; public interface IUserLifecycleStore : IVersionedStore { - Task> QueryAsync(TenantKey tenant, UserLifecycleQuery query, CancellationToken ct = default); + Task> QueryAsync(UserLifecycleQuery query, CancellationToken ct = default); } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserLifecycleStoreFactory.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserLifecycleStoreFactory.cs new file mode 100644 index 00000000..adb68dea --- /dev/null +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserLifecycleStoreFactory.cs @@ -0,0 +1,8 @@ +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Users.Reference; + +public interface IUserLifecycleStoreFactory +{ + IUserLifecycleStore Create(TenantKey tenant); +} diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserProfileStore.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserProfileStore.cs index dbedae45..5d63cb60 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserProfileStore.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserProfileStore.cs @@ -1,12 +1,11 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Users.Reference; public interface IUserProfileStore : IVersionedStore { - Task> QueryAsync(TenantKey tenant, UserProfileQuery query, CancellationToken ct = default); - Task> GetByUsersAsync(TenantKey tenant, IReadOnlyList userKeys, CancellationToken ct = default); + Task> QueryAsync(UserProfileQuery query, CancellationToken ct = default); + Task> GetByUsersAsync(IReadOnlyList userKeys, CancellationToken ct = default); } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserProfileStoreFactory.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserProfileStoreFactory.cs new file mode 100644 index 00000000..8ceeb0fe --- /dev/null +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserProfileStoreFactory.cs @@ -0,0 +1,8 @@ +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Users.Reference; + +public interface IUserProfileStoreFactory +{ + IUserProfileStore Create(TenantKey tenant); +} diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/UserRuntimeStateProvider.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/UserRuntimeStateProvider.cs index 4a4e16fb..f7b218e8 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/UserRuntimeStateProvider.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/UserRuntimeStateProvider.cs @@ -7,17 +7,18 @@ namespace CodeBeam.UltimateAuth.Users.Reference; internal sealed class UserRuntimeStateProvider : IUserRuntimeStateProvider { - private readonly IUserLifecycleStore _lifecycleStore; + private readonly IUserLifecycleStoreFactory _lifecycleStoreFactory; - public UserRuntimeStateProvider(IUserLifecycleStore lifecycleStore) + public UserRuntimeStateProvider(IUserLifecycleStoreFactory lifecycleStoreFactory) { - _lifecycleStore = lifecycleStore; + _lifecycleStoreFactory = lifecycleStoreFactory; } public async Task GetAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default) { var userLifecycleKey = new UserLifecycleKey(tenant, userKey); - var lifecycle = await _lifecycleStore.GetAsync(userLifecycleKey, ct); + var lifecycleStore = _lifecycleStoreFactory.Create(tenant); + var lifecycle = await lifecycleStore.GetAsync(userLifecycleKey, ct); if (lifecycle is null) return null; diff --git a/src/users/CodeBeam.UltimateAuth.Users/Infrastructure/ICustomLoginIdentifierResolver.cs b/src/users/CodeBeam.UltimateAuth.Users/Abstractions/ICustomLoginIdentifierResolver.cs similarity index 100% rename from src/users/CodeBeam.UltimateAuth.Users/Infrastructure/ICustomLoginIdentifierResolver.cs rename to src/users/CodeBeam.UltimateAuth.Users/Abstractions/ICustomLoginIdentifierResolver.cs diff --git a/src/users/CodeBeam.UltimateAuth.Users/Infrastructure/ILoginIdentifierResolver.cs b/src/users/CodeBeam.UltimateAuth.Users/Abstractions/ILoginIdentifierResolver.cs similarity index 100% rename from src/users/CodeBeam.UltimateAuth.Users/Infrastructure/ILoginIdentifierResolver.cs rename to src/users/CodeBeam.UltimateAuth.Users/Abstractions/ILoginIdentifierResolver.cs diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/IUserCreateValidator.cs b/src/users/CodeBeam.UltimateAuth.Users/Abstractions/IUserCreateValidator.cs similarity index 83% rename from src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/IUserCreateValidator.cs rename to src/users/CodeBeam.UltimateAuth.Users/Abstractions/IUserCreateValidator.cs index 8157aeee..598cfafc 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/IUserCreateValidator.cs +++ b/src/users/CodeBeam.UltimateAuth.Users/Abstractions/IUserCreateValidator.cs @@ -1,7 +1,7 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Users.Contracts; -namespace CodeBeam.UltimateAuth.Server.Infrastructure; +namespace CodeBeam.UltimateAuth.Users; public interface IUserCreateValidator { diff --git a/src/users/CodeBeam.UltimateAuth.Users/CodeBeam.UltimateAuth.Users.csproj b/src/users/CodeBeam.UltimateAuth.Users/CodeBeam.UltimateAuth.Users.csproj index aacabccb..e6096f81 100644 --- a/src/users/CodeBeam.UltimateAuth.Users/CodeBeam.UltimateAuth.Users.csproj +++ b/src/users/CodeBeam.UltimateAuth.Users/CodeBeam.UltimateAuth.Users.csproj @@ -4,13 +4,44 @@ net8.0;net9.0;net10.0 enable enable - true $(NoWarn);1591 + + CodeBeam.UltimateAuth.Users + 0.1.0-preview.1 + + CodeBeam + CodeBeam + + + Users module for UltimateAuth. + Provides orchestration, abstractions and dependency injection wiring for user management functionality. + Use with a persistence provider such as EntityFrameworkCore or InMemory. + This package is included transitively by CodeBeam.UltimateAuth.Server and usually does not need to be installed directly. + + + authentication;identity;users;module;auth-framework + + README.md + https://github.com/CodeBeamOrg/UltimateAuth + https://github.com/CodeBeamOrg/UltimateAuth + Apache-2.0 + + true + true + snupkg + + true + + + + + + + - - + -
+
\ No newline at end of file diff --git a/src/users/CodeBeam.UltimateAuth.Users/README.md b/src/users/CodeBeam.UltimateAuth.Users/README.md new file mode 100644 index 00000000..8c0cf3db --- /dev/null +++ b/src/users/CodeBeam.UltimateAuth.Users/README.md @@ -0,0 +1,22 @@ +# UltimateAuth Users + +User management module for UltimateAuth. + +## Purpose + +This package provides: + +- Dependency injection setup +- User module orchestration +- Integration points for persistence providers + +## Does NOT include + +- Persistence (use EntityFrameworkCore or InMemory packages) +- Domain implementation (use Reference package if needed) + +⚠️ This package is typically installed transitively via: + +- CodeBeam.UltimateAuth.Server + +In most cases, you do not need to install it directly unless you are building custom integrations or extensions. \ No newline at end of file diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/AssemblyVisibility.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/AssemblyVisibility.cs new file mode 100644 index 00000000..f9e4007a --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/AssemblyVisibility.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/SessionCoordinatorTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/SessionCoordinatorTests.cs index d87c40f2..c3338855 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/SessionCoordinatorTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/SessionCoordinatorTests.cs @@ -1,7 +1,7 @@ using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Client.Blazor.Infrastructure; using CodeBeam.UltimateAuth.Client.Contracts; using CodeBeam.UltimateAuth.Client.Diagnostics; -using CodeBeam.UltimateAuth.Client.Infrastructure; using CodeBeam.UltimateAuth.Client.Options; using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Tests.Unit.Helpers; diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/CodeBeam.UltimateAuth.Tests.Unit.csproj b/tests/CodeBeam.UltimateAuth.Tests.Unit/CodeBeam.UltimateAuth.Tests.Unit.csproj index 6b7f2055..de0ad788 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/CodeBeam.UltimateAuth.Tests.Unit.csproj +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/CodeBeam.UltimateAuth.Tests.Unit.csproj @@ -10,6 +10,7 @@ + @@ -18,13 +19,16 @@ + + + - + diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreAuthenticationStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreAuthenticationStoreTests.cs new file mode 100644 index 00000000..9144d984 --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreAuthenticationStoreTests.cs @@ -0,0 +1,214 @@ +using CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Errors; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Core.Security; +using CodeBeam.UltimateAuth.Tests.Unit.Helpers; +using Microsoft.Data.Sqlite; + +namespace CodeBeam.UltimateAuth.Tests.Unit; + +public class EfCoreAuthenticationStoreTests : EfCoreTestBase +{ + private static UAuthAuthenticationDbContext CreateDb(SqliteConnection connection) + { + return CreateDbContext(connection, options => new UAuthAuthenticationDbContext(options)); + } + + [Fact] + public async Task Add_And_Get_Should_Work() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant = TenantKeys.Single; + var store = new EfCoreAuthenticationSecurityStateStore(db, new TenantContext(tenant)); + + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var state = AuthenticationSecurityState.CreateAccount( + tenant, + userKey); + + await store.AddAsync(state); + + var result = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null); + + Assert.NotNull(result); + Assert.Equal(state.Id, result!.Id); + } + + [Fact] + public async Task Update_With_RegisterFailure_Should_Increment_Version() + { + using var connection = CreateOpenConnection(); + + var tenant = TenantKeys.Single; + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var state = AuthenticationSecurityState.CreateAccount(tenant, userKey); + + await using (var db1 = CreateDb(connection)) + { + var store = new EfCoreAuthenticationSecurityStateStore(db1, new TenantContext(tenant)); + await store.AddAsync(state); + } + + await using (var db2 = CreateDb(connection)) + { + var store = new EfCoreAuthenticationSecurityStateStore(db2, new TenantContext(tenant)); + var existing = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null); + + var updated = existing!.RegisterFailure( + DateTimeOffset.UtcNow, + threshold: 3, + lockoutDuration: TimeSpan.FromMinutes(5)); + + await store.UpdateAsync(updated, expectedVersion: 0); + } + + await using (var db3 = CreateDb(connection)) + { + var store = new EfCoreAuthenticationSecurityStateStore(db3, new TenantContext(tenant)); + var result = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null); + + Assert.Equal(1, result!.SecurityVersion); + Assert.Equal(1, result.FailedAttempts); + } + } + + [Fact] + public async Task Update_With_Wrong_Version_Should_Throw() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant = TenantKeys.Single; + var store = new EfCoreAuthenticationSecurityStateStore(db, new TenantContext(tenant)); + + var userKey = UserKey.FromGuid(Guid.NewGuid()); + var state = AuthenticationSecurityState.CreateAccount(tenant, userKey); + await store.AddAsync(state); + var updated = state.RegisterFailure(DateTimeOffset.UtcNow, 3, TimeSpan.FromMinutes(5)); + + await Assert.ThrowsAsync(() => store.UpdateAsync(updated, expectedVersion: 999)); + } + + [Fact] + public async Task RegisterSuccess_Should_Clear_Failures() + { + using var connection = CreateOpenConnection(); + + var tenant = TenantKeys.Single; + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var state = AuthenticationSecurityState.CreateAccount(tenant, userKey) + .RegisterFailure(DateTimeOffset.UtcNow, 3, TimeSpan.FromMinutes(5)); + + await using (var db1 = CreateDb(connection)) + { + var store = new EfCoreAuthenticationSecurityStateStore(db1, new TenantContext(tenant)); + await store.AddAsync(state); + } + + await using (var db2 = CreateDb(connection)) + { + var store = new EfCoreAuthenticationSecurityStateStore(db2, new TenantContext(tenant)); + var existing = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null); + var updated = existing!.RegisterSuccess(); + await store.UpdateAsync(updated, expectedVersion: 1); + } + + await using (var db3 = CreateDb(connection)) + { + var store = new EfCoreAuthenticationSecurityStateStore(db3, new TenantContext(tenant)); + var result = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null); + + Assert.Equal(0, result!.FailedAttempts); + Assert.Null(result.LockedUntil); + } + } + + [Fact] + public async Task BeginReset_And_Consume_Should_Work() + { + using var connection = CreateOpenConnection(); + + var tenant = TenantKeys.Single; + var userKey = UserKey.FromGuid(Guid.NewGuid()); + var state = AuthenticationSecurityState.CreateAccount(tenant, userKey); + + await using (var db1 = CreateDb(connection)) + { + var store = new EfCoreAuthenticationSecurityStateStore(db1, new TenantContext(tenant)); + await store.AddAsync(state); + } + + DateTimeOffset now = DateTimeOffset.UtcNow; + + await using (var db2 = CreateDb(connection)) + { + var store = new EfCoreAuthenticationSecurityStateStore(db2, new TenantContext(tenant)); + var existing = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null); + var updated = existing!.BeginReset("hash", now, TimeSpan.FromMinutes(10)); + await store.UpdateAsync(updated, expectedVersion: 0); + } + + await using (var db3 = CreateDb(connection)) + { + var store = new EfCoreAuthenticationSecurityStateStore(db3, new TenantContext(tenant)); + var existing = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null); + var consumed = existing!.ConsumeReset(DateTimeOffset.UtcNow); + await store.UpdateAsync(consumed, expectedVersion: 1); + } + + await using (var db4 = CreateDb(connection)) + { + var store = new EfCoreAuthenticationSecurityStateStore(db4, new TenantContext(tenant)); + var result = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null); + Assert.NotNull(result!.ResetConsumedAt); + } + } + + [Fact] + public async Task Delete_Should_Work() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant = TenantKeys.Single; + var store = new EfCoreAuthenticationSecurityStateStore(db, new TenantContext(tenant)); + + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var state = AuthenticationSecurityState.CreateAccount(tenant, userKey); + + await store.AddAsync(state); + + await store.DeleteAsync(userKey, AuthenticationSecurityScope.Account, null); + + var result = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null); + + Assert.Null(result); + } + + [Fact] + public async Task Should_Not_See_Data_From_Other_Tenant() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant1 = TenantKeys.Single; + var tenant2 = TenantKey.FromInternal("tenant-2"); + + var store1 = new EfCoreAuthenticationSecurityStateStore(db, new TenantContext(tenant1)); + var store2 = new EfCoreAuthenticationSecurityStateStore(db, new TenantContext(tenant2)); + + var userKey = UserKey.FromGuid(Guid.NewGuid()); + var state = AuthenticationSecurityState.CreateAccount(tenant1, userKey); + await store1.AddAsync(state); + var result = await store2.GetAsync(userKey, AuthenticationSecurityScope.Account, null); + + Assert.Null(result); + } +} diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreCredentialStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreCredentialStoreTests.cs new file mode 100644 index 00000000..36f0a519 --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreCredentialStoreTests.cs @@ -0,0 +1,294 @@ +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Errors; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Credentials.Contracts; +using CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore; +using CodeBeam.UltimateAuth.Credentials.Reference; +using CodeBeam.UltimateAuth.Tests.Unit.Helpers; +using Microsoft.Data.Sqlite; + +namespace CodeBeam.UltimateAuth.Tests.Unit; + +public class EfCoreCredentialStoreTests : EfCoreTestBase +{ + private static UAuthCredentialDbContext CreateDb(SqliteConnection connection) + { + return CreateDbContext(connection, options => new UAuthCredentialDbContext(options)); + } + + [Fact] + public async Task Add_And_Get_Should_Work() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant = TenantKeys.Single; + var store = new EfCorePasswordCredentialStore(db, new TenantContext(tenant)); + + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var credential = PasswordCredential.Create( + Guid.NewGuid(), + tenant, + userKey, + "hash", + CredentialSecurityState.Active(), + new CredentialMetadata(), + DateTimeOffset.UtcNow); + + await store.AddAsync(credential); + + var result = await store.GetAsync(new CredentialKey(tenant, credential.Id)); + + Assert.NotNull(result); + Assert.Equal("hash", result!.SecretHash); + } + + [Fact] + public async Task Exists_Should_Return_True_When_Exists() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant = TenantKeys.Single; + var store = new EfCorePasswordCredentialStore(db, new TenantContext(tenant)); + + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var credential = PasswordCredential.Create( + Guid.NewGuid(), + tenant, + userKey, + "hash", + CredentialSecurityState.Active(), + new CredentialMetadata(), + DateTimeOffset.UtcNow); + + await store.AddAsync(credential); + var exists = await store.ExistsAsync(new CredentialKey(tenant, credential.Id)); + + Assert.True(exists); + } + + [Fact] + public async Task Save_Should_Increment_Version() + { + using var connection = CreateOpenConnection(); + + var tenant = TenantKeys.Single; + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var credential = PasswordCredential.Create( + Guid.NewGuid(), + tenant, + userKey, + "hash", + CredentialSecurityState.Active(), + new CredentialMetadata(), + DateTimeOffset.UtcNow); + + await using (var db1 = CreateDb(connection)) + { + var store1 = new EfCorePasswordCredentialStore(db1, new TenantContext(tenant)); + await store1.AddAsync(credential); + } + + await using (var db2 = CreateDb(connection)) + { + var store2 = new EfCorePasswordCredentialStore(db2, new TenantContext(tenant)); + var existing = await store2.GetAsync(new CredentialKey(tenant, credential.Id)); + var updated = existing!.ChangeSecret("new_hash", DateTimeOffset.UtcNow); + await store2.SaveAsync(updated, expectedVersion: 0); + } + + await using (var db3 = CreateDb(connection)) + { + var store3 = new EfCorePasswordCredentialStore(db3, new TenantContext(tenant)); + var result = await store3.GetAsync(new CredentialKey(tenant, credential.Id)); + + Assert.Equal(1, result!.Version); + Assert.Equal("new_hash", result.SecretHash); + } + } + + [Fact] + public async Task Save_With_Wrong_Version_Should_Throw() + { + using var connection = CreateOpenConnection(); + + var tenant = TenantKeys.Single; + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var credential = PasswordCredential.Create( + Guid.NewGuid(), + tenant, + userKey, + "hash", + CredentialSecurityState.Active(), + new CredentialMetadata(), + DateTimeOffset.UtcNow); + + await using (var db1 = CreateDb(connection)) + { + var store1 = new EfCorePasswordCredentialStore(db1, new TenantContext(tenant)); + await store1.AddAsync(credential); + } + + await using (var db2 = CreateDb(connection)) + { + var store2 = new EfCorePasswordCredentialStore(db2, new TenantContext(tenant)); + + var existing = await store2.GetAsync(new CredentialKey(tenant, credential.Id)); + var updated = existing!.ChangeSecret("new_hash", DateTimeOffset.UtcNow); + + await Assert.ThrowsAsync(() => + store2.SaveAsync(updated, expectedVersion: 999)); + } + } + + [Fact] + public async Task Should_Not_See_Data_From_Other_Tenant() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant1 = TenantKeys.Single; + var tenant2 = TenantKey.FromInternal("tenant-2"); + + var store1 = new EfCorePasswordCredentialStore(db, new TenantContext(tenant1)); + var store2 = new EfCorePasswordCredentialStore(db, new TenantContext(tenant2)); + + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var credential = PasswordCredential.Create( + Guid.NewGuid(), + tenant1, + userKey, + "hash", + CredentialSecurityState.Active(), + new CredentialMetadata(), + DateTimeOffset.UtcNow); + + await store1.AddAsync(credential); + var result = await store2.GetAsync(new CredentialKey(tenant2, credential.Id)); + + Assert.Null(result); + } + + [Fact] + public async Task Soft_Delete_Should_Work() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant = TenantKeys.Single; + var store = new EfCorePasswordCredentialStore(db, new TenantContext(tenant)); + + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var credential = PasswordCredential.Create( + Guid.NewGuid(), + tenant, + userKey, + "hash", + CredentialSecurityState.Active(), + new CredentialMetadata(), + DateTimeOffset.UtcNow); + + await store.AddAsync(credential); + + await store.DeleteAsync( + new CredentialKey(tenant, credential.Id), + expectedVersion: 0, + DeleteMode.Soft, + DateTimeOffset.UtcNow); + + var result = await store.GetAsync(new CredentialKey(tenant, credential.Id)); + + Assert.NotNull(result); + Assert.NotNull(result!.DeletedAt); + } + + [Fact] + public async Task Revoke_Should_Persist() + { + using var connection = CreateOpenConnection(); + + var tenant = TenantKeys.Single; + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var credential = PasswordCredential.Create( + Guid.NewGuid(), + tenant, + userKey, + "hash", + CredentialSecurityState.Active(), + new CredentialMetadata(), + DateTimeOffset.UtcNow); + + await using (var db1 = CreateDb(connection)) + { + var store1 = new EfCorePasswordCredentialStore(db1, new TenantContext(tenant)); + await store1.AddAsync(credential); + } + + await using (var db2 = CreateDb(connection)) + { + var store2 = new EfCorePasswordCredentialStore(db2, new TenantContext(tenant)); + var existing = await store2.GetAsync(new CredentialKey(tenant, credential.Id)); + var revoked = existing!.Revoke(DateTimeOffset.UtcNow); + await store2.SaveAsync(revoked, expectedVersion: 0); + } + + await using (var db3 = CreateDb(connection)) + { + var store3 = new EfCorePasswordCredentialStore(db3, new TenantContext(tenant)); + var result = await store3.GetAsync(new CredentialKey(tenant, credential.Id)); + + Assert.True(result!.IsRevoked); + } + } + + [Fact] + public async Task ChangeSecret_Should_Update_SecurityState() + { + using var connection = CreateOpenConnection(); + + var tenant = TenantKeys.Single; + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var credential = PasswordCredential.Create( + Guid.NewGuid(), + tenant, + userKey, + "hash", + CredentialSecurityState.Active(), + new CredentialMetadata(), + DateTimeOffset.UtcNow); + + await using (var db1 = CreateDb(connection)) + { + var store1 = new EfCorePasswordCredentialStore(db1, new TenantContext(tenant)); + await store1.AddAsync(credential); + } + + await using (var db2 = CreateDb(connection)) + { + var store2 = new EfCorePasswordCredentialStore(db2, new TenantContext(tenant)); + var existing = await store2.GetAsync(new CredentialKey(tenant, credential.Id)); + var updated = existing!.ChangeSecret("new_hash", DateTimeOffset.UtcNow); + + await store2.SaveAsync(updated, expectedVersion: 0); + } + + await using (var db3 = CreateDb(connection)) + { + var store3 = new EfCorePasswordCredentialStore(db3, new TenantContext(tenant)); + var result = await store3.GetAsync(new CredentialKey(tenant, credential.Id)); + + Assert.Equal("new_hash", result!.SecretHash); + Assert.NotNull(result.UpdatedAt); + } + } +} diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreRoleStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreRoleStoreTests.cs new file mode 100644 index 00000000..0332e7f6 --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreRoleStoreTests.cs @@ -0,0 +1,253 @@ +using CodeBeam.UltimateAuth.Authorization; +using CodeBeam.UltimateAuth.Authorization.Contracts; +using CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Defaults; +using CodeBeam.UltimateAuth.Core.Errors; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Tests.Unit.Helpers; +using Microsoft.Data.Sqlite; + +namespace CodeBeam.UltimateAuth.Tests.Unit; + +public class EfCoreRoleStoreTests : EfCoreTestBase +{ + private static UAuthAuthorizationDbContext CreateDb(SqliteConnection connection) + { + return CreateDbContext(connection, options => new UAuthAuthorizationDbContext(options)); + } + + [Fact] + public async Task Add_And_Get_Should_Work() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant = TenantKeys.Single; + var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + + var role = Role.Create( + null, + tenant, + "admin", + new[] { Permission.From("read"), Permission.From("write") }, + DateTimeOffset.UtcNow); + + await store.AddAsync(role); + + var result = await store.GetAsync(new RoleKey(tenant, role.Id)); + + Assert.NotNull(result); + Assert.Equal("admin", result!.Name); + Assert.Equal(2, result.Permissions.Count); + } + + [Fact] + public async Task Add_With_Duplicate_Name_Should_Throw() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant = TenantKeys.Single; + var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + + var role1 = Role.Create(null, tenant, "admin", null, DateTimeOffset.UtcNow); + var role2 = Role.Create(null, tenant, "ADMIN", null, DateTimeOffset.UtcNow); + + await store.AddAsync(role1); + + await Assert.ThrowsAsync(() => store.AddAsync(role2)); + } + + [Fact] + public async Task Save_Should_Increment_Version() + { + using var connection = CreateOpenConnection(); + + var tenant = TenantKeys.Single; + RoleId roleId; + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var role = Role.Create(null, tenant, "admin", null, DateTimeOffset.UtcNow); + roleId = role.Id; + await store.AddAsync(role); + } + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var existing = await store.GetAsync(new RoleKey(tenant, roleId)); + var updated = existing!.Rename("admin2", DateTimeOffset.UtcNow); + await store.SaveAsync(updated, expectedVersion: 0); + } + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var result = await store.GetAsync(new RoleKey(tenant, roleId)); + + Assert.Equal(1, result!.Version); + Assert.Equal("admin2", result.Name); + } + } + + [Fact] + public async Task Save_With_Wrong_Version_Should_Throw() + { + using var connection = CreateOpenConnection(); + + var tenant = TenantKeys.Single; + RoleId roleId; + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var role = Role.Create(null, tenant, "admin", null, DateTimeOffset.UtcNow); + roleId = role.Id; + await store.AddAsync(role); + } + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var existing = await store.GetAsync(new RoleKey(tenant, roleId)); + var updated = existing!.Rename("admin2", DateTimeOffset.UtcNow); + + await Assert.ThrowsAsync(() => store.SaveAsync(updated, expectedVersion: 999)); + } + } + + [Fact] + public async Task Rename_To_Existing_Name_Should_Throw() + { + using var connection = CreateOpenConnection(); + + var tenant = TenantKeys.Single; + + RoleId role1Id; + RoleId role2Id; + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var role1 = Role.Create(null, tenant, "admin", null, DateTimeOffset.UtcNow); + var role2 = Role.Create(null, tenant, "user", null, DateTimeOffset.UtcNow); + role1Id = role1.Id; + role2Id = role2.Id; + await store.AddAsync(role1); + await store.AddAsync(role2); + } + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var role = await store.GetAsync(new RoleKey(tenant, role2Id)); + var updated = role!.Rename("admin", DateTimeOffset.UtcNow); + + await Assert.ThrowsAsync(() => store.SaveAsync(updated, 0)); + } + } + + [Fact] + public async Task Save_Should_Replace_Permissions() + { + using var connection = CreateOpenConnection(); + + var tenant = TenantKeys.Single; + RoleId roleId; + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + + var role = Role.Create( + null, + tenant, + "admin", + new[] { Permission.From(UAuthActions.Authorization.Roles.GetAdmin) }, + DateTimeOffset.UtcNow); + + roleId = role.Id; + + await store.AddAsync(role); + } + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var existing = await store.GetAsync(new RoleKey(tenant, roleId)); + var updated = existing!.SetPermissions( + new[] + { + Permission.From(UAuthActions.Authorization.Roles.SetPermissionsAdmin) + }, + DateTimeOffset.UtcNow); + await store.SaveAsync(updated, 0); + } + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var result = await store.GetAsync(new RoleKey(tenant, roleId)); + + Assert.Single(result!.Permissions); + Assert.Contains(result.Permissions, p => p.Value == UAuthActions.Authorization.Roles.SetPermissionsAdmin); + } + } + + [Fact] + public async Task Soft_Delete_Should_Work() + { + using var connection = CreateOpenConnection(); + + var tenant = TenantKeys.Single; + RoleId roleId; + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var role = Role.Create(null, tenant, "admin", null, DateTimeOffset.UtcNow); + roleId = role.Id; + await store.AddAsync(role); + } + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + await store.DeleteAsync(new RoleKey(tenant, roleId), 0, DeleteMode.Soft, DateTimeOffset.UtcNow); + } + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var result = await store.GetAsync(new RoleKey(tenant, roleId)); + Assert.NotNull(result!.DeletedAt); + } + } + + [Fact] + public async Task Query_Should_Filter_And_Page() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant = TenantKeys.Single; + var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + + await store.AddAsync(Role.Create(null, tenant, "admin", null, DateTimeOffset.UtcNow)); + await store.AddAsync(Role.Create(null, tenant, "user", null, DateTimeOffset.UtcNow)); + await store.AddAsync(Role.Create(null, tenant, "guest", null, DateTimeOffset.UtcNow)); + + var result = await store.QueryAsync(new RoleQuery + { + Search = "us", + PageNumber = 1, + PageSize = 10 + }); + + Assert.Single(result.Items); + Assert.Equal("user", result.Items.First().Name); + } +} diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreSessionStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreSessionStoreTests.cs new file mode 100644 index 00000000..3970a4ed --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreSessionStoreTests.cs @@ -0,0 +1,876 @@ +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Errors; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; +using CodeBeam.UltimateAuth.Tests.Unit.Helpers; +using Microsoft.Data.Sqlite; +using System.Security.Claims; + +namespace CodeBeam.UltimateAuth.Tests.Unit; + +public class EfCoreSessionStoreTests : EfCoreTestBase +{ + private const string ValidRaw = "session-aaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + + private static UAuthSessionDbContext CreateDb(SqliteConnection connection) + { + return CreateDbContext(connection, options => new UAuthSessionDbContext(options)); + } + + [Fact] + public async Task Create_And_Get_Session_Should_Work() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant = TenantKeys.Single; + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var root = UAuthSessionRoot.Create( + tenant, + userKey, + DateTimeOffset.UtcNow); + + var chain = UAuthSessionChain.Create( + SessionChainId.New(), + root.RootId, + tenant, + userKey, + DateTimeOffset.UtcNow, + null, + TestDevice.Default(), + ClaimsSnapshot.Empty, + 0); + + AuthSessionId.TryCreate(ValidRaw, out var sessionId); + + var session = UAuthSession.Create( + sessionId, + tenant, + userKey, + chain.ChainId, + DateTimeOffset.UtcNow, + DateTimeOffset.UtcNow.AddHours(1), + 0, + TestDevice.Default(), + ClaimsSnapshot.Empty, + SessionMetadata.Empty); + + await store.ExecuteAsync(async ct => + { + await store.CreateRootAsync(root, ct); + await store.CreateChainAsync(chain, ct); + await store.CreateSessionAsync(session, ct); + }); + + var result = await store.GetSessionAsync(session.SessionId); + + Assert.NotNull(result); + Assert.Equal(session.SessionId, result!.SessionId); + } + + [Fact] + public async Task Session_Should_Persist_DeviceContext() + { + using var connection = CreateOpenConnection(); + var tenant = TenantKeys.Single; + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var device = DeviceContext.Create( + DeviceId.Create("1234567890123456"), + deviceType: "mobile", + platform: "ios", + operatingSystem: "ios", + browser: "safari", + ipAddress: "127.0.0.1"); + + AuthSessionId.TryCreate(ValidRaw, out var sessionId); + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + + var root = UAuthSessionRoot.Create(tenant, userKey, DateTimeOffset.UtcNow); + + var chain = UAuthSessionChain.Create( + SessionChainId.New(), + root.RootId, + tenant, + userKey, + DateTimeOffset.UtcNow, + null, + device, + ClaimsSnapshot.Empty, + 0); + + var session = UAuthSession.Create( + sessionId, + tenant, + userKey, + chain.ChainId, + DateTimeOffset.UtcNow, + DateTimeOffset.UtcNow.AddHours(1), + 0, + device, + ClaimsSnapshot.Empty, + SessionMetadata.Empty); + + await store.ExecuteAsync(async ct => + { + await store.CreateRootAsync(root, ct); + await store.CreateChainAsync(chain, ct); + await store.CreateSessionAsync(session, ct); + }); + } + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + + var result = await store.GetSessionAsync(sessionId); + + Assert.NotNull(result); + Assert.NotNull(result!.Device.DeviceId); + Assert.Equal("mobile", result.Device.DeviceType); + } + } + + [Fact] + public async Task Session_Should_Persist_Claims_And_Metadata() + { + using var connection = CreateOpenConnection(); + + var tenant = TenantKeys.Single; + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + AuthSessionId.TryCreate(ValidRaw, out var sessionId); + + var claims = ClaimsSnapshot.From( + (ClaimTypes.Role, "admin"), + (ClaimTypes.Role, "user"), + ("uauth:permission", "read"), + ("uauth:permission", "write")); + + var metadata = new SessionMetadata + { + AppVersion = "1.0.0", + Locale = "en-US", + CsrfToken = "csrf-token-123", + Custom = new Dictionary + { + ["theme"] = "dark", + ["feature_flag"] = true + } + }; + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + + var root = UAuthSessionRoot.Create(tenant, userKey, DateTimeOffset.UtcNow); + + var chain = UAuthSessionChain.Create( + SessionChainId.New(), + root.RootId, + tenant, + userKey, + DateTimeOffset.UtcNow, + null, + TestDevice.Default(), + claims, + 0); + + var session = UAuthSession.Create( + sessionId, + tenant, + userKey, + chain.ChainId, + DateTimeOffset.UtcNow, + DateTimeOffset.UtcNow.AddHours(1), + 0, + TestDevice.Default(), + claims, + metadata); + + await store.ExecuteAsync(async ct => + { + await store.CreateRootAsync(root, ct); + await store.CreateChainAsync(chain, ct); + await store.CreateSessionAsync(session, ct); + }); + } + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + + var result = await store.GetSessionAsync(sessionId); + + Assert.NotNull(result); + + Assert.True(result!.Claims.IsInRole("admin")); + Assert.True(result.Claims.HasPermission("read")); + Assert.Equal(2, result.Claims.Roles.Count); + + Assert.Equal("1.0.0", result.Metadata.AppVersion); + Assert.Equal("en-US", result.Metadata.Locale); + Assert.Equal("csrf-token-123", result.Metadata.CsrfToken); + + Assert.NotNull(result.Metadata.Custom); + Assert.Equal("dark", result.Metadata.Custom!["theme"].ToString()); + } + } + + [Fact] + public async Task Revoke_Session_Should_Work() + { + using var connection = CreateOpenConnection(); + var tenant = TenantKeys.Single; + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + AuthSessionId.TryCreate(ValidRaw, out var sessionId); + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + + var root = UAuthSessionRoot.Create(tenant, userKey, DateTimeOffset.UtcNow); + + var chain = UAuthSessionChain.Create( + SessionChainId.New(), + root.RootId, + tenant, + userKey, + DateTimeOffset.UtcNow, + null, + TestDevice.Default(), + ClaimsSnapshot.Empty, + 0); + + var session = UAuthSession.Create( + sessionId, + tenant, + userKey, + chain.ChainId, + DateTimeOffset.UtcNow, + DateTimeOffset.UtcNow.AddHours(1), + 0, + TestDevice.Default(), + ClaimsSnapshot.Empty, + SessionMetadata.Empty); + + await store.ExecuteAsync(async ct => + { + await store.CreateRootAsync(root, ct); + await store.CreateChainAsync(chain, ct); + await store.CreateSessionAsync(session, ct); + }); + + var revoked = await store.RevokeSessionAsync(sessionId, DateTimeOffset.UtcNow); + + Assert.True(revoked); + } + } + + [Fact] + public async Task Should_Not_See_Session_From_Other_Tenant() + { + using var connection = CreateOpenConnection(); + + var tenant1 = TenantKeys.Single; + var tenant2 = TenantKey.FromInternal("tenant-2"); + + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + AuthSessionId.TryCreate(ValidRaw, out var sessionId); + + await using (var db = CreateDb(connection)) + { + var store1 = new EfCoreSessionStore(db, new TenantContext(tenant1)); + + var root = UAuthSessionRoot.Create(tenant1, userKey, DateTimeOffset.UtcNow); + + var chain = UAuthSessionChain.Create( + SessionChainId.New(), + root.RootId, + tenant1, + userKey, + DateTimeOffset.UtcNow, + null, + TestDevice.Default(), + ClaimsSnapshot.Empty, + 0); + + var session = UAuthSession.Create( + sessionId, + tenant1, + userKey, + chain.ChainId, + DateTimeOffset.UtcNow, + DateTimeOffset.UtcNow.AddHours(1), + 0, + TestDevice.Default(), + ClaimsSnapshot.Empty, + SessionMetadata.Empty); + + await store1.ExecuteAsync(async ct => + { + await store1.CreateRootAsync(root, ct); + await store1.CreateChainAsync(chain, ct); + await store1.CreateSessionAsync(session, ct); + }); + } + + await using (var db = CreateDb(connection)) + { + var store2 = new EfCoreSessionStore(db, new TenantContext(tenant2)); + + var result = await store2.GetSessionAsync(sessionId); + + Assert.Null(result); + } + } + + [Fact] + public async Task ExecuteAsync_Should_Rollback_On_Error() + { + using var connection = CreateOpenConnection(); + + var tenant = TenantKeys.Single; + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + + await Assert.ThrowsAsync(async () => + { + await store.ExecuteAsync(async ct => + { + throw new InvalidOperationException("boom"); + }); + }); + } + } + + [Fact] + public async Task GetSessionsByChain_Should_Return_Sessions() + { + using var connection = CreateOpenConnection(); + + var tenant = TenantKeys.Single; + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + AuthSessionId.TryCreate(ValidRaw, out var sessionId); + SessionChainId chainId; + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var root = UAuthSessionRoot.Create(tenant, userKey, DateTimeOffset.UtcNow); + chainId = SessionChainId.New(); + + var chain = UAuthSessionChain.Create( + chainId, + root.RootId, + tenant, + userKey, + DateTimeOffset.UtcNow, + null, + TestDevice.Default(), + ClaimsSnapshot.Empty, + 0); + + var session = UAuthSession.Create( + sessionId, + tenant, + userKey, + chainId, + DateTimeOffset.UtcNow, + DateTimeOffset.UtcNow.AddHours(1), + 0, + TestDevice.Default(), + ClaimsSnapshot.Empty, + SessionMetadata.Empty); + + await store.ExecuteAsync(async ct => + { + await store.CreateRootAsync(root, ct); + await store.CreateChainAsync(chain, ct); + await store.CreateSessionAsync(session, ct); + }); + } + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var sessions = await store.GetSessionsByChainAsync(chainId); + Assert.Single(sessions); + } + } + + [Fact] + public async Task ExecuteAsync_Should_Commit_Multiple_Operations() + { + using var connection = CreateOpenConnection(); + + var tenant = TenantKeys.Single; + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + AuthSessionId.TryCreate(ValidRaw, out var sessionId); + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + + await store.ExecuteAsync(async ct => + { + var root = UAuthSessionRoot.Create(tenant, userKey, DateTimeOffset.UtcNow); + + var chain = UAuthSessionChain.Create( + SessionChainId.New(), + root.RootId, + tenant, + userKey, + DateTimeOffset.UtcNow, + null, + TestDevice.Default(), + ClaimsSnapshot.Empty, + 0); + + var session = UAuthSession.Create( + sessionId, + tenant, + userKey, + chain.ChainId, + DateTimeOffset.UtcNow, + DateTimeOffset.UtcNow.AddHours(1), + 0, + TestDevice.Default(), + ClaimsSnapshot.Empty, + SessionMetadata.Empty); + + await store.CreateRootAsync(root, ct); + await store.CreateChainAsync(chain, ct); + await store.CreateSessionAsync(session, ct); + }); + } + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + + var result = await store.GetSessionAsync(sessionId); + + Assert.NotNull(result); + } + } + + [Fact] + public async Task ExecuteAsync_Should_Rollback_All_On_Failure() + { + using var connection = CreateOpenConnection(); + + var tenant = TenantKeys.Single; + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + AuthSessionId.TryCreate(ValidRaw, out var sessionId); + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + + await Assert.ThrowsAsync(async () => + { + await store.ExecuteAsync(async ct => + { + var root = UAuthSessionRoot.Create(tenant, userKey, DateTimeOffset.UtcNow); + + await store.CreateRootAsync(root, ct); + + // 💥 simulate failure + throw new InvalidOperationException("boom"); + }); + }); + } + + await using (var db = CreateDb(connection)) + { + var count = db.Roots.Count(); + Assert.Equal(0, count); + } + } + + [Fact] + public async Task RevokeChainCascade_Should_Revoke_All_Sessions() + { + using var connection = CreateOpenConnection(); + + var tenant = TenantKeys.Single; + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + AuthSessionId.TryCreate(ValidRaw, out var sessionId); + SessionChainId chainId; + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var root = UAuthSessionRoot.Create(tenant, userKey, DateTimeOffset.UtcNow); + + chainId = SessionChainId.New(); + + var chain = UAuthSessionChain.Create( + chainId, + root.RootId, + tenant, + userKey, + DateTimeOffset.UtcNow, + null, + TestDevice.Default(), + ClaimsSnapshot.Empty, + 0); + + var session = UAuthSession.Create( + sessionId, + tenant, + userKey, + chainId, + DateTimeOffset.UtcNow, + DateTimeOffset.UtcNow.AddHours(1), + 0, + TestDevice.Default(), + ClaimsSnapshot.Empty, + SessionMetadata.Empty); + + await store.ExecuteAsync(async ct => + { + await store.CreateRootAsync(root, ct); + await store.CreateChainAsync(chain, ct); + await store.CreateSessionAsync(session, ct); + }); + } + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + + await store.ExecuteAsync(async ct => + { + await store.RevokeChainCascadeAsync(chainId, DateTimeOffset.UtcNow, ct); + }); + } + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var sessions = await store.GetSessionsByChainAsync(chainId); + + Assert.All(sessions, s => Assert.True(s.IsRevoked)); + } + } + + [Fact] + public async Task SetActiveSession_Should_Work() + { + using var connection = CreateOpenConnection(); + + var tenant = TenantKeys.Single; + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + AuthSessionId.TryCreate(ValidRaw, out var sessionId); + SessionChainId chainId; + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + + var root = UAuthSessionRoot.Create(tenant, userKey, DateTimeOffset.UtcNow); + + chainId = SessionChainId.New(); + + var chain = UAuthSessionChain.Create( + chainId, + root.RootId, + tenant, + userKey, + DateTimeOffset.UtcNow, + null, + TestDevice.Default(), + ClaimsSnapshot.Empty, + 0); + + await store.ExecuteAsync(async ct => + { + await store.CreateRootAsync(root, ct); + await store.CreateChainAsync(chain, ct); + await store.SetActiveSessionIdAsync(chainId, sessionId); + }); + + } + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var active = await store.GetActiveSessionIdAsync(chainId); + + Assert.Equal(sessionId, active); + } + } + + [Fact] + public async Task Query_Should_Not_Use_Domain_Computed_Properties() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var ex = await Record.ExceptionAsync(async () => + { + db.Sessions + .Where(x => x.RevokedAt == null) + .ToList(); + }); + + Assert.Null(ex); + } + + [Fact] + public async Task SaveSession_Should_Increment_Version() + { + using var connection = CreateOpenConnection(); + + var tenant = TenantKeys.Single; + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + AuthSessionId.TryCreate(ValidRaw, out var sessionId); + SessionChainId chainId; + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + + var root = UAuthSessionRoot.Create(tenant, userKey, DateTimeOffset.UtcNow); + chainId = SessionChainId.New(); + + var chain = UAuthSessionChain.Create( + chainId, + root.RootId, + tenant, + userKey, + DateTimeOffset.UtcNow, + null, + TestDevice.Default(), + ClaimsSnapshot.Empty, + 0); + + var session = UAuthSession.Create( + sessionId, + tenant, + userKey, + chainId, + DateTimeOffset.UtcNow, + DateTimeOffset.UtcNow.AddHours(1), + 0, + TestDevice.Default(), + ClaimsSnapshot.Empty, + SessionMetadata.Empty); + + await store.ExecuteAsync(async ct => + { + await store.CreateRootAsync(root, ct); + await store.CreateChainAsync(chain, ct); + await store.CreateSessionAsync(session, ct); + }); + } + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + + await store.ExecuteAsync(async ct => + { + var existing = await store.GetSessionAsync(sessionId, ct); + var updated = existing!.Revoke(DateTimeOffset.UtcNow); + + await store.SaveSessionAsync(updated, expectedVersion: 0, ct); + }); + } + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var result = await store.GetSessionAsync(sessionId); + + Assert.Equal(1, result!.Version); + } + } + + [Fact] + public async Task SaveSession_With_Wrong_Version_Should_Throw() + { + using var connection = CreateOpenConnection(); + + var tenant = TenantKeys.Single; + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + AuthSessionId.TryCreate(ValidRaw, out var sessionId); + SessionChainId chainId; + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + + var root = UAuthSessionRoot.Create(tenant, userKey, DateTimeOffset.UtcNow); + chainId = SessionChainId.New(); + + var chain = UAuthSessionChain.Create( + chainId, + root.RootId, + tenant, + userKey, + DateTimeOffset.UtcNow, + null, + TestDevice.Default(), + ClaimsSnapshot.Empty, + 0); + + var session = UAuthSession.Create( + sessionId, + tenant, + userKey, + chainId, + DateTimeOffset.UtcNow, + DateTimeOffset.UtcNow.AddHours(1), + 0, + TestDevice.Default(), + ClaimsSnapshot.Empty, + SessionMetadata.Empty); + + await store.ExecuteAsync(async ct => + { + await store.CreateRootAsync(root, ct); + await store.CreateChainAsync(chain, ct); + await store.CreateSessionAsync(session, ct); + }); + } + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + + await Assert.ThrowsAsync(async () => + { + await store.ExecuteAsync(async ct => + { + var existing = await store.GetSessionAsync(sessionId, ct); + var updated = existing!.Revoke(DateTimeOffset.UtcNow); + + await store.SaveSessionAsync(updated, expectedVersion: 999, ct); + }); + }); + } + } + + [Fact] + public async Task SaveChain_Should_Increment_Version() + { + using var connection = CreateOpenConnection(); + + var tenant = TenantKeys.Single; + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + SessionChainId chainId; + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + + var root = UAuthSessionRoot.Create(tenant, userKey, DateTimeOffset.UtcNow); + chainId = SessionChainId.New(); + + var chain = UAuthSessionChain.Create( + chainId, + root.RootId, + tenant, + userKey, + DateTimeOffset.UtcNow, + null, + TestDevice.Default(), + ClaimsSnapshot.Empty, + 0); + + await store.ExecuteAsync(async ct => + { + await store.CreateRootAsync(root, ct); + await store.CreateChainAsync(chain, ct); + }); + } + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + + await store.ExecuteAsync(async ct => + { + var existing = await store.GetChainAsync(chainId, ct); + var updated = existing!.Revoke(DateTimeOffset.UtcNow); + + await store.SaveChainAsync(updated, expectedVersion: 0, ct); + }); + } + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var result = await store.GetChainAsync(chainId); + + Assert.Equal(1, result!.Version); + } + } + + [Fact] + public async Task SaveRoot_Should_Increment_Version() + { + using var connection = CreateOpenConnection(); + + var tenant = TenantKeys.Single; + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + + var root = UAuthSessionRoot.Create( + tenant, + userKey, + DateTimeOffset.UtcNow); + + await store.ExecuteAsync(async ct => + { + await store.CreateRootAsync(root, ct); + }); + } + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + + await store.ExecuteAsync(async ct => + { + var existing = await store.GetRootByUserAsync(userKey, ct); + var updated = existing!.Revoke(DateTimeOffset.UtcNow); + + await store.SaveRootAsync(updated, expectedVersion: 0, ct); + }); + } + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var result = await store.GetRootByUserAsync(userKey); + + Assert.Equal(1, result!.Version); + } + } +} diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreTokenStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreTokenStoreTests.cs new file mode 100644 index 00000000..58e608a9 --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreTokenStoreTests.cs @@ -0,0 +1,122 @@ +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Tests.Unit.Helpers; +using CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore; +using Microsoft.Data.Sqlite; + +namespace CodeBeam.UltimateAuth.Tests.Unit; + +public class EfCoreTokenStoreTests : EfCoreTestBase +{ + private const string ValidRaw = "session-aaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + + private static UAuthTokenDbContext CreateDb(SqliteConnection connection) + { + return CreateDbContext(connection, options => new UAuthTokenDbContext(options)); + } + + [Fact] + public async Task Store_And_Find_Should_Work() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant = TenantKeys.Single; + var store = new EfCoreRefreshTokenStore(db, new TenantContext(tenant)); + AuthSessionId.TryCreate(ValidRaw, out var sessionId); + + var token = RefreshToken.Create( + TokenId.From(Guid.NewGuid()), + "hash", + tenant, + UserKey.FromGuid(Guid.NewGuid()), + sessionId, + null, + DateTimeOffset.UtcNow, + DateTimeOffset.UtcNow.AddHours(1) + ); + + await store.ExecuteAsync(async ct => + { + await store.StoreAsync(token, ct); + }); + + var result = await store.FindByHashAsync("hash"); + + Assert.NotNull(result); + Assert.Equal("hash", result!.TokenHash); + } + + [Fact] + public async Task Revoke_Should_Set_RevokedAt() + { + using var connection = CreateOpenConnection(); + + var tenant = TenantKeys.Single; + var tokenHash = "hash"; + AuthSessionId.TryCreate(ValidRaw, out var sessionId); + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreRefreshTokenStore(db, new TenantContext(tenant)); + + var token = RefreshToken.Create( + TokenId.From(Guid.NewGuid()), + "hash", + tenant, + UserKey.FromGuid(Guid.NewGuid()), + sessionId, + null, + DateTimeOffset.UtcNow, + DateTimeOffset.UtcNow.AddHours(1) + ); + + await store.ExecuteAsync(async ct => + { + await store.StoreAsync(token, ct); + }); + } + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreRefreshTokenStore(db, new TenantContext(tenant)); + + await store.ExecuteAsync(async ct => + { + await store.RevokeAsync(tokenHash, DateTimeOffset.UtcNow, null, ct); + }); + } + + await using (var db = CreateDb(connection)) + { + var store = new EfCoreRefreshTokenStore(db, new TenantContext(tenant)); + var result = await store.FindByHashAsync(tokenHash); + + Assert.NotNull(result!.RevokedAt); + } + } + + [Fact] + public async Task Store_Outside_Transaction_Should_Throw() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant = TenantKeys.Single; + var store = new EfCoreRefreshTokenStore(db, new TenantContext(tenant)); + AuthSessionId.TryCreate(ValidRaw, out var sessionId); + + var token = RefreshToken.Create( + TokenId.From(Guid.NewGuid()), + "hash", + tenant, + UserKey.FromGuid(Guid.NewGuid()), + sessionId, + null, + DateTimeOffset.UtcNow, + DateTimeOffset.UtcNow.AddHours(1) + ); + + await Assert.ThrowsAsync(() => store.StoreAsync(token)); + } +} diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserIdentifierStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserIdentifierStoreTests.cs new file mode 100644 index 00000000..751f24f2 --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserIdentifierStoreTests.cs @@ -0,0 +1,331 @@ +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Errors; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Tests.Unit.Helpers; +using CodeBeam.UltimateAuth.Users.Contracts; +using CodeBeam.UltimateAuth.Users.EntityFrameworkCore; +using CodeBeam.UltimateAuth.Users.Reference; +using Microsoft.Data.Sqlite; + +namespace CodeBeam.UltimateAuth.Tests.Unit; + +public class EfCoreUserIdentifierStoreTests : EfCoreTestBase +{ + private static UAuthUserDbContext CreateDb(SqliteConnection connection) + { + return CreateDbContext(connection, options => new UAuthUserDbContext(options)); + } + + [Fact] + public async Task Add_And_Get_Should_Work() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + var tenant = TenantKeys.Single; + var store = new EfCoreUserIdentifierStore(db, new TenantContext(tenant)); + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var identifier = UserIdentifier.Create( + Guid.NewGuid(), + tenant, + userKey, + UserIdentifierType.Username, + "user", + "user", + DateTimeOffset.UtcNow, + isPrimary: true); + + await store.AddAsync(identifier); + var result = await store.GetAsync(identifier.Id); + + Assert.NotNull(result); + Assert.Equal(identifier.Id, result!.Id); + } + + [Fact] + public async Task Exists_Should_Return_True_When_Exists() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + var tenant = TenantKeys.Single; + var store = new EfCoreUserIdentifierStore(db, new TenantContext(tenant)); + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var identifier = UserIdentifier.Create( + Guid.NewGuid(), + tenant, + userKey, + UserIdentifierType.Username, + "user", + "user", + DateTimeOffset.UtcNow, + isPrimary: true); + + await store.AddAsync(identifier); + var exists = await store.ExistsAsync(identifier.Id); + + Assert.True(exists); + } + + [Fact] + public async Task Save_With_Wrong_Version_Should_Throw() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + var tenant = TenantKeys.Single; + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + await using var db1 = CreateDb(connection); + var store1 = new EfCoreUserIdentifierStore(db1, new TenantContext(tenant)); + + var identifier = UserIdentifier.Create( + Guid.NewGuid(), + tenant, + userKey, + UserIdentifierType.Username, + "user", + "user", + DateTimeOffset.UtcNow, + isPrimary: true); + + await store1.AddAsync(identifier); + + await using var db2 = CreateDb(connection); + var store2 = new EfCoreUserIdentifierStore(db2, new TenantContext(tenant)); + + var updated = identifier.SetPrimary(DateTimeOffset.UtcNow); + + await Assert.ThrowsAsync(() => + store2.SaveAsync(updated, expectedVersion: 999)); + } + + [Fact] + public async Task Save_Should_Increment_Version() + { + using var connection = CreateOpenConnection(); + + var tenant = TenantKeys.Single; + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var identifier = UserIdentifier.Create( + Guid.NewGuid(), + tenant, + userKey, + UserIdentifierType.Username, + "user", + "user", + DateTimeOffset.UtcNow, + isPrimary: true); + + await using (var db1 = CreateDb(connection)) + { + var store1 = new EfCoreUserIdentifierStore(db1, new TenantContext(tenant)); + await store1.AddAsync(identifier); + } + + await using (var db2 = CreateDb(connection)) + { + var store2 = new EfCoreUserIdentifierStore(db2, new TenantContext(tenant)); + var existing = await store2.GetAsync(identifier.Id); + var updated = existing!.SetPrimary(DateTimeOffset.UtcNow); + await store2.SaveAsync(updated, expectedVersion: 0); + } + + await using (var db3 = CreateDb(connection)) + { + var store3 = new EfCoreUserIdentifierStore(db3, new TenantContext(tenant)); + var result = await store3.GetAsync(identifier.Id); + Assert.Equal(1, result!.Version); + } + } + + [Fact] + public async Task Should_Not_See_Data_From_Other_Tenant() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + var tenant1 = TenantKeys.Single; + var tenant2 = TenantKey.FromInternal("tenant-2"); + var store1 = new EfCoreUserIdentifierStore(db, new TenantContext(tenant1)); + var store2 = new EfCoreUserIdentifierStore(db, new TenantContext(tenant2)); + + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var identifier = UserIdentifier.Create( + Guid.NewGuid(), + tenant1, + userKey, + UserIdentifierType.Username, + "user", + "user", + DateTimeOffset.UtcNow, + isPrimary: true); + + await store1.AddAsync(identifier); + var result = await store2.GetAsync(identifier.Id); + + Assert.Null(result); + } + + [Fact] + public async Task Soft_Delete_Should_Work() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + var tenant = TenantKeys.Single; + var store = new EfCoreUserIdentifierStore(db, new TenantContext(tenant)); + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var identifier = UserIdentifier.Create( + Guid.NewGuid(), + tenant, + userKey, + UserIdentifierType.Username, + "user", + "user", + DateTimeOffset.UtcNow, + isPrimary: true); + + await store.AddAsync(identifier); + await store.DeleteAsync(identifier.Id, 0, DeleteMode.Soft, DateTimeOffset.UtcNow); + var result = await store.GetAsync(identifier.Id); + + Assert.NotNull(result); + Assert.NotNull(result!.DeletedAt); + } + + [Fact] + public void ChangeValue_Should_Update_Value() + { + var now = DateTimeOffset.UtcNow; + + var id = UserIdentifier.Create( + Guid.NewGuid(), + TenantKeys.Single, + UserKey.FromGuid(Guid.NewGuid()), + UserIdentifierType.Username, + "user", + "user", + now); + + id.ChangeValue("new", "new", now); + + Assert.Equal("new", id.Value); + Assert.Equal("new", id.NormalizedValue); + Assert.Null(id.VerifiedAt); + } + + [Fact] + public void ChangeValue_SameValue_Should_Throw() + { + var now = DateTimeOffset.UtcNow; + + var id = UserIdentifier.Create( + Guid.NewGuid(), + TenantKeys.Single, + UserKey.FromGuid(Guid.NewGuid()), + UserIdentifierType.Username, + "user", + "user", + now); + + Assert.Throws(() => id.ChangeValue("user", "user", now)); + } + + [Fact] + public void SetPrimary_AlreadyPrimary_Should_NotChange() + { + var now = DateTimeOffset.UtcNow; + + var id = UserIdentifier.Create( + Guid.NewGuid(), + TenantKeys.Single, + UserKey.FromGuid(Guid.NewGuid()), + UserIdentifierType.Username, + "user", + "user", + now, + isPrimary: true); + + var result = id.SetPrimary(now); + + Assert.Same(id, result); + } + + [Fact] + public void UnsetPrimary_Should_Work() + { + var now = DateTimeOffset.UtcNow; + + var id = UserIdentifier.Create( + Guid.NewGuid(), + TenantKeys.Single, + UserKey.FromGuid(Guid.NewGuid()), + UserIdentifierType.Username, + "user", + "user", + now, + isPrimary: true); + + id.UnsetPrimary(now); + + Assert.False(id.IsPrimary); + } + + [Fact] + public void UnsetPrimary_NotPrimary_Should_Throw() + { + var now = DateTimeOffset.UtcNow; + + var id = UserIdentifier.Create( + Guid.NewGuid(), + TenantKeys.Single, + UserKey.FromGuid(Guid.NewGuid()), + UserIdentifierType.Username, + "user", + "user", + now); + + Assert.Throws(() => id.UnsetPrimary(now)); + } + + [Fact] + public void MarkVerified_Should_SetVerifiedAt() + { + var now = DateTimeOffset.UtcNow; + + var id = UserIdentifier.Create( + Guid.NewGuid(), + TenantKeys.Single, + UserKey.FromGuid(Guid.NewGuid()), + UserIdentifierType.Username, + "user", + "user", + now); + + id.MarkVerified(now); + + Assert.True(id.IsVerified); + } + + [Fact] + public void Deleted_Entity_Should_Not_Allow_Mutation() + { + var now = DateTimeOffset.UtcNow; + + var id = UserIdentifier.Create( + Guid.NewGuid(), + TenantKeys.Single, + UserKey.FromGuid(Guid.NewGuid()), + UserIdentifierType.Username, + "user", + "user", + now); + + id.MarkDeleted(now); + + Assert.Throws(() => + id.SetPrimary(now)); + } +} diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserLifecycleStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserLifecycleStoreTests.cs new file mode 100644 index 00000000..4c08b7b3 --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserLifecycleStoreTests.cs @@ -0,0 +1,227 @@ +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Errors; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Tests.Unit.Helpers; +using CodeBeam.UltimateAuth.Users.EntityFrameworkCore; +using CodeBeam.UltimateAuth.Users.Reference; +using Microsoft.Data.Sqlite; + +namespace CodeBeam.UltimateAuth.Tests.Unit; + +public class EfCoreUserLifecycleStoreTests : EfCoreTestBase +{ + private static UAuthUserDbContext CreateDb(SqliteConnection connection) + { + return CreateDbContext(connection, options => new UAuthUserDbContext(options)); + } + + [Fact] + public async Task Add_And_Get_Should_Work() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant = TenantKeys.Single; + var store = new EfCoreUserLifecycleStore(db, new TenantContext(tenant)); + + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var lifecycle = UserLifecycle.Create( + tenant, + userKey, + DateTimeOffset.UtcNow); + + await store.AddAsync(lifecycle); + + var result = await store.GetAsync(new UserLifecycleKey(tenant, userKey)); + + Assert.NotNull(result); + Assert.Equal(userKey, result!.UserKey); + Assert.Equal(UserStatus.Active, result.Status); + } + + [Fact] + public async Task Exists_Should_Return_True_When_Exists() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant = TenantKeys.Single; + var store = new EfCoreUserLifecycleStore(db, new TenantContext(tenant)); + + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var lifecycle = UserLifecycle.Create( + tenant, + userKey, + DateTimeOffset.UtcNow); + + await store.AddAsync(lifecycle); + var exists = await store.ExistsAsync(new UserLifecycleKey(tenant, userKey)); + + Assert.True(exists); + } + + [Fact] + public async Task Save_Should_Increment_Version() + { + using var connection = CreateOpenConnection(); + + var tenant = TenantKeys.Single; + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var lifecycle = UserLifecycle.Create( + tenant, + userKey, + DateTimeOffset.UtcNow); + + await using (var db1 = CreateDb(connection)) + { + var store1 = new EfCoreUserLifecycleStore(db1, new TenantContext(tenant)); + await store1.AddAsync(lifecycle); + } + + await using (var db2 = CreateDb(connection)) + { + var store2 = new EfCoreUserLifecycleStore(db2, new TenantContext(tenant)); + var existing = await store2.GetAsync(new UserLifecycleKey(tenant, userKey)); + var updated = existing!.ChangeStatus(DateTimeOffset.UtcNow, UserStatus.Suspended); + await store2.SaveAsync(updated, expectedVersion: 0); + } + + await using (var db3 = CreateDb(connection)) + { + var store3 = new EfCoreUserLifecycleStore(db3, new TenantContext(tenant)); + var result = await store3.GetAsync(new UserLifecycleKey(tenant, userKey)); + + Assert.Equal(1, result!.Version); + Assert.Equal(UserStatus.Suspended, result.Status); + } + } + + [Fact] + public async Task Save_With_Wrong_Version_Should_Throw() + { + using var connection = CreateOpenConnection(); + + var tenant = TenantKeys.Single; + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var lifecycle = UserLifecycle.Create( + tenant, + userKey, + DateTimeOffset.UtcNow); + + await using (var db1 = CreateDb(connection)) + { + var store1 = new EfCoreUserLifecycleStore(db1, new TenantContext(tenant)); + await store1.AddAsync(lifecycle); + } + + await using (var db2 = CreateDb(connection)) + { + var store2 = new EfCoreUserLifecycleStore(db2, new TenantContext(tenant)); + var existing = await store2.GetAsync(new UserLifecycleKey(tenant, userKey)); + var updated = existing!.ChangeStatus(DateTimeOffset.UtcNow, UserStatus.Suspended); + + await Assert.ThrowsAsync(() => + store2.SaveAsync(updated, expectedVersion: 999)); + } + } + + [Fact] + public async Task Should_Not_See_Data_From_Other_Tenant() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant1 = TenantKeys.Single; + var tenant2 = TenantKey.FromInternal("tenant-2"); + + var store1 = new EfCoreUserLifecycleStore(db, new TenantContext(tenant1)); + var store2 = new EfCoreUserLifecycleStore(db, new TenantContext(tenant2)); + + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var lifecycle = UserLifecycle.Create( + tenant1, + userKey, + DateTimeOffset.UtcNow); + + await store1.AddAsync(lifecycle); + + var result = await store2.GetAsync(new UserLifecycleKey(tenant2, userKey)); + + Assert.Null(result); + } + + [Fact] + public async Task Soft_Delete_Should_Work() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant = TenantKeys.Single; + var store = new EfCoreUserLifecycleStore(db, new TenantContext(tenant)); + + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var lifecycle = UserLifecycle.Create( + tenant, + userKey, + DateTimeOffset.UtcNow); + + await store.AddAsync(lifecycle); + + await store.DeleteAsync( + new UserLifecycleKey(tenant, userKey), + expectedVersion: 0, + DeleteMode.Soft, + DateTimeOffset.UtcNow); + + var result = await store.GetAsync(new UserLifecycleKey(tenant, userKey)); + + Assert.NotNull(result); + Assert.NotNull(result!.DeletedAt); + } + + [Fact] + public async Task Delete_Should_Increment_SecurityVersion() + { + using var connection = CreateOpenConnection(); + + var tenant = TenantKeys.Single; + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var lifecycle = UserLifecycle.Create( + tenant, + userKey, + DateTimeOffset.UtcNow); + + await using (var db1 = CreateDb(connection)) + { + var store1 = new EfCoreUserLifecycleStore(db1, new TenantContext(tenant)); + await store1.AddAsync(lifecycle); + } + + await using (var db2 = CreateDb(connection)) + { + var store2 = new EfCoreUserLifecycleStore(db2, new TenantContext(tenant)); + + var existing = await store2.GetAsync(new UserLifecycleKey(tenant, userKey)); + var deleted = existing!.MarkDeleted(DateTimeOffset.UtcNow); + + await store2.SaveAsync(deleted, expectedVersion: 0); + } + + await using (var db3 = CreateDb(connection)) + { + var store3 = new EfCoreUserLifecycleStore(db3, new TenantContext(tenant)); + + var result = await store3.GetAsync(new UserLifecycleKey(tenant, userKey)); + + Assert.Equal(1, result!.SecurityVersion); + } + } +} diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserProfileStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserProfileStoreTests.cs new file mode 100644 index 00000000..db39136f --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserProfileStoreTests.cs @@ -0,0 +1,215 @@ +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Errors; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Tests.Unit.Helpers; +using CodeBeam.UltimateAuth.Users.EntityFrameworkCore; +using CodeBeam.UltimateAuth.Users.Reference; +using Microsoft.Data.Sqlite; + +namespace CodeBeam.UltimateAuth.Tests.Unit; + +public class EfCoreUserProfileStoreTests : EfCoreTestBase +{ + private static UAuthUserDbContext CreateDb(SqliteConnection connection) + { + return CreateDbContext(connection, options => new UAuthUserDbContext(options)); + } + + [Fact] + public async Task Add_And_Get_Should_Work() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant = TenantKeys.Single; + var store = new EfCoreUserProfileStore(db, new TenantContext(tenant)); + + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var profile = UserProfile.Create( + Guid.NewGuid(), + tenant, + userKey, + DateTimeOffset.UtcNow, + displayName: "display", + firstName: "first", + lastName: "last" + ); + + await store.AddAsync(profile); + var result = await store.GetAsync(new UserProfileKey(tenant, userKey)); + + Assert.NotNull(result); + Assert.Equal(userKey, result!.UserKey); + } + + [Fact] + public async Task Exists_Should_Return_True_When_Exists() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant = TenantKeys.Single; + var store = new EfCoreUserProfileStore(db, new TenantContext(tenant)); + + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var profile = UserProfile.Create( + Guid.NewGuid(), + tenant, + userKey, + DateTimeOffset.UtcNow, + displayName: "display", + firstName: "first", + lastName: "last" + ); + + await store.AddAsync(profile); + var exists = await store.ExistsAsync(new UserProfileKey(tenant, userKey)); + + Assert.True(exists); + } + + [Fact] + public async Task Save_Should_Increment_Version() + { + using var connection = CreateOpenConnection(); + + var tenant = TenantKeys.Single; + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var profile = UserProfile.Create( + Guid.NewGuid(), + tenant, + userKey, + DateTimeOffset.UtcNow, + displayName: "display", + firstName: "first", + lastName: "last" + ); + + await using (var db1 = CreateDb(connection)) + { + var store1 = new EfCoreUserProfileStore(db1, new TenantContext(tenant)); + await store1.AddAsync(profile); + } + + await using (var db2 = CreateDb(connection)) + { + var store2 = new EfCoreUserProfileStore(db2, new TenantContext(tenant)); + var existing = await store2.GetAsync(new UserProfileKey(tenant, userKey)); + var updated = existing!.UpdateName(existing.FirstName, existing.LastName, "new", DateTimeOffset.UtcNow); + await store2.SaveAsync(updated, expectedVersion: 0); + } + + await using (var db3 = CreateDb(connection)) + { + var store3 = new EfCoreUserProfileStore(db3, new TenantContext(tenant)); + var result = await store3.GetAsync(new UserProfileKey(tenant, userKey)); + + Assert.Equal(1, result!.Version); + Assert.Equal("new", result.DisplayName); + } + } + + [Fact] + public async Task Save_With_Wrong_Version_Should_Throw() + { + using var connection = CreateOpenConnection(); + + var tenant = TenantKeys.Single; + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var profile = UserProfile.Create( + Guid.NewGuid(), + tenant, + userKey, + DateTimeOffset.UtcNow, + displayName: "display", + firstName: "first", + lastName: "last" + ); + + await using (var db1 = CreateDb(connection)) + { + var store1 = new EfCoreUserProfileStore(db1, new TenantContext(tenant)); + await store1.AddAsync(profile); + } + + await using (var db2 = CreateDb(connection)) + { + var store2 = new EfCoreUserProfileStore(db2, new TenantContext(tenant)); + var existing = await store2.GetAsync(new UserProfileKey(tenant, userKey)); + var updated = existing!.UpdateName(existing.FirstName, existing.LastName, "new", DateTimeOffset.UtcNow); + + await Assert.ThrowsAsync(() => + store2.SaveAsync(updated, expectedVersion: 999)); + } + } + + [Fact] + public async Task Should_Not_See_Data_From_Other_Tenant() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant1 = TenantKeys.Single; + var tenant2 = TenantKey.FromInternal("tenant-2"); + + var store1 = new EfCoreUserProfileStore(db, new TenantContext(tenant1)); + var store2 = new EfCoreUserProfileStore(db, new TenantContext(tenant2)); + + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var profile = UserProfile.Create( + Guid.NewGuid(), + tenant1, + userKey, + DateTimeOffset.UtcNow, + displayName: "display", + firstName: "first", + lastName: "last" + ); + + await store1.AddAsync(profile); + var result = await store2.GetAsync(new UserProfileKey(tenant2, userKey)); + + Assert.Null(result); + } + + [Fact] + public async Task Soft_Delete_Should_Work() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant = TenantKeys.Single; + var store = new EfCoreUserProfileStore(db, new TenantContext(tenant)); + + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var profile = UserProfile.Create( + Guid.NewGuid(), + tenant, + userKey, + DateTimeOffset.UtcNow, + displayName: "display", + firstName: "first", + lastName: "last" + ); + + await store.AddAsync(profile); + + await store.DeleteAsync( + new UserProfileKey(tenant, userKey), + expectedVersion: 0, + DeleteMode.Soft, + DateTimeOffset.UtcNow); + + var result = await store.GetAsync(new UserProfileKey(tenant, userKey)); + + Assert.NotNull(result); + Assert.NotNull(result!.DeletedAt); + } +} diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserRoleStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserRoleStoreTests.cs new file mode 100644 index 00000000..f1b5f3b6 --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserRoleStoreTests.cs @@ -0,0 +1,152 @@ +using CodeBeam.UltimateAuth.Authorization.Contracts; +using CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Errors; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Tests.Unit.Helpers; +using Microsoft.Data.Sqlite; + +namespace CodeBeam.UltimateAuth.Tests.Unit; + +public class EfCoreUserRoleStoreTests : EfCoreTestBase +{ + private static UAuthAuthorizationDbContext CreateDb(SqliteConnection connection) + { + return CreateDbContext(connection, options => new UAuthAuthorizationDbContext(options)); + } + + [Fact] + public async Task Assign_And_GetAssignments_Should_Work() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant = TenantKeys.Single; + var store = new EfCoreUserRoleStore(db, new TenantContext(tenant)); + + var userKey = UserKey.FromGuid(Guid.NewGuid()); + var roleId = RoleId.New(); + + await store.AssignAsync(userKey, roleId, DateTimeOffset.UtcNow); + var result = await store.GetAssignmentsAsync(userKey); + + Assert.Single(result); + Assert.Equal(roleId, result.First().RoleId); + } + + [Fact] + public async Task Assign_Duplicate_Should_Throw() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant = TenantKeys.Single; + var store = new EfCoreUserRoleStore(db, new TenantContext(tenant)); + + var userKey = UserKey.FromGuid(Guid.NewGuid()); + var roleId = RoleId.New(); + + await store.AssignAsync(userKey, roleId, DateTimeOffset.UtcNow); + + await Assert.ThrowsAsync(() => store.AssignAsync(userKey, roleId, DateTimeOffset.UtcNow)); + } + + [Fact] + public async Task Remove_Should_Work() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant = TenantKeys.Single; + var store = new EfCoreUserRoleStore(db, new TenantContext(tenant)); + + var userKey = UserKey.FromGuid(Guid.NewGuid()); + var roleId = RoleId.New(); + + await store.AssignAsync(userKey, roleId, DateTimeOffset.UtcNow); + await store.RemoveAsync(userKey, roleId); + var result = await store.GetAssignmentsAsync(userKey); + + Assert.Empty(result); + } + + [Fact] + public async Task Remove_NonExisting_Should_Not_Throw() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant = TenantKeys.Single; + var store = new EfCoreUserRoleStore(db, new TenantContext(tenant)); + + var userKey = UserKey.FromGuid(Guid.NewGuid()); + var roleId = RoleId.New(); + + await store.RemoveAsync(userKey, roleId); // should not throw + } + + [Fact] + public async Task CountAssignments_Should_Return_Correct_Count() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant = TenantKeys.Single; + var store = new EfCoreUserRoleStore(db, new TenantContext(tenant)); + + var roleId = RoleId.New(); + + await store.AssignAsync(UserKey.FromGuid(Guid.NewGuid()), roleId, DateTimeOffset.UtcNow); + await store.AssignAsync(UserKey.FromGuid(Guid.NewGuid()), roleId, DateTimeOffset.UtcNow); + + var count = await store.CountAssignmentsAsync(roleId); + + Assert.Equal(2, count); + } + + [Fact] + public async Task RemoveAssignmentsByRole_Should_Remove_All() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant = TenantKeys.Single; + var store = new EfCoreUserRoleStore(db, new TenantContext(tenant)); + + var roleId = RoleId.New(); + + var user1 = UserKey.FromGuid(Guid.NewGuid()); + var user2 = UserKey.FromGuid(Guid.NewGuid()); + + await store.AssignAsync(user1, roleId, DateTimeOffset.UtcNow); + await store.AssignAsync(user2, roleId, DateTimeOffset.UtcNow); + + await store.RemoveAssignmentsByRoleAsync(roleId); + + var count = await store.CountAssignmentsAsync(roleId); + + Assert.Equal(0, count); + } + + [Fact] + public async Task Should_Not_See_Data_From_Other_Tenant() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant1 = TenantKeys.Single; + var tenant2 = TenantKey.FromInternal("tenant-2"); + + var store1 = new EfCoreUserRoleStore(db, new TenantContext(tenant1)); + var store2 = new EfCoreUserRoleStore(db, new TenantContext(tenant2)); + + var userKey = UserKey.FromGuid(Guid.NewGuid()); + var roleId = RoleId.New(); + + await store1.AssignAsync(userKey, roleId, DateTimeOffset.UtcNow); + + var result = await store2.GetAssignmentsAsync(userKey); + + Assert.Empty(result); + } +} diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Helpers/EfCoreTestBase.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Helpers/EfCoreTestBase.cs new file mode 100644 index 00000000..7cef91ab --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Helpers/EfCoreTestBase.cs @@ -0,0 +1,26 @@ +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; + +namespace CodeBeam.UltimateAuth.Tests.Unit.Helpers; + +public abstract class EfCoreTestBase +{ + protected SqliteConnection CreateOpenConnection() + { + var conn = new SqliteConnection("Filename=:memory:"); + conn.Open(); + return conn; + } + + protected static TDbContext CreateDbContext(SqliteConnection connection, Func, TDbContext> factory) where TDbContext : DbContext + { + var options = new DbContextOptionsBuilder() + .UseSqlite(connection) + .EnableSensitiveDataLogging() + .Options; + + var db = factory(options); + db.Database.EnsureCreated(); + return db; + } +} diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Helpers/TestAuthRuntime.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Helpers/TestAuthRuntime.cs index 26e6d480..f2e38d0f 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Helpers/TestAuthRuntime.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Helpers/TestAuthRuntime.cs @@ -1,6 +1,4 @@ using CodeBeam.UltimateAuth.Authentication.InMemory; -using CodeBeam.UltimateAuth.Authorization.InMemory.Extensions; -using CodeBeam.UltimateAuth.Authorization.Reference.Extensions; using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; @@ -8,18 +6,13 @@ using CodeBeam.UltimateAuth.Core.Infrastructure; using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Core.Options; -using CodeBeam.UltimateAuth.Credentials.InMemory.Extensions; using CodeBeam.UltimateAuth.Credentials.Reference; +using CodeBeam.UltimateAuth.InMemory; using CodeBeam.UltimateAuth.Server.Auth; using CodeBeam.UltimateAuth.Server.Extensions; using CodeBeam.UltimateAuth.Server.Flows; -using CodeBeam.UltimateAuth.Server.Infrastructure; using CodeBeam.UltimateAuth.Server.Options; -using CodeBeam.UltimateAuth.Sessions.InMemory; -using CodeBeam.UltimateAuth.Tokens.InMemory; -using CodeBeam.UltimateAuth.Users.InMemory.Extensions; using CodeBeam.UltimateAuth.Users.Reference; -using CodeBeam.UltimateAuth.Users.Reference.Extensions; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -45,21 +38,8 @@ public TestAuthRuntime(Action? configureServer = null, Actio services.AddSingleton(); // InMemory plugins - services.AddUltimateAuthUsersInMemory(); - services.AddUltimateAuthCredentialsInMemory(); - services.AddUltimateAuthInMemorySessions(); - services.AddUltimateAuthInMemoryTokens(); - services.AddUltimateAuthInMemoryAuthenticationSecurity(); - services.AddUltimateAuthAuthorizationInMemory(); - services.AddUltimateAuthUsersReference(); - services.AddUltimateAuthAuthorizationReference(); - services.AddUltimateAuthCredentialsReference(); - - services.AddScoped(); - services.AddScoped(); - services.AddSingleton(); - services.AddSingleton(sp => - sp.GetRequiredService()); + services.AddInMemoryReference(); + var configuration = new ConfigurationBuilder().AddInMemoryCollection().Build(); @@ -67,7 +47,15 @@ public TestAuthRuntime(Action? configureServer = null, Actio services.AddSingleton(Clock); Services = services.BuildServiceProvider(); - Services.GetRequiredService().RunAsync(null).GetAwaiter().GetResult(); + + using (var scope = Services.CreateScope()) + { + var seedRunner = scope.ServiceProvider.GetRequiredService(); + seedRunner.RunAsync(null).GetAwaiter().GetResult(); + } + + //Services = services.BuildServiceProvider(); + //Services.GetRequiredService().RunAsync(null).GetAwaiter().GetResult(); } public ILoginOrchestrator GetLoginOrchestrator() @@ -100,7 +88,7 @@ public async Task LoginAsync(AuthFlowContext flow) return await orchestrator.LoginAsync(flow, new LoginRequest { - Tenant = TenantKey.Single, + Tenant = TenantKeys.Single, Identifier = "user", Secret = "user" }); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/LoginOrchestratorTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/LoginOrchestratorTests.cs index 2f6aabb7..bff92f6d 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/LoginOrchestratorTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/LoginOrchestratorTests.cs @@ -67,8 +67,9 @@ await orchestrator.LoginAsync(flow, Secret = "wrong", }); - var store = runtime.Services.GetRequiredService(); - var state = await store.GetAsync(TenantKey.Single, TestUsers.User, AuthenticationSecurityScope.Factor, CredentialType.Password); + var factory = runtime.Services.GetRequiredService(); + var store = factory.Create(TenantKeys.Single); + var state = await store.GetAsync(TestUsers.User, AuthenticationSecurityScope.Factor, CredentialType.Password); state?.FailedAttempts.Should().Be(1); } @@ -99,8 +100,9 @@ await orchestrator.LoginAsync(flow, Secret = "user", // valid password }); - var store = runtime.Services.GetRequiredService(); - var state = await store.GetAsync(TenantKey.Single, TestUsers.User, AuthenticationSecurityScope.Factor, CredentialType.Password); + var factory = runtime.Services.GetRequiredService(); + var store = factory.Create(TenantKeys.Single); + var state = await store.GetAsync(TestUsers.User, AuthenticationSecurityScope.Factor, CredentialType.Password); state?.FailedAttempts.Should().Be(0); } @@ -159,8 +161,9 @@ await orchestrator.LoginAsync(flow, Secret = "wrong", }); - var store = runtime.Services.GetRequiredService(); - var state = await store.GetAsync(TenantKey.Single, TestUsers.User, AuthenticationSecurityScope.Factor, CredentialType.Password); + var factory = runtime.Services.GetRequiredService(); + var store = factory.Create(TenantKeys.Single); + var state = await store.GetAsync(TestUsers.User, AuthenticationSecurityScope.Factor, CredentialType.Password); state!.IsLocked(DateTimeOffset.UtcNow).Should().BeTrue(); } @@ -214,8 +217,9 @@ await orchestrator.LoginAsync(flow, Secret = "wrong", }); - var store = runtime.Services.GetRequiredService(); - var state1 = await store.GetAsync(TenantKey.Single, TestUsers.User, AuthenticationSecurityScope.Factor, CredentialType.Password); + var factory = runtime.Services.GetRequiredService(); + var store = factory.Create(TenantKeys.Single); + var state1 = await store.GetAsync(TestUsers.User, AuthenticationSecurityScope.Factor, CredentialType.Password); await orchestrator.LoginAsync(flow, new LoginRequest @@ -224,7 +228,7 @@ await orchestrator.LoginAsync(flow, Identifier = "user", }); - var state2 = await store.GetAsync(TenantKey.Single, TestUsers.User, AuthenticationSecurityScope.Factor, CredentialType.Password); + var state2 = await store.GetAsync(TestUsers.User, AuthenticationSecurityScope.Factor, CredentialType.Password); state2?.FailedAttempts.Should().Be(state1!.FailedAttempts); } @@ -250,8 +254,9 @@ await orchestrator.LoginAsync(flow, }); } - var store = runtime.Services.GetRequiredService(); - var state = await store.GetAsync(TenantKey.Single, TestUsers.User, AuthenticationSecurityScope.Factor, CredentialType.Password); + var factory = runtime.Services.GetRequiredService(); + var store = factory.Create(TenantKeys.Single); + var state = await store.GetAsync(TestUsers.User, AuthenticationSecurityScope.Factor, CredentialType.Password); state?.IsLocked(DateTimeOffset.UtcNow).Should().BeFalse(); state?.FailedAttempts.Should().Be(5); @@ -277,8 +282,9 @@ await orchestrator.LoginAsync(flow, Secret = "wrong", }); - var store = runtime.Services.GetRequiredService(); - var state1 = await store.GetAsync(TenantKey.Single, TestUsers.User, AuthenticationSecurityScope.Factor, CredentialType.Password); + var factory = runtime.Services.GetRequiredService(); + var store = factory.Create(TenantKeys.Single); + var state1 = await store.GetAsync(TestUsers.User, AuthenticationSecurityScope.Factor, CredentialType.Password); var lockedUntil = state1!.LockedUntil; @@ -290,7 +296,7 @@ await orchestrator.LoginAsync(flow, Secret = "wrong", }); - var state2 = await store.GetAsync(TenantKey.Single, TestUsers.User, AuthenticationSecurityScope.Factor, CredentialType.Password); + var state2 = await store.GetAsync(TestUsers.User, AuthenticationSecurityScope.Factor, CredentialType.Password); state2?.LockedUntil.Should().Be(lockedUntil); } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/SessionTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/SessionTests.cs index dec7a17f..404a9fc2 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/SessionTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/SessionTests.cs @@ -96,7 +96,7 @@ public async Task Get_chain_detail_should_return_sessions() await orchestrator.LoginAsync(flow, new LoginRequest { - Tenant = TenantKey.Single, + Tenant = TenantKeys.Single, Identifier = "user", Secret = "user" }); @@ -121,7 +121,7 @@ public async Task Revoke_chain_should_revoke_all_sessions() await orchestrator.LoginAsync(flow, new LoginRequest { - Tenant = TenantKey.Single, + Tenant = TenantKeys.Single, Identifier = "user", Secret = "user" }); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/IdentifierConcurrencyTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/IdentifierConcurrencyTests.cs index a66d3dd7..ef600fd8 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/IdentifierConcurrencyTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/IdentifierConcurrencyTests.cs @@ -13,7 +13,7 @@ public class IdentifierConcurrencyTests [Fact] public async Task Save_should_increment_version() { - var store = new InMemoryUserIdentifierStore(); + var store = new InMemoryUserIdentifierStore(new TenantContext(TenantKeys.Single)); var now = DateTimeOffset.UtcNow; var id = Guid.NewGuid(); @@ -34,7 +34,7 @@ public async Task Save_should_increment_version() [Fact] public async Task Delete_should_throw_when_version_conflicts() { - var store = new InMemoryUserIdentifierStore(); + var store = new InMemoryUserIdentifierStore(new TenantContext(TenantKeys.Single)); var now = DateTimeOffset.UtcNow; var id = Guid.NewGuid(); @@ -57,7 +57,7 @@ await Assert.ThrowsAsync(async () => [Fact] public async Task Parallel_SetPrimary_should_conflict_deterministic() { - var store = new InMemoryUserIdentifierStore(); + var store = new InMemoryUserIdentifierStore(new TenantContext(TenantKeys.Single)); var now = DateTimeOffset.UtcNow; var id = Guid.NewGuid(); @@ -103,7 +103,7 @@ public async Task Parallel_SetPrimary_should_conflict_deterministic() [Fact] public async Task Update_should_throw_concurrency_when_versions_conflict() { - var store = new InMemoryUserIdentifierStore(); + var store = new InMemoryUserIdentifierStore(new TenantContext(TenantKeys.Single)); var id = Guid.NewGuid(); var now = DateTimeOffset.UtcNow; var tenant = TenantKey.Single; @@ -130,7 +130,7 @@ await Assert.ThrowsAsync(async () => [Fact] public async Task Parallel_updates_should_result_in_single_success_deterministic() { - var store = new InMemoryUserIdentifierStore(); + var store = new InMemoryUserIdentifierStore(new TenantContext(TenantKeys.Single)); var now = DateTimeOffset.UtcNow; var tenant = TenantKey.Single; var id = Guid.NewGuid(); @@ -181,7 +181,7 @@ public async Task Parallel_updates_should_result_in_single_success_deterministic [Fact] public async Task High_contention_updates_should_allow_only_one_success() { - var store = new InMemoryUserIdentifierStore(); + var store = new InMemoryUserIdentifierStore(new TenantContext(TenantKeys.Single)); var now = DateTimeOffset.UtcNow; var tenant = TenantKey.Single; var id = Guid.NewGuid(); @@ -227,7 +227,7 @@ public async Task High_contention_updates_should_allow_only_one_success() [Fact] public async Task High_contention_SetPrimary_should_allow_only_one_deterministic() { - var store = new InMemoryUserIdentifierStore(); + var store = new InMemoryUserIdentifierStore(new TenantContext(TenantKeys.Single)); var now = DateTimeOffset.UtcNow; var tenant = TenantKey.Single; @@ -276,7 +276,7 @@ public async Task High_contention_SetPrimary_should_allow_only_one_deterministic [Fact] public async Task Two_identifiers_racing_for_primary_should_allow() { - var store = new InMemoryUserIdentifierStore(); + var store = new InMemoryUserIdentifierStore(new TenantContext(TenantKeys.Single)); var now = DateTimeOffset.UtcNow; var tenant = TenantKey.Single; var user = TestUsers.Admin; @@ -344,7 +344,7 @@ public async Task Two_identifiers_racing_for_primary_should_allow() Assert.Equal(2, success); Assert.Equal(0, conflicts); - var all = await store.GetByUserAsync(tenant, user); + var all = await store.GetByUserAsync(user); var primaries = all .Where(x => x.Type == UserIdentifierType.Email && x.IsPrimary)