diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 25c396c5..39c4c0a7 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,15 +1,15 @@
# 🚀 Pull Request
-Thank you for contributing to **UltimateAuth**!
-Please complete the following checklist to help us review your PR effectively.
+
+
## 📘 Summary
-Describe what this PR does and why it’s needed.
+
## 🔍 Details
-Explain any important implementation details, design decisions, or considerations.
+
## 🧩 Related Issues
diff --git a/README.md b/README.md
index af08c023..9e7a8215 100644
--- a/README.md
+++ b/README.md
@@ -33,7 +33,7 @@ UltimateAuth is an open-source auth framework with platform-level capabilities t
| Phase | Version | Scope | Status | Release Date |
| ----------------------- | ------------- | ----------------------------------------- | -------------- | ------------ |
| First Preview | 0.1.0-preview | "Stable" Preview Core | ✅ Completed | 07.04.2026 |
-| First Release* | 0.1.0 | Fully Documented & Quality Tested | ✅ Completed | 04.10.2026 |
+| First Release* | 0.1.0 | Fully Documented & Quality Tested | ✅ Completed | 08.10.2026 |
| Product Expansion | 0.2.0 | Full Auth Modes | 🟡 In Progress | Q4 2026 |
| Security Expansion | 0.3.0 | MFA, Reauth, Rate Limiting | 🟡 In Progress | Q4 2026 |
| Infrastructure Expansion| 0.4.0 | Redis, Distributed Cache, Password Hasher | 🔜 Planned | Q1 2027 |
diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20260412205559_InitUltimateAuth.Designer.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20261004164613_001_Initial.Designer.cs
similarity index 98%
rename from samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20260412205559_InitUltimateAuth.Designer.cs
rename to samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20261004164613_001_Initial.Designer.cs
index 96400072..3f1152e9 100644
--- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20260412205559_InitUltimateAuth.Designer.cs
+++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20261004164613_001_Initial.Designer.cs
@@ -11,14 +11,14 @@
namespace CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.Migrations
{
[DbContext(typeof(UAuthDbContext))]
- [Migration("20260412205559_InitUltimateAuth")]
- partial class InitUltimateAuth
+ [Migration("20261004164613_001_Initial")]
+ partial class _001_Initial
{
///
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
- modelBuilder.HasAnnotation("ProductVersion", "10.0.5");
+ modelBuilder.HasAnnotation("ProductVersion", "10.0.12");
modelBuilder.Entity("CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.AuthenticationSecurityStateProjection", b =>
{
@@ -434,8 +434,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
b.HasIndex("Tenant", "RootId")
.IsUnique();
- b.HasIndex("Tenant", "UserKey")
- .IsUnique();
+ b.HasIndex("Tenant", "UserKey");
+
+ b.HasIndex("Tenant", "UserKey", "RevokedAt");
b.ToTable("UAuth_SessionRoots", (string)null);
});
diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20260412205559_InitUltimateAuth.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20261004164613_001_Initial.cs
similarity index 98%
rename from samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20260412205559_InitUltimateAuth.cs
rename to samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20261004164613_001_Initial.cs
index 35dc371f..77f4d9bf 100644
--- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20260412205559_InitUltimateAuth.cs
+++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20261004164613_001_Initial.cs
@@ -6,7 +6,7 @@
namespace CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.Migrations
{
///
- public partial class InitUltimateAuth : Migration
+ public partial class _001_Initial : Migration
{
///
protected override void Up(MigrationBuilder migrationBuilder)
@@ -432,8 +432,12 @@ protected override void Up(MigrationBuilder migrationBuilder)
migrationBuilder.CreateIndex(
name: "IX_UAuth_SessionRoots_Tenant_UserKey",
table: "UAuth_SessionRoots",
- columns: new[] { "Tenant", "UserKey" },
- unique: true);
+ columns: new[] { "Tenant", "UserKey" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_UAuth_SessionRoots_Tenant_UserKey_RevokedAt",
+ table: "UAuth_SessionRoots",
+ columns: new[] { "Tenant", "UserKey", "RevokedAt" });
migrationBuilder.CreateIndex(
name: "IX_UAuth_Sessions_Tenant_ChainId",
diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/UAuthDbContextModelSnapshot.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/UAuthDbContextModelSnapshot.cs
index 4000ca4d..bef97e5f 100644
--- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/UAuthDbContextModelSnapshot.cs
+++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/UAuthDbContextModelSnapshot.cs
@@ -15,7 +15,7 @@ partial class UAuthDbContextModelSnapshot : ModelSnapshot
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
- modelBuilder.HasAnnotation("ProductVersion", "10.0.5");
+ modelBuilder.HasAnnotation("ProductVersion", "10.0.12");
modelBuilder.Entity("CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.AuthenticationSecurityStateProjection", b =>
{
@@ -431,8 +431,9 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.HasIndex("Tenant", "RootId")
.IsUnique();
- b.HasIndex("Tenant", "UserKey")
- .IsUnique();
+ b.HasIndex("Tenant", "UserKey");
+
+ b.HasIndex("Tenant", "UserKey", "RevokedAt");
b.ToTable("UAuth_SessionRoots", (string)null);
});
diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Program.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Program.cs
index 0de4d2c1..fe875991 100644
--- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Program.cs
+++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Program.cs
@@ -39,7 +39,7 @@
//o.Token.RefreshTokenLifetime = TimeSpan.FromSeconds(32);
o.Login.MaxFailedAttempts = 2;
o.Login.LockoutDuration = TimeSpan.FromSeconds(10);
- o.Identifiers.AllowMultipleUsernames = true;
+ o.Identifiers.Behavior.AllowMultipleUsernames = true;
o.UserProfile.EnableMultiProfile = true;
})
.AddUltimateAuthEntityFrameworkCore(db =>
diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db
index 4e86411b..c9cf4b4e 100644
Binary files a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db and b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db differ
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 274b15c6..86e2b19d 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 4eab4800..932093f3 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 1182dcf7..54e3346e 100644
--- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs
+++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs
@@ -37,7 +37,7 @@
//o.Token.RefreshTokenLifetime = TimeSpan.FromSeconds(32);
o.Login.MaxFailedAttempts = 2;
o.Login.LockoutDuration = TimeSpan.FromSeconds(10);
- o.Identifiers.AllowMultipleUsernames = true;
+ o.Identifiers.Behavior.AllowMultipleUsernames = true;
o.UserProfile.EnableMultiProfile = true;
})
.AddUltimateAuthInMemory()
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260406192328_InitUltimateAuth.Designer.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20261004164018_001_Initial.Designer.cs
similarity index 98%
rename from samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260406192328_InitUltimateAuth.Designer.cs
rename to samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20261004164018_001_Initial.Designer.cs
index 1302213b..7df8d5ae 100644
--- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260406192328_InitUltimateAuth.Designer.cs
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20261004164018_001_Initial.Designer.cs
@@ -11,14 +11,14 @@
namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Migrations
{
[DbContext(typeof(UAuthDbContext))]
- [Migration("20260406192328_InitUltimateAuth")]
- partial class InitUltimateAuth
+ [Migration("20261004164018_001_Initial")]
+ partial class _001_Initial
{
///
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
- modelBuilder.HasAnnotation("ProductVersion", "10.0.5");
+ modelBuilder.HasAnnotation("ProductVersion", "10.0.12");
modelBuilder.Entity("CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.AuthenticationSecurityStateProjection", b =>
{
@@ -434,8 +434,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
b.HasIndex("Tenant", "RootId")
.IsUnique();
- b.HasIndex("Tenant", "UserKey")
- .IsUnique();
+ b.HasIndex("Tenant", "UserKey");
+
+ b.HasIndex("Tenant", "UserKey", "RevokedAt");
b.ToTable("UAuth_SessionRoots", (string)null);
});
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260406192328_InitUltimateAuth.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20261004164018_001_Initial.cs
similarity index 98%
rename from samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260406192328_InitUltimateAuth.cs
rename to samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20261004164018_001_Initial.cs
index bd4101fd..0d43c8e2 100644
--- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260406192328_InitUltimateAuth.cs
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20261004164018_001_Initial.cs
@@ -6,7 +6,7 @@
namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Migrations
{
///
- public partial class InitUltimateAuth : Migration
+ public partial class _001_Initial : Migration
{
///
protected override void Up(MigrationBuilder migrationBuilder)
@@ -432,8 +432,12 @@ protected override void Up(MigrationBuilder migrationBuilder)
migrationBuilder.CreateIndex(
name: "IX_UAuth_SessionRoots_Tenant_UserKey",
table: "UAuth_SessionRoots",
- columns: new[] { "Tenant", "UserKey" },
- unique: true);
+ columns: new[] { "Tenant", "UserKey" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_UAuth_SessionRoots_Tenant_UserKey_RevokedAt",
+ table: "UAuth_SessionRoots",
+ columns: new[] { "Tenant", "UserKey", "RevokedAt" });
migrationBuilder.CreateIndex(
name: "IX_UAuth_Sessions_Tenant_ChainId",
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/UAuthDbContextModelSnapshot.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/UAuthDbContextModelSnapshot.cs
index af13077f..65798d28 100644
--- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/UAuthDbContextModelSnapshot.cs
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/UAuthDbContextModelSnapshot.cs
@@ -15,7 +15,7 @@ partial class UAuthDbContextModelSnapshot : ModelSnapshot
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
- modelBuilder.HasAnnotation("ProductVersion", "10.0.5");
+ modelBuilder.HasAnnotation("ProductVersion", "10.0.12");
modelBuilder.Entity("CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.AuthenticationSecurityStateProjection", b =>
{
@@ -431,8 +431,9 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.HasIndex("Tenant", "RootId")
.IsUnique();
- b.HasIndex("Tenant", "UserKey")
- .IsUnique();
+ b.HasIndex("Tenant", "UserKey");
+
+ b.HasIndex("Tenant", "UserKey", "RevokedAt");
b.ToTable("UAuth_SessionRoots", (string)null);
});
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 99fe67c7..33b5d2af 100644
--- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Program.cs
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Program.cs
@@ -51,7 +51,7 @@
//o.Token.RefreshTokenLifetime = TimeSpan.FromSeconds(32);
o.Login.MaxFailedAttempts = 2;
o.Login.LockoutDuration = TimeSpan.FromSeconds(10);
- o.Identifiers.AllowMultipleUsernames = true;
+ o.Identifiers.Behavior.AllowMultipleUsernames = true;
})
.AddUltimateAuthEntityFrameworkCore(db =>
{
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 4e86411b..c9cf4b4e 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
index 1c694283..9f70b715 100644
Binary files a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-shm 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
index b5380fc8..9f632717 100644
Binary files a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-wal and b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-wal differ
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs
index 074e07a3..b939d487 100644
--- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs
@@ -50,7 +50,7 @@
//o.Token.RefreshTokenLifetime = TimeSpan.FromSeconds(32);
o.Login.MaxFailedAttempts = 2;
o.Login.LockoutDuration = TimeSpan.FromSeconds(10);
- o.Identifiers.AllowMultipleUsernames = true;
+ o.Identifiers.Behavior.AllowMultipleUsernames = true;
})
.AddUltimateAuthInMemory();
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/IUAuthAtomicExecutor.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/IUAuthAtomicExecutor.cs
new file mode 100644
index 00000000..1a8f9cec
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/IUAuthAtomicExecutor.cs
@@ -0,0 +1,8 @@
+namespace CodeBeam.UltimateAuth.Core.Abstractions;
+
+public interface IUAuthAtomicExecutor
+{
+ Task ExecuteAsync(Func operation, CancellationToken ct = default);
+
+ Task ExecuteAsync(Func> operation, CancellationToken ct = default);
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/AssemblyVisibility.cs b/src/CodeBeam.UltimateAuth.Core/AssemblyVisibility.cs
index a7a9e51d..b346c2c9 100644
--- a/src/CodeBeam.UltimateAuth.Core/AssemblyVisibility.cs
+++ b/src/CodeBeam.UltimateAuth.Core/AssemblyVisibility.cs
@@ -8,3 +8,4 @@
[assembly: InternalsVisibleTo("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore")]
[assembly: InternalsVisibleTo("CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore")]
[assembly: InternalsVisibleTo("CodeBeam.UltimateAuth.Tests.Unit")]
+[assembly: InternalsVisibleTo("CodeBeam.UltimateAuth.Tests.Integration")]
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Common/UniquenessScope.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Common/UniquenessScope.cs
new file mode 100644
index 00000000..0ab50a03
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Common/UniquenessScope.cs
@@ -0,0 +1,24 @@
+namespace CodeBeam.UltimateAuth.Core.Contracts;
+
+// TODO: Add global scope
+///
+/// Defines the scope of uniqueness for an identifier.
+///
+public enum UniquenessScope
+{
+ ///
+ /// No uniqueness is enforced.
+ /// Note that users still can't create duplicate identifiers.
+ ///
+ None = 0,
+
+ ///
+ /// Uniqueness is enforced within a single user.
+ ///
+ WithinUser = 10,
+
+ ///
+ /// Uniqueness is enforced within a tenant.
+ ///
+ Tenant = 20
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Defaults/UAuthActions.cs b/src/CodeBeam.UltimateAuth.Core/Defaults/UAuthActions.cs
index 337cc47c..4fc140d6 100644
--- a/src/CodeBeam.UltimateAuth.Core/Defaults/UAuthActions.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Defaults/UAuthActions.cs
@@ -94,6 +94,7 @@ public static class UserIdentifiers
public const string VerifyAdmin = "users.identifiers.verify.admin";
public const string DeleteSelf = "users.identifiers.delete.self";
public const string DeleteAdmin = "users.identifiers.delete.admin";
+ public const string CheckAvailability = "users.identifiers.check-availability.anonymous";
}
public static class Credentials
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUserEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUserEndpointHandler.cs
index fa21263e..f5638b2e 100644
--- a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUserEndpointHandler.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUserEndpointHandler.cs
@@ -42,4 +42,5 @@ public interface IUserEndpointHandler
Task UnsetPrimaryUserIdentifierAdminAsync(UserKey userKey, HttpContext ctx);
Task VerifyUserIdentifierAdminAsync(UserKey userKey, HttpContext ctx);
Task DeleteUserIdentifierAdminAsync(UserKey userKey, HttpContext ctx);
+ Task CheckIdentifierAvailabilityAsync(HttpContext ctx);
}
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs
index 4ba6e369..686deff8 100644
--- a/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs
@@ -311,6 +311,10 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options
if (Enabled(UAuthActions.UserIdentifiers.DeleteAdmin))
adminUsers.MapPost("/{userKey}/identifiers/delete", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx)
=> await h.DeleteUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement));
+
+ if (Enabled(UAuthActions.UserIdentifiers.CheckAvailability))
+ self.MapPost("/identifiers/check-availability", async ([FromServices] IUserEndpointHandler h, HttpContext ctx)
+ => await h.CheckIdentifierAvailabilityAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement));
}
if (options.Endpoints.Credentials != false)
diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs
index 20cf8a70..95b36ba5 100644
--- a/src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs
@@ -236,7 +236,8 @@ private static IServiceCollection AddUltimateAuthServerInternal(this IServiceCol
services.TryAddScoped();
services.TryAddScoped();
services.TryAddScoped();
- services.TryAddScoped();
+ services.TryAddScoped();
+ services.TryAddScoped();
services.TryAddScoped();
services.TryAddScoped();
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Normalizer/IdentifierNormalizer.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Normalizer/IdentifierNormalizer.cs
index 0fd00be7..de6bfb2b 100644
--- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Normalizer/IdentifierNormalizer.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Normalizer/IdentifierNormalizer.cs
@@ -13,7 +13,7 @@ public sealed class IdentifierNormalizer : IIdentifierNormalizer
public IdentifierNormalizer(IOptions options)
{
- _options = options.Value.LoginIdentifiers.Normalization;
+ _options = options.Value.Identifiers.Normalization;
}
public NormalizedIdentifier Normalize(UserIdentifierType type, string value)
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/IIdentifierValidator.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/IIdentifierValidator.cs
deleted file mode 100644
index 8054fa86..00000000
--- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/IIdentifierValidator.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using CodeBeam.UltimateAuth.Core.Contracts;
-using CodeBeam.UltimateAuth.Users.Contracts;
-
-namespace CodeBeam.UltimateAuth.Server.Infrastructure;
-
-public interface IIdentifierValidator
-{
- Task ValidateAsync(AccessContext context, UserIdentifierInfo identifier, CancellationToken ct = default);
-}
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/IUserIdentifierValidator.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/IUserIdentifierValidator.cs
new file mode 100644
index 00000000..f045ef9b
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/IUserIdentifierValidator.cs
@@ -0,0 +1,9 @@
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Users.Contracts;
+
+namespace CodeBeam.UltimateAuth.Server.Infrastructure;
+
+public interface IUserIdentifierValidator
+{
+ Task ValidateAsync(AccessContext context, UserIdentifierInfo identifier, CancellationToken ct = default);
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/IUserProfileValidator.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/IUserProfileValidator.cs
new file mode 100644
index 00000000..5d32d436
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/IUserProfileValidator.cs
@@ -0,0 +1,9 @@
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Users.Contracts;
+
+namespace CodeBeam.UltimateAuth.Server.Infrastructure;
+
+public interface IUserProfileValidator
+{
+ Task ValidateAsync(AccessContext context, UserProfileInfo profile, CancellationToken ct = default);
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserCreateValidator.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserCreateValidator.cs
index b3441fbe..3d16c55a 100644
--- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserCreateValidator.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserCreateValidator.cs
@@ -6,11 +6,13 @@ namespace CodeBeam.UltimateAuth.Server.Infrastructure;
public sealed class UserCreateValidator : IUserCreateValidator
{
- private readonly IIdentifierValidator _identifierValidator;
+ private readonly IUserIdentifierValidator _identifierValidator;
+ private readonly IUserProfileValidator _profileValidator;
- public UserCreateValidator(IIdentifierValidator identifierValidator)
+ public UserCreateValidator(IUserIdentifierValidator identifierValidator, IUserProfileValidator profileValidator)
{
_identifierValidator = identifierValidator;
+ _profileValidator = profileValidator;
}
public async Task ValidateAsync(AccessContext context, CreateUserRequest request, CancellationToken ct = default)
@@ -57,6 +59,30 @@ public async Task ValidateAsync(AccessContext context
errors.AddRange(r.Errors);
}
+ var effectiveDisplayName =
+ request.DisplayName
+ ?? request.UserName
+ ?? request.Email
+ ?? request.Phone;
+
+ var profileValidation = await _profileValidator.ValidateAsync(context,
+ new UserProfileInfo
+ {
+ ProfileKey = ProfileKey.Default,
+ FirstName = request.FirstName,
+ LastName = request.LastName,
+ DisplayName = effectiveDisplayName,
+ BirthDate = request.BirthDate,
+ Gender = request.Gender,
+ Bio = request.Bio,
+ Language = request.Language,
+ TimeZone = request.TimeZone,
+ Culture = request.Culture
+ },
+ ct);
+
+ errors.AddRange(profileValidation.Errors);
+
if (errors.Count == 0)
return UserCreateValidatorResult.Success();
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/IdentifierValidator.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserIdentifierValidator.cs
similarity index 83%
rename from src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/IdentifierValidator.cs
rename to src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserIdentifierValidator.cs
index 627c73e1..78373a9a 100644
--- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/IdentifierValidator.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserIdentifierValidator.cs
@@ -6,16 +6,16 @@
namespace CodeBeam.UltimateAuth.Server.Infrastructure;
-public sealed class IdentifierValidator : IIdentifierValidator
+public sealed class UserIdentifierValidator : IUserIdentifierValidator
{
private readonly UAuthIdentifierValidationOptions _options;
- public IdentifierValidator(IOptions options)
+ public UserIdentifierValidator(IOptions options)
{
_options = options.Value.IdentifierValidation;
}
- public Task ValidateAsync(AccessContext context, UserIdentifierInfo identifier, CancellationToken ct = default)
+ public Task ValidateAsync(AccessContext context, UserIdentifierInfo identifier, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
@@ -24,7 +24,7 @@ public Task ValidateAsync(AccessContext context, Use
if (string.IsNullOrWhiteSpace(identifier.Value))
{
errors.Add(new("identifier_empty"));
- return Task.FromResult(IdentifierValidationResult.Failed(errors));
+ return Task.FromResult(UserIdentifierValidationResult.Failed(errors));
}
identifier.Value = identifier.Value.Trim();
@@ -45,9 +45,9 @@ public Task ValidateAsync(AccessContext context, Use
}
if (errors.Count == 0)
- return Task.FromResult(IdentifierValidationResult.Success());
+ return Task.FromResult(UserIdentifierValidationResult.Success());
- return Task.FromResult(IdentifierValidationResult.Failed(errors));
+ return Task.FromResult(UserIdentifierValidationResult.Failed(errors));
}
private void ValidateUsername(string username, List errors)
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserProfileValidator.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserProfileValidator.cs
new file mode 100644
index 00000000..7d9ac809
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserProfileValidator.cs
@@ -0,0 +1,14 @@
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Users.Contracts;
+
+namespace CodeBeam.UltimateAuth.Server.Infrastructure;
+
+public sealed class UserProfileValidator : IUserProfileValidator
+{
+ public Task ValidateAsync(AccessContext context, UserProfileInfo profile, CancellationToken ct = default)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ return Task.FromResult(UserProfileValidationResult.Success());
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Options/UAuthIdentifierBehaviorOptions.cs b/src/CodeBeam.UltimateAuth.Server/Options/UAuthIdentifierBehaviorOptions.cs
new file mode 100644
index 00000000..cbe7e9ee
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Options/UAuthIdentifierBehaviorOptions.cs
@@ -0,0 +1,31 @@
+namespace CodeBeam.UltimateAuth.Server.Options;
+
+public sealed class UAuthIdentifierBehaviorOptions
+{
+ public bool AllowUsernameChange { get; set; } = true;
+
+ public bool AllowMultipleUsernames { get; set; } = false;
+ public bool AllowMultipleEmail { get; set; } = true;
+ public bool AllowMultiplePhone { get; set; } = true;
+
+ public bool RequireUsernameIdentifier { get; set; } = false;
+
+ public bool RequireEmailVerification { get; set; } = false;
+ public bool RequirePhoneVerification { get; set; } = false;
+
+ public bool AllowAdminOverride { get; set; } = true;
+ public bool AllowUserOverride { get; set; } = true;
+
+ internal UAuthIdentifierBehaviorOptions Clone() => new()
+ {
+ AllowUsernameChange = AllowUsernameChange,
+ AllowMultipleUsernames = AllowMultipleUsernames,
+ AllowMultipleEmail = AllowMultipleEmail,
+ AllowMultiplePhone = AllowMultiplePhone,
+ RequireUsernameIdentifier = RequireUsernameIdentifier,
+ RequireEmailVerification = RequireEmailVerification,
+ RequirePhoneVerification = RequirePhoneVerification,
+ AllowAdminOverride = AllowAdminOverride,
+ AllowUserOverride = AllowUserOverride
+ };
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Options/UAuthIdentifierNormalizationOptions.cs b/src/CodeBeam.UltimateAuth.Server/Options/UAuthIdentifierNormalizationOptions.cs
new file mode 100644
index 00000000..0b8d5f95
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Options/UAuthIdentifierNormalizationOptions.cs
@@ -0,0 +1,17 @@
+using CodeBeam.UltimateAuth.Core.Contracts;
+
+namespace CodeBeam.UltimateAuth.Server.Options;
+
+public sealed class UAuthIdentifierNormalizationOptions
+{
+ public CaseHandling UsernameCase { get; set; } = CaseHandling.ToLower;
+ public CaseHandling EmailCase { get; set; } = CaseHandling.ToLower;
+ public CaseHandling CustomCase { get; set; } = CaseHandling.Preserve;
+
+ internal UAuthIdentifierNormalizationOptions Clone() => new()
+ {
+ UsernameCase = UsernameCase,
+ EmailCase = EmailCase,
+ CustomCase = CustomCase
+ };
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Options/UAuthIdentifierOptions.cs b/src/CodeBeam.UltimateAuth.Server/Options/UAuthIdentifierOptions.cs
index 1d7c9c6a..cde7676b 100644
--- a/src/CodeBeam.UltimateAuth.Server/Options/UAuthIdentifierOptions.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Options/UAuthIdentifierOptions.cs
@@ -2,28 +2,19 @@
public sealed class UAuthIdentifierOptions
{
- public bool AllowUsernameChange { get; set; } = true;
- public bool AllowMultipleUsernames { get; set; } = false;
- public bool AllowMultipleEmail { get; set; } = true;
- public bool AllowMultiplePhone { get; set; } = true;
+ public UAuthIdentifierBehaviorOptions Behavior { get; set; } = new();
- public bool RequireUsernameIdentifier { get; set; } = false;
- public bool RequireEmailVerification { get; set; } = false;
- public bool RequirePhoneVerification { get; set; } = false;
+ public UAuthIdentifierValidationOptions Validation { get; set; } = new();
- public bool AllowAdminOverride { get; set; } = true;
- public bool AllowUserOverride { get; set; } = true;
+ public UAuthIdentifierNormalizationOptions Normalization { get; set; } = new();
+
+ public UAuthIdentifierUniquenessOptions Uniqueness { get; set; } = new();
internal UAuthIdentifierOptions Clone() => new()
{
- AllowUsernameChange = AllowUsernameChange,
- AllowMultipleUsernames = AllowMultipleUsernames,
- AllowMultipleEmail = AllowMultipleEmail,
- AllowMultiplePhone = AllowMultiplePhone,
- RequireUsernameIdentifier = RequireUsernameIdentifier,
- RequireEmailVerification = RequireEmailVerification,
- RequirePhoneVerification = RequirePhoneVerification,
- AllowAdminOverride = AllowAdminOverride,
- AllowUserOverride = AllowUserOverride
+ Behavior = Behavior.Clone(),
+ Validation = Validation.Clone(),
+ Normalization = Normalization.Clone(),
+ Uniqueness = Uniqueness.Clone()
};
}
diff --git a/src/CodeBeam.UltimateAuth.Server/Options/UAuthIdentifierUniquenessOptions.cs b/src/CodeBeam.UltimateAuth.Server/Options/UAuthIdentifierUniquenessOptions.cs
new file mode 100644
index 00000000..b4886616
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Options/UAuthIdentifierUniquenessOptions.cs
@@ -0,0 +1,23 @@
+using CodeBeam.UltimateAuth.Core.Contracts;
+
+namespace CodeBeam.UltimateAuth.Server.Options;
+
+// TODO(security):
+// Validate identifier uniqueness against login identifier configuration.
+// Identifier types used for direct login resolution should normally be tenant-unique unless a custom resolver explicitly guarantees
+// unambiguous user resolution.
+public sealed class UAuthIdentifierUniquenessOptions
+{
+ public UniquenessScope Username { get; set; } = UniquenessScope.Tenant;
+ public UniquenessScope Email { get; set; } = UniquenessScope.Tenant;
+ public UniquenessScope Phone { get; set; } = UniquenessScope.Tenant;
+ public UniquenessScope Custom { get; set; } = UniquenessScope.Tenant;
+
+ internal UAuthIdentifierUniquenessOptions Clone() => new()
+ {
+ Username = Username,
+ Email = Email,
+ Phone = Phone,
+ Custom = Custom
+ };
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Options/UAuthLoginIdentifierOptions.cs b/src/CodeBeam.UltimateAuth.Server/Options/UAuthLoginIdentifierOptions.cs
index 07df6483..d94fbf3c 100644
--- a/src/CodeBeam.UltimateAuth.Server/Options/UAuthLoginIdentifierOptions.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Options/UAuthLoginIdentifierOptions.cs
@@ -1,5 +1,4 @@
-using CodeBeam.UltimateAuth.Core.Contracts;
-using CodeBeam.UltimateAuth.Users.Contracts;
+using CodeBeam.UltimateAuth.Users.Contracts;
namespace CodeBeam.UltimateAuth.Server.Options;
public sealed class UAuthLoginIdentifierOptions
@@ -18,10 +17,6 @@ public sealed class UAuthLoginIdentifierOptions
public bool EnableCustomResolvers { get; set; } = true;
public bool CustomResolversFirst { get; set; } = true;
- public UAuthIdentifierNormalizationOptions Normalization { get; set; } = new();
-
- public bool EnforceGlobalUniquenessForAllIdentifiers { get; set; } = false;
-
internal UAuthLoginIdentifierOptions Clone() => new()
{
AllowedTypes = new HashSet(AllowedTypes),
@@ -29,21 +24,5 @@ public sealed class UAuthLoginIdentifierOptions
RequireVerificationForPhone = RequireVerificationForPhone,
EnableCustomResolvers = EnableCustomResolvers,
CustomResolversFirst = CustomResolversFirst,
- EnforceGlobalUniquenessForAllIdentifiers = EnforceGlobalUniquenessForAllIdentifiers,
- Normalization = Normalization.Clone()
- };
-}
-
-public sealed class UAuthIdentifierNormalizationOptions
-{
- public CaseHandling UsernameCase { get; set; } = CaseHandling.ToLower;
- public CaseHandling EmailCase { get; set; } = CaseHandling.ToLower;
- public CaseHandling CustomCase { get; set; } = CaseHandling.Preserve;
-
- internal UAuthIdentifierNormalizationOptions Clone() => new()
- {
- UsernameCase = UsernameCase,
- EmailCase = EmailCase,
- CustomCase = CustomCase
};
}
diff --git a/src/CodeBeam.UltimateAuth.Server/Options/Validators/UAuthServerUserIdentifierOptionsValidator.cs b/src/CodeBeam.UltimateAuth.Server/Options/Validators/UAuthServerUserIdentifierOptionsValidator.cs
index 3e8ce27b..b66de941 100644
--- a/src/CodeBeam.UltimateAuth.Server/Options/Validators/UAuthServerUserIdentifierOptionsValidator.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Options/Validators/UAuthServerUserIdentifierOptionsValidator.cs
@@ -6,7 +6,7 @@ public sealed class UAuthServerUserIdentifierOptionsValidator : IValidateOptions
{
public ValidateOptionsResult Validate(string? name, UAuthServerOptions options)
{
- if (!options.Identifiers.AllowAdminOverride && !options.Identifiers.AllowUserOverride)
+ if (!options.Identifiers.Behavior.AllowAdminOverride && !options.Identifiers.Behavior.AllowUserOverride)
{
return ValidateOptionsResult.Fail("Both AllowAdminOverride and AllowUserOverride cannot be false. " +
"At least one actor must be able to manage user identifiers.");
diff --git a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs
index 1a9db677..1445aada 100644
--- a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs
+++ b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs
@@ -1,4 +1,5 @@
using CodeBeam.UltimateAuth.Core.Abstractions;
+using CodeBeam.UltimateAuth.EntityFrameworkCore.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
@@ -12,7 +13,9 @@ public static IServiceCollection AddUltimateAuthAuthenticationEntityFrameworkCor
{
services.AddDbContext(configureDb);
}
-
+
+ services.AddUltimateAuthEntityFrameworkCore();
+
services.AddScoped>();
return services;
}
diff --git a/src/authentication/CodeBeam.UltimateAuth.Authentication.InMemory/ServiceCollectionExtensions.cs b/src/authentication/CodeBeam.UltimateAuth.Authentication.InMemory/ServiceCollectionExtensions.cs
index cdab2eec..2020a87c 100644
--- a/src/authentication/CodeBeam.UltimateAuth.Authentication.InMemory/ServiceCollectionExtensions.cs
+++ b/src/authentication/CodeBeam.UltimateAuth.Authentication.InMemory/ServiceCollectionExtensions.cs
@@ -1,4 +1,5 @@
using CodeBeam.UltimateAuth.Core.Abstractions;
+using CodeBeam.UltimateAuth.InMemory.Extensions;
using Microsoft.Extensions.DependencyInjection;
namespace CodeBeam.UltimateAuth.Authentication.InMemory.Extensions;
@@ -7,6 +8,8 @@ public static class ServiceCollectionExtensions
{
public static IServiceCollection AddUltimateAuthAuthenticationInMemory(this IServiceCollection services)
{
+ services.AddUltimateAuthInMemoryInfrastructure();
+
services.AddSingleton();
return services;
}
diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs
index 8ed556c8..26739fbf 100644
--- a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs
+++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs
@@ -1,4 +1,5 @@
-using Microsoft.EntityFrameworkCore;
+using CodeBeam.UltimateAuth.EntityFrameworkCore.Extensions;
+using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.Extensions;
@@ -11,7 +12,9 @@ public static IServiceCollection AddUltimateAuthAuthorizationEntityFrameworkCore
{
services.AddDbContext(configureDb);
}
-
+
+ services.AddUltimateAuthEntityFrameworkCore();
+
services.AddScoped>();
services.AddScoped>();
return services;
diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Extensions/ServiceCollectionExtensions.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Extensions/ServiceCollectionExtensions.cs
index 919b6a61..6af07653 100644
--- a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Extensions/ServiceCollectionExtensions.cs
+++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Extensions/ServiceCollectionExtensions.cs
@@ -1,4 +1,5 @@
using CodeBeam.UltimateAuth.Core.Abstractions;
+using CodeBeam.UltimateAuth.InMemory.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
@@ -8,6 +9,8 @@ public static class ServiceCollectionExtensions
{
public static IServiceCollection AddUltimateAuthAuthorizationInMemory(this IServiceCollection services)
{
+ services.AddUltimateAuthInMemoryInfrastructure();
+
services.TryAddSingleton();
services.TryAddSingleton();
diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryRoleStore.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryRoleStore.cs
index 5dc2e15e..78075797 100644
--- a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryRoleStore.cs
+++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryRoleStore.cs
@@ -10,7 +10,7 @@ internal sealed class InMemoryRoleStore : InMemoryTenantVersionedStore new(entity.Tenant, entity.Id);
- public InMemoryRoleStore(TenantExecutionContext tenant) : base(tenant)
+ public InMemoryRoleStore(TenantExecutionContext tenant, InMemoryAtomicContextAccessor atomicContext) : base(tenant, atomicContext)
{
}
diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryRoleStoreFactory.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryRoleStoreFactory.cs
index 7b5b2df3..b51d4f2a 100644
--- a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryRoleStoreFactory.cs
+++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryRoleStoreFactory.cs
@@ -1,14 +1,21 @@
using System.Collections.Concurrent;
using CodeBeam.UltimateAuth.Core.MultiTenancy;
+using CodeBeam.UltimateAuth.InMemory;
namespace CodeBeam.UltimateAuth.Authorization.InMemory;
public sealed class InMemoryRoleStoreFactory : IRoleStoreFactory
{
private readonly ConcurrentDictionary _stores = new();
+ private readonly InMemoryAtomicContextAccessor _atomicContext;
+
+ public InMemoryRoleStoreFactory(InMemoryAtomicContextAccessor atomicContext)
+ {
+ _atomicContext = atomicContext;
+ }
public IRoleStore Create(TenantKey tenant)
{
- return _stores.GetOrAdd(tenant, t => new InMemoryRoleStore(new TenantExecutionContext(t)));
+ return _stores.GetOrAdd(tenant, t => new InMemoryRoleStore(new TenantExecutionContext(t), _atomicContext));
}
}
diff --git a/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserIdentifierClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserIdentifierClient.cs
index 96dfd2bf..12d300e3 100644
--- a/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserIdentifierClient.cs
+++ b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserIdentifierClient.cs
@@ -122,4 +122,9 @@ public interface IUserIdentifierClient
/// Deletes an identifier of a specific user.
///
Task DeleteUserAsync(UserKey userKey, DeleteUserIdentifierRequest request);
+
+ ///
+ /// Checks the availability of a user identifier (e.g., email, username, phone) for registration or assignment.
+ ///
+ Task> CheckAvailabilityAsync(CheckUserIdentifierAvailabilityRequest request);
}
diff --git a/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthUserIdentifierClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthUserIdentifierClient.cs
index fa240378..c81a8746 100644
--- a/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthUserIdentifierClient.cs
+++ b/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthUserIdentifierClient.cs
@@ -132,4 +132,10 @@ public async Task DeleteUserAsync(UserKey userKey, DeleteUserIdenti
var raw = await _request.SendJsonAsync(Url($"/admin/users/{userKey.Value}/identifiers/delete"), request);
return UAuthResultMapper.From(raw);
}
+
+ public async Task> CheckAvailabilityAsync(CheckUserIdentifierAvailabilityRequest request)
+ {
+ var raw = await _request.SendJsonAsync(Url("/users/identifiers/check-availability"), request);
+ return UAuthResultMapper.FromJson(raw);
+ }
}
diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs
index 7c390a43..2925ccb8 100644
--- a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs
+++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs
@@ -1,4 +1,5 @@
using CodeBeam.UltimateAuth.Credentials.Reference;
+using CodeBeam.UltimateAuth.EntityFrameworkCore.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
@@ -12,7 +13,9 @@ public static IServiceCollection AddUltimateAuthCredentialsEntityFrameworkCore(configureDb);
}
-
+
+ services.AddUltimateAuthEntityFrameworkCore();
+
services.AddScoped>();
return services;
}
diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryPasswordCredentialStore.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryPasswordCredentialStore.cs
index 4cbd31b2..70e8a47d 100644
--- a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryPasswordCredentialStore.cs
+++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryPasswordCredentialStore.cs
@@ -13,7 +13,7 @@ internal sealed class InMemoryPasswordCredentialStore : InMemoryTenantVersionedS
protected override CredentialKey GetKey(PasswordCredential entity)
=> new(entity.Tenant, entity.Id);
- public InMemoryPasswordCredentialStore(TenantExecutionContext tenant) : base(tenant)
+ public InMemoryPasswordCredentialStore(TenantExecutionContext tenant, InMemoryAtomicContextAccessor atomicContext) : base(tenant, atomicContext)
{
}
diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryPasswordCredentialStoreFactory.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryPasswordCredentialStoreFactory.cs
index fb48648d..e9b0de45 100644
--- a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryPasswordCredentialStoreFactory.cs
+++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryPasswordCredentialStoreFactory.cs
@@ -1,5 +1,6 @@
using CodeBeam.UltimateAuth.Core.MultiTenancy;
using CodeBeam.UltimateAuth.Credentials.Reference;
+using CodeBeam.UltimateAuth.InMemory;
using System.Collections.Concurrent;
namespace CodeBeam.UltimateAuth.Credentials.InMemory;
@@ -7,9 +8,15 @@ namespace CodeBeam.UltimateAuth.Credentials.InMemory;
public sealed class InMemoryPasswordCredentialStoreFactory : IPasswordCredentialStoreFactory
{
private readonly ConcurrentDictionary _stores = new();
+ private readonly InMemoryAtomicContextAccessor _atomicContext;
+
+ public InMemoryPasswordCredentialStoreFactory(InMemoryAtomicContextAccessor atomicContext)
+ {
+ _atomicContext = atomicContext;
+ }
public IPasswordCredentialStore Create(TenantKey tenant)
{
- return _stores.GetOrAdd(tenant, t => new InMemoryPasswordCredentialStore(new TenantExecutionContext(t)));
+ return _stores.GetOrAdd(tenant, t => new InMemoryPasswordCredentialStore(new TenantExecutionContext(t), _atomicContext));
}
}
diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/ServiceCollectionExtensions.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/ServiceCollectionExtensions.cs
index 9b53af9c..46acfd8f 100644
--- a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/ServiceCollectionExtensions.cs
+++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/ServiceCollectionExtensions.cs
@@ -1,5 +1,6 @@
using CodeBeam.UltimateAuth.Core.Abstractions;
using CodeBeam.UltimateAuth.Credentials.Reference;
+using CodeBeam.UltimateAuth.InMemory.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
@@ -9,6 +10,8 @@ public static class ServiceCollectionExtensions
{
public static IServiceCollection AddUltimateAuthCredentialsInMemory(this IServiceCollection services)
{
+ services.AddUltimateAuthInMemoryInfrastructure();
+
services.TryAddSingleton();
return services;
diff --git a/src/persistence/CodeBeam.UltimateAuth.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs b/src/persistence/CodeBeam.UltimateAuth.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs
new file mode 100644
index 00000000..dc0bd870
--- /dev/null
+++ b/src/persistence/CodeBeam.UltimateAuth.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs
@@ -0,0 +1,16 @@
+using CodeBeam.UltimateAuth.Core.Abstractions;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace CodeBeam.UltimateAuth.EntityFrameworkCore.Extensions;
+
+public static class ServiceCollectionExtensions
+{
+ public static IServiceCollection AddUltimateAuthEntityFrameworkCore(this IServiceCollection services) where TDbContext : DbContext
+ {
+ services.TryAddScoped>();
+
+ return services;
+ }
+}
diff --git a/src/persistence/CodeBeam.UltimateAuth.EntityFrameworkCore/Infrastructure/EfCoreUAuthAtomicExecutor.cs b/src/persistence/CodeBeam.UltimateAuth.EntityFrameworkCore/Infrastructure/EfCoreUAuthAtomicExecutor.cs
new file mode 100644
index 00000000..4e8149d8
--- /dev/null
+++ b/src/persistence/CodeBeam.UltimateAuth.EntityFrameworkCore/Infrastructure/EfCoreUAuthAtomicExecutor.cs
@@ -0,0 +1,55 @@
+using CodeBeam.UltimateAuth.Core.Abstractions;
+using Microsoft.EntityFrameworkCore;
+
+namespace CodeBeam.UltimateAuth.EntityFrameworkCore;
+
+internal sealed class EfCoreUAuthAtomicExecutor : IUAuthAtomicExecutor where TDbContext : DbContext
+{
+ private readonly TDbContext _db;
+
+ public EfCoreUAuthAtomicExecutor(TDbContext db)
+ {
+ _db = db;
+ }
+
+ public Task ExecuteAsync(Func operation, CancellationToken ct = default)
+ {
+ ArgumentNullException.ThrowIfNull(operation);
+
+ return ExecuteAsync