diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/MyIdentityPage.razor b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/MyIdentityPage.razor index e90a060f..3ee4da90 100644 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/MyIdentityPage.razor +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/MyIdentityPage.razor @@ -1,8 +1,8 @@ @page "/dashboard/identity" -@using CodeBeam.UltimateAuth.Core.Contracts -@using CodeBeam.UltimateAuth.IdentityLab.Services -@using CodeBeam.UltimateAuth.Users.Contracts +@using CodeBeam.UltimateAuth.Server.Options +@using Microsoft.Extensions.Options @layout DashboardLayout +@inject IOptions ServerOptions @inject SandboxAccountDeletionService SandboxAccountDeletionService @inject LabDashboardService LabDashboardService @inject IUAuthClient UAuthClient @@ -55,43 +55,26 @@ @if (_loading) { - - - - - - - - - + + + + + } else if (_loadError is not null) { - + @_loadError - + Try Again } else if (_profile is null) { - + Profile information is currently unavailable. } @@ -239,14 +222,22 @@
-

Sign-in Identifiers

-

- Identifiers associated with your account. -

+

+ Sign-in Identifiers + + @if (!_identifiersLoading && _identifiersError is null) + { + + @ActiveIdentifierCount/@MaxIdentifiers + + } +

+ +

Identifiers associated with your account.

