From bc4214c7871f39c6295bb199841d7130f11ff1a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Mon, 26 Oct 2026 21:58:26 +0300 Subject: [PATCH 1/3] Add Users GetByKeys Endpoint --- .../Policy/SandboxUAuthAccessPolicy.cs | 19 ++-- .../Services/LabAdminService.cs | 32 +++---- .../Services/SandboxUserService.cs | 29 +++++-- .../Defaults/UAuthActions.cs | 1 + .../Abstractions/IUserEndpointHandler.cs | 1 + .../Endpoints/UAuthEndpointRegistrar.cs | 4 + .../Services/Abstractions/IUserClient.cs | 8 ++ .../Services/UAuthUserClient.cs | 14 +++ .../Abstractions/IUserSummaryQueryStore.cs | 2 + .../Requests/GetUsersByKeysRequest.cs | 8 ++ .../Stores/EfCoreUserSummaryQueryStore.cs | 70 +++++++++++++++ .../Stores/InMemoryUserSummaryQueryStore.cs | 86 +++++++++++++++++-- .../Endpoints/UserEndpointHandler.cs | 23 +++++ .../Services/IUserApplicationService.cs | 2 + .../Services/UserApplicationService.cs | 55 ++++++++---- 15 files changed, 297 insertions(+), 57 deletions(-) create mode 100644 src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/GetUsersByKeysRequest.cs diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Policy/SandboxUAuthAccessPolicy.cs b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Policy/SandboxUAuthAccessPolicy.cs index b2f6c8a6..50020dee 100644 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Policy/SandboxUAuthAccessPolicy.cs +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Policy/SandboxUAuthAccessPolicy.cs @@ -14,9 +14,7 @@ public sealed class SandboxUAuthAccessPolicy(IDbContextFactory dbF public bool AppliesTo(AccessContext context) => IsProtectedAction(context.Action); - public async ValueTask DecideAsync( - AccessContext context, - CancellationToken cancellationToken = default) + public async ValueTask DecideAsync(AccessContext context, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); @@ -45,8 +43,7 @@ public async ValueTask DecideAsync( if (rootResolver.IsLadmin(context)) return AccessDecision.Allow(); - await using var db = await dbFactory.CreateDbContextAsync( - cancellationToken); + await using var db = await dbFactory.CreateDbContextAsync(cancellationToken); var actor = await db.SandboxMembers .AsNoTracking() @@ -91,10 +88,7 @@ public async ValueTask DecideAsync( var belongsToSandbox = await db.SandboxMembers .AsNoTracking() - .AnyAsync( - x => x.UserKey == targetKey && - x.SandboxId == actor.SandboxId, - cancellationToken); + .AnyAsync(x => x.UserKey == targetKey && x.SandboxId == actor.SandboxId, cancellationToken); if (!belongsToSandbox) return AccessDecision.Deny("sandbox_target_out_of_scope"); @@ -140,16 +134,15 @@ private static SandboxOperation ClassifyAction(string action) // creation workflow, including membership and capacity. if (action is UAuthActions.Users.CreateAdmin or - UAuthActions.Users.QueryAdmin) + UAuthActions.Users.QueryAdmin or + UAuthActions.Users.GetByKeysAdmin) { return SandboxOperation.Denied; } // Global role definitions and assignments are // handled separately by LabApplicationPolicy. - if (action.StartsWith( - "authorization.roles.", - StringComparison.Ordinal)) + if (action.StartsWith("authorization.roles.", StringComparison.Ordinal)) { return action is UAuthActions.Authorization.Roles.GetSelf or diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Services/LabAdminService.cs b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Services/LabAdminService.cs index e20d670d..d0ff6974 100644 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Services/LabAdminService.cs +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Services/LabAdminService.cs @@ -5,12 +5,13 @@ using CodeBeam.UltimateAuth.IdentityLab.Contracts; using CodeBeam.UltimateAuth.IdentityLab.Domain; using CodeBeam.UltimateAuth.IdentityLab.Persistence; +using CodeBeam.UltimateAuth.Users.Contracts; using Microsoft.EntityFrameworkCore; namespace CodeBeam.UltimateAuth.IdentityLab.Services; public sealed class LabAdminService(IDbContextFactory dbFactory, IRoleStoreFactory roleStoreFactory, - IUserRoleStoreFactory userRoleStoreFactory, IUAuthClient uAuthClient) + IUserRoleStoreFactory userRoleStoreFactory, IUserSummaryQueryStoreFactory userSummaryQueryStoreFactory) { public async Task IsAdminAsync(UserKey actorUserKey, CancellationToken ct = default) { @@ -30,11 +31,7 @@ public async Task IsAdminAsync(UserKey actorUserKey, CancellationToken ct .Create(tenant) .GetByIdsAsync(roleIds, ct); - return roles.Any(x => - string.Equals( - x.Name, - "Ladmin", - StringComparison.OrdinalIgnoreCase)); + return roles.Any(x => string.Equals(x.Name, "Ladmin", StringComparison.OrdinalIgnoreCase)); } private async Task RequireAdminAsync(UserKey actorUserKey, CancellationToken ct) @@ -142,21 +139,26 @@ public async Task> GetMemberDetailsAsync(U { var members = await GetMembersAsync(actorUserKey, sandboxId, ct); - var result = new List(); + if (members.Count == 0) + return Array.Empty(); + + var keys = members.Select(x => x.UserKey).Distinct().ToArray(); + + var store = userSummaryQueryStoreFactory.Create(TenantKeys.Single); + + var summaries = await store.GetByKeysAsync(keys, ct); + + var userMap = summaries.ToDictionary(x => x.UserKey); + + var result = new List(members.Count); foreach (var member in members) { - var userResult = await uAuthClient.Users.GetUserAsync( - member.UserKey); - - if (!userResult.IsSuccess || userResult.Value is null) + if (!userMap.TryGetValue(member.UserKey, out var user)) { - throw new InvalidOperationException( - $"Unable to load member {member.UserKey.Value}."); + throw new InvalidOperationException($"Unable to load member {member.UserKey.Value}."); } - var user = userResult.Value; - result.Add(new AdminSandboxMemberItem( member.UserKey, user.DisplayName ?? user.UserName ?? "Unknown", diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Services/SandboxUserService.cs b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Services/SandboxUserService.cs index 845dc9f3..ab67ecf7 100644 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Services/SandboxUserService.cs +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Services/SandboxUserService.cs @@ -10,7 +10,8 @@ namespace CodeBeam.UltimateAuth.IdentityLab.Services; public sealed class SandboxUserService(SandboxService sandboxService, SandboxAccessService accessService, IUAuthClient uAuthClient, - IRoleStoreFactory roleStoreFactory, IUserRoleStoreFactory userRoleStoreFactory, ActivityLogService activityLogService) + IRoleStoreFactory roleStoreFactory, IUserRoleStoreFactory userRoleStoreFactory, IUserSummaryQueryStoreFactory userSummaryQueryStoreFactory, + ActivityLogService activityLogService) { public async Task GetUsersAsync(UserKey actorUserKey, CancellationToken ct = default) { @@ -25,16 +26,28 @@ public sealed class SandboxUserService(SandboxService sandboxService, SandboxAcc throw new UnauthorizedAccessException(); var members = await sandboxService.GetMembersAsync(sandbox.Id, ct); - var users = new List(); - foreach (var member in members) + if (members.Count == 0) { - var result = await uAuthClient.Users.GetUserAsync(member.UserKey); + return new SandboxUsersSnapshot(sandbox.Id, sandbox.Settings.MaxUsers, Array.Empty()); + } - if (!result.IsSuccess || result.Value is null) - throw new InvalidOperationException($"Unable to load user {member.UserKey.Value}."); + var keys = members.Select(x => x.UserKey).Distinct().ToArray(); + + var store = userSummaryQueryStoreFactory.Create(TenantKeys.Single); + + var summaries = await store.GetByKeysAsync(keys, ct); - var user = result.Value; + var userMap = summaries.ToDictionary(x => x.UserKey); + + var users = new List(members.Count); + + foreach (var member in members) + { + if (!userMap.TryGetValue(member.UserKey, out var user)) + { + throw new InvalidOperationException($"Unable to load user {member.UserKey.Value}."); + } users.Add(new SandboxUserItem( UserKey: member.UserKey, @@ -45,7 +58,7 @@ public sealed class SandboxUserService(SandboxService sandboxService, SandboxAcc Email: user.PrimaryEmail ?? "", IsOwner: member.Role == SandboxMemberRole.Owner, Status: user.Status, - CreatedAt: user.CreatedAt ?? member.JoinedAt + CreatedAt: user.CreatedAt )); } diff --git a/src/CodeBeam.UltimateAuth.Core/Defaults/UAuthActions.cs b/src/CodeBeam.UltimateAuth.Core/Defaults/UAuthActions.cs index 4fc140d6..a65087b4 100644 --- a/src/CodeBeam.UltimateAuth.Core/Defaults/UAuthActions.cs +++ b/src/CodeBeam.UltimateAuth.Core/Defaults/UAuthActions.cs @@ -54,6 +54,7 @@ public static class Users public const string Wildcard = "users.*"; public const string QueryAdmin = "users.query.admin"; + public const string GetByKeysAdmin = "users.getbykeys.admin"; public const string CreateAnonymous = "users.create.anonymous"; public const string CreateAdmin = "users.create.admin"; public const string DeleteSelf = "users.delete.self"; diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUserEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUserEndpointHandler.cs index f5638b2e..cd1f3122 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUserEndpointHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUserEndpointHandler.cs @@ -6,6 +6,7 @@ namespace CodeBeam.UltimateAuth.Server.Endpoints; public interface IUserEndpointHandler { Task QueryUsersAsync(HttpContext ctx); + Task GetUsersByKeysAsync(HttpContext ctx); Task CreateAsync(HttpContext ctx); Task CreateAdminAsync(HttpContext ctx); Task ChangeStatusSelfAsync(HttpContext ctx); diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs index 4fe2f145..c40cc611 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs @@ -204,6 +204,10 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options adminUsers.MapPost("/query", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) => await h.QueryUsersAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement)); + if (Enabled(UAuthActions.Users.GetByKeysAdmin)) + adminUsers.MapPost("/get-by-keys", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) + => await h.GetUsersByKeysAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement)); + if (Enabled(UAuthActions.Users.CreateAdmin)) adminUsers.MapPost("/create", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) => await h.CreateAdminAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement)); diff --git a/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserClient.cs index 5583aec9..846341c5 100644 --- a/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserClient.cs +++ b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserClient.cs @@ -50,6 +50,14 @@ public interface IUserClient /// Task>> QueryAsync(UserQuery query); + /// + /// Retrieves summaries for multiple users by their stable user keys. + /// + /// + /// Requires permission for the administrative batch retrieval action. Missing or deleted users are omitted from the result. + /// + Task>> GetByKeysAsync(IReadOnlyCollection userKeys); + /// /// Creates a new user as the current user context. /// diff --git a/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthUserClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthUserClient.cs index 8393787a..6600c843 100644 --- a/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthUserClient.cs +++ b/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthUserClient.cs @@ -64,6 +64,20 @@ public async Task>> QueryAsync(UserQuery qu return UAuthResultMapper.FromJson>(raw); } + public async Task>> GetByKeysAsync(IReadOnlyCollection userKeys) + { + ArgumentNullException.ThrowIfNull(userKeys); + + var request = new GetUsersByKeysRequest + { + UserKeys = userKeys.ToArray() + }; + + var raw = await _request.SendJsonAsync(Url("/admin/users/get-by-keys"), request); + + return UAuthResultMapper.FromJson>(raw); + } + public async Task CreateMyProfileAsync(CreateProfileRequest request) { var raw = await _request.SendJsonAsync(Url("/me/profile/create"), request); diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Abstractions/IUserSummaryQueryStore.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Abstractions/IUserSummaryQueryStore.cs index abf8f9dc..bd332f71 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Abstractions/IUserSummaryQueryStore.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Abstractions/IUserSummaryQueryStore.cs @@ -1,8 +1,10 @@ using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; namespace CodeBeam.UltimateAuth.Users.Contracts; public interface IUserSummaryQueryStore { Task> QueryAsync(UserQuery query, CancellationToken ct = default); + Task> GetByKeysAsync(IReadOnlyCollection userKeys, CancellationToken ct = default); } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/GetUsersByKeysRequest.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/GetUsersByKeysRequest.cs new file mode 100644 index 00000000..e446dbde --- /dev/null +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/GetUsersByKeysRequest.cs @@ -0,0 +1,8 @@ +using CodeBeam.UltimateAuth.Core.Domain; + +namespace CodeBeam.UltimateAuth.Users.Contracts; + +public sealed record GetUsersByKeysRequest +{ + public IReadOnlyList UserKeys { get; init; } = Array.Empty(); +} diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserSummaryQueryStore.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserSummaryQueryStore.cs index a33c7790..abf1df1e 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserSummaryQueryStore.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserSummaryQueryStore.cs @@ -195,6 +195,76 @@ public async Task> QueryAsync(UserQuery query, Cancella query.Descending); } + public async Task> GetByKeysAsync(IReadOnlyCollection userKeys, CancellationToken ct = default) + { + ArgumentNullException.ThrowIfNull(userKeys); + ct.ThrowIfCancellationRequested(); + + var keys = userKeys.Distinct().ToArray(); + + if (keys.Length == 0) + return Array.Empty(); + + var profileKey = ProfileKey.Default; + + var lifecycles = _db.Set() + .AsNoTracking() + .Where(x => + x.Tenant == _tenant && + x.DeletedAt == null && + keys.Contains(x.UserKey)); + + var profiles = _db.Set() + .AsNoTracking() + .Where(x => + x.Tenant == _tenant && + x.ProfileKey == profileKey && + x.DeletedAt == null); + + var identifiers = _db.Set() + .AsNoTracking() + .Where(x => + x.Tenant == _tenant && + x.DeletedAt == null && + x.IsPrimary); + + return await lifecycles + .Select(l => new UserSummary + { + UserKey = l.UserKey, + Status = l.Status, + CreatedAt = l.CreatedAt, + + DisplayName = profiles + .Where(p => p.UserKey == l.UserKey) + .Select(p => p.DisplayName) + .FirstOrDefault(), + + UserName = identifiers + .Where(i => i.UserKey == l.UserKey && i.Type == UserIdentifierType.Username) + .OrderBy(i => i.CreatedAt) + .ThenBy(i => i.Id) + .Select(i => i.Value) + .FirstOrDefault(), + + PrimaryEmail = identifiers + .Where(i => i.UserKey == l.UserKey && i.Type == UserIdentifierType.Email) + .OrderBy(i => i.CreatedAt) + .ThenBy(i => i.Id) + .Select(i => i.Value) + .FirstOrDefault(), + + PrimaryPhone = identifiers + .Where(i => i.UserKey == l.UserKey && i.Type == UserIdentifierType.Phone) + .OrderBy(i => i.CreatedAt) + .ThenBy(i => i.Id) + .Select(i => i.Value) + .FirstOrDefault() + }) + .OrderBy(x => x.UserKey) + .ToListAsync(ct); + } + private static string EscapeLike(string value) { return value diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserSummaryQueryStore.cs b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserSummaryQueryStore.cs index 3fe9285b..97b02a1b 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserSummaryQueryStore.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserSummaryQueryStore.cs @@ -177,16 +177,90 @@ IOrderedEnumerable OrderByCreatedAt() query.Descending); } + public async Task> GetByKeysAsync(IReadOnlyCollection userKeys, CancellationToken ct = default) + { + ArgumentNullException.ThrowIfNull(userKeys); + ct.ThrowIfCancellationRequested(); + + var keys = userKeys.Distinct().ToArray(); + + if (keys.Length == 0) + return Array.Empty(); + + var requested = keys.ToHashSet(); + + // Reuse the existing lifecycle query to obtain active users. + var lifecycles = new List(); + var page = 1; + + const int pageSize = 500; + + while (true) + { + ct.ThrowIfCancellationRequested(); + + var result = await _lifecycles.QueryAsync( + new UserLifecycleQuery + { + PageNumber = page, + PageSize = pageSize, + IncludeDeleted = false + }, + ct); + + lifecycles.AddRange(result.Items.Where(x => requested.Contains(x.UserKey))); + + if (!result.HasNext) + break; + + page++; + } + + if (lifecycles.Count == 0) + return Array.Empty(); + + var foundKeys = lifecycles.Select(x => x.UserKey).ToArray(); + + var profiles = await _profiles.GetByUsersAsync(foundKeys, ProfileKey.Default, ct); + var identifiers = await _identifiers.GetByUsersAsync(foundKeys, ct); + var profileMap = profiles.ToDictionary(x => x.UserKey); + + var identifierGroups = identifiers + .GroupBy(x => x.UserKey) + .ToDictionary(x => x.Key, x => x.ToList()); + + var summaries = new List(lifecycles.Count); + + foreach (var lifecycle in lifecycles) + { + profileMap.TryGetValue(lifecycle.UserKey, out var profile); + + identifierGroups.TryGetValue(lifecycle.UserKey, out var ids); + + summaries.Add(new UserSummary + { + UserKey = lifecycle.UserKey, + Status = lifecycle.Status, + CreatedAt = lifecycle.CreatedAt, + + DisplayName = profile?.DisplayName, + UserName = PrimaryValue(ids, UserIdentifierType.Username), + PrimaryEmail = PrimaryValue(ids, UserIdentifierType.Email), + PrimaryPhone = PrimaryValue(ids, UserIdentifierType.Phone) + }); + } + + return summaries + .OrderBy(x => x.UserKey) + .ToArray(); + } + private static bool Contains(string? value, string search) { - return value?.Contains( - search, - StringComparison.InvariantCultureIgnoreCase) ?? false; + return value?.Contains(search, StringComparison.InvariantCultureIgnoreCase) ?? false; } - private static string? PrimaryValue( - IReadOnlyList? identifiers, - UserIdentifierType type) + private static string? PrimaryValue(IReadOnlyList? identifiers, UserIdentifierType type) { return identifiers? .Where(x => x.Type == type && x.IsPrimary) diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Endpoints/UserEndpointHandler.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Endpoints/UserEndpointHandler.cs index 9d14cd2d..d6e0a11d 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Endpoints/UserEndpointHandler.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Endpoints/UserEndpointHandler.cs @@ -40,6 +40,29 @@ public async Task QueryUsersAsync(HttpContext ctx) return Results.Ok(result); } + public async Task GetUsersByKeysAsync(HttpContext ctx) + { + var flow = _authFlow.Current; + + if (!flow.IsAuthenticated) + return Results.Unauthorized(); + + var request = await ctx.ReadJsonAsync( + ctx.RequestAborted); + + if (request is null || request.UserKeys is null) + return Results.BadRequest(); + + var accessContext = await _accessContextFactory.CreateAsync( + authFlow: flow, + action: UAuthActions.Users.GetByKeysAdmin, + resource: "users"); + + var result = await _users.GetUsersByKeysAsync(accessContext, request.UserKeys, ctx.RequestAborted); + + return Results.Ok(result); + } + public async Task CreateAsync(HttpContext ctx) { var flow = _authFlow.Current; diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/IUserApplicationService.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/IUserApplicationService.cs index d9938222..4cf5aa69 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/IUserApplicationService.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/IUserApplicationService.cs @@ -1,4 +1,5 @@ using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Users.Contracts; namespace CodeBeam.UltimateAuth.Users.Reference; @@ -9,6 +10,7 @@ public interface IUserApplicationService Task GetUserProfileAsync(AccessContext context, ProfileKey? profileKey = null, CancellationToken ct = default); Task> QueryUsersAsync(AccessContext context, UserQuery query, CancellationToken ct = default); + Task> GetUsersByKeysAsync(AccessContext context, IReadOnlyCollection userKeys, CancellationToken ct = default); Task CreateUserAsync(AccessContext context, CreateUserRequest request, CancellationToken ct = default); Task ChangeUserStatusAsync(AccessContext context, object request, CancellationToken ct = default); diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs index b46ba8ea..f5e72748 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs @@ -819,6 +819,46 @@ public async Task CheckIdentifierAvailabilityA #endregion + public async Task> QueryUsersAsync(AccessContext context, UserQuery query, CancellationToken ct = default) + { + var command = new AccessCommand>( + async innerCt => + { + var normalized = (UserQuery)(query ?? new UserQuery()).Normalize(_options.Pagination); + var store = _queryStoreFactory.Create(context.ResourceTenant); + + return await store.QueryAsync(normalized, innerCt); + }); + + return await _accessOrchestrator.ExecuteAsync(context, command, ct); + } + + public async Task> GetUsersByKeysAsync(AccessContext context, IReadOnlyCollection userKeys, CancellationToken ct = default) + { + ArgumentNullException.ThrowIfNull(userKeys); + + const int maxBatchSize = 250; + + var keys = userKeys.Distinct().ToArray(); + + if (keys.Length > maxBatchSize) + { + throw new UAuthValidationException("user_batch_size_exceeded"); + } + + var command = new AccessCommand>( + async innerCt => + { + if (keys.Length == 0) + return Array.Empty(); + + var store = _queryStoreFactory.Create(context.ResourceTenant); + + return await store.GetByKeysAsync(keys, innerCt); + }); + + return await _accessOrchestrator.ExecuteAsync(context, command, ct); + } #region Helpers @@ -912,21 +952,6 @@ UserIdentifierType.Email or #endregion - public async Task> QueryUsersAsync(AccessContext context, UserQuery query, CancellationToken ct = default) - { - var command = new AccessCommand>( - async innerCt => - { - var normalized = (UserQuery)(query ?? new UserQuery()).Normalize(_options.Pagination); - var store = _queryStoreFactory.Create(context.ResourceTenant); - - return await store.QueryAsync(normalized, innerCt); - }); - - return await _accessOrchestrator.ExecuteAsync(context, command, ct); - } - - private async Task EnsureIdentifierUniquenessAsync( IUserIdentifierStore store, UserIdentifierType type, From b9dc15db4441b1766e01889fa3c13135e504dca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Mon, 26 Oct 2026 22:18:59 +0300 Subject: [PATCH 2/3] Add TargetUserKeys to AccessContext and Add Tests --- .../Policy/SandboxUAuthAccessPolicy.cs | 31 ++- .../Contracts/Authority/AccessContext.cs | 19 +- .../Auth/Context/AccessContextFactory.cs | 23 +- .../Auth/Context/IAccessContextFactory.cs | 6 + .../Endpoints/UserEndpointHandler.cs | 30 ++- .../Server/AccessContextFactoryTests.cs | 117 ++++++++++ .../Users/UserEndpointHandlerTests.cs | 199 ++++++++++++++++++ 7 files changed, 405 insertions(+), 20 deletions(-) diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Policy/SandboxUAuthAccessPolicy.cs b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Policy/SandboxUAuthAccessPolicy.cs index 50020dee..cbe7e692 100644 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Policy/SandboxUAuthAccessPolicy.cs +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Policy/SandboxUAuthAccessPolicy.cs @@ -67,18 +67,35 @@ public async ValueTask DecideAsync(AccessContext context, Cancel if (operation == SandboxOperation.Denied) return AccessDecision.Deny("sandbox_operation_restricted"); - if (actor.Status == SandboxStatus.Passive && - operation != SandboxOperation.Read) + if (actor.Status == SandboxStatus.Passive && operation != SandboxOperation.Read) { return AccessDecision.Deny("sandbox_read_only"); } - if (operation == SandboxOperation.Manage && - actor.Role != SandboxMemberRole.Owner) + if (operation == SandboxOperation.Manage && actor.Role != SandboxMemberRole.Owner) { return AccessDecision.Deny("sandbox_owner_required"); } + if (action == UAuthActions.Users.GetByKeysAdmin) + { + var requestedKeys = context.TargetUserKeys; + + if (requestedKeys.Count == 0) + return AccessDecision.Deny("sandbox_targets_required"); + + var keys = requestedKeys.Distinct().ToArray(); + + var allowedCount = await db.SandboxMembers + .AsNoTracking() + .CountAsync(x => x.SandboxId == actor.SandboxId && keys.Contains(x.UserKey), cancellationToken); + + if (allowedCount != keys.Length) + { + return AccessDecision.Deny("sandbox_target_out_of_scope"); + } + } + // All target-based admin operations must remain // inside the actor's sandbox. if (RequiresTarget(action)) @@ -132,10 +149,7 @@ private static SandboxOperation ClassifyAction(string action) { // User creation requires a controlled sandbox // creation workflow, including membership and capacity. - if (action is - UAuthActions.Users.CreateAdmin or - UAuthActions.Users.QueryAdmin or - UAuthActions.Users.GetByKeysAdmin) + if (action is UAuthActions.Users.CreateAdmin or UAuthActions.Users.QueryAdmin) { return SandboxOperation.Denied; } @@ -166,6 +180,7 @@ UAuthActions.Authorization.Roles.GetAdmin or private static bool IsReadAction(string action) => action is + UAuthActions.Users.GetByKeysAdmin or UAuthActions.UserProfiles.GetSelf or UAuthActions.UserProfiles.GetAdmin or UAuthActions.UserIdentifiers.GetSelf or diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AccessContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AccessContext.cs index a6c89c2e..e71108bb 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AccessContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AccessContext.cs @@ -18,6 +18,16 @@ public sealed class AccessContext // Target public string? Resource { get; init; } public UserKey? TargetUserKey { get; init; } + + /// + /// Gets the target users of a batch operation. + /// + /// + /// The collection is normalized and immutable. + /// A batch operation may target multiple users without + /// having a single . + /// + public IReadOnlyList TargetUserKeys { get; } public TenantKey ResourceTenant { get; init; } public string Action { get; init; } = default!; @@ -27,6 +37,7 @@ public sealed class AccessContext public bool IsSelfAction => ActorUserKey != null && TargetUserKey != null && string.Equals(ActorUserKey.Value, TargetUserKey.Value, StringComparison.Ordinal); public bool HasActor => ActorUserKey != null; public bool HasTarget => TargetUserKey != null; + public bool HasMultipleTargets => TargetUserKeys.Count > 0; public bool IsAdminContext => Action.EndsWith(".admin", StringComparison.Ordinal); public bool IsSelfContext => Action.EndsWith(".self", StringComparison.Ordinal); @@ -51,7 +62,8 @@ internal AccessContext( UserKey? targetUserKey, TenantKey resourceTenant, string action, - IReadOnlyDictionary attributes) + IReadOnlyDictionary attributes, + IReadOnlyCollection? targetUserKeys = null) { ActorUserKey = actorUserKey; ActorTenant = actorTenant; @@ -62,6 +74,8 @@ internal AccessContext( Resource = resource; TargetUserKey = targetUserKey; + TargetUserKeys = Array.AsReadOnly((targetUserKeys ?? Array.Empty()).Distinct().ToArray()); + ResourceTenant = resourceTenant; Action = action; @@ -86,7 +100,8 @@ public AccessContext WithAttribute(string key, object value) TargetUserKey, ResourceTenant, Action, - merged + merged, + TargetUserKeys ); } } diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AccessContextFactory.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AccessContextFactory.cs index 81dc07ad..3dd551e3 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AccessContextFactory.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AccessContextFactory.cs @@ -31,7 +31,25 @@ public async Task CreateForExplicitTenantResourceAsync(AuthFlowCo return await CreateInternalAsync(authFlow, action, resource, resourceTenant, resourceId, attributes, ct); } - private async Task CreateInternalAsync(AuthFlowContext authFlow, string action, string resource, TenantKey resourceTenant, string? resourceId, IDictionary? attributes, CancellationToken ct) + public async Task CreateForUserTargetsAsync(AuthFlowContext authFlow, string action, string resource, IReadOnlyCollection targetUserKeys, CancellationToken ct = default) + { + ArgumentNullException.ThrowIfNull(targetUserKeys); + + var normalizedKeys = targetUserKeys.Distinct().ToArray(); + + return await CreateInternalAsync( + authFlow, + action, + resource, + authFlow.Tenant, + resourceId: null, + attributes: null, + ct: ct, + targetUserKeys: normalizedKeys); + } + + private async Task CreateInternalAsync(AuthFlowContext authFlow, string action, string resource, TenantKey resourceTenant, string? resourceId, + IDictionary? attributes, CancellationToken ct, IReadOnlyCollection? targetUserKeys = null) { if (string.IsNullOrWhiteSpace(action)) throw new ArgumentException("Action is required.", nameof(action)); @@ -80,7 +98,8 @@ private async Task CreateInternalAsync(AuthFlowContext authFlow, action: action, attributes: attrs.Count > 0 ? new ReadOnlyDictionary(attrs) - : EmptyAttributes.Instance + : EmptyAttributes.Instance, + targetUserKeys: targetUserKeys ); } } diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Context/IAccessContextFactory.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Context/IAccessContextFactory.cs index 7c6a5fe2..fd3bbf7b 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Context/IAccessContextFactory.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Context/IAccessContextFactory.cs @@ -1,4 +1,5 @@ using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Server.Auth; @@ -7,4 +8,9 @@ public interface IAccessContextFactory { Task CreateAsync(AuthFlowContext authFlow, string action, string resource, string? resourceId = null, IDictionary? attributes = null, CancellationToken ct = default); Task CreateForExplicitTenantResourceAsync(AuthFlowContext authFlow, string action, string resource, TenantKey resourceTenant, string? resourceId = null, IDictionary? attributes = null, CancellationToken ct = default); + + /// + /// Creates an access context for an operation targeting multiple users. + /// + Task CreateForUserTargetsAsync(AuthFlowContext authFlow, string action, string resource, IReadOnlyCollection targetUserKeys, CancellationToken ct = default); } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Endpoints/UserEndpointHandler.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Endpoints/UserEndpointHandler.cs index d6e0a11d..101daa66 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Endpoints/UserEndpointHandler.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Endpoints/UserEndpointHandler.cs @@ -47,18 +47,32 @@ public async Task GetUsersByKeysAsync(HttpContext ctx) if (!flow.IsAuthenticated) return Results.Unauthorized(); - var request = await ctx.ReadJsonAsync( - ctx.RequestAborted); + var request = await ctx.ReadJsonAsync(ctx.RequestAborted); - if (request is null || request.UserKeys is null) + if (request?.UserKeys is null) return Results.BadRequest(); - var accessContext = await _accessContextFactory.CreateAsync( - authFlow: flow, - action: UAuthActions.Users.GetByKeysAdmin, - resource: "users"); + const int maxBatchSize = 250; + + var keys = request.UserKeys.Distinct().ToArray(); + + if (keys.Length > maxBatchSize) + { + return Results.BadRequest(new + { + Error = "user_batch_size_exceeded" + }); + } + + var accessContext = + await _accessContextFactory.CreateForUserTargetsAsync( + authFlow: flow, + action: UAuthActions.Users.GetByKeysAdmin, + resource: "users", + targetUserKeys: keys, + ct: ctx.RequestAborted); - var result = await _users.GetUsersByKeysAsync(accessContext, request.UserKeys, ctx.RequestAborted); + var result = await _users.GetUsersByKeysAsync(accessContext, keys, ctx.RequestAborted); return Results.Ok(result); } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/AccessContextFactoryTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/AccessContextFactoryTests.cs index 2504f2fe..4f627fb1 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/AccessContextFactoryTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/AccessContextFactoryTests.cs @@ -176,4 +176,121 @@ public async Task CreateAsync_ShouldIgnoreUntrustedRolesForAnonymousActor() userRoleFactory.VerifyNoOtherCalls(); roleFactory.VerifyNoOtherCalls(); } + + [Fact] + public async Task CreateForUserTargetsAsync_ShouldNormalizeAndPreserveTargets() + { + var tenant = TenantKey.Single; + var first = UserKey.New(); + var second = UserKey.New(); + + var userRoleFactory = new Mock(); + var roleFactory = new Mock(); + + var resolver = new AccessActorRoleResolver( + userRoleFactory.Object, + roleFactory.Object); + + var sut = new AccessContextFactory( + resolver, + Mock.Of()); + + var flow = AuthFlowTestFactory.New( + tenant: tenant, + isAuthenticated: false); + + var context = await sut.CreateForUserTargetsAsync( + flow, + "users.getbykeys.admin", + "users", + new[] { first, second, first }); + + context.TargetUserKeys.Should().HaveCount(2); + context.TargetUserKeys.Should().ContainInOrder(first, second); + context.TargetUserKey.Should().BeNull(); + context.ResourceTenant.Should().Be(tenant); + context.IsAdminContext.Should().BeTrue(); + } + + [Fact] + public async Task CreateForUserTargetsAsync_ShouldSnapshotInputCollection() + { + var first = UserKey.New(); + var second = UserKey.New(); + + var userRoleFactory = new Mock(); + var roleFactory = new Mock(); + + var sut = new AccessContextFactory( + new AccessActorRoleResolver( + userRoleFactory.Object, + roleFactory.Object), + Mock.Of()); + + var flow = AuthFlowTestFactory.New( + isAuthenticated: false); + + var input = new List { first }; + + var context = await sut.CreateForUserTargetsAsync( + flow, + "users.getbykeys.admin", + "users", + input); + + input.Add(second); + + context.TargetUserKeys.Should().ContainSingle() + .Which.Should().Be(first); + } + + [Fact] + public async Task CreateForUserTargetsAsync_WithAttribute_ShouldPreserveTargets() + { + var first = UserKey.New(); + var second = UserKey.New(); + + var sut = new AccessContextFactory( + new AccessActorRoleResolver( + Mock.Of(), + Mock.Of()), + Mock.Of()); + + var flow = AuthFlowTestFactory.New( + isAuthenticated: false); + + var context = await sut.CreateForUserTargetsAsync( + flow, + "users.getbykeys.admin", + "users", + new[] { first, second }); + + var updated = context.WithAttribute("source", "test"); + + updated.TargetUserKeys.Should() + .BeEquivalentTo(new[] { first, second }); + + updated.Attributes["source"].Should().Be("test"); + updated.TargetUserKey.Should().BeNull(); + } + + [Fact] + public async Task CreateAsync_SingleTargetContext_ShouldHaveEmptyTargetUserKeys() + { + var sut = new AccessContextFactory( + new AccessActorRoleResolver( + Mock.Of(), + Mock.Of()), + Mock.Of()); + + var flow = AuthFlowTestFactory.New( + isAuthenticated: false); + + var context = await sut.CreateAsync( + flow, + "users.query.admin", + "users"); + + context.TargetUserKeys.Should().BeEmpty(); + } } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserEndpointHandlerTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserEndpointHandlerTests.cs index 538d85ba..97ea6934 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserEndpointHandlerTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserEndpointHandlerTests.cs @@ -1733,6 +1733,205 @@ await f.Sut.DeleteUserIdentifierSelfAsync( f.Users.VerifyNoOtherCalls(); } + [Fact] + public async Task GetUsersByKeysAsync_WhenUnauthenticated_ReturnsUnauthorized() + { + var f = CreateFixture(authenticated: false); + + var result = await f.Sut.GetUsersByKeysAsync(f.HttpContext); + + result.Should().BeOfType(); + + f.AccessContextFactory.VerifyNoOtherCalls(); + f.Users.VerifyNoOtherCalls(); + } + + [Fact] + public async Task GetUsersByKeysAsync_WhenOver250DistinctKeys_ReturnsBadRequest() + { + var f = CreateFixture(); + + var keys = Enumerable.Range(0, 251) + .Select(_ => UserKey.New()) + .ToArray(); + + SetJsonBody( + f.HttpContext, + new GetUsersByKeysRequest { UserKeys = keys }); + + var result = await f.Sut.GetUsersByKeysAsync(f.HttpContext); + + result.Should().BeAssignableTo() + .Which.StatusCode.Should().Be(StatusCodes.Status400BadRequest); + + f.AccessContextFactory.VerifyNoOtherCalls(); + f.Users.VerifyNoOtherCalls(); + } + + [Fact] + public async Task GetUsersByKeysAsync_When250Keys_UsesSameTargetsForAuthorizationAndQuery() + { + var f = CreateFixture(); + + var keys = Enumerable.Range(0, 250) + .Select(_ => UserKey.New()) + .ToArray(); + + SetJsonBody( + f.HttpContext, + new GetUsersByKeysRequest { UserKeys = keys }); + + var accessContext = CreateAccessContext(f); + + f.AccessContextFactory + .Setup(x => x.CreateForUserTargetsAsync( + f.Flow, + UAuthActions.Users.GetByKeysAdmin, + "users", + It.Is>( + x => x.Count == 250 && + keys.All(x.Contains)), + f.HttpContext.RequestAborted)) + .ReturnsAsync(accessContext); + + IReadOnlyList expected = + Array.Empty(); + + f.Users + .Setup(x => x.GetUsersByKeysAsync( + accessContext, + It.Is>( + x => x.Count == 250 && + keys.All(x.Contains)), + f.HttpContext.RequestAborted)) + .ReturnsAsync(expected); + + var result = await f.Sut.GetUsersByKeysAsync(f.HttpContext); + + var ok = result.Should() + .BeOfType>>() + .Subject; + + ok.Value.Should().BeSameAs(expected); + + f.AccessContextFactory.Verify(x => + x.CreateForUserTargetsAsync( + f.Flow, + UAuthActions.Users.GetByKeysAdmin, + "users", + It.IsAny>(), + f.HttpContext.RequestAborted), + Times.Once); + + f.Users.Verify(x => + x.GetUsersByKeysAsync( + accessContext, + It.IsAny>(), + f.HttpContext.RequestAborted), + Times.Once); + } + + [Fact] + public async Task GetUsersByKeysAsync_ShouldRemoveDuplicatesBeforeAuthorization() + { + var f = CreateFixture(); + + var first = UserKey.New(); + var second = UserKey.New(); + + SetJsonBody( + f.HttpContext, + new GetUsersByKeysRequest + { + UserKeys = new[] { first, first, second } + }); + + var accessContext = CreateAccessContext(f); + + f.AccessContextFactory + .Setup(x => x.CreateForUserTargetsAsync( + f.Flow, + UAuthActions.Users.GetByKeysAdmin, + "users", + It.Is>( + x => x.Count == 2 && + x.Contains(first) && + x.Contains(second)), + f.HttpContext.RequestAborted)) + .ReturnsAsync(accessContext); + + f.Users + .Setup(x => x.GetUsersByKeysAsync( + accessContext, + It.IsAny>(), + f.HttpContext.RequestAborted)) + .ReturnsAsync(Array.Empty()); + + await f.Sut.GetUsersByKeysAsync(f.HttpContext); + + f.AccessContextFactory.Verify(x => + x.CreateForUserTargetsAsync( + f.Flow, + UAuthActions.Users.GetByKeysAdmin, + "users", + It.Is>(x => x.Count == 2), + f.HttpContext.RequestAborted), + Times.Once); + } + + [Fact] + public async Task GetUsersByKeysAsync_ShouldUseSameNormalizedKeysForBothLayers() + { + var f = CreateFixture(); + + var first = UserKey.New(); + var second = UserKey.New(); + + SetJsonBody( + f.HttpContext, + new GetUsersByKeysRequest + { + UserKeys = new[] { first, second, first } + }); + + IReadOnlyCollection? authorizedKeys = null; + IReadOnlyCollection? queriedKeys = null; + + var accessContext = CreateAccessContext(f); + + f.AccessContextFactory + .Setup(x => x.CreateForUserTargetsAsync( + f.Flow, + UAuthActions.Users.GetByKeysAdmin, + "users", + It.IsAny>(), + f.HttpContext.RequestAborted)) + .Callback, CancellationToken>( + (_, _, _, keys, _) => authorizedKeys = keys) + .ReturnsAsync(accessContext); + + f.Users + .Setup(x => x.GetUsersByKeysAsync( + accessContext, + It.IsAny>(), + f.HttpContext.RequestAborted)) + .Callback, + CancellationToken>( + (_, keys, _) => queriedKeys = keys) + .ReturnsAsync(Array.Empty()); + + await f.Sut.GetUsersByKeysAsync(f.HttpContext); + + authorizedKeys.Should().NotBeNull(); + queriedKeys.Should().NotBeNull(); + + ReferenceEquals(authorizedKeys, queriedKeys) + .Should().BeTrue(); + + queriedKeys.Should().BeEquivalentTo(new[] { first, second }); + } + // ============================================================ // Fixture // ============================================================ From 40a69d1e1f09587d1ba13057fc8487f4dba14d55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Mon, 26 Oct 2026 22:56:05 +0300 Subject: [PATCH 3/3] Add Tests and Docs Content --- docs/content/client-sdk/user-management.md | 158 +++++++++- docs/content/plugin-domains/policies.md | 162 ++++++++++ docs/content/plugin-domains/users.md | 75 +++++ .../security/authorization-pipeline.md | 2 + .../docs/client-sdk/user-management.json | 58 +++- .../wwwroot/docs/plugin-domains/policies.json | 34 ++- .../wwwroot/docs/plugin-domains/users.json | 29 +- .../docs/security/authorization-pipeline.json | 4 +- .../Stores/InMemoryUserSummaryQueryStore.cs | 2 +- .../UserSummaryQueryStoreContractTests.cs | 289 ++++++++++++++++++ .../Users/UserApplicationServiceTests.cs | 159 +++++++++- 11 files changed, 930 insertions(+), 42 deletions(-) diff --git a/docs/content/client-sdk/user-management.md b/docs/content/client-sdk/user-management.md index 68c785c4..694fe263 100644 --- a/docs/content/client-sdk/user-management.md +++ b/docs/content/client-sdk/user-management.md @@ -585,7 +585,141 @@ if (result.IsSuccess && result.Value is { } page) Use `PagedResult` rather than assuming every query returns the complete user collection. -## 13. Get a Specific User +## 13. Retrieve Users by Keys + +Use `GetByKeysAsync` to retrieve multiple user summaries when their `UserKey` values are already known. + +Unlike `QueryAsync`, which searches and paginates users, `GetByKeysAsync` retrieves a specific collection of users in a single request. + +This is particularly useful for applications that maintain relationships between users, such as: + +- Team members. +- Organization members. +- Project participants. +- Group memberships. +- Users assigned to a particular resource. + +Instead of requesting each user's information separately, the application can retrieve the required summaries in one operation. + +### Basic Usage + +```csharp +var userKeys = new[] +{ + firstUserKey, + secondUserKey, + thirdUserKey +}; + +var result = await UAuthClient.Users.GetByKeysAsync(userKeys); + +if (result.IsSuccess && result.Value is { } users) +{ + foreach (var user in users) + { + Console.WriteLine($"{user.DisplayName} ({user.UserName})"); + } +} +``` + +The method returns: + +```csharp +UAuthResult> +``` + +Each returned item uses the same `UserSummary` model described above. + +### Batch Size and Behavior + +A single request supports up to **250 distinct user keys**. + +The operation: + +- Accepts a collection of strongly typed `UserKey` values. +- Removes duplicate keys. +- Returns existing, non-deleted users within the applicable tenant. +- Omits user keys that do not match an available user. +- Returns an empty collection when no users match. +- Does not use pagination. +- Does not guarantee that the response follows the order of the requested keys. + +Requests exceeding 250 distinct keys are rejected with the `user_batch_size_exceeded` validation error. + +For larger collections, split the keys into batches: + +```csharp +var allUsers = new List(); + +foreach (var batch in userKeys.Distinct().Chunk(250)) +{ + var result = await UAuthClient.Users.GetByKeysAsync(batch); + + if (!result.IsSuccess || result.Value is null) + { + // Handle the failed batch. + return; + } + + allUsers.AddRange(result.Value); +} +``` + +For large datasets that require searching, filtering, sorting, or pagination, use `QueryAsync` instead. + +### QueryAsync vs. GetByKeysAsync + +| Capability | QueryAsync | GetByKeysAsync | +|---|---|---| +| Search users | Yes | No | +| Retrieve known user keys | Indirectly | Yes | +| Pagination | Yes | No | +| Filtering and sorting | Yes | No | +| Multiple specific users | Not optimized for this purpose | Yes | +| Maximum batch size | Pagination limits | 250 distinct keys | +| Response | `PagedResult` | `IReadOnlyList` | + +### Authorization + +`GetByKeysAsync` is a protected operation. + +It uses the action: + +```csharp +UAuthActions.Users.GetByKeysAdmin +``` + +The corresponding permission value is: + +```text +users.getbykeys.admin +``` + +Although the action uses the `.admin` suffix, access is determined by the server's authorization policies and the actor's effective permissions. + +Applications can authorize non-administrator users to retrieve specific groups of users when their policies explicitly allow it. + +For example, an application may allow team members to retrieve summaries of other members belonging to the same team while denying access to users outside that team. + +The server exposes the requested users through `AccessContext.TargetUserKeys`, allowing application policies to validate the complete target collection. + +**Important:** Possessing the `users.getbykeys.admin` permission does not automatically establish that every requested user belongs to an allowed group. Applications requiring group, organization, or resource-level isolation must enforce those restrictions through server-side authorization policies. + +For details, see [Policies & Access Control](/docs/plugin-domains/policies). + +### Performance Considerations + +Batch retrieval avoids issuing a separate client request for every user. + +The reference persistence implementations support retrieving multiple user summaries through a single batch operation. + +Actual database query counts and execution costs depend on the configured persistence provider. + +For example, an application displaying 50 team members can request their summaries together rather than making 50 individual `GetUserAsync` calls. + +Use `GetByKeysAsync` when user keys are already available. Use `GetUserAsync` when the application needs the detailed profile of one specific user. + +## 14. Get a Specific User Use `GetUserAsync` to retrieve a user's profile through the administrative API. @@ -618,7 +752,7 @@ var result = await UAuthClient.Users.GetUserAsync( The actor must have permission to access the target user's requested information. -## 14. Update Another User's Profile +## 15. Update Another User's Profile Use `UpdateUserProfileAsync` to update a selected user's profile. @@ -659,7 +793,7 @@ var result = await UAuthClient.Users.UpdateUserProfileAsync(userKey, Administrative profile updates require the appropriate authorization for the target user. -## 15. Manage Another User's Profiles +## 16. Manage Another User's Profiles Administrators can create and delete profiles for a selected user. @@ -694,7 +828,7 @@ Administrative profile operations do not automatically publish the same local `P Refresh affected administrative UI data explicitly when needed. -## 16. Understanding User Status +## 17. Understanding User Status UltimateAuth separates account lifecycle status from profile information. @@ -742,7 +876,7 @@ They do not mean every transition is permitted in every application context. The server validates the requested transition. -## 17. Change Your Own Status +## 18. Change Your Own Status Use `ChangeMyStatusAsync` for supported self-service status transitions. @@ -800,7 +934,7 @@ This is the current client event behavior, even though the operation changes acc Applications should not assume that a status change always produces a dedicated `AuthorizationChanged` event. -## 18. Change Another User's Status +## 19. Change Another User's Status Use `ChangeUserStatusAsync` for administrative lifecycle management. @@ -843,7 +977,7 @@ Whether a particular transition is permitted depends on the server's lifecycle r Do not assume that setting `Active` automatically restores every form of previously revoked authentication state. -## 19. Delete the Current User +## 20. Delete the Current User Use `DeleteMeAsync` to request deletion of the current account. @@ -872,7 +1006,7 @@ Before exposing a delete-account action, applications should provide an appropri The server remains responsible for authorization, data cleanup, and deletion policy enforcement. -## 20. Administrative User Deletion +## 21. Administrative User Deletion Administrators can delete users through `DeleteUserAsync`. @@ -943,7 +1077,7 @@ A previously deleted account may produce an operation-level success result indic Do not infer the complete persistence or retention behavior from the `DeleteMode` value alone. -## 21. Build a User Administration Table +## 22. Build a User Administration Table The following example demonstrates a basic Blazor user list using `QueryAsync`. @@ -1076,7 +1210,7 @@ A production administration interface should additionally include loading indica All administrative operations must remain protected by server-side authorization. -## 22. Handling User Operation Results +## 23. Handling User Operation Results UltimateAuth uses structured result contracts for user operations. @@ -1143,7 +1277,7 @@ Additional diagnostic fields include `CorrelationId`, `TraceId`, `Problem`, and Avoid exposing sensitive internal failure details directly to end users. -## 23. Client State Events +## 24. Client State Events `UAuthUserClient` publishes state events for successful self-service operations. @@ -1163,7 +1297,7 @@ For example, after updating another user's profile, an administration panel shou Client events help synchronize application state, but they do not replace authoritative server-side account status or permission checks. -## 24. Security and Lifecycle Considerations +## 25. Security and Lifecycle Considerations ### Self-Service and Administrative Boundaries diff --git a/docs/content/plugin-domains/policies.md b/docs/content/plugin-domains/policies.md index 44a66655..63c660aa 100644 --- a/docs/content/plugin-domains/policies.md +++ b/docs/content/plugin-domains/policies.md @@ -80,6 +80,168 @@ The actor and target are expected to represent the same identity. That relationship must be established from trusted context, not assumed from the action suffix. +### Multi-Target Authorization + +Some operations affect or retrieve multiple users within a single request. + +For example, an application may need to retrieve the summaries of several users belonging to a team. + +A traditional single-target authorization context identifies one target user: + +```csharp +context.TargetUserKey +``` + +Batch operations may not have a single target. + +UltimateAuth therefore also provides: + +```csharp +context.TargetUserKeys +``` + +This property contains the user keys associated with a multi-target operation. + +The collection is normalized, immutable, and available to authorization policies. + +| Property | Purpose | +|---|---| +| `TargetUserKey` | Identifies a single target user | +| `TargetUserKeys` | Identifies multiple target users | +| `HasTarget` | Indicates whether a single target is present | +| `HasMultipleTargets` | Indicates whether a batch target collection is present | + +Single-target and multi-target operations remain distinct. + +An operation targeting multiple users does not automatically become a self-service operation, even when the actor is included in the requested collection. + +### Creating a Multi-Target Context + +Server-side operations can construct a multi-target authorization context through `IAccessContextFactory`: + +```csharp +var accessContext = + await accessContextFactory.CreateForUserTargetsAsync( + authFlow: authFlow, + action: UAuthActions.Users.GetByKeysAdmin, + resource: "users", + targetUserKeys: userKeys, + ct: cancellationToken); +``` + +The factory preserves the existing actor, tenant, role resolution, and authorization context construction mechanisms. + +The requested target collection is supplied explicitly. + +The resulting context can be evaluated by the existing authorization pipeline without introducing a separate authorization mechanism. + +### Example: Restrict Batch Access to Group Members + +Consider an application where users belong to groups. + +A member may retrieve the summaries of other users in the same group, but must not retrieve users belonging to another group. + +The application can implement this restriction through an `IAccessPolicy`. + +The following example illustrates the target validation logic inside a policy: + +```csharp +if (context.Action == UAuthActions.Users.GetByKeysAdmin) +{ + var requestedKeys = context.TargetUserKeys; + + if (requestedKeys.Count == 0) + { + return AccessDecision.Deny("batch_targets_required"); + } + + var keys = requestedKeys.Distinct().ToArray(); + + var allowedCount = await db.GroupMembers + .AsNoTracking() + .CountAsync( + member => + member.GroupId == actorGroupId && + keys.Contains(member.UserKey), + cancellationToken); + + if (allowedCount != keys.Length) + { + return AccessDecision.Deny("batch_target_out_of_scope"); + } + + return AccessDecision.Allow(); +} +``` + +Here, `db`, `actorGroupId`, and `cancellationToken` represent application-specific dependencies and values resolved by the surrounding policy. + +The example assumes that each user has at most one membership record within the selected group. + +The policy verifies that every requested user belongs to the actor's authorized group. + +If any target falls outside the permitted scope, the entire operation is denied. + +This prevents a request from mixing authorized and unauthorized users to obtain information about another group. + +### Authorization Must Cover the Complete Batch + +A secure multi-target policy should evaluate every requested target. + +Checking only the first user or allowing the operation because at least one user is authorized is insufficient. + +For example: + +```text +Requested users: + User A — Group 1 + User B — Group 1 + User C — Group 2 + +Actor: + Member of Group 1 + +Decision: + Deny +``` + +The entire request is denied because User C is outside the actor's permitted scope. + +Applications should not silently return unauthorized users or assume that a batch operation is safe merely because its action permission was granted. + +### Permission and Scope Are Separate Requirements + +The batch user retrieval action is: + +```text +users.getbykeys.admin +``` + +An application may grant this permission to ordinary users when group-level access is required. + +However, permission evaluation and target scope validation solve different problems. + +- **Permission:** May this actor perform batch user retrieval? +- **Scope policy:** May this actor retrieve these particular users? + +Both requirements must be satisfied. + +An application's authorization policy must enforce the relevant target restrictions. + +### Trusted Target Collections + +Target keys originate from an operation request, but they are not automatically trusted authorization facts. + +The server must validate and normalize the requested keys before constructing the authorization context. + +The same normalized collection must be used for authorization and execution. + +Otherwise, a policy could approve one collection while the application service retrieves another. + +Applications should also ensure that batch authorization fails closed when required target information is missing. + +`TargetUserKeys` makes multi-target authorization possible; it does not automatically enforce group membership, ownership, or other application-specific restrictions. + ### Trusted Context Construction Authorization decisions are only as reliable as the information supplied to them. diff --git a/docs/content/plugin-domains/users.md b/docs/content/plugin-domains/users.md index 43beb4a8..3b5ffdbe 100644 --- a/docs/content/plugin-domains/users.md +++ b/docs/content/plugin-domains/users.md @@ -179,6 +179,81 @@ A profile attribute may still be relevant to application authorization or busine The reference model provides a starting point; applications remain responsible for deciding which additional user attributes belong in their own domain. +## Batch User Summary Retrieval + +Applications frequently maintain relationships between users without storing complete user profile information alongside those relationships. + +For example, a project management application may store a collection of user keys representing project members. + +When displaying those members, the application needs their current names, usernames, and other summary information. + +Retrieving each user independently can introduce unnecessary requests and repeated persistence operations. + +The Users domain addresses this through batch user summary retrieval. + +### IUserSummaryQueryStore + +The `IUserSummaryQueryStore` contract supports retrieving user summaries by their keys: + +```csharp +Task> GetByKeysAsync(IReadOnlyCollection userKeys, CancellationToken ct = default); +``` + +The operation retrieves summaries for a known collection of users within the store's tenant context. + +The reference implementations include support for Entity Framework Core and InMemory persistence. + +### Application Service and Authorization + +Batch retrieval is exposed through the Users application service: + +```csharp +GetUsersByKeysAsync(AccessContext context, IReadOnlyCollection userKeys, CancellationToken ct = default) +``` + +The application service coordinates authorization and persistence access. + +The operation is executed through the standard `AccessOrchestrator`, ensuring that batch retrieval participates in UltimateAuth's existing authorization pipeline. + +The public Client SDK exposes the functionality through: + +```csharp +UAuthClient.Users.GetByKeysAsync(userKeys) +``` + +### Target-Aware Access Control + +A batch request may target multiple users belonging to different groups, organizations, or application-defined security scopes. + +For this reason, UltimateAuth supports multiple target identities in `AccessContext`. + +A policy can inspect the complete target collection before allowing the operation. + +This makes it possible to implement restrictions such as: + +- Allowing access only to members of the actor's organization. +- Restricting visibility to users assigned to the same project. +- Limiting member directories to a particular group. +- Denying requests containing users outside an authorized scope. + +The Users domain does not define these application-specific membership rules. + +Instead, it provides the batch operation and authorization context needed for applications to enforce them. + +See [Policies & Access Control](/docs/plugin-domains/policies) for details. + +### Choosing the Appropriate Retrieval Method + +Use: + +- `GetUserAsync` for detailed information about one user. +- `QueryAsync` for searching, filtering, sorting, and paginating users. +- `GetByKeysAsync` for retrieving summaries of a known collection of users. + +Batch retrieval supports up to 250 distinct user keys per request. + +It is intended for targeted retrieval, not as a replacement for paginated user discovery. + ## Reference Implementation The Users Plugin Domain includes a reference implementation that demonstrates and provides a practical user management model. diff --git a/docs/content/security/authorization-pipeline.md b/docs/content/security/authorization-pipeline.md index 9964c32b..10e7347b 100644 --- a/docs/content/security/authorization-pipeline.md +++ b/docs/content/security/authorization-pipeline.md @@ -85,6 +85,8 @@ Conceptually, its responsibilities include: The context is shared across the authorization process so that individual policies do not need to independently reconstruct the operation. +> Authorization contexts can represent either a single target user through TargetUserKey or multiple target users through TargetUserKeys. Multi-target operations use the same authorization pipeline as single-target operations. Application policies are responsible for validating the complete target collection when access depends on membership, ownership, or another resource-specific scope. + ### Actor and Target Separation Consider the action: diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/client-sdk/user-management.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/client-sdk/user-management.json index e07a1979..89a7e8ca 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/client-sdk/user-management.json +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/client-sdk/user-management.json @@ -1,7 +1,7 @@ { "Slug": "client-sdk/user-management", "Title": "User Management", - "Html": "\n\u003Cp\u003EThe UltimateAuth Client SDK provides a comprehensive API for managing users, profiles, account status, and user lifecycle operations.\u003C/p\u003E\n\u003Cp\u003EThrough \u003Ccode\u003EUAuthClient.Users\u003C/code\u003E, applications can implement account settings, profile management, user registration, and administrative user management without manually constructing API requests.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth distinguishes between \u003Cstrong\u003Euser identity\u003C/strong\u003E, \u003Cstrong\u003Euser profiles\u003C/strong\u003E, and \u003Cstrong\u003Eaccount lifecycle state\u003C/strong\u003E.\u003C/p\u003E\n\u003Cp\u003EA user has a stable identity and may have multiple profiles. Account status controls lifecycle behavior independently of profile data.\u003C/p\u003E\n\u003Cp\u003EThis guide covers the public \u003Ccode\u003EIUserClient\u003C/code\u003E API, including self-service and administrative operations, structured results, pagination, multi-profile support, and important update semantics.\u003C/p\u003E\n\u003Cp\u003EFor email addresses, usernames, phone numbers, and identifier verification, see \u003Ca href=\u0022/docs/client/identifiers\u0022\u003EIdentifiers\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022accessing-the-user-client\u0022\u003E1. Accessing the User Client\u003C/h2\u003E\n\u003Cp\u003EIn a Blazor component, inject \u003Ccode\u003EIUAuthClient\u003C/code\u003E:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@using CodeBeam.UltimateAuth.Client\n@inject IUAuthClient UAuthClient\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUser operations are available through:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthClient.Users\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe \u003Ccode\u003EUsers\u003C/code\u003E property exposes \u003Ccode\u003EIUserClient\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EIts operations are divided into three functional areas:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EArea\u003C/th\u003E\n\u003Cth\u003ECapabilities\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EUser lifecycle\u003C/td\u003E\n\u003Ctd\u003ECreate users, change status, delete accounts\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ESelf-service profiles\u003C/td\u003E\n\u003Ctd\u003ERetrieve, update, create, and delete your own profiles\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAdministration\u003C/td\u003E\n\u003Ctd\u003EQuery users and manage another user\u0027s account and profiles\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003ESelf-service methods operate on the authenticated user.\u003C/p\u003E\n\u003Cp\u003EAdministrative methods operate on an explicitly identified target and require the appropriate server-side authorization.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022get-the-current-user\u0022\u003E2. Get the Current User\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EGetMeAsync\u003C/code\u003E to retrieve the current user\u0027s profile.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.GetMeAsync();\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } user)\n{\n Console.WriteLine(user.DisplayName);\n Console.WriteLine(user.Status);\n Console.WriteLine(user.UserKey);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthResult\u0026lt;UserView\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWhen no profile is specified, UltimateAuth returns the default profile.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022userview\u0022\u003EUserView\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EUserView\u003C/code\u003E contains account information and the selected profile\u0027s data.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserKey\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EStable user identifier\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EStatus\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECurrent account status\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUsername\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPrimaryEmail\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPrimary email address\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPrimaryPhone\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPrimary phone number\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EProfileKey\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESelected profile\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EFirstName\u003C/code\u003E, \u003Ccode\u003ELastName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EProfile name fields\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDisplayName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EDisplay name\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EBio\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EBiography\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EBirthDate\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EBirth date\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EGender\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EProfile gender field\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELanguage\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPreferred language\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETimeZone\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPreferred time zone\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECulture\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECulture preference\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEmailVerified\u003C/code\u003E, \u003Ccode\u003EPhoneVerified\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifier verification information\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECreatedAt\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAccount creation timestamp\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMetadata\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECustom profile metadata\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe returned profile is a view of server-managed user information.\u003C/p\u003E\n\u003Cp\u003EIt should not be treated as a substitute for authoritative authentication or authorization state.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022update-the-current-users-profile\u0022\u003E3. Update the Current User\u0027s Profile\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EUpdateMyProfileAsync\u003C/code\u003E to update the authenticated user\u0027s profile.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.UpdateMyProfileAsync(\n new UpdateProfileRequest\n {\n DisplayName = \u0026quot;John Doe\u0026quot;,\n FirstName = \u0026quot;John\u0026quot;,\n LastName = \u0026quot;Doe\u0026quot;,\n Language = \u0026quot;en\u0026quot;,\n TimeZone = \u0026quot;Europe/London\u0026quot;\n });\n\nif (result.IsSuccess)\n{\n // Profile update succeeded.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns \u003Ccode\u003EUAuthResult\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022important-profile-updates-replace-editable-fields\u0022\u003EImportant: Profile Updates Replace Editable Fields\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EUpdateProfileRequest\u003C/code\u003E uses replacement semantics rather than partial-update semantics.\u003C/p\u003E\n\u003Cp\u003EThis means:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EEditable fields supplied in the request replace their previous values.\u003C/li\u003E\n\u003Cli\u003EExplicit \u003Ccode\u003Enull\u003C/code\u003E values clear existing values.\u003C/li\u003E\n\u003Cli\u003EOmitted editable properties are also treated as \u003Ccode\u003Enull\u003C/code\u003E.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eawait UAuthClient.Users.UpdateMyProfileAsync(\n new UpdateProfileRequest\n {\n DisplayName = \u0026quot;John Doe\u0026quot;\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis does \u003Cstrong\u003Enot\u003C/strong\u003E mean \u201Cchange only the display name.\u201D\u003C/p\u003E\n\u003Cp\u003EOther editable profile fields may be cleared.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022recommended-read-modify-and-submit\u0022\u003ERecommended: Read, Modify, and Submit\u003C/h3\u003E\n\u003Cp\u003EWhen building a profile editor, retrieve the current profile and preserve values that should remain unchanged.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar currentResult = await UAuthClient.Users.GetMeAsync();\n\nif (!currentResult.IsSuccess || currentResult.Value is not { } current)\n{\n return;\n}\n\nvar update = new UpdateProfileRequest\n{\n FirstName = current.FirstName,\n LastName = current.LastName,\n DisplayName = \u0026quot;John Doe\u0026quot;,\n BirthDate = current.BirthDate,\n Gender = current.Gender,\n Bio = current.Bio,\n Language = current.Language,\n TimeZone = current.TimeZone,\n Culture = current.Culture,\n Metadata = current.Metadata\n};\n\nvar result = await UAuthClient.Users.UpdateMyProfileAsync(update);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis preserves the values returned by the initial read, but it is not an atomic patch operation.\u003C/p\u003E\n\u003Cp\u003EIf another request modifies the profile between the read and update, the later replacement may overwrite those changes.\u003C/p\u003E\n\u003Cp\u003EApplications with concurrent profile editing requirements should account for this behavior.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022profile-state-events\u0022\u003EProfile State Events\u003C/h3\u003E\n\u003Cp\u003EAfter a successful \u003Ccode\u003EUpdateMyProfileAsync\u003C/code\u003E operation, the client publishes a \u003Ccode\u003EProfileChanged\u003C/code\u003E event.\u003C/p\u003E\n\u003Cp\u003EThe event carries the \u003Ccode\u003EUpdateProfileRequest\u003C/code\u003E used for the operation.\u003C/p\u003E\n\u003Cp\u003EThe configured client state event handling mode determines how the application processes the notification.\u003C/p\u003E\n\u003Cp\u003EThe event is published only when the SDK operation reports success.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022understanding-multiple-profiles\u0022\u003E4. Understanding Multiple Profiles\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth supports multiple profiles for a single user when enabled through server configuration.\u003C/p\u003E\n\u003Cp\u003EFor example, one account may have:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EUser\n |\n \u002B-- default\n |\n \u002B-- business\n |\n \u002B-- public\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAll profiles belong to the same user identity.\u003C/p\u003E\n\u003Cp\u003EThey do not represent separate authentication accounts or independently authenticated principals.\u003C/p\u003E\n\u003Cp\u003EProfiles allow applications to organize different sets of user-facing information under one account.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022default-profile\u0022\u003EDefault Profile\u003C/h3\u003E\n\u003Cp\u003EEvery newly created user receives a default profile.\u003C/p\u003E\n\u003Cp\u003EIts key is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EProfileKey.Default\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe default profile cannot be deleted.\u003C/p\u003E\n\u003Cp\u003EWhen a profile request omits \u003Ccode\u003EProfileKey\u003C/code\u003E, the default profile is selected.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022profile-keys\u0022\u003EProfile Keys\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EProfileKey\u003C/code\u003E is a strongly typed value.\u003C/p\u003E\n\u003Cp\u003EUse \u003Ccode\u003ETryCreate\u003C/code\u003E to construct a custom key:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eif (!ProfileKey.TryCreate(\u0026quot;business\u0026quot;, out var profileKey))\n{\n throw new InvalidOperationException(\u0026quot;Invalid profile key.\u0026quot;);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EProfile keys are normalized by trimming whitespace and converting the value to lowercase.\u003C/p\u003E\n\u003Cp\u003EA valid key must not be empty or exceed 64 characters.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022retrieve-a-specific-profile\u0022\u003E5. Retrieve a Specific Profile\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EGetProfileRequest\u003C/code\u003E to select a profile.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eif (!ProfileKey.TryCreate(\u0026quot;business\u0026quot;, out var profileKey))\n{\n return;\n}\n\nvar result = await UAuthClient.Users.GetMeAsync(\n new GetProfileRequest\n {\n ProfileKey = profileKey\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } profile)\n{\n Console.WriteLine(profile.DisplayName);\n Console.WriteLine(profile.ProfileKey);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe response is still \u003Ccode\u003EUserView\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe account identity and status remain associated with the same user; the selected profile determines which profile information is returned.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022create-an-additional-profile\u0022\u003E6. Create an Additional Profile\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003ECreateMyProfileAsync\u003C/code\u003E to create a new profile for the authenticated user.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eif (!ProfileKey.TryCreate(\u0026quot;business\u0026quot;, out var profileKey))\n{\n return;\n}\n\nvar result = await UAuthClient.Users.CreateMyProfileAsync(\n new CreateProfileRequest\n {\n ProfileKey = profileKey,\n DisplayName = \u0026quot;John Doe Consulting\u0026quot;,\n Bio = \u0026quot;Business profile\u0026quot;,\n Language = \u0026quot;en\u0026quot;\n });\n\nif (result.IsSuccess)\n{\n // Profile created.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003ECreateProfileRequest\u003C/code\u003E requires \u003Ccode\u003EProfileKey\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EIt also supports the same general profile information used for user-facing profiles.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022clone-an-existing-profile\u0022\u003EClone an Existing Profile\u003C/h3\u003E\n\u003Cp\u003EA new profile can be created using an existing profile as its source.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EProfileKey.TryCreate(\u0026quot;business\u0026quot;, out var businessKey);\n\nvar result = await UAuthClient.Users.CreateMyProfileAsync(\n new CreateProfileRequest\n {\n ProfileKey = businessKey,\n CloneFrom = ProfileKey.Default\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003ECloneFrom\u003C/code\u003E identifies the source profile.\u003C/p\u003E\n\u003Cp\u003EThis is useful when creating a new profile that should initially reuse information from an existing one.\u003C/p\u003E\n\u003Cp\u003EThe exact merge and override behavior is determined by the server\u0027s profile creation implementation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022profile-creation-events\u0022\u003EProfile Creation Events\u003C/h3\u003E\n\u003Cp\u003EAfter successful self-service profile creation, the SDK publishes \u003Ccode\u003EProfileChanged\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThis allows application components to refresh profile-related views.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022update-a-specific-profile\u0022\u003E7. Update a Specific Profile\u003C/h2\u003E\n\u003Cp\u003ESet \u003Ccode\u003EProfileKey\u003C/code\u003E in \u003Ccode\u003EUpdateProfileRequest\u003C/code\u003E to select the profile being updated.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EProfileKey.TryCreate(\u0026quot;business\u0026quot;, out var businessKey);\n\nvar result = await UAuthClient.Users.UpdateMyProfileAsync(\n new UpdateProfileRequest\n {\n ProfileKey = businessKey,\n DisplayName = \u0026quot;John Doe Consulting\u0026quot;,\n Bio = \u0026quot;Updated business profile\u0026quot;\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe same replacement semantics apply to custom profiles.\u003C/p\u003E\n\u003Cp\u003EIf the application intends to preserve existing fields, retrieve the selected profile first and include the values that should remain unchanged.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022delete-a-profile\u0022\u003E8. Delete a Profile\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EDeleteMyProfileAsync\u003C/code\u003E to remove a custom profile.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EProfileKey.TryCreate(\u0026quot;business\u0026quot;, out var businessKey);\n\nvar result = await UAuthClient.Users.DeleteMyProfileAsync(\n new DeleteProfileRequest\n {\n ProfileKey = businessKey\n });\n\nif (result.IsSuccess)\n{\n // Profile deleted.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe default profile cannot be deleted.\u003C/p\u003E\n\u003Cp\u003EDeleting a profile is different from deleting the user account.\u003C/p\u003E\n\u003Cp\u003EThe user identity and other profiles remain subject to their own lifecycle and server-side state.\u003C/p\u003E\n\u003Cp\u003EOn successful self-service profile deletion, the SDK publishes \u003Ccode\u003EProfileChanged\u003C/code\u003E with the deleted profile key.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022create-a-user\u0022\u003E9. Create a User\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003ECreateAsync\u003C/code\u003E to request user creation through the standard user creation endpoint.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.CreateAsync(\n new CreateUserRequest\n {\n UserName = \u0026quot;johndoe\u0026quot;,\n Email = \u0026quot;john@example.com\u0026quot;,\n Password = password,\n FirstName = \u0026quot;John\u0026quot;,\n LastName = \u0026quot;Doe\u0026quot;,\n DisplayName = \u0026quot;John Doe\u0026quot;\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { IsSuccess: true } created)\n{\n Console.WriteLine($\u0026quot;Created user: {created.UserKey}\u0026quot;);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthResult\u0026lt;UserCreateResult\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUser creation availability depends on the server\u0027s configuration and access policies.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022createuserrequest\u0022\u003ECreateUserRequest\u003C/h3\u003E\n\u003Cp\u003EThe request supports several groups of fields:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003ECategory\u003C/th\u003E\n\u003Cth\u003EProperties\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EIdentifiers\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserName\u003C/code\u003E, \u003Ccode\u003EEmail\u003C/code\u003E, \u003Ccode\u003EPhone\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EVerification flags\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserNameVerified\u003C/code\u003E, \u003Ccode\u003EEmailVerified\u003C/code\u003E, \u003Ccode\u003EPhoneVerified\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ECredentials\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EPassword\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EProfile\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EFirstName\u003C/code\u003E, \u003Ccode\u003ELastName\u003C/code\u003E, \u003Ccode\u003EDisplayName\u003C/code\u003E, \u003Ccode\u003EBirthDate\u003C/code\u003E, \u003Ccode\u003EGender\u003C/code\u003E, \u003Ccode\u003EBio\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EPreferences\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003ELanguage\u003C/code\u003E, \u003Ccode\u003ETimeZone\u003C/code\u003E, \u003Ccode\u003ECulture\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EExtensions\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EMetadata\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EVerification flags must not be treated as client-controlled proof of identifier ownership.\u003C/p\u003E\n\u003Cp\u003EThe server is responsible for enforcing verification requirements and determining whether a caller is authorized to establish verified identifier state.\u003C/p\u003E\n\u003Cp\u003EFor identifier-specific operations and verification workflows, see \u003Ca href=\u0022/docs/client/identifiers\u0022\u003EIdentifiers\u003C/a\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022usercreateresult\u0022\u003EUserCreateResult\u003C/h3\u003E\n\u003Cp\u003EThe returned value includes:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Epublic sealed record UserCreateResult\n{\n public required bool IsSuccess { get; init; }\n public string? UserKey { get; init; }\n public string? FailureReason { get; init; }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe \u003Ccode\u003EUserKey\u003C/code\u003E is returned as a string when creation succeeds.\u003C/p\u003E\n\u003Cp\u003ECheck both the transport-level \u003Ccode\u003EUAuthResult.IsSuccess\u003C/code\u003E and the operation-level \u003Ccode\u003EUserCreateResult.IsSuccess\u003C/code\u003E where applicable.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022administrative-user-creation\u0022\u003E10. Administrative User Creation\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003ECreateAsAdminAsync\u003C/code\u003E when creating a user through an administrative context.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.CreateAsAdminAsync(\n new CreateUserRequest\n {\n UserName = \u0026quot;newemployee\u0026quot;,\n Email = \u0026quot;employee@example.com\u0026quot;,\n DisplayName = \u0026quot;New Employee\u0026quot;\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { IsSuccess: true } created)\n{\n Console.WriteLine(created.UserKey);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAdministrative creation is distinct from ordinary user creation.\u003C/p\u003E\n\u003Cp\u003EThe server enforces the required permissions and applies the relevant user creation policies.\u003C/p\u003E\n\u003Cp\u003EApplications should not assume that calling the administrative endpoint bypasses validation or identifier ownership rules.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022query-users\u0022\u003E11. Query Users\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EQueryAsync\u003C/code\u003E to search and paginate users.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.QueryAsync(\n new UserQuery\n {\n Search = \u0026quot;john\u0026quot;,\n PageNumber = 1,\n PageSize = 20\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } page)\n{\n foreach (var user in page.Items)\n {\n Console.WriteLine(user.DisplayName);\n Console.WriteLine(user.Status);\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthResult\u0026lt;PagedResult\u0026lt;UserSummary\u0026gt;\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022userquery-filters\u0022\u003EUserQuery Filters\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EUserQuery\u003C/code\u003E extends \u003Ccode\u003EPageRequest\u003C/code\u003E and supports:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESearch\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESearch term\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EStatus\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EFilter by user status\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIncludeDeleted\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EInclude deleted users when authorized\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EProfileKey\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESelect the profile context for the query\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPageNumber\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPage number\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPageSize\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPage size\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESortBy\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional sort field\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDescending\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EDescending sort order\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022search-behavior\u0022\u003ESearch Behavior\u003C/h3\u003E\n\u003Cp\u003EThe query model documents case-insensitive partial matching for:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EDisplay name.\u003C/li\u003E\n\u003Cli\u003EUsername.\u003C/li\u003E\n\u003Cli\u003EPrimary email.\u003C/li\u003E\n\u003Cli\u003EPrimary phone.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EA complete valid \u003Ccode\u003EUserKey\u003C/code\u003E can also be used as a search term.\u003C/p\u003E\n\u003Cp\u003EPartial \u003Ccode\u003EUserKey\u003C/code\u003E matching is not supported.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022filter-by-status\u0022\u003EFilter by Status\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.QueryAsync(\n new UserQuery\n {\n Status = UserStatus.Active,\n PageNumber = 1,\n PageSize = 20\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022include-deleted-users\u0022\u003EInclude Deleted Users\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.QueryAsync(\n new UserQuery\n {\n IncludeDeleted = true,\n PageNumber = 1,\n PageSize = 20\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe ability to query deleted accounts depends on server-side authorization and data retention behavior.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022usersummary-and-pagination\u0022\u003E12. UserSummary and Pagination\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EQueryAsync\u003C/code\u003E returns \u003Ccode\u003EUserSummary\u003C/code\u003E items.\u003C/p\u003E\n\u003Cp\u003EEach summary contains:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserKey\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUser identifier\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUsername\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDisplayName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EDisplay name\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPrimaryEmail\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPrimary email\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPrimaryPhone\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPrimary phone\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EStatus\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAccount status\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECreatedAt\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECreation timestamp\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThis model is suitable for administrative user lists.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022navigating-pages\u0022\u003ENavigating Pages\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.QueryAsync(\n new UserQuery\n {\n PageNumber = 2,\n PageSize = 25\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } page)\n{\n Console.WriteLine($\u0026quot;Total users: {page.TotalCount}\u0026quot;);\n Console.WriteLine($\u0026quot;Current page: {page.PageNumber}\u0026quot;);\n Console.WriteLine($\u0026quot;Has next page: {page.HasNext}\u0026quot;);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUse \u003Ccode\u003EPagedResult\u0026lt;T\u0026gt;\u003C/code\u003E rather than assuming every query returns the complete user collection.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022get-a-specific-user\u0022\u003E13. Get a Specific User\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EGetUserAsync\u003C/code\u003E to retrieve a user\u0027s profile through the administrative API.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.GetUserAsync(userKey);\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } user)\n{\n Console.WriteLine(user.DisplayName);\n Console.WriteLine(user.Status);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns \u003Ccode\u003EUAuthResult\u0026lt;UserView\u0026gt;\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EWhen no profile is specified, the default profile is selected.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022retrieve-a-specific-user-profile\u0022\u003ERetrieve a Specific User Profile\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EProfileKey.TryCreate(\u0026quot;business\u0026quot;, out var businessKey);\n\nvar result = await UAuthClient.Users.GetUserAsync(\n userKey,\n new GetProfileRequest\n {\n ProfileKey = businessKey\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe actor must have permission to access the target user\u0027s requested information.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022update-another-users-profile\u0022\u003E14. Update Another User\u0027s Profile\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EUpdateUserProfileAsync\u003C/code\u003E to update a selected user\u0027s profile.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.UpdateUserProfileAsync(\n userKey,\n new UpdateProfileRequest\n {\n DisplayName = \u0026quot;Updated Display Name\u0026quot;,\n FirstName = \u0026quot;John\u0026quot;,\n LastName = \u0026quot;Doe\u0026quot;\n });\n\nif (result.IsSuccess)\n{\n // Profile updated.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis operation uses the same replacement semantics as \u003Ccode\u003EUpdateMyProfileAsync\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EOmitted editable fields may be cleared.\u003C/p\u003E\n\u003Cp\u003EFor partial UI editing, retrieve the target profile and preserve values that should not change.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022update-a-named-profile\u0022\u003EUpdate a Named Profile\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EProfileKey.TryCreate(\u0026quot;business\u0026quot;, out var businessKey);\n\nvar result = await UAuthClient.Users.UpdateUserProfileAsync(userKey,\n new UpdateProfileRequest\n {\n ProfileKey = businessKey,\n DisplayName = \u0026quot;Updated Business Profile\u0026quot;\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAdministrative profile updates require the appropriate authorization for the target user.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022manage-another-users-profiles\u0022\u003E15. Manage Another User\u0027s Profiles\u003C/h2\u003E\n\u003Cp\u003EAdministrators can create and delete profiles for a selected user.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022create-a-profile\u0022\u003ECreate a Profile\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EProfileKey.TryCreate(\u0026quot;business\u0026quot;, out var businessKey);\n\nvar result = await UAuthClient.Users.CreateUserProfileAsync(userKey,\n new CreateProfileRequest\n {\n ProfileKey = businessKey,\n DisplayName = \u0026quot;Business Profile\u0026quot;\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022delete-a-profile-1\u0022\u003EDelete a Profile\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EProfileKey.TryCreate(\u0026quot;business\u0026quot;, out var businessKey);\n\nvar result = await UAuthClient.Users.DeleteUserProfileAsync(userKey,\n new DeleteProfileRequest\n {\n ProfileKey = businessKey\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe default profile cannot be deleted.\u003C/p\u003E\n\u003Cp\u003EAdministrative profile operations do not automatically publish the same local \u003Ccode\u003EProfileChanged\u003C/code\u003E event as self-service profile operations.\u003C/p\u003E\n\u003Cp\u003ERefresh affected administrative UI data explicitly when needed.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022understanding-user-status\u0022\u003E16. Understanding User Status\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth separates account lifecycle status from profile information.\u003C/p\u003E\n\u003Cp\u003EA user may be active, suspended, disabled, locked, awaiting activation, or in another supported lifecycle state.\u003C/p\u003E\n\u003Cp\u003EStatus can affect authentication eligibility, session continuation, and authorization.\u003C/p\u003E\n\u003Cp\u003EHowever, the exact enforcement behavior depends on the configured server policies and authentication mode.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022self-service-and-administrative-statuses\u0022\u003ESelf-Service and Administrative Statuses\u003C/h3\u003E\n\u003Cp\u003EUltimateAuth exposes separate status enums for self-service and administrative operations.\u003C/p\u003E\n\u003Cp\u003EThis prevents the client contract from treating every administrative lifecycle transition as a self-service capability.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003ESelfAssignableUserStatus\u003C/strong\u003E\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Epublic enum SelfAssignableUserStatus\n{\n Active = 0,\n SelfSuspended = 10\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Cstrong\u003EAdminAssignableUserStatus\u003C/strong\u003E\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Epublic enum AdminAssignableUserStatus\n{\n Active = 0,\n Disabled = 20,\n Suspended = 30,\n Locked = 40,\n RiskHold = 50,\n PendingActivation = 60,\n PendingVerification = 70,\n Unknown = 100\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThese enums describe requestable statuses.\u003C/p\u003E\n\u003Cp\u003EThey do not mean every transition is permitted in every application context.\u003C/p\u003E\n\u003Cp\u003EThe server validates the requested transition.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022change-your-own-status\u0022\u003E17. Change Your Own Status\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EChangeMyStatusAsync\u003C/code\u003E for supported self-service status transitions.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.ChangeMyStatusAsync(\n new ChangeUserStatusSelfRequest\n {\n NewStatus = SelfAssignableUserStatus.SelfSuspended\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { IsSuccess: true } change)\n{\n Console.WriteLine($\u0026quot;New status: {change.CurrentStatus}\u0026quot;);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis operation can be used to implement account self-suspension.\u003C/p\u003E\n\u003Cp\u003EThe server determines whether the transition is permitted.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022userstatuschangeresult\u0022\u003EUserStatusChangeResult\u003C/h3\u003E\n\u003Cp\u003EThe result model includes:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIsSuccess\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EWhether the status operation succeeded\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPreviousStatus\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EStatus before the operation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECurrentStatus\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EStatus after the operation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EFailureReason\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional failure description\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } change)\n{\n if (change.IsSuccess)\n {\n Console.WriteLine($\u0026quot;{change.PreviousStatus} -\u0026gt; {change.CurrentStatus}\u0026quot;);\n }\n else\n {\n Console.WriteLine(change.FailureReason ?? \u0026quot;Status change failed.\u0026quot;);\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe result may also report a successful no-change outcome when the requested state already matches the current state.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022state-event\u0022\u003EState Event\u003C/h3\u003E\n\u003Cp\u003EAfter a successful SDK operation, \u003Ccode\u003EChangeMyStatusAsync\u003C/code\u003E publishes \u003Ccode\u003EProfileChanged\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThis is the current client event behavior, even though the operation changes account status rather than editable profile fields.\u003C/p\u003E\n\u003Cp\u003EApplications should not assume that a status change always produces a dedicated \u003Ccode\u003EAuthorizationChanged\u003C/code\u003E event.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022change-another-users-status\u0022\u003E18. Change Another User\u0027s Status\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EChangeUserStatusAsync\u003C/code\u003E for administrative lifecycle management.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.ChangeUserStatusAsync(userKey,\n new ChangeUserStatusAdminRequest\n {\n NewStatus = AdminAssignableUserStatus.Suspended\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { IsSuccess: true } change)\n{\n Console.WriteLine($\u0026quot;Current status: {change.CurrentStatus}\u0026quot;);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis operation requires administrative authorization.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-disable-an-account\u0022\u003EExample: Disable an Account\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.ChangeUserStatusAsync(userKey,\n new ChangeUserStatusAdminRequest\n {\n NewStatus = AdminAssignableUserStatus.Disabled\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-restore-an-active-status\u0022\u003EExample: Restore an Active Status\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.ChangeUserStatusAsync(userKey,\n new ChangeUserStatusAdminRequest\n {\n NewStatus = AdminAssignableUserStatus.Active\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWhether a particular transition is permitted depends on the server\u0027s lifecycle rules.\u003C/p\u003E\n\u003Cp\u003EDo not assume that setting \u003Ccode\u003EActive\u003C/code\u003E automatically restores every form of previously revoked authentication state.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022delete-the-current-user\u0022\u003E19. Delete the Current User\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EDeleteMeAsync\u003C/code\u003E to request deletion of the current account.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.DeleteMeAsync();\n\nif (result.IsSuccess)\n{\n // Account deletion succeeded.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ESelf-service deletion is a soft-delete operation.\u003C/p\u003E\n\u003Cp\u003EAccording to the \u003Ccode\u003EIUserClient\u003C/code\u003E contract, this operation removes associated profiles and sessions.\u003C/p\u003E\n\u003Cp\u003EAfter successful deletion, the client publishes a \u003Ccode\u003EUserDeleted\u003C/code\u003E state event using patch handling mode.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022important-considerations\u0022\u003EImportant Considerations\u003C/h3\u003E\n\u003Cp\u003EAccount deletion is not equivalent to ordinary logout.\u003C/p\u003E\n\u003Cp\u003EIt changes the user\u0027s lifecycle state and may affect associated data across multiple domains.\u003C/p\u003E\n\u003Cp\u003EBefore exposing a delete-account action, applications should provide an appropriate confirmation flow and explain the consequences.\u003C/p\u003E\n\u003Cp\u003EThe server remains responsible for authorization, data cleanup, and deletion policy enforcement.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022administrative-user-deletion\u0022\u003E20. Administrative User Deletion\u003C/h2\u003E\n\u003Cp\u003EAdministrators can delete users through \u003Ccode\u003EDeleteUserAsync\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe method accepts a \u003Ccode\u003EDeleteUserRequest\u003C/code\u003E specifying the deletion mode.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022soft-delete\u0022\u003ESoft Delete\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.DeleteUserAsync(userKey,\n new DeleteUserRequest\n {\n Mode = DeleteMode.Soft\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { IsSuccess: true } deleted)\n{\n Console.WriteLine($\u0026quot;Deletion mode: {deleted.Mode}\u0026quot;);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022hard-delete\u0022\u003EHard Delete\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.DeleteUserAsync(userKey,\n new DeleteUserRequest\n {\n Mode = DeleteMode.Hard\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EHard deletion is a more destructive operation and should be restricted to appropriately authorized administrative workflows.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022deletemode\u0022\u003EDeleteMode\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Epublic enum DeleteMode\n{\n Soft = 0,\n Hard = 10\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022userdeleteresult\u0022\u003EUserDeleteResult\u003C/h3\u003E\n\u003Cp\u003EThe operation returns \u003Ccode\u003EUAuthResult\u0026lt;UserDeleteResult\u0026gt;\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EUserDeleteResult\u003C/code\u003E includes:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003EIsSuccess\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EMode\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EFailureReason\u003C/code\u003E\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eif (!result.IsSuccess)\n{\n // Handle transport or API-level failure.\n}\nelse if (result.Value is { IsSuccess: false } deletion)\n{\n // Handle the operation-level failure.\n Console.WriteLine(deletion.FailureReason);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EA previously deleted account may produce an operation-level success result indicating that no additional deletion was necessary.\u003C/p\u003E\n\u003Cp\u003EDo not infer the complete persistence or retention behavior from the \u003Ccode\u003EDeleteMode\u003C/code\u003E value alone.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022build-a-user-administration-table\u0022\u003E21. Build a User Administration Table\u003C/h2\u003E\n\u003Cp\u003EThe following example demonstrates a basic Blazor user list using \u003Ccode\u003EQueryAsync\u003C/code\u003E.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@using CodeBeam.UltimateAuth.Client\n@using CodeBeam.UltimateAuth.Core.Contracts\n@using CodeBeam.UltimateAuth.Users.Contracts\n@inject IUAuthClient UAuthClient\n\n\u0026lt;h3\u0026gt;Users\u0026lt;/h3\u0026gt;\n\n\u0026lt;input @bind=\u0026quot;_search\u0026quot; placeholder=\u0026quot;Search users\u0026quot; /\u0026gt;\n\u0026lt;button @onclick=\u0026quot;SearchAsync\u0026quot;\u0026gt;Search\u0026lt;/button\u0026gt;\n\n@if (_loading)\n{\n \u0026lt;p\u0026gt;Loading users...\u0026lt;/p\u0026gt;\n}\nelse if (_error is not null)\n{\n \u0026lt;p role=\u0026quot;alert\u0026quot;\u0026gt;@_error\u0026lt;/p\u0026gt;\n}\nelse\n{\n \u0026lt;table\u0026gt;\n \u0026lt;thead\u0026gt;\n \u0026lt;tr\u0026gt;\n \u0026lt;th\u0026gt;Name\u0026lt;/th\u0026gt;\n \u0026lt;th\u0026gt;Username\u0026lt;/th\u0026gt;\n \u0026lt;th\u0026gt;Status\u0026lt;/th\u0026gt;\n \u0026lt;/tr\u0026gt;\n \u0026lt;/thead\u0026gt;\n \u0026lt;tbody\u0026gt;\n @foreach (var user in _users)\n {\n \u0026lt;tr @key=\u0026quot;user.UserKey\u0026quot;\u0026gt;\n \u0026lt;td\u0026gt;@user.DisplayName\u0026lt;/td\u0026gt;\n \u0026lt;td\u0026gt;@user.UserName\u0026lt;/td\u0026gt;\n \u0026lt;td\u0026gt;@user.Status\u0026lt;/td\u0026gt;\n \u0026lt;/tr\u0026gt;\n }\n \u0026lt;/tbody\u0026gt;\n \u0026lt;/table\u0026gt;\n\n \u0026lt;button @onclick=\u0026quot;PreviousPageAsync\u0026quot; disabled=\u0026quot;@(_pageNumber \u0026lt;= 1)\u0026quot;\u0026gt;Previous\u0026lt;/button\u0026gt;\n \u0026lt;button @onclick=\u0026quot;NextPageAsync\u0026quot; disabled=\u0026quot;@(!_hasNext)\u0026quot;\u0026gt;Next\u0026lt;/button\u0026gt;\n}\n\n@code {\n private string _search = \u0026quot;\u0026quot;;\n private int _pageNumber = 1;\n private bool _hasNext;\n private bool _loading;\n private string? _error;\n private IReadOnlyList\u0026lt;UserSummary\u0026gt; _users = [];\n\n protected override async Task OnInitializedAsync()\n {\n await LoadAsync();\n }\n\n private async Task SearchAsync()\n {\n _pageNumber = 1;\n await LoadAsync();\n }\n\n private async Task PreviousPageAsync()\n {\n if (_pageNumber \u0026lt;= 1)\n return;\n\n _pageNumber--;\n await LoadAsync();\n }\n\n private async Task NextPageAsync()\n {\n if (!_hasNext)\n return;\n\n _pageNumber\u002B\u002B;\n await LoadAsync();\n }\n\n private async Task LoadAsync()\n {\n _loading = true;\n _error = null;\n\n try\n {\n var result = await UAuthClient.Users.QueryAsync(\n new UserQuery\n {\n Search = _search,\n PageNumber = _pageNumber,\n PageSize = 20\n });\n\n if (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } page)\n {\n _users = page.Items;\n _hasNext = page.HasNext;\n }\n else\n {\n _error = \u0026quot;Unable to load users.\u0026quot;;\n _users = [];\n _hasNext = false;\n }\n }\n catch (Exception)\n {\n _error = \u0026quot;The user service is unavailable.\u0026quot;;\n _users = [];\n _hasNext = false;\n }\n finally\n {\n _loading = false;\n }\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis example demonstrates searching and pagination.\u003C/p\u003E\n\u003Cp\u003EA production administration interface should additionally include loading indicators, action confirmation, permission-aware UI controls, and appropriate handling for concurrent changes.\u003C/p\u003E\n\u003Cp\u003EAll administrative operations must remain protected by server-side authorization.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022handling-user-operation-results\u0022\u003E22. Handling User Operation Results\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth uses structured result contracts for user operations.\u003C/p\u003E\n\u003Cp\u003ESome methods return:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthResult\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EOthers return:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthResult\u0026lt;T\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EFor operations such as user creation, status changes, and deletion, the returned value also contains an operation-level \u003Ccode\u003EIsSuccess\u003C/code\u003E property.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022transport-level-versus-operation-level-success\u0022\u003ETransport-Level Versus Operation-Level Success\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.CreateAsync(request);\n\nif (!result.IsSuccess)\n{\n // API-level failure.\n}\nelse if (result.Value is { IsSuccess: false } operation)\n{\n // User creation was processed but did not succeed.\n Console.WriteLine(operation.FailureReason);\n}\nelse if (result.Value is { IsSuccess: true } created)\n{\n Console.WriteLine(created.UserKey);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ECheck the appropriate success level for the operation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022http-status-helpers\u0022\u003EHTTP Status Helpers\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthResult\u003C/code\u003E exposes convenient status helpers.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eif (result.Http.IsUnauthorized)\n{\n // Authentication required.\n}\nelse if (result.Http.IsForbidden)\n{\n // Insufficient permission.\n}\nelse if (result.Http.IsConflict)\n{\n // Conflicting operation or state.\n}\nelse if (result.Http.IsBadRequest)\n{\n // Invalid request.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAdditional diagnostic fields include \u003Ccode\u003ECorrelationId\u003C/code\u003E, \u003Ccode\u003ETraceId\u003C/code\u003E, \u003Ccode\u003EProblem\u003C/code\u003E, and \u003Ccode\u003EErrorText\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EAvoid exposing sensitive internal failure details directly to end users.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022client-state-events\u0022\u003E23. Client State Events\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthUserClient\u003C/code\u003E publishes state events for successful self-service operations.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EOperation\u003C/th\u003E\n\u003Cth\u003EEvent\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUpdateMyProfileAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EProfileChanged\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECreateMyProfileAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EProfileChanged\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDeleteMyProfileAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EProfileChanged\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EChangeMyStatusAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EProfileChanged\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDeleteMeAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserDeleted\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe event payload depends on the operation.\u003C/p\u003E\n\u003Cp\u003EAdministrative user operations do not automatically publish these same local self-service events.\u003C/p\u003E\n\u003Cp\u003EFor example, after updating another user\u0027s profile, an administration panel should refresh the affected user data as needed.\u003C/p\u003E\n\u003Cp\u003EClient events help synchronize application state, but they do not replace authoritative server-side account status or permission checks.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-and-lifecycle-considerations\u0022\u003E24. Security and Lifecycle Considerations\u003C/h2\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022self-service-and-administrative-boundaries\u0022\u003ESelf-Service and Administrative Boundaries\u003C/h3\u003E\n\u003Cp\u003ESelf-service methods operate on the current authenticated user.\u003C/p\u003E\n\u003Cp\u003EAdministrative methods identify an explicit target user.\u003C/p\u003E\n\u003Cp\u003EDo not assume that access to the SDK automatically authorizes an operation.\u003C/p\u003E\n\u003Cp\u003EThe server must enforce actor, target, tenant, and action requirements.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022status-changes-and-existing-sessions\u0022\u003EStatus Changes and Existing Sessions\u003C/h3\u003E\n\u003Cp\u003EChanging account status may affect authentication eligibility and session continuation.\u003C/p\u003E\n\u003Cp\u003EHowever, immediate invalidation of existing credentials depends on the server\u0027s security policy and authentication mode.\u003C/p\u003E\n\u003Cp\u003EA client-side status update is not itself proof that every previously issued token has been invalidated.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022profile-data-is-not-authorization-data\u0022\u003EProfile Data Is Not Authorization Data\u003C/h3\u003E\n\u003Cp\u003EA user\u0027s display name, biography, or profile metadata should not be used as proof of permissions.\u003C/p\u003E\n\u003Cp\u003EAuthorization decisions must use the relevant trusted security context.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022profile-replacement-can-cause-data-loss\u0022\u003EProfile Replacement Can Cause Data Loss\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EUpdateProfileRequest\u003C/code\u003E is not a patch contract.\u003C/p\u003E\n\u003Cp\u003ESubmitting an incomplete request may clear existing profile fields.\u003C/p\u003E\n\u003Cp\u003EApplications should preserve intended values and consider concurrency when multiple editors can modify the same profile.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022deletion-is-a-lifecycle-operation\u0022\u003EDeletion Is a Lifecycle Operation\u003C/h3\u003E\n\u003Cp\u003ESoft deletion and hard deletion have different implications.\u003C/p\u003E\n\u003Cp\u003EApplications should consider retention, recovery, audit requirements, and dependent data before exposing destructive actions.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022next-steps\u0022\u003ENext Steps\u003C/h2\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/client/identifiers\u0022\u003EIdentifiers\u003C/a\u003E \u2014 Manage usernames, email addresses, phone numbers, and verification.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/client/authentication\u0022\u003EAuthentication\u003C/a\u003E \u2014 Login, refresh, logout, and authentication state.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/client/session-management\u0022\u003ESession Management\u003C/a\u003E \u2014 Inspect and revoke sessions across devices.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/plugin-domains/users\u0022\u003EUsers\u003C/a\u003E \u2014 Understand the user domain and lifecycle architecture.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/plugin-domains/policies\u0022\u003EPolicies \u0026amp; Access Control\u003C/a\u003E \u2014 Understand authorization enforcement for user operations.\u003C/li\u003E\n\u003C/ul\u003E\n", + "Html": "\n\u003Cp\u003EThe UltimateAuth Client SDK provides a comprehensive API for managing users, profiles, account status, and user lifecycle operations.\u003C/p\u003E\n\u003Cp\u003EThrough \u003Ccode\u003EUAuthClient.Users\u003C/code\u003E, applications can implement account settings, profile management, user registration, and administrative user management without manually constructing API requests.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth distinguishes between \u003Cstrong\u003Euser identity\u003C/strong\u003E, \u003Cstrong\u003Euser profiles\u003C/strong\u003E, and \u003Cstrong\u003Eaccount lifecycle state\u003C/strong\u003E.\u003C/p\u003E\n\u003Cp\u003EA user has a stable identity and may have multiple profiles. Account status controls lifecycle behavior independently of profile data.\u003C/p\u003E\n\u003Cp\u003EThis guide covers the public \u003Ccode\u003EIUserClient\u003C/code\u003E API, including self-service and administrative operations, structured results, pagination, multi-profile support, and important update semantics.\u003C/p\u003E\n\u003Cp\u003EFor email addresses, usernames, phone numbers, and identifier verification, see \u003Ca href=\u0022/docs/client/identifiers\u0022\u003EIdentifiers\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022accessing-the-user-client\u0022\u003E1. Accessing the User Client\u003C/h2\u003E\n\u003Cp\u003EIn a Blazor component, inject \u003Ccode\u003EIUAuthClient\u003C/code\u003E:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@using CodeBeam.UltimateAuth.Client\n@inject IUAuthClient UAuthClient\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUser operations are available through:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthClient.Users\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe \u003Ccode\u003EUsers\u003C/code\u003E property exposes \u003Ccode\u003EIUserClient\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EIts operations are divided into three functional areas:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EArea\u003C/th\u003E\n\u003Cth\u003ECapabilities\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EUser lifecycle\u003C/td\u003E\n\u003Ctd\u003ECreate users, change status, delete accounts\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ESelf-service profiles\u003C/td\u003E\n\u003Ctd\u003ERetrieve, update, create, and delete your own profiles\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAdministration\u003C/td\u003E\n\u003Ctd\u003EQuery users and manage another user\u0027s account and profiles\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003ESelf-service methods operate on the authenticated user.\u003C/p\u003E\n\u003Cp\u003EAdministrative methods operate on an explicitly identified target and require the appropriate server-side authorization.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022get-the-current-user\u0022\u003E2. Get the Current User\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EGetMeAsync\u003C/code\u003E to retrieve the current user\u0027s profile.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.GetMeAsync();\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } user)\n{\n Console.WriteLine(user.DisplayName);\n Console.WriteLine(user.Status);\n Console.WriteLine(user.UserKey);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthResult\u0026lt;UserView\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWhen no profile is specified, UltimateAuth returns the default profile.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022userview\u0022\u003EUserView\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EUserView\u003C/code\u003E contains account information and the selected profile\u0027s data.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserKey\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EStable user identifier\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EStatus\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECurrent account status\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUsername\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPrimaryEmail\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPrimary email address\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPrimaryPhone\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPrimary phone number\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EProfileKey\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESelected profile\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EFirstName\u003C/code\u003E, \u003Ccode\u003ELastName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EProfile name fields\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDisplayName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EDisplay name\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EBio\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EBiography\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EBirthDate\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EBirth date\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EGender\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EProfile gender field\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELanguage\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPreferred language\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETimeZone\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPreferred time zone\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECulture\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECulture preference\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEmailVerified\u003C/code\u003E, \u003Ccode\u003EPhoneVerified\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifier verification information\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECreatedAt\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAccount creation timestamp\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMetadata\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECustom profile metadata\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe returned profile is a view of server-managed user information.\u003C/p\u003E\n\u003Cp\u003EIt should not be treated as a substitute for authoritative authentication or authorization state.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022update-the-current-users-profile\u0022\u003E3. Update the Current User\u0027s Profile\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EUpdateMyProfileAsync\u003C/code\u003E to update the authenticated user\u0027s profile.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.UpdateMyProfileAsync(\n new UpdateProfileRequest\n {\n DisplayName = \u0026quot;John Doe\u0026quot;,\n FirstName = \u0026quot;John\u0026quot;,\n LastName = \u0026quot;Doe\u0026quot;,\n Language = \u0026quot;en\u0026quot;,\n TimeZone = \u0026quot;Europe/London\u0026quot;\n });\n\nif (result.IsSuccess)\n{\n // Profile update succeeded.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns \u003Ccode\u003EUAuthResult\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022important-profile-updates-replace-editable-fields\u0022\u003EImportant: Profile Updates Replace Editable Fields\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EUpdateProfileRequest\u003C/code\u003E uses replacement semantics rather than partial-update semantics.\u003C/p\u003E\n\u003Cp\u003EThis means:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EEditable fields supplied in the request replace their previous values.\u003C/li\u003E\n\u003Cli\u003EExplicit \u003Ccode\u003Enull\u003C/code\u003E values clear existing values.\u003C/li\u003E\n\u003Cli\u003EOmitted editable properties are also treated as \u003Ccode\u003Enull\u003C/code\u003E.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eawait UAuthClient.Users.UpdateMyProfileAsync(\n new UpdateProfileRequest\n {\n DisplayName = \u0026quot;John Doe\u0026quot;\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis does \u003Cstrong\u003Enot\u003C/strong\u003E mean \u201Cchange only the display name.\u201D\u003C/p\u003E\n\u003Cp\u003EOther editable profile fields may be cleared.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022recommended-read-modify-and-submit\u0022\u003ERecommended: Read, Modify, and Submit\u003C/h3\u003E\n\u003Cp\u003EWhen building a profile editor, retrieve the current profile and preserve values that should remain unchanged.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar currentResult = await UAuthClient.Users.GetMeAsync();\n\nif (!currentResult.IsSuccess || currentResult.Value is not { } current)\n{\n return;\n}\n\nvar update = new UpdateProfileRequest\n{\n FirstName = current.FirstName,\n LastName = current.LastName,\n DisplayName = \u0026quot;John Doe\u0026quot;,\n BirthDate = current.BirthDate,\n Gender = current.Gender,\n Bio = current.Bio,\n Language = current.Language,\n TimeZone = current.TimeZone,\n Culture = current.Culture,\n Metadata = current.Metadata\n};\n\nvar result = await UAuthClient.Users.UpdateMyProfileAsync(update);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis preserves the values returned by the initial read, but it is not an atomic patch operation.\u003C/p\u003E\n\u003Cp\u003EIf another request modifies the profile between the read and update, the later replacement may overwrite those changes.\u003C/p\u003E\n\u003Cp\u003EApplications with concurrent profile editing requirements should account for this behavior.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022profile-state-events\u0022\u003EProfile State Events\u003C/h3\u003E\n\u003Cp\u003EAfter a successful \u003Ccode\u003EUpdateMyProfileAsync\u003C/code\u003E operation, the client publishes a \u003Ccode\u003EProfileChanged\u003C/code\u003E event.\u003C/p\u003E\n\u003Cp\u003EThe event carries the \u003Ccode\u003EUpdateProfileRequest\u003C/code\u003E used for the operation.\u003C/p\u003E\n\u003Cp\u003EThe configured client state event handling mode determines how the application processes the notification.\u003C/p\u003E\n\u003Cp\u003EThe event is published only when the SDK operation reports success.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022understanding-multiple-profiles\u0022\u003E4. Understanding Multiple Profiles\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth supports multiple profiles for a single user when enabled through server configuration.\u003C/p\u003E\n\u003Cp\u003EFor example, one account may have:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EUser\n |\n \u002B-- default\n |\n \u002B-- business\n |\n \u002B-- public\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAll profiles belong to the same user identity.\u003C/p\u003E\n\u003Cp\u003EThey do not represent separate authentication accounts or independently authenticated principals.\u003C/p\u003E\n\u003Cp\u003EProfiles allow applications to organize different sets of user-facing information under one account.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022default-profile\u0022\u003EDefault Profile\u003C/h3\u003E\n\u003Cp\u003EEvery newly created user receives a default profile.\u003C/p\u003E\n\u003Cp\u003EIts key is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EProfileKey.Default\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe default profile cannot be deleted.\u003C/p\u003E\n\u003Cp\u003EWhen a profile request omits \u003Ccode\u003EProfileKey\u003C/code\u003E, the default profile is selected.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022profile-keys\u0022\u003EProfile Keys\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EProfileKey\u003C/code\u003E is a strongly typed value.\u003C/p\u003E\n\u003Cp\u003EUse \u003Ccode\u003ETryCreate\u003C/code\u003E to construct a custom key:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eif (!ProfileKey.TryCreate(\u0026quot;business\u0026quot;, out var profileKey))\n{\n throw new InvalidOperationException(\u0026quot;Invalid profile key.\u0026quot;);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EProfile keys are normalized by trimming whitespace and converting the value to lowercase.\u003C/p\u003E\n\u003Cp\u003EA valid key must not be empty or exceed 64 characters.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022retrieve-a-specific-profile\u0022\u003E5. Retrieve a Specific Profile\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EGetProfileRequest\u003C/code\u003E to select a profile.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eif (!ProfileKey.TryCreate(\u0026quot;business\u0026quot;, out var profileKey))\n{\n return;\n}\n\nvar result = await UAuthClient.Users.GetMeAsync(\n new GetProfileRequest\n {\n ProfileKey = profileKey\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } profile)\n{\n Console.WriteLine(profile.DisplayName);\n Console.WriteLine(profile.ProfileKey);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe response is still \u003Ccode\u003EUserView\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe account identity and status remain associated with the same user; the selected profile determines which profile information is returned.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022create-an-additional-profile\u0022\u003E6. Create an Additional Profile\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003ECreateMyProfileAsync\u003C/code\u003E to create a new profile for the authenticated user.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eif (!ProfileKey.TryCreate(\u0026quot;business\u0026quot;, out var profileKey))\n{\n return;\n}\n\nvar result = await UAuthClient.Users.CreateMyProfileAsync(\n new CreateProfileRequest\n {\n ProfileKey = profileKey,\n DisplayName = \u0026quot;John Doe Consulting\u0026quot;,\n Bio = \u0026quot;Business profile\u0026quot;,\n Language = \u0026quot;en\u0026quot;\n });\n\nif (result.IsSuccess)\n{\n // Profile created.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003ECreateProfileRequest\u003C/code\u003E requires \u003Ccode\u003EProfileKey\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EIt also supports the same general profile information used for user-facing profiles.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022clone-an-existing-profile\u0022\u003EClone an Existing Profile\u003C/h3\u003E\n\u003Cp\u003EA new profile can be created using an existing profile as its source.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EProfileKey.TryCreate(\u0026quot;business\u0026quot;, out var businessKey);\n\nvar result = await UAuthClient.Users.CreateMyProfileAsync(\n new CreateProfileRequest\n {\n ProfileKey = businessKey,\n CloneFrom = ProfileKey.Default\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003ECloneFrom\u003C/code\u003E identifies the source profile.\u003C/p\u003E\n\u003Cp\u003EThis is useful when creating a new profile that should initially reuse information from an existing one.\u003C/p\u003E\n\u003Cp\u003EThe exact merge and override behavior is determined by the server\u0027s profile creation implementation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022profile-creation-events\u0022\u003EProfile Creation Events\u003C/h3\u003E\n\u003Cp\u003EAfter successful self-service profile creation, the SDK publishes \u003Ccode\u003EProfileChanged\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThis allows application components to refresh profile-related views.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022update-a-specific-profile\u0022\u003E7. Update a Specific Profile\u003C/h2\u003E\n\u003Cp\u003ESet \u003Ccode\u003EProfileKey\u003C/code\u003E in \u003Ccode\u003EUpdateProfileRequest\u003C/code\u003E to select the profile being updated.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EProfileKey.TryCreate(\u0026quot;business\u0026quot;, out var businessKey);\n\nvar result = await UAuthClient.Users.UpdateMyProfileAsync(\n new UpdateProfileRequest\n {\n ProfileKey = businessKey,\n DisplayName = \u0026quot;John Doe Consulting\u0026quot;,\n Bio = \u0026quot;Updated business profile\u0026quot;\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe same replacement semantics apply to custom profiles.\u003C/p\u003E\n\u003Cp\u003EIf the application intends to preserve existing fields, retrieve the selected profile first and include the values that should remain unchanged.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022delete-a-profile\u0022\u003E8. Delete a Profile\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EDeleteMyProfileAsync\u003C/code\u003E to remove a custom profile.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EProfileKey.TryCreate(\u0026quot;business\u0026quot;, out var businessKey);\n\nvar result = await UAuthClient.Users.DeleteMyProfileAsync(\n new DeleteProfileRequest\n {\n ProfileKey = businessKey\n });\n\nif (result.IsSuccess)\n{\n // Profile deleted.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe default profile cannot be deleted.\u003C/p\u003E\n\u003Cp\u003EDeleting a profile is different from deleting the user account.\u003C/p\u003E\n\u003Cp\u003EThe user identity and other profiles remain subject to their own lifecycle and server-side state.\u003C/p\u003E\n\u003Cp\u003EOn successful self-service profile deletion, the SDK publishes \u003Ccode\u003EProfileChanged\u003C/code\u003E with the deleted profile key.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022create-a-user\u0022\u003E9. Create a User\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003ECreateAsync\u003C/code\u003E to request user creation through the standard user creation endpoint.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.CreateAsync(\n new CreateUserRequest\n {\n UserName = \u0026quot;johndoe\u0026quot;,\n Email = \u0026quot;john@example.com\u0026quot;,\n Password = password,\n FirstName = \u0026quot;John\u0026quot;,\n LastName = \u0026quot;Doe\u0026quot;,\n DisplayName = \u0026quot;John Doe\u0026quot;\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { IsSuccess: true } created)\n{\n Console.WriteLine($\u0026quot;Created user: {created.UserKey}\u0026quot;);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthResult\u0026lt;UserCreateResult\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUser creation availability depends on the server\u0027s configuration and access policies.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022createuserrequest\u0022\u003ECreateUserRequest\u003C/h3\u003E\n\u003Cp\u003EThe request supports several groups of fields:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003ECategory\u003C/th\u003E\n\u003Cth\u003EProperties\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EIdentifiers\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserName\u003C/code\u003E, \u003Ccode\u003EEmail\u003C/code\u003E, \u003Ccode\u003EPhone\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EVerification flags\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserNameVerified\u003C/code\u003E, \u003Ccode\u003EEmailVerified\u003C/code\u003E, \u003Ccode\u003EPhoneVerified\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ECredentials\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EPassword\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EProfile\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EFirstName\u003C/code\u003E, \u003Ccode\u003ELastName\u003C/code\u003E, \u003Ccode\u003EDisplayName\u003C/code\u003E, \u003Ccode\u003EBirthDate\u003C/code\u003E, \u003Ccode\u003EGender\u003C/code\u003E, \u003Ccode\u003EBio\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EPreferences\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003ELanguage\u003C/code\u003E, \u003Ccode\u003ETimeZone\u003C/code\u003E, \u003Ccode\u003ECulture\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EExtensions\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EMetadata\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EVerification flags must not be treated as client-controlled proof of identifier ownership.\u003C/p\u003E\n\u003Cp\u003EThe server is responsible for enforcing verification requirements and determining whether a caller is authorized to establish verified identifier state.\u003C/p\u003E\n\u003Cp\u003EFor identifier-specific operations and verification workflows, see \u003Ca href=\u0022/docs/client/identifiers\u0022\u003EIdentifiers\u003C/a\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022usercreateresult\u0022\u003EUserCreateResult\u003C/h3\u003E\n\u003Cp\u003EThe returned value includes:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Epublic sealed record UserCreateResult\n{\n public required bool IsSuccess { get; init; }\n public string? UserKey { get; init; }\n public string? FailureReason { get; init; }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe \u003Ccode\u003EUserKey\u003C/code\u003E is returned as a string when creation succeeds.\u003C/p\u003E\n\u003Cp\u003ECheck both the transport-level \u003Ccode\u003EUAuthResult.IsSuccess\u003C/code\u003E and the operation-level \u003Ccode\u003EUserCreateResult.IsSuccess\u003C/code\u003E where applicable.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022administrative-user-creation\u0022\u003E10. Administrative User Creation\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003ECreateAsAdminAsync\u003C/code\u003E when creating a user through an administrative context.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.CreateAsAdminAsync(\n new CreateUserRequest\n {\n UserName = \u0026quot;newemployee\u0026quot;,\n Email = \u0026quot;employee@example.com\u0026quot;,\n DisplayName = \u0026quot;New Employee\u0026quot;\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { IsSuccess: true } created)\n{\n Console.WriteLine(created.UserKey);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAdministrative creation is distinct from ordinary user creation.\u003C/p\u003E\n\u003Cp\u003EThe server enforces the required permissions and applies the relevant user creation policies.\u003C/p\u003E\n\u003Cp\u003EApplications should not assume that calling the administrative endpoint bypasses validation or identifier ownership rules.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022query-users\u0022\u003E11. Query Users\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EQueryAsync\u003C/code\u003E to search and paginate users.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.QueryAsync(\n new UserQuery\n {\n Search = \u0026quot;john\u0026quot;,\n PageNumber = 1,\n PageSize = 20\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } page)\n{\n foreach (var user in page.Items)\n {\n Console.WriteLine(user.DisplayName);\n Console.WriteLine(user.Status);\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthResult\u0026lt;PagedResult\u0026lt;UserSummary\u0026gt;\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022userquery-filters\u0022\u003EUserQuery Filters\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EUserQuery\u003C/code\u003E extends \u003Ccode\u003EPageRequest\u003C/code\u003E and supports:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESearch\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESearch term\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EStatus\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EFilter by user status\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIncludeDeleted\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EInclude deleted users when authorized\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EProfileKey\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESelect the profile context for the query\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPageNumber\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPage number\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPageSize\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPage size\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESortBy\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional sort field\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDescending\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EDescending sort order\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022search-behavior\u0022\u003ESearch Behavior\u003C/h3\u003E\n\u003Cp\u003EThe query model documents case-insensitive partial matching for:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EDisplay name.\u003C/li\u003E\n\u003Cli\u003EUsername.\u003C/li\u003E\n\u003Cli\u003EPrimary email.\u003C/li\u003E\n\u003Cli\u003EPrimary phone.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EA complete valid \u003Ccode\u003EUserKey\u003C/code\u003E can also be used as a search term.\u003C/p\u003E\n\u003Cp\u003EPartial \u003Ccode\u003EUserKey\u003C/code\u003E matching is not supported.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022filter-by-status\u0022\u003EFilter by Status\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.QueryAsync(\n new UserQuery\n {\n Status = UserStatus.Active,\n PageNumber = 1,\n PageSize = 20\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022include-deleted-users\u0022\u003EInclude Deleted Users\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.QueryAsync(\n new UserQuery\n {\n IncludeDeleted = true,\n PageNumber = 1,\n PageSize = 20\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe ability to query deleted accounts depends on server-side authorization and data retention behavior.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022usersummary-and-pagination\u0022\u003E12. UserSummary and Pagination\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EQueryAsync\u003C/code\u003E returns \u003Ccode\u003EUserSummary\u003C/code\u003E items.\u003C/p\u003E\n\u003Cp\u003EEach summary contains:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserKey\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUser identifier\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUsername\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDisplayName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EDisplay name\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPrimaryEmail\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPrimary email\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPrimaryPhone\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPrimary phone\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EStatus\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAccount status\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECreatedAt\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECreation timestamp\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThis model is suitable for administrative user lists.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022navigating-pages\u0022\u003ENavigating Pages\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.QueryAsync(\n new UserQuery\n {\n PageNumber = 2,\n PageSize = 25\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } page)\n{\n Console.WriteLine($\u0026quot;Total users: {page.TotalCount}\u0026quot;);\n Console.WriteLine($\u0026quot;Current page: {page.PageNumber}\u0026quot;);\n Console.WriteLine($\u0026quot;Has next page: {page.HasNext}\u0026quot;);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUse \u003Ccode\u003EPagedResult\u0026lt;T\u0026gt;\u003C/code\u003E rather than assuming every query returns the complete user collection.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022retrieve-users-by-keys\u0022\u003E13. Retrieve Users by Keys\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EGetByKeysAsync\u003C/code\u003E to retrieve multiple user summaries when their \u003Ccode\u003EUserKey\u003C/code\u003E values are already known.\u003C/p\u003E\n\u003Cp\u003EUnlike \u003Ccode\u003EQueryAsync\u003C/code\u003E, which searches and paginates users, \u003Ccode\u003EGetByKeysAsync\u003C/code\u003E retrieves a specific collection of users in a single request.\u003C/p\u003E\n\u003Cp\u003EThis is particularly useful for applications that maintain relationships between users, such as:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003ETeam members.\u003C/li\u003E\n\u003Cli\u003EOrganization members.\u003C/li\u003E\n\u003Cli\u003EProject participants.\u003C/li\u003E\n\u003Cli\u003EGroup memberships.\u003C/li\u003E\n\u003Cli\u003EUsers assigned to a particular resource.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EInstead of requesting each user\u0027s information separately, the application can retrieve the required summaries in one operation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022basic-usage\u0022\u003EBasic Usage\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar userKeys = new[]\n{\n firstUserKey,\n secondUserKey,\n thirdUserKey\n};\n\nvar result = await UAuthClient.Users.GetByKeysAsync(userKeys);\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } users)\n{\n foreach (var user in users)\n {\n Console.WriteLine($\u0026quot;{user.DisplayName} ({user.UserName})\u0026quot;);\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthResult\u0026lt;IReadOnlyList\u0026lt;UserSummary\u0026gt;\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EEach returned item uses the same \u003Ccode\u003EUserSummary\u003C/code\u003E model described above.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022batch-size-and-behavior\u0022\u003EBatch Size and Behavior\u003C/h3\u003E\n\u003Cp\u003EA single request supports up to \u003Cstrong\u003E250 distinct user keys\u003C/strong\u003E.\u003C/p\u003E\n\u003Cp\u003EThe operation:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EAccepts a collection of strongly typed \u003Ccode\u003EUserKey\u003C/code\u003E values.\u003C/li\u003E\n\u003Cli\u003ERemoves duplicate keys.\u003C/li\u003E\n\u003Cli\u003EReturns existing, non-deleted users within the applicable tenant.\u003C/li\u003E\n\u003Cli\u003EOmits user keys that do not match an available user.\u003C/li\u003E\n\u003Cli\u003EReturns an empty collection when no users match.\u003C/li\u003E\n\u003Cli\u003EDoes not use pagination.\u003C/li\u003E\n\u003Cli\u003EDoes not guarantee that the response follows the order of the requested keys.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003ERequests exceeding 250 distinct keys are rejected with the \u003Ccode\u003Euser_batch_size_exceeded\u003C/code\u003E validation error.\u003C/p\u003E\n\u003Cp\u003EFor larger collections, split the keys into batches:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar allUsers = new List\u0026lt;UserSummary\u0026gt;();\n\nforeach (var batch in userKeys.Distinct().Chunk(250))\n{\n var result = await UAuthClient.Users.GetByKeysAsync(batch);\n\n if (!result.IsSuccess || result.Value is null)\n {\n // Handle the failed batch.\n return;\n }\n\n allUsers.AddRange(result.Value);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EFor large datasets that require searching, filtering, sorting, or pagination, use \u003Ccode\u003EQueryAsync\u003C/code\u003E instead.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022queryasync-vs.getbykeysasync\u0022\u003EQueryAsync vs. GetByKeysAsync\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003ECapability\u003C/th\u003E\n\u003Cth\u003EQueryAsync\u003C/th\u003E\n\u003Cth\u003EGetByKeysAsync\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ESearch users\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003Ctd\u003ENo\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ERetrieve known user keys\u003C/td\u003E\n\u003Ctd\u003EIndirectly\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EPagination\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003Ctd\u003ENo\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EFiltering and sorting\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003Ctd\u003ENo\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EMultiple specific users\u003C/td\u003E\n\u003Ctd\u003ENot optimized for this purpose\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EMaximum batch size\u003C/td\u003E\n\u003Ctd\u003EPagination limits\u003C/td\u003E\n\u003Ctd\u003E250 distinct keys\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EResponse\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EPagedResult\u0026lt;UserSummary\u0026gt;\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EIReadOnlyList\u0026lt;UserSummary\u0026gt;\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022authorization\u0022\u003EAuthorization\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EGetByKeysAsync\u003C/code\u003E is a protected operation.\u003C/p\u003E\n\u003Cp\u003EIt uses the action:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthActions.Users.GetByKeysAdmin\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe corresponding permission value is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003Eusers.getbykeys.admin\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAlthough the action uses the \u003Ccode\u003E.admin\u003C/code\u003E suffix, access is determined by the server\u0027s authorization policies and the actor\u0027s effective permissions.\u003C/p\u003E\n\u003Cp\u003EApplications can authorize non-administrator users to retrieve specific groups of users when their policies explicitly allow it.\u003C/p\u003E\n\u003Cp\u003EFor example, an application may allow team members to retrieve summaries of other members belonging to the same team while denying access to users outside that team.\u003C/p\u003E\n\u003Cp\u003EThe server exposes the requested users through \u003Ccode\u003EAccessContext.TargetUserKeys\u003C/code\u003E, allowing application policies to validate the complete target collection.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EImportant:\u003C/strong\u003E Possessing the \u003Ccode\u003Eusers.getbykeys.admin\u003C/code\u003E permission does not automatically establish that every requested user belongs to an allowed group. Applications requiring group, organization, or resource-level isolation must enforce those restrictions through server-side authorization policies.\u003C/p\u003E\n\u003Cp\u003EFor details, see \u003Ca href=\u0022/docs/plugin-domains/policies\u0022\u003EPolicies \u0026amp; Access Control\u003C/a\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022performance-considerations\u0022\u003EPerformance Considerations\u003C/h3\u003E\n\u003Cp\u003EBatch retrieval avoids issuing a separate client request for every user.\u003C/p\u003E\n\u003Cp\u003EThe reference persistence implementations support retrieving multiple user summaries through a single batch operation.\u003C/p\u003E\n\u003Cp\u003EActual database query counts and execution costs depend on the configured persistence provider.\u003C/p\u003E\n\u003Cp\u003EFor example, an application displaying 50 team members can request their summaries together rather than making 50 individual \u003Ccode\u003EGetUserAsync\u003C/code\u003E calls.\u003C/p\u003E\n\u003Cp\u003EUse \u003Ccode\u003EGetByKeysAsync\u003C/code\u003E when user keys are already available. Use \u003Ccode\u003EGetUserAsync\u003C/code\u003E when the application needs the detailed profile of one specific user.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022get-a-specific-user\u0022\u003E14. Get a Specific User\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EGetUserAsync\u003C/code\u003E to retrieve a user\u0027s profile through the administrative API.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.GetUserAsync(userKey);\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } user)\n{\n Console.WriteLine(user.DisplayName);\n Console.WriteLine(user.Status);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns \u003Ccode\u003EUAuthResult\u0026lt;UserView\u0026gt;\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EWhen no profile is specified, the default profile is selected.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022retrieve-a-specific-user-profile\u0022\u003ERetrieve a Specific User Profile\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EProfileKey.TryCreate(\u0026quot;business\u0026quot;, out var businessKey);\n\nvar result = await UAuthClient.Users.GetUserAsync(\n userKey,\n new GetProfileRequest\n {\n ProfileKey = businessKey\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe actor must have permission to access the target user\u0027s requested information.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022update-another-users-profile\u0022\u003E15. Update Another User\u0027s Profile\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EUpdateUserProfileAsync\u003C/code\u003E to update a selected user\u0027s profile.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.UpdateUserProfileAsync(\n userKey,\n new UpdateProfileRequest\n {\n DisplayName = \u0026quot;Updated Display Name\u0026quot;,\n FirstName = \u0026quot;John\u0026quot;,\n LastName = \u0026quot;Doe\u0026quot;\n });\n\nif (result.IsSuccess)\n{\n // Profile updated.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis operation uses the same replacement semantics as \u003Ccode\u003EUpdateMyProfileAsync\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EOmitted editable fields may be cleared.\u003C/p\u003E\n\u003Cp\u003EFor partial UI editing, retrieve the target profile and preserve values that should not change.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022update-a-named-profile\u0022\u003EUpdate a Named Profile\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EProfileKey.TryCreate(\u0026quot;business\u0026quot;, out var businessKey);\n\nvar result = await UAuthClient.Users.UpdateUserProfileAsync(userKey,\n new UpdateProfileRequest\n {\n ProfileKey = businessKey,\n DisplayName = \u0026quot;Updated Business Profile\u0026quot;\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAdministrative profile updates require the appropriate authorization for the target user.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022manage-another-users-profiles\u0022\u003E16. Manage Another User\u0027s Profiles\u003C/h2\u003E\n\u003Cp\u003EAdministrators can create and delete profiles for a selected user.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022create-a-profile\u0022\u003ECreate a Profile\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EProfileKey.TryCreate(\u0026quot;business\u0026quot;, out var businessKey);\n\nvar result = await UAuthClient.Users.CreateUserProfileAsync(userKey,\n new CreateProfileRequest\n {\n ProfileKey = businessKey,\n DisplayName = \u0026quot;Business Profile\u0026quot;\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022delete-a-profile-1\u0022\u003EDelete a Profile\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EProfileKey.TryCreate(\u0026quot;business\u0026quot;, out var businessKey);\n\nvar result = await UAuthClient.Users.DeleteUserProfileAsync(userKey,\n new DeleteProfileRequest\n {\n ProfileKey = businessKey\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe default profile cannot be deleted.\u003C/p\u003E\n\u003Cp\u003EAdministrative profile operations do not automatically publish the same local \u003Ccode\u003EProfileChanged\u003C/code\u003E event as self-service profile operations.\u003C/p\u003E\n\u003Cp\u003ERefresh affected administrative UI data explicitly when needed.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022understanding-user-status\u0022\u003E17. Understanding User Status\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth separates account lifecycle status from profile information.\u003C/p\u003E\n\u003Cp\u003EA user may be active, suspended, disabled, locked, awaiting activation, or in another supported lifecycle state.\u003C/p\u003E\n\u003Cp\u003EStatus can affect authentication eligibility, session continuation, and authorization.\u003C/p\u003E\n\u003Cp\u003EHowever, the exact enforcement behavior depends on the configured server policies and authentication mode.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022self-service-and-administrative-statuses\u0022\u003ESelf-Service and Administrative Statuses\u003C/h3\u003E\n\u003Cp\u003EUltimateAuth exposes separate status enums for self-service and administrative operations.\u003C/p\u003E\n\u003Cp\u003EThis prevents the client contract from treating every administrative lifecycle transition as a self-service capability.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003ESelfAssignableUserStatus\u003C/strong\u003E\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Epublic enum SelfAssignableUserStatus\n{\n Active = 0,\n SelfSuspended = 10\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Cstrong\u003EAdminAssignableUserStatus\u003C/strong\u003E\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Epublic enum AdminAssignableUserStatus\n{\n Active = 0,\n Disabled = 20,\n Suspended = 30,\n Locked = 40,\n RiskHold = 50,\n PendingActivation = 60,\n PendingVerification = 70,\n Unknown = 100\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThese enums describe requestable statuses.\u003C/p\u003E\n\u003Cp\u003EThey do not mean every transition is permitted in every application context.\u003C/p\u003E\n\u003Cp\u003EThe server validates the requested transition.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022change-your-own-status\u0022\u003E18. Change Your Own Status\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EChangeMyStatusAsync\u003C/code\u003E for supported self-service status transitions.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.ChangeMyStatusAsync(\n new ChangeUserStatusSelfRequest\n {\n NewStatus = SelfAssignableUserStatus.SelfSuspended\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { IsSuccess: true } change)\n{\n Console.WriteLine($\u0026quot;New status: {change.CurrentStatus}\u0026quot;);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis operation can be used to implement account self-suspension.\u003C/p\u003E\n\u003Cp\u003EThe server determines whether the transition is permitted.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022userstatuschangeresult\u0022\u003EUserStatusChangeResult\u003C/h3\u003E\n\u003Cp\u003EThe result model includes:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIsSuccess\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EWhether the status operation succeeded\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPreviousStatus\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EStatus before the operation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECurrentStatus\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EStatus after the operation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EFailureReason\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional failure description\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } change)\n{\n if (change.IsSuccess)\n {\n Console.WriteLine($\u0026quot;{change.PreviousStatus} -\u0026gt; {change.CurrentStatus}\u0026quot;);\n }\n else\n {\n Console.WriteLine(change.FailureReason ?? \u0026quot;Status change failed.\u0026quot;);\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe result may also report a successful no-change outcome when the requested state already matches the current state.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022state-event\u0022\u003EState Event\u003C/h3\u003E\n\u003Cp\u003EAfter a successful SDK operation, \u003Ccode\u003EChangeMyStatusAsync\u003C/code\u003E publishes \u003Ccode\u003EProfileChanged\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThis is the current client event behavior, even though the operation changes account status rather than editable profile fields.\u003C/p\u003E\n\u003Cp\u003EApplications should not assume that a status change always produces a dedicated \u003Ccode\u003EAuthorizationChanged\u003C/code\u003E event.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022change-another-users-status\u0022\u003E19. Change Another User\u0027s Status\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EChangeUserStatusAsync\u003C/code\u003E for administrative lifecycle management.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.ChangeUserStatusAsync(userKey,\n new ChangeUserStatusAdminRequest\n {\n NewStatus = AdminAssignableUserStatus.Suspended\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { IsSuccess: true } change)\n{\n Console.WriteLine($\u0026quot;Current status: {change.CurrentStatus}\u0026quot;);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis operation requires administrative authorization.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-disable-an-account\u0022\u003EExample: Disable an Account\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.ChangeUserStatusAsync(userKey,\n new ChangeUserStatusAdminRequest\n {\n NewStatus = AdminAssignableUserStatus.Disabled\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-restore-an-active-status\u0022\u003EExample: Restore an Active Status\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.ChangeUserStatusAsync(userKey,\n new ChangeUserStatusAdminRequest\n {\n NewStatus = AdminAssignableUserStatus.Active\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWhether a particular transition is permitted depends on the server\u0027s lifecycle rules.\u003C/p\u003E\n\u003Cp\u003EDo not assume that setting \u003Ccode\u003EActive\u003C/code\u003E automatically restores every form of previously revoked authentication state.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022delete-the-current-user\u0022\u003E20. Delete the Current User\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EDeleteMeAsync\u003C/code\u003E to request deletion of the current account.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.DeleteMeAsync();\n\nif (result.IsSuccess)\n{\n // Account deletion succeeded.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ESelf-service deletion is a soft-delete operation.\u003C/p\u003E\n\u003Cp\u003EAccording to the \u003Ccode\u003EIUserClient\u003C/code\u003E contract, this operation removes associated profiles and sessions.\u003C/p\u003E\n\u003Cp\u003EAfter successful deletion, the client publishes a \u003Ccode\u003EUserDeleted\u003C/code\u003E state event using patch handling mode.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022important-considerations\u0022\u003EImportant Considerations\u003C/h3\u003E\n\u003Cp\u003EAccount deletion is not equivalent to ordinary logout.\u003C/p\u003E\n\u003Cp\u003EIt changes the user\u0027s lifecycle state and may affect associated data across multiple domains.\u003C/p\u003E\n\u003Cp\u003EBefore exposing a delete-account action, applications should provide an appropriate confirmation flow and explain the consequences.\u003C/p\u003E\n\u003Cp\u003EThe server remains responsible for authorization, data cleanup, and deletion policy enforcement.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022administrative-user-deletion\u0022\u003E21. Administrative User Deletion\u003C/h2\u003E\n\u003Cp\u003EAdministrators can delete users through \u003Ccode\u003EDeleteUserAsync\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe method accepts a \u003Ccode\u003EDeleteUserRequest\u003C/code\u003E specifying the deletion mode.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022soft-delete\u0022\u003ESoft Delete\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.DeleteUserAsync(userKey,\n new DeleteUserRequest\n {\n Mode = DeleteMode.Soft\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { IsSuccess: true } deleted)\n{\n Console.WriteLine($\u0026quot;Deletion mode: {deleted.Mode}\u0026quot;);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022hard-delete\u0022\u003EHard Delete\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.DeleteUserAsync(userKey,\n new DeleteUserRequest\n {\n Mode = DeleteMode.Hard\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EHard deletion is a more destructive operation and should be restricted to appropriately authorized administrative workflows.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022deletemode\u0022\u003EDeleteMode\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Epublic enum DeleteMode\n{\n Soft = 0,\n Hard = 10\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022userdeleteresult\u0022\u003EUserDeleteResult\u003C/h3\u003E\n\u003Cp\u003EThe operation returns \u003Ccode\u003EUAuthResult\u0026lt;UserDeleteResult\u0026gt;\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EUserDeleteResult\u003C/code\u003E includes:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003EIsSuccess\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EMode\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EFailureReason\u003C/code\u003E\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eif (!result.IsSuccess)\n{\n // Handle transport or API-level failure.\n}\nelse if (result.Value is { IsSuccess: false } deletion)\n{\n // Handle the operation-level failure.\n Console.WriteLine(deletion.FailureReason);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EA previously deleted account may produce an operation-level success result indicating that no additional deletion was necessary.\u003C/p\u003E\n\u003Cp\u003EDo not infer the complete persistence or retention behavior from the \u003Ccode\u003EDeleteMode\u003C/code\u003E value alone.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022build-a-user-administration-table\u0022\u003E22. Build a User Administration Table\u003C/h2\u003E\n\u003Cp\u003EThe following example demonstrates a basic Blazor user list using \u003Ccode\u003EQueryAsync\u003C/code\u003E.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@using CodeBeam.UltimateAuth.Client\n@using CodeBeam.UltimateAuth.Core.Contracts\n@using CodeBeam.UltimateAuth.Users.Contracts\n@inject IUAuthClient UAuthClient\n\n\u0026lt;h3\u0026gt;Users\u0026lt;/h3\u0026gt;\n\n\u0026lt;input @bind=\u0026quot;_search\u0026quot; placeholder=\u0026quot;Search users\u0026quot; /\u0026gt;\n\u0026lt;button @onclick=\u0026quot;SearchAsync\u0026quot;\u0026gt;Search\u0026lt;/button\u0026gt;\n\n@if (_loading)\n{\n \u0026lt;p\u0026gt;Loading users...\u0026lt;/p\u0026gt;\n}\nelse if (_error is not null)\n{\n \u0026lt;p role=\u0026quot;alert\u0026quot;\u0026gt;@_error\u0026lt;/p\u0026gt;\n}\nelse\n{\n \u0026lt;table\u0026gt;\n \u0026lt;thead\u0026gt;\n \u0026lt;tr\u0026gt;\n \u0026lt;th\u0026gt;Name\u0026lt;/th\u0026gt;\n \u0026lt;th\u0026gt;Username\u0026lt;/th\u0026gt;\n \u0026lt;th\u0026gt;Status\u0026lt;/th\u0026gt;\n \u0026lt;/tr\u0026gt;\n \u0026lt;/thead\u0026gt;\n \u0026lt;tbody\u0026gt;\n @foreach (var user in _users)\n {\n \u0026lt;tr @key=\u0026quot;user.UserKey\u0026quot;\u0026gt;\n \u0026lt;td\u0026gt;@user.DisplayName\u0026lt;/td\u0026gt;\n \u0026lt;td\u0026gt;@user.UserName\u0026lt;/td\u0026gt;\n \u0026lt;td\u0026gt;@user.Status\u0026lt;/td\u0026gt;\n \u0026lt;/tr\u0026gt;\n }\n \u0026lt;/tbody\u0026gt;\n \u0026lt;/table\u0026gt;\n\n \u0026lt;button @onclick=\u0026quot;PreviousPageAsync\u0026quot; disabled=\u0026quot;@(_pageNumber \u0026lt;= 1)\u0026quot;\u0026gt;Previous\u0026lt;/button\u0026gt;\n \u0026lt;button @onclick=\u0026quot;NextPageAsync\u0026quot; disabled=\u0026quot;@(!_hasNext)\u0026quot;\u0026gt;Next\u0026lt;/button\u0026gt;\n}\n\n@code {\n private string _search = \u0026quot;\u0026quot;;\n private int _pageNumber = 1;\n private bool _hasNext;\n private bool _loading;\n private string? _error;\n private IReadOnlyList\u0026lt;UserSummary\u0026gt; _users = [];\n\n protected override async Task OnInitializedAsync()\n {\n await LoadAsync();\n }\n\n private async Task SearchAsync()\n {\n _pageNumber = 1;\n await LoadAsync();\n }\n\n private async Task PreviousPageAsync()\n {\n if (_pageNumber \u0026lt;= 1)\n return;\n\n _pageNumber--;\n await LoadAsync();\n }\n\n private async Task NextPageAsync()\n {\n if (!_hasNext)\n return;\n\n _pageNumber\u002B\u002B;\n await LoadAsync();\n }\n\n private async Task LoadAsync()\n {\n _loading = true;\n _error = null;\n\n try\n {\n var result = await UAuthClient.Users.QueryAsync(\n new UserQuery\n {\n Search = _search,\n PageNumber = _pageNumber,\n PageSize = 20\n });\n\n if (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } page)\n {\n _users = page.Items;\n _hasNext = page.HasNext;\n }\n else\n {\n _error = \u0026quot;Unable to load users.\u0026quot;;\n _users = [];\n _hasNext = false;\n }\n }\n catch (Exception)\n {\n _error = \u0026quot;The user service is unavailable.\u0026quot;;\n _users = [];\n _hasNext = false;\n }\n finally\n {\n _loading = false;\n }\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis example demonstrates searching and pagination.\u003C/p\u003E\n\u003Cp\u003EA production administration interface should additionally include loading indicators, action confirmation, permission-aware UI controls, and appropriate handling for concurrent changes.\u003C/p\u003E\n\u003Cp\u003EAll administrative operations must remain protected by server-side authorization.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022handling-user-operation-results\u0022\u003E23. Handling User Operation Results\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth uses structured result contracts for user operations.\u003C/p\u003E\n\u003Cp\u003ESome methods return:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthResult\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EOthers return:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthResult\u0026lt;T\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EFor operations such as user creation, status changes, and deletion, the returned value also contains an operation-level \u003Ccode\u003EIsSuccess\u003C/code\u003E property.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022transport-level-versus-operation-level-success\u0022\u003ETransport-Level Versus Operation-Level Success\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Users.CreateAsync(request);\n\nif (!result.IsSuccess)\n{\n // API-level failure.\n}\nelse if (result.Value is { IsSuccess: false } operation)\n{\n // User creation was processed but did not succeed.\n Console.WriteLine(operation.FailureReason);\n}\nelse if (result.Value is { IsSuccess: true } created)\n{\n Console.WriteLine(created.UserKey);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ECheck the appropriate success level for the operation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022http-status-helpers\u0022\u003EHTTP Status Helpers\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthResult\u003C/code\u003E exposes convenient status helpers.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eif (result.Http.IsUnauthorized)\n{\n // Authentication required.\n}\nelse if (result.Http.IsForbidden)\n{\n // Insufficient permission.\n}\nelse if (result.Http.IsConflict)\n{\n // Conflicting operation or state.\n}\nelse if (result.Http.IsBadRequest)\n{\n // Invalid request.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAdditional diagnostic fields include \u003Ccode\u003ECorrelationId\u003C/code\u003E, \u003Ccode\u003ETraceId\u003C/code\u003E, \u003Ccode\u003EProblem\u003C/code\u003E, and \u003Ccode\u003EErrorText\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EAvoid exposing sensitive internal failure details directly to end users.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022client-state-events\u0022\u003E24. Client State Events\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthUserClient\u003C/code\u003E publishes state events for successful self-service operations.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EOperation\u003C/th\u003E\n\u003Cth\u003EEvent\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUpdateMyProfileAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EProfileChanged\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECreateMyProfileAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EProfileChanged\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDeleteMyProfileAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EProfileChanged\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EChangeMyStatusAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EProfileChanged\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDeleteMeAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserDeleted\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe event payload depends on the operation.\u003C/p\u003E\n\u003Cp\u003EAdministrative user operations do not automatically publish these same local self-service events.\u003C/p\u003E\n\u003Cp\u003EFor example, after updating another user\u0027s profile, an administration panel should refresh the affected user data as needed.\u003C/p\u003E\n\u003Cp\u003EClient events help synchronize application state, but they do not replace authoritative server-side account status or permission checks.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-and-lifecycle-considerations\u0022\u003E25. Security and Lifecycle Considerations\u003C/h2\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022self-service-and-administrative-boundaries\u0022\u003ESelf-Service and Administrative Boundaries\u003C/h3\u003E\n\u003Cp\u003ESelf-service methods operate on the current authenticated user.\u003C/p\u003E\n\u003Cp\u003EAdministrative methods identify an explicit target user.\u003C/p\u003E\n\u003Cp\u003EDo not assume that access to the SDK automatically authorizes an operation.\u003C/p\u003E\n\u003Cp\u003EThe server must enforce actor, target, tenant, and action requirements.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022status-changes-and-existing-sessions\u0022\u003EStatus Changes and Existing Sessions\u003C/h3\u003E\n\u003Cp\u003EChanging account status may affect authentication eligibility and session continuation.\u003C/p\u003E\n\u003Cp\u003EHowever, immediate invalidation of existing credentials depends on the server\u0027s security policy and authentication mode.\u003C/p\u003E\n\u003Cp\u003EA client-side status update is not itself proof that every previously issued token has been invalidated.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022profile-data-is-not-authorization-data\u0022\u003EProfile Data Is Not Authorization Data\u003C/h3\u003E\n\u003Cp\u003EA user\u0027s display name, biography, or profile metadata should not be used as proof of permissions.\u003C/p\u003E\n\u003Cp\u003EAuthorization decisions must use the relevant trusted security context.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022profile-replacement-can-cause-data-loss\u0022\u003EProfile Replacement Can Cause Data Loss\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EUpdateProfileRequest\u003C/code\u003E is not a patch contract.\u003C/p\u003E\n\u003Cp\u003ESubmitting an incomplete request may clear existing profile fields.\u003C/p\u003E\n\u003Cp\u003EApplications should preserve intended values and consider concurrency when multiple editors can modify the same profile.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022deletion-is-a-lifecycle-operation\u0022\u003EDeletion Is a Lifecycle Operation\u003C/h3\u003E\n\u003Cp\u003ESoft deletion and hard deletion have different implications.\u003C/p\u003E\n\u003Cp\u003EApplications should consider retention, recovery, audit requirements, and dependent data before exposing destructive actions.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022next-steps\u0022\u003ENext Steps\u003C/h2\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/client/identifiers\u0022\u003EIdentifiers\u003C/a\u003E \u2014 Manage usernames, email addresses, phone numbers, and verification.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/client/authentication\u0022\u003EAuthentication\u003C/a\u003E \u2014 Login, refresh, logout, and authentication state.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/client/session-management\u0022\u003ESession Management\u003C/a\u003E \u2014 Inspect and revoke sessions across devices.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/plugin-domains/users\u0022\u003EUsers\u003C/a\u003E \u2014 Understand the user domain and lifecycle architecture.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/plugin-domains/policies\u0022\u003EPolicies \u0026amp; Access Control\u003C/a\u003E \u2014 Understand authorization enforcement for user operations.\u003C/li\u003E\n\u003C/ul\u003E\n", "Headings": [ { "Id": "accessing-the-user-client", @@ -138,9 +138,39 @@ "Text": "Navigating Pages", "Level": 1 }, + { + "Id": "retrieve-users-by-keys", + "Text": "13. Retrieve Users by Keys", + "Level": 0 + }, + { + "Id": "basic-usage", + "Text": "Basic Usage", + "Level": 1 + }, + { + "Id": "batch-size-and-behavior", + "Text": "Batch Size and Behavior", + "Level": 1 + }, + { + "Id": "queryasync-vs.getbykeysasync", + "Text": "QueryAsync vs. GetByKeysAsync", + "Level": 1 + }, + { + "Id": "authorization", + "Text": "Authorization", + "Level": 1 + }, + { + "Id": "performance-considerations", + "Text": "Performance Considerations", + "Level": 1 + }, { "Id": "get-a-specific-user", - "Text": "13. Get a Specific User", + "Text": "14. Get a Specific User", "Level": 0 }, { @@ -150,7 +180,7 @@ }, { "Id": "update-another-users-profile", - "Text": "14. Update Another User\u0027s Profile", + "Text": "15. Update Another User\u0027s Profile", "Level": 0 }, { @@ -160,7 +190,7 @@ }, { "Id": "manage-another-users-profiles", - "Text": "15. Manage Another User\u0027s Profiles", + "Text": "16. Manage Another User\u0027s Profiles", "Level": 0 }, { @@ -175,7 +205,7 @@ }, { "Id": "understanding-user-status", - "Text": "16. Understanding User Status", + "Text": "17. Understanding User Status", "Level": 0 }, { @@ -185,7 +215,7 @@ }, { "Id": "change-your-own-status", - "Text": "17. Change Your Own Status", + "Text": "18. Change Your Own Status", "Level": 0 }, { @@ -200,7 +230,7 @@ }, { "Id": "change-another-users-status", - "Text": "18. Change Another User\u0027s Status", + "Text": "19. Change Another User\u0027s Status", "Level": 0 }, { @@ -215,7 +245,7 @@ }, { "Id": "delete-the-current-user", - "Text": "19. Delete the Current User", + "Text": "20. Delete the Current User", "Level": 0 }, { @@ -225,7 +255,7 @@ }, { "Id": "administrative-user-deletion", - "Text": "20. Administrative User Deletion", + "Text": "21. Administrative User Deletion", "Level": 0 }, { @@ -250,12 +280,12 @@ }, { "Id": "build-a-user-administration-table", - "Text": "21. Build a User Administration Table", + "Text": "22. Build a User Administration Table", "Level": 0 }, { "Id": "handling-user-operation-results", - "Text": "22. Handling User Operation Results", + "Text": "23. Handling User Operation Results", "Level": 0 }, { @@ -270,12 +300,12 @@ }, { "Id": "client-state-events", - "Text": "23. Client State Events", + "Text": "24. Client State Events", "Level": 0 }, { "Id": "security-and-lifecycle-considerations", - "Text": "24. Security and Lifecycle Considerations", + "Text": "25. Security and Lifecycle Considerations", "Level": 0 }, { @@ -309,6 +339,6 @@ "Level": 0 } ], - "SourceHash": "B8C4309BCFECF2ED5F4AE1CFDB7DD6303B0633F8576568BFA0D5497EE2E42DCF", + "SourceHash": "6E53F83F18629B64B09F13E568D5A6D71C6954D07C5842E288430FFB7B526FB6", "BuilderVersion": "1" } \ No newline at end of file diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/policies.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/policies.json index 0cf27717..0251d021 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/policies.json +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/policies.json @@ -1,7 +1,7 @@ { "Slug": "plugin-domains/policies", "Title": "Policies \u0026 Access Control", - "Html": "\n\u003Cp\u003EAuthorization is not simply a question of whether an identity possesses a permission.\u003C/p\u003E\n\u003Cp\u003EA protected operation may involve an authenticated actor, a target identity, tenant boundaries, resource ownership, and security conditions that cannot be represented by a permission alone.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth addresses these requirements through a \u003Cstrong\u003Epolicy-driven access control pipeline\u003C/strong\u003E.\u003C/p\u003E\n\u003Cp\u003EPolicies are independent decision units evaluated within a shared access context. The authorization orchestrator coordinates those decisions and determines whether the requested operation can proceed.\u003C/p\u003E\n\u003Cp\u003EThis page explains how that process works.\u003C/p\u003E\n\u003Cp\u003EFor actions, permissions, roles, and scope conventions, see \u003Ca href=\u0022/docs/plugin-domains/authorization\u0022\u003EAuthorization\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022the-access-decision-model\u0022\u003EThe Access Decision Model\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth evaluates authorization using three primary elements:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EElement\u003C/th\u003E\n\u003Cth\u003EResponsibility\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EAccessContext\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EDescribes the actor, target, operation, and relevant security context\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EPolicies\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EEvaluate individual authorization requirements\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EAccessOrchestrator\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003ECoordinates policy resolution, evaluation, and execution\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThese elements separate the description of an operation from the rules used to authorize it.\u003C/p\u003E\n\u003Cp\u003EA permission may grant authority to perform an action, but policies determine whether that authority is valid in the current context.\u003C/p\u003E\n\u003Cp\u003EFor example, a caller may possess an administrative permission while attempting to access a resource belonging to another tenant.\u003C/p\u003E\n\u003Cp\u003EThe permission may match, but the operation must still satisfy tenant isolation requirements.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022accesscontext\u0022\u003EAccessContext\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EAccessContext\u003C/code\u003E represents the security context of an authorization operation.\u003C/p\u003E\n\u003Cp\u003EIt provides the information needed to evaluate access without requiring each policy to reconstruct the request independently.\u003C/p\u003E\n\u003Cp\u003EConceptually, the context includes:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EComponent\u003C/th\u003E\n\u003Cth\u003EPurpose\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EActor\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EThe identity or system principal performing the operation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003ETarget\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EThe identity or resource affected by the operation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EAction\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EThe protected operation being requested\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003ETenant context\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EThe tenant boundaries relevant to the actor and target\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EAuthorization information\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003ERelevant claims, permissions, and security state\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe context is more than a collection of request parameters.\u003C/p\u003E\n\u003Cp\u003EIt establishes the relationship between the authenticated actor and the operation being performed.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022actor-and-target-are-different-concepts\u0022\u003EActor and Target Are Different Concepts\u003C/h3\u003E\n\u003Cp\u003EConsider an administrative operation:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003Eusers.delete.admin\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe actor is the administrator requesting the deletion.\u003C/p\u003E\n\u003Cp\u003EThe target is the user account being deleted.\u003C/p\u003E\n\u003Cp\u003EThese identities must remain distinguishable throughout authorization and execution.\u003C/p\u003E\n\u003Cp\u003EFor a self-service operation:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003Eusers.delete.self\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe actor and target are expected to represent the same identity.\u003C/p\u003E\n\u003Cp\u003EThat relationship must be established from trusted context, not assumed from the action suffix.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022trusted-context-construction\u0022\u003ETrusted Context Construction\u003C/h3\u003E\n\u003Cp\u003EAuthorization decisions are only as reliable as the information supplied to them.\u003C/p\u003E\n\u003Cp\u003EAn endpoint may receive a target user ID or tenant ID from the caller, but these values must not automatically become trusted authorization facts.\u003C/p\u003E\n\u003Cp\u003EThe authentication infrastructure establishes the actor\u0027s identity. The application or domain operation resolves the intended target. The authorization context brings those elements together.\u003C/p\u003E\n\u003Cp\u003EPolicies then evaluate the resulting relationships.\u003C/p\u003E\n\u003Cp\u003EThis distinction is essential for preventing cross-user and cross-tenant access.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022policies-as-decision-units\u0022\u003EPolicies as Decision Units\u003C/h2\u003E\n\u003Cp\u003EA policy evaluates a specific security requirement.\u003C/p\u003E\n\u003Cp\u003ERather than combining every authorization rule into one large conditional expression, UltimateAuth separates requirements into reusable policies.\u003C/p\u003E\n\u003Cp\u003EExamples include:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003ERequiring an authenticated actor.\u003C/li\u003E\n\u003Cli\u003ERequiring an active user.\u003C/li\u003E\n\u003Cli\u003EEnforcing tenant isolation.\u003C/li\u003E\n\u003Cli\u003EVerifying self-access relationships.\u003C/li\u003E\n\u003Cli\u003ERequiring an appropriate permission.\u003C/li\u003E\n\u003Cli\u003ERestricting an operation to a system context.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EEach policy evaluates its own responsibility within the available access context.\u003C/p\u003E\n\u003Cp\u003EA policy should not need to understand every other policy to make its decision.\u003C/p\u003E\n\u003Cp\u003EThis allows authorization rules to be composed without losing their individual meaning.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022policy-resolution\u0022\u003EPolicy Resolution\u003C/h2\u003E\n\u003Cp\u003ENot every policy is relevant to every operation.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth supports policy resolution based on action matching and runtime applicability.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022action-based-matching\u0022\u003EAction-Based Matching\u003C/h3\u003E\n\u003Cp\u003EPolicies can be associated with action patterns.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003Eusers.create.admin\nusers.*\nauthorization.roles.*\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAn action-specific policy can target one operation, while a broader pattern can apply to a related group of operations.\u003C/p\u003E\n\u003Cp\u003EThis allows common rules to be shared across a domain without duplicating policy registration for every action.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022runtime-applicability\u0022\u003ERuntime Applicability\u003C/h3\u003E\n\u003Cp\u003EAction matching alone may not provide enough information to determine whether a policy should execute.\u003C/p\u003E\n\u003Cp\u003EA policy can also evaluate whether it applies to the current context through \u003Ccode\u003EAppliesTo\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThis enables context-dependent policy selection.\u003C/p\u003E\n\u003Cp\u003EFor example, a policy may apply only to administrative operations that modify account state, rather than every administrative read operation.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EPolicy resolution determines which rules participate in the decision. Policy evaluation determines whether those rules permit the operation.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EKeeping these stages separate makes authorization behavior easier to reason about.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022policy-categories\u0022\u003EPolicy Categories\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth distinguishes between security requirements that must be enforced broadly and policies selected for particular operations.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022invariants\u0022\u003EInvariants\u003C/h3\u003E\n\u003Cp\u003EInvariants represent fundamental security rules that must not be bypassed by ordinary permission grants or application-level authorization decisions.\u003C/p\u003E\n\u003Cp\u003EThey are evaluated before the remaining policy stages.\u003C/p\u003E\n\u003Cp\u003EAn invariant is not equivalent to a role permission or an optional business rule.\u003C/p\u003E\n\u003Cp\u003EIts purpose is to preserve a security condition that must hold regardless of the requested operation\u0027s ordinary authorization configuration.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022global-policies\u0022\u003EGlobal Policies\u003C/h3\u003E\n\u003Cp\u003EGlobal policies apply broadly across protected operations.\u003C/p\u003E\n\u003Cp\u003EThey provide common security requirements that should not need to be registered independently for every action.\u003C/p\u003E\n\u003Cp\u003EExamples of such requirements include authentication and tenant isolation.\u003C/p\u003E\n\u003Cp\u003ETheir applicability may still depend on the operation\u0027s context. An intentionally anonymous action, for example, cannot be treated identically to an authenticated administrative operation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022runtime-policies\u0022\u003ERuntime Policies\u003C/h3\u003E\n\u003Cp\u003ERuntime policies are selected and evaluated according to the requested action and current access context.\u003C/p\u003E\n\u003Cp\u003EThey support requirements such as:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EPermission checks.\u003C/li\u003E\n\u003Cli\u003ESelf-access validation.\u003C/li\u003E\n\u003Cli\u003EActive-user requirements.\u003C/li\u003E\n\u003Cli\u003EOperation-specific restrictions.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThis separation allows the authorization system to maintain mandatory security boundaries while remaining extensible for individual domains and applications.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022policy-evaluation-order\u0022\u003EPolicy Evaluation Order\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth\u0027s authorization pipeline follows a defined evaluation order:\u003C/p\u003E\n\u003Col\u003E\n\u003Cli\u003E\u003Cstrong\u003EInvariants\u003C/strong\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EGlobal policies\u003C/strong\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003ERuntime policies\u003C/strong\u003E\u003C/li\u003E\n\u003C/ol\u003E\n\u003Cp\u003EThis order establishes a clear security hierarchy.\u003C/p\u003E\n\u003Cp\u003EA runtime permission grant must not override an invariant that has already rejected the operation.\u003C/p\u003E\n\u003Cp\u003ELikewise, satisfying an action-specific policy does not eliminate the need to satisfy applicable global requirements.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022deny-takes-precedence\u0022\u003EDeny Takes Precedence\u003C/h3\u003E\n\u003Cp\u003EThe decision model is deny-oriented.\u003C/p\u003E\n\u003Cp\u003EWhen a required policy denies an operation, a later policy cannot convert that denial into an authorization grant.\u003C/p\u003E\n\u003Cp\u003EThis prevents authorization from depending on the accidental presence of a permissive policy.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022allow-is-not-a-universal-grant\u0022\u003EAllow Is Not a Universal Grant\u003C/h3\u003E\n\u003Cp\u003EA policy returning an allow result means that its own requirement has been satisfied or that it has no objection to the operation.\u003C/p\u003E\n\u003Cp\u003EIt does not mean that all other requirements have been satisfied.\u003C/p\u003E\n\u003Cp\u003EThe overall decision is produced by the authorization pipeline, not by one successful policy in isolation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022reauthentication\u0022\u003EReauthentication\u003C/h3\u003E\n\u003Cp\u003ESome authorization decisions may require additional authentication rather than producing an ordinary allow or deny outcome.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth\u0027s decision model includes the ability to represent a reauthentication requirement.\u003C/p\u003E\n\u003Cp\u003EThis allows authorization to distinguish between insufficient authority and situations where the actor may need to establish a stronger or more recent authentication state.\u003C/p\u003E\n\u003Cp\u003EThe exact handling of such a result depends on the surrounding authentication and application flow.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022accessorchestrator\u0022\u003EAccessOrchestrator\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EAccessOrchestrator\u003C/code\u003E coordinates the authorization process.\u003C/p\u003E\n\u003Cp\u003EIts responsibilities include preparing the effective access context, resolving applicable policies, evaluating the required security conditions, and coordinating execution when access is permitted.\u003C/p\u003E\n\u003Cp\u003EThe conceptual pipeline is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003ERequested operation\n |\n v\nBuild AccessContext\n |\n v\nResolve authorization information\n |\n v\nEvaluate invariants\n |\n v\nEvaluate global policies\n |\n v\nResolve applicable runtime policies\n |\n v\nEvaluate runtime policies\n |\n \u002B---- Deny ------\u0026gt; Reject operation\n |\n \u002B---- Reauth ----\u0026gt; Require reauthentication\n |\n v\n Allow\n |\n v\nExecute authorized operation\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis is a conceptual representation of the access decision pipeline, not a substitute for the concrete orchestration API.\u003C/p\u003E\n\u003Cp\u003EThe orchestrator centralizes authorization coordination so that individual application services do not need to reproduce the complete evaluation process.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022authorization-before-execution\u0022\u003EAuthorization Before Execution\u003C/h2\u003E\n\u003Cp\u003EAuthorization should be enforced before a protected operation performs security-sensitive changes.\u003C/p\u003E\n\u003Cp\u003EConsider an administrative credential operation.\u003C/p\u003E\n\u003Cp\u003EThe system may need to establish:\u003C/p\u003E\n\u003Col\u003E\n\u003Cli\u003EWho is performing the operation.\u003C/li\u003E\n\u003Cli\u003EWhich account is being targeted.\u003C/li\u003E\n\u003Cli\u003EWhether the actor possesses the required administrative permission.\u003C/li\u003E\n\u003Cli\u003EWhether the target belongs to an allowed tenant.\u003C/li\u003E\n\u003Cli\u003EWhether additional operation-specific restrictions apply.\u003C/li\u003E\n\u003C/ol\u003E\n\u003Cp\u003EOnly after the applicable requirements succeed should the protected mutation proceed.\u003C/p\u003E\n\u003Cp\u003EThis separation is particularly important for operations that affect credentials, account status, roles, or sessions.\u003C/p\u003E\n\u003Cp\u003EA rejected authorization decision must not leave behind a partially completed protected operation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-preventing-administrative-self-modification\u0022\u003EExample: Preventing Administrative Self-Modification\u003C/h2\u003E\n\u003Cp\u003ESome administrative operations may intentionally prohibit an administrator from modifying their own account through an administrative route.\u003C/p\u003E\n\u003Cp\u003EThis is different from ordinary self-service account management.\u003C/p\u003E\n\u003Cp\u003EA policy enforcing this restriction could consider:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EWhether the requested action has administrative scope.\u003C/li\u003E\n\u003Cli\u003EWhether the operation modifies security-sensitive account state.\u003C/li\u003E\n\u003Cli\u003EWhether the actor and target represent the same identity.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EIf the policy applies and the actor is also the target, it denies the operation.\u003C/p\u003E\n\u003Cp\u003EThe purpose is to prevent an administrative capability from bypassing the separate rules governing self-service operations.\u003C/p\u003E\n\u003Cp\u003ESuch a policy illustrates why permission checks alone are insufficient.\u003C/p\u003E\n\u003Cp\u003EAn administrator may possess the required action permission while still being prohibited from performing the operation against a particular target.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-requiring-an-active-user\u0022\u003EExample: Requiring an Active User\u003C/h2\u003E\n\u003Cp\u003EAn active-user policy evaluates whether the relevant identity satisfies the account state requirements for an operation.\u003C/p\u003E\n\u003Cp\u003EAn authenticated principal may exist even after the corresponding user account has been suspended or otherwise made ineligible.\u003C/p\u003E\n\u003Cp\u003EThe authorization system therefore needs a way to enforce account eligibility independently of the original authentication event.\u003C/p\u003E\n\u003Cp\u003EThe policy can evaluate the relevant user state and deny operations that require an active account.\u003C/p\u003E\n\u003Cp\u003EAnonymous operations may follow different applicability rules because they do not necessarily have an authenticated user identity.\u003C/p\u003E\n\u003Cp\u003EThe exact enforcement behavior also depends on how current user security state is obtained.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022policy-decisions-and-authentication-modes\u0022\u003EPolicy Decisions and Authentication Modes\u003C/h2\u003E\n\u003Cp\u003EThe policy model is conceptually independent of the authentication mode, but the freshness of its input data matters.\u003C/p\u003E\n\u003Cp\u003EIn session-authoritative modes, server-side state may be consulted as part of protected operations.\u003C/p\u003E\n\u003Cp\u003EIn JWT-based modes, some authorization information may originate from claims issued earlier.\u003C/p\u003E\n\u003Cp\u003EThis creates a distinction between:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EEvaluating a policy against current authoritative data.\u003C/li\u003E\n\u003Cli\u003EEvaluating a policy against previously issued authorization claims.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EFor example, a permission removed after token issuance may still be present in a locally validated JWT until that token expires.\u003C/p\u003E\n\u003Cp\u003EA policy can evaluate its inputs correctly while those inputs reflect an earlier authorization state.\u003C/p\u003E\n\u003Cp\u003EApplications must account for this when choosing token lifetimes, permission resolution strategies, and security requirements.\u003C/p\u003E\n\u003Cp\u003ESee \u003Ca href=\u0022/docs/fundamentals/auth-modes\u0022\u003EAuthentication Modes\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022application-level-access-control\u0022\u003EApplication-Level Access Control\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth\u0027s built-in authorization rules protect framework operations, but applications also have their own resources and business requirements.\u003C/p\u003E\n\u003Cp\u003EA business application may need policies for:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EEditing resources owned by a particular user.\u003C/li\u003E\n\u003Cli\u003ERestricting operations to a tenant.\u003C/li\u003E\n\u003Cli\u003ERequiring elevated authority for administrative changes.\u003C/li\u003E\n\u003Cli\u003EPreventing sensitive changes under particular account conditions.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThese requirements can be modeled as application-specific authorization policies.\u003C/p\u003E\n\u003Cp\u003EThe recommended architectural approach is to keep security decisions at a boundary that cannot be bypassed by alternative execution paths.\u003C/p\u003E\n\u003Cp\u003EFor HTTP endpoints, ASP.NET Core authorization can provide the request-level enforcement boundary.\u003C/p\u003E\n\u003Cp\u003EFor direct application service calls, including in-process operations, authorization must also be enforced where the protected operation actually executes.\u003C/p\u003E\n\u003Cp\u003EThe presence of an \u003Ccode\u003E[Authorize]\u003C/code\u003E attribute on a controller does not automatically protect a service invoked directly from another part of the application.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022extending-the-policy-pipeline\u0022\u003EExtending the Policy Pipeline\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth\u0027s policy model is designed to accommodate application-specific requirements without modifying the authentication engine.\u003C/p\u003E\n\u003Cp\u003ECustom policies can introduce additional authorization conditions while participating in the established access decision process.\u003C/p\u003E\n\u003Cp\u003ESuch policies should preserve the framework\u0027s security boundaries:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EDo not treat untrusted request values as established identity facts.\u003C/li\u003E\n\u003Cli\u003EDo not bypass mandatory invariants.\u003C/li\u003E\n\u003Cli\u003EDo not use broad permission grants to override target or tenant restrictions.\u003C/li\u003E\n\u003Cli\u003EKeep policy applicability and evaluation behavior explicit.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EFor a complete, practical example of implementing and registering custom application policies, see \u003Ca href=\u0022/docs/how-to-guides/define-authorization-policies\u0022\u003EDefine Authorization Policies\u003C/a\u003E. The guide demonstrates how to enforce Separation of Duties, compose multiple authorization requirements, and simplify application services.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022architectural-perspective\u0022\u003EArchitectural Perspective\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth authorization is a coordinated evaluation of security requirements.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EAccessContext\u003C/code\u003E describes the operation. Policy resolution identifies the relevant requirements. Policy evaluation determines whether those requirements are satisfied. \u003Ccode\u003EAccessOrchestrator\u003C/code\u003E coordinates the process and its execution boundary.\u003C/p\u003E\n\u003Cp\u003EThe result is an authorization model that is \u003Cstrong\u003Econtext-aware, policy-driven, and extensible without making individual permissions responsible for every security decision\u003C/strong\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022next-steps\u0022\u003ENext Steps\u003C/h2\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/plugin-domains/authorization\u0022\u003EAuthorization\u003C/a\u003E \u2014 Actions, permissions, roles, and scope semantics.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/plugin-domains/users\u0022\u003EUsers\u003C/a\u003E \u2014 Account lifecycle and identity state.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/plugin-domains/credentials\u0022\u003ECredentials\u003C/a\u003E \u2014 Credential ownership and security operations.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/fundamentals/request-lifecycle\u0022\u003ERequest Lifecycle\u003C/a\u003E \u2014 Authorization within protected requests.\u003C/li\u003E\n\u003C/ul\u003E\n", + "Html": "\n\u003Cp\u003EAuthorization is not simply a question of whether an identity possesses a permission.\u003C/p\u003E\n\u003Cp\u003EA protected operation may involve an authenticated actor, a target identity, tenant boundaries, resource ownership, and security conditions that cannot be represented by a permission alone.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth addresses these requirements through a \u003Cstrong\u003Epolicy-driven access control pipeline\u003C/strong\u003E.\u003C/p\u003E\n\u003Cp\u003EPolicies are independent decision units evaluated within a shared access context. The authorization orchestrator coordinates those decisions and determines whether the requested operation can proceed.\u003C/p\u003E\n\u003Cp\u003EThis page explains how that process works.\u003C/p\u003E\n\u003Cp\u003EFor actions, permissions, roles, and scope conventions, see \u003Ca href=\u0022/docs/plugin-domains/authorization\u0022\u003EAuthorization\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022the-access-decision-model\u0022\u003EThe Access Decision Model\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth evaluates authorization using three primary elements:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EElement\u003C/th\u003E\n\u003Cth\u003EResponsibility\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EAccessContext\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EDescribes the actor, target, operation, and relevant security context\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EPolicies\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EEvaluate individual authorization requirements\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EAccessOrchestrator\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003ECoordinates policy resolution, evaluation, and execution\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThese elements separate the description of an operation from the rules used to authorize it.\u003C/p\u003E\n\u003Cp\u003EA permission may grant authority to perform an action, but policies determine whether that authority is valid in the current context.\u003C/p\u003E\n\u003Cp\u003EFor example, a caller may possess an administrative permission while attempting to access a resource belonging to another tenant.\u003C/p\u003E\n\u003Cp\u003EThe permission may match, but the operation must still satisfy tenant isolation requirements.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022accesscontext\u0022\u003EAccessContext\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EAccessContext\u003C/code\u003E represents the security context of an authorization operation.\u003C/p\u003E\n\u003Cp\u003EIt provides the information needed to evaluate access without requiring each policy to reconstruct the request independently.\u003C/p\u003E\n\u003Cp\u003EConceptually, the context includes:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EComponent\u003C/th\u003E\n\u003Cth\u003EPurpose\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EActor\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EThe identity or system principal performing the operation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003ETarget\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EThe identity or resource affected by the operation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EAction\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EThe protected operation being requested\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003ETenant context\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EThe tenant boundaries relevant to the actor and target\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EAuthorization information\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003ERelevant claims, permissions, and security state\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe context is more than a collection of request parameters.\u003C/p\u003E\n\u003Cp\u003EIt establishes the relationship between the authenticated actor and the operation being performed.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022actor-and-target-are-different-concepts\u0022\u003EActor and Target Are Different Concepts\u003C/h3\u003E\n\u003Cp\u003EConsider an administrative operation:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003Eusers.delete.admin\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe actor is the administrator requesting the deletion.\u003C/p\u003E\n\u003Cp\u003EThe target is the user account being deleted.\u003C/p\u003E\n\u003Cp\u003EThese identities must remain distinguishable throughout authorization and execution.\u003C/p\u003E\n\u003Cp\u003EFor a self-service operation:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003Eusers.delete.self\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe actor and target are expected to represent the same identity.\u003C/p\u003E\n\u003Cp\u003EThat relationship must be established from trusted context, not assumed from the action suffix.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022multi-target-authorization\u0022\u003EMulti-Target Authorization\u003C/h3\u003E\n\u003Cp\u003ESome operations affect or retrieve multiple users within a single request.\u003C/p\u003E\n\u003Cp\u003EFor example, an application may need to retrieve the summaries of several users belonging to a team.\u003C/p\u003E\n\u003Cp\u003EA traditional single-target authorization context identifies one target user:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Econtext.TargetUserKey\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EBatch operations may not have a single target.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth therefore also provides:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Econtext.TargetUserKeys\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis property contains the user keys associated with a multi-target operation.\u003C/p\u003E\n\u003Cp\u003EThe collection is normalized, immutable, and available to authorization policies.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EPurpose\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETargetUserKey\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifies a single target user\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETargetUserKeys\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifies multiple target users\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EHasTarget\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIndicates whether a single target is present\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EHasMultipleTargets\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIndicates whether a batch target collection is present\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003ESingle-target and multi-target operations remain distinct.\u003C/p\u003E\n\u003Cp\u003EAn operation targeting multiple users does not automatically become a self-service operation, even when the actor is included in the requested collection.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022creating-a-multi-target-context\u0022\u003ECreating a Multi-Target Context\u003C/h3\u003E\n\u003Cp\u003EServer-side operations can construct a multi-target authorization context through \u003Ccode\u003EIAccessContextFactory\u003C/code\u003E:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar accessContext =\n await accessContextFactory.CreateForUserTargetsAsync(\n authFlow: authFlow,\n action: UAuthActions.Users.GetByKeysAdmin,\n resource: \u0026quot;users\u0026quot;,\n targetUserKeys: userKeys,\n ct: cancellationToken);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe factory preserves the existing actor, tenant, role resolution, and authorization context construction mechanisms.\u003C/p\u003E\n\u003Cp\u003EThe requested target collection is supplied explicitly.\u003C/p\u003E\n\u003Cp\u003EThe resulting context can be evaluated by the existing authorization pipeline without introducing a separate authorization mechanism.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-restrict-batch-access-to-group-members\u0022\u003EExample: Restrict Batch Access to Group Members\u003C/h3\u003E\n\u003Cp\u003EConsider an application where users belong to groups.\u003C/p\u003E\n\u003Cp\u003EA member may retrieve the summaries of other users in the same group, but must not retrieve users belonging to another group.\u003C/p\u003E\n\u003Cp\u003EThe application can implement this restriction through an \u003Ccode\u003EIAccessPolicy\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe following example illustrates the target validation logic inside a policy:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eif (context.Action == UAuthActions.Users.GetByKeysAdmin)\n{\n var requestedKeys = context.TargetUserKeys;\n\n if (requestedKeys.Count == 0)\n {\n return AccessDecision.Deny(\u0026quot;batch_targets_required\u0026quot;);\n }\n\n var keys = requestedKeys.Distinct().ToArray();\n\n var allowedCount = await db.GroupMembers\n .AsNoTracking()\n .CountAsync(\n member =\u0026gt;\n member.GroupId == actorGroupId \u0026amp;\u0026amp;\n keys.Contains(member.UserKey),\n cancellationToken);\n\n if (allowedCount != keys.Length)\n {\n return AccessDecision.Deny(\u0026quot;batch_target_out_of_scope\u0026quot;);\n }\n\n return AccessDecision.Allow();\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EHere, \u003Ccode\u003Edb\u003C/code\u003E, \u003Ccode\u003EactorGroupId\u003C/code\u003E, and \u003Ccode\u003EcancellationToken\u003C/code\u003E represent application-specific dependencies and values resolved by the surrounding policy.\u003C/p\u003E\n\u003Cp\u003EThe example assumes that each user has at most one membership record within the selected group.\u003C/p\u003E\n\u003Cp\u003EThe policy verifies that every requested user belongs to the actor\u0027s authorized group.\u003C/p\u003E\n\u003Cp\u003EIf any target falls outside the permitted scope, the entire operation is denied.\u003C/p\u003E\n\u003Cp\u003EThis prevents a request from mixing authorized and unauthorized users to obtain information about another group.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022authorization-must-cover-the-complete-batch\u0022\u003EAuthorization Must Cover the Complete Batch\u003C/h3\u003E\n\u003Cp\u003EA secure multi-target policy should evaluate every requested target.\u003C/p\u003E\n\u003Cp\u003EChecking only the first user or allowing the operation because at least one user is authorized is insufficient.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003ERequested users:\n User A \u2014 Group 1\n User B \u2014 Group 1\n User C \u2014 Group 2\n\nActor:\n Member of Group 1\n\nDecision:\n Deny\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe entire request is denied because User C is outside the actor\u0027s permitted scope.\u003C/p\u003E\n\u003Cp\u003EApplications should not silently return unauthorized users or assume that a batch operation is safe merely because its action permission was granted.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022permission-and-scope-are-separate-requirements\u0022\u003EPermission and Scope Are Separate Requirements\u003C/h3\u003E\n\u003Cp\u003EThe batch user retrieval action is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003Eusers.getbykeys.admin\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAn application may grant this permission to ordinary users when group-level access is required.\u003C/p\u003E\n\u003Cp\u003EHowever, permission evaluation and target scope validation solve different problems.\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Cstrong\u003EPermission:\u003C/strong\u003E May this actor perform batch user retrieval?\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EScope policy:\u003C/strong\u003E May this actor retrieve these particular users?\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EBoth requirements must be satisfied.\u003C/p\u003E\n\u003Cp\u003EAn application\u0027s authorization policy must enforce the relevant target restrictions.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022trusted-target-collections\u0022\u003ETrusted Target Collections\u003C/h3\u003E\n\u003Cp\u003ETarget keys originate from an operation request, but they are not automatically trusted authorization facts.\u003C/p\u003E\n\u003Cp\u003EThe server must validate and normalize the requested keys before constructing the authorization context.\u003C/p\u003E\n\u003Cp\u003EThe same normalized collection must be used for authorization and execution.\u003C/p\u003E\n\u003Cp\u003EOtherwise, a policy could approve one collection while the application service retrieves another.\u003C/p\u003E\n\u003Cp\u003EApplications should also ensure that batch authorization fails closed when required target information is missing.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003ETargetUserKeys\u003C/code\u003E makes multi-target authorization possible; it does not automatically enforce group membership, ownership, or other application-specific restrictions.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022trusted-context-construction\u0022\u003ETrusted Context Construction\u003C/h3\u003E\n\u003Cp\u003EAuthorization decisions are only as reliable as the information supplied to them.\u003C/p\u003E\n\u003Cp\u003EAn endpoint may receive a target user ID or tenant ID from the caller, but these values must not automatically become trusted authorization facts.\u003C/p\u003E\n\u003Cp\u003EThe authentication infrastructure establishes the actor\u0027s identity. The application or domain operation resolves the intended target. The authorization context brings those elements together.\u003C/p\u003E\n\u003Cp\u003EPolicies then evaluate the resulting relationships.\u003C/p\u003E\n\u003Cp\u003EThis distinction is essential for preventing cross-user and cross-tenant access.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022policies-as-decision-units\u0022\u003EPolicies as Decision Units\u003C/h2\u003E\n\u003Cp\u003EA policy evaluates a specific security requirement.\u003C/p\u003E\n\u003Cp\u003ERather than combining every authorization rule into one large conditional expression, UltimateAuth separates requirements into reusable policies.\u003C/p\u003E\n\u003Cp\u003EExamples include:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003ERequiring an authenticated actor.\u003C/li\u003E\n\u003Cli\u003ERequiring an active user.\u003C/li\u003E\n\u003Cli\u003EEnforcing tenant isolation.\u003C/li\u003E\n\u003Cli\u003EVerifying self-access relationships.\u003C/li\u003E\n\u003Cli\u003ERequiring an appropriate permission.\u003C/li\u003E\n\u003Cli\u003ERestricting an operation to a system context.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EEach policy evaluates its own responsibility within the available access context.\u003C/p\u003E\n\u003Cp\u003EA policy should not need to understand every other policy to make its decision.\u003C/p\u003E\n\u003Cp\u003EThis allows authorization rules to be composed without losing their individual meaning.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022policy-resolution\u0022\u003EPolicy Resolution\u003C/h2\u003E\n\u003Cp\u003ENot every policy is relevant to every operation.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth supports policy resolution based on action matching and runtime applicability.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022action-based-matching\u0022\u003EAction-Based Matching\u003C/h3\u003E\n\u003Cp\u003EPolicies can be associated with action patterns.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003Eusers.create.admin\nusers.*\nauthorization.roles.*\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAn action-specific policy can target one operation, while a broader pattern can apply to a related group of operations.\u003C/p\u003E\n\u003Cp\u003EThis allows common rules to be shared across a domain without duplicating policy registration for every action.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022runtime-applicability\u0022\u003ERuntime Applicability\u003C/h3\u003E\n\u003Cp\u003EAction matching alone may not provide enough information to determine whether a policy should execute.\u003C/p\u003E\n\u003Cp\u003EA policy can also evaluate whether it applies to the current context through \u003Ccode\u003EAppliesTo\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThis enables context-dependent policy selection.\u003C/p\u003E\n\u003Cp\u003EFor example, a policy may apply only to administrative operations that modify account state, rather than every administrative read operation.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EPolicy resolution determines which rules participate in the decision. Policy evaluation determines whether those rules permit the operation.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EKeeping these stages separate makes authorization behavior easier to reason about.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022policy-categories\u0022\u003EPolicy Categories\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth distinguishes between security requirements that must be enforced broadly and policies selected for particular operations.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022invariants\u0022\u003EInvariants\u003C/h3\u003E\n\u003Cp\u003EInvariants represent fundamental security rules that must not be bypassed by ordinary permission grants or application-level authorization decisions.\u003C/p\u003E\n\u003Cp\u003EThey are evaluated before the remaining policy stages.\u003C/p\u003E\n\u003Cp\u003EAn invariant is not equivalent to a role permission or an optional business rule.\u003C/p\u003E\n\u003Cp\u003EIts purpose is to preserve a security condition that must hold regardless of the requested operation\u0027s ordinary authorization configuration.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022global-policies\u0022\u003EGlobal Policies\u003C/h3\u003E\n\u003Cp\u003EGlobal policies apply broadly across protected operations.\u003C/p\u003E\n\u003Cp\u003EThey provide common security requirements that should not need to be registered independently for every action.\u003C/p\u003E\n\u003Cp\u003EExamples of such requirements include authentication and tenant isolation.\u003C/p\u003E\n\u003Cp\u003ETheir applicability may still depend on the operation\u0027s context. An intentionally anonymous action, for example, cannot be treated identically to an authenticated administrative operation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022runtime-policies\u0022\u003ERuntime Policies\u003C/h3\u003E\n\u003Cp\u003ERuntime policies are selected and evaluated according to the requested action and current access context.\u003C/p\u003E\n\u003Cp\u003EThey support requirements such as:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EPermission checks.\u003C/li\u003E\n\u003Cli\u003ESelf-access validation.\u003C/li\u003E\n\u003Cli\u003EActive-user requirements.\u003C/li\u003E\n\u003Cli\u003EOperation-specific restrictions.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThis separation allows the authorization system to maintain mandatory security boundaries while remaining extensible for individual domains and applications.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022policy-evaluation-order\u0022\u003EPolicy Evaluation Order\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth\u0027s authorization pipeline follows a defined evaluation order:\u003C/p\u003E\n\u003Col\u003E\n\u003Cli\u003E\u003Cstrong\u003EInvariants\u003C/strong\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EGlobal policies\u003C/strong\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003ERuntime policies\u003C/strong\u003E\u003C/li\u003E\n\u003C/ol\u003E\n\u003Cp\u003EThis order establishes a clear security hierarchy.\u003C/p\u003E\n\u003Cp\u003EA runtime permission grant must not override an invariant that has already rejected the operation.\u003C/p\u003E\n\u003Cp\u003ELikewise, satisfying an action-specific policy does not eliminate the need to satisfy applicable global requirements.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022deny-takes-precedence\u0022\u003EDeny Takes Precedence\u003C/h3\u003E\n\u003Cp\u003EThe decision model is deny-oriented.\u003C/p\u003E\n\u003Cp\u003EWhen a required policy denies an operation, a later policy cannot convert that denial into an authorization grant.\u003C/p\u003E\n\u003Cp\u003EThis prevents authorization from depending on the accidental presence of a permissive policy.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022allow-is-not-a-universal-grant\u0022\u003EAllow Is Not a Universal Grant\u003C/h3\u003E\n\u003Cp\u003EA policy returning an allow result means that its own requirement has been satisfied or that it has no objection to the operation.\u003C/p\u003E\n\u003Cp\u003EIt does not mean that all other requirements have been satisfied.\u003C/p\u003E\n\u003Cp\u003EThe overall decision is produced by the authorization pipeline, not by one successful policy in isolation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022reauthentication\u0022\u003EReauthentication\u003C/h3\u003E\n\u003Cp\u003ESome authorization decisions may require additional authentication rather than producing an ordinary allow or deny outcome.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth\u0027s decision model includes the ability to represent a reauthentication requirement.\u003C/p\u003E\n\u003Cp\u003EThis allows authorization to distinguish between insufficient authority and situations where the actor may need to establish a stronger or more recent authentication state.\u003C/p\u003E\n\u003Cp\u003EThe exact handling of such a result depends on the surrounding authentication and application flow.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022accessorchestrator\u0022\u003EAccessOrchestrator\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EAccessOrchestrator\u003C/code\u003E coordinates the authorization process.\u003C/p\u003E\n\u003Cp\u003EIts responsibilities include preparing the effective access context, resolving applicable policies, evaluating the required security conditions, and coordinating execution when access is permitted.\u003C/p\u003E\n\u003Cp\u003EThe conceptual pipeline is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003ERequested operation\n |\n v\nBuild AccessContext\n |\n v\nResolve authorization information\n |\n v\nEvaluate invariants\n |\n v\nEvaluate global policies\n |\n v\nResolve applicable runtime policies\n |\n v\nEvaluate runtime policies\n |\n \u002B---- Deny ------\u0026gt; Reject operation\n |\n \u002B---- Reauth ----\u0026gt; Require reauthentication\n |\n v\n Allow\n |\n v\nExecute authorized operation\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis is a conceptual representation of the access decision pipeline, not a substitute for the concrete orchestration API.\u003C/p\u003E\n\u003Cp\u003EThe orchestrator centralizes authorization coordination so that individual application services do not need to reproduce the complete evaluation process.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022authorization-before-execution\u0022\u003EAuthorization Before Execution\u003C/h2\u003E\n\u003Cp\u003EAuthorization should be enforced before a protected operation performs security-sensitive changes.\u003C/p\u003E\n\u003Cp\u003EConsider an administrative credential operation.\u003C/p\u003E\n\u003Cp\u003EThe system may need to establish:\u003C/p\u003E\n\u003Col\u003E\n\u003Cli\u003EWho is performing the operation.\u003C/li\u003E\n\u003Cli\u003EWhich account is being targeted.\u003C/li\u003E\n\u003Cli\u003EWhether the actor possesses the required administrative permission.\u003C/li\u003E\n\u003Cli\u003EWhether the target belongs to an allowed tenant.\u003C/li\u003E\n\u003Cli\u003EWhether additional operation-specific restrictions apply.\u003C/li\u003E\n\u003C/ol\u003E\n\u003Cp\u003EOnly after the applicable requirements succeed should the protected mutation proceed.\u003C/p\u003E\n\u003Cp\u003EThis separation is particularly important for operations that affect credentials, account status, roles, or sessions.\u003C/p\u003E\n\u003Cp\u003EA rejected authorization decision must not leave behind a partially completed protected operation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-preventing-administrative-self-modification\u0022\u003EExample: Preventing Administrative Self-Modification\u003C/h2\u003E\n\u003Cp\u003ESome administrative operations may intentionally prohibit an administrator from modifying their own account through an administrative route.\u003C/p\u003E\n\u003Cp\u003EThis is different from ordinary self-service account management.\u003C/p\u003E\n\u003Cp\u003EA policy enforcing this restriction could consider:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EWhether the requested action has administrative scope.\u003C/li\u003E\n\u003Cli\u003EWhether the operation modifies security-sensitive account state.\u003C/li\u003E\n\u003Cli\u003EWhether the actor and target represent the same identity.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EIf the policy applies and the actor is also the target, it denies the operation.\u003C/p\u003E\n\u003Cp\u003EThe purpose is to prevent an administrative capability from bypassing the separate rules governing self-service operations.\u003C/p\u003E\n\u003Cp\u003ESuch a policy illustrates why permission checks alone are insufficient.\u003C/p\u003E\n\u003Cp\u003EAn administrator may possess the required action permission while still being prohibited from performing the operation against a particular target.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-requiring-an-active-user\u0022\u003EExample: Requiring an Active User\u003C/h2\u003E\n\u003Cp\u003EAn active-user policy evaluates whether the relevant identity satisfies the account state requirements for an operation.\u003C/p\u003E\n\u003Cp\u003EAn authenticated principal may exist even after the corresponding user account has been suspended or otherwise made ineligible.\u003C/p\u003E\n\u003Cp\u003EThe authorization system therefore needs a way to enforce account eligibility independently of the original authentication event.\u003C/p\u003E\n\u003Cp\u003EThe policy can evaluate the relevant user state and deny operations that require an active account.\u003C/p\u003E\n\u003Cp\u003EAnonymous operations may follow different applicability rules because they do not necessarily have an authenticated user identity.\u003C/p\u003E\n\u003Cp\u003EThe exact enforcement behavior also depends on how current user security state is obtained.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022policy-decisions-and-authentication-modes\u0022\u003EPolicy Decisions and Authentication Modes\u003C/h2\u003E\n\u003Cp\u003EThe policy model is conceptually independent of the authentication mode, but the freshness of its input data matters.\u003C/p\u003E\n\u003Cp\u003EIn session-authoritative modes, server-side state may be consulted as part of protected operations.\u003C/p\u003E\n\u003Cp\u003EIn JWT-based modes, some authorization information may originate from claims issued earlier.\u003C/p\u003E\n\u003Cp\u003EThis creates a distinction between:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EEvaluating a policy against current authoritative data.\u003C/li\u003E\n\u003Cli\u003EEvaluating a policy against previously issued authorization claims.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EFor example, a permission removed after token issuance may still be present in a locally validated JWT until that token expires.\u003C/p\u003E\n\u003Cp\u003EA policy can evaluate its inputs correctly while those inputs reflect an earlier authorization state.\u003C/p\u003E\n\u003Cp\u003EApplications must account for this when choosing token lifetimes, permission resolution strategies, and security requirements.\u003C/p\u003E\n\u003Cp\u003ESee \u003Ca href=\u0022/docs/fundamentals/auth-modes\u0022\u003EAuthentication Modes\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022application-level-access-control\u0022\u003EApplication-Level Access Control\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth\u0027s built-in authorization rules protect framework operations, but applications also have their own resources and business requirements.\u003C/p\u003E\n\u003Cp\u003EA business application may need policies for:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EEditing resources owned by a particular user.\u003C/li\u003E\n\u003Cli\u003ERestricting operations to a tenant.\u003C/li\u003E\n\u003Cli\u003ERequiring elevated authority for administrative changes.\u003C/li\u003E\n\u003Cli\u003EPreventing sensitive changes under particular account conditions.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThese requirements can be modeled as application-specific authorization policies.\u003C/p\u003E\n\u003Cp\u003EThe recommended architectural approach is to keep security decisions at a boundary that cannot be bypassed by alternative execution paths.\u003C/p\u003E\n\u003Cp\u003EFor HTTP endpoints, ASP.NET Core authorization can provide the request-level enforcement boundary.\u003C/p\u003E\n\u003Cp\u003EFor direct application service calls, including in-process operations, authorization must also be enforced where the protected operation actually executes.\u003C/p\u003E\n\u003Cp\u003EThe presence of an \u003Ccode\u003E[Authorize]\u003C/code\u003E attribute on a controller does not automatically protect a service invoked directly from another part of the application.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022extending-the-policy-pipeline\u0022\u003EExtending the Policy Pipeline\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth\u0027s policy model is designed to accommodate application-specific requirements without modifying the authentication engine.\u003C/p\u003E\n\u003Cp\u003ECustom policies can introduce additional authorization conditions while participating in the established access decision process.\u003C/p\u003E\n\u003Cp\u003ESuch policies should preserve the framework\u0027s security boundaries:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EDo not treat untrusted request values as established identity facts.\u003C/li\u003E\n\u003Cli\u003EDo not bypass mandatory invariants.\u003C/li\u003E\n\u003Cli\u003EDo not use broad permission grants to override target or tenant restrictions.\u003C/li\u003E\n\u003Cli\u003EKeep policy applicability and evaluation behavior explicit.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EFor a complete, practical example of implementing and registering custom application policies, see \u003Ca href=\u0022/docs/how-to-guides/define-authorization-policies\u0022\u003EDefine Authorization Policies\u003C/a\u003E. The guide demonstrates how to enforce Separation of Duties, compose multiple authorization requirements, and simplify application services.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022architectural-perspective\u0022\u003EArchitectural Perspective\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth authorization is a coordinated evaluation of security requirements.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EAccessContext\u003C/code\u003E describes the operation. Policy resolution identifies the relevant requirements. Policy evaluation determines whether those requirements are satisfied. \u003Ccode\u003EAccessOrchestrator\u003C/code\u003E coordinates the process and its execution boundary.\u003C/p\u003E\n\u003Cp\u003EThe result is an authorization model that is \u003Cstrong\u003Econtext-aware, policy-driven, and extensible without making individual permissions responsible for every security decision\u003C/strong\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022next-steps\u0022\u003ENext Steps\u003C/h2\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/plugin-domains/authorization\u0022\u003EAuthorization\u003C/a\u003E \u2014 Actions, permissions, roles, and scope semantics.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/plugin-domains/users\u0022\u003EUsers\u003C/a\u003E \u2014 Account lifecycle and identity state.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/plugin-domains/credentials\u0022\u003ECredentials\u003C/a\u003E \u2014 Credential ownership and security operations.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/fundamentals/request-lifecycle\u0022\u003ERequest Lifecycle\u003C/a\u003E \u2014 Authorization within protected requests.\u003C/li\u003E\n\u003C/ul\u003E\n", "Headings": [ { "Id": "the-access-decision-model", @@ -18,6 +18,36 @@ "Text": "Actor and Target Are Different Concepts", "Level": 1 }, + { + "Id": "multi-target-authorization", + "Text": "Multi-Target Authorization", + "Level": 1 + }, + { + "Id": "creating-a-multi-target-context", + "Text": "Creating a Multi-Target Context", + "Level": 1 + }, + { + "Id": "example-restrict-batch-access-to-group-members", + "Text": "Example: Restrict Batch Access to Group Members", + "Level": 1 + }, + { + "Id": "authorization-must-cover-the-complete-batch", + "Text": "Authorization Must Cover the Complete Batch", + "Level": 1 + }, + { + "Id": "permission-and-scope-are-separate-requirements", + "Text": "Permission and Scope Are Separate Requirements", + "Level": 1 + }, + { + "Id": "trusted-target-collections", + "Text": "Trusted Target Collections", + "Level": 1 + }, { "Id": "trusted-context-construction", "Text": "Trusted Context Construction", @@ -129,6 +159,6 @@ "Level": 0 } ], - "SourceHash": "AACFE2DFBDB6087048AEABB9281B9236E91588694EBA8DBD50A1F10B2FE52EBC", + "SourceHash": "05FA98F9ED56849BAB9A4450CA6B85F273091D8A4F1422F7238421375E5DCA62", "BuilderVersion": "1" } \ No newline at end of file diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/users.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/users.json index d46a6373..1454edff 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/users.json +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/users.json @@ -1,7 +1,7 @@ { "Slug": "plugin-domains/users", "Title": "Users", - "Html": "\n\u003Cp\u003EA user is more than a database record, an email address, or a profile.\u003C/p\u003E\n\u003Cp\u003EIn UltimateAuth, user identity is modeled through a separation of \u003Cstrong\u003Eauthentication identity, account lifecycle, identifiers, and application-specific user information\u003C/strong\u003E.\u003C/p\u003E\n\u003Cp\u003EThis distinction allows the authentication engine to operate independently of a particular user entity or persistence model, while the Users Plugin Domain provides the capabilities needed to manage identities throughout their lifecycle.\u003C/p\u003E\n\u003Cp\u003EThe Users domain builds on the authentication infrastructure provided by UltimateAuth Core and Server. It does not redefine how authentication works; it defines how user identities are represented, maintained, and integrated with that infrastructure.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022understanding-user-identity\u0022\u003EUnderstanding User Identity\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth distinguishes between three aspects of a user:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EComponent\u003C/th\u003E\n\u003Cth\u003EResponsibility\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003ELifecycle\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EAccount existence, status, and security-relevant state\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EIdentifiers\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EValues through which a user can be identified or discovered\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EProfile\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EApplication-specific information associated with the user\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThese components represent different responsibilities and should not be treated as interchangeable.\u003C/p\u003E\n\u003Cp\u003EFor example, changing a display name should not affect authentication state. Verifying an email address should not implicitly change account status. Suspending an account should not require deleting its identifiers or profile.\u003C/p\u003E\n\u003Cp\u003ESeparating these concerns makes identity operations more predictable and allows security-sensitive changes to be handled explicitly.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022user-identity-in-core-and-server\u0022\u003EUser Identity in Core and Server\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth does not require applications to adopt the Users Plugin Domain simply to authenticate users.\u003C/p\u003E\n\u003Cp\u003ECore and Server provide the foundational authentication infrastructure, including the contracts and runtime mechanisms needed to associate authentication operations with user identities.\u003C/p\u003E\n\u003Cp\u003EAt this level, a user is primarily an \u003Cstrong\u003Eauthentication subject\u003C/strong\u003E.\u003C/p\u003E\n\u003Cp\u003EThe authentication system needs to establish which identity is involved in an operation, evaluate the relevant security conditions, and maintain the appropriate authentication state.\u003C/p\u003E\n\u003Cp\u003EIt does not inherently need to know the user\u0027s display name, profile fields, or how the application\u0027s user records are stored.\u003C/p\u003E\n\u003Cp\u003EThis separation is intentional:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Cstrong\u003ECore\u003C/strong\u003E defines foundational authentication concepts, contracts, and security semantics.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EServer\u003C/strong\u003E executes authentication flows and enforces the relevant runtime security boundaries.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EUsers\u003C/strong\u003E provides higher-level identity management capabilities through a dedicated Plugin Domain.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EAn application with an existing identity system can integrate that system with UltimateAuth\u0027s authentication infrastructure without necessarily adopting the reference user model.\u003C/p\u003E\n\u003Cp\u003EThe authentication identity is therefore a framework-level concept, while the complete user management model belongs to the application or the Users domain.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022what-the-users-domain-adds\u0022\u003EWhat the Users Domain Adds\u003C/h2\u003E\n\u003Cp\u003EThe Users Plugin Domain provides a structured approach to managing user identities beyond the minimum requirements of authentication.\u003C/p\u003E\n\u003Cp\u003EIts responsibilities include:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003ECreating and managing user accounts.\u003C/li\u003E\n\u003Cli\u003EMaintaining account lifecycle and status.\u003C/li\u003E\n\u003Cli\u003ERegistering and managing user identifiers.\u003C/li\u003E\n\u003Cli\u003EEnforcing identifier ownership and verification rules.\u003C/li\u003E\n\u003Cli\u003EApplying user management validation and access boundaries.\u003C/li\u003E\n\u003Cli\u003ECoordinating security-relevant changes with authentication infrastructure.\u003C/li\u003E\n\u003Cli\u003EExposing user management operations through application services and shared contracts.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThese capabilities are distinct from credential management.\u003C/p\u003E\n\u003Cp\u003EThe Users domain determines \u003Cstrong\u003Ewhich identity an identifier belongs to\u003C/strong\u003E and maintains the relevant user state. The Credentials domain manages the authentication credentials associated with identities.\u003C/p\u003E\n\u003Cp\u003EFor example, an email address is a user identifier; a password is a credential. They may participate in the same login flow, but they have different ownership and lifecycle rules.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022account-lifecycle\u0022\u003EAccount Lifecycle\u003C/h2\u003E\n\u003Cp\u003EThe lifecycle represents the existence and security-relevant state of a user account.\u003C/p\u003E\n\u003Cp\u003EIn the reference model, lifecycle information includes account creation, account status, and security state used to coordinate authentication-related changes.\u003C/p\u003E\n\u003Cp\u003EAccount status may represent conditions such as active, suspended, or deleted, according to the supported lifecycle model.\u003C/p\u003E\n\u003Cp\u003EThese states are not merely descriptive profile properties. They can influence whether an account is eligible to participate in authentication and other protected operations.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-state-and-session-invalidation\u0022\u003ESecurity State and Session Invalidation\u003C/h3\u003E\n\u003Cp\u003EUser lifecycle changes may require existing authentication state to be invalidated.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth separates the decision to change a user\u0027s security state from the mechanisms used to enforce that change.\u003C/p\u003E\n\u003Cp\u003EDepending on the configured authentication mode, enforcement may involve server-side session invalidation, token lifecycle rules, or other supported security mechanisms.\u003C/p\u003E\n\u003Cp\u003EThis distinction matters because not every authentication mode can invalidate an already-issued credential immediately.\u003C/p\u003E\n\u003Cp\u003EFor example, a locally validated JWT may remain valid until expiration unless the protected resource performs additional server-side validation.\u003C/p\u003E\n\u003Cp\u003EThe Users domain participates in security-sensitive account operations, while the authentication infrastructure determines how the resulting security state is enforced.\u003C/p\u003E\n\u003Cp\u003EFor more information, see \u003Ca href=\u0022/docs/fundamentals/auth-modes\u0022\u003EAuthentication Modes\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022user-identifiers\u0022\u003EUser Identifiers\u003C/h2\u003E\n\u003Cp\u003EAn identifier is a value associated with a user that allows the identity to be referenced or resolved.\u003C/p\u003E\n\u003Cp\u003ECommon examples include:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EEmail addresses.\u003C/li\u003E\n\u003Cli\u003EUsernames.\u003C/li\u003E\n\u003Cli\u003EPhone numbers.\u003C/li\u003E\n\u003Cli\u003EApplication-defined identifier types.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EUltimateAuth treats identifiers as independent identity records rather than assuming that every user has exactly one email address or username.\u003C/p\u003E\n\u003Cp\u003EThis supports applications with different identity requirements without changing the underlying authentication model.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022identifier-properties\u0022\u003EIdentifier Properties\u003C/h3\u003E\n\u003Cp\u003EThe reference identifier model distinguishes several important properties:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EPurpose\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EType\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifies the kind of identifier, such as email or username\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EValue\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EThe identifier value associated with the user\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003ENormalized value\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EA representation used for consistent lookup and comparison\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EPrimary state\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EIndicates the identifier\u0027s designated primary role\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EVerification state\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EIndicates whether ownership has been verified\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThese properties address separate questions.\u003C/p\u003E\n\u003Cp\u003EAn identifier may exist without being verified. A verified identifier does not automatically become primary. A primary identifier does not necessarily satisfy every authentication policy.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022login-identifier-resolution\u0022\u003ELogin Identifier Resolution\u003C/h3\u003E\n\u003Cp\u003EIdentifier resolution determines how an incoming login identifier is associated with a user identity.\u003C/p\u003E\n\u003Cp\u003EIn the reference approach, primary identifiers define the intended login surface, subject to the configured identifier resolution and verification policies.\u003C/p\u003E\n\u003Cp\u003EApplications can control which identifier types are accepted and how identifiers are resolved.\u003C/p\u003E\n\u003Cp\u003EThis allows authentication to support different identity strategies without embedding email-specific or username-specific assumptions into Core.\u003C/p\u003E\n\u003Cp\u003EThe distinction is important: \u003Cstrong\u003Ean identifier is identity data; whether it is acceptable for login is an authentication policy decision.\u003C/strong\u003E\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022identifier-ownership-and-verification\u0022\u003EIdentifier Ownership and Verification\u003C/h3\u003E\n\u003Cp\u003EIdentifier verification is a security operation, not a simple update to a Boolean property.\u003C/p\u003E\n\u003Cp\u003EMarking an identifier as verified must be based on an appropriate ownership proof or an explicitly authorized administrative operation.\u003C/p\u003E\n\u003Cp\u003EThe reference domain therefore distinguishes between:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003ERegistering an identifier.\u003C/li\u003E\n\u003Cli\u003EEstablishing ownership of an identifier.\u003C/li\u003E\n\u003Cli\u003EVerifying that ownership.\u003C/li\u003E\n\u003Cli\u003EChanging an identifier\u0027s role or status.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EAn anonymous or self-service request must not be able to claim verified ownership merely by supplying a verification flag.\u003C/p\u003E\n\u003Cp\u003ELikewise, operations targeting another user\u0027s identifiers must respect the applicable authorization and ownership boundaries.\u003C/p\u003E\n\u003Cp\u003EThis prevents identifier management from becoming an alternative route around authentication security.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022user-profiles\u0022\u003EUser Profiles\u003C/h2\u003E\n\u003Cp\u003EA profile contains information about the user that is not itself an authentication credential or an identity verification proof.\u003C/p\u003E\n\u003Cp\u003ETypical profile information may include:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EDisplay name and personal information.\u003C/li\u003E\n\u003Cli\u003ELocalization preferences.\u003C/li\u003E\n\u003Cli\u003EApplication metadata.\u003C/li\u003E\n\u003Cli\u003EOther domain-specific user attributes.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EUltimateAuth deliberately separates profile data from the security-critical identity model.\u003C/p\u003E\n\u003Cp\u003EApplications should be able to evolve their profile structure without redesigning authentication flows or changing how identifiers are verified.\u003C/p\u003E\n\u003Cp\u003EA profile attribute may still be relevant to application authorization or business rules, but it should not automatically be treated as proof of identity.\u003C/p\u003E\n\u003Cp\u003EThe reference model provides a starting point; applications remain responsible for deciding which additional user attributes belong in their own domain.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022reference-implementation\u0022\u003EReference Implementation\u003C/h2\u003E\n\u003Cp\u003EThe Users Plugin Domain includes a reference implementation that demonstrates and provides a practical user management model.\u003C/p\u003E\n\u003Cp\u003EIt brings together lifecycle, identifiers, and profile management through application services and supporting abstractions.\u003C/p\u003E\n\u003Cp\u003ERather than exposing persistence operations as the primary application interface, the reference implementation places user management rules within the application service layer.\u003C/p\u003E\n\u003Cp\u003EThis layer coordinates operations such as:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EUser creation and account management.\u003C/li\u003E\n\u003Cli\u003EIdentifier registration and updates.\u003C/li\u003E\n\u003Cli\u003EIdentifier verification and ownership validation.\u003C/li\u003E\n\u003Cli\u003EAccount status changes.\u003C/li\u003E\n\u003Cli\u003EIntegration and lifecycle notifications.\u003C/li\u003E\n\u003Cli\u003ESecurity-related coordination with the authentication infrastructure.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThis is an important architectural boundary.\u003C/p\u003E\n\u003Cp\u003EA request to verify an identifier, for example, is not equivalent to directly updating a database field. The application service is responsible for enforcing the rules that make the operation valid.\u003C/p\u003E\n\u003Cp\u003EThe reference implementation provides a recommended starting point for these behaviors without requiring every application to use the same persistence technology or user profile structure.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022domain-integration\u0022\u003EDomain Integration\u003C/h2\u003E\n\u003Cp\u003EUser management frequently affects other parts of an identity platform.\u003C/p\u003E\n\u003Cp\u003ECreating a user may be followed by credential enrollment. Changing account security state may require session invalidation. Deleting an account may require coordinated cleanup.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth supports these scenarios through explicit integration contracts rather than direct dependencies between Plugin Domain implementations.\u003C/p\u003E\n\u003Cp\u003EFor example, \u003Ccode\u003EIUserLifecycleIntegration\u003C/code\u003E provides an integration boundary for user lifecycle operations.\u003C/p\u003E\n\u003Cp\u003EThis allows other capabilities to participate in user-related workflows while keeping the Users domain responsible for its own state and rules.\u003C/p\u003E\n\u003Cp\u003EThe distinction between domain ownership and cross-domain coordination is central to UltimateAuth\u0027s modular architecture.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022customizing-user-management\u0022\u003ECustomizing User Management\u003C/h2\u003E\n\u003Cp\u003EApplications are not required to accept every detail of the reference implementation.\u003C/p\u003E\n\u003Cp\u003EDepending on their requirements and the available extension contracts, they may:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EProvide custom identifier resolution.\u003C/li\u003E\n\u003Cli\u003EConfigure supported identifier and verification policies.\u003C/li\u003E\n\u003Cli\u003EReplace storage implementations.\u003C/li\u003E\n\u003Cli\u003EIntegrate existing user repositories.\u003C/li\u003E\n\u003Cli\u003EExtend supported lifecycle and application service integrations.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EFor most applications, the recommended approach is to begin with the reference implementation and customize specific behavior rather than replacing the entire domain.\u003C/p\u003E\n\u003Cp\u003EMore advanced scenarios, including custom domain implementations and integration with existing identity systems, will be covered separately in the customization documentation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022choosing-your-user-model\u0022\u003EChoosing Your User Model\u003C/h2\u003E\n\u003Cp\u003EThere are two broad integration approaches.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EUse the reference Users domain\u003C/strong\u003E when the application needs a ready-made foundation for account lifecycle, identifiers, verification, and user management.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EIntegrate an existing identity model\u003C/strong\u003E when the application already owns these responsibilities and needs UltimateAuth primarily for its authentication infrastructure.\u003C/p\u003E\n\u003Cp\u003ENeither approach changes the fundamental security requirements.\u003C/p\u003E\n\u003Cp\u003EIdentity resolution must be trustworthy, identifier ownership must be protected, and authentication state must be managed according to the configured security model.\u003C/p\u003E\n\u003Cp\u003EThe difference is which part of the application owns the user management implementation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022next-steps\u0022\u003ENext Steps\u003C/h2\u003E\n\u003Cp\u003EThe Users domain establishes identity ownership and account lifecycle, but it does not independently provide every capability of an identity platform.\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/plugin-domains/credentials\u0022\u003ECredentials\u003C/a\u003E \u2014 Learn how authentication credentials are managed independently of user identifiers.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/plugin-domains/authorization\u0022\u003EAuthorization\u003C/a\u003E \u2014 Explore how identities are associated with permissions and access policies.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/plugin-domains\u0022\u003EPlugin Domains\u003C/a\u003E \u2014 Return to the overall Plugin Domain architecture.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EFor the distinction between identity and authentication infrastructure, see \u003Ca href=\u0022/docs/fundamentals/auth-model\u0022\u003EAuthentication Model\u003C/a\u003E.\u003C/p\u003E\n", + "Html": "\n\u003Cp\u003EA user is more than a database record, an email address, or a profile.\u003C/p\u003E\n\u003Cp\u003EIn UltimateAuth, user identity is modeled through a separation of \u003Cstrong\u003Eauthentication identity, account lifecycle, identifiers, and application-specific user information\u003C/strong\u003E.\u003C/p\u003E\n\u003Cp\u003EThis distinction allows the authentication engine to operate independently of a particular user entity or persistence model, while the Users Plugin Domain provides the capabilities needed to manage identities throughout their lifecycle.\u003C/p\u003E\n\u003Cp\u003EThe Users domain builds on the authentication infrastructure provided by UltimateAuth Core and Server. It does not redefine how authentication works; it defines how user identities are represented, maintained, and integrated with that infrastructure.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022understanding-user-identity\u0022\u003EUnderstanding User Identity\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth distinguishes between three aspects of a user:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EComponent\u003C/th\u003E\n\u003Cth\u003EResponsibility\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003ELifecycle\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EAccount existence, status, and security-relevant state\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EIdentifiers\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EValues through which a user can be identified or discovered\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EProfile\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EApplication-specific information associated with the user\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThese components represent different responsibilities and should not be treated as interchangeable.\u003C/p\u003E\n\u003Cp\u003EFor example, changing a display name should not affect authentication state. Verifying an email address should not implicitly change account status. Suspending an account should not require deleting its identifiers or profile.\u003C/p\u003E\n\u003Cp\u003ESeparating these concerns makes identity operations more predictable and allows security-sensitive changes to be handled explicitly.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022user-identity-in-core-and-server\u0022\u003EUser Identity in Core and Server\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth does not require applications to adopt the Users Plugin Domain simply to authenticate users.\u003C/p\u003E\n\u003Cp\u003ECore and Server provide the foundational authentication infrastructure, including the contracts and runtime mechanisms needed to associate authentication operations with user identities.\u003C/p\u003E\n\u003Cp\u003EAt this level, a user is primarily an \u003Cstrong\u003Eauthentication subject\u003C/strong\u003E.\u003C/p\u003E\n\u003Cp\u003EThe authentication system needs to establish which identity is involved in an operation, evaluate the relevant security conditions, and maintain the appropriate authentication state.\u003C/p\u003E\n\u003Cp\u003EIt does not inherently need to know the user\u0027s display name, profile fields, or how the application\u0027s user records are stored.\u003C/p\u003E\n\u003Cp\u003EThis separation is intentional:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Cstrong\u003ECore\u003C/strong\u003E defines foundational authentication concepts, contracts, and security semantics.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EServer\u003C/strong\u003E executes authentication flows and enforces the relevant runtime security boundaries.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EUsers\u003C/strong\u003E provides higher-level identity management capabilities through a dedicated Plugin Domain.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EAn application with an existing identity system can integrate that system with UltimateAuth\u0027s authentication infrastructure without necessarily adopting the reference user model.\u003C/p\u003E\n\u003Cp\u003EThe authentication identity is therefore a framework-level concept, while the complete user management model belongs to the application or the Users domain.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022what-the-users-domain-adds\u0022\u003EWhat the Users Domain Adds\u003C/h2\u003E\n\u003Cp\u003EThe Users Plugin Domain provides a structured approach to managing user identities beyond the minimum requirements of authentication.\u003C/p\u003E\n\u003Cp\u003EIts responsibilities include:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003ECreating and managing user accounts.\u003C/li\u003E\n\u003Cli\u003EMaintaining account lifecycle and status.\u003C/li\u003E\n\u003Cli\u003ERegistering and managing user identifiers.\u003C/li\u003E\n\u003Cli\u003EEnforcing identifier ownership and verification rules.\u003C/li\u003E\n\u003Cli\u003EApplying user management validation and access boundaries.\u003C/li\u003E\n\u003Cli\u003ECoordinating security-relevant changes with authentication infrastructure.\u003C/li\u003E\n\u003Cli\u003EExposing user management operations through application services and shared contracts.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThese capabilities are distinct from credential management.\u003C/p\u003E\n\u003Cp\u003EThe Users domain determines \u003Cstrong\u003Ewhich identity an identifier belongs to\u003C/strong\u003E and maintains the relevant user state. The Credentials domain manages the authentication credentials associated with identities.\u003C/p\u003E\n\u003Cp\u003EFor example, an email address is a user identifier; a password is a credential. They may participate in the same login flow, but they have different ownership and lifecycle rules.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022account-lifecycle\u0022\u003EAccount Lifecycle\u003C/h2\u003E\n\u003Cp\u003EThe lifecycle represents the existence and security-relevant state of a user account.\u003C/p\u003E\n\u003Cp\u003EIn the reference model, lifecycle information includes account creation, account status, and security state used to coordinate authentication-related changes.\u003C/p\u003E\n\u003Cp\u003EAccount status may represent conditions such as active, suspended, or deleted, according to the supported lifecycle model.\u003C/p\u003E\n\u003Cp\u003EThese states are not merely descriptive profile properties. They can influence whether an account is eligible to participate in authentication and other protected operations.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-state-and-session-invalidation\u0022\u003ESecurity State and Session Invalidation\u003C/h3\u003E\n\u003Cp\u003EUser lifecycle changes may require existing authentication state to be invalidated.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth separates the decision to change a user\u0027s security state from the mechanisms used to enforce that change.\u003C/p\u003E\n\u003Cp\u003EDepending on the configured authentication mode, enforcement may involve server-side session invalidation, token lifecycle rules, or other supported security mechanisms.\u003C/p\u003E\n\u003Cp\u003EThis distinction matters because not every authentication mode can invalidate an already-issued credential immediately.\u003C/p\u003E\n\u003Cp\u003EFor example, a locally validated JWT may remain valid until expiration unless the protected resource performs additional server-side validation.\u003C/p\u003E\n\u003Cp\u003EThe Users domain participates in security-sensitive account operations, while the authentication infrastructure determines how the resulting security state is enforced.\u003C/p\u003E\n\u003Cp\u003EFor more information, see \u003Ca href=\u0022/docs/fundamentals/auth-modes\u0022\u003EAuthentication Modes\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022user-identifiers\u0022\u003EUser Identifiers\u003C/h2\u003E\n\u003Cp\u003EAn identifier is a value associated with a user that allows the identity to be referenced or resolved.\u003C/p\u003E\n\u003Cp\u003ECommon examples include:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EEmail addresses.\u003C/li\u003E\n\u003Cli\u003EUsernames.\u003C/li\u003E\n\u003Cli\u003EPhone numbers.\u003C/li\u003E\n\u003Cli\u003EApplication-defined identifier types.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EUltimateAuth treats identifiers as independent identity records rather than assuming that every user has exactly one email address or username.\u003C/p\u003E\n\u003Cp\u003EThis supports applications with different identity requirements without changing the underlying authentication model.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022identifier-properties\u0022\u003EIdentifier Properties\u003C/h3\u003E\n\u003Cp\u003EThe reference identifier model distinguishes several important properties:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EPurpose\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EType\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifies the kind of identifier, such as email or username\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EValue\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EThe identifier value associated with the user\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003ENormalized value\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EA representation used for consistent lookup and comparison\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EPrimary state\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EIndicates the identifier\u0027s designated primary role\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EVerification state\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EIndicates whether ownership has been verified\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThese properties address separate questions.\u003C/p\u003E\n\u003Cp\u003EAn identifier may exist without being verified. A verified identifier does not automatically become primary. A primary identifier does not necessarily satisfy every authentication policy.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022login-identifier-resolution\u0022\u003ELogin Identifier Resolution\u003C/h3\u003E\n\u003Cp\u003EIdentifier resolution determines how an incoming login identifier is associated with a user identity.\u003C/p\u003E\n\u003Cp\u003EIn the reference approach, primary identifiers define the intended login surface, subject to the configured identifier resolution and verification policies.\u003C/p\u003E\n\u003Cp\u003EApplications can control which identifier types are accepted and how identifiers are resolved.\u003C/p\u003E\n\u003Cp\u003EThis allows authentication to support different identity strategies without embedding email-specific or username-specific assumptions into Core.\u003C/p\u003E\n\u003Cp\u003EThe distinction is important: \u003Cstrong\u003Ean identifier is identity data; whether it is acceptable for login is an authentication policy decision.\u003C/strong\u003E\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022identifier-ownership-and-verification\u0022\u003EIdentifier Ownership and Verification\u003C/h3\u003E\n\u003Cp\u003EIdentifier verification is a security operation, not a simple update to a Boolean property.\u003C/p\u003E\n\u003Cp\u003EMarking an identifier as verified must be based on an appropriate ownership proof or an explicitly authorized administrative operation.\u003C/p\u003E\n\u003Cp\u003EThe reference domain therefore distinguishes between:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003ERegistering an identifier.\u003C/li\u003E\n\u003Cli\u003EEstablishing ownership of an identifier.\u003C/li\u003E\n\u003Cli\u003EVerifying that ownership.\u003C/li\u003E\n\u003Cli\u003EChanging an identifier\u0027s role or status.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EAn anonymous or self-service request must not be able to claim verified ownership merely by supplying a verification flag.\u003C/p\u003E\n\u003Cp\u003ELikewise, operations targeting another user\u0027s identifiers must respect the applicable authorization and ownership boundaries.\u003C/p\u003E\n\u003Cp\u003EThis prevents identifier management from becoming an alternative route around authentication security.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022user-profiles\u0022\u003EUser Profiles\u003C/h2\u003E\n\u003Cp\u003EA profile contains information about the user that is not itself an authentication credential or an identity verification proof.\u003C/p\u003E\n\u003Cp\u003ETypical profile information may include:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EDisplay name and personal information.\u003C/li\u003E\n\u003Cli\u003ELocalization preferences.\u003C/li\u003E\n\u003Cli\u003EApplication metadata.\u003C/li\u003E\n\u003Cli\u003EOther domain-specific user attributes.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EUltimateAuth deliberately separates profile data from the security-critical identity model.\u003C/p\u003E\n\u003Cp\u003EApplications should be able to evolve their profile structure without redesigning authentication flows or changing how identifiers are verified.\u003C/p\u003E\n\u003Cp\u003EA profile attribute may still be relevant to application authorization or business rules, but it should not automatically be treated as proof of identity.\u003C/p\u003E\n\u003Cp\u003EThe reference model provides a starting point; applications remain responsible for deciding which additional user attributes belong in their own domain.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022batch-user-summary-retrieval\u0022\u003EBatch User Summary Retrieval\u003C/h2\u003E\n\u003Cp\u003EApplications frequently maintain relationships between users without storing complete user profile information alongside those relationships.\u003C/p\u003E\n\u003Cp\u003EFor example, a project management application may store a collection of user keys representing project members.\u003C/p\u003E\n\u003Cp\u003EWhen displaying those members, the application needs their current names, usernames, and other summary information.\u003C/p\u003E\n\u003Cp\u003ERetrieving each user independently can introduce unnecessary requests and repeated persistence operations.\u003C/p\u003E\n\u003Cp\u003EThe Users domain addresses this through batch user summary retrieval.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022iusersummaryquerystore\u0022\u003EIUserSummaryQueryStore\u003C/h3\u003E\n\u003Cp\u003EThe \u003Ccode\u003EIUserSummaryQueryStore\u003C/code\u003E contract supports retrieving user summaries by their keys:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003ETask\u0026lt;IReadOnlyList\u0026lt;UserSummary\u0026gt;\u0026gt; GetByKeysAsync(IReadOnlyCollection\u0026lt;UserKey\u0026gt; userKeys, CancellationToken ct = default);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe operation retrieves summaries for a known collection of users within the store\u0027s tenant context.\u003C/p\u003E\n\u003Cp\u003EThe reference implementations include support for Entity Framework Core and InMemory persistence.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022application-service-and-authorization\u0022\u003EApplication Service and Authorization\u003C/h3\u003E\n\u003Cp\u003EBatch retrieval is exposed through the Users application service:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EGetUsersByKeysAsync(AccessContext context, IReadOnlyCollection\u0026lt;UserKey\u0026gt; userKeys, CancellationToken ct = default)\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe application service coordinates authorization and persistence access.\u003C/p\u003E\n\u003Cp\u003EThe operation is executed through the standard \u003Ccode\u003EAccessOrchestrator\u003C/code\u003E, ensuring that batch retrieval participates in UltimateAuth\u0027s existing authorization pipeline.\u003C/p\u003E\n\u003Cp\u003EThe public Client SDK exposes the functionality through:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthClient.Users.GetByKeysAsync(userKeys)\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022target-aware-access-control\u0022\u003ETarget-Aware Access Control\u003C/h3\u003E\n\u003Cp\u003EA batch request may target multiple users belonging to different groups, organizations, or application-defined security scopes.\u003C/p\u003E\n\u003Cp\u003EFor this reason, UltimateAuth supports multiple target identities in \u003Ccode\u003EAccessContext\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EA policy can inspect the complete target collection before allowing the operation.\u003C/p\u003E\n\u003Cp\u003EThis makes it possible to implement restrictions such as:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EAllowing access only to members of the actor\u0027s organization.\u003C/li\u003E\n\u003Cli\u003ERestricting visibility to users assigned to the same project.\u003C/li\u003E\n\u003Cli\u003ELimiting member directories to a particular group.\u003C/li\u003E\n\u003Cli\u003EDenying requests containing users outside an authorized scope.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThe Users domain does not define these application-specific membership rules.\u003C/p\u003E\n\u003Cp\u003EInstead, it provides the batch operation and authorization context needed for applications to enforce them.\u003C/p\u003E\n\u003Cp\u003ESee \u003Ca href=\u0022/docs/plugin-domains/policies\u0022\u003EPolicies \u0026amp; Access Control\u003C/a\u003E for details.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022choosing-the-appropriate-retrieval-method\u0022\u003EChoosing the Appropriate Retrieval Method\u003C/h3\u003E\n\u003Cp\u003EUse:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003EGetUserAsync\u003C/code\u003E for detailed information about one user.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EQueryAsync\u003C/code\u003E for searching, filtering, sorting, and paginating users.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EGetByKeysAsync\u003C/code\u003E for retrieving summaries of a known collection of users.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EBatch retrieval supports up to 250 distinct user keys per request.\u003C/p\u003E\n\u003Cp\u003EIt is intended for targeted retrieval, not as a replacement for paginated user discovery.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022reference-implementation\u0022\u003EReference Implementation\u003C/h2\u003E\n\u003Cp\u003EThe Users Plugin Domain includes a reference implementation that demonstrates and provides a practical user management model.\u003C/p\u003E\n\u003Cp\u003EIt brings together lifecycle, identifiers, and profile management through application services and supporting abstractions.\u003C/p\u003E\n\u003Cp\u003ERather than exposing persistence operations as the primary application interface, the reference implementation places user management rules within the application service layer.\u003C/p\u003E\n\u003Cp\u003EThis layer coordinates operations such as:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EUser creation and account management.\u003C/li\u003E\n\u003Cli\u003EIdentifier registration and updates.\u003C/li\u003E\n\u003Cli\u003EIdentifier verification and ownership validation.\u003C/li\u003E\n\u003Cli\u003EAccount status changes.\u003C/li\u003E\n\u003Cli\u003EIntegration and lifecycle notifications.\u003C/li\u003E\n\u003Cli\u003ESecurity-related coordination with the authentication infrastructure.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThis is an important architectural boundary.\u003C/p\u003E\n\u003Cp\u003EA request to verify an identifier, for example, is not equivalent to directly updating a database field. The application service is responsible for enforcing the rules that make the operation valid.\u003C/p\u003E\n\u003Cp\u003EThe reference implementation provides a recommended starting point for these behaviors without requiring every application to use the same persistence technology or user profile structure.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022domain-integration\u0022\u003EDomain Integration\u003C/h2\u003E\n\u003Cp\u003EUser management frequently affects other parts of an identity platform.\u003C/p\u003E\n\u003Cp\u003ECreating a user may be followed by credential enrollment. Changing account security state may require session invalidation. Deleting an account may require coordinated cleanup.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth supports these scenarios through explicit integration contracts rather than direct dependencies between Plugin Domain implementations.\u003C/p\u003E\n\u003Cp\u003EFor example, \u003Ccode\u003EIUserLifecycleIntegration\u003C/code\u003E provides an integration boundary for user lifecycle operations.\u003C/p\u003E\n\u003Cp\u003EThis allows other capabilities to participate in user-related workflows while keeping the Users domain responsible for its own state and rules.\u003C/p\u003E\n\u003Cp\u003EThe distinction between domain ownership and cross-domain coordination is central to UltimateAuth\u0027s modular architecture.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022customizing-user-management\u0022\u003ECustomizing User Management\u003C/h2\u003E\n\u003Cp\u003EApplications are not required to accept every detail of the reference implementation.\u003C/p\u003E\n\u003Cp\u003EDepending on their requirements and the available extension contracts, they may:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EProvide custom identifier resolution.\u003C/li\u003E\n\u003Cli\u003EConfigure supported identifier and verification policies.\u003C/li\u003E\n\u003Cli\u003EReplace storage implementations.\u003C/li\u003E\n\u003Cli\u003EIntegrate existing user repositories.\u003C/li\u003E\n\u003Cli\u003EExtend supported lifecycle and application service integrations.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EFor most applications, the recommended approach is to begin with the reference implementation and customize specific behavior rather than replacing the entire domain.\u003C/p\u003E\n\u003Cp\u003EMore advanced scenarios, including custom domain implementations and integration with existing identity systems, will be covered separately in the customization documentation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022choosing-your-user-model\u0022\u003EChoosing Your User Model\u003C/h2\u003E\n\u003Cp\u003EThere are two broad integration approaches.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EUse the reference Users domain\u003C/strong\u003E when the application needs a ready-made foundation for account lifecycle, identifiers, verification, and user management.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EIntegrate an existing identity model\u003C/strong\u003E when the application already owns these responsibilities and needs UltimateAuth primarily for its authentication infrastructure.\u003C/p\u003E\n\u003Cp\u003ENeither approach changes the fundamental security requirements.\u003C/p\u003E\n\u003Cp\u003EIdentity resolution must be trustworthy, identifier ownership must be protected, and authentication state must be managed according to the configured security model.\u003C/p\u003E\n\u003Cp\u003EThe difference is which part of the application owns the user management implementation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022next-steps\u0022\u003ENext Steps\u003C/h2\u003E\n\u003Cp\u003EThe Users domain establishes identity ownership and account lifecycle, but it does not independently provide every capability of an identity platform.\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/plugin-domains/credentials\u0022\u003ECredentials\u003C/a\u003E \u2014 Learn how authentication credentials are managed independently of user identifiers.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/plugin-domains/authorization\u0022\u003EAuthorization\u003C/a\u003E \u2014 Explore how identities are associated with permissions and access policies.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/plugin-domains\u0022\u003EPlugin Domains\u003C/a\u003E \u2014 Return to the overall Plugin Domain architecture.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EFor the distinction between identity and authentication infrastructure, see \u003Ca href=\u0022/docs/fundamentals/auth-model\u0022\u003EAuthentication Model\u003C/a\u003E.\u003C/p\u003E\n", "Headings": [ { "Id": "understanding-user-identity", @@ -53,6 +53,31 @@ "Text": "User Profiles", "Level": 0 }, + { + "Id": "batch-user-summary-retrieval", + "Text": "Batch User Summary Retrieval", + "Level": 0 + }, + { + "Id": "iusersummaryquerystore", + "Text": "IUserSummaryQueryStore", + "Level": 1 + }, + { + "Id": "application-service-and-authorization", + "Text": "Application Service and Authorization", + "Level": 1 + }, + { + "Id": "target-aware-access-control", + "Text": "Target-Aware Access Control", + "Level": 1 + }, + { + "Id": "choosing-the-appropriate-retrieval-method", + "Text": "Choosing the Appropriate Retrieval Method", + "Level": 1 + }, { "Id": "reference-implementation", "Text": "Reference Implementation", @@ -79,6 +104,6 @@ "Level": 0 } ], - "SourceHash": "13C75CB97B677253340246EBBE289AA49D6D519BFDB598CA00CE46E7A19126F9", + "SourceHash": "E59EBD0AC1985AD4C0CDAB7844881C58E3DA3EE294DE089B2AE1B7C73A72A0FB", "BuilderVersion": "1" } \ No newline at end of file diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/security/authorization-pipeline.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/security/authorization-pipeline.json index f09f6f13..1ebc18d8 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/security/authorization-pipeline.json +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/security/authorization-pipeline.json @@ -1,7 +1,7 @@ { "Slug": "security/authorization-pipeline", "Title": "Authorization Pipeline Internals", - "Html": "\n\u003Cp\u003EUltimateAuth authorization is implemented as a coordinated evaluation process rather than a collection of unrelated permission checks.\u003C/p\u003E\n\u003Cp\u003EThe authorization pipeline brings together the operation being requested, the identities involved, the applicable permissions, and the policies responsible for enforcing security requirements.\u003C/p\u003E\n\u003Cp\u003EAt its center are several distinct responsibilities:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Cstrong\u003EContext preparation:\u003C/strong\u003E Establish the security information required to evaluate an operation.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EPermission resolution:\u003C/strong\u003E Make effective authorization grants available to the decision process.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EPolicy resolution:\u003C/strong\u003E Determine which security rules apply to the requested action.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EPolicy evaluation:\u003C/strong\u003E Evaluate the applicable requirements in a defined order.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EDecision enforcement:\u003C/strong\u003E Permit the protected operation to proceed only when the authorization result allows it.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThis page examines how those responsibilities fit together.\u003C/p\u003E\n\u003Cp\u003EFor an introduction to actions, permissions, and roles, see \u003Ca href=\u0022/docs/plugin-domains/authorization\u0022\u003EAuthorization\u003C/a\u003E. For policy concepts and their intended use, see \u003Ca href=\u0022/docs/plugin-domains/policies\u0022\u003EPolicies \u0026amp; Access Control\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022pipeline-architecture\u0022\u003EPipeline Architecture\u003C/h2\u003E\n\u003Cp\u003EThe authorization process can be represented as a sequence of stages:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EProtected Operation\n |\n v\n AccessContext\n |\n v\nContext Enrichment\n |\n v\nPermission Resolution\n |\n v\nInvariant Evaluation\n |\n v\nGlobal Policy Evaluation\n |\n v\nRuntime Policy Resolution\n |\n v\nRuntime Policy Evaluation\n |\n v\n Final Decision\n |\n \u002B---- Deny ----------------\u0026gt; Reject\n |\n \u002B---- Reauthentication ---\u0026gt; Require Reauthentication\n |\n \u002B---- Allow ---------------\u0026gt; Execute\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis diagram describes the logical responsibilities of the pipeline.\u003C/p\u003E\n\u003Cp\u003EIt should not be interpreted as a guarantee that every stage performs a separate database query or that every authentication mode uses the same mechanism to obtain authorization information.\u003C/p\u003E\n\u003Cp\u003EThe important property is that \u003Cstrong\u003Eauthorization is evaluated against a coherent access context before the protected operation is allowed to proceed\u003C/strong\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022accesscontext-as-the-evaluation-boundary\u0022\u003EAccessContext as the Evaluation Boundary\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EAccessContext\u003C/code\u003E carries the information required to evaluate an authorization request.\u003C/p\u003E\n\u003Cp\u003EIt connects the authenticated actor, the intended target, the requested action, and the relevant security environment.\u003C/p\u003E\n\u003Cp\u003EConceptually, its responsibilities include:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EInformation\u003C/th\u003E\n\u003Cth\u003EPurpose\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EActor\u003C/td\u003E\n\u003Ctd\u003EIdentifies the principal performing the operation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ETarget\u003C/td\u003E\n\u003Ctd\u003EIdentifies the account or resource affected\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAction\u003C/td\u003E\n\u003Ctd\u003EDescribes the protected operation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ETenant context\u003C/td\u003E\n\u003Ctd\u003EEstablishes the relevant isolation boundaries\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EClaims\u003C/td\u003E\n\u003Ctd\u003EProvides authentication and authorization information\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EPermissions\u003C/td\u003E\n\u003Ctd\u003ERepresents the effective permission information available for evaluation\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe context is shared across the authorization process so that individual policies do not need to independently reconstruct the operation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022actor-and-target-separation\u0022\u003EActor and Target Separation\u003C/h3\u003E\n\u003Cp\u003EConsider the action:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003Eusers.delete.admin\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe actor is the principal requesting the deletion.\u003C/p\u003E\n\u003Cp\u003EThe target is the user account being deleted.\u003C/p\u003E\n\u003Cp\u003EThese identities may be different, and the authorization pipeline must preserve that distinction.\u003C/p\u003E\n\u003Cp\u003EFor a self-service action:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003Eusers.delete.self\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe action indicates a self-service operation, but the action name alone does not prove that actor and target are the same identity.\u003C/p\u003E\n\u003Cp\u003EThat relationship must be established from trusted context and evaluated by the applicable security rules.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022context-integrity\u0022\u003EContext Integrity\u003C/h3\u003E\n\u003Cp\u003EThe reliability of the authorization decision depends on the integrity of the context.\u003C/p\u003E\n\u003Cp\u003EA target identifier or tenant identifier supplied by a client must not automatically be accepted as a trusted authorization fact.\u003C/p\u003E\n\u003Cp\u003EThe pipeline must operate on the authenticated actor and appropriately resolved target information.\u003C/p\u003E\n\u003Cp\u003EA correctly implemented policy cannot compensate for a context that incorrectly identifies the actor or misrepresents the target.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022context-enrichment\u0022\u003EContext Enrichment\u003C/h2\u003E\n\u003Cp\u003EThe initial authorization context may not contain all the information required by the applicable policies.\u003C/p\u003E\n\u003Cp\u003EContext enrichment makes additional authorization information available before evaluation.\u003C/p\u003E\n\u003Cp\u003EThis can include resolving effective permissions and preparing them for efficient policy checks.\u003C/p\u003E\n\u003Cp\u003EConceptually:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EAuthenticated Actor\n |\n v\nResolve Authorization Information\n |\n v\nEffective Permissions\n |\n v\nCompiledPermissionSet\n |\n v\nEnriched AccessContext\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe purpose of enrichment is to separate authorization data resolution from the individual rules that consume that data.\u003C/p\u003E\n\u003Cp\u003EA permission policy should not need to independently load role assignments and rebuild the user\u0027s effective permission set every time it evaluates an action.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022authorization-data-freshness\u0022\u003EAuthorization Data Freshness\u003C/h3\u003E\n\u003Cp\u003EEnrichment does not automatically guarantee that all authorization information is current.\u003C/p\u003E\n\u003Cp\u003EThe source of permission data matters.\u003C/p\u003E\n\u003Cp\u003EFor example, permissions obtained from current server-side state may reflect a recent role change, while permissions derived exclusively from an already-issued JWT may reflect an earlier state.\u003C/p\u003E\n\u003Cp\u003EThe pipeline can evaluate the information it receives consistently, but it cannot make stale claims current without consulting an authoritative source.\u003C/p\u003E\n\u003Cp\u003EThis distinction is particularly relevant to SemiHybrid and PureJwt authentication.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022permission-compilation\u0022\u003EPermission Compilation\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth uses \u003Ccode\u003ECompiledPermissionSet\u003C/code\u003E to represent permissions in a form suitable for repeated authorization checks.\u003C/p\u003E\n\u003Cp\u003EPermission compilation separates the representation of stored grants from the process of deciding whether an action is permitted.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003ERequested action:\n users.delete.admin\n\nEffective grants:\n users.read.self\n users.delete.admin\n credentials.list.self\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EA permission policy can evaluate whether the effective grants cover the requested action.\u003C/p\u003E\n\u003Cp\u003EConceptually:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EpermissionSet.IsAllowed(action);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis illustrates the permission-checking responsibility, rather than a complete public API integration example.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022wildcard-permissions\u0022\u003EWildcard Permissions\u003C/h3\u003E\n\u003Cp\u003ECompiled permissions may also represent wildcard grants.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003Eusers.*\nusers.profile.*\n*\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWildcard matching must respect the permission model\u0027s defined action hierarchy.\u003C/p\u003E\n\u003Cp\u003EA broad grant should not be inferred merely because several currently visible permissions share a prefix.\u003C/p\u003E\n\u003Cp\u003EWhere permission normalization replaces a complete action group with a wildcard, the normalization must preserve the intended permission semantics.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022compilation-is-not-authorization\u0022\u003ECompilation Is Not Authorization\u003C/h3\u003E\n\u003Cp\u003EA compiled permission set is an efficient representation of grants.\u003C/p\u003E\n\u003Cp\u003EIt does not independently establish that the actor may perform the operation.\u003C/p\u003E\n\u003Cp\u003EA matching permission can still be insufficient when:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EThe actor is not permitted to access the target.\u003C/li\u003E\n\u003Cli\u003EThe operation crosses a prohibited tenant boundary.\u003C/li\u003E\n\u003Cli\u003EThe account does not satisfy required security conditions.\u003C/li\u003E\n\u003Cli\u003EAn operation-specific policy denies the action.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EPermission matching is one requirement within the broader authorization decision.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022policy-registration-and-resolution\u0022\u003EPolicy Registration and Resolution\u003C/h2\u003E\n\u003Cp\u003ERuntime policies must be discoverable for the operations they protect.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth\u0027s authorization architecture includes policy registration and compilation concepts represented by:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003EAccessPolicyRegistry\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003ECompiledAccessPolicySet\u003C/code\u003E\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThese components separate policy configuration from runtime selection.\u003C/p\u003E\n\u003Cp\u003EConceptually:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003ERegistered Policies\n |\n v\nAccessPolicyRegistry\n |\n v\nCompiledAccessPolicySet\n |\n v\nPolicies Matching Requested Action\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022action-based-selection\u0022\u003EAction-Based Selection\u003C/h3\u003E\n\u003Cp\u003EPolicies may be associated with action prefixes or patterns.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003Eusers.*\nusers.delete.*\nusers.delete.admin\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EA request for:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003Eusers.delete.admin\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Emay select policies associated with the relevant action hierarchy.\u003C/p\u003E\n\u003Cp\u003EThis allows broad policies to cover a domain while more specific policies enforce requirements for individual operations.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022prefix-matching-and-boundaries\u0022\u003EPrefix Matching and Boundaries\u003C/h3\u003E\n\u003Cp\u003EAction matching must respect the intended structure of action identifiers.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003Eusers.delete.admin\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Ebelongs to the \u003Ccode\u003Eusers\u003C/code\u003E action domain.\u003C/p\u003E\n\u003Cp\u003EBut an unrelated action whose text merely begins with similar characters should not automatically be treated as belonging to that domain.\u003C/p\u003E\n\u003Cp\u003EPolicy matching must therefore follow the actual action-pattern semantics implemented by the registry.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022resolution-versus-evaluation\u0022\u003EResolution Versus Evaluation\u003C/h3\u003E\n\u003Cp\u003EPolicy resolution answers:\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EWhich policies are candidates for this operation?\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EPolicy evaluation answers:\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EDo those policies permit this operation in the current context?\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EA policy may match an action pattern but still be inapplicable to a particular runtime context.\u003C/p\u003E\n\u003Cp\u003EThat distinction is handled through policy applicability rules.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022runtime-applicability\u0022\u003ERuntime Applicability\u003C/h2\u003E\n\u003Cp\u003EA policy can determine whether it applies to the current authorization context.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth represents this responsibility through \u003Ccode\u003EAppliesTo\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EFor example, a policy restricting administrative self-modification may only be relevant when:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EThe requested action has administrative scope.\u003C/li\u003E\n\u003Cli\u003EThe operation changes protected account state.\u003C/li\u003E\n\u003Cli\u003EThe actor and target are the same identity.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EIt may not apply to administrative read operations or unrelated domains.\u003C/p\u003E\n\u003Cp\u003EConceptually:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EPolicy selected by action\n |\n v\nAppliesTo(context)\n |\n \u002B---- No ----\u0026gt; Skip policy\n |\n \u002B---- Yes ---\u0026gt; Evaluate policy\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis enables policies to remain focused on specific security requirements without requiring every action to have an entirely separate policy implementation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022applicability-is-not-a-security-bypass\u0022\u003EApplicability Is Not a Security Bypass\u003C/h3\u003E\n\u003Cp\u003EAn applicability rule determines when a policy participates in evaluation.\u003C/p\u003E\n\u003Cp\u003EIf a mandatory security requirement can be skipped because its applicability condition is too broad or incorrectly implemented, the resulting authorization boundary may be weaker than intended.\u003C/p\u003E\n\u003Cp\u003EPolicies enforcing fundamental security invariants should not depend on application-controlled applicability rules that can bypass those invariants.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022policy-evaluation-stages\u0022\u003EPolicy Evaluation Stages\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth\u0027s policy architecture distinguishes between invariants, global policies, and runtime policies.\u003C/p\u003E\n\u003Cp\u003EThese categories establish the order and scope of security evaluation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022stage-1-invariants\u0022\u003EStage 1: Invariants\u003C/h3\u003E\n\u003Cp\u003EInvariants represent mandatory security requirements.\u003C/p\u003E\n\u003Cp\u003EThey are evaluated before ordinary policy stages.\u003C/p\u003E\n\u003Cp\u003EAn invariant denial prevents the requested operation from proceeding, regardless of whether another policy would otherwise permit it.\u003C/p\u003E\n\u003Cp\u003EInvariants provide the strongest enforcement boundary within the policy pipeline.\u003C/p\u003E\n\u003Cp\u003EThey should not be treated as ordinary permissions that can be overridden by a broader grant.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022stage-2-global-policies\u0022\u003EStage 2: Global Policies\u003C/h3\u003E\n\u003Cp\u003EGlobal policies enforce broadly applicable security requirements.\u003C/p\u003E\n\u003Cp\u003EThey may examine the operation\u0027s context to determine whether their rule is relevant.\u003C/p\u003E\n\u003Cp\u003EExamples include requirements related to authentication state, account eligibility, and tenant isolation.\u003C/p\u003E\n\u003Cp\u003EGlobal policies allow these requirements to be enforced consistently without attaching identical rules to every individual action.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022stage-3-runtime-policies\u0022\u003EStage 3: Runtime Policies\u003C/h3\u003E\n\u003Cp\u003ERuntime policies are resolved according to the requested action and evaluated against the current context.\u003C/p\u003E\n\u003Cp\u003EThey support requirements such as:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003ERequired permissions.\u003C/li\u003E\n\u003Cli\u003ESelf-access restrictions.\u003C/li\u003E\n\u003Cli\u003EAdministrative operation constraints.\u003C/li\u003E\n\u003Cli\u003EDomain-specific authorization rules.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003ERuntime policies extend the decision process without replacing the mandatory security requirements established by earlier stages.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022iaccessauthority-and-decision-evaluation\u0022\u003EIAccessAuthority and Decision Evaluation\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EIAccessAuthority\u003C/code\u003E represents the authorization decision responsibility within the pipeline.\u003C/p\u003E\n\u003Cp\u003EThe authority evaluates the applicable security requirements and produces an authorization result.\u003C/p\u003E\n\u003Cp\u003EConceptually, the result can represent:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EResult\u003C/th\u003E\n\u003Cth\u003EMeaning\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EAllow\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EThe applicable requirements permit the operation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EDeny\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EA required security condition rejects the operation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EReauthentication required\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EThe operation requires an additional authentication condition before proceeding\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThese outcomes must not be confused with the result of an individual policy.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022policy-allow-versus-final-allow\u0022\u003EPolicy Allow Versus Final Allow\u003C/h3\u003E\n\u003Cp\u003EAn individual policy may return an allow result because its own requirement has been satisfied.\u003C/p\u003E\n\u003Cp\u003EThat does not necessarily mean the entire operation is authorized.\u003C/p\u003E\n\u003Cp\u003EOther applicable policies may still deny the request or require reauthentication.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EAn individual policy\u0027s approval is not a universal authorization grant.\u003C/strong\u003E\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022deny-precedence\u0022\u003EDeny Precedence\u003C/h3\u003E\n\u003Cp\u003EA denial from an applicable mandatory policy prevents the operation from being authorized.\u003C/p\u003E\n\u003Cp\u003EThe pipeline must not allow a later permissive policy to override a previously established denial.\u003C/p\u003E\n\u003Cp\u003EThis supports predictable enforcement across multiple independent security rules.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022reauthentication-requirements\u0022\u003EReauthentication Requirements\u003C/h3\u003E\n\u003Cp\u003ESome operations may require stronger or more recent authentication.\u003C/p\u003E\n\u003Cp\u003EA policy can represent that requirement separately from an ordinary denial.\u003C/p\u003E\n\u003Cp\u003EThe authorization system must distinguish between:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EAn operation that is prohibited.\u003C/li\u003E\n\u003Cli\u003EAn operation that may proceed after additional authentication.\u003C/li\u003E\n\u003Cli\u003EAn operation that is currently authorized.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThe exact precedence and propagation of reauthentication results must follow the implemented decision engine.\u003C/p\u003E\n\u003Cp\u003EA reauthentication result should not be interpreted as permission to execute the protected operation immediately.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022permission-policies\u0022\u003EPermission Policies\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EMustHavePermissionPolicy\u003C/code\u003E represents the connection between the permission model and the policy pipeline.\u003C/p\u003E\n\u003Cp\u003EIts responsibility is to evaluate whether the actor has the permission required for the requested action.\u003C/p\u003E\n\u003Cp\u003EConceptually:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EAccessContext.Action\n |\n v\nCompiledPermissionSet\n |\n v\nPermission Evaluation\n |\n v\nPolicy Decision\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis design separates permission storage and resolution from permission enforcement.\u003C/p\u003E\n\u003Cp\u003ERoles and assignments determine effective grants.\u003C/p\u003E\n\u003Cp\u003EThe compiled permission set represents those grants.\u003C/p\u003E\n\u003Cp\u003EThe permission policy evaluates the requested action against them.\u003C/p\u003E\n\u003Cp\u003EThe final authorization decision also considers the other applicable security policies.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022permissions-are-not-a-replacement-for-policies\u0022\u003EPermissions Are Not a Replacement for Policies\u003C/h3\u003E\n\u003Cp\u003EFor example, an actor may possess:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003Eusers.delete.admin\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThat permission does not automatically authorize deleting any user in any tenant.\u003C/p\u003E\n\u003Cp\u003EThe operation must still satisfy the relevant target, tenant, and account security requirements.\u003C/p\u003E\n\u003Cp\u003EThis is why the pipeline evaluates multiple requirements rather than relying on a single permission match.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022orchestration-and-protected-execution\u0022\u003EOrchestration and Protected Execution\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EAccessOrchestrator\u003C/code\u003E coordinates authorization and the protected operation.\u003C/p\u003E\n\u003Cp\u003EIts purpose is to establish a consistent enforcement boundary.\u003C/p\u003E\n\u003Cp\u003EConceptually:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EOperation Requested\n |\n v\nPrepare AccessContext\n |\n v\nResolve and Evaluate Policies\n |\n \u002B---- Not Allowed ---\u0026gt; Stop\n |\n v\nExecute Protected Operation\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe security requirement is straightforward:\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EA protected operation must not execute merely because its caller reached an application service or endpoint.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EAuthorization must succeed at an enforcement boundary that covers the operation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022http-and-in-process-execution\u0022\u003EHTTP and In-Process Execution\u003C/h3\u003E\n\u003Cp\u003EHTTP endpoints may enforce authorization through ASP.NET Core authentication and authorization integration.\u003C/p\u003E\n\u003Cp\u003EHowever, applications may also execute operations directly through in-process services.\u003C/p\u003E\n\u003Cp\u003EFor example, a Blazor Server application can invoke a server-side service without making a separate HTTP request.\u003C/p\u003E\n\u003Cp\u003EA controller-level \u003Ccode\u003E[Authorize]\u003C/code\u003E attribute does not automatically protect such a direct service invocation.\u003C/p\u003E\n\u003Cp\u003EWhere operations can be reached through multiple execution paths, authorization must be enforced at an appropriate shared boundary.\u003C/p\u003E\n\u003Cp\u003EThis prevents one entry point from bypassing rules enforced by another.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022determinism-and-security\u0022\u003EDeterminism and Security\u003C/h2\u003E\n\u003Cp\u003EA structured policy pipeline makes authorization behavior easier to reason about.\u003C/p\u003E\n\u003Cp\u003EFor the same effective context, policy configuration, and authoritative security state, the decision should follow the same evaluation rules.\u003C/p\u003E\n\u003Cp\u003EHowever, authorization can depend on mutable information:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EAccount status.\u003C/li\u003E\n\u003Cli\u003ETenant membership.\u003C/li\u003E\n\u003Cli\u003ERole assignments.\u003C/li\u003E\n\u003Cli\u003EEffective permissions.\u003C/li\u003E\n\u003Cli\u003ESession security state.\u003C/li\u003E\n\u003Cli\u003ETarget ownership.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EA policy evaluation that uses stale or inconsistent data may produce a result that differs from one using current authoritative state.\u003C/p\u003E\n\u003Cp\u003EDeterministic policy evaluation therefore depends on both the evaluation rules and the consistency of the information supplied to them.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022time-of-check-and-time-of-use\u0022\u003ETime-of-Check and Time-of-Use\u003C/h3\u003E\n\u003Cp\u003EAuthorization may be evaluated before a protected operation modifies data.\u003C/p\u003E\n\u003Cp\u003EIf relevant security or ownership information changes between the authorization decision and the mutation, a time-of-check/time-of-use problem may arise.\u003C/p\u003E\n\u003Cp\u003ESensitive operations may require transactional consistency, concurrency controls, or revalidation within the protected operation.\u003C/p\u003E\n\u003Cp\u003EA policy pipeline provides a structured decision process, but it does not automatically make every application-level data mutation atomic.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022extensibility-boundaries\u0022\u003EExtensibility Boundaries\u003C/h2\u003E\n\u003Cp\u003EThe authorization pipeline is designed to support application-specific policies.\u003C/p\u003E\n\u003Cp\u003ECustom policies can introduce new requirements without replacing the underlying permission model or authentication engine.\u003C/p\u003E\n\u003Cp\u003EHowever, extensibility must preserve the security hierarchy.\u003C/p\u003E\n\u003Cp\u003EA custom policy should not:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EOverride mandatory invariants.\u003C/li\u003E\n\u003Cli\u003ETrust caller-supplied identity information without validation.\u003C/li\u003E\n\u003Cli\u003ETreat an action name as proof of actor-target ownership.\u003C/li\u003E\n\u003Cli\u003EBypass tenant isolation through a broad permission.\u003C/li\u003E\n\u003Cli\u003EAssume that JWT claims always reflect current authorization state.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003ECustom policies should evaluate well-defined requirements using trusted context and participate in the established decision model.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022architectural-perspective\u0022\u003EArchitectural Perspective\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth\u0027s authorization pipeline separates the responsibilities involved in an access decision.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EAccessContext\u003C/code\u003E describes the operation.\u003C/p\u003E\n\u003Cp\u003EContext enrichment and permission compilation prepare the required authorization information.\u003C/p\u003E\n\u003Cp\u003EPolicy resolution identifies the relevant rules.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EIAccessAuthority\u003C/code\u003E evaluates those rules according to the pipeline\u0027s decision semantics.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EAccessOrchestrator\u003C/code\u003E coordinates authorization with protected execution.\u003C/p\u003E\n\u003Cp\u003ETogether, these components provide a structured foundation for context-aware, policy-driven authorization.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EThe pipeline\u0027s security comes not from the number of policies it executes, but from consistently enforcing the applicable requirements before a protected operation is allowed to proceed.\u003C/strong\u003E\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022next-steps\u0022\u003ENext Steps\u003C/h2\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/plugin-domains/authorization\u0022\u003EAuthorization\u003C/a\u003E \u2014 Actions, permissions, roles, and scopes.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/plugin-domains/policies\u0022\u003EPolicies \u0026amp; Access Control\u003C/a\u003E \u2014 Policy concepts and access decision behavior.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/security/session-security\u0022\u003ESession Security Model\u003C/a\u003E \u2014 Authentication security state and validation boundaries.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/security/token-security\u0022\u003EToken Security\u003C/a\u003E \u2014 Credential validity and authorization freshness.\u003C/li\u003E\n\u003C/ul\u003E\n", + "Html": "\n\u003Cp\u003EUltimateAuth authorization is implemented as a coordinated evaluation process rather than a collection of unrelated permission checks.\u003C/p\u003E\n\u003Cp\u003EThe authorization pipeline brings together the operation being requested, the identities involved, the applicable permissions, and the policies responsible for enforcing security requirements.\u003C/p\u003E\n\u003Cp\u003EAt its center are several distinct responsibilities:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Cstrong\u003EContext preparation:\u003C/strong\u003E Establish the security information required to evaluate an operation.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EPermission resolution:\u003C/strong\u003E Make effective authorization grants available to the decision process.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EPolicy resolution:\u003C/strong\u003E Determine which security rules apply to the requested action.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EPolicy evaluation:\u003C/strong\u003E Evaluate the applicable requirements in a defined order.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EDecision enforcement:\u003C/strong\u003E Permit the protected operation to proceed only when the authorization result allows it.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThis page examines how those responsibilities fit together.\u003C/p\u003E\n\u003Cp\u003EFor an introduction to actions, permissions, and roles, see \u003Ca href=\u0022/docs/plugin-domains/authorization\u0022\u003EAuthorization\u003C/a\u003E. For policy concepts and their intended use, see \u003Ca href=\u0022/docs/plugin-domains/policies\u0022\u003EPolicies \u0026amp; Access Control\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022pipeline-architecture\u0022\u003EPipeline Architecture\u003C/h2\u003E\n\u003Cp\u003EThe authorization process can be represented as a sequence of stages:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EProtected Operation\n |\n v\n AccessContext\n |\n v\nContext Enrichment\n |\n v\nPermission Resolution\n |\n v\nInvariant Evaluation\n |\n v\nGlobal Policy Evaluation\n |\n v\nRuntime Policy Resolution\n |\n v\nRuntime Policy Evaluation\n |\n v\n Final Decision\n |\n \u002B---- Deny ----------------\u0026gt; Reject\n |\n \u002B---- Reauthentication ---\u0026gt; Require Reauthentication\n |\n \u002B---- Allow ---------------\u0026gt; Execute\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis diagram describes the logical responsibilities of the pipeline.\u003C/p\u003E\n\u003Cp\u003EIt should not be interpreted as a guarantee that every stage performs a separate database query or that every authentication mode uses the same mechanism to obtain authorization information.\u003C/p\u003E\n\u003Cp\u003EThe important property is that \u003Cstrong\u003Eauthorization is evaluated against a coherent access context before the protected operation is allowed to proceed\u003C/strong\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022accesscontext-as-the-evaluation-boundary\u0022\u003EAccessContext as the Evaluation Boundary\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EAccessContext\u003C/code\u003E carries the information required to evaluate an authorization request.\u003C/p\u003E\n\u003Cp\u003EIt connects the authenticated actor, the intended target, the requested action, and the relevant security environment.\u003C/p\u003E\n\u003Cp\u003EConceptually, its responsibilities include:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EInformation\u003C/th\u003E\n\u003Cth\u003EPurpose\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EActor\u003C/td\u003E\n\u003Ctd\u003EIdentifies the principal performing the operation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ETarget\u003C/td\u003E\n\u003Ctd\u003EIdentifies the account or resource affected\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAction\u003C/td\u003E\n\u003Ctd\u003EDescribes the protected operation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ETenant context\u003C/td\u003E\n\u003Ctd\u003EEstablishes the relevant isolation boundaries\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EClaims\u003C/td\u003E\n\u003Ctd\u003EProvides authentication and authorization information\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EPermissions\u003C/td\u003E\n\u003Ctd\u003ERepresents the effective permission information available for evaluation\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe context is shared across the authorization process so that individual policies do not need to independently reconstruct the operation.\u003C/p\u003E\n\u003Cblockquote\u003E\n\u003Cp\u003EAuthorization contexts can represent either a single target user through TargetUserKey or multiple target users through TargetUserKeys. Multi-target operations use the same authorization pipeline as single-target operations. Application policies are responsible for validating the complete target collection when access depends on membership, ownership, or another resource-specific scope.\u003C/p\u003E\n\u003C/blockquote\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022actor-and-target-separation\u0022\u003EActor and Target Separation\u003C/h3\u003E\n\u003Cp\u003EConsider the action:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003Eusers.delete.admin\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe actor is the principal requesting the deletion.\u003C/p\u003E\n\u003Cp\u003EThe target is the user account being deleted.\u003C/p\u003E\n\u003Cp\u003EThese identities may be different, and the authorization pipeline must preserve that distinction.\u003C/p\u003E\n\u003Cp\u003EFor a self-service action:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003Eusers.delete.self\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe action indicates a self-service operation, but the action name alone does not prove that actor and target are the same identity.\u003C/p\u003E\n\u003Cp\u003EThat relationship must be established from trusted context and evaluated by the applicable security rules.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022context-integrity\u0022\u003EContext Integrity\u003C/h3\u003E\n\u003Cp\u003EThe reliability of the authorization decision depends on the integrity of the context.\u003C/p\u003E\n\u003Cp\u003EA target identifier or tenant identifier supplied by a client must not automatically be accepted as a trusted authorization fact.\u003C/p\u003E\n\u003Cp\u003EThe pipeline must operate on the authenticated actor and appropriately resolved target information.\u003C/p\u003E\n\u003Cp\u003EA correctly implemented policy cannot compensate for a context that incorrectly identifies the actor or misrepresents the target.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022context-enrichment\u0022\u003EContext Enrichment\u003C/h2\u003E\n\u003Cp\u003EThe initial authorization context may not contain all the information required by the applicable policies.\u003C/p\u003E\n\u003Cp\u003EContext enrichment makes additional authorization information available before evaluation.\u003C/p\u003E\n\u003Cp\u003EThis can include resolving effective permissions and preparing them for efficient policy checks.\u003C/p\u003E\n\u003Cp\u003EConceptually:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EAuthenticated Actor\n |\n v\nResolve Authorization Information\n |\n v\nEffective Permissions\n |\n v\nCompiledPermissionSet\n |\n v\nEnriched AccessContext\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe purpose of enrichment is to separate authorization data resolution from the individual rules that consume that data.\u003C/p\u003E\n\u003Cp\u003EA permission policy should not need to independently load role assignments and rebuild the user\u0027s effective permission set every time it evaluates an action.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022authorization-data-freshness\u0022\u003EAuthorization Data Freshness\u003C/h3\u003E\n\u003Cp\u003EEnrichment does not automatically guarantee that all authorization information is current.\u003C/p\u003E\n\u003Cp\u003EThe source of permission data matters.\u003C/p\u003E\n\u003Cp\u003EFor example, permissions obtained from current server-side state may reflect a recent role change, while permissions derived exclusively from an already-issued JWT may reflect an earlier state.\u003C/p\u003E\n\u003Cp\u003EThe pipeline can evaluate the information it receives consistently, but it cannot make stale claims current without consulting an authoritative source.\u003C/p\u003E\n\u003Cp\u003EThis distinction is particularly relevant to SemiHybrid and PureJwt authentication.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022permission-compilation\u0022\u003EPermission Compilation\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth uses \u003Ccode\u003ECompiledPermissionSet\u003C/code\u003E to represent permissions in a form suitable for repeated authorization checks.\u003C/p\u003E\n\u003Cp\u003EPermission compilation separates the representation of stored grants from the process of deciding whether an action is permitted.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003ERequested action:\n users.delete.admin\n\nEffective grants:\n users.read.self\n users.delete.admin\n credentials.list.self\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EA permission policy can evaluate whether the effective grants cover the requested action.\u003C/p\u003E\n\u003Cp\u003EConceptually:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EpermissionSet.IsAllowed(action);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis illustrates the permission-checking responsibility, rather than a complete public API integration example.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022wildcard-permissions\u0022\u003EWildcard Permissions\u003C/h3\u003E\n\u003Cp\u003ECompiled permissions may also represent wildcard grants.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003Eusers.*\nusers.profile.*\n*\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWildcard matching must respect the permission model\u0027s defined action hierarchy.\u003C/p\u003E\n\u003Cp\u003EA broad grant should not be inferred merely because several currently visible permissions share a prefix.\u003C/p\u003E\n\u003Cp\u003EWhere permission normalization replaces a complete action group with a wildcard, the normalization must preserve the intended permission semantics.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022compilation-is-not-authorization\u0022\u003ECompilation Is Not Authorization\u003C/h3\u003E\n\u003Cp\u003EA compiled permission set is an efficient representation of grants.\u003C/p\u003E\n\u003Cp\u003EIt does not independently establish that the actor may perform the operation.\u003C/p\u003E\n\u003Cp\u003EA matching permission can still be insufficient when:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EThe actor is not permitted to access the target.\u003C/li\u003E\n\u003Cli\u003EThe operation crosses a prohibited tenant boundary.\u003C/li\u003E\n\u003Cli\u003EThe account does not satisfy required security conditions.\u003C/li\u003E\n\u003Cli\u003EAn operation-specific policy denies the action.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EPermission matching is one requirement within the broader authorization decision.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022policy-registration-and-resolution\u0022\u003EPolicy Registration and Resolution\u003C/h2\u003E\n\u003Cp\u003ERuntime policies must be discoverable for the operations they protect.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth\u0027s authorization architecture includes policy registration and compilation concepts represented by:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003EAccessPolicyRegistry\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003ECompiledAccessPolicySet\u003C/code\u003E\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThese components separate policy configuration from runtime selection.\u003C/p\u003E\n\u003Cp\u003EConceptually:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003ERegistered Policies\n |\n v\nAccessPolicyRegistry\n |\n v\nCompiledAccessPolicySet\n |\n v\nPolicies Matching Requested Action\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022action-based-selection\u0022\u003EAction-Based Selection\u003C/h3\u003E\n\u003Cp\u003EPolicies may be associated with action prefixes or patterns.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003Eusers.*\nusers.delete.*\nusers.delete.admin\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EA request for:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003Eusers.delete.admin\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Emay select policies associated with the relevant action hierarchy.\u003C/p\u003E\n\u003Cp\u003EThis allows broad policies to cover a domain while more specific policies enforce requirements for individual operations.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022prefix-matching-and-boundaries\u0022\u003EPrefix Matching and Boundaries\u003C/h3\u003E\n\u003Cp\u003EAction matching must respect the intended structure of action identifiers.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003Eusers.delete.admin\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Ebelongs to the \u003Ccode\u003Eusers\u003C/code\u003E action domain.\u003C/p\u003E\n\u003Cp\u003EBut an unrelated action whose text merely begins with similar characters should not automatically be treated as belonging to that domain.\u003C/p\u003E\n\u003Cp\u003EPolicy matching must therefore follow the actual action-pattern semantics implemented by the registry.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022resolution-versus-evaluation\u0022\u003EResolution Versus Evaluation\u003C/h3\u003E\n\u003Cp\u003EPolicy resolution answers:\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EWhich policies are candidates for this operation?\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EPolicy evaluation answers:\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EDo those policies permit this operation in the current context?\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EA policy may match an action pattern but still be inapplicable to a particular runtime context.\u003C/p\u003E\n\u003Cp\u003EThat distinction is handled through policy applicability rules.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022runtime-applicability\u0022\u003ERuntime Applicability\u003C/h2\u003E\n\u003Cp\u003EA policy can determine whether it applies to the current authorization context.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth represents this responsibility through \u003Ccode\u003EAppliesTo\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EFor example, a policy restricting administrative self-modification may only be relevant when:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EThe requested action has administrative scope.\u003C/li\u003E\n\u003Cli\u003EThe operation changes protected account state.\u003C/li\u003E\n\u003Cli\u003EThe actor and target are the same identity.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EIt may not apply to administrative read operations or unrelated domains.\u003C/p\u003E\n\u003Cp\u003EConceptually:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EPolicy selected by action\n |\n v\nAppliesTo(context)\n |\n \u002B---- No ----\u0026gt; Skip policy\n |\n \u002B---- Yes ---\u0026gt; Evaluate policy\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis enables policies to remain focused on specific security requirements without requiring every action to have an entirely separate policy implementation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022applicability-is-not-a-security-bypass\u0022\u003EApplicability Is Not a Security Bypass\u003C/h3\u003E\n\u003Cp\u003EAn applicability rule determines when a policy participates in evaluation.\u003C/p\u003E\n\u003Cp\u003EIf a mandatory security requirement can be skipped because its applicability condition is too broad or incorrectly implemented, the resulting authorization boundary may be weaker than intended.\u003C/p\u003E\n\u003Cp\u003EPolicies enforcing fundamental security invariants should not depend on application-controlled applicability rules that can bypass those invariants.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022policy-evaluation-stages\u0022\u003EPolicy Evaluation Stages\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth\u0027s policy architecture distinguishes between invariants, global policies, and runtime policies.\u003C/p\u003E\n\u003Cp\u003EThese categories establish the order and scope of security evaluation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022stage-1-invariants\u0022\u003EStage 1: Invariants\u003C/h3\u003E\n\u003Cp\u003EInvariants represent mandatory security requirements.\u003C/p\u003E\n\u003Cp\u003EThey are evaluated before ordinary policy stages.\u003C/p\u003E\n\u003Cp\u003EAn invariant denial prevents the requested operation from proceeding, regardless of whether another policy would otherwise permit it.\u003C/p\u003E\n\u003Cp\u003EInvariants provide the strongest enforcement boundary within the policy pipeline.\u003C/p\u003E\n\u003Cp\u003EThey should not be treated as ordinary permissions that can be overridden by a broader grant.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022stage-2-global-policies\u0022\u003EStage 2: Global Policies\u003C/h3\u003E\n\u003Cp\u003EGlobal policies enforce broadly applicable security requirements.\u003C/p\u003E\n\u003Cp\u003EThey may examine the operation\u0027s context to determine whether their rule is relevant.\u003C/p\u003E\n\u003Cp\u003EExamples include requirements related to authentication state, account eligibility, and tenant isolation.\u003C/p\u003E\n\u003Cp\u003EGlobal policies allow these requirements to be enforced consistently without attaching identical rules to every individual action.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022stage-3-runtime-policies\u0022\u003EStage 3: Runtime Policies\u003C/h3\u003E\n\u003Cp\u003ERuntime policies are resolved according to the requested action and evaluated against the current context.\u003C/p\u003E\n\u003Cp\u003EThey support requirements such as:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003ERequired permissions.\u003C/li\u003E\n\u003Cli\u003ESelf-access restrictions.\u003C/li\u003E\n\u003Cli\u003EAdministrative operation constraints.\u003C/li\u003E\n\u003Cli\u003EDomain-specific authorization rules.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003ERuntime policies extend the decision process without replacing the mandatory security requirements established by earlier stages.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022iaccessauthority-and-decision-evaluation\u0022\u003EIAccessAuthority and Decision Evaluation\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EIAccessAuthority\u003C/code\u003E represents the authorization decision responsibility within the pipeline.\u003C/p\u003E\n\u003Cp\u003EThe authority evaluates the applicable security requirements and produces an authorization result.\u003C/p\u003E\n\u003Cp\u003EConceptually, the result can represent:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EResult\u003C/th\u003E\n\u003Cth\u003EMeaning\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EAllow\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EThe applicable requirements permit the operation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EDeny\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EA required security condition rejects the operation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EReauthentication required\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EThe operation requires an additional authentication condition before proceeding\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThese outcomes must not be confused with the result of an individual policy.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022policy-allow-versus-final-allow\u0022\u003EPolicy Allow Versus Final Allow\u003C/h3\u003E\n\u003Cp\u003EAn individual policy may return an allow result because its own requirement has been satisfied.\u003C/p\u003E\n\u003Cp\u003EThat does not necessarily mean the entire operation is authorized.\u003C/p\u003E\n\u003Cp\u003EOther applicable policies may still deny the request or require reauthentication.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EAn individual policy\u0027s approval is not a universal authorization grant.\u003C/strong\u003E\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022deny-precedence\u0022\u003EDeny Precedence\u003C/h3\u003E\n\u003Cp\u003EA denial from an applicable mandatory policy prevents the operation from being authorized.\u003C/p\u003E\n\u003Cp\u003EThe pipeline must not allow a later permissive policy to override a previously established denial.\u003C/p\u003E\n\u003Cp\u003EThis supports predictable enforcement across multiple independent security rules.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022reauthentication-requirements\u0022\u003EReauthentication Requirements\u003C/h3\u003E\n\u003Cp\u003ESome operations may require stronger or more recent authentication.\u003C/p\u003E\n\u003Cp\u003EA policy can represent that requirement separately from an ordinary denial.\u003C/p\u003E\n\u003Cp\u003EThe authorization system must distinguish between:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EAn operation that is prohibited.\u003C/li\u003E\n\u003Cli\u003EAn operation that may proceed after additional authentication.\u003C/li\u003E\n\u003Cli\u003EAn operation that is currently authorized.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThe exact precedence and propagation of reauthentication results must follow the implemented decision engine.\u003C/p\u003E\n\u003Cp\u003EA reauthentication result should not be interpreted as permission to execute the protected operation immediately.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022permission-policies\u0022\u003EPermission Policies\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EMustHavePermissionPolicy\u003C/code\u003E represents the connection between the permission model and the policy pipeline.\u003C/p\u003E\n\u003Cp\u003EIts responsibility is to evaluate whether the actor has the permission required for the requested action.\u003C/p\u003E\n\u003Cp\u003EConceptually:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EAccessContext.Action\n |\n v\nCompiledPermissionSet\n |\n v\nPermission Evaluation\n |\n v\nPolicy Decision\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis design separates permission storage and resolution from permission enforcement.\u003C/p\u003E\n\u003Cp\u003ERoles and assignments determine effective grants.\u003C/p\u003E\n\u003Cp\u003EThe compiled permission set represents those grants.\u003C/p\u003E\n\u003Cp\u003EThe permission policy evaluates the requested action against them.\u003C/p\u003E\n\u003Cp\u003EThe final authorization decision also considers the other applicable security policies.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022permissions-are-not-a-replacement-for-policies\u0022\u003EPermissions Are Not a Replacement for Policies\u003C/h3\u003E\n\u003Cp\u003EFor example, an actor may possess:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003Eusers.delete.admin\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThat permission does not automatically authorize deleting any user in any tenant.\u003C/p\u003E\n\u003Cp\u003EThe operation must still satisfy the relevant target, tenant, and account security requirements.\u003C/p\u003E\n\u003Cp\u003EThis is why the pipeline evaluates multiple requirements rather than relying on a single permission match.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022orchestration-and-protected-execution\u0022\u003EOrchestration and Protected Execution\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EAccessOrchestrator\u003C/code\u003E coordinates authorization and the protected operation.\u003C/p\u003E\n\u003Cp\u003EIts purpose is to establish a consistent enforcement boundary.\u003C/p\u003E\n\u003Cp\u003EConceptually:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EOperation Requested\n |\n v\nPrepare AccessContext\n |\n v\nResolve and Evaluate Policies\n |\n \u002B---- Not Allowed ---\u0026gt; Stop\n |\n v\nExecute Protected Operation\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe security requirement is straightforward:\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EA protected operation must not execute merely because its caller reached an application service or endpoint.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EAuthorization must succeed at an enforcement boundary that covers the operation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022http-and-in-process-execution\u0022\u003EHTTP and In-Process Execution\u003C/h3\u003E\n\u003Cp\u003EHTTP endpoints may enforce authorization through ASP.NET Core authentication and authorization integration.\u003C/p\u003E\n\u003Cp\u003EHowever, applications may also execute operations directly through in-process services.\u003C/p\u003E\n\u003Cp\u003EFor example, a Blazor Server application can invoke a server-side service without making a separate HTTP request.\u003C/p\u003E\n\u003Cp\u003EA controller-level \u003Ccode\u003E[Authorize]\u003C/code\u003E attribute does not automatically protect such a direct service invocation.\u003C/p\u003E\n\u003Cp\u003EWhere operations can be reached through multiple execution paths, authorization must be enforced at an appropriate shared boundary.\u003C/p\u003E\n\u003Cp\u003EThis prevents one entry point from bypassing rules enforced by another.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022determinism-and-security\u0022\u003EDeterminism and Security\u003C/h2\u003E\n\u003Cp\u003EA structured policy pipeline makes authorization behavior easier to reason about.\u003C/p\u003E\n\u003Cp\u003EFor the same effective context, policy configuration, and authoritative security state, the decision should follow the same evaluation rules.\u003C/p\u003E\n\u003Cp\u003EHowever, authorization can depend on mutable information:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EAccount status.\u003C/li\u003E\n\u003Cli\u003ETenant membership.\u003C/li\u003E\n\u003Cli\u003ERole assignments.\u003C/li\u003E\n\u003Cli\u003EEffective permissions.\u003C/li\u003E\n\u003Cli\u003ESession security state.\u003C/li\u003E\n\u003Cli\u003ETarget ownership.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EA policy evaluation that uses stale or inconsistent data may produce a result that differs from one using current authoritative state.\u003C/p\u003E\n\u003Cp\u003EDeterministic policy evaluation therefore depends on both the evaluation rules and the consistency of the information supplied to them.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022time-of-check-and-time-of-use\u0022\u003ETime-of-Check and Time-of-Use\u003C/h3\u003E\n\u003Cp\u003EAuthorization may be evaluated before a protected operation modifies data.\u003C/p\u003E\n\u003Cp\u003EIf relevant security or ownership information changes between the authorization decision and the mutation, a time-of-check/time-of-use problem may arise.\u003C/p\u003E\n\u003Cp\u003ESensitive operations may require transactional consistency, concurrency controls, or revalidation within the protected operation.\u003C/p\u003E\n\u003Cp\u003EA policy pipeline provides a structured decision process, but it does not automatically make every application-level data mutation atomic.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022extensibility-boundaries\u0022\u003EExtensibility Boundaries\u003C/h2\u003E\n\u003Cp\u003EThe authorization pipeline is designed to support application-specific policies.\u003C/p\u003E\n\u003Cp\u003ECustom policies can introduce new requirements without replacing the underlying permission model or authentication engine.\u003C/p\u003E\n\u003Cp\u003EHowever, extensibility must preserve the security hierarchy.\u003C/p\u003E\n\u003Cp\u003EA custom policy should not:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EOverride mandatory invariants.\u003C/li\u003E\n\u003Cli\u003ETrust caller-supplied identity information without validation.\u003C/li\u003E\n\u003Cli\u003ETreat an action name as proof of actor-target ownership.\u003C/li\u003E\n\u003Cli\u003EBypass tenant isolation through a broad permission.\u003C/li\u003E\n\u003Cli\u003EAssume that JWT claims always reflect current authorization state.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003ECustom policies should evaluate well-defined requirements using trusted context and participate in the established decision model.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022architectural-perspective\u0022\u003EArchitectural Perspective\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth\u0027s authorization pipeline separates the responsibilities involved in an access decision.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EAccessContext\u003C/code\u003E describes the operation.\u003C/p\u003E\n\u003Cp\u003EContext enrichment and permission compilation prepare the required authorization information.\u003C/p\u003E\n\u003Cp\u003EPolicy resolution identifies the relevant rules.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EIAccessAuthority\u003C/code\u003E evaluates those rules according to the pipeline\u0027s decision semantics.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EAccessOrchestrator\u003C/code\u003E coordinates authorization with protected execution.\u003C/p\u003E\n\u003Cp\u003ETogether, these components provide a structured foundation for context-aware, policy-driven authorization.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EThe pipeline\u0027s security comes not from the number of policies it executes, but from consistently enforcing the applicable requirements before a protected operation is allowed to proceed.\u003C/strong\u003E\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022next-steps\u0022\u003ENext Steps\u003C/h2\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/plugin-domains/authorization\u0022\u003EAuthorization\u003C/a\u003E \u2014 Actions, permissions, roles, and scopes.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/plugin-domains/policies\u0022\u003EPolicies \u0026amp; Access Control\u003C/a\u003E \u2014 Policy concepts and access decision behavior.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/security/session-security\u0022\u003ESession Security Model\u003C/a\u003E \u2014 Authentication security state and validation boundaries.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/security/token-security\u0022\u003EToken Security\u003C/a\u003E \u2014 Credential validity and authorization freshness.\u003C/li\u003E\n\u003C/ul\u003E\n", "Headings": [ { "Id": "pipeline-architecture", @@ -164,6 +164,6 @@ "Level": 0 } ], - "SourceHash": "11AF093DB571457E519CECB1BD55B6F0D587DD1D831F6E574E4F070B3438A4CD", + "SourceHash": "DF736EC7733E14A0E972F6F6C68600EFBFECC7CDF4725B8DC531FD96C0027C16", "BuilderVersion": "1" } \ No newline at end of file diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserSummaryQueryStore.cs b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserSummaryQueryStore.cs index 97b02a1b..ee7d689f 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserSummaryQueryStore.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserSummaryQueryStore.cs @@ -251,7 +251,7 @@ public async Task> GetByKeysAsync(IReadOnlyCollection } return summaries - .OrderBy(x => x.UserKey) + .OrderBy(x => x.UserKey.Value, StringComparer.Ordinal) .ToArray(); } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/Store/Query/UserSummaryQueryStoreContractTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/Store/Query/UserSummaryQueryStoreContractTests.cs index f774d5c3..860497da 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/Store/Query/UserSummaryQueryStoreContractTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/Store/Query/UserSummaryQueryStoreContractTests.cs @@ -485,6 +485,295 @@ await db.SeedAsync(TenantA, result.Items.Should().BeEmpty(); } + [Fact] + public async Task GetByKeysAsync_WhenEmpty_ReturnsEmpty() + { + await using var db = await CreateDatabaseAsync(); + + var result = await db.CreateStore(TenantA) + .GetByKeysAsync(Array.Empty()); + + result.Should().BeEmpty(); + } + + [Fact] + public async Task GetByKeysAsync_WhenKeysAreDuplicated_ReturnsEachUserOnce() + { + await using var db = await CreateDatabaseAsync(); + + var first = UserKey.New(); + var second = UserKey.New(); + + await db.SeedAsync(TenantA, + [ + CreateUser(first, "Alice"), + CreateUser(second, "Bob") + ]); + + var result = await db.CreateStore(TenantA) + .GetByKeysAsync([first, first, second, second]); + + result.Should().HaveCount(2); + + result.Select(x => x.UserKey) + .Should().BeEquivalentTo([first, second]); + } + + [Fact] + public async Task GetByKeysAsync_WhenSomeKeysDoNotExist_ReturnsOnlyExistingUsers() + { + await using var db = await CreateDatabaseAsync(); + + var existing = UserKey.New(); + var missing = UserKey.New(); + + await db.SeedAsync(TenantA, + [ + CreateUser(existing, "Existing User") + ]); + + var result = await db.CreateStore(TenantA) + .GetByKeysAsync([existing, missing]); + + result.Should().ContainSingle(); + + result[0].UserKey.Should().Be(existing); + result[0].DisplayName.Should().Be("Existing User"); + } + + [Fact] + public async Task GetByKeysAsync_ExcludesDeletedUsers() + { + await using var db = await CreateDatabaseAsync(); + + var active = UserKey.New(); + var deleted = UserKey.New(); + + await db.SeedAsync(TenantA, + [ + CreateUser(active, "Active"), + CreateUser(deleted, "Deleted") with + { + IsDeleted = true + } + ]); + + var result = await db.CreateStore(TenantA) + .GetByKeysAsync([active, deleted]); + + result.Should().ContainSingle(); + + result[0].UserKey.Should().Be(active); + } + + [Fact] + public async Task GetByKeysAsync_IsTenantIsolated() + { + await using var db = await CreateDatabaseAsync(); + + var sharedKey = UserKey.New(); + var tenantAOnly = UserKey.New(); + var tenantBOnly = UserKey.New(); + + await db.SeedAsync(TenantA, + [ + CreateUser(sharedKey, "Tenant A Shared"), + CreateUser(tenantAOnly, "Tenant A Only") + ]); + + await db.SeedAsync(TenantB, + [ + CreateUser(sharedKey, "Tenant B Shared"), + CreateUser(tenantBOnly, "Tenant B Only") + ]); + + var requested = new[] + { + sharedKey, + tenantAOnly, + tenantBOnly + }; + + var resultA = await db.CreateStore(TenantA) + .GetByKeysAsync(requested); + + var resultB = await db.CreateStore(TenantB) + .GetByKeysAsync(requested); + + resultA.Should().HaveCount(2); + resultB.Should().HaveCount(2); + + resultA.Select(x => x.UserKey) + .Should().BeEquivalentTo([sharedKey, tenantAOnly]); + + resultB.Select(x => x.UserKey) + .Should().BeEquivalentTo([sharedKey, tenantBOnly]); + + resultA.Single(x => x.UserKey == sharedKey) + .DisplayName.Should().Be("Tenant A Shared"); + + resultB.Single(x => x.UserKey == sharedKey) + .DisplayName.Should().Be("Tenant B Shared"); + } + + [Fact] + public async Task GetByKeysAsync_ReturnsCompleteUserSummary() + { + await using var db = await CreateDatabaseAsync(); + + var key = UserKey.New(); + + await db.SeedAsync(TenantA, + [ + new UserSummarySeed + { + UserKey = key, + CreatedAt = Now, + Status = UserStatus.Active, + DisplayName = "John Doe", + UserName = "johndoe", + Email = "john@example.com", + Phone = "5551234567" + } + ]); + + var result = await db.CreateStore(TenantA) + .GetByKeysAsync([key]); + + var user = result.Should().ContainSingle().Subject; + + user.UserKey.Should().Be(key); + user.DisplayName.Should().Be("John Doe"); + user.UserName.Should().Be("johndoe"); + user.PrimaryEmail.Should().Be("john@example.com"); + user.PrimaryPhone.Should().Be("5551234567"); + user.Status.Should().Be(UserStatus.Active); + user.CreatedAt.Should().Be(Now); + } + + [Fact] + public async Task GetByKeysAsync_WhenProfileDoesNotExist_ReturnsUserWithoutDisplayName() + { + await using var db = await CreateDatabaseAsync(); + + var key = UserKey.New(); + + await db.SeedAsync(TenantA, + [ + CreateUser(key, "Not Persisted") with + { + HasProfile = false + } + ]); + + var result = await db.CreateStore(TenantA) + .GetByKeysAsync([key]); + + result.Should().ContainSingle(); + + result[0].UserKey.Should().Be(key); + result[0].DisplayName.Should().BeNull(); + } + + [Fact] + public async Task GetByKeysAsync_UsesDefaultProfileOnly() + { + await using var db = await CreateDatabaseAsync(); + + var key = UserKey.New(); + + var customProfile = ProfileKey.Parse("business", null); + + await db.SeedAsync(TenantA, + [ + CreateUser(key, "Business Display") with + { + ProfileKey = customProfile + } + ]); + + var result = await db.CreateStore(TenantA) + .GetByKeysAsync([key]); + + result.Should().ContainSingle(); + result[0].DisplayName.Should().BeNull(); + } + + [Fact] + public async Task GetByKeysAsync_With250Keys_ReturnsAllUsers() + { + await using var db = await CreateDatabaseAsync(); + + var users = Enumerable.Range(1, 250) + .Select(i => CreateUser( + UserKey.New(), + $"User {i:D3}", + Now.AddSeconds(i))) + .ToArray(); + + await db.SeedAsync(TenantA, users); + + var keys = users + .Select(x => x.UserKey) + .ToArray(); + + var result = await db.CreateStore(TenantA) + .GetByKeysAsync(keys); + + result.Should().HaveCount(250); + + result.Select(x => x.UserKey) + .Should().BeEquivalentTo(keys); + } + + [Fact] + public async Task GetByKeysAsync_FindsUsersBeyondFirstLifecyclePage() + { + await using var db = await CreateDatabaseAsync(); + + var users = Enumerable.Range(1, 1001) + .Select(i => CreateUser( + UserKey.New(), + $"User {i:D4}", + Now.AddSeconds(i))) + .ToArray(); + + await db.SeedAsync(TenantA, users); + + var requested = new[] + { + users[0].UserKey, + users[500].UserKey, + users[1000].UserKey + }; + + var result = await db.CreateStore(TenantA) + .GetByKeysAsync(requested); + + result.Should().HaveCount(3); + + result.Select(x => x.UserKey) + .Should().BeEquivalentTo(requested); + } + + [Fact] + public async Task GetByKeysAsync_WhenCancelled_Throws() + { + await using var db = await CreateDatabaseAsync(); + + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + var store = db.CreateStore(TenantA); + + var act = () => store.GetByKeysAsync( + [UserKey.New()], + cts.Token); + + await act.Should() + .ThrowAsync(); + } + private static UserSummarySeed CreateUser( UserKey key, diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserApplicationServiceTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserApplicationServiceTests.cs index d1cb169c..9685b2c7 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserApplicationServiceTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserApplicationServiceTests.cs @@ -1765,6 +1765,133 @@ public async Task UserIdentifierExistsAsync_WhenStoreReportsMissing_ReturnsFalse result.Should().BeFalse(); } + [Fact] + public async Task GetUsersByKeysAsync_WhenOver250Keys_ThrowsValidationException() + { + var f = CreateFixture(); + + var context = CreateContext( + action: UAuthActions.Users.GetByKeysAdmin); + + var keys = Enumerable.Range(0, 251) + .Select(_ => UserKey.New()) + .ToArray(); + + var act = () => f.Sut.GetUsersByKeysAsync(context, keys); + + await act.Should() + .ThrowAsync(); + + f.QueryStoreFactory.VerifyNoOtherCalls(); + f.QueryStore.VerifyNoOtherCalls(); + } + + [Fact] + public async Task GetUsersByKeysAsync_WhenEmpty_ReturnsEmptyWithoutStoreAccess() + { + var f = CreateFixture(); + + var context = CreateContext( + action: UAuthActions.Users.GetByKeysAdmin); + + var result = await f.Sut.GetUsersByKeysAsync( + context, + Array.Empty()); + + result.Should().BeEmpty(); + + f.QueryStoreFactory.VerifyNoOtherCalls(); + f.QueryStore.VerifyNoOtherCalls(); + } + + [Fact] + public async Task GetUsersByKeysAsync_ShouldNormalizeKeysAndUseResourceTenant() + { + var f = CreateFixture(); + + var context = CreateContext( + action: UAuthActions.Users.GetByKeysAdmin); + + var first = UserKey.New(); + var second = UserKey.New(); + + var expected = new UserSummary[] + { + new() + { + UserKey = first, + DisplayName = "Alice", + Status = UserStatus.Active, + CreatedAt = Now + }, + new() + { + UserKey = second, + DisplayName = "Bob", + Status = UserStatus.Active, + CreatedAt = Now + } + }; + + f.QueryStore + .Setup(x => x.GetByKeysAsync( + It.Is>(keys => + keys.Count == 2 && + keys.Contains(first) && + keys.Contains(second)), + It.IsAny())) + .ReturnsAsync(expected); + + var result = await f.Sut.GetUsersByKeysAsync( + context, + [first, first, second]); + + result.Should().BeEquivalentTo(expected); + + f.QueryStoreFactory.Verify( + x => x.Create(context.ResourceTenant), + Times.Once); + + f.QueryStore.Verify( + x => x.GetByKeysAsync( + It.Is>(keys => + keys.Count == 2), + It.IsAny()), + Times.Once); + } + + [Fact] + public async Task GetUsersByKeysAsync_When250DistinctKeys_ExecutesSuccessfully() + { + var f = CreateFixture(); + + var context = CreateContext( + action: UAuthActions.Users.GetByKeysAdmin); + + var keys = Enumerable.Range(0, 250) + .Select(_ => UserKey.New()) + .ToArray(); + + f.QueryStore + .Setup(x => x.GetByKeysAsync( + It.Is>(x => + x.Count == 250), + It.IsAny())) + .ReturnsAsync(Array.Empty()); + + var result = await f.Sut.GetUsersByKeysAsync( + context, + keys); + + result.Should().BeEmpty(); + + f.QueryStore.Verify(x => x.GetByKeysAsync( + It.Is>(x => + x.Count == 250), + It.IsAny()), + Times.Once); + } + // ============================================================ // Helpers // ============================================================ @@ -1788,9 +1915,14 @@ private static Fixture CreateFixture(params IUserLifecycleIntegration[] integrat var queryStoreFactory = new Mock(MockBehavior.Strict); var sessionStore = new Mock(MockBehavior.Strict); var clock = new Mock(MockBehavior.Strict); + var queryStore = new Mock(MockBehavior.Strict); clock.SetupGet(x => x.UtcNow).Returns(Now); + queryStoreFactory + .Setup(x => x.Create(It.IsAny())) + .Returns(queryStore.Object); + lifecycleFactory .Setup(x => x.Create(It.IsAny())) .Returns(lifecycleStore.Object); @@ -1862,6 +1994,16 @@ private static Fixture CreateFixture(params IUserLifecycleIntegration[] integrat .Returns, CancellationToken>( (_, command, ct) => command.ExecuteAsync(ct)); + access + .Setup(x => x.ExecuteAsync( + It.IsAny(), + It.IsAny>>(), + It.IsAny())) + .Returns>, + CancellationToken>( + (_, command, ct) => command.ExecuteAsync(ct)); + atomic .Setup(x => x.ExecuteAsync( It.IsAny>>(), @@ -1900,7 +2042,9 @@ private static Fixture CreateFixture(params IUserLifecycleIntegration[] integrat identifierValidator, normalizer, identifierVerifier, - sessionStore); + sessionStore, + queryStoreFactory, + queryStore); } private static AccessContext CreateContext( @@ -1930,10 +2074,7 @@ private static CreateUserRequest CreateUserRequest() UserName = "alice" }; - private static void SetupValidCreate( - Fixture f, - AccessContext context, - CreateUserRequest request) + private static void SetupValidCreate(Fixture f, AccessContext context, CreateUserRequest request) { f.UserCreateValidator .Setup(x => x.ValidateAsync( @@ -2013,9 +2154,7 @@ private static void SetupDeleteSelfPersistence( .Returns(Task.CompletedTask); } - private static void SetupCreateNormalizers( - Fixture f, - CreateUserRequest request) + private static void SetupCreateNormalizers(Fixture f, CreateUserRequest request) { if (!string.IsNullOrWhiteSpace(request.UserName)) { @@ -2068,7 +2207,9 @@ private sealed record Fixture( Mock IdentifierValidator, Mock IdentifierNormalizer, Mock IdentifierVerifier, - Mock SessionStore); + Mock SessionStore, + Mock QueryStoreFactory, + Mock QueryStore); private static UserIdentifier CreateIdentifier( UserKey userKey,