diff --git a/.gitignore b/.gitignore index 6786d1dc..049d5f46 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ *.userosscache *.sln.docstates +*.db-shm +*.db-wal + # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/Directory.Build.props b/Directory.Build.props index 7b23949a..96fa6cdf 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 0.1.0-preview.2 + 0.1.0-rc.1 $(NoWarn);1591 CodeBeam diff --git a/README.md b/README.md index 5331a4c9..930e8a4f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -UltimateAuth Banner +

+ +



diff --git a/docs/content/getting-started/quickstart.md b/docs/content/getting-started/quickstart.md index 8d77e763..77853bb1 100644 --- a/docs/content/getting-started/quickstart.md +++ b/docs/content/getting-started/quickstart.md @@ -12,7 +12,7 @@ In this guide, you will set up UltimateAuth in a few minutes and perform your ** ## 1. Create a Project -Create a new Blazor Server web app: +Start by creating a new Blazor app: ```bash dotnet new blazorserver -n UltimateAuthDemo @@ -21,7 +21,7 @@ cd UltimateAuthDemo ## 2. Install Packages -Add UltimateAuth packages: +Install the required UltimateAuth packages: ```csharp dotnet add package CodeBeam.UltimateAuth.Server @@ -69,7 +69,46 @@ Replace `Routes.razor` with this code: ``` -## 8. Perform Your First Login +## 8. Recommended Setup (Optional) +Add these for better experience: + +For login page (Use this only once in your application) +```csharp +@attribute [UAuthLoginPage] +``` + +For protected pages +```csharp +@attribute [UAuthAuthorize] +``` + +For any page that you use UltimateAuth features like AuthState etc. +```csharp +@inherits UAuthFlowPageBase +``` + +## 9. Seed Data For QuickStart (Optional) +This code creates admin and user users with same password and admin role. + +For in memory +```csharp +builder.Services.AddUltimateAuthSampleSeed(); +``` + +For entity framework core: +```csharp +builder.Services.AddScopedUltimateAuthSampleSeed(); +``` + +In pipeline configuration +```csharp +if (app.Environment.IsDevelopment()) +{ + await app.SeedUltimateAuthAsync(); +} +``` + +## 10. Perform Your First Login Example using IUAuthClient: ```csharp [Inject] IUAuthClient UAuthClient { get; set; } = null!; @@ -78,8 +117,8 @@ private async Task Login() { await UAuthClient.Flows.LoginAsync(new LoginRequest { - Identifier = "demo", - Secret = "password" + Identifier = "admin", + Secret = "admin" }); } ``` diff --git a/docs/content/getting-started/real-world-setup.md b/docs/content/getting-started/real-world-setup.md index 574cf928..92afaeef 100644 --- a/docs/content/getting-started/real-world-setup.md +++ b/docs/content/getting-started/real-world-setup.md @@ -65,7 +65,7 @@ builder.Services.AddUltimateAuthServer(o => { }); ``` -## Blazor WASM Setup +## Blazor Standalone WASM Setup Blazor WASM applications run entirely on the client and cannot securely handle credentials. For this reason, UltimateAuth uses a dedicated Auth server called **UAuthHub**. @@ -91,6 +91,42 @@ app.MapUltimateAuthEndpoints(); app.MapUAuthHub(); ``` +## Blazor Web App Setup +A blazor web app contains two projects that includes host and client. You need to arrange them both. + +In the host project: +```csharp +builder.Services.AddUltimateAuthClientBlazor(o => +{ + o.Endpoints.BasePath = "https://localhost:6112/auth"; // UAuthHub URL + o.Pkce.ReturnUrl = "https://localhost:6132/home"; // Current application domain + path +}); + +// In pipeline configuration +app.MapRazorComponents() + .AddInteractiveWebAssemblyRenderMode() + .AddAdditionalAssemblies(UAuthAssemblies.BlazorClient().First()); +``` + +In the client project: +```csharp +builder.Services.AddUltimateAuthClientBlazor(o => +{ + o.Endpoints.BasePath = "https://localhost:6112/auth"; // UAuthHub URL + o.Pkce.ReturnUrl = "https://localhost:6132/home"; // Current application domain + path +}); + +builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); + +// Optional if you use external API calls in your client project. +builder.Services.AddHttpClient("resourceApi", client => +{ + client.BaseAddress = new Uri("https://localhost:6122"); +}); +``` + +> If you want to use embedded UAuthHub in host project, you can register server services as shown in quickstart. + > ℹ️ UltimateAuth automatically selects the appropriate authentication mode (PureOpaque, Hybrid, etc.) based on the client type. ## ResourceApi Setup diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/DocsSidebar.razor b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/DocsSidebar.razor index 08c0b652..8743c5d6 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/DocsSidebar.razor +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/DocsSidebar.razor @@ -35,7 +35,6 @@ @code { private Dictionary>? _groups; - private DocIndexItem? _selectedValue; private bool _expanded = true; [Parameter] diff --git a/samples/CodeBeam.UltimateAuth.Sample.Seed/CodeBeam.UltimateAuth.Sample.Seed.csproj b/samples/CodeBeam.UltimateAuth.Sample.Seed/CodeBeam.UltimateAuth.Sample.Seed.csproj index 92487581..cc80e9e8 100644 --- a/samples/CodeBeam.UltimateAuth.Sample.Seed/CodeBeam.UltimateAuth.Sample.Seed.csproj +++ b/samples/CodeBeam.UltimateAuth.Sample.Seed/CodeBeam.UltimateAuth.Sample.Seed.csproj @@ -1,13 +1,22 @@  - net10.0 - enable - enable + net8.0;net9.0;net10.0 + $(NoWarn);1591 + CodeBeam.UltimateAuth.Sample.Seed + + Minimal seeded data for UltimateAuth samples and quickstart projects. + + authentication;session;identity;auth-framework;seed;data + uauthlogo.png + + + + diff --git a/samples/CodeBeam.UltimateAuth.Sample.Seed/Extensions/UAuthSeedExtensions.cs b/samples/CodeBeam.UltimateAuth.Sample.Seed/Extensions/UAuthSeedExtensions.cs new file mode 100644 index 00000000..aeea00f2 --- /dev/null +++ b/samples/CodeBeam.UltimateAuth.Sample.Seed/Extensions/UAuthSeedExtensions.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using CodeBeam.UltimateAuth.Core.Infrastructure; +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Sample.Seed.Extensions; + +public static class UAuthSeedExtensions +{ + public static async Task SeedUltimateAuthAsync(this WebApplication app, TenantKey? tenant = null, CancellationToken ct = default) + { + using var scope = app.Services.CreateScope(); + var runner = scope.ServiceProvider.GetRequiredService(); + await runner.RunAsync(tenant, ct); + } +} diff --git a/samples/CodeBeam.UltimateAuth.Sample.Seed/uauthlogo.png b/samples/CodeBeam.UltimateAuth.Sample.Seed/uauthlogo.png new file mode 100644 index 00000000..911f2530 Binary files /dev/null and b/samples/CodeBeam.UltimateAuth.Sample.Seed/uauthlogo.png differ diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Program.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Program.cs index 125730ca..0de4d2c1 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Program.cs +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Program.cs @@ -71,10 +71,9 @@ using (var scope = app.Services.CreateScope()) { await UAuthDbInitializer.InitializeAsync(app.Services, reset: true); - - var seedRunner = scope.ServiceProvider.GetRequiredService(); - await seedRunner.RunAsync(null); } + + await app.SeedUltimateAuthAsync(); } app.UseHttpsRedirection(); diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db-shm b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db-shm index c6cedbd5..3490745a 100644 Binary files a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db-shm and b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db-shm differ diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db-wal b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db-wal index a23ad2d5..d3a275a6 100644 Binary files a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db-wal and b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db-wal differ diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs index 9784acb8..1182dcf7 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs @@ -63,9 +63,7 @@ app.MapOpenApi(); app.MapScalarApiReference(); - using var scope = app.Services.CreateScope(); - var seedRunner = scope.ServiceProvider.GetRequiredService(); - await seedRunner.RunAsync(null); + await app.SeedUltimateAuthAsync(); } app.UseHttpsRedirection(); diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/CreateUserDialog.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/CreateUserDialog.razor.cs index 820b1119..a06a2acd 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/CreateUserDialog.razor.cs +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/CreateUserDialog.razor.cs @@ -18,7 +18,7 @@ public partial class CreateUserDialog private async Task CreateUserAsync() { - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) return; diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/CredentialDialog.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/CredentialDialog.razor.cs index 5f419abf..5e931ea6 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/CredentialDialog.razor.cs +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/CredentialDialog.razor.cs @@ -31,7 +31,7 @@ private async Task ChangePasswordAsync() if (_form is null) return; - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) { Snackbar.Add("Form is not valid.", Severity.Error); diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/ProfileDialog.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/ProfileDialog.razor.cs index 955e8e98..f89b038e 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/ProfileDialog.razor.cs +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/ProfileDialog.razor.cs @@ -70,7 +70,7 @@ private async Task SaveAsync() if (_form is not null) { - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) return; } diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/AuthorizedTestPage.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/AuthorizedTestPage.razor index 5dc5d8aa..7218a9c1 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/AuthorizedTestPage.razor +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/AuthorizedTestPage.razor @@ -1,5 +1,6 @@ @page "/authorized-test" -@attribute [Authorize] +@attribute [UAuthAuthorize] +@inherits UAuthFlowPageBase diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/Home.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/Home.razor index 76de9054..74cb1b79 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/Home.razor +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/Home.razor @@ -1,5 +1,5 @@ @page "/home" -@attribute [Authorize] +@attribute [UAuthAuthorize] @inherits UAuthFlowPageBase @inject IUAuthClient UAuthClient diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/Register.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/Register.razor.cs index e8c16205..41eadd64 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/Register.razor.cs +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/Register.razor.cs @@ -20,7 +20,7 @@ protected override async Task OnInitializedAsync() private async Task HandleRegisterAsync() { - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) return; diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/ResetCredential.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/ResetCredential.razor.cs index 21fad18a..71a4d93e 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/ResetCredential.razor.cs +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/ResetCredential.razor.cs @@ -12,7 +12,7 @@ public partial class ResetCredential private async Task ResetPasswordAsync() { - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) { Snackbar.Add("Please fix the validation errors.", Severity.Error); diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Program.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Program.cs index 377eb8b1..99fe67c7 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Program.cs +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Program.cs @@ -89,12 +89,9 @@ using (var scope = app.Services.CreateScope()) { await UAuthDbInitializer.InitializeAsync(app.Services, reset: true); - - var seedRunner = scope.ServiceProvider.GetRequiredService(); - await seedRunner.RunAsync(null); } + await app.SeedUltimateAuthAsync(); } - app.UseForwardedHeaders(); app.UseHttpsRedirection(); diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db index b911a93d..4e86411b 100644 Binary files a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db and b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db differ diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-shm b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-shm new file mode 100644 index 00000000..f64b9c90 Binary files /dev/null and b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-shm differ diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-wal b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-wal new file mode 100644 index 00000000..05f8b783 Binary files /dev/null and b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-wal differ diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/CreateUserDialog.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/CreateUserDialog.razor.cs index ec16e78f..af13ee27 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/CreateUserDialog.razor.cs +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/CreateUserDialog.razor.cs @@ -18,7 +18,7 @@ public partial class CreateUserDialog private async Task CreateUserAsync() { - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) return; diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/CredentialDialog.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/CredentialDialog.razor.cs index ee48c215..887707c5 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/CredentialDialog.razor.cs +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/CredentialDialog.razor.cs @@ -31,7 +31,7 @@ private async Task ChangePasswordAsync() if (_form is null) return; - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) { Snackbar.Add("Form is not valid.", Severity.Error); diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/ProfileDialog.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/ProfileDialog.razor.cs index 24fd603e..d2186593 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/ProfileDialog.razor.cs +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/ProfileDialog.razor.cs @@ -70,7 +70,7 @@ private async Task SaveAsync() if (_form is not null) { - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) return; } diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/AuthorizedTestPage.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/AuthorizedTestPage.razor index 5dc5d8aa..d0a06c06 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/AuthorizedTestPage.razor +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/AuthorizedTestPage.razor @@ -1,5 +1,6 @@ @page "/authorized-test" -@attribute [Authorize] +@attribute [UAuthAuthorize] +@inherits UAuthFlowPageBase @@ -13,7 +14,7 @@ - Go Profile + Go Home Page diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor index b71e9282..02cb0f28 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor @@ -1,5 +1,5 @@ @page "/home" -@attribute [Authorize] +@attribute [UAuthAuthorize] @inherits UAuthFlowPageBase @inject IUAuthClient UAuthClient diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Register.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Register.razor.cs index d1e67865..6adc7dc6 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Register.razor.cs +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Register.razor.cs @@ -20,7 +20,7 @@ protected override async Task OnInitializedAsync() private async Task HandleRegisterAsync() { - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) return; diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/ResetCredential.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/ResetCredential.razor.cs index db40becc..9bcaf5f7 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/ResetCredential.razor.cs +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/ResetCredential.razor.cs @@ -12,7 +12,7 @@ public partial class ResetCredential private async Task ResetPasswordAsync() { - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) { Snackbar.Add("Please fix the validation errors.", Severity.Error); diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs index 7b54009b..074e07a3 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs @@ -82,9 +82,7 @@ app.MapOpenApi(); app.MapScalarApiReference(); - using var scope = app.Services.CreateScope(); - var seedRunner = scope.ServiceProvider.GetRequiredService(); - await seedRunner.RunAsync(null); + await app.SeedUltimateAuthAsync(); } app.UseForwardedHeaders(); diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/CreateUserDialog.razor.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/CreateUserDialog.razor.cs index 61aa56fb..b10d7d9d 100644 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/CreateUserDialog.razor.cs +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/CreateUserDialog.razor.cs @@ -18,7 +18,7 @@ public partial class CreateUserDialog private async Task CreateUserAsync() { - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) return; diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/CredentialDialog.razor.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/CredentialDialog.razor.cs index 926eba3d..a9c76d1c 100644 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/CredentialDialog.razor.cs +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/CredentialDialog.razor.cs @@ -31,7 +31,7 @@ private async Task ChangePasswordAsync() if (_form is null) return; - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) { Snackbar.Add("Form is not valid.", Severity.Error); diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/ProfileDialog.razor.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/ProfileDialog.razor.cs index c0442702..c96497e8 100644 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/ProfileDialog.razor.cs +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/ProfileDialog.razor.cs @@ -72,7 +72,7 @@ private async Task SaveAsync() if (_form is not null) { - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) return; } diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/AuthorizedTestPage.razor b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/AuthorizedTestPage.razor index e5554c4e..2dd294b2 100644 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/AuthorizedTestPage.razor +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/AuthorizedTestPage.razor @@ -1,5 +1,6 @@ @page "/authorized-test" -@attribute [Authorize] +@attribute [UAuthAuthorize] +@inherits UAuthFlowPageBase diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Home.razor b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Home.razor index beac4f94..4db6dfcf 100644 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Home.razor +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Home.razor @@ -1,5 +1,5 @@ @page "/home" -@attribute [Authorize] +@attribute [UAuthAuthorize] @inherits UAuthFlowPageBase @inject IUAuthClient UAuthClient diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Register.razor.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Register.razor.cs index db73fd6a..8219cee3 100644 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Register.razor.cs +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Register.razor.cs @@ -20,7 +20,7 @@ protected override async Task OnInitializedAsync() private async Task HandleRegisterAsync() { - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) return; diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/ResetCredential.razor.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/ResetCredential.razor.cs index 726c4864..b76e12b1 100644 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/ResetCredential.razor.cs +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/ResetCredential.razor.cs @@ -12,7 +12,7 @@ public partial class ResetCredential private async Task ResetPasswordAsync() { - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) { Snackbar.Add("Please fix the validation errors.", Severity.Error); diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/AuthorizedTestPage.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/AuthorizedTestPage.razor index e5554c4e..9d78b597 100644 --- a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/AuthorizedTestPage.razor +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/AuthorizedTestPage.razor @@ -1,5 +1,6 @@ @page "/authorized-test" -@attribute [Authorize] +@attribute [UAuthAuthorize] +@inherits UAuthFlowPageBase @@ -13,7 +14,7 @@ - Go Profile + Go Home Page diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/Home.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/Home.razor index d1a9096c..b2aba719 100644 --- a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/Home.razor +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/Home.razor @@ -1,6 +1,5 @@ @page "/home" -@* To make Authorize attribute to work, add ResourceApi in Program.cs, but it affects performance significantly *@ -@* @attribute [Authorize] *@ +@attribute [UAuthAuthorize] @inherits UAuthFlowPageBase @inject IUAuthClient UAuthClient diff --git a/src/CodeBeam.UltimateAuth.Server/Services/SessionApplicationService.cs b/src/CodeBeam.UltimateAuth.Server/Services/SessionApplicationService.cs index 4b62d6ab..d7640693 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/SessionApplicationService.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/SessionApplicationService.cs @@ -164,7 +164,9 @@ public async Task RevokeUserSessionAsync(AccessContext context, UserKey userKey, var expected = session.Version; var revoked = session.Revoke(now); - await store.SaveSessionAsync(revoked, expected); + await store.ExecuteAsync(async innerCt2 => { + await store.SaveSessionAsync(revoked, expected); + }); }); await _accessOrchestrator.ExecuteAsync(context, command, ct); @@ -176,7 +178,10 @@ public async Task RevokeUserChainAsync(AccessContext context, User { var isCurrent = context.ActorChainId == chainId; var store = _storeFactory.Create(context.ResourceTenant); - await store.RevokeChainCascadeAsync(chainId, _clock.UtcNow); + + await store.ExecuteAsync(async innerCt2 => { + await store.RevokeChainCascadeAsync(chainId, _clock.UtcNow); + }); return new RevokeResult { @@ -198,15 +203,18 @@ public async Task RevokeAllChainsAsync(AccessContext context, UserKey userKey, S var command = new AccessCommand(async innerCt => { var store = _storeFactory.Create(context.ResourceTenant); - var chains = await store.GetChainsByUserAsync(userKey); - foreach (var chain in chains) - { - if (exceptChainId.HasValue && chain.ChainId == exceptChainId.Value) - continue; + await store.ExecuteAsync(async innerCt2 => { + var chains = await store.GetChainsByUserAsync(userKey); - await store.RevokeChainCascadeAsync(chain.ChainId, _clock.UtcNow); - } + foreach (var chain in chains) + { + if (exceptChainId.HasValue && chain.ChainId == exceptChainId.Value) + continue; + + await store.RevokeChainCascadeAsync(chain.ChainId, _clock.UtcNow); + } + }); }); await _accessOrchestrator.ExecuteAsync(context, command, ct); @@ -220,7 +228,9 @@ public async Task LogoutDeviceAsync(AccessContext context, Session var store = _storeFactory.Create(context.ResourceTenant); var now = _clock.UtcNow; - await store.LogoutChainAsync(currentChainId, now, innerCt); + await store.ExecuteAsync(async innerCt2 => { + await store.LogoutChainAsync(currentChainId, now, innerCt2); + }); return new RevokeResult { @@ -239,7 +249,10 @@ public async Task LogoutOtherDevicesAsync(AccessContext context, UserKey userKey var store = _storeFactory.Create(context.ResourceTenant); var now = _clock.UtcNow; - await store.RevokeOtherSessionsAsync(userKey, currentChainId, now, innerCt); + await store.ExecuteAsync(async innerCt2 => { + await store.RevokeOtherSessionsAsync(userKey, currentChainId, now, innerCt2); + }); + }); await _accessOrchestrator.ExecuteAsync(context, command, ct); @@ -252,7 +265,9 @@ public async Task LogoutAllDevicesAsync(AccessContext context, UserKey userKey, var store = _storeFactory.Create(context.ResourceTenant); var now = _clock.UtcNow; - await store.RevokeAllSessionsAsync(userKey, now, innerCt); + await store.ExecuteAsync(async innerCt2 => { + await store.RevokeAllSessionsAsync(userKey, now, innerCt2); + }); }); await _accessOrchestrator.ExecuteAsync(context, command, ct); @@ -263,7 +278,10 @@ public async Task RevokeRootAsync(AccessContext context, UserKey userKey, Cancel var command = new AccessCommand(async innerCt => { var store = _storeFactory.Create(context.ResourceTenant); - await store.RevokeRootCascadeAsync(userKey, _clock.UtcNow); + + await store.ExecuteAsync(async innerCt2 => { + await store.RevokeRootCascadeAsync(userKey, _clock.UtcNow); + }); }); await _accessOrchestrator.ExecuteAsync(context, command, ct); diff --git a/src/client/CodeBeam.UltimateAuth.Client.Blazor/Attributes/UAuthAuthorizeAttribute.cs b/src/client/CodeBeam.UltimateAuth.Client.Blazor/Attributes/UAuthAuthorizeAttribute.cs new file mode 100644 index 00000000..971364ac --- /dev/null +++ b/src/client/CodeBeam.UltimateAuth.Client.Blazor/Attributes/UAuthAuthorizeAttribute.cs @@ -0,0 +1,8 @@ +namespace CodeBeam.UltimateAuth.Client.Blazor; + +[AttributeUsage(AttributeTargets.Class)] +public sealed class UAuthAuthorizeAttribute : Attribute +{ + public string? Roles { get; set; } + public string? Permissions { get; set; } +} diff --git a/src/client/CodeBeam.UltimateAuth.Client.Blazor/Components/Base/UAuthFlowPageBase.cs b/src/client/CodeBeam.UltimateAuth.Client.Blazor/Components/Base/UAuthFlowPageBase.cs index 8f805211..c1305221 100644 --- a/src/client/CodeBeam.UltimateAuth.Client.Blazor/Components/Base/UAuthFlowPageBase.cs +++ b/src/client/CodeBeam.UltimateAuth.Client.Blazor/Components/Base/UAuthFlowPageBase.cs @@ -1,5 +1,4 @@ using CodeBeam.UltimateAuth.Core.Contracts; -using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.WebUtilities; using System.Text; using System.Text.Json; @@ -8,8 +7,6 @@ namespace CodeBeam.UltimateAuth.Client.Blazor; public abstract class UAuthFlowPageBase : UAuthReactiveComponentBase { - [Inject] protected NavigationManager Nav { get; set; } = default!; - protected AuthFlowPayload? UAuthPayload { get; private set; } protected string? ReturnUrl { get; private set; } protected bool ShouldFocus { get; private set; } diff --git a/src/client/CodeBeam.UltimateAuth.Client.Blazor/Components/Base/UAuthReactiveComponentBase.cs b/src/client/CodeBeam.UltimateAuth.Client.Blazor/Components/Base/UAuthReactiveComponentBase.cs index fb67afe8..38d8ade6 100644 --- a/src/client/CodeBeam.UltimateAuth.Client.Blazor/Components/Base/UAuthReactiveComponentBase.cs +++ b/src/client/CodeBeam.UltimateAuth.Client.Blazor/Components/Base/UAuthReactiveComponentBase.cs @@ -5,10 +5,13 @@ namespace CodeBeam.UltimateAuth.Client.Blazor; public abstract class UAuthReactiveComponentBase : ComponentBase, IDisposable { private UAuthState? _previousState; + private bool _rendered; [CascadingParameter] protected UAuthState AuthState { get; set; } = default!; + [Inject] protected NavigationManager Nav { get; set; } = default!; + /// /// Automatically re-render when UAuthState changes. /// Can be overridden to disable. @@ -31,12 +34,24 @@ protected override void OnParametersSet() AuthState.Changed += OnAuthStateChanged; _previousState = AuthState; } + + EvaluateAuthorization(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + + if (firstRender) + _rendered = true; } private void OnAuthStateChanged(UAuthStateChangeReason reason) { HandleAuthStateChanged(reason); + EvaluateAuthorization(); + if (AutoRefreshOnAuthStateChanged) _ = InvokeAsync(StateHasChanged); } @@ -48,6 +63,54 @@ protected virtual void HandleAuthStateChanged(UAuthStateChangeReason reason) { } + private void EvaluateAuthorization() + { + var attr = GetType() + .GetCustomAttributes(typeof(UAuthAuthorizeAttribute), true) + .FirstOrDefault() as UAuthAuthorizeAttribute; + + if (attr is null) + return; + + if (_rendered && !AuthState.IsAuthenticated) + { + OnUnauthorized(); + return; + } + + if (_rendered && !string.IsNullOrEmpty(attr.Roles)) + { + var roles = attr.Roles.Split(','); + + if (!roles.Any(r => AuthState.IsInRole(r.Trim()))) + { + OnForbidden(); + return; + } + } + + if (_rendered && !string.IsNullOrEmpty(attr.Permissions)) + { + var permissions = attr.Permissions.Split(','); + + if (!permissions.Any(p => AuthState.HasPermission(p.Trim()))) + { + OnForbidden(); + return; + } + } + } + + protected virtual void OnUnauthorized() + { + Nav.NavigateTo("/"); + } + + protected virtual void OnForbidden() + { + Nav.NavigateTo("/forbidden"); + } + public virtual void Dispose() { if (_previousState is not null) diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStore.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStore.cs index a1108ca4..99824242 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStore.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStore.cs @@ -11,6 +11,7 @@ internal sealed class EfCoreSessionStore : ISessionStore where TDbCo { private readonly TDbContext _db; private readonly TenantKey _tenant; + private bool _inExecution; public EfCoreSessionStore(TDbContext db, TenantExecutionContext tenant) { @@ -32,6 +33,7 @@ await strategy.ExecuteAsync(async () => try { + _inExecution = true; await action(ct); await _db.SaveChangesAsync(ct); await tx.CommitAsync(ct); @@ -46,6 +48,10 @@ await strategy.ExecuteAsync(async () => await tx.RollbackAsync(ct); throw; } + finally + { + _inExecution = false; + } }); } @@ -59,6 +65,7 @@ public async Task ExecuteAsync(Func ExecuteAsync(Func x.Tenant == _tenant && x.SessionId == session.SessionId); if (projection == null) @@ -122,6 +136,9 @@ public Task CreateSessionAsync(UAuthSession session, CancellationToken ct = defa { ct.ThrowIfCancellationRequested(); + if (!_inExecution) + throw new InvalidOperationException("Must be called inside ExecuteAsync"); + var projection = session.ToProjection(); if (session.Version != 0) @@ -136,6 +153,9 @@ public async Task RevokeSessionAsync(AuthSessionId sessionId, DateTimeOffs { ct.ThrowIfCancellationRequested(); + if (!_inExecution) + throw new InvalidOperationException("Must be called inside ExecuteAsync"); + var projection = await DbSetSession.SingleOrDefaultAsync(x => x.Tenant == _tenant && x.SessionId == sessionId, ct); if (projection is null || projection.RevokedAt is not null) @@ -152,6 +172,9 @@ public async Task RevokeAllSessionsAsync(UserKey user, DateTimeOffset at, Cancel { ct.ThrowIfCancellationRequested(); + if (!_inExecution) + throw new InvalidOperationException("Must be called inside ExecuteAsync"); + var chains = await DbSetChain .Where(x => x.Tenant == _tenant && x.UserKey == user) .ToListAsync(ct); @@ -188,6 +211,9 @@ public async Task RevokeOtherSessionsAsync(UserKey user, SessionChainId keepChai { ct.ThrowIfCancellationRequested(); + if (!_inExecution) + throw new InvalidOperationException("Must be called inside ExecuteAsync"); + var chains = await DbSetChain .Where(x => x.Tenant == _tenant && x.UserKey == user && x.ChainId != keepChain) .ToListAsync(ct); @@ -267,6 +293,9 @@ public async Task SaveChainAsync(UAuthSessionChain chain, long expectedVersion, { ct.ThrowIfCancellationRequested(); + if (!_inExecution) + throw new InvalidOperationException("Must be called inside ExecuteAsync"); + var projection = DbSetChain.Local.FirstOrDefault(x => x.Tenant == _tenant && x.ChainId == chain.ChainId); if (projection is null) @@ -289,6 +318,9 @@ public Task CreateChainAsync(UAuthSessionChain chain, CancellationToken ct = def { ct.ThrowIfCancellationRequested(); + if (!_inExecution) + throw new InvalidOperationException("Must be called inside ExecuteAsync"); + if (chain.Version != 0) throw new InvalidOperationException("New chain must have version 0."); @@ -304,6 +336,9 @@ public async Task RevokeChainAsync(SessionChainId chainId, DateTimeOffset at, Ca { ct.ThrowIfCancellationRequested(); + if (!_inExecution) + throw new InvalidOperationException("Must be called inside ExecuteAsync"); + var projection = await DbSetChain .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.ChainId == chainId, ct); @@ -319,6 +354,9 @@ public async Task LogoutChainAsync(SessionChainId chainId, DateTimeOffset at, Ca { ct.ThrowIfCancellationRequested(); + if (!_inExecution) + throw new InvalidOperationException("Must be called inside ExecuteAsync"); + var chainProjection = await DbSetChain .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.ChainId == chainId, ct); @@ -352,6 +390,9 @@ public async Task RevokeOtherChainsAsync(UserKey userKey, SessionChainId current { ct.ThrowIfCancellationRequested(); + if (!_inExecution) + throw new InvalidOperationException("Must be called inside ExecuteAsync"); + var projections = await DbSetChain .Where(x => x.Tenant == _tenant && @@ -372,6 +413,9 @@ public async Task RevokeAllChainsAsync(UserKey userKey, DateTimeOffset at, Cance { ct.ThrowIfCancellationRequested(); + if (!_inExecution) + throw new InvalidOperationException("Must be called inside ExecuteAsync"); + var projections = await DbSetChain .Where(x => x.Tenant == _tenant && @@ -402,6 +446,9 @@ public async Task SetActiveSessionIdAsync(SessionChainId chainId, AuthSessionId { ct.ThrowIfCancellationRequested(); + if (!_inExecution) + throw new InvalidOperationException("Must be called inside ExecuteAsync"); + var projection = DbSetChain.Local.FirstOrDefault(x => x.Tenant == _tenant && x.ChainId == chainId); if (projection is null) @@ -428,6 +475,9 @@ public async Task SaveRootAsync(UAuthSessionRoot root, long expectedVersion, Can { ct.ThrowIfCancellationRequested(); + if (!_inExecution) + throw new InvalidOperationException("Must be called inside ExecuteAsync"); + var projection = await DbSetRoot .SingleOrDefaultAsync(x => x.Tenant == _tenant && @@ -448,6 +498,9 @@ public Task CreateRootAsync(UAuthSessionRoot root, CancellationToken ct = defaul { ct.ThrowIfCancellationRequested(); + if (!_inExecution) + throw new InvalidOperationException("Must be called inside ExecuteAsync"); + if (root.Version != 0) throw new InvalidOperationException("New root must have version 0."); @@ -462,6 +515,9 @@ public async Task RevokeRootAsync(UserKey userKey, DateTimeOffset at, Cancellati { ct.ThrowIfCancellationRequested(); + if (!_inExecution) + throw new InvalidOperationException("Must be called inside ExecuteAsync"); + var projection = await DbSetRoot .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.UserKey == userKey, ct); @@ -540,6 +596,9 @@ public async Task RemoveSessionAsync(AuthSessionId sessionId, CancellationToken { ct.ThrowIfCancellationRequested(); + if (!_inExecution) + throw new InvalidOperationException("Must be called inside ExecuteAsync"); + var projection = await DbSetSession.SingleOrDefaultAsync(x => x.Tenant == _tenant && x.SessionId == sessionId, ct); if (projection is null) @@ -552,6 +611,9 @@ public async Task RevokeChainCascadeAsync(SessionChainId chainId, DateTimeOffset { ct.ThrowIfCancellationRequested(); + if (!_inExecution) + throw new InvalidOperationException("Must be called inside ExecuteAsync"); + var chainProjection = await DbSetChain .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.ChainId == chainId, ct); @@ -581,6 +643,9 @@ public async Task RevokeRootCascadeAsync(UserKey userKey, DateTimeOffset at, Can { ct.ThrowIfCancellationRequested(); + if (!_inExecution) + throw new InvalidOperationException("Must be called inside ExecuteAsync"); + var rootProjection = await DbSetRoot .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.UserKey == userKey, ct); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreSessionStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreSessionStoreTests.cs index 6eb7d7c9..504bcf49 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreSessionStoreTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreSessionStoreTests.cs @@ -261,6 +261,8 @@ public async Task Revoke_Session_Should_Work() ClaimsSnapshot.Empty, SessionMetadata.Empty); + bool revoked = false; + await store.ExecuteAsync(async ct => { await store.CreateRootAsync(root, ct); @@ -268,7 +270,10 @@ await store.ExecuteAsync(async ct => await store.CreateSessionAsync(session, ct); }); - var revoked = await store.RevokeSessionAsync(sessionId, DateTimeOffset.UtcNow); + await store.ExecuteAsync(async ct => + { + revoked = await store.RevokeSessionAsync(sessionId, DateTimeOffset.UtcNow); + }); Assert.True(revoked); }