- +
@@ -458,6 +449,10 @@ private string? _identifiersError; private bool _isSandboxOwner; + private int MaxIdentifiers => ServerOptions.Value.Identifiers.Behavior.MaxIdentifiersPerUser; + private int ActiveIdentifierCount => _identifiers.Count; + private bool IdentifierLimitReached => ActiveIdentifierCount >= MaxIdentifiers; + private static string FormatValue(string? value) => string.IsNullOrWhiteSpace(value) ? "Not provided" : value; protected override async Task OnInitializedAsync() diff --git a/docs/content/client-sdk/identifiers.md b/docs/content/client-sdk/identifiers.md index 4e6fbec7..1fb33760 100644 --- a/docs/content/client-sdk/identifiers.md +++ b/docs/content/client-sdk/identifiers.md @@ -252,6 +252,38 @@ The server determines whether the new value can be assigned as primary. Adding an identifier does not automatically establish verified ownership. +### Identifier Limits + +The server may restrict how many identifiers a user can maintain. + +By default, UltimateAuth allows up to **10 active identifiers per user**. Applications may configure a different global limit or optional limits for individual identifier types. + +These limits apply to both self-service and administrative identifier operations. + +When adding an identifier, the server validates the configured limits before accepting the operation. + +Possible validation errors include: + +| Error Code | Description | +|---|---| +| `identifier_total_limit_exceeded` | The maximum number of active identifiers has been reached | +| `identifier_type_limit_exceeded` | The maximum number of active identifiers of this type has been reached | + +Applications should display an appropriate message when an addition is rejected. + +For example: + +- **Total limit reached:** "You have reached the maximum number of identifiers allowed for your account." +- **Type limit reached:** "You have reached the maximum number of identifiers allowed for this type." + +Soft-deleted identifiers do not count toward active limits. + +However, deleting an identifier does not necessarily make its value available for reuse. Identifier uniqueness and identifier capacity are separate rules. + +The server remains authoritative when enforcing these limits. Client applications should not rely solely on local identifier counts to determine whether an addition will succeed. + +For configuration details, see [Server Options](/docs/configuration/server-options). + ## 6. Identifier Uniqueness and Normalization UltimateAuth normalizes identifier values and enforces uniqueness within the relevant tenant boundary. diff --git a/docs/content/configuration/server-options.md b/docs/content/configuration/server-options.md index 62f53aa7..1f2c7dcd 100644 --- a/docs/content/configuration/server-options.md +++ b/docs/content/configuration/server-options.md @@ -920,6 +920,69 @@ Identifier normalization and uniqueness are particularly important because they Use the provided options rather than implementing inconsistent normalization independently in the client. +### Identifier Limits + +UltimateAuth supports configurable limits on the number of identifiers associated with each user. + +By default, a user can have up to **10 active identifiers**, regardless of their type. + +```csharp +options.Identifiers.Behavior.MaxIdentifiersPerUser = 10; +``` + +This is a global per-user limit. It applies to the combined number of usernames, email addresses, phone numbers, and custom identifiers. + +#### Optional Per-Type Limits + +Applications can optionally define limits for individual identifier types: + +```csharp +options.Identifiers.Behavior.MaxIdentifiersPerUser = 10; + +options.Identifiers.Behavior.MaxIdentifiersPerType = new() +{ + [UserIdentifierType.Username] = 2, + [UserIdentifierType.Email] = 5, + [UserIdentifierType.Phone] = 3 +}; +``` + +**Per-type limits are not configured by default.** + +When no per-type limit is specified, only the global limit applies. + +For example, with the configuration above, a user may have up to five email addresses, but the combined number of all active identifiers cannot exceed ten. + +#### Default Behavior + +| Option | Default | Description | +|---|---|---| +| `MaxIdentifiersPerUser` | `10` | Maximum total active identifiers per user | +| `MaxIdentifiersPerType` | Empty dictionary | Optional limits for individual identifier types | + +The following rules apply: + +- Primary and non-primary identifiers count equally. +- Soft-deleted identifiers do not count toward active limits. +- Self-service and administrative operations follow the same limits. +- Per-type limits do not override the global limit. +- Existing identifier behavior options remain applicable. + +For example, `AllowMultipleUsernames = false` still prevents multiple usernames, even when the configured username limit is greater than one. + +#### Limit Validation + +When an identifier addition exceeds a configured limit, the operation is rejected with a validation error. + +| Error Code | Meaning | +|---|---| +| `identifier_total_limit_exceeded` | The user has reached the global identifier limit | +| `identifier_type_limit_exceeded` | The user has reached the configured limit for that identifier type | + +Applications should handle these errors and display appropriate feedback. + +Identifier limits are enforced by the Users application layer rather than individual persistence stores. + ## 18. User Profile Options `options.UserProfile` controls profile-related capabilities. diff --git a/docs/content/plugin-domains/users.md b/docs/content/plugin-domains/users.md index 3b5ffdbe..b6fb6ef0 100644 --- a/docs/content/plugin-domains/users.md +++ b/docs/content/plugin-domains/users.md @@ -113,6 +113,26 @@ UltimateAuth treats identifiers as independent identity records rather than assu This supports applications with different identity requirements without changing the underlying authentication model. +### Identifier Capacity and Limits + +UltimateAuth allows users to maintain multiple identifiers while providing configurable limits to prevent uncontrolled identifier growth. + +The default configuration allows **10 active identifiers per user**, shared across all identifier types. + +Applications can optionally configure additional per-type limits when their identity model requires them. + +For example, an application may allow several email addresses and phone numbers while restricting the number of usernames. + +Identifier limits are independent of identifier uniqueness, ownership verification, and primary designation. + +A primary identifier consumes the same capacity as a non-primary identifier. Soft-deleted identifiers do not consume active capacity, although their normalized values may remain reserved under the configured uniqueness rules. + +Limits apply consistently to self-service and administrative identifier additions. + +These rules belong to the Users application layer, keeping persistence implementations independent of application-specific identifier policies. + +See [Server Options](/docs/configuration/server-options) for configuration examples. + ### Identifier Properties The reference identifier model distinguishes several important properties: diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/client-sdk/identifiers.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/client-sdk/identifiers.json index 12fae5b6..d360d1c1 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/client-sdk/identifiers.json +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/client-sdk/identifiers.json @@ -1,7 +1,7 @@ { "Slug": "client-sdk/identifiers", "Title": "Identifiers", - "Html": "\n\u003Cp\u003EUltimateAuth treats user identifiers as independent, managed entities rather than simple fields stored directly on a user profile.\u003C/p\u003E\n\u003Cp\u003EThrough \u003Ccode\u003EUAuthClient.Identifiers\u003C/code\u003E, applications can manage usernames, email addresses, phone numbers, and custom identifiers, including primary designation, ownership verification, availability checks, and administrative operations.\u003C/p\u003E\n\u003Cp\u003EThis guide covers the public \u003Ccode\u003EIUserIdentifierClient\u003C/code\u003E API with practical examples for account settings, registration forms, and administration interfaces.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022understanding-user-identifiers\u0022\u003E1. Understanding User Identifiers\u003C/h2\u003E\n\u003Cp\u003EAn identifier represents a value associated with a user that may be used for identification, authentication, or communication, depending on the application\u0027s configuration.\u003C/p\u003E\n\u003Cp\u003ESupported identifier types are:\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 UserIdentifierType\n{\n Username = 0,\n Email = 10,\n Phone = 20,\n Custom = 100\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EA single user can have multiple 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\u003EUser\n |\n \u002B-- Username\n | johndoe [Primary]\n |\n \u002B-- Email\n | john@example.com [Primary, Verified]\n | john@business.com [Verified]\n |\n \u002B-- Phone\n \u002B1 555 0100 [Primary, Verified]\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EEach identifier has its own value, type, verification state, and primary designation.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EIdentifiers are not profiles.\u003C/strong\u003E A user profile contains information such as display name and biography, while identifiers represent managed identity-related values.\u003C/p\u003E\n\u003Cp\u003EFor general profile management, see \u003Ca href=\u0022/docs/client/user-management\u0022\u003EUser Management\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022accessing-the-identifier-client\u0022\u003E2. Accessing the Identifier Client\u003C/h2\u003E\n\u003Cp\u003EInject \u003Ccode\u003EIUAuthClient\u003C/code\u003E into a Blazor component:\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\u003EIdentifier 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.Identifiers\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis property exposes \u003Ccode\u003EIUserIdentifierClient\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe client supports three main categories of operations:\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\u003EPurpose\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ESelf-service\u003C/td\u003E\n\u003Ctd\u003EManage identifiers belonging to the current user\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAdministration\u003C/td\u003E\n\u003Ctd\u003EManage identifiers belonging to a specified user\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAvailability\u003C/td\u003E\n\u003Ctd\u003ECheck whether an identifier value is valid and available\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EAll operations are subject to server-side authorization and identifier policies.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022get-my-identifiers\u0022\u003E3. Get My Identifiers\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EGetMyAsync\u003C/code\u003E to retrieve identifiers belonging to 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\u003Evar result = await UAuthClient.Identifiers.GetMyAsync();\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } page)\n{\n foreach (var identifier in page.Items)\n {\n Console.WriteLine($\u0026quot;{identifier.Type}: {identifier.Value}\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;PagedResult\u0026lt;UserIdentifierInfo\u0026gt;\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022useridentifierinfo\u0022\u003EUserIdentifierInfo\u003C/h3\u003E\n\u003Cp\u003EEach identifier is represented by \u003Ccode\u003EUserIdentifierInfo\u003C/code\u003E.\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\u003EType\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\u003EId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EGuid\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifier record ID\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EType\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserIdentifierType\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifier type\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EValue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Estring\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifier value\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ENormalizedValue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Estring\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EServer-normalized value\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIsPrimary\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Ebool\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EWhether it is primary for its type\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIsVerified\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Ebool\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EWhether ownership has been verified\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECreatedAt\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EDateTimeOffset\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECreation timestamp\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EVerifiedAt\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EDateTimeOffset?\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EVerification timestamp\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EVersion\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Elong\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEntity version\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe \u003Ccode\u003EId\u003C/code\u003E identifies the managed identifier record and is used by update, verification, primary designation, and deletion operations.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022filter-identifiers-in-the-ui\u0022\u003EFilter Identifiers in the UI\u003C/h3\u003E\n\u003Cp\u003EFor example, display verified email addresses from a returned page:\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.Identifiers.GetMyAsync();\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } page)\n{\n var verifiedEmails = page.Items\n .Where(x =\u0026gt;\n x.Type == UserIdentifierType.Email \u0026amp;\u0026amp;\n x.IsVerified)\n .ToList();\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis filters the current page only. Retrieve additional pages when the application needs the complete collection.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022pagination\u0022\u003E4. Pagination\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EGetMyAsync\u003C/code\u003E accepts an optional \u003Ccode\u003EPageRequest\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 result = await UAuthClient.Identifiers.GetMyAsync(\n new PageRequest\n {\n PageNumber = 1,\n PageSize = 20\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe server applies its configured pagination policy.\u003C/p\u003E\n\u003Cp\u003EThe returned \u003Ccode\u003EPagedResult\u0026lt;UserIdentifierInfo\u0026gt;\u003C/code\u003E includes:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003EItems\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003ETotalCount\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EPageNumber\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EPageSize\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EHasNext\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 \u0026amp;\u0026amp; result.Value is { } page)\n{\n Console.WriteLine($\u0026quot;Total: {page.TotalCount}\u0026quot;);\n\n if (page.HasNext)\n {\n // Enable the next-page control.\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EDo not assume the first page contains every identifier.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022add-an-identifier\u0022\u003E5. Add an Identifier\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EAddMyAsync\u003C/code\u003E to associate a new identifier with the current user.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022add-an-email-address\u0022\u003EAdd an Email Address\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.Identifiers.AddMyAsync(\n new AddUserIdentifierRequest\n {\n Type = UserIdentifierType.Email,\n Value = \u0026quot;john@business.com\u0026quot;\n });\n\nif (result.IsSuccess)\n{\n // Identifier added.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022add-a-phone-number\u0022\u003EAdd a Phone Number\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.Identifiers.AddMyAsync(\n new AddUserIdentifierRequest\n {\n Type = UserIdentifierType.Phone,\n Value = \u0026quot;\u002B15550100\u0026quot;\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022add-a-username\u0022\u003EAdd a Username\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.Identifiers.AddMyAsync(\n new AddUserIdentifierRequest\n {\n Type = UserIdentifierType.Username,\n Value = \u0026quot;johndoe\u0026quot;\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022add-a-custom-identifier\u0022\u003EAdd a Custom Identifier\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.Identifiers.AddMyAsync(\n new AddUserIdentifierRequest\n {\n Type = UserIdentifierType.Custom,\n Value = \u0026quot;external-customer-123\u0026quot;\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ECustom identifier support does not automatically mean that arbitrary custom values can be used for authentication.\u003C/p\u003E\n\u003Cp\u003ELogin eligibility depends on server configuration and the relevant identifier policies.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022add-as-primary\u0022\u003EAdd as Primary\u003C/h3\u003E\n\u003Cp\u003ESet \u003Ccode\u003EIsPrimary\u003C/code\u003E when creating the identifier:\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.Identifiers.AddMyAsync(\n new AddUserIdentifierRequest\n {\n Type = UserIdentifierType.Email,\n Value = \u0026quot;john@example.com\u0026quot;,\n IsPrimary = true\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe server determines whether the new value can be assigned as primary.\u003C/p\u003E\n\u003Cp\u003EAdding an identifier does not automatically establish verified ownership.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022identifier-uniqueness-and-normalization\u0022\u003E6. Identifier Uniqueness and Normalization\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth normalizes identifier values and enforces uniqueness within the relevant tenant boundary.\u003C/p\u003E\n\u003Cp\u003EThis allows applications to work with consistent identifier representations.\u003C/p\u003E\n\u003Cp\u003EFor example, email or username normalization may affect how equivalent input values are compared.\u003C/p\u003E\n\u003Cp\u003EThe exact normalization rules depend on the identifier type and server implementation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022use-server-results-as-authoritative\u0022\u003EUse Server Results as Authoritative\u003C/h3\u003E\n\u003Cp\u003EDo not implement a separate client-side normalization algorithm and assume it exactly matches the server.\u003C/p\u003E\n\u003Cp\u003EClient-side formatting can improve the user experience, but the server must remain responsible for:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EValidating identifier format.\u003C/li\u003E\n\u003Cli\u003ENormalizing identifier values.\u003C/li\u003E\n\u003Cli\u003EEnforcing uniqueness.\u003C/li\u003E\n\u003Cli\u003EChecking tenant ownership.\u003C/li\u003E\n\u003Cli\u003EApplying identifier-specific policies.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EAn identifier value that appears available in the UI may become unavailable before the user submits it.\u003C/p\u003E\n\u003Cp\u003EAlways handle the result of the actual add or update operation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022check-identifier-availability\u0022\u003E7. Check Identifier Availability\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003ECheckAvailabilityAsync\u003C/code\u003E to check whether an identifier value is valid and available.\u003C/p\u003E\n\u003Cp\u003EThis is useful for registration forms and account settings.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022check-a-username\u0022\u003ECheck a Username\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.Identifiers.CheckAvailabilityAsync(\n new CheckUserIdentifierAvailabilityRequest\n {\n Type = UserIdentifierType.Username,\n Value = \u0026quot;johndoe\u0026quot;\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } availability)\n{\n if (!availability.IsValid)\n {\n // The identifier value is invalid.\n }\n else if (!availability.IsAvailable)\n {\n // The identifier is already unavailable.\n }\n else\n {\n // The identifier is currently available.\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022check-an-email-address\u0022\u003ECheck an Email Address\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.Identifiers.CheckAvailabilityAsync(\n new CheckUserIdentifierAvailabilityRequest\n {\n Type = UserIdentifierType.Email,\n Value = \u0026quot;john@example.com\u0026quot;\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022availability-result\u0022\u003EAvailability Result\u003C/h3\u003E\n\u003Cp\u003EThe operation 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;UserIdentifierAvailabilityResult\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe response value 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\u003EIsValid\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EWhether the value passes validation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIsAvailable\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EWhether the value is currently available\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ENormalizedValue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENormalized identifier value, when available\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EErrors\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EValidation errors\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=\u0022display-validation-feedback\u0022\u003EDisplay Validation Feedback\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.Identifiers.CheckAvailabilityAsync(\n new CheckUserIdentifierAvailabilityRequest\n {\n Type = UserIdentifierType.Username,\n Value = username\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } availability)\n{\n if (!availability.IsValid)\n {\n foreach (var error in availability.Errors)\n {\n // Display the appropriate validation feedback.\n }\n }\n else if (!availability.IsAvailable)\n {\n message = \u0026quot;This username is unavailable.\u0026quot;;\n }\n else\n {\n message = \u0026quot;This username is available.\u0026quot;;\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Cstrong\u003EAn availability check is advisory, not a reservation.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EThe value may become unavailable between the availability check and the subsequent creation operation.\u003C/p\u003E\n\u003Cp\u003EFor public registration forms, also consider whether exposing detailed availability information could enable account enumeration. Apply appropriate server-side controls.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022update-an-identifier\u0022\u003E8. Update an Identifier\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EUpdateMyAsync\u003C/code\u003E to change the value of an existing identifier.\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.Identifiers.UpdateMyAsync(\n new UpdateUserIdentifierRequest\n {\n Id = identifierId,\n NewValue = \u0026quot;newemail@example.com\u0026quot;\n });\n\nif (result.IsSuccess)\n{\n // Identifier updated.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EUpdateUserIdentifierRequest\u003C/code\u003E 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\u003EId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EExisting identifier record ID\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ENewValue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReplacement identifier value\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EUnlike profile updates, this operation targets one identifier record.\u003C/p\u003E\n\u003Cp\u003EIt does not replace the user\u0027s entire identifier collection.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022verification-after-an-update\u0022\u003EVerification After an Update\u003C/h3\u003E\n\u003Cp\u003EChanging an identifier\u0027s value may require ownership verification again.\u003C/p\u003E\n\u003Cp\u003EFor example, verification of an old email address does not prove ownership of a new email address.\u003C/p\u003E\n\u003Cp\u003EApplications should reload the identifier after updating it and inspect its current \u003Ccode\u003EIsVerified\u003C/code\u003E state.\u003C/p\u003E\n\u003Cp\u003EDo not assume the previous verification state remains valid.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022understanding-primary-identifiers\u0022\u003E9. Understanding Primary Identifiers\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth supports \u003Cstrong\u003Eone primary identifier per type\u003C/strong\u003E, rather than one global primary identifier for the entire user.\u003C/p\u003E\n\u003Cp\u003EFor example, a user may have:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EOne primary username.\u003C/li\u003E\n\u003Cli\u003EOne primary email.\u003C/li\u003E\n\u003Cli\u003EOne primary phone number.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThese primary designations are independent.\u003C/p\u003E\n\u003Cp\u003EThe primary flag identifies the preferred identifier within its type.\u003C/p\u003E\n\u003Cp\u003EHow the application uses primary identifiers for display, communication, or authentication depends on its configuration and business rules.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022set-a-primary-identifier\u0022\u003ESet a Primary Identifier\u003C/h3\u003E\n\u003Cp\u003EUse \u003Ccode\u003ESetMyPrimaryAsync\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 result = await UAuthClient.Identifiers.SetMyPrimaryAsync(\n new SetPrimaryUserIdentifierRequest\n {\n Id = identifierId\n });\n\nif (result.IsSuccess)\n{\n // Primary designation updated.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWhen a different identifier becomes primary within the same type, the server maintains the per-type primary constraint.\u003C/p\u003E\n\u003Cp\u003ESetting an email address as primary does not unset the primary phone number or username.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022unset-a-primary-identifier\u0022\u003EUnset a Primary Identifier\u003C/h3\u003E\n\u003Cp\u003EUse \u003Ccode\u003EUnsetMyPrimaryAsync\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 result = await UAuthClient.Identifiers.UnsetMyPrimaryAsync(\n new UnsetPrimaryUserIdentifierRequest\n {\n Id = identifierId\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWhether a primary identifier can be unset without selecting a replacement depends on server policy.\u003C/p\u003E\n\u003Cp\u003EApplications should handle rejection when the server requires a primary identifier of that type.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022display-primary-identifiers\u0022\u003EDisplay Primary Identifiers\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.Identifiers.GetMyAsync();\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } page)\n{\n var primaryIdentifiers = page.Items.Where(x =\u0026gt; x.IsPrimary).ToList();\n\n foreach (var identifier in primaryIdentifiers)\n {\n Console.WriteLine($\u0026quot;{identifier.Type}: {identifier.Value}\u0026quot;);\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ERemember that this example searches only the returned page.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022verify-identifier-ownership\u0022\u003E10. Verify Identifier Ownership\u003C/h2\u003E\n\u003Cp\u003EVerification is a security-sensitive operation.\u003C/p\u003E\n\u003Cp\u003EA user must not be able to mark an email address or phone number as verified merely by knowing or submitting its identifier ID.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth requires ownership proof for self-service verification.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022verify-an-identifier\u0022\u003EVerify an Identifier\u003C/h3\u003E\n\u003Cp\u003EUse \u003Ccode\u003EVerifyMyAsync\u003C/code\u003E with a proof obtained through your application\u0027s verification workflow.\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.Identifiers.VerifyMyAsync(\n new VerifyUserIdentifierRequest\n {\n Id = identifierId,\n Proof = verificationProof\n });\n\nif (result.IsSuccess)\n{\n // Identifier verification succeeded.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe \u003Ccode\u003EProof\u003C/code\u003E property is required for self-service verification.\u003C/p\u003E\n\u003Cp\u003EFor example, a verification workflow may deliver a code to the email address or phone number being verified.\u003C/p\u003E\n\u003Cp\u003EThe application then submits the received proof through \u003Ccode\u003EVerifyMyAsync\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe SDK does not itself guarantee delivery of verification messages.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022verification-flow\u0022\u003EVerification Flow\u003C/h3\u003E\n\u003Cp\u003EA typical verification workflow looks like this:\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 adds identifier\n |\n v\nApplication initiates verification\n |\n v\nVerification proof is delivered\n |\n v\nUser submits proof\n |\n v\nIdentifiers.VerifyMyAsync(...)\n |\n v\nServer validates ownership proof\n |\n \u002B-- Valid --\u0026gt; Identifier verified\n |\n \u002B-- Invalid --\u0026gt; Verification rejected\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe mechanism used to issue and deliver the proof is application-specific.\u003C/p\u003E\n\u003Cp\u003EThe server-side verification implementation must validate the proof before updating the identifier\u0027s verified state.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022server-side-verification-with-iuseridentifierverifier\u0022\u003E11. Server-Side Verification with IUserIdentifierVerifier\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth defines \u003Ccode\u003EIUserIdentifierVerifier\u003C/code\u003E for validating identifier ownership proof.\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 interface IUserIdentifierVerifier\n{\n Task\u0026lt;bool\u0026gt; VerifyAsync(UserIdentifierVerificationContext context, CancellationToken ct = default);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe verification context 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 UserIdentifierVerificationContext(\n TenantKey Tenant,\n UserKey UserKey,\n Guid IdentifierId,\n UserIdentifierType Type,\n string Value,\n string Proof);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EA secure verifier must bind the proof to:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EThe tenant.\u003C/li\u003E\n\u003Cli\u003EThe user.\u003C/li\u003E\n\u003Cli\u003EThe identifier record.\u003C/li\u003E\n\u003Cli\u003EThe current identifier value.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EIt must also enforce appropriate expiration, attempt limits, and single-use consumption.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022register-your-verifier\u0022\u003ERegister Your Verifier\u003C/h3\u003E\n\u003Cp\u003EIn the server application, register your implementation:\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\u003Ebuilder.Services.AddScoped\u0026lt;IUserIdentifierVerifier, ApplicationIdentifierVerifier\u0026gt;();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EApplicationIdentifierVerifier\u003C/code\u003E represents your own implementation of the interface.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth includes a \u003Ccode\u003ENotConfiguredUserIdentifierVerifier\u003C/code\u003E fallback that throws an explanatory exception when verification is attempted without a configured implementation.\u003C/p\u003E\n\u003Cp\u003EThis prevents an unconfigured verifier from silently approving identifier ownership.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EDo not implement a verifier that returns \u003Ccode\u003Etrue\u003C/code\u003E unconditionally in production.\u003C/strong\u003E\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022delete-an-identifier\u0022\u003E12. Delete an Identifier\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EDeleteMyAsync\u003C/code\u003E to remove an identifier.\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.Identifiers.DeleteMyAsync(\n new DeleteUserIdentifierRequest\n {\n Id = identifierId,\n Mode = DeleteMode.Soft\n });\n\nif (result.IsSuccess)\n{\n // Identifier deleted.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe request includes a deletion mode:\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 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=\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.Identifiers.DeleteMyAsync(\n new DeleteUserIdentifierRequest\n {\n Id = identifierId,\n Mode = DeleteMode.Hard\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EDeletion behavior depends on the server\u0027s identifier lifecycle and authorization rules.\u003C/p\u003E\n\u003Cp\u003EAn application should not assume that every caller can select either deletion mode.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022deleting-primary-identifiers\u0022\u003EDeleting Primary Identifiers\u003C/h3\u003E\n\u003Cp\u003EDeleting a primary identifier may be restricted.\u003C/p\u003E\n\u003Cp\u003EFor example, the server may require another identifier of the same type to become primary first.\u003C/p\u003E\n\u003Cp\u003EApplications should inspect the result and provide appropriate feedback.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022deleting-login-identifiers\u0022\u003EDeleting Login Identifiers\u003C/h3\u003E\n\u003Cp\u003ERemoving an identifier that is used for authentication may affect future login attempts.\u003C/p\u003E\n\u003Cp\u003EBefore exposing deletion, consider whether the user will retain a supported method of authentication.\u003C/p\u003E\n\u003Cp\u003EIdentifier deletion should not be treated as equivalent to revoking all existing sessions.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022build-an-identifier-management-page\u0022\u003E13. Build an Identifier Management Page\u003C/h2\u003E\n\u003Cp\u003EThe following example demonstrates a basic Blazor page that lists identifiers, adds an email address, and allows users to change primary designation.\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;My Identifiers\u0026lt;/h3\u0026gt;\n\n@if (_loading)\n{\n \u0026lt;p\u0026gt;Loading identifiers...\u0026lt;/p\u0026gt;\n}\nelse\n{\n @foreach (var item in _identifiers)\n {\n \u0026lt;div @key=\u0026quot;item.Id\u0026quot;\u0026gt;\n \u0026lt;strong\u0026gt;@item.Value\u0026lt;/strong\u0026gt;\n \u0026lt;span\u0026gt;@item.Type\u0026lt;/span\u0026gt;\n\n @if (item.IsVerified)\n {\n \u0026lt;span\u0026gt;Verified\u0026lt;/span\u0026gt;\n }\n else\n {\n \u0026lt;span\u0026gt;Not verified\u0026lt;/span\u0026gt;\n }\n\n @if (item.IsPrimary)\n {\n \u0026lt;span\u0026gt;Primary\u0026lt;/span\u0026gt;\n }\n else\n {\n \u0026lt;button type=\u0026quot;button\u0026quot; @onclick=\u0026quot;() =\u0026gt; SetPrimaryAsync(item.Id)\u0026quot;\u0026gt;Make primary\u0026lt;/button\u0026gt;\n }\n \u0026lt;/div\u0026gt;\n }\n}\n\n\u0026lt;h4\u0026gt;Add Email\u0026lt;/h4\u0026gt;\n\u0026lt;input @bind=\u0026quot;_newEmail\u0026quot; type=\u0026quot;email\u0026quot; placeholder=\u0026quot;Email address\u0026quot; /\u0026gt;\n\u0026lt;button type=\u0026quot;button\u0026quot; @onclick=\u0026quot;AddEmailAsync\u0026quot; disabled=\u0026quot;@_busy\u0026quot;\u0026gt;Add email\u0026lt;/button\u0026gt;\n\n@if (!string.IsNullOrWhiteSpace(_message))\n{\n \u0026lt;p role=\u0026quot;status\u0026quot;\u0026gt;@_message\u0026lt;/p\u0026gt;\n}\n\n@code {\n private IReadOnlyList\u0026lt;UserIdentifierInfo\u0026gt; _identifiers = [];\n\n private string _newEmail = \u0026quot;\u0026quot;;\n private string? _message;\n\n private bool _loading = true;\n private bool _busy;\n\n protected override async Task OnInitializedAsync()\n {\n await LoadAsync();\n }\n\n private async Task LoadAsync()\n {\n _loading = true;\n\n try\n {\n var result = await UAuthClient.Identifiers.GetMyAsync(\n new PageRequest\n {\n PageNumber = 1,\n PageSize = 50\n });\n\n if (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } page)\n {\n _identifiers = page.Items;\n }\n else\n {\n _message = \u0026quot;Unable to load identifiers.\u0026quot;;\n }\n }\n catch (Exception)\n {\n _message = \u0026quot;The identifier service is unavailable.\u0026quot;;\n }\n finally\n {\n _loading = false;\n }\n }\n\n private async Task AddEmailAsync()\n {\n if (_busy || string.IsNullOrWhiteSpace(_newEmail))\n return;\n\n _busy = true;\n _message = null;\n\n try\n {\n var result = await UAuthClient.Identifiers.AddMyAsync(\n new AddUserIdentifierRequest\n {\n Type = UserIdentifierType.Email,\n Value = _newEmail\n });\n\n if (result.IsSuccess)\n {\n _newEmail = \u0026quot;\u0026quot;;\n await LoadAsync();\n }\n else\n {\n _message = \u0026quot;Unable to add email.\u0026quot;;\n }\n }\n finally\n {\n _busy = false;\n }\n }\n\n private async Task SetPrimaryAsync(Guid id)\n {\n var result = await UAuthClient.Identifiers.SetMyPrimaryAsync(\n new SetPrimaryUserIdentifierRequest\n {\n Id = id\n });\n\n if (result.IsSuccess)\n {\n await LoadAsync();\n }\n else\n {\n _message = \u0026quot;Unable to update primary identifier.\u0026quot;;\n }\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis example demonstrates the core SDK integration.\u003C/p\u003E\n\u003Cp\u003EIt displays the first page of identifiers. A production implementation should add pagination, verification actions, deletion confirmation, appropriate exception handling, and loading states for all mutations.\u003C/p\u003E\n\u003Cp\u003EFor sensitive changes, consider requiring recent authentication or additional verification according to application policy.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022administrative-identifier-management\u0022\u003E14. Administrative Identifier Management\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth provides administrative methods for managing identifiers belonging to another user.\u003C/p\u003E\n\u003Cp\u003EThese operations require appropriate server-side authorization.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022get-a-users-identifiers\u0022\u003EGet a User\u0027s Identifiers\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.Identifiers.GetUserAsync(userKey,\n new PageRequest\n {\n PageNumber = 1,\n PageSize = 20\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } page)\n{\n foreach (var identifier in page.Items)\n {\n Console.WriteLine(identifier.Value);\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022add-an-identifier-1\u0022\u003EAdd an Identifier\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.Identifiers.AddUserAsync(userKey,\n new AddUserIdentifierRequest\n {\n Type = UserIdentifierType.Email,\n Value = \u0026quot;employee@example.com\u0026quot;\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022update-an-identifier-1\u0022\u003EUpdate an Identifier\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.Identifiers.UpdateUserAsync(userKey,\n new UpdateUserIdentifierRequest\n {\n Id = identifierId,\n NewValue = \u0026quot;updated@example.com\u0026quot;\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022set-primary\u0022\u003ESet Primary\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.Identifiers.SetUserPrimaryAsync(userKey,\n new SetPrimaryUserIdentifierRequest\n {\n Id = identifierId\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022unset-primary\u0022\u003EUnset Primary\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.Identifiers.UnsetUserPrimaryAsync(userKey,\n new UnsetPrimaryUserIdentifierRequest\n {\n Id = identifierId\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022verify-an-identifier-as-administrator\u0022\u003EVerify an Identifier as Administrator\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.Identifiers.VerifyUserAsync(userKey,\n new VerifyUserIdentifierRequest\n {\n Id = identifierId\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe request contract does not require \u003Ccode\u003EProof\u003C/code\u003E for authorized administrative verification.\u003C/p\u003E\n\u003Cp\u003EHowever, this does not mean any authenticated caller may bypass ownership verification.\u003C/p\u003E\n\u003Cp\u003EThe server must authorize the administrative verification operation and enforce its security requirements.\u003C/p\u003E\n\u003Cp\u003EAdministrative verification should be reserved for trusted workflows with appropriate auditability.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022delete-an-identifier-1\u0022\u003EDelete an Identifier\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.Identifiers.DeleteUserAsync(userKey,\n new DeleteUserIdentifierRequest\n {\n Id = identifierId,\n Mode = DeleteMode.Soft\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAdministrative operations act on the specified target user.\u003C/p\u003E\n\u003Cp\u003EThey do not automatically publish the same local identifier state events as self-service operations.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022handling-identifier-operation-results\u0022\u003E15. Handling Identifier Operation Results\u003C/h2\u003E\n\u003Cp\u003EMost identifier mutations return \u003Ccode\u003EUAuthResult\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003ERetrieval and availability methods return \u003Ccode\u003EUAuthResult\u0026lt;T\u0026gt;\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022handle-success-and-failure\u0022\u003EHandle Success and Failure\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.Identifiers.AddMyAsync(request);\n\nif (result.IsSuccess)\n{\n // Operation succeeded.\n}\nelse if (result.Http.IsConflict)\n{\n // Identifier conflict.\n}\nelse if (result.Http.IsForbidden)\n{\n // Operation not permitted.\n}\nelse if (result.Http.IsBadRequest)\n{\n // Invalid request.\n}\nelse\n{\n // Handle another failure.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe actual status code depends on the server\u0027s response.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthResult\u003C/code\u003E also exposes \u003Ccode\u003EProblem\u003C/code\u003E, \u003Ccode\u003EErrorText\u003C/code\u003E, \u003Ccode\u003ECorrelationId\u003C/code\u003E, and \u003Ccode\u003ETraceId\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EUse structured errors when appropriate, but avoid displaying sensitive internal information directly to users.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022identifier-state-events\u0022\u003E16. Identifier State Events\u003C/h2\u003E\n\u003Cp\u003EThe client publishes \u003Ccode\u003EIdentifiersChanged\u003C/code\u003E after successful self-service identifier mutations.\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\u003EAddMyAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdentifiersChanged\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUpdateMyAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdentifiersChanged\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESetMyPrimaryAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdentifiersChanged\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUnsetMyPrimaryAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdentifiersChanged\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EVerifyMyAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdentifiersChanged\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDeleteMyAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdentifiersChanged\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EFor add and update operations, the event carries the corresponding request payload.\u003C/p\u003E\n\u003Cp\u003EFor primary designation, verification, and deletion, the SDK publishes the event without a request payload.\u003C/p\u003E\n\u003Cp\u003EThe event handling mode follows the configured client state event options.\u003C/p\u003E\n\u003Cp\u003ERead operations and availability checks do not publish \u003Ccode\u003EIdentifiersChanged\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EAdministrative mutations also do not automatically publish this local self-service event.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022refresh-identifier-data-after-changes\u0022\u003ERefresh Identifier Data After Changes\u003C/h3\u003E\n\u003Cp\u003EA component may respond to an identifier state event by reloading its identifier list.\u003C/p\u003E\n\u003Cp\u003EAlternatively, after a successful mutation, the calling component can explicitly refresh the relevant data.\u003C/p\u003E\n\u003Cp\u003EClient events are synchronization mechanisms, not authoritative security decisions.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-considerations\u0022\u003E17. Security Considerations\u003C/h2\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022verification-requires-proof\u0022\u003EVerification Requires Proof\u003C/h3\u003E\n\u003Cp\u003ESelf-service verification must establish ownership through a valid proof.\u003C/p\u003E\n\u003Cp\u003EKnowing an identifier ID is not sufficient.\u003C/p\u003E\n\u003Cp\u003EThe server must validate proof binding, expiration, attempt limits, and single-use consumption.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022primary-does-not-mean-verified\u0022\u003EPrimary Does Not Mean Verified\u003C/h3\u003E\n\u003Cp\u003EAn identifier\u0027s primary designation and verification state are separate properties.\u003C/p\u003E\n\u003Cp\u003EApplications should not assume that a primary email or phone number has been verified.\u003C/p\u003E\n\u003Cp\u003ECheck \u003Ccode\u003EIsVerified\u003C/code\u003E where verified ownership is required.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022login-eligibility-is-separate\u0022\u003ELogin Eligibility Is Separate\u003C/h3\u003E\n\u003Cp\u003EAn identifier may exist on an account without being permitted for login.\u003C/p\u003E\n\u003Cp\u003ELogin eligibility depends on the configured authentication and identifier policies.\u003C/p\u003E\n\u003Cp\u003EDo not assume that every custom identifier is automatically an authentication identifier.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022tenant-boundaries-matter\u0022\u003ETenant Boundaries Matter\u003C/h3\u003E\n\u003Cp\u003EIdentifier uniqueness and ownership are enforced within the relevant tenant context.\u003C/p\u003E\n\u003Cp\u003EClient code must not attempt to bypass tenant isolation by modifying request paths or target user identifiers.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022availability-checks-are-not-reservations\u0022\u003EAvailability Checks Are Not Reservations\u003C/h3\u003E\n\u003Cp\u003EAvailability can change between requests.\u003C/p\u003E\n\u003Cp\u003EAlways enforce uniqueness during the actual identifier mutation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022administrative-verification-is-privileged\u0022\u003EAdministrative Verification Is Privileged\u003C/h3\u003E\n\u003Cp\u003EThe administrative verification API has different proof requirements from self-service verification.\u003C/p\u003E\n\u003Cp\u003EOnly appropriately authorized actors should be able to use it.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022identifier-changes-may-affect-authentication\u0022\u003EIdentifier Changes May Affect Authentication\u003C/h3\u003E\n\u003Cp\u003EChanging or deleting a login identifier may affect future authentication.\u003C/p\u003E\n\u003Cp\u003EApplications should distinguish identifier management from session revocation and credential management.\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/user-management\u0022\u003EUser Management\u003C/a\u003E \u2014 Manage user profiles, account status, and lifecycle.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/client/credentials\u0022\u003ECredentials\u003C/a\u003E \u2014 Manage passwords, credentials, and recovery workflows.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/client/authentication\u0022\u003EAuthentication\u003C/a\u003E \u2014 Understand login and authentication flows.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/plugin-domains/users\u0022\u003EUsers\u003C/a\u003E \u2014 Explore the user domain architecture.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/security/session-security\u0022\u003ESession Security Model\u003C/a\u003E \u2014 Understand authentication security boundaries.\u003C/li\u003E\n\u003C/ul\u003E\n", + "Html": "\n\u003Cp\u003EUltimateAuth treats user identifiers as independent, managed entities rather than simple fields stored directly on a user profile.\u003C/p\u003E\n\u003Cp\u003EThrough \u003Ccode\u003EUAuthClient.Identifiers\u003C/code\u003E, applications can manage usernames, email addresses, phone numbers, and custom identifiers, including primary designation, ownership verification, availability checks, and administrative operations.\u003C/p\u003E\n\u003Cp\u003EThis guide covers the public \u003Ccode\u003EIUserIdentifierClient\u003C/code\u003E API with practical examples for account settings, registration forms, and administration interfaces.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022understanding-user-identifiers\u0022\u003E1. Understanding User Identifiers\u003C/h2\u003E\n\u003Cp\u003EAn identifier represents a value associated with a user that may be used for identification, authentication, or communication, depending on the application\u0027s configuration.\u003C/p\u003E\n\u003Cp\u003ESupported identifier types are:\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 UserIdentifierType\n{\n Username = 0,\n Email = 10,\n Phone = 20,\n Custom = 100\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EA single user can have multiple 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\u003EUser\n |\n \u002B-- Username\n | johndoe [Primary]\n |\n \u002B-- Email\n | john@example.com [Primary, Verified]\n | john@business.com [Verified]\n |\n \u002B-- Phone\n \u002B1 555 0100 [Primary, Verified]\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EEach identifier has its own value, type, verification state, and primary designation.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EIdentifiers are not profiles.\u003C/strong\u003E A user profile contains information such as display name and biography, while identifiers represent managed identity-related values.\u003C/p\u003E\n\u003Cp\u003EFor general profile management, see \u003Ca href=\u0022/docs/client/user-management\u0022\u003EUser Management\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022accessing-the-identifier-client\u0022\u003E2. Accessing the Identifier Client\u003C/h2\u003E\n\u003Cp\u003EInject \u003Ccode\u003EIUAuthClient\u003C/code\u003E into a Blazor component:\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\u003EIdentifier 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.Identifiers\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis property exposes \u003Ccode\u003EIUserIdentifierClient\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe client supports three main categories of operations:\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\u003EPurpose\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ESelf-service\u003C/td\u003E\n\u003Ctd\u003EManage identifiers belonging to the current user\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAdministration\u003C/td\u003E\n\u003Ctd\u003EManage identifiers belonging to a specified user\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAvailability\u003C/td\u003E\n\u003Ctd\u003ECheck whether an identifier value is valid and available\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EAll operations are subject to server-side authorization and identifier policies.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022get-my-identifiers\u0022\u003E3. Get My Identifiers\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EGetMyAsync\u003C/code\u003E to retrieve identifiers belonging to 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\u003Evar result = await UAuthClient.Identifiers.GetMyAsync();\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } page)\n{\n foreach (var identifier in page.Items)\n {\n Console.WriteLine($\u0026quot;{identifier.Type}: {identifier.Value}\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;PagedResult\u0026lt;UserIdentifierInfo\u0026gt;\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022useridentifierinfo\u0022\u003EUserIdentifierInfo\u003C/h3\u003E\n\u003Cp\u003EEach identifier is represented by \u003Ccode\u003EUserIdentifierInfo\u003C/code\u003E.\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\u003EType\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\u003EId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EGuid\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifier record ID\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EType\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserIdentifierType\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifier type\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EValue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Estring\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifier value\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ENormalizedValue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Estring\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EServer-normalized value\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIsPrimary\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Ebool\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EWhether it is primary for its type\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIsVerified\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Ebool\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EWhether ownership has been verified\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECreatedAt\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EDateTimeOffset\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECreation timestamp\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EVerifiedAt\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EDateTimeOffset?\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EVerification timestamp\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EVersion\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Elong\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEntity version\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe \u003Ccode\u003EId\u003C/code\u003E identifies the managed identifier record and is used by update, verification, primary designation, and deletion operations.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022filter-identifiers-in-the-ui\u0022\u003EFilter Identifiers in the UI\u003C/h3\u003E\n\u003Cp\u003EFor example, display verified email addresses from a returned page:\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.Identifiers.GetMyAsync();\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } page)\n{\n var verifiedEmails = page.Items\n .Where(x =\u0026gt;\n x.Type == UserIdentifierType.Email \u0026amp;\u0026amp;\n x.IsVerified)\n .ToList();\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis filters the current page only. Retrieve additional pages when the application needs the complete collection.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022pagination\u0022\u003E4. Pagination\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EGetMyAsync\u003C/code\u003E accepts an optional \u003Ccode\u003EPageRequest\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 result = await UAuthClient.Identifiers.GetMyAsync(\n new PageRequest\n {\n PageNumber = 1,\n PageSize = 20\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe server applies its configured pagination policy.\u003C/p\u003E\n\u003Cp\u003EThe returned \u003Ccode\u003EPagedResult\u0026lt;UserIdentifierInfo\u0026gt;\u003C/code\u003E includes:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003EItems\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003ETotalCount\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EPageNumber\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EPageSize\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EHasNext\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 \u0026amp;\u0026amp; result.Value is { } page)\n{\n Console.WriteLine($\u0026quot;Total: {page.TotalCount}\u0026quot;);\n\n if (page.HasNext)\n {\n // Enable the next-page control.\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EDo not assume the first page contains every identifier.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022add-an-identifier\u0022\u003E5. Add an Identifier\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EAddMyAsync\u003C/code\u003E to associate a new identifier with the current user.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022add-an-email-address\u0022\u003EAdd an Email Address\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.Identifiers.AddMyAsync(\n new AddUserIdentifierRequest\n {\n Type = UserIdentifierType.Email,\n Value = \u0026quot;john@business.com\u0026quot;\n });\n\nif (result.IsSuccess)\n{\n // Identifier added.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022add-a-phone-number\u0022\u003EAdd a Phone Number\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.Identifiers.AddMyAsync(\n new AddUserIdentifierRequest\n {\n Type = UserIdentifierType.Phone,\n Value = \u0026quot;\u002B15550100\u0026quot;\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022add-a-username\u0022\u003EAdd a Username\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.Identifiers.AddMyAsync(\n new AddUserIdentifierRequest\n {\n Type = UserIdentifierType.Username,\n Value = \u0026quot;johndoe\u0026quot;\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022add-a-custom-identifier\u0022\u003EAdd a Custom Identifier\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.Identifiers.AddMyAsync(\n new AddUserIdentifierRequest\n {\n Type = UserIdentifierType.Custom,\n Value = \u0026quot;external-customer-123\u0026quot;\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ECustom identifier support does not automatically mean that arbitrary custom values can be used for authentication.\u003C/p\u003E\n\u003Cp\u003ELogin eligibility depends on server configuration and the relevant identifier policies.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022add-as-primary\u0022\u003EAdd as Primary\u003C/h3\u003E\n\u003Cp\u003ESet \u003Ccode\u003EIsPrimary\u003C/code\u003E when creating the identifier:\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.Identifiers.AddMyAsync(\n new AddUserIdentifierRequest\n {\n Type = UserIdentifierType.Email,\n Value = \u0026quot;john@example.com\u0026quot;,\n IsPrimary = true\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe server determines whether the new value can be assigned as primary.\u003C/p\u003E\n\u003Cp\u003EAdding an identifier does not automatically establish verified ownership.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022identifier-limits\u0022\u003EIdentifier Limits\u003C/h3\u003E\n\u003Cp\u003EThe server may restrict how many identifiers a user can maintain.\u003C/p\u003E\n\u003Cp\u003EBy default, UltimateAuth allows up to \u003Cstrong\u003E10 active identifiers per user\u003C/strong\u003E. Applications may configure a different global limit or optional limits for individual identifier types.\u003C/p\u003E\n\u003Cp\u003EThese limits apply to both self-service and administrative identifier operations.\u003C/p\u003E\n\u003Cp\u003EWhen adding an identifier, the server validates the configured limits before accepting the operation.\u003C/p\u003E\n\u003Cp\u003EPossible validation errors include:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EError Code\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\u003Eidentifier_total_limit_exceeded\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EThe maximum number of active identifiers has been reached\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003Eidentifier_type_limit_exceeded\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EThe maximum number of active identifiers of this type has been reached\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EApplications should display an appropriate message when an addition is rejected.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Cstrong\u003ETotal limit reached:\u003C/strong\u003E \u0026quot;You have reached the maximum number of identifiers allowed for your account.\u0026quot;\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EType limit reached:\u003C/strong\u003E \u0026quot;You have reached the maximum number of identifiers allowed for this type.\u0026quot;\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003ESoft-deleted identifiers do not count toward active limits.\u003C/p\u003E\n\u003Cp\u003EHowever, deleting an identifier does not necessarily make its value available for reuse. Identifier uniqueness and identifier capacity are separate rules.\u003C/p\u003E\n\u003Cp\u003EThe server remains authoritative when enforcing these limits. Client applications should not rely solely on local identifier counts to determine whether an addition will succeed.\u003C/p\u003E\n\u003Cp\u003EFor configuration details, see \u003Ca href=\u0022/docs/configuration/server-options\u0022\u003EServer Options\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022identifier-uniqueness-and-normalization\u0022\u003E6. Identifier Uniqueness and Normalization\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth normalizes identifier values and enforces uniqueness within the relevant tenant boundary.\u003C/p\u003E\n\u003Cp\u003EThis allows applications to work with consistent identifier representations.\u003C/p\u003E\n\u003Cp\u003EFor example, email or username normalization may affect how equivalent input values are compared.\u003C/p\u003E\n\u003Cp\u003EThe exact normalization rules depend on the identifier type and server implementation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022use-server-results-as-authoritative\u0022\u003EUse Server Results as Authoritative\u003C/h3\u003E\n\u003Cp\u003EDo not implement a separate client-side normalization algorithm and assume it exactly matches the server.\u003C/p\u003E\n\u003Cp\u003EClient-side formatting can improve the user experience, but the server must remain responsible for:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EValidating identifier format.\u003C/li\u003E\n\u003Cli\u003ENormalizing identifier values.\u003C/li\u003E\n\u003Cli\u003EEnforcing uniqueness.\u003C/li\u003E\n\u003Cli\u003EChecking tenant ownership.\u003C/li\u003E\n\u003Cli\u003EApplying identifier-specific policies.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EAn identifier value that appears available in the UI may become unavailable before the user submits it.\u003C/p\u003E\n\u003Cp\u003EAlways handle the result of the actual add or update operation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022check-identifier-availability\u0022\u003E7. Check Identifier Availability\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003ECheckAvailabilityAsync\u003C/code\u003E to check whether an identifier value is valid and available.\u003C/p\u003E\n\u003Cp\u003EThis is useful for registration forms and account settings.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022check-a-username\u0022\u003ECheck a Username\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.Identifiers.CheckAvailabilityAsync(\n new CheckUserIdentifierAvailabilityRequest\n {\n Type = UserIdentifierType.Username,\n Value = \u0026quot;johndoe\u0026quot;\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } availability)\n{\n if (!availability.IsValid)\n {\n // The identifier value is invalid.\n }\n else if (!availability.IsAvailable)\n {\n // The identifier is already unavailable.\n }\n else\n {\n // The identifier is currently available.\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022check-an-email-address\u0022\u003ECheck an Email Address\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.Identifiers.CheckAvailabilityAsync(\n new CheckUserIdentifierAvailabilityRequest\n {\n Type = UserIdentifierType.Email,\n Value = \u0026quot;john@example.com\u0026quot;\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022availability-result\u0022\u003EAvailability Result\u003C/h3\u003E\n\u003Cp\u003EThe operation 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;UserIdentifierAvailabilityResult\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe response value 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\u003EIsValid\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EWhether the value passes validation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIsAvailable\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EWhether the value is currently available\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ENormalizedValue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENormalized identifier value, when available\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EErrors\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EValidation errors\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=\u0022display-validation-feedback\u0022\u003EDisplay Validation Feedback\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.Identifiers.CheckAvailabilityAsync(\n new CheckUserIdentifierAvailabilityRequest\n {\n Type = UserIdentifierType.Username,\n Value = username\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } availability)\n{\n if (!availability.IsValid)\n {\n foreach (var error in availability.Errors)\n {\n // Display the appropriate validation feedback.\n }\n }\n else if (!availability.IsAvailable)\n {\n message = \u0026quot;This username is unavailable.\u0026quot;;\n }\n else\n {\n message = \u0026quot;This username is available.\u0026quot;;\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Cstrong\u003EAn availability check is advisory, not a reservation.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EThe value may become unavailable between the availability check and the subsequent creation operation.\u003C/p\u003E\n\u003Cp\u003EFor public registration forms, also consider whether exposing detailed availability information could enable account enumeration. Apply appropriate server-side controls.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022update-an-identifier\u0022\u003E8. Update an Identifier\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EUpdateMyAsync\u003C/code\u003E to change the value of an existing identifier.\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.Identifiers.UpdateMyAsync(\n new UpdateUserIdentifierRequest\n {\n Id = identifierId,\n NewValue = \u0026quot;newemail@example.com\u0026quot;\n });\n\nif (result.IsSuccess)\n{\n // Identifier updated.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EUpdateUserIdentifierRequest\u003C/code\u003E 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\u003EId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EExisting identifier record ID\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ENewValue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReplacement identifier value\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EUnlike profile updates, this operation targets one identifier record.\u003C/p\u003E\n\u003Cp\u003EIt does not replace the user\u0027s entire identifier collection.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022verification-after-an-update\u0022\u003EVerification After an Update\u003C/h3\u003E\n\u003Cp\u003EChanging an identifier\u0027s value may require ownership verification again.\u003C/p\u003E\n\u003Cp\u003EFor example, verification of an old email address does not prove ownership of a new email address.\u003C/p\u003E\n\u003Cp\u003EApplications should reload the identifier after updating it and inspect its current \u003Ccode\u003EIsVerified\u003C/code\u003E state.\u003C/p\u003E\n\u003Cp\u003EDo not assume the previous verification state remains valid.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022understanding-primary-identifiers\u0022\u003E9. Understanding Primary Identifiers\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth supports \u003Cstrong\u003Eone primary identifier per type\u003C/strong\u003E, rather than one global primary identifier for the entire user.\u003C/p\u003E\n\u003Cp\u003EFor example, a user may have:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EOne primary username.\u003C/li\u003E\n\u003Cli\u003EOne primary email.\u003C/li\u003E\n\u003Cli\u003EOne primary phone number.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThese primary designations are independent.\u003C/p\u003E\n\u003Cp\u003EThe primary flag identifies the preferred identifier within its type.\u003C/p\u003E\n\u003Cp\u003EHow the application uses primary identifiers for display, communication, or authentication depends on its configuration and business rules.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022set-a-primary-identifier\u0022\u003ESet a Primary Identifier\u003C/h3\u003E\n\u003Cp\u003EUse \u003Ccode\u003ESetMyPrimaryAsync\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 result = await UAuthClient.Identifiers.SetMyPrimaryAsync(\n new SetPrimaryUserIdentifierRequest\n {\n Id = identifierId\n });\n\nif (result.IsSuccess)\n{\n // Primary designation updated.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWhen a different identifier becomes primary within the same type, the server maintains the per-type primary constraint.\u003C/p\u003E\n\u003Cp\u003ESetting an email address as primary does not unset the primary phone number or username.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022unset-a-primary-identifier\u0022\u003EUnset a Primary Identifier\u003C/h3\u003E\n\u003Cp\u003EUse \u003Ccode\u003EUnsetMyPrimaryAsync\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 result = await UAuthClient.Identifiers.UnsetMyPrimaryAsync(\n new UnsetPrimaryUserIdentifierRequest\n {\n Id = identifierId\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWhether a primary identifier can be unset without selecting a replacement depends on server policy.\u003C/p\u003E\n\u003Cp\u003EApplications should handle rejection when the server requires a primary identifier of that type.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022display-primary-identifiers\u0022\u003EDisplay Primary Identifiers\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.Identifiers.GetMyAsync();\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } page)\n{\n var primaryIdentifiers = page.Items.Where(x =\u0026gt; x.IsPrimary).ToList();\n\n foreach (var identifier in primaryIdentifiers)\n {\n Console.WriteLine($\u0026quot;{identifier.Type}: {identifier.Value}\u0026quot;);\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ERemember that this example searches only the returned page.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022verify-identifier-ownership\u0022\u003E10. Verify Identifier Ownership\u003C/h2\u003E\n\u003Cp\u003EVerification is a security-sensitive operation.\u003C/p\u003E\n\u003Cp\u003EA user must not be able to mark an email address or phone number as verified merely by knowing or submitting its identifier ID.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth requires ownership proof for self-service verification.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022verify-an-identifier\u0022\u003EVerify an Identifier\u003C/h3\u003E\n\u003Cp\u003EUse \u003Ccode\u003EVerifyMyAsync\u003C/code\u003E with a proof obtained through your application\u0027s verification workflow.\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.Identifiers.VerifyMyAsync(\n new VerifyUserIdentifierRequest\n {\n Id = identifierId,\n Proof = verificationProof\n });\n\nif (result.IsSuccess)\n{\n // Identifier verification succeeded.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe \u003Ccode\u003EProof\u003C/code\u003E property is required for self-service verification.\u003C/p\u003E\n\u003Cp\u003EFor example, a verification workflow may deliver a code to the email address or phone number being verified.\u003C/p\u003E\n\u003Cp\u003EThe application then submits the received proof through \u003Ccode\u003EVerifyMyAsync\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe SDK does not itself guarantee delivery of verification messages.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022verification-flow\u0022\u003EVerification Flow\u003C/h3\u003E\n\u003Cp\u003EA typical verification workflow looks like this:\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 adds identifier\n |\n v\nApplication initiates verification\n |\n v\nVerification proof is delivered\n |\n v\nUser submits proof\n |\n v\nIdentifiers.VerifyMyAsync(...)\n |\n v\nServer validates ownership proof\n |\n \u002B-- Valid --\u0026gt; Identifier verified\n |\n \u002B-- Invalid --\u0026gt; Verification rejected\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe mechanism used to issue and deliver the proof is application-specific.\u003C/p\u003E\n\u003Cp\u003EThe server-side verification implementation must validate the proof before updating the identifier\u0027s verified state.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022server-side-verification-with-iuseridentifierverifier\u0022\u003E11. Server-Side Verification with IUserIdentifierVerifier\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth defines \u003Ccode\u003EIUserIdentifierVerifier\u003C/code\u003E for validating identifier ownership proof.\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 interface IUserIdentifierVerifier\n{\n Task\u0026lt;bool\u0026gt; VerifyAsync(UserIdentifierVerificationContext context, CancellationToken ct = default);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe verification context 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 UserIdentifierVerificationContext(\n TenantKey Tenant,\n UserKey UserKey,\n Guid IdentifierId,\n UserIdentifierType Type,\n string Value,\n string Proof);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EA secure verifier must bind the proof to:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EThe tenant.\u003C/li\u003E\n\u003Cli\u003EThe user.\u003C/li\u003E\n\u003Cli\u003EThe identifier record.\u003C/li\u003E\n\u003Cli\u003EThe current identifier value.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EIt must also enforce appropriate expiration, attempt limits, and single-use consumption.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022register-your-verifier\u0022\u003ERegister Your Verifier\u003C/h3\u003E\n\u003Cp\u003EIn the server application, register your implementation:\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\u003Ebuilder.Services.AddScoped\u0026lt;IUserIdentifierVerifier, ApplicationIdentifierVerifier\u0026gt;();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EApplicationIdentifierVerifier\u003C/code\u003E represents your own implementation of the interface.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth includes a \u003Ccode\u003ENotConfiguredUserIdentifierVerifier\u003C/code\u003E fallback that throws an explanatory exception when verification is attempted without a configured implementation.\u003C/p\u003E\n\u003Cp\u003EThis prevents an unconfigured verifier from silently approving identifier ownership.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EDo not implement a verifier that returns \u003Ccode\u003Etrue\u003C/code\u003E unconditionally in production.\u003C/strong\u003E\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022delete-an-identifier\u0022\u003E12. Delete an Identifier\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EDeleteMyAsync\u003C/code\u003E to remove an identifier.\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.Identifiers.DeleteMyAsync(\n new DeleteUserIdentifierRequest\n {\n Id = identifierId,\n Mode = DeleteMode.Soft\n });\n\nif (result.IsSuccess)\n{\n // Identifier deleted.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe request includes a deletion mode:\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 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=\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.Identifiers.DeleteMyAsync(\n new DeleteUserIdentifierRequest\n {\n Id = identifierId,\n Mode = DeleteMode.Hard\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EDeletion behavior depends on the server\u0027s identifier lifecycle and authorization rules.\u003C/p\u003E\n\u003Cp\u003EAn application should not assume that every caller can select either deletion mode.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022deleting-primary-identifiers\u0022\u003EDeleting Primary Identifiers\u003C/h3\u003E\n\u003Cp\u003EDeleting a primary identifier may be restricted.\u003C/p\u003E\n\u003Cp\u003EFor example, the server may require another identifier of the same type to become primary first.\u003C/p\u003E\n\u003Cp\u003EApplications should inspect the result and provide appropriate feedback.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022deleting-login-identifiers\u0022\u003EDeleting Login Identifiers\u003C/h3\u003E\n\u003Cp\u003ERemoving an identifier that is used for authentication may affect future login attempts.\u003C/p\u003E\n\u003Cp\u003EBefore exposing deletion, consider whether the user will retain a supported method of authentication.\u003C/p\u003E\n\u003Cp\u003EIdentifier deletion should not be treated as equivalent to revoking all existing sessions.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022build-an-identifier-management-page\u0022\u003E13. Build an Identifier Management Page\u003C/h2\u003E\n\u003Cp\u003EThe following example demonstrates a basic Blazor page that lists identifiers, adds an email address, and allows users to change primary designation.\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;My Identifiers\u0026lt;/h3\u0026gt;\n\n@if (_loading)\n{\n \u0026lt;p\u0026gt;Loading identifiers...\u0026lt;/p\u0026gt;\n}\nelse\n{\n @foreach (var item in _identifiers)\n {\n \u0026lt;div @key=\u0026quot;item.Id\u0026quot;\u0026gt;\n \u0026lt;strong\u0026gt;@item.Value\u0026lt;/strong\u0026gt;\n \u0026lt;span\u0026gt;@item.Type\u0026lt;/span\u0026gt;\n\n @if (item.IsVerified)\n {\n \u0026lt;span\u0026gt;Verified\u0026lt;/span\u0026gt;\n }\n else\n {\n \u0026lt;span\u0026gt;Not verified\u0026lt;/span\u0026gt;\n }\n\n @if (item.IsPrimary)\n {\n \u0026lt;span\u0026gt;Primary\u0026lt;/span\u0026gt;\n }\n else\n {\n \u0026lt;button type=\u0026quot;button\u0026quot; @onclick=\u0026quot;() =\u0026gt; SetPrimaryAsync(item.Id)\u0026quot;\u0026gt;Make primary\u0026lt;/button\u0026gt;\n }\n \u0026lt;/div\u0026gt;\n }\n}\n\n\u0026lt;h4\u0026gt;Add Email\u0026lt;/h4\u0026gt;\n\u0026lt;input @bind=\u0026quot;_newEmail\u0026quot; type=\u0026quot;email\u0026quot; placeholder=\u0026quot;Email address\u0026quot; /\u0026gt;\n\u0026lt;button type=\u0026quot;button\u0026quot; @onclick=\u0026quot;AddEmailAsync\u0026quot; disabled=\u0026quot;@_busy\u0026quot;\u0026gt;Add email\u0026lt;/button\u0026gt;\n\n@if (!string.IsNullOrWhiteSpace(_message))\n{\n \u0026lt;p role=\u0026quot;status\u0026quot;\u0026gt;@_message\u0026lt;/p\u0026gt;\n}\n\n@code {\n private IReadOnlyList\u0026lt;UserIdentifierInfo\u0026gt; _identifiers = [];\n\n private string _newEmail = \u0026quot;\u0026quot;;\n private string? _message;\n\n private bool _loading = true;\n private bool _busy;\n\n protected override async Task OnInitializedAsync()\n {\n await LoadAsync();\n }\n\n private async Task LoadAsync()\n {\n _loading = true;\n\n try\n {\n var result = await UAuthClient.Identifiers.GetMyAsync(\n new PageRequest\n {\n PageNumber = 1,\n PageSize = 50\n });\n\n if (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } page)\n {\n _identifiers = page.Items;\n }\n else\n {\n _message = \u0026quot;Unable to load identifiers.\u0026quot;;\n }\n }\n catch (Exception)\n {\n _message = \u0026quot;The identifier service is unavailable.\u0026quot;;\n }\n finally\n {\n _loading = false;\n }\n }\n\n private async Task AddEmailAsync()\n {\n if (_busy || string.IsNullOrWhiteSpace(_newEmail))\n return;\n\n _busy = true;\n _message = null;\n\n try\n {\n var result = await UAuthClient.Identifiers.AddMyAsync(\n new AddUserIdentifierRequest\n {\n Type = UserIdentifierType.Email,\n Value = _newEmail\n });\n\n if (result.IsSuccess)\n {\n _newEmail = \u0026quot;\u0026quot;;\n await LoadAsync();\n }\n else\n {\n _message = \u0026quot;Unable to add email.\u0026quot;;\n }\n }\n finally\n {\n _busy = false;\n }\n }\n\n private async Task SetPrimaryAsync(Guid id)\n {\n var result = await UAuthClient.Identifiers.SetMyPrimaryAsync(\n new SetPrimaryUserIdentifierRequest\n {\n Id = id\n });\n\n if (result.IsSuccess)\n {\n await LoadAsync();\n }\n else\n {\n _message = \u0026quot;Unable to update primary identifier.\u0026quot;;\n }\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis example demonstrates the core SDK integration.\u003C/p\u003E\n\u003Cp\u003EIt displays the first page of identifiers. A production implementation should add pagination, verification actions, deletion confirmation, appropriate exception handling, and loading states for all mutations.\u003C/p\u003E\n\u003Cp\u003EFor sensitive changes, consider requiring recent authentication or additional verification according to application policy.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022administrative-identifier-management\u0022\u003E14. Administrative Identifier Management\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth provides administrative methods for managing identifiers belonging to another user.\u003C/p\u003E\n\u003Cp\u003EThese operations require appropriate server-side authorization.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022get-a-users-identifiers\u0022\u003EGet a User\u0027s Identifiers\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.Identifiers.GetUserAsync(userKey,\n new PageRequest\n {\n PageNumber = 1,\n PageSize = 20\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } page)\n{\n foreach (var identifier in page.Items)\n {\n Console.WriteLine(identifier.Value);\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022add-an-identifier-1\u0022\u003EAdd an Identifier\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.Identifiers.AddUserAsync(userKey,\n new AddUserIdentifierRequest\n {\n Type = UserIdentifierType.Email,\n Value = \u0026quot;employee@example.com\u0026quot;\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022update-an-identifier-1\u0022\u003EUpdate an Identifier\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.Identifiers.UpdateUserAsync(userKey,\n new UpdateUserIdentifierRequest\n {\n Id = identifierId,\n NewValue = \u0026quot;updated@example.com\u0026quot;\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022set-primary\u0022\u003ESet Primary\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.Identifiers.SetUserPrimaryAsync(userKey,\n new SetPrimaryUserIdentifierRequest\n {\n Id = identifierId\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022unset-primary\u0022\u003EUnset Primary\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.Identifiers.UnsetUserPrimaryAsync(userKey,\n new UnsetPrimaryUserIdentifierRequest\n {\n Id = identifierId\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022verify-an-identifier-as-administrator\u0022\u003EVerify an Identifier as Administrator\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.Identifiers.VerifyUserAsync(userKey,\n new VerifyUserIdentifierRequest\n {\n Id = identifierId\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe request contract does not require \u003Ccode\u003EProof\u003C/code\u003E for authorized administrative verification.\u003C/p\u003E\n\u003Cp\u003EHowever, this does not mean any authenticated caller may bypass ownership verification.\u003C/p\u003E\n\u003Cp\u003EThe server must authorize the administrative verification operation and enforce its security requirements.\u003C/p\u003E\n\u003Cp\u003EAdministrative verification should be reserved for trusted workflows with appropriate auditability.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022delete-an-identifier-1\u0022\u003EDelete an Identifier\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.Identifiers.DeleteUserAsync(userKey,\n new DeleteUserIdentifierRequest\n {\n Id = identifierId,\n Mode = DeleteMode.Soft\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAdministrative operations act on the specified target user.\u003C/p\u003E\n\u003Cp\u003EThey do not automatically publish the same local identifier state events as self-service operations.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022handling-identifier-operation-results\u0022\u003E15. Handling Identifier Operation Results\u003C/h2\u003E\n\u003Cp\u003EMost identifier mutations return \u003Ccode\u003EUAuthResult\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003ERetrieval and availability methods return \u003Ccode\u003EUAuthResult\u0026lt;T\u0026gt;\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022handle-success-and-failure\u0022\u003EHandle Success and Failure\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.Identifiers.AddMyAsync(request);\n\nif (result.IsSuccess)\n{\n // Operation succeeded.\n}\nelse if (result.Http.IsConflict)\n{\n // Identifier conflict.\n}\nelse if (result.Http.IsForbidden)\n{\n // Operation not permitted.\n}\nelse if (result.Http.IsBadRequest)\n{\n // Invalid request.\n}\nelse\n{\n // Handle another failure.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe actual status code depends on the server\u0027s response.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthResult\u003C/code\u003E also exposes \u003Ccode\u003EProblem\u003C/code\u003E, \u003Ccode\u003EErrorText\u003C/code\u003E, \u003Ccode\u003ECorrelationId\u003C/code\u003E, and \u003Ccode\u003ETraceId\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EUse structured errors when appropriate, but avoid displaying sensitive internal information directly to users.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022identifier-state-events\u0022\u003E16. Identifier State Events\u003C/h2\u003E\n\u003Cp\u003EThe client publishes \u003Ccode\u003EIdentifiersChanged\u003C/code\u003E after successful self-service identifier mutations.\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\u003EAddMyAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdentifiersChanged\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUpdateMyAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdentifiersChanged\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESetMyPrimaryAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdentifiersChanged\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUnsetMyPrimaryAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdentifiersChanged\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EVerifyMyAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdentifiersChanged\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDeleteMyAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdentifiersChanged\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EFor add and update operations, the event carries the corresponding request payload.\u003C/p\u003E\n\u003Cp\u003EFor primary designation, verification, and deletion, the SDK publishes the event without a request payload.\u003C/p\u003E\n\u003Cp\u003EThe event handling mode follows the configured client state event options.\u003C/p\u003E\n\u003Cp\u003ERead operations and availability checks do not publish \u003Ccode\u003EIdentifiersChanged\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EAdministrative mutations also do not automatically publish this local self-service event.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022refresh-identifier-data-after-changes\u0022\u003ERefresh Identifier Data After Changes\u003C/h3\u003E\n\u003Cp\u003EA component may respond to an identifier state event by reloading its identifier list.\u003C/p\u003E\n\u003Cp\u003EAlternatively, after a successful mutation, the calling component can explicitly refresh the relevant data.\u003C/p\u003E\n\u003Cp\u003EClient events are synchronization mechanisms, not authoritative security decisions.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-considerations\u0022\u003E17. Security Considerations\u003C/h2\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022verification-requires-proof\u0022\u003EVerification Requires Proof\u003C/h3\u003E\n\u003Cp\u003ESelf-service verification must establish ownership through a valid proof.\u003C/p\u003E\n\u003Cp\u003EKnowing an identifier ID is not sufficient.\u003C/p\u003E\n\u003Cp\u003EThe server must validate proof binding, expiration, attempt limits, and single-use consumption.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022primary-does-not-mean-verified\u0022\u003EPrimary Does Not Mean Verified\u003C/h3\u003E\n\u003Cp\u003EAn identifier\u0027s primary designation and verification state are separate properties.\u003C/p\u003E\n\u003Cp\u003EApplications should not assume that a primary email or phone number has been verified.\u003C/p\u003E\n\u003Cp\u003ECheck \u003Ccode\u003EIsVerified\u003C/code\u003E where verified ownership is required.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022login-eligibility-is-separate\u0022\u003ELogin Eligibility Is Separate\u003C/h3\u003E\n\u003Cp\u003EAn identifier may exist on an account without being permitted for login.\u003C/p\u003E\n\u003Cp\u003ELogin eligibility depends on the configured authentication and identifier policies.\u003C/p\u003E\n\u003Cp\u003EDo not assume that every custom identifier is automatically an authentication identifier.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022tenant-boundaries-matter\u0022\u003ETenant Boundaries Matter\u003C/h3\u003E\n\u003Cp\u003EIdentifier uniqueness and ownership are enforced within the relevant tenant context.\u003C/p\u003E\n\u003Cp\u003EClient code must not attempt to bypass tenant isolation by modifying request paths or target user identifiers.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022availability-checks-are-not-reservations\u0022\u003EAvailability Checks Are Not Reservations\u003C/h3\u003E\n\u003Cp\u003EAvailability can change between requests.\u003C/p\u003E\n\u003Cp\u003EAlways enforce uniqueness during the actual identifier mutation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022administrative-verification-is-privileged\u0022\u003EAdministrative Verification Is Privileged\u003C/h3\u003E\n\u003Cp\u003EThe administrative verification API has different proof requirements from self-service verification.\u003C/p\u003E\n\u003Cp\u003EOnly appropriately authorized actors should be able to use it.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022identifier-changes-may-affect-authentication\u0022\u003EIdentifier Changes May Affect Authentication\u003C/h3\u003E\n\u003Cp\u003EChanging or deleting a login identifier may affect future authentication.\u003C/p\u003E\n\u003Cp\u003EApplications should distinguish identifier management from session revocation and credential management.\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/user-management\u0022\u003EUser Management\u003C/a\u003E \u2014 Manage user profiles, account status, and lifecycle.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/client/credentials\u0022\u003ECredentials\u003C/a\u003E \u2014 Manage passwords, credentials, and recovery workflows.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/client/authentication\u0022\u003EAuthentication\u003C/a\u003E \u2014 Understand login and authentication flows.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/plugin-domains/users\u0022\u003EUsers\u003C/a\u003E \u2014 Explore the user domain architecture.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/security/session-security\u0022\u003ESession Security Model\u003C/a\u003E \u2014 Understand authentication security boundaries.\u003C/li\u003E\n\u003C/ul\u003E\n", "Headings": [ { "Id": "understanding-user-identifiers", @@ -63,6 +63,11 @@ "Text": "Add as Primary", "Level": 1 }, + { + "Id": "identifier-limits", + "Text": "Identifier Limits", + "Level": 1 + }, { "Id": "identifier-uniqueness-and-normalization", "Text": "6. Identifier Uniqueness and Normalization", @@ -284,6 +289,6 @@ "Level": 0 } ], - "SourceHash": "8E4DEB0C994EC32D3379CC565FF9A18B7A30AAC11FBE6C2668F2CBC0D1DC0226", + "SourceHash": "4F102EDD321F0410AEDF49D69BE34B330E136108A23527A994CE2EA152EF8356", "BuilderVersion": "1" } \ No newline at end of file diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/configuration/server-options.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/configuration/server-options.json index 399d4a59..a5f73fbd 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/configuration/server-options.json +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/configuration/server-options.json @@ -1,7 +1,7 @@ { "Slug": "configuration/server-options", "Title": "Server Options", - "Html": "\n\u003Cp\u003E\u003Ccode\u003EUAuthServerOptions\u003C/code\u003E is the primary configuration surface for UltimateAuth Server.\u003C/p\u003E\n\u003Cp\u003EIt controls authentication behavior, session lifecycles, token issuance, credential security, tenant resolution, endpoint exposure, and integration with UAuthHub.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth composes its Core-defined options directly into \u003Ccode\u003EUAuthServerOptions\u003C/code\u003E. When using UltimateAuth Server, you configure both shared authentication behavior and server-specific features through the same options object.\u003C/p\u003E\n\u003Cp\u003EThis guide covers the available configuration groups, their defaults, practical examples, and the distinction between configured and effective runtime behavior.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022register-ultimateauth-server\u0022\u003E1. Register UltimateAuth Server\u003C/h2\u003E\n\u003Cp\u003EConfigure the server through \u003Ccode\u003EAddUltimateAuthServer()\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Login.MaxFailedAttempts = 5;\n options.Session.Lifetime = TimeSpan.FromDays(7);\n options.Token.AccessTokenLifetime = TimeSpan.FromMinutes(10);\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe configuration delegate receives:\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\u003EUAuthServerOptions\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe server registration also supports an optional policy configuration delegate.\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\u003Ebuilder.Services.AddUltimateAuthServer(\n configure: options =\u0026gt;\n {\n options.Login.MaxFailedAttempts = 5;\n },\n configurePolicies: policies =\u0026gt;\n {\n // Register application access policies.\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe server automatically registers its Core infrastructure. You do not need to call \u003Ccode\u003EAddUltimateAuth()\u003C/code\u003E separately.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configuration-from-appsettings.json\u0022\u003EConfiguration from appsettings.json\u003C/h3\u003E\n\u003Cp\u003EUltimateAuth Server binds the configuration section:\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\u003EUltimateAuth:Server\n\u003C/code\u003E\u003C/pre\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\u003EJSON\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-json\u0022\u003E{\n \u0026quot;UltimateAuth\u0026quot;: {\n \u0026quot;Server\u0026quot;: {\n \u0026quot;Login\u0026quot;: {\n \u0026quot;MaxFailedAttempts\u0026quot;: 5,\n \u0026quot;LockoutDuration\u0026quot;: \u0026quot;00:15:00\u0026quot;\n },\n \u0026quot;Session\u0026quot;: {\n \u0026quot;Lifetime\u0026quot;: \u0026quot;7.00:00:00\u0026quot;\n },\n \u0026quot;Token\u0026quot;: {\n \u0026quot;AccessTokenLifetime\u0026quot;: \u0026quot;00:10:00\u0026quot;\n }\n }\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EASP.NET Core represents \u003Ccode\u003ETimeSpan\u003C/code\u003E values using strings such as \u003Ccode\u003E00:15:00\u003C/code\u003E for fifteen minutes and \u003Ccode\u003E7.00:00:00\u003C/code\u003E for seven days.\u003C/p\u003E\n\u003Cp\u003EThe configuration section is bound after the registration delegate, so configuration provider values take precedence for matching properties.\u003C/p\u003E\n\u003Cp\u003EFor configuration source ordering and environment-specific overrides, see \u003Ca href=\u0022/docs/configuration/configuration\u0022\u003EConfiguration Overview\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022server-options-architecture\u0022\u003E2. Server Options Architecture\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthServerOptions\u003C/code\u003E contains two categories of configuration.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022core-defined-options\u0022\u003ECore-defined options\u003C/h3\u003E\n\u003Cp\u003EThese types are defined in UltimateAuth Core and composed into the server options:\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\u003EType\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\u003ELogin\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthLoginOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELogin attempts and lockout behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESession\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthSessionOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESession lifetime and lifecycle\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EToken\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthTokenOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAccess and refresh token behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPkce\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthPkceOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPKCE authorization code lifetime\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMultiTenant\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthMultiTenantOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETenant identification and resolution\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPagination\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthPaginationOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPagination defaults and limits\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEvents\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthEvents\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication lifecycle callbacks\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=\u0022server-specific-options\u0022\u003EServer-specific options\u003C/h3\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\u003EAllowedModes\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERestrict permitted authentication modes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EHubDeploymentMode\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EDescribe the relationship between UAuthHub and the application\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EHub\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EHub integration and flow settings\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECookie\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECookie names, security attributes, and lifetime behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESessionResolution\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESession identifier sources and precedence\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEndpoints\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEndpoint groups and disabled actions\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPrimaryCredential\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPrimary credential selection for UI and API requests\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAuthResponse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication credential delivery and redirect behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECredentialSessionPolicy\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESession invalidation after credential changes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EResetCredential\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECredential recovery code and token settings\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdentifiers\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifier behavior, normalization, and uniqueness\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdentifierValidation\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUsername, email, and phone validation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELoginIdentifiers\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifier types allowed for login\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserProfile\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUser profile capabilities\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ENavigation\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication navigation behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDiagnostics\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional diagnostic response details\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EAdvanced configuration hooks are also available, including \u003Ccode\u003EConfigureMode()\u003C/code\u003E and \u003Ccode\u003EOnConfigureEndpoints\u003C/code\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022login-options\u0022\u003E3. Login Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Login\u003C/code\u003E controls login attempt limits, lockout behavior, and related security settings.\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Login.MaxFailedAttempts = 5;\n options.Login.LockoutDuration = TimeSpan.FromMinutes(15);\n options.Login.FailureWindow = TimeSpan.FromMinutes(15);\n options.Login.ExtendLockOnFailure = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-properties\u0022\u003EAvailable Properties\u003C/h3\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\u003EDefault\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\u003EMaxFailedAttempts\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E10\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EFailed login threshold; \u003Ccode\u003E0\u003C/code\u003E disables lockout\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELockoutDuration\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E15 minutes\u003C/td\u003E\n\u003Ctd\u003ELockout duration\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EFailureWindow\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E15 minutes\u003C/td\u003E\n\u003Ctd\u003EWindow for counting failed attempts\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EExtendLockOnFailure\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EExtend lockout when another attempt fails during lockout\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIncludeFailureDetails\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EInclude detailed authentication failure information\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETryLoginDuration\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E180 seconds\u003C/td\u003E\n\u003Ctd\u003EDuration associated with the try-login flow\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=\u0022example-restrict-login-failure-details\u0022\u003EExample: Restrict Login Failure Details\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Login.IncludeFailureDetails = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis setting is useful when an application does not want to expose detailed lockout information to callers.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022important\u0022\u003EImportant\u003C/h3\u003E\n\u003Cp\u003ELockout configuration is not equivalent to complete network-level rate limiting.\u003C/p\u003E\n\u003Cp\u003EProduction deployments should also consider infrastructure protections against abusive traffic and distributed login attempts.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022session-options\u0022\u003E4. Session Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Session\u003C/code\u003E controls session lifetime, expiration, sliding behavior, and session-chain limits.\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Session.Lifetime = TimeSpan.FromDays(7);\n options.Session.SlidingExpiration = true;\n options.Session.IdleTimeout = TimeSpan.FromDays(3);\n options.Session.MaxLifetime = TimeSpan.FromDays(30);\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022session-lifetime-properties\u0022\u003ESession Lifetime Properties\u003C/h3\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\u003EDefault\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\u003ELifetime\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E7 days\u003C/td\u003E\n\u003Ctd\u003EStandard session lifetime\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxLifetime\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional absolute lifetime limit\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESlidingExpiration\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAllow eligible activity or refresh operations to extend expiration\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdleTimeout\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional inactivity timeout\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETouchInterval\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E5 minutes\u003C/td\u003E\n\u003Ctd\u003EMinimum interval between last-seen updates\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThese settings have different purposes.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003ELifetime\u003C/code\u003E determines the standard lifetime assigned to a session.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EMaxLifetime\u003C/code\u003E establishes an optional hard boundary that cannot be extended indefinitely.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EIdleTimeout\u003C/code\u003E limits how long a session can remain inactive.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003ETouchInterval\u003C/code\u003E reduces unnecessary persistence operations caused by frequent activity updates.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022session-chain-properties\u0022\u003ESession-Chain Properties\u003C/h3\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\u003EDefault\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\u003EMaxChainsPerUser\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E0\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReserved user-level chain limit\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxSessionsPerChain\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E100\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESession-chain rotation limit\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxChainsPerPlatform\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReserved platform-specific chain limits\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPlatformCategories\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReserved platform group definitions\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxChainsPerCategory\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReserved category-specific chain limits\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableIpBinding\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReserved IP binding setting\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableUserAgentBinding\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReserved User-Agent binding setting\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDeviceMismatchBehavior\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EReject\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReserved device mismatch behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Cstrong\u003ECurrent implementation limitation:\u003C/strong\u003E The source identifies several device limits and binding settings as reserved for future enforcement. Setting these properties must not be treated as proof that the corresponding protection is active.\u003C/p\u003E\n\u003Cp\u003EFor example, do not rely on:\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\u003Eoptions.Session.MaxChainsPerUser = 1;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Eas a production guarantee that only one device chain can exist unless the current runtime implementation has been verified to enforce that restriction.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022mode-specific-session-defaults\u0022\u003EMode-Specific Session Defaults\u003C/h3\u003E\n\u003Cp\u003ESession settings can also be affected by authentication mode defaults.\u003C/p\u003E\n\u003Cp\u003EFor example, \u003Ccode\u003EPureOpaque\u003C/code\u003E supplies a longer idle timeout and touch interval when those values have not been configured.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EHybrid\u003C/code\u003E explicitly sets \u003Ccode\u003ETouchInterval\u003C/code\u003E to \u003Ccode\u003Enull\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003ESee \u003Ca href=\u0022/docs/configuration/advanced-configuration\u0022\u003EAdvanced Configuration\u003C/a\u003E for the effective-options resolution order.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022token-options\u0022\u003E5. Token Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Token\u003C/code\u003E configures access and refresh token behavior.\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Token.Issuer = \u0026quot;MyApplication\u0026quot;;\n options.Token.Audience = \u0026quot;MyApplicationApi\u0026quot;;\n options.Token.AccessTokenLifetime = TimeSpan.FromMinutes(10);\n options.Token.RefreshTokenLifetime = TimeSpan.FromDays(7);\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-properties-1\u0022\u003EAvailable Properties\u003C/h3\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\u003EDefault\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\u003EIssueJwt\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEnable JWT issuance where supported by the effective mode\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIssueOpaque\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEnable opaque token issuance where supported\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIssueRefresh\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEnable refresh token issuance where supported\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAccessTokenLifetime\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E10 minutes\u003C/td\u003E\n\u003Ctd\u003EAccess token lifetime\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERefreshTokenLifetime\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E7 days\u003C/td\u003E\n\u003Ctd\u003ERefresh token lifetime\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EOpaqueIdBytes\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E32\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERandom byte length used for opaque token identifiers\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERefreshTokenConcurrentRequestWindow\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E2 seconds\u003C/td\u003E\n\u003Ctd\u003EConcurrent duplicate refresh classification window\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIssuer\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuth\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EJWT issuer\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAudience\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthClient\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EJWT audience\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAddJwtIdClaim\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EInclude a JWT identifier claim\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EKeyId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional signing key identifier\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=\u0022refresh-token-concurrency-window\u0022\u003ERefresh Token Concurrency Window\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Token.RefreshTokenConcurrentRequestWindow = TimeSpan.FromSeconds(2);\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis option controls how the server distinguishes a closely timed duplicate refresh request from a confirmed replay.\u003C/p\u003E\n\u003Cp\u003EA duplicate refresh request is still rejected.\u003C/p\u003E\n\u003Cp\u003EThe window affects the replay-response classification, including whether the token family should be revoked.\u003C/p\u003E\n\u003Cp\u003EIt does \u003Cstrong\u003Enot\u003C/strong\u003E allow the same consumed refresh token to be used successfully more than once.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022mode-defaults-may-override-token-values\u0022\u003EMode Defaults May Override Token Values\u003C/h3\u003E\n\u003Cp\u003EThe effective \u003Ccode\u003EHybrid\u003C/code\u003E configuration explicitly sets:\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\u003EToken.AccessTokenLifetime = TimeSpan.FromMinutes(10);\nToken.RefreshTokenLifetime = TimeSpan.FromDays(7);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ETherefore, a different value configured globally may be replaced when the effective mode is resolved.\u003C/p\u003E\n\u003Cp\u003ETo customize the lifetime for \u003Ccode\u003EHybrid\u003C/code\u003E, use:\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.ConfigureMode(UAuthMode.Hybrid, mode =\u0026gt;\n {\n mode.Token.AccessTokenLifetime = TimeSpan.FromMinutes(5);\n });\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis distinction is essential when configuring token lifetimes.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022pkce-options\u0022\u003E6. PKCE Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Pkce\u003C/code\u003E configures the lifetime of PKCE authorization codes.\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Pkce.AuthorizationCodeLifetimeSeconds = 120;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\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\u003EDefault\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\u003EAuthorizationCodeLifetimeSeconds\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E120\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELifetime of a PKCE authorization code\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EPKCE is particularly important for browser-based clients such as Blazor WebAssembly.\u003C/p\u003E\n\u003Cp\u003EA shorter authorization code lifetime reduces the period during which an intercepted or exposed code could potentially be used.\u003C/p\u003E\n\u003Cp\u003EHowever, the value must remain long enough for legitimate authorization exchanges to complete.\u003C/p\u003E\n\u003Cp\u003EThe server\u0027s PKCE configuration is separate from the client\u0027s PKCE flow settings.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022authentication-modes\u0022\u003E7. Authentication Modes\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth supports authentication mode resolution based on the client profile.\u003C/p\u003E\n\u003Cp\u003EThe server exposes:\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\u003Eoptions.AllowedModes\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis property restricts the modes the server is permitted to use.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022restrict-allowed-modes\u0022\u003ERestrict Allowed Modes\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.AllowedModes =\n [\n UAuthMode.PureOpaque,\n UAuthMode.Hybrid\n ];\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EAllowedModes\u003C/code\u003E is a restriction, not a mode selector.\u003C/p\u003E\n\u003Cp\u003EThe effective mode is resolved by \u003Ccode\u003EIEffectiveAuthModeResolver\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022default-mode-resolution\u0022\u003EDefault Mode Resolution\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EClient profile\u003C/th\u003E\n\u003Cth\u003EDefault resolved mode\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EBlazorServer\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EPureOpaque\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EWebServer\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EHybrid\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EBlazorWasm\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EHybrid\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaui\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EHybrid\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EApi\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EPureJwt\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe current implementation has mode templates for \u003Ccode\u003EPureOpaque\u003C/code\u003E, \u003Ccode\u003EHybrid\u003C/code\u003E, \u003Ccode\u003ESemiHybrid\u003C/code\u003E, and \u003Ccode\u003EPureJwt\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EHowever, \u003Ccode\u003ESemiHybrid\u003C/code\u003E and \u003Ccode\u003EPureJwt\u003C/code\u003E are not yet accepted as enabled modes by the current server options validator.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EDo not enable an unsupported mode merely because it exists in the enum or has a mode-default template.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EThe default \u003Ccode\u003EApi\u003C/code\u003E profile mapping to \u003Ccode\u003EPureJwt\u003C/code\u003E is a current compatibility limitation to consider when deploying API-profile authentication flows.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configure-mode-specific-behavior\u0022\u003EConfigure Mode-Specific Behavior\u003C/h3\u003E\n\u003Cp\u003EUse \u003Ccode\u003EConfigureMode()\u003C/code\u003E for behavior that should differ by mode.\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.ConfigureMode(UAuthMode.PureOpaque, mode =\u0026gt;\n {\n mode.Session.IdleTimeout = TimeSpan.FromDays(7);\n });\n\n options.ConfigureMode(UAuthMode.Hybrid, mode =\u0026gt;\n {\n mode.Token.AccessTokenLifetime = TimeSpan.FromMinutes(5);\n });\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe mode override runs after the built-in mode defaults.\u003C/p\u003E\n\u003Cp\u003ESee \u003Ca href=\u0022/docs/configuration/advanced-configuration\u0022\u003EAdvanced Configuration\u003C/a\u003E for details.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022endpoint-options\u0022\u003E8. Endpoint Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Endpoints\u003C/code\u003E controls endpoint paths, feature groups, and individual action restrictions.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022base-path\u0022\u003EBase Path\u003C/h3\u003E\n\u003Cp\u003EThe default server endpoint base path 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\u003E/auth\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EConfigure it 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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Endpoints.BasePath = \u0026quot;/auth\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EChanging the base path can help avoid route conflicts when integrating UltimateAuth into an existing application.\u003C/p\u003E\n\u003Cp\u003EThe client must use compatible endpoint paths.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022endpoint-groups\u0022\u003EEndpoint Groups\u003C/h3\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\u003EDefault\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\u003EAuthentication\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication endpoints\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPkce\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPKCE endpoints\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESession\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESession management endpoints\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserLifecycle\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUser lifecycle endpoints\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserProfile\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUser profile endpoints\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserIdentifier\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifier management endpoints\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECredentials\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECredential management endpoints\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAuthorization\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthorization endpoints\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Endpoints.Authentication = true;\n options.Endpoints.Session = true;\n options.Endpoints.Authorization = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EDisabling an endpoint group controls endpoint exposure. It does not automatically remove the underlying domain services or invalidate previously issued credentials.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022disable-individual-actions\u0022\u003EDisable Individual Actions\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EDisabledActions\u003C/code\u003E is a collection of action identifiers:\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Endpoints.DisabledActions.Add(\u0026quot;users.create.anonymous\u0026quot;);\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe string must match the action identifier used by the server\u0027s endpoint registration and action policy.\u003C/p\u003E\n\u003Cp\u003EDo not pass a C# constant name as a string unless that exact text is also the registered action value.\u003C/p\u003E\n\u003Cp\u003EFor example, these are not necessarily equivalent:\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\u003E\u0026quot;UAuthActions.Users.Create.Anonymous\u0026quot;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Eand:\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\u003E\u0026quot;users.create.anonymous\u0026quot;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUse the actual action value defined by the corresponding \u003Ccode\u003EUAuthActions\u003C/code\u003E constant.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022endpoint-hardening\u0022\u003EEndpoint Hardening\u003C/h3\u003E\n\u003Cp\u003EDisable endpoints that are not required by the application.\u003C/p\u003E\n\u003Cp\u003EFor example, an application that delegates user provisioning to administrators may choose to disable anonymous user creation.\u003C/p\u003E\n\u003Cp\u003EEndpoint exposure is a separate concern from authorization: an exposed endpoint must still enforce the applicable security policy.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022session-resolution\u0022\u003E9. Session Resolution\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.SessionResolution\u003C/code\u003E controls how incoming requests provide session identifiers.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth supports four resolution sources:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EBearer credentials.\u003C/li\u003E\n\u003Cli\u003ECustom headers.\u003C/li\u003E\n\u003Cli\u003ECookies.\u003C/li\u003E\n\u003Cli\u003EQuery parameters.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022default-configuration\u0022\u003EDefault Configuration\u003C/h3\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\u003EDefault\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableBearer\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableHeader\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableCookie\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableQuery\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EHeaderName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EX-UAuth-Session\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EQueryParameterName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Esession_id\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe default precedence order 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\u003EBearer\nHeader\nCookie\nQuery\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configure-resolution-sources\u0022\u003EConfigure Resolution Sources\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.SessionResolution.EnableBearer = true;\n options.SessionResolution.EnableHeader = false;\n options.SessionResolution.EnableCookie = true;\n options.SessionResolution.EnableQuery = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis example disables custom-header and query-string session resolution.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022change-resolution-order\u0022\u003EChange Resolution Order\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.SessionResolution.Order =\n [\n \u0026quot;Cookie\u0026quot;,\n \u0026quot;Bearer\u0026quot;,\n \u0026quot;Header\u0026quot;,\n \u0026quot;Query\u0026quot;\n ];\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EResolution order matters when a request contains multiple candidate credentials.\u003C/p\u003E\n\u003Cp\u003EThe selected credential must still pass authentication and session validation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-recommendation\u0022\u003ESecurity Recommendation\u003C/h3\u003E\n\u003Cp\u003EAvoid query-string credentials unless the application has a specific, well-understood requirement.\u003C/p\u003E\n\u003Cp\u003EURLs may be recorded in browser history, reverse-proxy logs, analytics systems, and other infrastructure.\u003C/p\u003E\n\u003Cp\u003EFor most browser and API integrations, cookies or authorization headers are preferable.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022cookie-configuration\u0022\u003E10. Cookie Configuration\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Cookie\u003C/code\u003E controls cookies used for authentication credentials.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth defines three cookie groups:\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\u003Eoptions.Cookie.Session\noptions.Cookie.AccessToken\noptions.Cookie.RefreshToken\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022default-cookie-names\u0022\u003EDefault Cookie Names\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003ECookie\u003C/th\u003E\n\u003Cth\u003EDefault name\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ESession\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Euas\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAccess token\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Euat\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ERefresh token\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Euar\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe default cookie options use:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003EHttpOnly = true\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003ESecurePolicy = CookieSecurePolicy.Always\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EPath = \u0026quot;/\u0026quot;\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EDomain = null\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EMaxAge = null\u003C/code\u003E\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003E\u003Ccode\u003ESameSite\u003C/code\u003E is nullable and can be resolved through the applicable cookie and deployment policies.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022customize-cookie-names\u0022\u003ECustomize Cookie Names\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Cookie.Session.Name = \u0026quot;myapp_session\u0026quot;;\n options.Cookie.RefreshToken.Name = \u0026quot;myapp_refresh\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configure-cookie-scope\u0022\u003EConfigure Cookie Scope\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Cookie.Session.Path = \u0026quot;/\u0026quot;;\n options.Cookie.Session.Domain = null;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EA null domain produces a host-only cookie.\u003C/p\u003E\n\u003Cp\u003ESetting a parent domain can broaden cookie availability to subdomains and should be done only when necessary.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022cookie-security\u0022\u003ECookie Security\u003C/h3\u003E\n\u003Cp\u003EAvoid disabling \u003Ccode\u003EHttpOnly\u003C/code\u003E or relaxing secure transport requirements for authentication cookies.\u003C/p\u003E\n\u003Cp\u003ECookie settings interact with:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EThe deployment mode.\u003C/li\u003E\n\u003Cli\u003ECross-site requests.\u003C/li\u003E\n\u003Cli\u003EBrowser cookie policies.\u003C/li\u003E\n\u003Cli\u003ECredential delivery configuration.\u003C/li\u003E\n\u003Cli\u003EHTTPS requirements.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EFor cross-site deployments, configure cookie behavior together with CORS and the relevant browser security requirements.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022hub-deployment-mode\u0022\u003E11. Hub Deployment Mode\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.HubDeploymentMode\u003C/code\u003E describes how UAuthHub is deployed relative to the application.\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.HubDeploymentMode = UAuthHubDeploymentMode.Embedded;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-modes\u0022\u003EAvailable Modes\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EMode\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\u003EEmbedded\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EHub and application share the same origin\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIntegrated\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EHub is separate but within the same-site boundary\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EExternal\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EHub is hosted on a different site or domain\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe default 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\u003EUAuthHubDeploymentMode.Integrated\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022embedded\u0022\u003EEmbedded\u003C/h3\u003E\n\u003Cp\u003ESuitable for applications hosting UltimateAuth within the same application and origin.\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\u003Eoptions.HubDeploymentMode = UAuthHubDeploymentMode.Embedded;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022integrated\u0022\u003EIntegrated\u003C/h3\u003E\n\u003Cp\u003ESuitable for deployments such as:\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\u003Eauth.example.com\napp.example.com\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Ewhere the authentication service and application are separate origins within the same site.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022external\u0022\u003EExternal\u003C/h3\u003E\n\u003Cp\u003ESuitable for a separate authentication provider domain.\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\u003Eoptions.HubDeploymentMode = UAuthHubDeploymentMode.External;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EExternal deployment requires careful coordination of cookie policies, redirects, allowed origins, and browser restrictions.\u003C/p\u003E\n\u003Cp\u003EThe deployment mode influences authentication transport behavior; it is not merely descriptive metadata.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022hub-options\u0022\u003E12. Hub Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Hub\u003C/code\u003E configures UAuthHub integration.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-properties-2\u0022\u003EAvailable Properties\u003C/h3\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\u003EDefault\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\u003EClientBaseAddress\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional client application base address\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAllowedClientOrigins\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEmpty set\u003C/td\u003E\n\u003Ctd\u003EAllowed client origins\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EFlowLifetime\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E5 minutes\u003C/td\u003E\n\u003Ctd\u003ELifetime of Hub flow artifacts\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELoginPath\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E/login\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EHub login page path\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=\u0022example\u0022\u003EExample\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Hub.ClientBaseAddress = \u0026quot;https://app.example.com\u0026quot;;\n options.Hub.AllowedClientOrigins.Add(\u0026quot;https://app.example.com\u0026quot;);\n options.Hub.FlowLifetime = TimeSpan.FromMinutes(5);\n options.Hub.LoginPath = \u0026quot;/login\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe Hub flow lifetime is not the same as the authenticated session lifetime.\u003C/p\u003E\n\u003Cp\u003EHub artifacts support authentication orchestration and should generally be short-lived.\u003C/p\u003E\n\u003Cp\u003EOnly trusted origins should be included in \u003Ccode\u003EAllowedClientOrigins\u003C/code\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022primary-credential-policy\u0022\u003E13. Primary Credential Policy\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.PrimaryCredential\u003C/code\u003E controls which credential kind is preferred for UI and API requests.\u003C/p\u003E\n\u003Cp\u003EThe defaults are:\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\u003EDefault\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUi\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EPrimaryTokenKind.Session\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EApi\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EPrimaryTokenKind.AccessToken\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=\u0022example-1\u0022\u003EExample\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.PrimaryCredential.Ui = PrimaryTokenKind.Session;\n options.PrimaryCredential.Api = PrimaryTokenKind.AccessToken;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis allows UltimateAuth to distinguish between session-oriented browser interactions and access-token-oriented API interactions.\u003C/p\u003E\n\u003Cp\u003EThe primary credential policy does not bypass authentication validation.\u003C/p\u003E\n\u003Cp\u003EIt determines which credential kind should be preferred within the applicable request-processing rules.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022credential-session-policy\u0022\u003E14. Credential Session Policy\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.CredentialSessionPolicy\u003C/code\u003E determines what happens to active session chains after credential changes.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-properties-3\u0022\u003EAvailable Properties\u003C/h3\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\u003EDefault\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\u003EOnSelfChange\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EPreserveCurrent\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUser changes their own credential\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EOnAdminChange\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003ERevokeAll\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAdministrator changes a user\u0027s credential\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EOnRecovery\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003ERevokeAll\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECredential recovery completes\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=\u0022example-2\u0022\u003EExample\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.CredentialSessionPolicy.OnSelfChange = CredentialSessionBehavior.PreserveCurrent;\n options.CredentialSessionPolicy.OnAdminChange = CredentialSessionBehavior.RevokeAll;\n options.CredentialSessionPolicy.OnRecovery = CredentialSessionBehavior.RevokeAll;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-behaviors\u0022\u003EAvailable Behaviors\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EBehavior\u003C/th\u003E\n\u003Cth\u003EEffect\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPreserveAll\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EKeep existing session chains\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPreserveCurrent\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EKeep the current chain and revoke other applicable chains\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERevokeAll\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERevoke all applicable session chains\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EFor recovery flows, \u003Ccode\u003EPreserveCurrent\u003C/code\u003E is not always meaningful because the user may not have an authenticated current session.\u003C/p\u003E\n\u003Cp\u003EThe current implementation treats this situation using the more restrictive \u003Ccode\u003ERevokeAll\u003C/code\u003E behavior.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-recommendation-1\u0022\u003ESecurity Recommendation\u003C/h3\u003E\n\u003Cp\u003EPreserving the current session after a normal password change can provide a reasonable user experience.\u003C/p\u003E\n\u003Cp\u003ERevoking all sessions after administrative credential changes or account recovery is generally more conservative.\u003C/p\u003E\n\u003Cp\u003EChoose the policy according to the application\u0027s security requirements.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022credential-reset-options\u0022\u003E15. Credential Reset Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.ResetCredential\u003C/code\u003E configures credential recovery proof lifetimes and attempt limits.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022defaults\u0022\u003EDefaults\u003C/h3\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\u003EDefault\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\u003ETokenValidity\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E30 minutes\u003C/td\u003E\n\u003Ctd\u003EReset token lifetime\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECodeValidity\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E10 minutes\u003C/td\u003E\n\u003Ctd\u003ENumeric reset code lifetime\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxAttempts\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E3\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EMaximum permitted recovery attempts\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECodeLength\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E6\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENumeric reset code length\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=\u0022example-3\u0022\u003EExample\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.ResetCredential.TokenValidity = TimeSpan.FromMinutes(20);\n options.ResetCredential.CodeValidity = TimeSpan.FromMinutes(5);\n options.ResetCredential.MaxAttempts = 3;\n options.ResetCredential.CodeLength = 6;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003ECodeLength\u003C/code\u003E applies to numeric reset codes, not token-based recovery proofs.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022important-security-boundary\u0022\u003EImportant Security Boundary\u003C/h3\u003E\n\u003Cp\u003EReset proofs must be delivered through a trusted server-side mechanism.\u003C/p\u003E\n\u003Cp\u003EDo not return recovery tokens to anonymous callers simply to simplify a client-side reset form.\u003C/p\u003E\n\u003Cp\u003EThe application should provide the appropriate \u003Ccode\u003EICredentialResetNotifier\u003C/code\u003E implementation for supported recovery flows.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022login-identifier-options\u0022\u003E16. Login Identifier Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.LoginIdentifiers\u003C/code\u003E determines which identifier types can be used for login.\u003C/p\u003E\n\u003Cp\u003EThe default allowed types are:\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\u003EUserIdentifierType.Username\nUserIdentifierType.Email\nUserIdentifierType.Phone\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022restrict-login-identifier-types\u0022\u003ERestrict Login Identifier Types\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.LoginIdentifiers.AllowedTypes =\n new HashSet\u0026lt;UserIdentifierType\u0026gt;\n {\n UserIdentifierType.Email\n };\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis configures email as the allowed login identifier type.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022require-verification\u0022\u003ERequire Verification\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.LoginIdentifiers.RequireVerificationForEmail = true;\n options.LoginIdentifiers.RequireVerificationForPhone = true;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThese settings control whether the corresponding identifiers must be verified before being accepted for login.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022custom-resolvers\u0022\u003ECustom Resolvers\u003C/h3\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\u003EDefault\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableCustomResolvers\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECustomResolversFirst\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThese options control the participation and ordering of custom login identifier resolvers.\u003C/p\u003E\n\u003Cp\u003EAn identifier existing on a user account does not necessarily mean it is permitted for login.\u003C/p\u003E\n\u003Cp\u003EIdentifier management and login eligibility are separate concerns.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022identifier-validation\u0022\u003E17. Identifier Validation\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.IdentifierValidation\u003C/code\u003E provides rules for usernames, email addresses, and phone numbers.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022username-rules\u0022\u003EUsername Rules\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.IdentifierValidation.UserName.Enabled = true;\n options.IdentifierValidation.UserName.MinLength = 3;\n options.IdentifierValidation.UserName.MaxLength = 32;\n options.IdentifierValidation.UserName.AllowedRegex = \u0026quot;^[a-zA-Z0-9._-]\u002B$\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022default-validation-rules\u0022\u003EDefault Validation Rules\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EIdentifier\u003C/th\u003E\n\u003Cth\u003EEnabled\u003C/th\u003E\n\u003Cth\u003EMinimum length\u003C/th\u003E\n\u003Cth\u003EMaximum length\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EUsername\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E3\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E64\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EEmail\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E3\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E256\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EPhone\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E3\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E24\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EUsername validation additionally supports \u003Ccode\u003EAllowedRegex\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe default username expression 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\u003EREGEX\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-regex\u0022\u003E^[a-zA-Z0-9._-]\u002B$\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022identifier-behavior\u0022\u003EIdentifier Behavior\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Identifiers\u003C/code\u003E contains additional groups:\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\u003Eoptions.Identifiers.Behavior\noptions.Identifiers.Validation\noptions.Identifiers.Normalization\noptions.Identifiers.Uniqueness\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThese configure identifier behavior beyond the top-level validation rules.\u003C/p\u003E\n\u003Cp\u003EIdentifier normalization and uniqueness are particularly important because they affect how duplicate identifiers are detected.\u003C/p\u003E\n\u003Cp\u003EUse the provided options rather than implementing inconsistent normalization independently in the client.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022user-profile-options\u0022\u003E18. User Profile Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.UserProfile\u003C/code\u003E controls profile-related capabilities.\u003C/p\u003E\n\u003Cp\u003EThe current option 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\u003Eoptions.UserProfile.EnableMultiProfile\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EIts default is \u003Ccode\u003Efalse\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022enable-multiple-profiles\u0022\u003EEnable Multiple Profiles\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.UserProfile.EnableMultiProfile = true;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis allows applications to use UltimateAuth\u0027s multiple-profile capability where supported by the user domain.\u003C/p\u003E\n\u003Cp\u003EFor example, an application may maintain separate profile contexts for different purposes.\u003C/p\u003E\n\u003Cp\u003EMulti-profile support does not create separate user identities or independent authentication accounts.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022multi-tenant-options\u0022\u003E19. Multi-Tenant Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.MultiTenant\u003C/code\u003E controls whether tenant-aware behavior is enabled and how tenant identifiers are resolved.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-properties-4\u0022\u003EAvailable Properties\u003C/h3\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\u003EDefault\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\u003EEnabled\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEnable multi-tenant behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ENormalizeToLowercase\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENormalize tenant identifiers\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableRoute\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EResolve tenants from routes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableHeader\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EResolve tenants from headers\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableDomain\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EResolve tenants from hosts\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EHeaderName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EX-Tenant\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETenant header name\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=\u0022example-header-based-tenant-resolution\u0022\u003EExample: Header-Based Tenant Resolution\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.MultiTenant.Enabled = true;\n options.MultiTenant.EnableHeader = true;\n options.MultiTenant.HeaderName = \u0026quot;X-Tenant\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-domain-based-resolution\u0022\u003EExample: Domain-Based Resolution\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.MultiTenant.Enabled = true;\n options.MultiTenant.EnableDomain = true;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EMultiple resolution mechanisms can be enabled.\u003C/p\u003E\n\u003Cp\u003EThe server composes the configured tenant resolvers according to its resolution infrastructure.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-boundary\u0022\u003ESecurity Boundary\u003C/h3\u003E\n\u003Cp\u003ETenant identification is not tenant authorization.\u003C/p\u003E\n\u003Cp\u003EA request providing another tenant identifier must not gain access to that tenant without appropriate authorization.\u003C/p\u003E\n\u003Cp\u003EThe server remains responsible for tenant isolation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022pagination-options\u0022\u003E20. Pagination Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Pagination\u003C/code\u003E controls pagination defaults and maximum page sizes.\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\u003EDefault\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDefaultPageSize\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E250\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxPageSize\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E1000\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=\u0022example-4\u0022\u003EExample\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Pagination.DefaultPageSize = 50;\n options.Pagination.MaxPageSize = 200;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThese settings affect paginated queries exposed by UltimateAuth domain services.\u003C/p\u003E\n\u003Cp\u003EFor administrative interfaces, choosing a moderate page size can reduce unnecessary data transfer and processing.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022navigation-options\u0022\u003E21. Navigation Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Navigation\u003C/code\u003E configures automatic authentication navigation behavior.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-properties-5\u0022\u003EAvailable Properties\u003C/h3\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\u003EDefault\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableAutomaticNavigationRedirect\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELoginResolver\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAccessDeniedResolver\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\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=\u0022disable-automatic-redirects\u0022\u003EDisable Automatic Redirects\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Navigation.EnableAutomaticNavigationRedirect = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022custom-login-redirect\u0022\u003ECustom Login Redirect\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Navigation.LoginResolver = context =\u0026gt;\n {\n return \u0026quot;/account/login\u0026quot;;\n };\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022custom-access-denied-redirect\u0022\u003ECustom Access Denied Redirect\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Navigation.AccessDeniedResolver = context =\u0026gt;\n {\n return \u0026quot;/account/access-denied\u0026quot;;\n };\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThese delegates receive the current \u003Ccode\u003EHttpContext\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe application should return valid, trusted navigation destinations.\u003C/p\u003E\n\u003Cp\u003EAvoid constructing redirects from unvalidated user-controlled URLs.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022authentication-response-options\u0022\u003E22. Authentication Response Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.AuthResponse\u003C/code\u003E controls how authentication credentials and redirect behavior are represented in responses.\u003C/p\u003E\n\u003Cp\u003EThe available groups are:\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\u003Eoptions.AuthResponse.SessionIdDelivery\noptions.AuthResponse.AccessTokenDelivery\noptions.AuthResponse.RefreshTokenDelivery\n\noptions.AuthResponse.Login\noptions.AuthResponse.Logout\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThese settings govern credential delivery and authentication response behavior.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022important-mode-specific-response-defaults\u0022\u003EImportant: Mode-Specific Response Defaults\u003C/h3\u003E\n\u003Cp\u003EUltimateAuth provides response templates for authentication modes.\u003C/p\u003E\n\u003Cp\u003EFor example, \u003Ccode\u003EPureOpaque\u003C/code\u003E uses a session-oriented cookie response, while \u003Ccode\u003EHybrid\u003C/code\u003E combines session and token delivery behavior.\u003C/p\u003E\n\u003Cp\u003EThe effective response may therefore depend on the resolved authentication mode and flow.\u003C/p\u003E\n\u003Cp\u003EDo not assume that a single globally configured response delivery setting applies unchanged to every authentication mode.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-considerations\u0022\u003ESecurity Considerations\u003C/h3\u003E\n\u003Cp\u003ECredential delivery settings determine whether sensitive authentication material is exposed through cookies, headers, or other response mechanisms.\u003C/p\u003E\n\u003Cp\u003EOnly customize them when the application has a clear requirement and the selected transport is appropriate for the client platform.\u003C/p\u003E\n\u003Cp\u003EFor advanced response customization, see \u003Ca href=\u0022/docs/configuration/advanced-configuration\u0022\u003EAdvanced Configuration\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022diagnostics-options\u0022\u003E23. Diagnostics Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Diagnostics\u003C/code\u003E provides optional diagnostic behavior.\u003C/p\u003E\n\u003Cp\u003EThe current property 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\u003Eoptions.Diagnostics.EnableRefreshDetails\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EIts default is \u003Ccode\u003Efalse\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-5\u0022\u003EExample\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Diagnostics.EnableRefreshDetails = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWhen enabled, the option allows additional refresh-related diagnostic response information, such as the \u003Ccode\u003EX-UAuth-Refresh\u003C/code\u003E header.\u003C/p\u003E\n\u003Cp\u003EThis is primarily useful during development and debugging.\u003C/p\u003E\n\u003Cp\u003EAvoid enabling detailed authentication diagnostics in production unless the exposure is understood and justified.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022authentication-lifecycle-events\u0022\u003E24. Authentication Lifecycle Events\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Events\u003C/code\u003E exposes server-side authentication lifecycle callbacks.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-events\u0022\u003EAvailable Events\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\u003Eoptions.Events.OnAnyEvent\noptions.Events.OnUserLoggedIn\noptions.Events.OnUserLoggedOut\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-login-logging\u0022\u003EExample: Login Logging\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Events.OnUserLoggedIn = context =\u0026gt;\n {\n // Record the successful login.\n return Task.CompletedTask;\n };\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-global-event-handler\u0022\u003EExample: Global Event Handler\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Events.OnAnyEvent = context =\u0026gt;\n {\n // Process an authentication lifecycle event.\n return Task.CompletedTask;\n };\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ELifecycle events are intended for lightweight reactions such as logging, metrics, and notifications.\u003C/p\u003E\n\u003Cp\u003EThey should not replace security decisions, authentication validation, or application policies.\u003C/p\u003E\n\u003Cp\u003ESee \u003Ca href=\u0022/docs/configuration/advanced-configuration\u0022\u003EAdvanced Configuration\u003C/a\u003E for event and extension guidance.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022effective-server-options\u0022\u003E25. Effective Server Options\u003C/h2\u003E\n\u003Cp\u003EThe configured \u003Ccode\u003EUAuthServerOptions\u003C/code\u003E instance is the base configuration.\u003C/p\u003E\n\u003Cp\u003EAt runtime, UltimateAuth can resolve:\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\u003EEffectiveUAuthServerOptions\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe current resolution process 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\u003EConfigured UAuthServerOptions\n |\n v\n Resolve auth mode\n |\n v\n Clone base options\n |\n v\n Apply mode defaults\n |\n v\n Apply ConfigureMode overrides\n |\n v\n EffectiveUAuthServerOptions\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis means that the effective settings used for a \u003Ccode\u003EHybrid\u003C/code\u003E client can differ from those used for a \u003Ccode\u003EPureOpaque\u003C/code\u003E client.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-6\u0022\u003EExample\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Session.Lifetime = TimeSpan.FromDays(7);\n\n options.ConfigureMode(UAuthMode.Hybrid, mode =\u0026gt;\n {\n mode.Token.AccessTokenLifetime = TimeSpan.FromMinutes(5);\n });\n\n options.ConfigureMode(UAuthMode.PureOpaque, mode =\u0026gt;\n {\n mode.Session.IdleTimeout = TimeSpan.FromDays(3);\n });\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe original options remain the common starting point.\u003C/p\u003E\n\u003Cp\u003EMode defaults and overrides determine the effective values for the resolved mode.\u003C/p\u003E\n\u003Cp\u003EFor implementation details and inspection of effective options, see \u003Ca href=\u0022/docs/configuration/advanced-configuration\u0022\u003EAdvanced Configuration\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022validation-and-configuration-safety\u0022\u003E26. Validation and Configuration Safety\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth Server registers multiple options validators and enables startup validation.\u003C/p\u003E\n\u003Cp\u003EThese validators cover areas including:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003ELogin settings.\u003C/li\u003E\n\u003Cli\u003ESession settings.\u003C/li\u003E\n\u003Cli\u003EToken settings.\u003C/li\u003E\n\u003Cli\u003EPKCE.\u003C/li\u003E\n\u003Cli\u003EMulti-tenancy.\u003C/li\u003E\n\u003Cli\u003EIdentifier configuration.\u003C/li\u003E\n\u003Cli\u003ESession resolution.\u003C/li\u003E\n\u003Cli\u003EPagination.\u003C/li\u003E\n\u003Cli\u003EGeneral server configuration.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-invalid-session-lifetime\u0022\u003EExample: Invalid Session Lifetime\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Session.Lifetime = TimeSpan.Zero;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe server rejects an invalid session lifetime under the applicable validation rules.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-invalid-base-path\u0022\u003EExample: Invalid Base Path\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Endpoints.BasePath = \u0026quot;//auth\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe general server validator rejects paths containing \u003Ccode\u003E//\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022unsupported-modes\u0022\u003EUnsupported Modes\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.AllowedModes =\n [\n UAuthMode.PureJwt\n ];\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe current validator rejects explicitly enabling \u003Ccode\u003EPureJwt\u003C/code\u003E because that mode is not yet implemented as a supported server mode.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022validation-limitations\u0022\u003EValidation Limitations\u003C/h3\u003E\n\u003Cp\u003EStartup validation protects the configured options according to the registered validators.\u003C/p\u003E\n\u003Cp\u003EIt does not guarantee that every custom service implementation or mode-specific override is secure.\u003C/p\u003E\n\u003Cp\u003EWhen using advanced customization, test the resulting effective configuration and authentication flows.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022complete-configuration-example\u0022\u003E27. Complete Configuration Example\u003C/h2\u003E\n\u003Cp\u003EThe following example combines several commonly used server settings.\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n // Authentication modes\n options.AllowedModes =\n [\n UAuthMode.PureOpaque,\n UAuthMode.Hybrid\n ];\n\n // Login security\n options.Login.MaxFailedAttempts = 5;\n options.Login.LockoutDuration = TimeSpan.FromMinutes(15);\n options.Login.IncludeFailureDetails = false;\n\n // Sessions\n options.Session.Lifetime = TimeSpan.FromDays(7);\n options.Session.MaxLifetime = TimeSpan.FromDays(30);\n\n // Token identity\n options.Token.Issuer = \u0026quot;MyApplication\u0026quot;;\n options.Token.Audience = \u0026quot;MyApplicationApi\u0026quot;;\n\n // Hybrid-specific token lifetime\n options.ConfigureMode(UAuthMode.Hybrid, mode =\u0026gt;\n {\n mode.Token.AccessTokenLifetime = TimeSpan.FromMinutes(10);\n });\n\n // PKCE\n options.Pkce.AuthorizationCodeLifetimeSeconds = 120;\n\n // Hub deployment\n options.HubDeploymentMode = UAuthHubDeploymentMode.Integrated;\n options.Hub.ClientBaseAddress = \u0026quot;https://app.example.com\u0026quot;;\n options.Hub.AllowedClientOrigins.Add(\u0026quot;https://app.example.com\u0026quot;);\n\n // Credential security\n options.CredentialSessionPolicy.OnSelfChange = CredentialSessionBehavior.PreserveCurrent;\n options.CredentialSessionPolicy.OnAdminChange = CredentialSessionBehavior.RevokeAll;\n options.CredentialSessionPolicy.OnRecovery = CredentialSessionBehavior.RevokeAll;\n\n // Credential recovery\n options.ResetCredential.TokenValidity = TimeSpan.FromMinutes(30);\n options.ResetCredential.MaxAttempts = 3;\n\n // Identifier login rules\n options.LoginIdentifiers.RequireVerificationForEmail = true;\n\n // Session transport\n options.SessionResolution.EnableCookie = true;\n options.SessionResolution.EnableBearer = true;\n options.SessionResolution.EnableQuery = false;\n\n // Endpoint exposure\n options.Endpoints.Authentication = true;\n options.Endpoints.Session = true;\n options.Endpoints.Credentials = true;\n options.Endpoints.Authorization = true;\n\n // Pagination\n options.Pagination.DefaultPageSize = 50;\n options.Pagination.MaxPageSize = 200;\n\n // Diagnostics\n options.Diagnostics.EnableRefreshDetails = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis is an illustrative configuration, not a universal production preset.\u003C/p\u003E\n\u003Cp\u003EApplications should choose session lifetimes, authentication modes, credential transport, tenant behavior, and endpoint exposure according to their own deployment and security requirements.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022common-configuration-mistakes\u0022\u003E28. Common Configuration Mistakes\u003C/h2\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configuring-core-options-separately\u0022\u003EConfiguring Core Options Separately\u003C/h3\u003E\n\u003Cp\u003EWhen using \u003Ccode\u003EAddUltimateAuthServer()\u003C/code\u003E, configure Core-defined options through \u003Ccode\u003EUAuthServerOptions\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EA separate \u003Ccode\u003EUAuthOptions\u003C/code\u003E configuration does not automatically override the server\u0027s composed options.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022assuming-allowedmodes-selects-the-mode\u0022\u003EAssuming AllowedModes Selects the Mode\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EAllowedModes\u003C/code\u003E is a restriction.\u003C/p\u003E\n\u003Cp\u003EMode selection is performed by the authentication mode resolver.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022ignoring-mode-defaults\u0022\u003EIgnoring Mode Defaults\u003C/h3\u003E\n\u003Cp\u003EMode-specific defaults can replace globally configured values.\u003C/p\u003E\n\u003Cp\u003EUse \u003Ccode\u003EConfigureMode()\u003C/code\u003E for explicit mode-specific behavior.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022enabling-unsupported-modes\u0022\u003EEnabling Unsupported Modes\u003C/h3\u003E\n\u003Cp\u003EAn authentication mode appearing in an enum does not guarantee runtime support.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022treating-reserved-session-options-as-enforced-security-features\u0022\u003ETreating Reserved Session Options as Enforced Security Features\u003C/h3\u003E\n\u003Cp\u003ESome device and session-chain options are reserved for future enforcement.\u003C/p\u003E\n\u003Cp\u003EVerify actual runtime support before relying on them.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022exposing-unnecessary-endpoints\u0022\u003EExposing Unnecessary Endpoints\u003C/h3\u003E\n\u003Cp\u003EDisable endpoint groups or actions that the application does not require.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022allowing-query-string-credentials-without-a-specific-need\u0022\u003EAllowing Query-String Credentials Without a Specific Need\u003C/h3\u003E\n\u003Cp\u003EQuery-string credentials can leak through URLs and infrastructure logs.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022weakening-cookie-security\u0022\u003EWeakening Cookie Security\u003C/h3\u003E\n\u003Cp\u003EDo not relax secure transport or cookie protections without understanding the consequences.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022assuming-configuration-validation-covers-everything\u0022\u003EAssuming Configuration Validation Covers Everything\u003C/h3\u003E\n\u003Cp\u003EStartup validation does not replace testing, infrastructure security, or validation of custom extension implementations.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022server-options-quick-reference\u0022\u003E29. Server Options Quick Reference\u003C/h2\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EConfiguration group\u003C/th\u003E\n\u003Cth\u003ETypical use\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELogin\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELogin attempts and lockout\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESession\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESession expiration and lifecycle\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EToken\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EToken issuance and lifetime\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPkce\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPKCE code lifetime\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAllowedModes\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPermitted authentication modes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMultiTenant\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETenant resolution\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPagination\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EQuery page limits\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEndpoints\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEndpoint exposure\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESessionResolution\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECredential source precedence\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECookie\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECookie security and scope\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EHubDeploymentMode\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EHub deployment topology\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EHub\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EHub integration\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPrimaryCredential\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUI/API primary credential selection\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAuthResponse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECredential delivery\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECredentialSessionPolicy\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESession invalidation after credential changes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EResetCredential\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECredential recovery rules\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdentifiers\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifier behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdentifierValidation\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifier validation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELoginIdentifiers\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELogin identifier eligibility\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserProfile\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EMulti-profile capability\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ENavigation\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication redirects\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDiagnostics\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERefresh diagnostics\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEvents\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication lifecycle callbacks\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EConfigureMode()\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EMode-specific option overrides\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EOnConfigureEndpoints\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAdvanced endpoint customization\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\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/configuration/configuration\u0022\u003EConfiguration Overview\u003C/a\u003E \u2014 Understand the configuration architecture and precedence.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/configuration/client-options\u0022\u003EClient Options\u003C/a\u003E \u2014 Configure client SDK behavior.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/configuration/advanced-configuration\u0022\u003EAdvanced Configuration\u003C/a\u003E \u2014 Explore effective options, mode resolution, and extension points.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/client/session-management\u0022\u003ESession Management\u003C/a\u003E \u2014 Manage sessions through the client.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/client/authentication\u0022\u003EAuthentication\u003C/a\u003E \u2014 Use the client authentication flows.\u003C/li\u003E\n\u003C/ul\u003E\n", + "Html": "\n\u003Cp\u003E\u003Ccode\u003EUAuthServerOptions\u003C/code\u003E is the primary configuration surface for UltimateAuth Server.\u003C/p\u003E\n\u003Cp\u003EIt controls authentication behavior, session lifecycles, token issuance, credential security, tenant resolution, endpoint exposure, and integration with UAuthHub.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth composes its Core-defined options directly into \u003Ccode\u003EUAuthServerOptions\u003C/code\u003E. When using UltimateAuth Server, you configure both shared authentication behavior and server-specific features through the same options object.\u003C/p\u003E\n\u003Cp\u003EThis guide covers the available configuration groups, their defaults, practical examples, and the distinction between configured and effective runtime behavior.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022register-ultimateauth-server\u0022\u003E1. Register UltimateAuth Server\u003C/h2\u003E\n\u003Cp\u003EConfigure the server through \u003Ccode\u003EAddUltimateAuthServer()\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Login.MaxFailedAttempts = 5;\n options.Session.Lifetime = TimeSpan.FromDays(7);\n options.Token.AccessTokenLifetime = TimeSpan.FromMinutes(10);\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe configuration delegate receives:\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\u003EUAuthServerOptions\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe server registration also supports an optional policy configuration delegate.\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\u003Ebuilder.Services.AddUltimateAuthServer(\n configure: options =\u0026gt;\n {\n options.Login.MaxFailedAttempts = 5;\n },\n configurePolicies: policies =\u0026gt;\n {\n // Register application access policies.\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe server automatically registers its Core infrastructure. You do not need to call \u003Ccode\u003EAddUltimateAuth()\u003C/code\u003E separately.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configuration-from-appsettings.json\u0022\u003EConfiguration from appsettings.json\u003C/h3\u003E\n\u003Cp\u003EUltimateAuth Server binds the configuration section:\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\u003EUltimateAuth:Server\n\u003C/code\u003E\u003C/pre\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\u003EJSON\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-json\u0022\u003E{\n \u0026quot;UltimateAuth\u0026quot;: {\n \u0026quot;Server\u0026quot;: {\n \u0026quot;Login\u0026quot;: {\n \u0026quot;MaxFailedAttempts\u0026quot;: 5,\n \u0026quot;LockoutDuration\u0026quot;: \u0026quot;00:15:00\u0026quot;\n },\n \u0026quot;Session\u0026quot;: {\n \u0026quot;Lifetime\u0026quot;: \u0026quot;7.00:00:00\u0026quot;\n },\n \u0026quot;Token\u0026quot;: {\n \u0026quot;AccessTokenLifetime\u0026quot;: \u0026quot;00:10:00\u0026quot;\n }\n }\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EASP.NET Core represents \u003Ccode\u003ETimeSpan\u003C/code\u003E values using strings such as \u003Ccode\u003E00:15:00\u003C/code\u003E for fifteen minutes and \u003Ccode\u003E7.00:00:00\u003C/code\u003E for seven days.\u003C/p\u003E\n\u003Cp\u003EThe configuration section is bound after the registration delegate, so configuration provider values take precedence for matching properties.\u003C/p\u003E\n\u003Cp\u003EFor configuration source ordering and environment-specific overrides, see \u003Ca href=\u0022/docs/configuration/configuration\u0022\u003EConfiguration Overview\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022server-options-architecture\u0022\u003E2. Server Options Architecture\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthServerOptions\u003C/code\u003E contains two categories of configuration.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022core-defined-options\u0022\u003ECore-defined options\u003C/h3\u003E\n\u003Cp\u003EThese types are defined in UltimateAuth Core and composed into the server options:\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\u003EType\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\u003ELogin\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthLoginOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELogin attempts and lockout behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESession\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthSessionOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESession lifetime and lifecycle\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EToken\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthTokenOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAccess and refresh token behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPkce\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthPkceOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPKCE authorization code lifetime\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMultiTenant\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthMultiTenantOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETenant identification and resolution\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPagination\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthPaginationOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPagination defaults and limits\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEvents\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthEvents\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication lifecycle callbacks\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=\u0022server-specific-options\u0022\u003EServer-specific options\u003C/h3\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\u003EAllowedModes\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERestrict permitted authentication modes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EHubDeploymentMode\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EDescribe the relationship between UAuthHub and the application\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EHub\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EHub integration and flow settings\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECookie\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECookie names, security attributes, and lifetime behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESessionResolution\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESession identifier sources and precedence\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEndpoints\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEndpoint groups and disabled actions\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPrimaryCredential\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPrimary credential selection for UI and API requests\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAuthResponse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication credential delivery and redirect behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECredentialSessionPolicy\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESession invalidation after credential changes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EResetCredential\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECredential recovery code and token settings\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdentifiers\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifier behavior, normalization, and uniqueness\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdentifierValidation\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUsername, email, and phone validation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELoginIdentifiers\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifier types allowed for login\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserProfile\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUser profile capabilities\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ENavigation\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication navigation behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDiagnostics\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional diagnostic response details\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EAdvanced configuration hooks are also available, including \u003Ccode\u003EConfigureMode()\u003C/code\u003E and \u003Ccode\u003EOnConfigureEndpoints\u003C/code\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022login-options\u0022\u003E3. Login Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Login\u003C/code\u003E controls login attempt limits, lockout behavior, and related security settings.\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Login.MaxFailedAttempts = 5;\n options.Login.LockoutDuration = TimeSpan.FromMinutes(15);\n options.Login.FailureWindow = TimeSpan.FromMinutes(15);\n options.Login.ExtendLockOnFailure = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-properties\u0022\u003EAvailable Properties\u003C/h3\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\u003EDefault\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\u003EMaxFailedAttempts\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E10\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EFailed login threshold; \u003Ccode\u003E0\u003C/code\u003E disables lockout\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELockoutDuration\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E15 minutes\u003C/td\u003E\n\u003Ctd\u003ELockout duration\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EFailureWindow\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E15 minutes\u003C/td\u003E\n\u003Ctd\u003EWindow for counting failed attempts\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EExtendLockOnFailure\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EExtend lockout when another attempt fails during lockout\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIncludeFailureDetails\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EInclude detailed authentication failure information\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETryLoginDuration\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E180 seconds\u003C/td\u003E\n\u003Ctd\u003EDuration associated with the try-login flow\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=\u0022example-restrict-login-failure-details\u0022\u003EExample: Restrict Login Failure Details\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Login.IncludeFailureDetails = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis setting is useful when an application does not want to expose detailed lockout information to callers.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022important\u0022\u003EImportant\u003C/h3\u003E\n\u003Cp\u003ELockout configuration is not equivalent to complete network-level rate limiting.\u003C/p\u003E\n\u003Cp\u003EProduction deployments should also consider infrastructure protections against abusive traffic and distributed login attempts.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022session-options\u0022\u003E4. Session Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Session\u003C/code\u003E controls session lifetime, expiration, sliding behavior, and session-chain limits.\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Session.Lifetime = TimeSpan.FromDays(7);\n options.Session.SlidingExpiration = true;\n options.Session.IdleTimeout = TimeSpan.FromDays(3);\n options.Session.MaxLifetime = TimeSpan.FromDays(30);\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022session-lifetime-properties\u0022\u003ESession Lifetime Properties\u003C/h3\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\u003EDefault\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\u003ELifetime\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E7 days\u003C/td\u003E\n\u003Ctd\u003EStandard session lifetime\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxLifetime\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional absolute lifetime limit\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESlidingExpiration\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAllow eligible activity or refresh operations to extend expiration\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdleTimeout\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional inactivity timeout\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETouchInterval\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E5 minutes\u003C/td\u003E\n\u003Ctd\u003EMinimum interval between last-seen updates\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThese settings have different purposes.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003ELifetime\u003C/code\u003E determines the standard lifetime assigned to a session.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EMaxLifetime\u003C/code\u003E establishes an optional hard boundary that cannot be extended indefinitely.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EIdleTimeout\u003C/code\u003E limits how long a session can remain inactive.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003ETouchInterval\u003C/code\u003E reduces unnecessary persistence operations caused by frequent activity updates.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022session-chain-properties\u0022\u003ESession-Chain Properties\u003C/h3\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\u003EDefault\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\u003EMaxChainsPerUser\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E0\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReserved user-level chain limit\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxSessionsPerChain\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E100\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESession-chain rotation limit\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxChainsPerPlatform\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReserved platform-specific chain limits\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPlatformCategories\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReserved platform group definitions\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxChainsPerCategory\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReserved category-specific chain limits\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableIpBinding\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReserved IP binding setting\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableUserAgentBinding\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReserved User-Agent binding setting\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDeviceMismatchBehavior\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EReject\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReserved device mismatch behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Cstrong\u003ECurrent implementation limitation:\u003C/strong\u003E The source identifies several device limits and binding settings as reserved for future enforcement. Setting these properties must not be treated as proof that the corresponding protection is active.\u003C/p\u003E\n\u003Cp\u003EFor example, do not rely on:\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\u003Eoptions.Session.MaxChainsPerUser = 1;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Eas a production guarantee that only one device chain can exist unless the current runtime implementation has been verified to enforce that restriction.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022mode-specific-session-defaults\u0022\u003EMode-Specific Session Defaults\u003C/h3\u003E\n\u003Cp\u003ESession settings can also be affected by authentication mode defaults.\u003C/p\u003E\n\u003Cp\u003EFor example, \u003Ccode\u003EPureOpaque\u003C/code\u003E supplies a longer idle timeout and touch interval when those values have not been configured.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EHybrid\u003C/code\u003E explicitly sets \u003Ccode\u003ETouchInterval\u003C/code\u003E to \u003Ccode\u003Enull\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003ESee \u003Ca href=\u0022/docs/configuration/advanced-configuration\u0022\u003EAdvanced Configuration\u003C/a\u003E for the effective-options resolution order.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022token-options\u0022\u003E5. Token Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Token\u003C/code\u003E configures access and refresh token behavior.\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Token.Issuer = \u0026quot;MyApplication\u0026quot;;\n options.Token.Audience = \u0026quot;MyApplicationApi\u0026quot;;\n options.Token.AccessTokenLifetime = TimeSpan.FromMinutes(10);\n options.Token.RefreshTokenLifetime = TimeSpan.FromDays(7);\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-properties-1\u0022\u003EAvailable Properties\u003C/h3\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\u003EDefault\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\u003EIssueJwt\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEnable JWT issuance where supported by the effective mode\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIssueOpaque\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEnable opaque token issuance where supported\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIssueRefresh\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEnable refresh token issuance where supported\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAccessTokenLifetime\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E10 minutes\u003C/td\u003E\n\u003Ctd\u003EAccess token lifetime\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERefreshTokenLifetime\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E7 days\u003C/td\u003E\n\u003Ctd\u003ERefresh token lifetime\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EOpaqueIdBytes\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E32\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERandom byte length used for opaque token identifiers\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERefreshTokenConcurrentRequestWindow\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E2 seconds\u003C/td\u003E\n\u003Ctd\u003EConcurrent duplicate refresh classification window\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIssuer\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuth\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EJWT issuer\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAudience\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthClient\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EJWT audience\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAddJwtIdClaim\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EInclude a JWT identifier claim\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EKeyId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional signing key identifier\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=\u0022refresh-token-concurrency-window\u0022\u003ERefresh Token Concurrency Window\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Token.RefreshTokenConcurrentRequestWindow = TimeSpan.FromSeconds(2);\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis option controls how the server distinguishes a closely timed duplicate refresh request from a confirmed replay.\u003C/p\u003E\n\u003Cp\u003EA duplicate refresh request is still rejected.\u003C/p\u003E\n\u003Cp\u003EThe window affects the replay-response classification, including whether the token family should be revoked.\u003C/p\u003E\n\u003Cp\u003EIt does \u003Cstrong\u003Enot\u003C/strong\u003E allow the same consumed refresh token to be used successfully more than once.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022mode-defaults-may-override-token-values\u0022\u003EMode Defaults May Override Token Values\u003C/h3\u003E\n\u003Cp\u003EThe effective \u003Ccode\u003EHybrid\u003C/code\u003E configuration explicitly sets:\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\u003EToken.AccessTokenLifetime = TimeSpan.FromMinutes(10);\nToken.RefreshTokenLifetime = TimeSpan.FromDays(7);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ETherefore, a different value configured globally may be replaced when the effective mode is resolved.\u003C/p\u003E\n\u003Cp\u003ETo customize the lifetime for \u003Ccode\u003EHybrid\u003C/code\u003E, use:\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.ConfigureMode(UAuthMode.Hybrid, mode =\u0026gt;\n {\n mode.Token.AccessTokenLifetime = TimeSpan.FromMinutes(5);\n });\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis distinction is essential when configuring token lifetimes.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022pkce-options\u0022\u003E6. PKCE Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Pkce\u003C/code\u003E configures the lifetime of PKCE authorization codes.\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Pkce.AuthorizationCodeLifetimeSeconds = 120;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\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\u003EDefault\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\u003EAuthorizationCodeLifetimeSeconds\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E120\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELifetime of a PKCE authorization code\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EPKCE is particularly important for browser-based clients such as Blazor WebAssembly.\u003C/p\u003E\n\u003Cp\u003EA shorter authorization code lifetime reduces the period during which an intercepted or exposed code could potentially be used.\u003C/p\u003E\n\u003Cp\u003EHowever, the value must remain long enough for legitimate authorization exchanges to complete.\u003C/p\u003E\n\u003Cp\u003EThe server\u0027s PKCE configuration is separate from the client\u0027s PKCE flow settings.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022authentication-modes\u0022\u003E7. Authentication Modes\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth supports authentication mode resolution based on the client profile.\u003C/p\u003E\n\u003Cp\u003EThe server exposes:\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\u003Eoptions.AllowedModes\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis property restricts the modes the server is permitted to use.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022restrict-allowed-modes\u0022\u003ERestrict Allowed Modes\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.AllowedModes =\n [\n UAuthMode.PureOpaque,\n UAuthMode.Hybrid\n ];\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EAllowedModes\u003C/code\u003E is a restriction, not a mode selector.\u003C/p\u003E\n\u003Cp\u003EThe effective mode is resolved by \u003Ccode\u003EIEffectiveAuthModeResolver\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022default-mode-resolution\u0022\u003EDefault Mode Resolution\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EClient profile\u003C/th\u003E\n\u003Cth\u003EDefault resolved mode\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EBlazorServer\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EPureOpaque\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EWebServer\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EHybrid\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EBlazorWasm\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EHybrid\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaui\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EHybrid\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EApi\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EPureJwt\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe current implementation has mode templates for \u003Ccode\u003EPureOpaque\u003C/code\u003E, \u003Ccode\u003EHybrid\u003C/code\u003E, \u003Ccode\u003ESemiHybrid\u003C/code\u003E, and \u003Ccode\u003EPureJwt\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EHowever, \u003Ccode\u003ESemiHybrid\u003C/code\u003E and \u003Ccode\u003EPureJwt\u003C/code\u003E are not yet accepted as enabled modes by the current server options validator.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EDo not enable an unsupported mode merely because it exists in the enum or has a mode-default template.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EThe default \u003Ccode\u003EApi\u003C/code\u003E profile mapping to \u003Ccode\u003EPureJwt\u003C/code\u003E is a current compatibility limitation to consider when deploying API-profile authentication flows.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configure-mode-specific-behavior\u0022\u003EConfigure Mode-Specific Behavior\u003C/h3\u003E\n\u003Cp\u003EUse \u003Ccode\u003EConfigureMode()\u003C/code\u003E for behavior that should differ by mode.\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.ConfigureMode(UAuthMode.PureOpaque, mode =\u0026gt;\n {\n mode.Session.IdleTimeout = TimeSpan.FromDays(7);\n });\n\n options.ConfigureMode(UAuthMode.Hybrid, mode =\u0026gt;\n {\n mode.Token.AccessTokenLifetime = TimeSpan.FromMinutes(5);\n });\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe mode override runs after the built-in mode defaults.\u003C/p\u003E\n\u003Cp\u003ESee \u003Ca href=\u0022/docs/configuration/advanced-configuration\u0022\u003EAdvanced Configuration\u003C/a\u003E for details.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022endpoint-options\u0022\u003E8. Endpoint Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Endpoints\u003C/code\u003E controls endpoint paths, feature groups, and individual action restrictions.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022base-path\u0022\u003EBase Path\u003C/h3\u003E\n\u003Cp\u003EThe default server endpoint base path 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\u003E/auth\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EConfigure it 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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Endpoints.BasePath = \u0026quot;/auth\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EChanging the base path can help avoid route conflicts when integrating UltimateAuth into an existing application.\u003C/p\u003E\n\u003Cp\u003EThe client must use compatible endpoint paths.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022endpoint-groups\u0022\u003EEndpoint Groups\u003C/h3\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\u003EDefault\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\u003EAuthentication\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication endpoints\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPkce\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPKCE endpoints\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESession\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESession management endpoints\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserLifecycle\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUser lifecycle endpoints\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserProfile\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUser profile endpoints\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserIdentifier\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifier management endpoints\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECredentials\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECredential management endpoints\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAuthorization\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthorization endpoints\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Endpoints.Authentication = true;\n options.Endpoints.Session = true;\n options.Endpoints.Authorization = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EDisabling an endpoint group controls endpoint exposure. It does not automatically remove the underlying domain services or invalidate previously issued credentials.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022disable-individual-actions\u0022\u003EDisable Individual Actions\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EDisabledActions\u003C/code\u003E is a collection of action identifiers:\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Endpoints.DisabledActions.Add(\u0026quot;users.create.anonymous\u0026quot;);\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe string must match the action identifier used by the server\u0027s endpoint registration and action policy.\u003C/p\u003E\n\u003Cp\u003EDo not pass a C# constant name as a string unless that exact text is also the registered action value.\u003C/p\u003E\n\u003Cp\u003EFor example, these are not necessarily equivalent:\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\u003E\u0026quot;UAuthActions.Users.Create.Anonymous\u0026quot;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Eand:\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\u003E\u0026quot;users.create.anonymous\u0026quot;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUse the actual action value defined by the corresponding \u003Ccode\u003EUAuthActions\u003C/code\u003E constant.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022endpoint-hardening\u0022\u003EEndpoint Hardening\u003C/h3\u003E\n\u003Cp\u003EDisable endpoints that are not required by the application.\u003C/p\u003E\n\u003Cp\u003EFor example, an application that delegates user provisioning to administrators may choose to disable anonymous user creation.\u003C/p\u003E\n\u003Cp\u003EEndpoint exposure is a separate concern from authorization: an exposed endpoint must still enforce the applicable security policy.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022session-resolution\u0022\u003E9. Session Resolution\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.SessionResolution\u003C/code\u003E controls how incoming requests provide session identifiers.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth supports four resolution sources:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EBearer credentials.\u003C/li\u003E\n\u003Cli\u003ECustom headers.\u003C/li\u003E\n\u003Cli\u003ECookies.\u003C/li\u003E\n\u003Cli\u003EQuery parameters.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022default-configuration\u0022\u003EDefault Configuration\u003C/h3\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\u003EDefault\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableBearer\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableHeader\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableCookie\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableQuery\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EHeaderName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EX-UAuth-Session\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EQueryParameterName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Esession_id\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe default precedence order 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\u003EBearer\nHeader\nCookie\nQuery\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configure-resolution-sources\u0022\u003EConfigure Resolution Sources\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.SessionResolution.EnableBearer = true;\n options.SessionResolution.EnableHeader = false;\n options.SessionResolution.EnableCookie = true;\n options.SessionResolution.EnableQuery = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis example disables custom-header and query-string session resolution.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022change-resolution-order\u0022\u003EChange Resolution Order\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.SessionResolution.Order =\n [\n \u0026quot;Cookie\u0026quot;,\n \u0026quot;Bearer\u0026quot;,\n \u0026quot;Header\u0026quot;,\n \u0026quot;Query\u0026quot;\n ];\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EResolution order matters when a request contains multiple candidate credentials.\u003C/p\u003E\n\u003Cp\u003EThe selected credential must still pass authentication and session validation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-recommendation\u0022\u003ESecurity Recommendation\u003C/h3\u003E\n\u003Cp\u003EAvoid query-string credentials unless the application has a specific, well-understood requirement.\u003C/p\u003E\n\u003Cp\u003EURLs may be recorded in browser history, reverse-proxy logs, analytics systems, and other infrastructure.\u003C/p\u003E\n\u003Cp\u003EFor most browser and API integrations, cookies or authorization headers are preferable.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022cookie-configuration\u0022\u003E10. Cookie Configuration\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Cookie\u003C/code\u003E controls cookies used for authentication credentials.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth defines three cookie groups:\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\u003Eoptions.Cookie.Session\noptions.Cookie.AccessToken\noptions.Cookie.RefreshToken\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022default-cookie-names\u0022\u003EDefault Cookie Names\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003ECookie\u003C/th\u003E\n\u003Cth\u003EDefault name\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ESession\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Euas\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAccess token\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Euat\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ERefresh token\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Euar\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe default cookie options use:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003EHttpOnly = true\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003ESecurePolicy = CookieSecurePolicy.Always\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EPath = \u0026quot;/\u0026quot;\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EDomain = null\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EMaxAge = null\u003C/code\u003E\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003E\u003Ccode\u003ESameSite\u003C/code\u003E is nullable and can be resolved through the applicable cookie and deployment policies.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022customize-cookie-names\u0022\u003ECustomize Cookie Names\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Cookie.Session.Name = \u0026quot;myapp_session\u0026quot;;\n options.Cookie.RefreshToken.Name = \u0026quot;myapp_refresh\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configure-cookie-scope\u0022\u003EConfigure Cookie Scope\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Cookie.Session.Path = \u0026quot;/\u0026quot;;\n options.Cookie.Session.Domain = null;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EA null domain produces a host-only cookie.\u003C/p\u003E\n\u003Cp\u003ESetting a parent domain can broaden cookie availability to subdomains and should be done only when necessary.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022cookie-security\u0022\u003ECookie Security\u003C/h3\u003E\n\u003Cp\u003EAvoid disabling \u003Ccode\u003EHttpOnly\u003C/code\u003E or relaxing secure transport requirements for authentication cookies.\u003C/p\u003E\n\u003Cp\u003ECookie settings interact with:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EThe deployment mode.\u003C/li\u003E\n\u003Cli\u003ECross-site requests.\u003C/li\u003E\n\u003Cli\u003EBrowser cookie policies.\u003C/li\u003E\n\u003Cli\u003ECredential delivery configuration.\u003C/li\u003E\n\u003Cli\u003EHTTPS requirements.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EFor cross-site deployments, configure cookie behavior together with CORS and the relevant browser security requirements.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022hub-deployment-mode\u0022\u003E11. Hub Deployment Mode\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.HubDeploymentMode\u003C/code\u003E describes how UAuthHub is deployed relative to the application.\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.HubDeploymentMode = UAuthHubDeploymentMode.Embedded;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-modes\u0022\u003EAvailable Modes\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EMode\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\u003EEmbedded\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EHub and application share the same origin\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIntegrated\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EHub is separate but within the same-site boundary\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EExternal\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EHub is hosted on a different site or domain\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe default 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\u003EUAuthHubDeploymentMode.Integrated\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022embedded\u0022\u003EEmbedded\u003C/h3\u003E\n\u003Cp\u003ESuitable for applications hosting UltimateAuth within the same application and origin.\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\u003Eoptions.HubDeploymentMode = UAuthHubDeploymentMode.Embedded;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022integrated\u0022\u003EIntegrated\u003C/h3\u003E\n\u003Cp\u003ESuitable for deployments such as:\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\u003Eauth.example.com\napp.example.com\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Ewhere the authentication service and application are separate origins within the same site.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022external\u0022\u003EExternal\u003C/h3\u003E\n\u003Cp\u003ESuitable for a separate authentication provider domain.\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\u003Eoptions.HubDeploymentMode = UAuthHubDeploymentMode.External;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EExternal deployment requires careful coordination of cookie policies, redirects, allowed origins, and browser restrictions.\u003C/p\u003E\n\u003Cp\u003EThe deployment mode influences authentication transport behavior; it is not merely descriptive metadata.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022hub-options\u0022\u003E12. Hub Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Hub\u003C/code\u003E configures UAuthHub integration.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-properties-2\u0022\u003EAvailable Properties\u003C/h3\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\u003EDefault\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\u003EClientBaseAddress\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional client application base address\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAllowedClientOrigins\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEmpty set\u003C/td\u003E\n\u003Ctd\u003EAllowed client origins\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EFlowLifetime\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E5 minutes\u003C/td\u003E\n\u003Ctd\u003ELifetime of Hub flow artifacts\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELoginPath\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E/login\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EHub login page path\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=\u0022example\u0022\u003EExample\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Hub.ClientBaseAddress = \u0026quot;https://app.example.com\u0026quot;;\n options.Hub.AllowedClientOrigins.Add(\u0026quot;https://app.example.com\u0026quot;);\n options.Hub.FlowLifetime = TimeSpan.FromMinutes(5);\n options.Hub.LoginPath = \u0026quot;/login\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe Hub flow lifetime is not the same as the authenticated session lifetime.\u003C/p\u003E\n\u003Cp\u003EHub artifacts support authentication orchestration and should generally be short-lived.\u003C/p\u003E\n\u003Cp\u003EOnly trusted origins should be included in \u003Ccode\u003EAllowedClientOrigins\u003C/code\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022primary-credential-policy\u0022\u003E13. Primary Credential Policy\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.PrimaryCredential\u003C/code\u003E controls which credential kind is preferred for UI and API requests.\u003C/p\u003E\n\u003Cp\u003EThe defaults are:\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\u003EDefault\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUi\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EPrimaryTokenKind.Session\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EApi\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EPrimaryTokenKind.AccessToken\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=\u0022example-1\u0022\u003EExample\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.PrimaryCredential.Ui = PrimaryTokenKind.Session;\n options.PrimaryCredential.Api = PrimaryTokenKind.AccessToken;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis allows UltimateAuth to distinguish between session-oriented browser interactions and access-token-oriented API interactions.\u003C/p\u003E\n\u003Cp\u003EThe primary credential policy does not bypass authentication validation.\u003C/p\u003E\n\u003Cp\u003EIt determines which credential kind should be preferred within the applicable request-processing rules.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022credential-session-policy\u0022\u003E14. Credential Session Policy\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.CredentialSessionPolicy\u003C/code\u003E determines what happens to active session chains after credential changes.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-properties-3\u0022\u003EAvailable Properties\u003C/h3\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\u003EDefault\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\u003EOnSelfChange\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EPreserveCurrent\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUser changes their own credential\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EOnAdminChange\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003ERevokeAll\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAdministrator changes a user\u0027s credential\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EOnRecovery\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003ERevokeAll\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECredential recovery completes\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=\u0022example-2\u0022\u003EExample\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.CredentialSessionPolicy.OnSelfChange = CredentialSessionBehavior.PreserveCurrent;\n options.CredentialSessionPolicy.OnAdminChange = CredentialSessionBehavior.RevokeAll;\n options.CredentialSessionPolicy.OnRecovery = CredentialSessionBehavior.RevokeAll;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-behaviors\u0022\u003EAvailable Behaviors\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EBehavior\u003C/th\u003E\n\u003Cth\u003EEffect\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPreserveAll\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EKeep existing session chains\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPreserveCurrent\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EKeep the current chain and revoke other applicable chains\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERevokeAll\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERevoke all applicable session chains\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EFor recovery flows, \u003Ccode\u003EPreserveCurrent\u003C/code\u003E is not always meaningful because the user may not have an authenticated current session.\u003C/p\u003E\n\u003Cp\u003EThe current implementation treats this situation using the more restrictive \u003Ccode\u003ERevokeAll\u003C/code\u003E behavior.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-recommendation-1\u0022\u003ESecurity Recommendation\u003C/h3\u003E\n\u003Cp\u003EPreserving the current session after a normal password change can provide a reasonable user experience.\u003C/p\u003E\n\u003Cp\u003ERevoking all sessions after administrative credential changes or account recovery is generally more conservative.\u003C/p\u003E\n\u003Cp\u003EChoose the policy according to the application\u0027s security requirements.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022credential-reset-options\u0022\u003E15. Credential Reset Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.ResetCredential\u003C/code\u003E configures credential recovery proof lifetimes and attempt limits.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022defaults\u0022\u003EDefaults\u003C/h3\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\u003EDefault\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\u003ETokenValidity\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E30 minutes\u003C/td\u003E\n\u003Ctd\u003EReset token lifetime\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECodeValidity\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E10 minutes\u003C/td\u003E\n\u003Ctd\u003ENumeric reset code lifetime\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxAttempts\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E3\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EMaximum permitted recovery attempts\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECodeLength\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E6\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENumeric reset code length\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=\u0022example-3\u0022\u003EExample\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.ResetCredential.TokenValidity = TimeSpan.FromMinutes(20);\n options.ResetCredential.CodeValidity = TimeSpan.FromMinutes(5);\n options.ResetCredential.MaxAttempts = 3;\n options.ResetCredential.CodeLength = 6;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003ECodeLength\u003C/code\u003E applies to numeric reset codes, not token-based recovery proofs.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022important-security-boundary\u0022\u003EImportant Security Boundary\u003C/h3\u003E\n\u003Cp\u003EReset proofs must be delivered through a trusted server-side mechanism.\u003C/p\u003E\n\u003Cp\u003EDo not return recovery tokens to anonymous callers simply to simplify a client-side reset form.\u003C/p\u003E\n\u003Cp\u003EThe application should provide the appropriate \u003Ccode\u003EICredentialResetNotifier\u003C/code\u003E implementation for supported recovery flows.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022login-identifier-options\u0022\u003E16. Login Identifier Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.LoginIdentifiers\u003C/code\u003E determines which identifier types can be used for login.\u003C/p\u003E\n\u003Cp\u003EThe default allowed types are:\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\u003EUserIdentifierType.Username\nUserIdentifierType.Email\nUserIdentifierType.Phone\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022restrict-login-identifier-types\u0022\u003ERestrict Login Identifier Types\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.LoginIdentifiers.AllowedTypes =\n new HashSet\u0026lt;UserIdentifierType\u0026gt;\n {\n UserIdentifierType.Email\n };\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis configures email as the allowed login identifier type.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022require-verification\u0022\u003ERequire Verification\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.LoginIdentifiers.RequireVerificationForEmail = true;\n options.LoginIdentifiers.RequireVerificationForPhone = true;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThese settings control whether the corresponding identifiers must be verified before being accepted for login.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022custom-resolvers\u0022\u003ECustom Resolvers\u003C/h3\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\u003EDefault\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableCustomResolvers\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECustomResolversFirst\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThese options control the participation and ordering of custom login identifier resolvers.\u003C/p\u003E\n\u003Cp\u003EAn identifier existing on a user account does not necessarily mean it is permitted for login.\u003C/p\u003E\n\u003Cp\u003EIdentifier management and login eligibility are separate concerns.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022identifier-validation\u0022\u003E17. Identifier Validation\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.IdentifierValidation\u003C/code\u003E provides rules for usernames, email addresses, and phone numbers.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022username-rules\u0022\u003EUsername Rules\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.IdentifierValidation.UserName.Enabled = true;\n options.IdentifierValidation.UserName.MinLength = 3;\n options.IdentifierValidation.UserName.MaxLength = 32;\n options.IdentifierValidation.UserName.AllowedRegex = \u0026quot;^[a-zA-Z0-9._-]\u002B$\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022default-validation-rules\u0022\u003EDefault Validation Rules\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EIdentifier\u003C/th\u003E\n\u003Cth\u003EEnabled\u003C/th\u003E\n\u003Cth\u003EMinimum length\u003C/th\u003E\n\u003Cth\u003EMaximum length\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EUsername\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E3\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E64\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EEmail\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E3\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E256\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EPhone\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E3\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E24\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EUsername validation additionally supports \u003Ccode\u003EAllowedRegex\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe default username expression 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\u003EREGEX\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-regex\u0022\u003E^[a-zA-Z0-9._-]\u002B$\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022identifier-behavior\u0022\u003EIdentifier Behavior\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Identifiers\u003C/code\u003E contains additional groups:\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\u003Eoptions.Identifiers.Behavior\noptions.Identifiers.Validation\noptions.Identifiers.Normalization\noptions.Identifiers.Uniqueness\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThese configure identifier behavior beyond the top-level validation rules.\u003C/p\u003E\n\u003Cp\u003EIdentifier normalization and uniqueness are particularly important because they affect how duplicate identifiers are detected.\u003C/p\u003E\n\u003Cp\u003EUse the provided options rather than implementing inconsistent normalization independently in the client.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022identifier-limits\u0022\u003EIdentifier Limits\u003C/h3\u003E\n\u003Cp\u003EUltimateAuth supports configurable limits on the number of identifiers associated with each user.\u003C/p\u003E\n\u003Cp\u003EBy default, a user can have up to \u003Cstrong\u003E10 active identifiers\u003C/strong\u003E, regardless of their type.\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\u003Eoptions.Identifiers.Behavior.MaxIdentifiersPerUser = 10;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis is a global per-user limit. It applies to the combined number of usernames, email addresses, phone numbers, and custom identifiers.\u003C/p\u003E\n\u003Ch4 id=\u0022optional-per-type-limits\u0022\u003EOptional Per-Type Limits\u003C/h4\u003E\n\u003Cp\u003EApplications can optionally define limits for individual identifier types:\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\u003Eoptions.Identifiers.Behavior.MaxIdentifiersPerUser = 10;\n\noptions.Identifiers.Behavior.MaxIdentifiersPerType = new()\n{\n [UserIdentifierType.Username] = 2,\n [UserIdentifierType.Email] = 5,\n [UserIdentifierType.Phone] = 3\n};\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Cstrong\u003EPer-type limits are not configured by default.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EWhen no per-type limit is specified, only the global limit applies.\u003C/p\u003E\n\u003Cp\u003EFor example, with the configuration above, a user may have up to five email addresses, but the combined number of all active identifiers cannot exceed ten.\u003C/p\u003E\n\u003Ch4 id=\u0022default-behavior\u0022\u003EDefault Behavior\u003C/h4\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EOption\u003C/th\u003E\n\u003Cth\u003EDefault\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\u003EMaxIdentifiersPerUser\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E10\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EMaximum total active identifiers per user\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxIdentifiersPerType\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEmpty dictionary\u003C/td\u003E\n\u003Ctd\u003EOptional limits for individual identifier types\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe following rules apply:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EPrimary and non-primary identifiers count equally.\u003C/li\u003E\n\u003Cli\u003ESoft-deleted identifiers do not count toward active limits.\u003C/li\u003E\n\u003Cli\u003ESelf-service and administrative operations follow the same limits.\u003C/li\u003E\n\u003Cli\u003EPer-type limits do not override the global limit.\u003C/li\u003E\n\u003Cli\u003EExisting identifier behavior options remain applicable.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EFor example, \u003Ccode\u003EAllowMultipleUsernames = false\u003C/code\u003E still prevents multiple usernames, even when the configured username limit is greater than one.\u003C/p\u003E\n\u003Ch4 id=\u0022limit-validation\u0022\u003ELimit Validation\u003C/h4\u003E\n\u003Cp\u003EWhen an identifier addition exceeds a configured limit, the operation is rejected with a validation error.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EError Code\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\u003Ccode\u003Eidentifier_total_limit_exceeded\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EThe user has reached the global identifier limit\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003Eidentifier_type_limit_exceeded\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EThe user has reached the configured limit for that identifier type\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EApplications should handle these errors and display appropriate feedback.\u003C/p\u003E\n\u003Cp\u003EIdentifier limits are enforced by the Users application layer rather than individual persistence stores.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022user-profile-options\u0022\u003E18. User Profile Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.UserProfile\u003C/code\u003E controls profile-related capabilities.\u003C/p\u003E\n\u003Cp\u003EThe current option 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\u003Eoptions.UserProfile.EnableMultiProfile\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EIts default is \u003Ccode\u003Efalse\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022enable-multiple-profiles\u0022\u003EEnable Multiple Profiles\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.UserProfile.EnableMultiProfile = true;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis allows applications to use UltimateAuth\u0027s multiple-profile capability where supported by the user domain.\u003C/p\u003E\n\u003Cp\u003EFor example, an application may maintain separate profile contexts for different purposes.\u003C/p\u003E\n\u003Cp\u003EMulti-profile support does not create separate user identities or independent authentication accounts.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022multi-tenant-options\u0022\u003E19. Multi-Tenant Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.MultiTenant\u003C/code\u003E controls whether tenant-aware behavior is enabled and how tenant identifiers are resolved.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-properties-4\u0022\u003EAvailable Properties\u003C/h3\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\u003EDefault\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\u003EEnabled\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEnable multi-tenant behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ENormalizeToLowercase\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENormalize tenant identifiers\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableRoute\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EResolve tenants from routes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableHeader\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EResolve tenants from headers\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableDomain\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EResolve tenants from hosts\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EHeaderName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EX-Tenant\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETenant header name\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=\u0022example-header-based-tenant-resolution\u0022\u003EExample: Header-Based Tenant Resolution\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.MultiTenant.Enabled = true;\n options.MultiTenant.EnableHeader = true;\n options.MultiTenant.HeaderName = \u0026quot;X-Tenant\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-domain-based-resolution\u0022\u003EExample: Domain-Based Resolution\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.MultiTenant.Enabled = true;\n options.MultiTenant.EnableDomain = true;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EMultiple resolution mechanisms can be enabled.\u003C/p\u003E\n\u003Cp\u003EThe server composes the configured tenant resolvers according to its resolution infrastructure.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-boundary\u0022\u003ESecurity Boundary\u003C/h3\u003E\n\u003Cp\u003ETenant identification is not tenant authorization.\u003C/p\u003E\n\u003Cp\u003EA request providing another tenant identifier must not gain access to that tenant without appropriate authorization.\u003C/p\u003E\n\u003Cp\u003EThe server remains responsible for tenant isolation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022pagination-options\u0022\u003E20. Pagination Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Pagination\u003C/code\u003E controls pagination defaults and maximum page sizes.\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\u003EDefault\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDefaultPageSize\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E250\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxPageSize\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E1000\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=\u0022example-4\u0022\u003EExample\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Pagination.DefaultPageSize = 50;\n options.Pagination.MaxPageSize = 200;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThese settings affect paginated queries exposed by UltimateAuth domain services.\u003C/p\u003E\n\u003Cp\u003EFor administrative interfaces, choosing a moderate page size can reduce unnecessary data transfer and processing.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022navigation-options\u0022\u003E21. Navigation Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Navigation\u003C/code\u003E configures automatic authentication navigation behavior.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-properties-5\u0022\u003EAvailable Properties\u003C/h3\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\u003EDefault\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableAutomaticNavigationRedirect\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELoginResolver\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAccessDeniedResolver\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\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=\u0022disable-automatic-redirects\u0022\u003EDisable Automatic Redirects\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Navigation.EnableAutomaticNavigationRedirect = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022custom-login-redirect\u0022\u003ECustom Login Redirect\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Navigation.LoginResolver = context =\u0026gt;\n {\n return \u0026quot;/account/login\u0026quot;;\n };\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022custom-access-denied-redirect\u0022\u003ECustom Access Denied Redirect\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Navigation.AccessDeniedResolver = context =\u0026gt;\n {\n return \u0026quot;/account/access-denied\u0026quot;;\n };\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThese delegates receive the current \u003Ccode\u003EHttpContext\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe application should return valid, trusted navigation destinations.\u003C/p\u003E\n\u003Cp\u003EAvoid constructing redirects from unvalidated user-controlled URLs.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022authentication-response-options\u0022\u003E22. Authentication Response Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.AuthResponse\u003C/code\u003E controls how authentication credentials and redirect behavior are represented in responses.\u003C/p\u003E\n\u003Cp\u003EThe available groups are:\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\u003Eoptions.AuthResponse.SessionIdDelivery\noptions.AuthResponse.AccessTokenDelivery\noptions.AuthResponse.RefreshTokenDelivery\n\noptions.AuthResponse.Login\noptions.AuthResponse.Logout\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThese settings govern credential delivery and authentication response behavior.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022important-mode-specific-response-defaults\u0022\u003EImportant: Mode-Specific Response Defaults\u003C/h3\u003E\n\u003Cp\u003EUltimateAuth provides response templates for authentication modes.\u003C/p\u003E\n\u003Cp\u003EFor example, \u003Ccode\u003EPureOpaque\u003C/code\u003E uses a session-oriented cookie response, while \u003Ccode\u003EHybrid\u003C/code\u003E combines session and token delivery behavior.\u003C/p\u003E\n\u003Cp\u003EThe effective response may therefore depend on the resolved authentication mode and flow.\u003C/p\u003E\n\u003Cp\u003EDo not assume that a single globally configured response delivery setting applies unchanged to every authentication mode.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-considerations\u0022\u003ESecurity Considerations\u003C/h3\u003E\n\u003Cp\u003ECredential delivery settings determine whether sensitive authentication material is exposed through cookies, headers, or other response mechanisms.\u003C/p\u003E\n\u003Cp\u003EOnly customize them when the application has a clear requirement and the selected transport is appropriate for the client platform.\u003C/p\u003E\n\u003Cp\u003EFor advanced response customization, see \u003Ca href=\u0022/docs/configuration/advanced-configuration\u0022\u003EAdvanced Configuration\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022diagnostics-options\u0022\u003E23. Diagnostics Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Diagnostics\u003C/code\u003E provides optional diagnostic behavior.\u003C/p\u003E\n\u003Cp\u003EThe current property 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\u003Eoptions.Diagnostics.EnableRefreshDetails\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EIts default is \u003Ccode\u003Efalse\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-5\u0022\u003EExample\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Diagnostics.EnableRefreshDetails = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWhen enabled, the option allows additional refresh-related diagnostic response information, such as the \u003Ccode\u003EX-UAuth-Refresh\u003C/code\u003E header.\u003C/p\u003E\n\u003Cp\u003EThis is primarily useful during development and debugging.\u003C/p\u003E\n\u003Cp\u003EAvoid enabling detailed authentication diagnostics in production unless the exposure is understood and justified.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022authentication-lifecycle-events\u0022\u003E24. Authentication Lifecycle Events\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Events\u003C/code\u003E exposes server-side authentication lifecycle callbacks.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-events\u0022\u003EAvailable Events\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\u003Eoptions.Events.OnAnyEvent\noptions.Events.OnUserLoggedIn\noptions.Events.OnUserLoggedOut\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-login-logging\u0022\u003EExample: Login Logging\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Events.OnUserLoggedIn = context =\u0026gt;\n {\n // Record the successful login.\n return Task.CompletedTask;\n };\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-global-event-handler\u0022\u003EExample: Global Event Handler\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Events.OnAnyEvent = context =\u0026gt;\n {\n // Process an authentication lifecycle event.\n return Task.CompletedTask;\n };\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ELifecycle events are intended for lightweight reactions such as logging, metrics, and notifications.\u003C/p\u003E\n\u003Cp\u003EThey should not replace security decisions, authentication validation, or application policies.\u003C/p\u003E\n\u003Cp\u003ESee \u003Ca href=\u0022/docs/configuration/advanced-configuration\u0022\u003EAdvanced Configuration\u003C/a\u003E for event and extension guidance.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022effective-server-options\u0022\u003E25. Effective Server Options\u003C/h2\u003E\n\u003Cp\u003EThe configured \u003Ccode\u003EUAuthServerOptions\u003C/code\u003E instance is the base configuration.\u003C/p\u003E\n\u003Cp\u003EAt runtime, UltimateAuth can resolve:\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\u003EEffectiveUAuthServerOptions\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe current resolution process 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\u003EConfigured UAuthServerOptions\n |\n v\n Resolve auth mode\n |\n v\n Clone base options\n |\n v\n Apply mode defaults\n |\n v\n Apply ConfigureMode overrides\n |\n v\n EffectiveUAuthServerOptions\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis means that the effective settings used for a \u003Ccode\u003EHybrid\u003C/code\u003E client can differ from those used for a \u003Ccode\u003EPureOpaque\u003C/code\u003E client.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-6\u0022\u003EExample\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Session.Lifetime = TimeSpan.FromDays(7);\n\n options.ConfigureMode(UAuthMode.Hybrid, mode =\u0026gt;\n {\n mode.Token.AccessTokenLifetime = TimeSpan.FromMinutes(5);\n });\n\n options.ConfigureMode(UAuthMode.PureOpaque, mode =\u0026gt;\n {\n mode.Session.IdleTimeout = TimeSpan.FromDays(3);\n });\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe original options remain the common starting point.\u003C/p\u003E\n\u003Cp\u003EMode defaults and overrides determine the effective values for the resolved mode.\u003C/p\u003E\n\u003Cp\u003EFor implementation details and inspection of effective options, see \u003Ca href=\u0022/docs/configuration/advanced-configuration\u0022\u003EAdvanced Configuration\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022validation-and-configuration-safety\u0022\u003E26. Validation and Configuration Safety\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth Server registers multiple options validators and enables startup validation.\u003C/p\u003E\n\u003Cp\u003EThese validators cover areas including:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003ELogin settings.\u003C/li\u003E\n\u003Cli\u003ESession settings.\u003C/li\u003E\n\u003Cli\u003EToken settings.\u003C/li\u003E\n\u003Cli\u003EPKCE.\u003C/li\u003E\n\u003Cli\u003EMulti-tenancy.\u003C/li\u003E\n\u003Cli\u003EIdentifier configuration.\u003C/li\u003E\n\u003Cli\u003ESession resolution.\u003C/li\u003E\n\u003Cli\u003EPagination.\u003C/li\u003E\n\u003Cli\u003EGeneral server configuration.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-invalid-session-lifetime\u0022\u003EExample: Invalid Session Lifetime\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Session.Lifetime = TimeSpan.Zero;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe server rejects an invalid session lifetime under the applicable validation rules.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-invalid-base-path\u0022\u003EExample: Invalid Base Path\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Endpoints.BasePath = \u0026quot;//auth\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe general server validator rejects paths containing \u003Ccode\u003E//\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022unsupported-modes\u0022\u003EUnsupported Modes\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.AllowedModes =\n [\n UAuthMode.PureJwt\n ];\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe current validator rejects explicitly enabling \u003Ccode\u003EPureJwt\u003C/code\u003E because that mode is not yet implemented as a supported server mode.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022validation-limitations\u0022\u003EValidation Limitations\u003C/h3\u003E\n\u003Cp\u003EStartup validation protects the configured options according to the registered validators.\u003C/p\u003E\n\u003Cp\u003EIt does not guarantee that every custom service implementation or mode-specific override is secure.\u003C/p\u003E\n\u003Cp\u003EWhen using advanced customization, test the resulting effective configuration and authentication flows.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022complete-configuration-example\u0022\u003E27. Complete Configuration Example\u003C/h2\u003E\n\u003Cp\u003EThe following example combines several commonly used server settings.\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n // Authentication modes\n options.AllowedModes =\n [\n UAuthMode.PureOpaque,\n UAuthMode.Hybrid\n ];\n\n // Login security\n options.Login.MaxFailedAttempts = 5;\n options.Login.LockoutDuration = TimeSpan.FromMinutes(15);\n options.Login.IncludeFailureDetails = false;\n\n // Sessions\n options.Session.Lifetime = TimeSpan.FromDays(7);\n options.Session.MaxLifetime = TimeSpan.FromDays(30);\n\n // Token identity\n options.Token.Issuer = \u0026quot;MyApplication\u0026quot;;\n options.Token.Audience = \u0026quot;MyApplicationApi\u0026quot;;\n\n // Hybrid-specific token lifetime\n options.ConfigureMode(UAuthMode.Hybrid, mode =\u0026gt;\n {\n mode.Token.AccessTokenLifetime = TimeSpan.FromMinutes(10);\n });\n\n // PKCE\n options.Pkce.AuthorizationCodeLifetimeSeconds = 120;\n\n // Hub deployment\n options.HubDeploymentMode = UAuthHubDeploymentMode.Integrated;\n options.Hub.ClientBaseAddress = \u0026quot;https://app.example.com\u0026quot;;\n options.Hub.AllowedClientOrigins.Add(\u0026quot;https://app.example.com\u0026quot;);\n\n // Credential security\n options.CredentialSessionPolicy.OnSelfChange = CredentialSessionBehavior.PreserveCurrent;\n options.CredentialSessionPolicy.OnAdminChange = CredentialSessionBehavior.RevokeAll;\n options.CredentialSessionPolicy.OnRecovery = CredentialSessionBehavior.RevokeAll;\n\n // Credential recovery\n options.ResetCredential.TokenValidity = TimeSpan.FromMinutes(30);\n options.ResetCredential.MaxAttempts = 3;\n\n // Identifier login rules\n options.LoginIdentifiers.RequireVerificationForEmail = true;\n\n // Session transport\n options.SessionResolution.EnableCookie = true;\n options.SessionResolution.EnableBearer = true;\n options.SessionResolution.EnableQuery = false;\n\n // Endpoint exposure\n options.Endpoints.Authentication = true;\n options.Endpoints.Session = true;\n options.Endpoints.Credentials = true;\n options.Endpoints.Authorization = true;\n\n // Pagination\n options.Pagination.DefaultPageSize = 50;\n options.Pagination.MaxPageSize = 200;\n\n // Diagnostics\n options.Diagnostics.EnableRefreshDetails = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis is an illustrative configuration, not a universal production preset.\u003C/p\u003E\n\u003Cp\u003EApplications should choose session lifetimes, authentication modes, credential transport, tenant behavior, and endpoint exposure according to their own deployment and security requirements.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022common-configuration-mistakes\u0022\u003E28. Common Configuration Mistakes\u003C/h2\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configuring-core-options-separately\u0022\u003EConfiguring Core Options Separately\u003C/h3\u003E\n\u003Cp\u003EWhen using \u003Ccode\u003EAddUltimateAuthServer()\u003C/code\u003E, configure Core-defined options through \u003Ccode\u003EUAuthServerOptions\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EA separate \u003Ccode\u003EUAuthOptions\u003C/code\u003E configuration does not automatically override the server\u0027s composed options.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022assuming-allowedmodes-selects-the-mode\u0022\u003EAssuming AllowedModes Selects the Mode\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EAllowedModes\u003C/code\u003E is a restriction.\u003C/p\u003E\n\u003Cp\u003EMode selection is performed by the authentication mode resolver.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022ignoring-mode-defaults\u0022\u003EIgnoring Mode Defaults\u003C/h3\u003E\n\u003Cp\u003EMode-specific defaults can replace globally configured values.\u003C/p\u003E\n\u003Cp\u003EUse \u003Ccode\u003EConfigureMode()\u003C/code\u003E for explicit mode-specific behavior.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022enabling-unsupported-modes\u0022\u003EEnabling Unsupported Modes\u003C/h3\u003E\n\u003Cp\u003EAn authentication mode appearing in an enum does not guarantee runtime support.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022treating-reserved-session-options-as-enforced-security-features\u0022\u003ETreating Reserved Session Options as Enforced Security Features\u003C/h3\u003E\n\u003Cp\u003ESome device and session-chain options are reserved for future enforcement.\u003C/p\u003E\n\u003Cp\u003EVerify actual runtime support before relying on them.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022exposing-unnecessary-endpoints\u0022\u003EExposing Unnecessary Endpoints\u003C/h3\u003E\n\u003Cp\u003EDisable endpoint groups or actions that the application does not require.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022allowing-query-string-credentials-without-a-specific-need\u0022\u003EAllowing Query-String Credentials Without a Specific Need\u003C/h3\u003E\n\u003Cp\u003EQuery-string credentials can leak through URLs and infrastructure logs.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022weakening-cookie-security\u0022\u003EWeakening Cookie Security\u003C/h3\u003E\n\u003Cp\u003EDo not relax secure transport or cookie protections without understanding the consequences.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022assuming-configuration-validation-covers-everything\u0022\u003EAssuming Configuration Validation Covers Everything\u003C/h3\u003E\n\u003Cp\u003EStartup validation does not replace testing, infrastructure security, or validation of custom extension implementations.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022server-options-quick-reference\u0022\u003E29. Server Options Quick Reference\u003C/h2\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EConfiguration group\u003C/th\u003E\n\u003Cth\u003ETypical use\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELogin\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELogin attempts and lockout\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESession\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESession expiration and lifecycle\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EToken\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EToken issuance and lifetime\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPkce\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPKCE code lifetime\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAllowedModes\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPermitted authentication modes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMultiTenant\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETenant resolution\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPagination\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EQuery page limits\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEndpoints\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEndpoint exposure\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESessionResolution\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECredential source precedence\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECookie\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECookie security and scope\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EHubDeploymentMode\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EHub deployment topology\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EHub\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EHub integration\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPrimaryCredential\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUI/API primary credential selection\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAuthResponse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECredential delivery\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECredentialSessionPolicy\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESession invalidation after credential changes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EResetCredential\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECredential recovery rules\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdentifiers\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifier behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdentifierValidation\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifier validation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELoginIdentifiers\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELogin identifier eligibility\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserProfile\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EMulti-profile capability\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ENavigation\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication redirects\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDiagnostics\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERefresh diagnostics\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEvents\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication lifecycle callbacks\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EConfigureMode()\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EMode-specific option overrides\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EOnConfigureEndpoints\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAdvanced endpoint customization\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\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/configuration/configuration\u0022\u003EConfiguration Overview\u003C/a\u003E \u2014 Understand the configuration architecture and precedence.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/configuration/client-options\u0022\u003EClient Options\u003C/a\u003E \u2014 Configure client SDK behavior.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/configuration/advanced-configuration\u0022\u003EAdvanced Configuration\u003C/a\u003E \u2014 Explore effective options, mode resolution, and extension points.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/client/session-management\u0022\u003ESession Management\u003C/a\u003E \u2014 Manage sessions through the client.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/client/authentication\u0022\u003EAuthentication\u003C/a\u003E \u2014 Use the client authentication flows.\u003C/li\u003E\n\u003C/ul\u003E\n", "Headings": [ { "Id": "register-ultimateauth-server", @@ -323,6 +323,11 @@ "Text": "Identifier Behavior", "Level": 1 }, + { + "Id": "identifier-limits", + "Text": "Identifier Limits", + "Level": 1 + }, { "Id": "user-profile-options", "Text": "18. User Profile Options", @@ -539,6 +544,6 @@ "Level": 0 } ], - "SourceHash": "35AE1CE661A0DCC3570C2FF5F8F104352B0D607D3B3BDB3C2286C0D9B78052EC", + "SourceHash": "F71BFB8BC585AF45F096355BDE4989040EEE7545BB8A3B476585E7B77731C19D", "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 1454edff..0c270314 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=\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", + "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-capacity-and-limits\u0022\u003EIdentifier Capacity and Limits\u003C/h3\u003E\n\u003Cp\u003EUltimateAuth allows users to maintain multiple identifiers while providing configurable limits to prevent uncontrolled identifier growth.\u003C/p\u003E\n\u003Cp\u003EThe default configuration allows \u003Cstrong\u003E10 active identifiers per user\u003C/strong\u003E, shared across all identifier types.\u003C/p\u003E\n\u003Cp\u003EApplications can optionally configure additional per-type limits when their identity model requires them.\u003C/p\u003E\n\u003Cp\u003EFor example, an application may allow several email addresses and phone numbers while restricting the number of usernames.\u003C/p\u003E\n\u003Cp\u003EIdentifier limits are independent of identifier uniqueness, ownership verification, and primary designation.\u003C/p\u003E\n\u003Cp\u003EA primary identifier consumes the same capacity as a non-primary identifier. Soft-deleted identifiers do not consume active capacity, although their normalized values may remain reserved under the configured uniqueness rules.\u003C/p\u003E\n\u003Cp\u003ELimits apply consistently to self-service and administrative identifier additions.\u003C/p\u003E\n\u003Cp\u003EThese rules belong to the Users application layer, keeping persistence implementations independent of application-specific identifier policies.\u003C/p\u003E\n\u003Cp\u003ESee \u003Ca href=\u0022/docs/configuration/server-options\u0022\u003EServer Options\u003C/a\u003E for configuration examples.\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", @@ -33,6 +33,11 @@ "Text": "User Identifiers", "Level": 0 }, + { + "Id": "identifier-capacity-and-limits", + "Text": "Identifier Capacity and Limits", + "Level": 1 + }, { "Id": "identifier-properties", "Text": "Identifier Properties", @@ -104,6 +109,6 @@ "Level": 0 } ], - "SourceHash": "E59EBD0AC1985AD4C0CDAB7844881C58E3DA3EE294DE089B2AE1B7C73A72A0FB", + "SourceHash": "ACF2BFBE24655F8060339AA6CFB29F4E67754306093E36BDECE73B0AD41881A0", "BuilderVersion": "1" } \ No newline at end of file diff --git a/src/CodeBeam.UltimateAuth.Server/Options/UAuthIdentifierBehaviorOptions.cs b/src/CodeBeam.UltimateAuth.Server/Options/UAuthIdentifierBehaviorOptions.cs index cbe7e9ee..68dee600 100644 --- a/src/CodeBeam.UltimateAuth.Server/Options/UAuthIdentifierBehaviorOptions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Options/UAuthIdentifierBehaviorOptions.cs @@ -1,4 +1,6 @@ -namespace CodeBeam.UltimateAuth.Server.Options; +using CodeBeam.UltimateAuth.Users.Contracts; + +namespace CodeBeam.UltimateAuth.Server.Options; public sealed class UAuthIdentifierBehaviorOptions { @@ -16,6 +18,9 @@ public sealed class UAuthIdentifierBehaviorOptions public bool AllowAdminOverride { get; set; } = true; public bool AllowUserOverride { get; set; } = true; + public int MaxIdentifiersPerUser { get; set; } = 10; + public Dictionary MaxIdentifiersPerType { get; set; } = new(); + internal UAuthIdentifierBehaviorOptions Clone() => new() { AllowUsernameChange = AllowUsernameChange, @@ -26,6 +31,9 @@ public sealed class UAuthIdentifierBehaviorOptions RequireEmailVerification = RequireEmailVerification, RequirePhoneVerification = RequirePhoneVerification, AllowAdminOverride = AllowAdminOverride, - AllowUserOverride = AllowUserOverride + AllowUserOverride = AllowUserOverride, + + MaxIdentifiersPerUser = MaxIdentifiersPerUser, + MaxIdentifiersPerType = new Dictionary(MaxIdentifiersPerType) }; } diff --git a/src/CodeBeam.UltimateAuth.Server/Options/Validators/UAuthServerUserIdentifierOptionsValidator.cs b/src/CodeBeam.UltimateAuth.Server/Options/Validators/UAuthServerUserIdentifierOptionsValidator.cs index b66de941..f8fc605e 100644 --- a/src/CodeBeam.UltimateAuth.Server/Options/Validators/UAuthServerUserIdentifierOptionsValidator.cs +++ b/src/CodeBeam.UltimateAuth.Server/Options/Validators/UAuthServerUserIdentifierOptionsValidator.cs @@ -6,12 +6,38 @@ public sealed class UAuthServerUserIdentifierOptionsValidator : IValidateOptions { public ValidateOptionsResult Validate(string? name, UAuthServerOptions options) { + var errors = new List(); + if (!options.Identifiers.Behavior.AllowAdminOverride && !options.Identifiers.Behavior.AllowUserOverride) { - return ValidateOptionsResult.Fail("Both AllowAdminOverride and AllowUserOverride cannot be false. " + + errors.Add("Both AllowAdminOverride and AllowUserOverride cannot be false. " + "At least one actor must be able to manage user identifiers."); } - return ValidateOptionsResult.Success; + var behavior = options.Identifiers.Behavior; + + if (behavior.MaxIdentifiersPerUser < 1) + { + errors.Add("Identifiers.Behavior.MaxIdentifiersPerUser must be greater than zero."); + } + + if (behavior.MaxIdentifiersPerType is null) + { + errors.Add("Identifiers.Behavior.MaxIdentifiersPerType cannot be null."); + } + else + { + foreach (var (type, limit) in behavior.MaxIdentifiersPerType) + { + if (!Enum.IsDefined(type) || limit < 1) + { + errors.Add($"Invalid identifier limit for {type}. Limit must be greater than zero."); + } + } + } + + return errors.Count == 0 + ? ValidateOptionsResult.Success + : ValidateOptionsResult.Fail(errors); } } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs index 12036f54..41ac9a69 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs @@ -7,7 +7,6 @@ using CodeBeam.UltimateAuth.Server.Infrastructure; using CodeBeam.UltimateAuth.Server.Options; using CodeBeam.UltimateAuth.Server.Services; -using CodeBeam.UltimateAuth.Users; using CodeBeam.UltimateAuth.Users.Contracts; using Microsoft.Extensions.Options; @@ -77,6 +76,8 @@ public async Task CreateUserAsync(AccessContext context, Creat throw new UAuthValidationException(string.Join(", ", validationResult.Errors)); } + EnsureCreateUserIdentifierLimits(request); + return await _atomicExecutor.ExecuteAsync( async atomicCt => { @@ -625,6 +626,11 @@ public async Task AddUserIdentifierAsync(AccessContext context, AddUserIdentifie var existing = await identifierStore.GetByUserAsync(userKey, innerCt); EnsureMultipleIdentifierAllowed(request.Type, existing); + // TODO: Enforce identifier limits atomically for concurrent additions. + // The current read-check-insert sequence can exceed configured limits when multiple requests add identifiers to the same user simultaneously. + // Introduce per-user synchronization or a persistence-level guarantee for both EF Core and InMemory implementations. + EnsureIdentifierLimits(request.Type, existing); + var userScopeResult = await identifierStore.ExistsAsync( new IdentifierExistenceQuery(request.Type, normalized.Normalized, IdentifierExistenceScope.WithinUser, UserKey: userKey), innerCt); @@ -980,6 +986,28 @@ private void EnsureMultipleIdentifierAllowed(UserIdentifierType type, IReadOnlyL throw new UAuthValidationException("multiple_phones_not_allowed"); } + private void EnsureIdentifierLimits(UserIdentifierType type, IReadOnlyList existing) + { + var behavior = _options.Identifiers.Behavior; + + var activeIdentifiers = existing.Where(i => !i.IsDeleted).ToList(); + + if (activeIdentifiers.Count >= behavior.MaxIdentifiersPerUser) + { + throw new UAuthIdentifierValidationException("identifier_total_limit_exceeded"); + } + + if (behavior.MaxIdentifiersPerType.TryGetValue(type, out var maxPerType)) + { + var activeTypeCount = activeIdentifiers.Count(i => i.Type == type); + + if (activeTypeCount >= maxPerType) + { + throw new UAuthIdentifierValidationException("identifier_type_limit_exceeded"); + } + } + } + private void EnsureVerificationRequirements(UserIdentifierType type, bool isVerified) { if (type == UserIdentifierType.Email && _options.Identifiers.Behavior.RequireEmailVerification && !isVerified) @@ -1057,6 +1085,35 @@ private async Task EnsureIdentifierUniquenessAsync( } } + private void EnsureCreateUserIdentifierLimits(CreateUserRequest request) + { + var behavior = _options.Identifiers.Behavior; + + var types = new List(3); + + if (!string.IsNullOrWhiteSpace(request.UserName)) + types.Add(UserIdentifierType.Username); + + if (!string.IsNullOrWhiteSpace(request.Email)) + types.Add(UserIdentifierType.Email); + + if (!string.IsNullOrWhiteSpace(request.Phone)) + types.Add(UserIdentifierType.Phone); + + if (types.Count > behavior.MaxIdentifiersPerUser) + { + throw new UAuthIdentifierValidationException("identifier_total_limit_exceeded"); + } + + foreach (var group in types.GroupBy(type => type)) + { + if (behavior.MaxIdentifiersPerType.TryGetValue(group.Key, out var maxPerType) && group.Count() > maxPerType) + { + throw new UAuthIdentifierValidationException("identifier_type_limit_exceeded"); + } + } + } + // TODO(policy): Move identifier uniqueness decision/enforcement to the Policy layer. private UniquenessScope GetUniquenessScope(UserIdentifierType type) { diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserApplicationServiceTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserApplicationServiceTests.cs index 2087b2c4..e34d512c 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserApplicationServiceTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserApplicationServiceTests.cs @@ -1,4 +1,5 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; +using AwesomeAssertions; +using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Defaults; using CodeBeam.UltimateAuth.Core.Domain; @@ -7,12 +8,13 @@ using CodeBeam.UltimateAuth.Server.Infrastructure; using CodeBeam.UltimateAuth.Server.Options; using CodeBeam.UltimateAuth.Server.Services; +using CodeBeam.UltimateAuth.Tests.Unit.Helpers; using CodeBeam.UltimateAuth.Users; using CodeBeam.UltimateAuth.Users.Contracts; using CodeBeam.UltimateAuth.Users.Reference; -using AwesomeAssertions; using Microsoft.Extensions.Options; using Moq; +using static CodeBeam.UltimateAuth.Core.Defaults.UAuthActions; namespace CodeBeam.UltimateAuth.Tests.Unit.Users; @@ -2040,11 +2042,182 @@ public async Task GetUsersByKeysAsync_When250DistinctKeys_ExecutesSuccessfully() Times.Once); } + [Fact] + public async Task Adding_identifier_should_fail_when_global_limit_reached() + { + var runtime = new TestAuthRuntime(configureServer: o => + { + o.Identifiers.Behavior.MaxIdentifiersPerUser = 1; + }); + + var service = runtime.GetUserApplicationService(); + var context = TestAccessContext.ForUser( + TestUsers.User, + UserIdentifiers.AddSelf); + + var act = () => service.AddUserIdentifierAsync( + context, + new AddUserIdentifierRequest + { + Type = UserIdentifierType.Phone, + Value = "+905551234567" + }); + + await act.Should() + .ThrowAsync() + .WithMessage("*identifier_total_limit_exceeded*"); + } + + [Fact] + public async Task Adding_identifier_should_fail_when_type_limit_reached() + { + var runtime = new TestAuthRuntime(configureServer: o => + { + o.Identifiers.Behavior.MaxIdentifiersPerType = new() + { + [UserIdentifierType.Email] = 1 + }; + }); + + var service = runtime.GetUserApplicationService(); + var context = TestAccessContext.ForUser( + TestUsers.User, + UserIdentifiers.AddSelf); + + var act = () => service.AddUserIdentifierAsync( + context, + new AddUserIdentifierRequest + { + Type = UserIdentifierType.Email, + Value = "another@example.com" + }); + + await act.Should() + .ThrowAsync() + .WithMessage("*identifier_type_limit_exceeded*"); + } + + [Fact] + public async Task CreateUserAsync_WhenGlobalIdentifierLimitExceeded_ThrowsWithoutPersisting() + { + var f = CreateFixtureWithOptions(options => + { + options.Identifiers.Behavior.MaxIdentifiersPerUser = 2; + }); + + var context = CreateContext(); + + var request = new CreateUserRequest + { + UserName = "alice", + Email = "alice@example.com", + Phone = "+905551112233" + }; + + SetupValidCreate(f, context, request); + + var act = () => f.Sut.CreateUserAsync(context, request); + + await act.Should() + .ThrowAsync() + .WithMessage("*identifier_total_limit_exceeded*"); + + f.LifecycleStore.Verify( + x => x.AddAsync( + It.IsAny(), + It.IsAny()), + Times.Never); + + f.ProfileStore.Verify( + x => x.AddAsync( + It.IsAny(), + It.IsAny()), + Times.Never); + + f.IdentifierStore.Verify( + x => x.AddAsync( + It.IsAny(), + It.IsAny()), + Times.Never); + } + + [Fact] + public async Task CreateUserAsync_WhenIdentifierCountEqualsLimit_Succeeds() + { + var f = CreateFixtureWithOptions(options => + { + options.Identifiers.Behavior.MaxIdentifiersPerUser = 2; + }); + + var context = CreateContext(); + + var request = new CreateUserRequest + { + UserName = "alice", + Email = "alice@example.com" + }; + + SetupValidCreate(f, context, request); + SetupSuccessfulCreatePersistence(f); + + var result = await f.Sut.CreateUserAsync(context, request); + + result.IsSuccess.Should().BeTrue(); + + f.LifecycleStore.Verify( + x => x.AddAsync( + It.IsAny(), + It.IsAny()), + Times.Once); + + f.ProfileStore.Verify( + x => x.AddAsync( + It.IsAny(), + It.IsAny()), + Times.Once); + + f.IdentifierStore.Verify( + x => x.AddAsync( + It.IsAny(), + It.IsAny()), + Times.Exactly(2)); + } + + [Fact] + public async Task CreateUserAsync_WithDefaultLimit_AllowsThreeInitialIdentifiers() + { + var f = CreateFixture(); + var context = CreateContext(); + + var request = new CreateUserRequest + { + UserName = "alice", + Email = "alice@example.com", + Phone = "+905551112233" + }; + + SetupValidCreate(f, context, request); + SetupSuccessfulCreatePersistence(f); + + var result = await f.Sut.CreateUserAsync(context, request); + + result.IsSuccess.Should().BeTrue(); + + f.IdentifierStore.Verify( + x => x.AddAsync( + It.IsAny(), + It.IsAny()), + Times.Exactly(3)); + } + // ============================================================ // Helpers // ============================================================ private static Fixture CreateFixture(params IUserLifecycleIntegration[] integrations) + => CreateFixtureWithOptions(null, integrations); + + private static Fixture CreateFixtureWithOptions(Action? configureOptions, params IUserLifecycleIntegration[] integrations) { var access = new Mock(MockBehavior.Strict); var atomic = new Mock(MockBehavior.Strict); @@ -2180,8 +2353,10 @@ private static Fixture CreateFixture(params IUserLifecycleIntegration[] integrat .Returns>, CancellationToken>( (operation, ct) => operation(ct)); - var options = Options.Create( - new UAuthServerOptions()); + var serverOptions = new UAuthServerOptions(); + configureOptions?.Invoke(serverOptions); + + var options = Options.Create(serverOptions); var sut = new UserApplicationService( access.Object,