diff --git a/README.md b/README.md
index 37dd42fc..6322cffd 100644
--- a/README.md
+++ b/README.md
@@ -42,7 +42,7 @@ UltimateAuth is an open-source auth framework with platform-level capabilities t
| Security Expansion | 0.3.0 | MFA, Reauth | 🟡 In Progress | Q1 2027 |
| Infrastructure Expansion| 0.4.0 | Redis, Distributed Cache, Password Hashers | 🔜 Planned | Q1 2027 |
| Multi-Tenant Expansion | 0.5.0 | Multi tenant management | 🔜 Planned | Q2 2027 |
-| Extensibility Expansion | 0.6.0 | Audit, events, hooks | 🔜 Planned | Q2 2027 |
+| Extensibility Expansion | 0.6.0 | Audit, Passwordless Login | 🔜 Planned | Q2 2027 |
| Performance Expansion | 0.7.0 | Benchmarks, caching | 🔜 Planned | Q3 2027 |
| Ecosystem Expansion | 0.8.0 | Migration tools | 🔜 Planned | Q3 2027 |
| v1.0 | 1.0.0 | Locked API, align with .NET 12 | 🔜 Planned | Q4 2027 |
@@ -62,9 +62,9 @@ UltimateAuth is designed to deliver a simple, consistent developer experience fr
| Milestone | Version | Target |
|---|---|---|
| **Developer Foundation** | 0.1.x | A functional, security-conscious authentication framework with a complete core architecture, extensibility, and a developer-first API. Suitable for development, early adoption, and production use following application-specific evaluation. |
-| **Production Expansion** | 0.2.0 | Ready to power real-world applications, particularly small and medium-sized products, with expanded authentication modes, built-in rate limiting, and a proven integration experience. Larger applications can also adopt UltimateAuth when its available capabilities meet their requirements. |
-| **Advanced Security & Scale** | 0.4.0 | Extends production capabilities with MFA, reauthentication, distributed infrastructure, and additional security options for applications with more demanding requirements. Builds on the same simple developer experience. |
-| **Migration & Adoption** | 0.8.0 | Makes adopting UltimateAuth in existing applications easier through migration tools, guidance, and established integration patterns, without requiring new applications to wait for this milestone. |
+| **Production Expansion** | 0.2.x | Ready to power real-world applications, particularly small and medium-sized products, with expanded authentication modes, built-in rate limiting, and a proven integration experience. Larger applications can also adopt UltimateAuth when its available capabilities meet their requirements. |
+| **Advanced Security & Scale** | 0.4.x | Extends production capabilities with MFA, reauthentication, distributed infrastructure, and additional security options for applications with more demanding requirements. Builds on the same simple developer experience. |
+| **Migration & Adoption** | 0.8.x | Makes adopting UltimateAuth in existing applications easier through migration tools, guidance, and established integration patterns, without requiring new applications to wait for this milestone. |
**The UltimateAuth philosophy:** Simple from day one, more capable with every release. Version milestones describe expanding capabilities — not a progression from difficult to easy, or from unusable to usable.
@@ -225,25 +225,20 @@ Change `Components/Pages/Home.razor`:
-
-@code {
- private string _identifier = "";
- private string _password = "";
-}
```
### 7. Protect your pages
diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/LoginPage.razor b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/LoginPage.razor
index d512cd87..648aa4bf 100644
--- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/LoginPage.razor
+++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/LoginPage.razor
@@ -70,15 +70,6 @@
- @*
-
-
- Authenticated session
-
-
-
We recognize your account. Your Identity Lab workspace is ready whenever you are.
-
*@
-
-
+
@if (IsLockedOut)
{
@@ -174,7 +165,7 @@
-
+
+ @* Do not add loginbusy disabled to the text and password fields, it will prevent the form from submitting with required fields. *@
@(IsLockedOut ? "Temporarily locked" : "Sign In")
-
diff --git a/docs/content/client-sdk/authentication.md b/docs/content/client-sdk/authentication.md
index 5bbb830c..b148a282 100644
--- a/docs/content/client-sdk/authentication.md
+++ b/docs/content/client-sdk/authentication.md
@@ -895,23 +895,59 @@ The component supports password-based login, PKCE completion through UAuthHub, c
### Basic Usage
-A login form can be implemented with the following component:
+`UAuthLoginForm` provides a complete, ready-to-use login form out of the box. No additional markup or credential bindings are required.
+
+```razor
+
+```
+
+That's it. UltimateAuth automatically renders the identifier field, password field, and submit button, and handles the authentication submission workflow.
+
+**Customizing the Form**
+
+When you need a custom layout or styling, simply provide your own form content:
```razor
@using CodeBeam.UltimateAuth.Client.Blazor
-
-
-
+
+
+
Sign in
-
-@code {
- private string email = "";
- private string password = "";
-}
```
+UltimateAuth handles the authentication workflow in both cases. Your custom fields only need the correct HTML `name` attributes; there is no need to bind credentials to Blazor component state.
+
+### Native Credential Submission
+
+`UAuthLoginForm` reads credentials directly from native browser form fields. It does not accept `Identifier` or `Secret` component parameters.
+
+When providing custom `ChildContent`, the form must contain enabled input elements with the exact names `Identifier` and `Secret`.
+
+Credential values should not be bound to Blazor component state solely for submission. This is particularly important in Blazor Server, where interactive component bindings can transmit credential values over the SignalR connection.
+
+**Important: Do Not Disable Credential Fields During Submission**
+
+HTML inputs marked as `disabled` are excluded from `FormData` and native form submissions.
+
+Do not set `Disabled="@isSubmitting"` on the username or password fields. A login preview may succeed, but the subsequent native commit can fail because the credential fields are missing from the submitted form.
+
+Instead, disable the submit button while leaving the credential inputs enabled.
+
+If you need to prevent editing during submission, consider `readonly` where supported, while accounting for component behavior, browser autofill, and accessibility.
+
+**Custom Form Requirements**
+
+- Use `name="Identifier"` and `name="Secret"` exactly.
+- Keep both credential inputs enabled throughout submission.
+- Avoid duplicate inputs with these names.
+- Use `type="password"` and `autocomplete="current-password"` for password fields.
+- Keep custom submit controls inside `UAuthLoginForm`.
+- Use `OnSubmittingChanged` to manage loading indicators and submit-button state.
+
+`UAuthLoginForm` handles the try and commit orchestration. The native commit includes the browser form fields, device information, and antiforgery data required by the configured authentication flow.
+
The component automatically uses `TryAndCommit` as its default submission mode.
When submitted, it:
@@ -934,13 +970,13 @@ For example, with MudBlazor:
@using CodeBeam.UltimateAuth.Client.Blazor
@using CodeBeam.UltimateAuth.Client
-
+
Sign in
-
-
+
+
- Sign in
+ Sign in
@if (!string.IsNullOrWhiteSpace(error))
{
@@ -950,10 +986,15 @@ For example, with MudBlazor:
@code {
- private string email = "";
- private string password = "";
+ private bool _isSubmitting;
private string? error;
+ private Task HandleSubmittingChanged(bool submitting)
+ {
+ _isSubmitting = submitting;
+ return Task.CompletedTask;
+ }
+
private Task HandleLoginResult(IUAuthTryResult result)
{
error = result.IsSuccess ? null : "Unable to sign in.";
@@ -973,7 +1014,7 @@ Because the component renders an HTML form internally, place the submit button i
Use `SubmitMode` to control how the authentication attempt is executed.
```razor
-
+
...
```
@@ -995,7 +1036,7 @@ For interactive login forms that need validation feedback, it is generally the r
The component exposes an `OnTryResult` callback:
```razor
-
+
...
```
@@ -1059,9 +1100,9 @@ The same component also supports PKCE login completion when hosted within a supp
Set `LoginType` to `Pkce`:
```razor
-
-
-
+
+
+
Continue
```
@@ -1085,7 +1126,7 @@ A Blazor WebAssembly application should initiate PKCE using `BeginPkceAsync`; th
You can provide an explicit return URL:
```razor
-
+
...
```
@@ -1103,7 +1144,7 @@ This allows a shared login component to participate in different authentication
For advanced integrations, the `Endpoint` parameter allows the form's HTML action endpoint to be overridden:
```razor
-
+
...
```
@@ -1117,9 +1158,9 @@ Note that `Endpoint` affects the form's HTML action. Try-based submissions invok
`UAuthLoginForm` exposes `SubmitAsync`, allowing submission through a component reference.
```razor
-
-
-
+
+
+
@@ -1129,9 +1170,6 @@ Note that `Endpoint` affects the form's HTML action. Try-based submissions invok
@code {
private UAuthLoginForm? loginForm;
- private string email = "";
- private string password = "";
-
private async Task SubmitLoginAsync()
{
if (loginForm is not null)
@@ -1153,7 +1191,7 @@ By default, the component supports Enter-key submission.
You can disable its built-in hidden submit button:
```razor
-
+
...
```
@@ -1165,7 +1203,7 @@ This option controls the component's built-in Enter-key submit mechanism. Other
The component exposes `ReloadAsync` to reload its Hub credentials and flow state.
```razor
-
+
...
@@ -1190,8 +1228,6 @@ It does not initiate a new PKCE authorization flow by itself.
| Parameter | Type | Default | Description |
|---|---|---|---|
-| `Identifier` | `string?` | `null` | Login identifier |
-| `Secret` | `string?` | `null` | Authentication secret |
| `LoginType` | `UAuthLoginType` | `Password` | Password or PKCE login |
| `SubmitMode` | `UAuthSubmitMode` | `TryAndCommit` | Submission strategy |
| `ReturnUrl` | `string?` | `null` | Redirect destination |
@@ -1199,6 +1235,7 @@ It does not initiate a new PKCE authorization flow by itself.
| `HubSessionId` | `HubSessionId?` | `null` | Explicit Hub session context |
| `AllowEnterKeyToSubmit` | `bool` | `true` | Built-in Enter-key submit support |
| `OnTryResult` | `EventCallback` | — | Structured try result callback |
+| `OnSubmittingChanged` | `EventCallback` | — | Submission state change callback |
| `ChildContent` | `RenderFragment?` | — | Custom form content |
### When to Use UAuthLoginForm
diff --git a/docs/content/getting-started/quickstart.md b/docs/content/getting-started/quickstart.md
index 18dc311c..d243d58e 100644
--- a/docs/content/getting-started/quickstart.md
+++ b/docs/content/getting-started/quickstart.md
@@ -190,25 +190,20 @@ Change `Components/Pages/Home.razor`:
-
-@code {
- private string _identifier = "";
- private string _password = "";
-}
```
`UAuthLoginForm` handles the login submission through UltimateAuth's client flow.
diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/client-sdk/authentication.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/client-sdk/authentication.json
index 8c7996d0..43f4bb3c 100644
--- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/client-sdk/authentication.json
+++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/client-sdk/authentication.json
@@ -1,7 +1,7 @@
{
"Slug": "client-sdk/authentication",
"Title": "Authentication",
- "Html": "\n\u003Cp\u003EThe UltimateAuth Client SDK provides a high-level API for authentication workflows, including login, PKCE, session validation, refresh, logout, and device-specific session revocation.\u003C/p\u003E\n\u003Cp\u003EThese operations are exposed through \u003Ccode\u003EUAuthClient.Flows\u003C/code\u003E, which implements the \u003Ccode\u003EIFlowClient\u003C/code\u003E contract.\u003C/p\u003E\n\u003Cp\u003EThis guide shows how to use the authentication client in application code, handle structured results, configure interactive behavior, and choose the correct flow for different client environments.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022accessing-the-authentication-client\u0022\u003E1. Accessing the Authentication Client\u003C/h2\u003E\n\u003Cp\u003EIn a Blazor component, inject \u003Ccode\u003EIUAuthClient\u003C/code\u003E:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@using CodeBeam.UltimateAuth.Client\n@inject IUAuthClient UAuthClient\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAuthentication operations are available through the \u003Ccode\u003EFlows\u003C/code\u003E property.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eawait UAuthClient.Flows.RefreshAsync();\nawait UAuthClient.Flows.ValidateAsync();\nawait UAuthClient.Flows.LogoutAsync();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe relevant methods are defined by \u003Ccode\u003EIFlowClient\u003C/code\u003E.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EMethod\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\u003ELoginAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESubmit a direct login flow\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETryLoginAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAttempt login with configurable try/commit behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EBeginPkceAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EStart a PKCE authentication flow\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECompletePkceLoginAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EComplete a PKCE login\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETryCompletePkceLoginAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EComplete PKCE with structured try/commit behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERefreshAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERefresh or continue authentication\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EValidateAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EValidate current authentication state\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELogoutAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELog out the current session\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELogoutMyDeviceAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERevoke a selected device-associated chain\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELogoutMyOtherDevicesAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERevoke other devices while retaining the current one\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELogoutAllMyDevicesAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERevoke all devices for the current user\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EAdministrative device logout operations are also available and covered later in this guide.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022choosing-the-correct-login-flow\u0022\u003E2. Choosing the Correct Login Flow\u003C/h2\u003E\n\u003Cp\u003EBefore implementing login, determine whether your application is a trusted server-hosted client or a public client.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EClient environment\u003C/th\u003E\n\u003Cth\u003ERecommended flow\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EBlazor Server\u003C/td\u003E\n\u003Ctd\u003EDirect login or supported interactive login flow\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EUAuthHub\u003C/td\u003E\n\u003Ctd\u003EServer-hosted authentication flow\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EBlazor WebAssembly\u003C/td\u003E\n\u003Ctd\u003EPKCE\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EBrowser SPA\u003C/td\u003E\n\u003Ctd\u003EPKCE\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EPublic native application\u003C/td\u003E\n\u003Ctd\u003EPKCE\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EPublic clients must not rely on embedding a secret or trusting browser-executed code to protect credentials.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth restricts direct credential posting based on the configured client profile and login options.\u003C/p\u003E\n\u003Cp\u003EFor public clients, use the PKCE flow instead of enabling direct credential posting.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022direct-login\u0022\u003E3. Direct Login\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003ELoginAsync\u003C/code\u003E performs a direct login submission.\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 request = new LoginRequest\n{\n Identifier = \u0026quot;user@example.com\u0026quot;,\n Secret = \u0026quot;password\u0026quot;\n};\n\nawait UAuthClient.Flows.LoginAsync(request);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns \u003Ccode\u003ETask\u003C/code\u003E, not a structured login result.\u003C/p\u003E\n\u003Cp\u003EIt submits the login payload through the configured navigation-based transport.\u003C/p\u003E\n\u003Cp\u003EDepending on the hosting environment, this may initiate navigation or redirect behavior.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022loginrequest-properties\u0022\u003ELoginRequest Properties\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003ELoginRequest\u003C/code\u003E supports the following 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\u003Ccode\u003EIdentifier\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELogin identifier\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESecret\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECredential secret\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECredentialType\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECredential type; defaults to \u003Ccode\u003EPassword\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMetadata\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional request metadata\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERequestTokens\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EHint to request tokens when supported\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPreviewReceipt\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional receipt associated with a prior login preview\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EExample:\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 request = new LoginRequest\n{\n Identifier = email,\n Secret = password,\n CredentialType = CredentialType.Password,\n RequestTokens = true\n};\n\nawait UAuthClient.Flows.LoginAsync(request);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003ERequestTokens\u003C/code\u003E is a hint, not a guarantee that tokens will be issued. The server\u0027s authentication mode and policy remain authoritative.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022redirect-after-login\u0022\u003ERedirect After Login\u003C/h3\u003E\n\u003Cp\u003EProvide an explicit return URL when necessary:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eawait UAuthClient.Flows.LoginAsync(request, returnUrl: \u0026quot;/dashboard\u0026quot;);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe direct login flow resolves its return URL in the following order:\u003C/p\u003E\n\u003Col\u003E\n\u003Cli\u003EThe explicit \u003Ccode\u003EreturnUrl\u003C/code\u003E argument.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003ELogin.ReturnUrl\u003C/code\u003E option.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EDefaultReturnUrl\u003C/code\u003E option.\u003C/li\u003E\n\u003C/ol\u003E\n\u003Cp\u003EFor example, client options can specify a default:\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.Login.ReturnUrl = \u0026quot;/dashboard\u0026quot;;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe return URL is part of the login workflow. It should not be treated as an authorization grant.\u003C/p\u003E\n\u003Cp\u003EApplications and servers must still enforce their redirect validation rules.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022interactive-login-with-tryloginasync\u0022\u003E4. Interactive Login with TryLoginAsync\u003C/h2\u003E\n\u003Cp\u003EFor interactive forms, \u003Ccode\u003ETryLoginAsync\u003C/code\u003E provides structured feedback before or during the authentication commit process.\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.Flows.TryLoginAsync(\n new LoginRequest\n {\n Identifier = email,\n Secret = password\n },\n UAuthSubmitMode.TryAndCommit);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis is generally the more useful API for a login form that needs to display validation feedback.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022submit-modes\u0022\u003ESubmit Modes\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthSubmitMode\u003C/code\u003E defines three submission behaviors.\u003C/p\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\u003EBehavior\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETryOnly\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EExecute the try operation and return its result without committing authentication\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDirectCommit\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESubmit directly to the commit endpoint without a separate try operation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETryAndCommit\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETry first, then commit when the try operation succeeds\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThese modes control client workflow, not whether the server validates authentication.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EDirectCommit does not bypass credential verification or server security requirements.\u003C/strong\u003E\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022tryonly-validate-without-login\u0022\u003ETryOnly: Validate Without Login\u003C/h3\u003E\n\u003Cp\u003EUse \u003Ccode\u003ETryOnly\u003C/code\u003E when you want structured feedback without establishing an authenticated session.\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.Flows.TryLoginAsync(\n new LoginRequest\n {\n Identifier = email,\n Secret = password\n },\n UAuthSubmitMode.TryOnly);\n\nif (result.IsSuccess)\n{\n // The try operation succeeded.\n // Authentication has not been committed.\n}\nelse\n{\n // Display the appropriate feedback.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis can be useful for a custom interactive workflow.\u003C/p\u003E\n\u003Cp\u003EA successful try result must not be interpreted as an authenticated application session.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022tryandcommit-interactive-login\u0022\u003ETryAndCommit: Interactive Login\u003C/h3\u003E\n\u003Cp\u003EUse \u003Ccode\u003ETryAndCommit\u003C/code\u003E to combine structured validation with authentication commit.\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.Flows.TryLoginAsync(\n new LoginRequest\n {\n Identifier = email,\n Secret = password\n },\n UAuthSubmitMode.TryAndCommit,\n returnUrl: \u0026quot;/dashboard\u0026quot;);\n\nif (!result.IsSuccess)\n{\n // Show validation feedback.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWhen the try stage succeeds, the client proceeds to the commit operation through its configured transport.\u003C/p\u003E\n\u003Cp\u003EThis can involve navigation, so application code should not assume that execution always continues within the same component instance.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022directcommit-single-commit-submission\u0022\u003EDirectCommit: Single Commit Submission\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\u003Eawait UAuthClient.Flows.TryLoginAsync(\n new LoginRequest\n {\n Identifier = email,\n Secret = password\n },\n UAuthSubmitMode.DirectCommit);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EIn this mode, the SDK submits the commit operation directly.\u003C/p\u003E\n\u003Cp\u003EThe method returns a \u003Ccode\u003ETryLoginResult\u003C/code\u003E with \u003Ccode\u003EIsSuccess = true\u003C/code\u003E after the navigation submission completes, but this is not a structured server-side authentication success confirmation.\u003C/p\u003E\n\u003Cp\u003EIf you need structured credential validation feedback, prefer \u003Ccode\u003ETryAndCommit\u003C/code\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022handling-login-results\u0022\u003E5. Handling Login Results\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003ETryLoginAsync\u003C/code\u003E returns \u003Ccode\u003ETryLoginResult\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EIts available properties include:\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\u003EMeaning\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIsSuccess\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EWhether the try operation succeeded\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EReason\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional authentication failure reason\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERemainingAttempts\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERemaining attempts, when provided\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELockoutUntilUtc\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELockout expiration, when provided\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERequiresMfa\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EWhether additional authentication is required\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPreviewReceipt\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional receipt for subsequent flow handling\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EExample:\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.Flows.TryLoginAsync(request, UAuthSubmitMode.TryOnly);\n\nif (!result.IsSuccess)\n{\n if (result.LockoutUntilUtc is not null)\n {\n errorMessage = $\u0026quot;Login temporarily unavailable until {result.LockoutUntilUtc:u}\u0026quot;;\n }\n else\n {\n errorMessage = \u0026quot;Unable to sign in.\u0026quot;;\n }\n\n return;\n}\n\nif (result.RequiresMfa)\n{\n // Continue with the supported additional authentication flow.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EDo not expose sensitive internal authentication details unnecessarily.\u003C/p\u003E\n\u003Cp\u003EFor public-facing login forms, generic failure messages may be preferable to displaying every failure reason.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022handling-transport-and-protocol-errors\u0022\u003EHandling Transport and Protocol Errors\u003C/h3\u003E\n\u003Cp\u003EA structured authentication failure is different from a network or protocol failure.\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\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Etry\n{\n var result = await UAuthClient.Flows.TryLoginAsync(request, UAuthSubmitMode.TryAndCommit);\n\n if (!result.IsSuccess)\n {\n errorMessage = \u0026quot;Unable to sign in.\u0026quot;;\n }\n}\ncatch (UAuthTransportException)\n{\n errorMessage = \u0026quot;Authentication service is unavailable.\u0026quot;;\n}\ncatch (UAuthProtocolException)\n{\n errorMessage = \u0026quot;An authentication protocol error occurred.\u0026quot;;\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUse the corresponding UltimateAuth client error namespace when referencing these exception types.\u003C/p\u003E\n\u003Cp\u003EA failed authentication attempt should normally be handled through the structured try result where supported. Transport and protocol exceptions indicate different failure categories.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022pkce-authentication\u0022\u003E6. PKCE Authentication\u003C/h2\u003E\n\u003Cp\u003EPKCE provides a proof mechanism for authorization-code-based authentication flows.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth supports PKCE using an S256 code challenge.\u003C/p\u003E\n\u003Cp\u003EThe SDK generates a cryptographically random verifier, derives its challenge, and coordinates the authorization flow.\u003C/p\u003E\n\u003Cp\u003EPublic clients such as Blazor WebAssembly should use PKCE rather than direct credential posting.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022start-pkce\u0022\u003EStart PKCE\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\u003Eawait UAuthClient.Flows.BeginPkceAsync();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EYou can provide a return URL:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eawait UAuthClient.Flows.BeginPkceAsync(returnUrl: \u0026quot;/dashboard\u0026quot;);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe client performs the following operations:\u003C/p\u003E\n\u003Col\u003E\n\u003Cli\u003EGenerates a PKCE code verifier.\u003C/li\u003E\n\u003Cli\u003EDerives an S256 code challenge.\u003C/li\u003E\n\u003Cli\u003ESends the authorization request.\u003C/li\u003E\n\u003Cli\u003EReceives an authorization code.\u003C/li\u003E\n\u003Cli\u003EInvokes the configured authorization callback, if any.\u003C/li\u003E\n\u003Cli\u003ENavigates to the configured UAuthHub login flow when automatic redirection is enabled.\u003C/li\u003E\n\u003C/ol\u003E\n\u003Cp\u003EThe authorization code and verifier are used later to complete the flow.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configure-pkce\u0022\u003EConfigure PKCE\u003C/h3\u003E\n\u003Cp\u003EThe relevant client options include:\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.Pkce.Enabled = true;\noptions.Pkce.AutoRedirect = true;\noptions.Pkce.ReturnUrl = \u0026quot;/dashboard\u0026quot;;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EPKCE is enabled by default in \u003Ccode\u003EUAuthClientPkceLoginFlowOptions\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EAutomatic redirection is also enabled by default.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022observe-authorization\u0022\u003EObserve Authorization\u003C/h3\u003E\n\u003Cp\u003EYou can register a callback that runs after the authorization code is received and before the automatic Hub redirect.\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.Pkce.OnAuthorized = async response =\u0026gt;\n{\n // Observe the authorization response.\n await Task.CompletedTask;\n};\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis callback is useful for application-specific coordination.\u003C/p\u003E\n\u003Cp\u003EDo not log authorization codes or expose them to unrelated components.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022disable-automatic-redirection\u0022\u003EDisable Automatic Redirection\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.Pkce.AutoRedirect = false;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWith this configuration, \u003Ccode\u003EBeginPkceAsync\u003C/code\u003E still performs the authorization request and invokes \u003Ccode\u003EOnAuthorized\u003C/code\u003E, but it does not automatically navigate to the Hub login endpoint.\u003C/p\u003E\n\u003Cp\u003EThe application becomes responsible for coordinating the remaining flow.\u003C/p\u003E\n\u003Cp\u003EThis is an advanced integration option.\u003C/p\u003E\n\u003Cp\u003EA custom implementation must preserve the authorization code, verifier, and relevant context securely and complete the flow using the supported protocol.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022complete-pkce-login\u0022\u003E7. Complete PKCE Login\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003ECompletePkceLoginAsync\u003C/code\u003E submits the PKCE completion request.\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 request = new PkceCompleteRequest\n{\n AuthorizationCode = authorizationCode,\n CodeVerifier = codeVerifier,\n Identifier = email,\n Secret = password,\n ReturnUrl = \u0026quot;/dashboard\u0026quot;\n};\n\nawait UAuthClient.Flows.CompletePkceLoginAsync(request);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe request model includes:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EPurpose\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAuthorizationCode\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECode obtained from the authorization step\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECodeVerifier\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOriginal PKCE verifier\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdentifier\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELogin identifier\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESecret\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELogin credential\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EReturnUrl\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional post-login destination\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EHubSessionId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional Hub session context\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe authorization code and verifier must correspond to the same PKCE flow.\u003C/p\u003E\n\u003Cp\u003EDo not generate a new verifier when completing an existing authorization.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022interactive-pkce-completion\u0022\u003EInteractive PKCE Completion\u003C/h3\u003E\n\u003Cp\u003EFor structured feedback, use \u003Ccode\u003ETryCompletePkceLoginAsync\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.Flows.TryCompletePkceLoginAsync(\n request,\n UAuthSubmitMode.TryAndCommit);\n\nif (!result.IsSuccess)\n{\n // Display PKCE login feedback.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe same submit modes are supported:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003ETryOnly\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EDirectCommit\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003ETryAndCommit\u003C/code\u003E\u003C/li\u003E\n\u003C/ul\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022try-pkce-without-commit\u0022\u003ETry PKCE Without Commit\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.Flows.TryCompletePkceLoginAsync(request, UAuthSubmitMode.TryOnly);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis returns a \u003Ccode\u003ETryPkceLoginResult\u003C/code\u003E without committing a successful login.\u003C/p\u003E\n\u003Cp\u003EThe result 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\u003Eresult.IsSuccess;\nresult.Reason;\nresult.RemainingAttempts;\nresult.LockoutUntilUtc;\nresult.RequiresMfa;\nresult.RetryWithNewPkce;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022retrywithnewpkce\u0022\u003ERetryWithNewPkce\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003ERetryWithNewPkce\u003C/code\u003E indicates that the client should begin a new PKCE authorization sequence rather than continuing with the existing authorization material.\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\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 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.RetryWithNewPkce)\n{\n await UAuthClient.Flows.BeginPkceAsync(\u0026quot;/login\u0026quot;);\n return;\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis is useful when the current PKCE context can no longer be used.\u003C/p\u003E\n\u003Cp\u003EThe application should not repeatedly resubmit the same invalid authorization code and verifier.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022direct-credential-posting-restrictions\u0022\u003E8. Direct Credential Posting Restrictions\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth restricts direct credential posting according to the configured client profile.\u003C/p\u003E\n\u003Cp\u003EThe client 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.Login.AllowCredentialPost = false;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis is the default.\u003C/p\u003E\n\u003Cp\u003EIf the current profile is not permitted to post credentials directly and the option has not enabled that capability, direct login methods throw an \u003Ccode\u003EInvalidOperationException\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EFor example, attempting direct login from a public client may produce an error indicating that PKCE is required.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022trusted-server-hosted-clients\u0022\u003ETrusted Server-Hosted Clients\u003C/h3\u003E\n\u003Cp\u003EThe option can be explicitly configured for a trusted server-hosted integration where direct credential posting is appropriate:\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.Login.AllowCredentialPost = true;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis must not be used to bypass PKCE requirements in a public browser application.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EDo not enable direct credential posting in production Blazor WebAssembly or SPA clients.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EA client profile is an integration descriptor, not cryptographic proof that the caller is trusted.\u003C/p\u003E\n\u003Cp\u003EServer-side security requirements remain authoritative.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022refresh-authentication\u0022\u003E9. Refresh Authentication\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003ERefreshAsync\u003C/code\u003E requests continuation of the current authentication state.\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.Flows.RefreshAsync();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns \u003Ccode\u003ERefreshResult\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\u003Epublic sealed record RefreshResult\n{\n public bool IsSuccess { get; init; }\n public int Status { get; init; }\n public RefreshOutcome Outcome { get; init; }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ERefresh behavior depends on the authentication mode.\u003C/p\u003E\n\u003Cp\u003EA refresh operation may update session activity, rotate authentication material, or determine that reauthentication is required.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022handling-refresh-results\u0022\u003EHandling Refresh Results\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.Flows.RefreshAsync();\n\nif (result.IsSuccess)\n{\n // Authentication continuation succeeded.\n}\nelse if (result.Outcome == RefreshOutcome.ReauthRequired)\n{\n // The current authentication can no longer continue.\n // Direct the user to the appropriate login flow.\n}\nelse\n{\n // Handle another unsuccessful refresh result.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EA \u003Ccode\u003E401 Unauthorized\u003C/code\u003E refresh response is handled as a normal reauthentication outcome.\u003C/p\u003E\n\u003Cp\u003EIt 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\u003Enew RefreshResult\n{\n IsSuccess = false,\n Status = 401,\n Outcome = RefreshOutcome.ReauthRequired\n};\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ENetwork and unexpected protocol failures can still produce exceptions.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022refresh-outcomes\u0022\u003ERefresh Outcomes\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003ERefreshOutcome\u003C/code\u003E defines the following values:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EOutcome\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\u003ENone\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENo specific semantic outcome\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESuccess\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EGeneric successful refresh\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ENoOp\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENo refresh state change was necessary\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETouched\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EApplicable session activity was updated\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERotated\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERotation occurred\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EReauthRequired\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication continuation is no longer permitted\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\u003Evar result = await UAuthClient.Flows.RefreshAsync();\n\nif (!result.IsSuccess)\n{\n return;\n}\n\nswitch (result.Outcome)\n{\n case RefreshOutcome.Rotated:\n // Authentication material was rotated.\n break;\n\n case RefreshOutcome.Touched:\n // Applicable session activity was updated.\n break;\n\n case RefreshOutcome.NoOp:\n // Successful refresh and no state change was required.\n break;\n\n case RefreshOutcome.Success:\n // Refresh succeeded without a more specific outcome.\n break;\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022detailed-refresh-diagnostics\u0022\u003EDetailed Refresh Diagnostics\u003C/h3\u003E\n\u003Cp\u003EThe server may return a generic success outcome rather than exposing the internal refresh action.\u003C/p\u003E\n\u003Cp\u003EWhere supported, detailed refresh outcomes can be enabled through server diagnostics:\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.Diagnostics.EnableRefreshDetails = true;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis setting is intended for diagnostics and observability.\u003C/p\u003E\n\u003Cp\u003EApplications should not require a specific successful outcome such as \u003Ccode\u003ERotated\u003C/code\u003E or \u003Ccode\u003ETouched\u003C/code\u003E to consider refresh successful.\u003C/p\u003E\n\u003Cp\u003EUse \u003Ccode\u003EIsSuccess\u003C/code\u003E for the primary success decision.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022manual-and-automatic-refresh\u0022\u003EManual and Automatic Refresh\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003ERefreshAsync\u003C/code\u003E has an optional \u003Ccode\u003EisAuto\u003C/code\u003E argument.\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// Manual refresh\nawait UAuthClient.Flows.RefreshAsync();\n\n// Refresh initiated by automatic coordination\nawait UAuthClient.Flows.RefreshAsync(isAuto: true);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe parameter distinguishes automatic refresh activity for client diagnostics.\u003C/p\u003E\n\u003Cp\u003EIt does not select a different authentication mode or override the server\u0027s refresh policy.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022automatic-refresh-configuration\u0022\u003E10. Automatic Refresh Configuration\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth exposes client-side automatic refresh options through \u003Ccode\u003EAutoRefresh\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\u003Eoptions.AutoRefresh.Enabled = true;\noptions.AutoRefresh.Interval = TimeSpan.FromMinutes(5);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EEnabled\u003C/code\u003E controls background refresh coordination.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EInterval\u003C/code\u003E specifies the interval used for background refresh attempts.\u003C/p\u003E\n\u003Cp\u003EThe effective defaults and registration behavior depend on the client integration. The options documentation identifies Blazor Server as the environment where background refresh is enabled by default.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022automatic-refresh-is-not-a-security-guarantee\u0022\u003EAutomatic Refresh Is Not a Security Guarantee\u003C/h3\u003E\n\u003Cp\u003EAutomatic refresh is a client-side convenience mechanism.\u003C/p\u003E\n\u003Cp\u003EIt does not guarantee that a session will remain valid indefinitely.\u003C/p\u003E\n\u003Cp\u003EAuthentication may still end because of:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EExpiration.\u003C/li\u003E\n\u003Cli\u003ERevocation.\u003C/li\u003E\n\u003Cli\u003EAccount security changes.\u003C/li\u003E\n\u003Cli\u003EInvalid refresh credentials.\u003C/li\u003E\n\u003Cli\u003EServer-side continuation restrictions.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThe client must remain prepared to handle \u003Ccode\u003EReauthRequired\u003C/code\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022validate-current-authentication\u0022\u003E11. Validate Current Authentication\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EValidateAsync\u003C/code\u003E to ask the server to validate the current authentication state.\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.Flows.ValidateAsync();\n\nif (result.IsValid)\n{\n // Current authentication is valid.\n}\nelse\n{\n // Authentication is not currently valid.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns \u003Ccode\u003EAuthValidationResult\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EIts properties include:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EPurpose\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EState\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECurrent session state\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIsValid\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EWhether the state is \u003Ccode\u003EActive\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESnapshot\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional authentication state snapshot\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EChainId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional associated session chain\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERootId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional session root\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EBoundDeviceId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional bound device 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=\u0022inspect-session-context\u0022\u003EInspect Session Context\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.Flows.ValidateAsync();\n\nif (result.IsValid \u0026amp;\u0026amp; result.ChainId is { } chainId)\n{\n // Use chainId for supported session-management operations.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ENot every authentication mode necessarily provides all session hierarchy fields.\u003C/p\u003E\n\u003Cp\u003ETreat optional values as optional.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022validateasync-versus-ui-authentication-state\u0022\u003EValidateAsync Versus UI Authentication State\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EValidateAsync\u003C/code\u003E performs an explicit server validation operation.\u003C/p\u003E\n\u003Cp\u003EIt should not be called on every UI render merely to determine whether a component should display authenticated content.\u003C/p\u003E\n\u003Cp\u003EFor UI authentication state, use the supported UltimateAuth state management integration, including \u003Ccode\u003EIUAuthStateManager\u003C/code\u003E where appropriate.\u003C/p\u003E\n\u003Cp\u003EExplicit validation is useful when an application needs to check current server-recognized authentication state.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022logout\u0022\u003E12. Logout\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003ELogoutAsync\u003C/code\u003E performs the current logout flow.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eawait UAuthClient.Flows.LogoutAsync();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method uses the configured logout endpoint and navigation transport.\u003C/p\u003E\n\u003Cp\u003EDepending on the client profile and configuration, logout may involve navigation and authentication state changes.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022logout-button\u0022\u003ELogout Button\u003C/h3\u003E\n\u003Cp\u003EA simple Blazor 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\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\n\u0026lt;button @onclick=\u0026quot;LogoutAsync\u0026quot;\u0026gt;Logout\u0026lt;/button\u0026gt;\n\n@code {\n private async Task LogoutAsync()\n {\n await UAuthClient.Flows.LogoutAsync();\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ELogout is distinct from clearing local UI state.\u003C/p\u003E\n\u003Cp\u003ERemoving a local authentication indicator does not necessarily revoke authoritative server-managed authentication state.\u003C/p\u003E\n\u003Cp\u003EUse the supported logout flow to terminate authentication.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022logout-a-specific-device\u0022\u003E13. Logout a Specific Device\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth supports revoking the session chain associated with a particular device context.\u003C/p\u003E\n\u003Cp\u003EThe relevant method 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\u003ELogoutMyDeviceAsync(LogoutDeviceRequest request)\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003ELogoutDeviceRequest\u003C/code\u003E requires a \u003Ccode\u003ESessionChainId\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 request = new LogoutDeviceRequest\n{\n ChainId = chainId\n};\n\nvar result = await UAuthClient.Flows.LogoutMyDeviceAsync(request);\n\nif (result.IsSuccess)\n{\n // The requested device logout operation succeeded.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis operation targets a session chain, not an arbitrary individual session ID.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022current-device-behavior\u0022\u003ECurrent Device Behavior\u003C/h3\u003E\n\u003Cp\u003EThe returned \u003Ccode\u003ERevokeResult\u003C/code\u003E can indicate whether the affected chain is the current chain.\u003C/p\u003E\n\u003Cp\u003EWhen the operation succeeds and \u003Ccode\u003ECurrentChain\u003C/code\u003E is true, the client publishes a logout-related state event.\u003C/p\u003E\n\u003Cp\u003EThis allows the application to react when the currently authenticated device context has been revoked.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022logout-other-devices\u0022\u003E14. Logout Other Devices\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003ELogoutMyOtherDevicesAsync\u003C/code\u003E to revoke other device-associated sessions while retaining the current one.\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.Flows.LogoutMyOtherDevicesAsync();\n\nif (result.IsSuccess)\n{\n // Other devices were logged out.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis is useful for account security pages that provide a \u003Cstrong\u003ELog out other devices\u003C/strong\u003E action.\u003C/p\u003E\n\u003Cp\u003EThe current authentication context is intended to remain active.\u003C/p\u003E\n\u003Cp\u003EThe server remains responsible for identifying the current session or chain and enforcing the appropriate revocation scope.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022logout-all-devices\u0022\u003E15. Logout All Devices\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003ELogoutAllMyDevicesAsync\u003C/code\u003E to revoke all applicable sessions for the current 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.Flows.LogoutAllMyDevicesAsync();\n\nif (result.IsSuccess)\n{\n // All applicable device sessions were revoked.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EOn success, the client publishes a logout-related state event.\u003C/p\u003E\n\u003Cp\u003EThis operation is useful when a user suspects account compromise or wants to terminate existing authentication sessions across devices.\u003C/p\u003E\n\u003Cp\u003EIn JWT-based modes, previously issued access tokens may remain cryptographically valid until expiration unless additional revocation enforcement is configured.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022administrative-device-logout\u0022\u003E16. Administrative Device Logout\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth also exposes administrative session revocation operations.\u003C/p\u003E\n\u003Cp\u003EThese methods act on a specified user and require appropriate server-side authorization.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022logout-one-user-device\u0022\u003ELogout One User Device\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.Flows.LogoutUserDeviceAsync(\n userKey,\n new LogoutDeviceRequest\n {\n ChainId = chainId\n });\n\nif (!result.IsSuccess)\n{\n // Handle the failure.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022logout-other-devices-for-a-user\u0022\u003ELogout Other Devices for a User\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.Flows.LogoutUserOtherDevicesAsync(userKey, request);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EHere, \u003Ccode\u003Erequest\u003C/code\u003E is a \u003Ccode\u003ELogoutOtherDevicesRequest\u003C/code\u003E identifying the chain to retain.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022logout-all-devices-for-a-user\u0022\u003ELogout All Devices for a User\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.Flows.LogoutAllUserDevicesAsync(userKey);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAdministrative methods do not grant administrative privileges by themselves.\u003C/p\u003E\n\u003Cp\u003EThe server must authorize the actor and validate the target user and requested revocation scope.\u003C/p\u003E\n\u003Cp\u003EThese operations are particularly relevant to administration panels and account security workflows.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022building-login-forms-with-uauthloginform\u0022\u003E17. Building Login Forms with UAuthLoginForm\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth provides \u003Ccode\u003EUAuthLoginForm\u003C/code\u003E, a reusable Blazor component that handles authentication form submission without requiring developers to implement the underlying flow orchestration themselves.\u003C/p\u003E\n\u003Cp\u003EThe component supports password-based login, PKCE completion through UAuthHub, configurable submission modes, structured validation feedback, and customizable UI content.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EYou design the login interface. UltimateAuth handles the authentication submission workflow.\u003C/strong\u003E\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022basic-usage\u0022\u003EBasic Usage\u003C/h3\u003E\n\u003Cp\u003EA login form can be implemented with the following 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.Blazor\n\n\u0026lt;UAuthLoginForm Identifier=\u0026quot;@email\u0026quot; Secret=\u0026quot;@password\u0026quot;\u0026gt;\n \u0026lt;input @bind=\u0026quot;email\u0026quot; placeholder=\u0026quot;Email\u0026quot; /\u0026gt;\n \u0026lt;input @bind=\u0026quot;password\u0026quot; type=\u0026quot;password\u0026quot; placeholder=\u0026quot;Password\u0026quot; /\u0026gt;\n \u0026lt;button type=\u0026quot;submit\u0026quot;\u0026gt;Sign in\u0026lt;/button\u0026gt;\n\u0026lt;/UAuthLoginForm\u0026gt;\n\n@code {\n private string email = \u0026quot;\u0026quot;;\n private string password = \u0026quot;\u0026quot;;\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe component automatically uses \u003Ccode\u003ETryAndCommit\u003C/code\u003E as its default submission mode.\u003C/p\u003E\n\u003Cp\u003EWhen submitted, it:\u003C/p\u003E\n\u003Col\u003E\n\u003Cli\u003EValidates that the identifier and secret are present.\u003C/li\u003E\n\u003Cli\u003ECreates the appropriate authentication request.\u003C/li\u003E\n\u003Cli\u003ECalls the UltimateAuth client flow.\u003C/li\u003E\n\u003Cli\u003EPerforms the configured try/commit behavior.\u003C/li\u003E\n\u003Cli\u003EExposes structured results through \u003Ccode\u003EOnTryResult\u003C/code\u003E.\u003C/li\u003E\n\u003C/ol\u003E\n\u003Cp\u003EThe example assumes that UltimateAuth\u0027s Blazor services have already been registered and that the hosting environment permits the selected login flow.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022customizing-the-login-ui\u0022\u003ECustomizing the Login UI\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthLoginForm\u003C/code\u003E uses \u003Ccode\u003EChildContent\u003C/code\u003E, allowing developers to use standard HTML elements or their preferred Blazor component library.\u003C/p\u003E\n\u003Cp\u003EFor example, with MudBlazor:\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.Blazor\n@using CodeBeam.UltimateAuth.Client\n\n\u0026lt;UAuthLoginForm Identifier=\u0026quot;@email\u0026quot; Secret=\u0026quot;@password\u0026quot; SubmitMode=\u0026quot;UAuthSubmitMode.TryAndCommit\u0026quot; OnTryResult=\u0026quot;HandleLoginResult\u0026quot;\u0026gt;\n \u0026lt;MudStack Spacing=\u0026quot;3\u0026quot;\u0026gt;\n \u0026lt;MudText Typo=\u0026quot;Typo.h5\u0026quot;\u0026gt;Sign in\u0026lt;/MudText\u0026gt;\n \u0026lt;MudTextField @bind-Value=\u0026quot;email\u0026quot; Label=\u0026quot;Email\u0026quot; Variant=\u0026quot;Variant.Outlined\u0026quot; /\u0026gt;\n \u0026lt;MudTextField @bind-Value=\u0026quot;password\u0026quot; Label=\u0026quot;Password\u0026quot; InputType=\u0026quot;InputType.Password\u0026quot; Variant=\u0026quot;Variant.Outlined\u0026quot; /\u0026gt;\n\n \u0026lt;MudButton ButtonType=\u0026quot;ButtonType.Submit\u0026quot; Variant=\u0026quot;Variant.Filled\u0026quot; Color=\u0026quot;Color.Primary\u0026quot; FullWidth\u0026gt;Sign in\u0026lt;/MudButton\u0026gt;\n\n @if (!string.IsNullOrWhiteSpace(error))\n {\n \u0026lt;MudAlert Severity=\u0026quot;Severity.Error\u0026quot;\u0026gt;@error\u0026lt;/MudAlert\u0026gt;\n }\n \u0026lt;/MudStack\u0026gt;\n\u0026lt;/UAuthLoginForm\u0026gt;\n\n@code {\n private string email = \u0026quot;\u0026quot;;\n private string password = \u0026quot;\u0026quot;;\n private string? error;\n\n private Task HandleLoginResult(IUAuthTryResult result)\n {\n error = result.IsSuccess ? null : \u0026quot;Unable to sign in.\u0026quot;;\n return Task.CompletedTask;\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe form controls remain under your control.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth does not require a particular visual design, CSS framework, or component library.\u003C/p\u003E\n\u003Cp\u003EBecause the component renders an HTML form internally, place the submit button inside its child content and avoid nesting another HTML form within it.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022submission-modes\u0022\u003ESubmission Modes\u003C/h3\u003E\n\u003Cp\u003EUse \u003Ccode\u003ESubmitMode\u003C/code\u003E to control how the authentication attempt is executed.\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\u0026lt;UAuthLoginForm Identifier=\u0026quot;@email\u0026quot; Secret=\u0026quot;@password\u0026quot; SubmitMode=\u0026quot;UAuthSubmitMode.TryAndCommit\u0026quot;\u0026gt;\n ...\n\u0026lt;/UAuthLoginForm\u0026gt;\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\u003EMode\u003C/th\u003E\n\u003Cth\u003EBehavior\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETryOnly\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EValidates the authentication attempt without committing login\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETryAndCommit\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAttempts authentication and commits on success\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDirectCommit\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESubmits directly to the commit flow\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003ETryAndCommit\u003C/code\u003E is the default.\u003C/p\u003E\n\u003Cp\u003EFor interactive login forms that need validation feedback, it is generally the recommended choice.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EDirectCommit\u003C/code\u003E does not invoke \u003Ccode\u003EOnTryResult\u003C/code\u003E, because it does not perform the structured try operation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022handling-authentication-results\u0022\u003EHandling Authentication Results\u003C/h3\u003E\n\u003Cp\u003EThe component exposes an \u003Ccode\u003EOnTryResult\u003C/code\u003E callback:\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\u0026lt;UAuthLoginForm Identifier=\u0026quot;@email\u0026quot; Secret=\u0026quot;@password\u0026quot; OnTryResult=\u0026quot;HandleResult\u0026quot;\u0026gt;\n ...\n\u0026lt;/UAuthLoginForm\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe callback receives \u003Ccode\u003EIUAuthTryResult\u003C/code\u003E, allowing the same component to report password login and PKCE completion results.\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\u003Eprivate Task HandleResult(IUAuthTryResult result)\n{\n if (!result.IsSuccess)\n {\n errorMessage = \u0026quot;Authentication failed.\u0026quot;;\n }\n\n return Task.CompletedTask;\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EFor flow-specific details, inspect the concrete result 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\u003Eprivate Task HandleResult(IUAuthTryResult result)\n{\n if (result is TryLoginResult loginResult)\n {\n if (loginResult.LockoutUntilUtc is not null)\n {\n errorMessage = \u0026quot;Your account is temporarily locked.\u0026quot;;\n }\n else if (loginResult.RequiresMfa)\n {\n // Continue the supported additional authentication workflow.\n }\n else if (!loginResult.IsSuccess)\n {\n errorMessage = \u0026quot;Unable to sign in.\u0026quot;;\n }\n }\n else if (result is TryPkceLoginResult pkceResult)\n {\n if (pkceResult.RetryWithNewPkce)\n {\n errorMessage = \u0026quot;Please restart the authentication flow.\u0026quot;;\n }\n else if (!pkceResult.IsSuccess)\n {\n errorMessage = \u0026quot;Unable to complete authentication.\u0026quot;;\n }\n }\n\n return Task.CompletedTask;\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe callback reports the structured try result. It does not replace authentication state management or server-side validation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022using-uauthloginform-with-pkce\u0022\u003EUsing UAuthLoginForm with PKCE\u003C/h3\u003E\n\u003Cp\u003EThe same component also supports PKCE login completion when hosted within a supported UAuthHub environment.\u003C/p\u003E\n\u003Cp\u003ESet \u003Ccode\u003ELoginType\u003C/code\u003E to \u003Ccode\u003EPkce\u003C/code\u003E:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E\u0026lt;UAuthLoginForm LoginType=\u0026quot;UAuthLoginType.Pkce\u0026quot; Identifier=\u0026quot;@email\u0026quot; Secret=\u0026quot;@password\u0026quot; SubmitMode=\u0026quot;UAuthSubmitMode.TryAndCommit\u0026quot; OnTryResult=\u0026quot;HandleLoginResult\u0026quot;\u0026gt;\n \u0026lt;input @bind=\u0026quot;email\u0026quot; placeholder=\u0026quot;Email\u0026quot; /\u0026gt;\n \u0026lt;input @bind=\u0026quot;password\u0026quot; type=\u0026quot;password\u0026quot; placeholder=\u0026quot;Password\u0026quot; /\u0026gt;\n \u0026lt;button type=\u0026quot;submit\u0026quot;\u0026gt;Continue\u0026lt;/button\u0026gt;\n\u0026lt;/UAuthLoginForm\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EIn PKCE mode, the component:\u003C/p\u003E\n\u003Col\u003E\n\u003Cli\u003EResolves the relevant UAuthHub session context.\u003C/li\u003E\n\u003Cli\u003ERetrieves the authorization code and code verifier through the configured Hub services.\u003C/li\u003E\n\u003Cli\u003EBuilds a \u003Ccode\u003EPkceCompleteRequest\u003C/code\u003E.\u003C/li\u003E\n\u003Cli\u003EInvokes the corresponding PKCE completion flow.\u003C/li\u003E\n\u003Cli\u003EReports the structured result when a try-enabled submission mode is used.\u003C/li\u003E\n\u003C/ol\u003E\n\u003Cp\u003EThe application does not need to manually construct the PKCE completion request for this form.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EImportant:\u003C/strong\u003E PKCE mode requires a supported server-side UAuthHub environment. \u003Ccode\u003EUAuthLoginForm\u003C/code\u003E checks the available Hub capabilities and throws when PKCE login is requested in an unsupported client profile.\u003C/p\u003E\n\u003Cp\u003EA Blazor WebAssembly application should initiate PKCE using \u003Ccode\u003EBeginPkceAsync\u003C/code\u003E; the Hub-hosted login form handles the completion stage.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022return-url-handling\u0022\u003EReturn URL Handling\u003C/h3\u003E\n\u003Cp\u003EYou can provide an explicit return URL:\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\u0026lt;UAuthLoginForm Identifier=\u0026quot;@email\u0026quot; Secret=\u0026quot;@password\u0026quot; ReturnUrl=\u0026quot;/dashboard\u0026quot;\u0026gt;\n ...\n\u0026lt;/UAuthLoginForm\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe component resolves the effective return URL using the following precedence:\u003C/p\u003E\n\u003Col\u003E\n\u003Cli\u003EExplicit \u003Ccode\u003EReturnUrl\u003C/code\u003E parameter.\u003C/li\u003E\n\u003Cli\u003EUAuthHub flow return URL when using PKCE.\u003C/li\u003E\n\u003Cli\u003ECurrent navigation URL.\u003C/li\u003E\n\u003C/ol\u003E\n\u003Cp\u003EThis allows a shared login component to participate in different authentication journeys without hardcoding navigation logic into the form.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022custom-login-endpoints\u0022\u003ECustom Login Endpoints\u003C/h3\u003E\n\u003Cp\u003EFor advanced integrations, the \u003Ccode\u003EEndpoint\u003C/code\u003E parameter allows the form\u0027s HTML action endpoint to be overridden:\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\u0026lt;UAuthLoginForm Identifier=\u0026quot;@email\u0026quot; Secret=\u0026quot;@password\u0026quot; Endpoint=\u0026quot;/custom-login\u0026quot; SubmitMode=\u0026quot;UAuthSubmitMode.DirectCommit\u0026quot;\u0026gt;\n ...\n\u0026lt;/UAuthLoginForm\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe endpoint is resolved relative to the configured UltimateAuth endpoint base path and multi-tenant settings.\u003C/p\u003E\n\u003Cp\u003ENote that \u003Ccode\u003EEndpoint\u003C/code\u003E affects the form\u0027s HTML action. Try-based submissions invoke the configured \u003Ccode\u003EUAuthClient.Flows\u003C/code\u003E endpoints, so changing \u003Ccode\u003EEndpoint\u003C/code\u003E alone does not redirect those SDK requests to a custom endpoint.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022submit-the-form-programmatically\u0022\u003ESubmit the Form Programmatically\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthLoginForm\u003C/code\u003E exposes \u003Ccode\u003ESubmitAsync\u003C/code\u003E, allowing submission through a component reference.\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\u0026lt;UAuthLoginForm @ref=\u0026quot;loginForm\u0026quot; Identifier=\u0026quot;@email\u0026quot; Secret=\u0026quot;@password\u0026quot; AllowEnterKeyToSubmit=\u0026quot;false\u0026quot;\u0026gt;\n \u0026lt;input @bind=\u0026quot;email\u0026quot; placeholder=\u0026quot;Email\u0026quot; /\u0026gt;\n \u0026lt;input @bind=\u0026quot;password\u0026quot; type=\u0026quot;password\u0026quot; /\u0026gt;\n\u0026lt;/UAuthLoginForm\u0026gt;\n\n\u0026lt;button type=\u0026quot;button\u0026quot; @onclick=\u0026quot;SubmitLoginAsync\u0026quot;\u0026gt;\n Sign in\n\u0026lt;/button\u0026gt;\n\n@code {\n private UAuthLoginForm? loginForm;\n\n private string email = \u0026quot;\u0026quot;;\n private string password = \u0026quot;\u0026quot;;\n\n private async Task SubmitLoginAsync()\n {\n if (loginForm is not null)\n {\n await loginForm.SubmitAsync();\n }\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003ESubmitAsync\u003C/code\u003E must be called after the component has rendered.\u003C/p\u003E\n\u003Cp\u003EThis is useful when submission is initiated from a custom button, dialog, or application-specific interaction.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022controlling-enter-key-submission\u0022\u003EControlling Enter-Key Submission\u003C/h3\u003E\n\u003Cp\u003EBy default, the component supports Enter-key submission.\u003C/p\u003E\n\u003Cp\u003EYou can disable its built-in hidden submit button:\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\u0026lt;UAuthLoginForm Identifier=\u0026quot;@email\u0026quot; Secret=\u0026quot;@password\u0026quot; AllowEnterKeyToSubmit=\u0026quot;false\u0026quot;\u0026gt;\n ...\n\u0026lt;/UAuthLoginForm\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis option controls the component\u0027s built-in Enter-key submit mechanism. Other submit buttons inside the form may still trigger normal browser form submission behavior.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022reloading-pkce-context\u0022\u003EReloading PKCE Context\u003C/h3\u003E\n\u003Cp\u003EThe component exposes \u003Ccode\u003EReloadAsync\u003C/code\u003E to reload its Hub credentials and flow state.\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\u0026lt;UAuthLoginForm @ref=\u0026quot;loginForm\u0026quot; LoginType=\u0026quot;UAuthLoginType.Pkce\u0026quot; Identifier=\u0026quot;@email\u0026quot; Secret=\u0026quot;@password\u0026quot;\u0026gt;\n ...\n\u0026lt;/UAuthLoginForm\u0026gt;\n\n@code {\n private UAuthLoginForm? loginForm;\n\n private async Task ReloadLoginContextAsync()\n {\n if (loginForm is not null)\n {\n await loginForm.ReloadAsync();\n }\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis is an advanced capability intended for scenarios where the associated Hub state changes and the component must refresh its internal context.\u003C/p\u003E\n\u003Cp\u003EIt does not initiate a new PKCE authorization flow by itself.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022component-parameters\u0022\u003EComponent Parameters\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EParameter\u003C/th\u003E\n\u003Cth\u003EType\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\u003EIdentifier\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Estring?\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELogin identifier\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESecret\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Estring?\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication secret\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELoginType\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthLoginType\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EPassword\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPassword or PKCE login\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESubmitMode\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthSubmitMode\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003ETryAndCommit\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESubmission strategy\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EReturnUrl\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Estring?\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERedirect destination\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEndpoint\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Estring?\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECustom HTML form endpoint\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EHubSessionId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EHubSessionId?\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EExplicit Hub session context\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAllowEnterKeyToSubmit\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Ebool\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EBuilt-in Enter-key submit support\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EOnTryResult\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EEventCallback\u0026lt;IUAuthTryResult\u0026gt;\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u2014\u003C/td\u003E\n\u003Ctd\u003EStructured try result callback\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EChildContent\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003ERenderFragment?\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u2014\u003C/td\u003E\n\u003Ctd\u003ECustom form content\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=\u0022when-to-use-uauthloginform\u0022\u003EWhen to Use UAuthLoginForm\u003C/h3\u003E\n\u003Cp\u003EUse \u003Ccode\u003EUAuthLoginForm\u003C/code\u003E when building a Blazor login interface that should integrate directly with UltimateAuth\u0027s supported authentication flows.\u003C/p\u003E\n\u003Cp\u003EIt is particularly useful when:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EYou want to use your own component library without implementing the authentication submission logic.\u003C/li\u003E\n\u003Cli\u003EYou need structured login validation feedback.\u003C/li\u003E\n\u003Cli\u003EYou want to switch between try-only and commit behavior.\u003C/li\u003E\n\u003Cli\u003EYou are building a UAuthHub login page that completes PKCE.\u003C/li\u003E\n\u003Cli\u003EYou want to share login UI across supported Blazor hosting environments.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EFor application-specific workflows that cannot be expressed through the component, use \u003Ccode\u003EUAuthClient.Flows\u003C/code\u003E directly.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EThe component handles the login workflow, while your application retains control over presentation, user experience, and surrounding application logic.\u003C/strong\u003E\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022common-integration-mistakes\u0022\u003E18. Common Integration Mistakes\u003C/h2\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022calling-direct-login-from-a-public-client\u0022\u003ECalling Direct Login from a Public Client\u003C/h3\u003E\n\u003Cp\u003E\u003Cstrong\u003EProblem:\u003C/strong\u003E \u003Ccode\u003ELoginAsync\u003C/code\u003E throws because direct credential posting is disabled.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003ESolution:\u003C/strong\u003E Use \u003Ccode\u003EBeginPkceAsync\u003C/code\u003E and the supported PKCE completion flow. Do not enable \u003Ccode\u003EAllowCredentialPost\u003C/code\u003E to bypass the public-client restriction.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022treating-tryonly-as-authentication\u0022\u003ETreating TryOnly as Authentication\u003C/h3\u003E\n\u003Cp\u003E\u003Cstrong\u003EProblem:\u003C/strong\u003E The application receives \u003Ccode\u003EIsSuccess = true\u003C/code\u003E from \u003Ccode\u003ETryOnly\u003C/code\u003E and assumes the user is logged in.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003ESolution:\u003C/strong\u003E A successful try result does not commit authentication. Use \u003Ccode\u003ETryAndCommit\u003C/code\u003E or the appropriate commit flow.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022assuming-directcommit-skips-server-validation\u0022\u003EAssuming DirectCommit Skips Server Validation\u003C/h3\u003E\n\u003Cp\u003E\u003Cstrong\u003EProblem:\u003C/strong\u003E DirectCommit is interpreted as a faster but less secure authentication mode.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003ESolution:\u003C/strong\u003E DirectCommit skips the separate client try stage. The server still validates the actual authentication request.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022assuming-every-refresh-rotates-tokens\u0022\u003EAssuming Every Refresh Rotates Tokens\u003C/h3\u003E\n\u003Cp\u003E\u003Cstrong\u003EProblem:\u003C/strong\u003E The application requires \u003Ccode\u003ERefreshOutcome.Rotated\u003C/code\u003E for refresh success.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003ESolution:\u003C/strong\u003E Check \u003Ccode\u003ERefreshResult.IsSuccess\u003C/code\u003E. Rotation, touch, and no-op behavior depend on the authentication mode and refresh strategy.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022using-a-session-id-for-device-logout\u0022\u003EUsing a Session ID for Device Logout\u003C/h3\u003E\n\u003Cp\u003E\u003Cstrong\u003EProblem:\u003C/strong\u003E A session identifier is passed to a device logout operation.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003ESolution:\u003C/strong\u003E \u003Ccode\u003ELogoutMyDeviceAsync\u003C/code\u003E requires \u003Ccode\u003ELogoutDeviceRequest\u003C/code\u003E with a \u003Ccode\u003ESessionChainId\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022assuming-logout-immediately-invalidates-all-jwts\u0022\u003EAssuming Logout Immediately Invalidates All JWTs\u003C/h3\u003E\n\u003Cp\u003E\u003Cstrong\u003EProblem:\u003C/strong\u003E An application expects every issued JWT access token to become unusable immediately after server-side logout.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003ESolution:\u003C/strong\u003E Understand the enforcement behavior of SemiHybrid and PureJwt. Local JWT validation may continue accepting previously issued tokens until expiration.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022using-validateasync-on-every-render\u0022\u003EUsing ValidateAsync on Every Render\u003C/h3\u003E\n\u003Cp\u003E\u003Cstrong\u003EProblem:\u003C/strong\u003E Components repeatedly make validation requests just to display authenticated UI.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003ESolution:\u003C/strong\u003E Use the appropriate authentication state integration for UI rendering, and reserve explicit validation for operations that require it.\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-sdk/session-management\u0022\u003ESession Management\u003C/a\u003E \u2014 Inspect and manage session and device information.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/security/token-security\u0022\u003EToken Security\u003C/a\u003E \u2014 Understand token validity, refresh behavior, and revocation.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/security/refresh-rotation\u0022\u003ERefresh Rotation \u0026amp; Replay Protection\u003C/a\u003E \u2014 Explore refresh concurrency and reuse security.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/fundamentals/auth-modes\u0022\u003EAuthentication Modes\u003C/a\u003E \u2014 Compare authentication behavior across modes.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/sessions-devices/device-management\u0022\u003EDevice Management\u003C/a\u003E \u2014 Understand device-associated session chains.\u003C/li\u003E\n\u003C/ul\u003E\n",
+ "Html": "\n\u003Cp\u003EThe UltimateAuth Client SDK provides a high-level API for authentication workflows, including login, PKCE, session validation, refresh, logout, and device-specific session revocation.\u003C/p\u003E\n\u003Cp\u003EThese operations are exposed through \u003Ccode\u003EUAuthClient.Flows\u003C/code\u003E, which implements the \u003Ccode\u003EIFlowClient\u003C/code\u003E contract.\u003C/p\u003E\n\u003Cp\u003EThis guide shows how to use the authentication client in application code, handle structured results, configure interactive behavior, and choose the correct flow for different client environments.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022accessing-the-authentication-client\u0022\u003E1. Accessing the Authentication Client\u003C/h2\u003E\n\u003Cp\u003EIn a Blazor component, inject \u003Ccode\u003EIUAuthClient\u003C/code\u003E:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@using CodeBeam.UltimateAuth.Client\n@inject IUAuthClient UAuthClient\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAuthentication operations are available through the \u003Ccode\u003EFlows\u003C/code\u003E property.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eawait UAuthClient.Flows.RefreshAsync();\nawait UAuthClient.Flows.ValidateAsync();\nawait UAuthClient.Flows.LogoutAsync();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe relevant methods are defined by \u003Ccode\u003EIFlowClient\u003C/code\u003E.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EMethod\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\u003ELoginAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESubmit a direct login flow\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETryLoginAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAttempt login with configurable try/commit behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EBeginPkceAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EStart a PKCE authentication flow\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECompletePkceLoginAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EComplete a PKCE login\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETryCompletePkceLoginAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EComplete PKCE with structured try/commit behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERefreshAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERefresh or continue authentication\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EValidateAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EValidate current authentication state\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELogoutAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELog out the current session\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELogoutMyDeviceAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERevoke a selected device-associated chain\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELogoutMyOtherDevicesAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERevoke other devices while retaining the current one\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELogoutAllMyDevicesAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERevoke all devices for the current user\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EAdministrative device logout operations are also available and covered later in this guide.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022choosing-the-correct-login-flow\u0022\u003E2. Choosing the Correct Login Flow\u003C/h2\u003E\n\u003Cp\u003EBefore implementing login, determine whether your application is a trusted server-hosted client or a public client.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EClient environment\u003C/th\u003E\n\u003Cth\u003ERecommended flow\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EBlazor Server\u003C/td\u003E\n\u003Ctd\u003EDirect login or supported interactive login flow\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EUAuthHub\u003C/td\u003E\n\u003Ctd\u003EServer-hosted authentication flow\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EBlazor WebAssembly\u003C/td\u003E\n\u003Ctd\u003EPKCE\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EBrowser SPA\u003C/td\u003E\n\u003Ctd\u003EPKCE\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EPublic native application\u003C/td\u003E\n\u003Ctd\u003EPKCE\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EPublic clients must not rely on embedding a secret or trusting browser-executed code to protect credentials.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth restricts direct credential posting based on the configured client profile and login options.\u003C/p\u003E\n\u003Cp\u003EFor public clients, use the PKCE flow instead of enabling direct credential posting.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022direct-login\u0022\u003E3. Direct Login\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003ELoginAsync\u003C/code\u003E performs a direct login submission.\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 request = new LoginRequest\n{\n Identifier = \u0026quot;user@example.com\u0026quot;,\n Secret = \u0026quot;password\u0026quot;\n};\n\nawait UAuthClient.Flows.LoginAsync(request);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns \u003Ccode\u003ETask\u003C/code\u003E, not a structured login result.\u003C/p\u003E\n\u003Cp\u003EIt submits the login payload through the configured navigation-based transport.\u003C/p\u003E\n\u003Cp\u003EDepending on the hosting environment, this may initiate navigation or redirect behavior.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022loginrequest-properties\u0022\u003ELoginRequest Properties\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003ELoginRequest\u003C/code\u003E supports the following 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\u003Ccode\u003EIdentifier\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELogin identifier\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESecret\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECredential secret\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECredentialType\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECredential type; defaults to \u003Ccode\u003EPassword\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMetadata\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional request metadata\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERequestTokens\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EHint to request tokens when supported\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPreviewReceipt\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional receipt associated with a prior login preview\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EExample:\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 request = new LoginRequest\n{\n Identifier = email,\n Secret = password,\n CredentialType = CredentialType.Password,\n RequestTokens = true\n};\n\nawait UAuthClient.Flows.LoginAsync(request);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003ERequestTokens\u003C/code\u003E is a hint, not a guarantee that tokens will be issued. The server\u0027s authentication mode and policy remain authoritative.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022redirect-after-login\u0022\u003ERedirect After Login\u003C/h3\u003E\n\u003Cp\u003EProvide an explicit return URL when necessary:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eawait UAuthClient.Flows.LoginAsync(request, returnUrl: \u0026quot;/dashboard\u0026quot;);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe direct login flow resolves its return URL in the following order:\u003C/p\u003E\n\u003Col\u003E\n\u003Cli\u003EThe explicit \u003Ccode\u003EreturnUrl\u003C/code\u003E argument.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003ELogin.ReturnUrl\u003C/code\u003E option.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EDefaultReturnUrl\u003C/code\u003E option.\u003C/li\u003E\n\u003C/ol\u003E\n\u003Cp\u003EFor example, client options can specify a default:\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.Login.ReturnUrl = \u0026quot;/dashboard\u0026quot;;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe return URL is part of the login workflow. It should not be treated as an authorization grant.\u003C/p\u003E\n\u003Cp\u003EApplications and servers must still enforce their redirect validation rules.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022interactive-login-with-tryloginasync\u0022\u003E4. Interactive Login with TryLoginAsync\u003C/h2\u003E\n\u003Cp\u003EFor interactive forms, \u003Ccode\u003ETryLoginAsync\u003C/code\u003E provides structured feedback before or during the authentication commit process.\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.Flows.TryLoginAsync(\n new LoginRequest\n {\n Identifier = email,\n Secret = password\n },\n UAuthSubmitMode.TryAndCommit);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis is generally the more useful API for a login form that needs to display validation feedback.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022submit-modes\u0022\u003ESubmit Modes\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthSubmitMode\u003C/code\u003E defines three submission behaviors.\u003C/p\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\u003EBehavior\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETryOnly\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EExecute the try operation and return its result without committing authentication\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDirectCommit\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESubmit directly to the commit endpoint without a separate try operation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETryAndCommit\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETry first, then commit when the try operation succeeds\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThese modes control client workflow, not whether the server validates authentication.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EDirectCommit does not bypass credential verification or server security requirements.\u003C/strong\u003E\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022tryonly-validate-without-login\u0022\u003ETryOnly: Validate Without Login\u003C/h3\u003E\n\u003Cp\u003EUse \u003Ccode\u003ETryOnly\u003C/code\u003E when you want structured feedback without establishing an authenticated session.\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.Flows.TryLoginAsync(\n new LoginRequest\n {\n Identifier = email,\n Secret = password\n },\n UAuthSubmitMode.TryOnly);\n\nif (result.IsSuccess)\n{\n // The try operation succeeded.\n // Authentication has not been committed.\n}\nelse\n{\n // Display the appropriate feedback.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis can be useful for a custom interactive workflow.\u003C/p\u003E\n\u003Cp\u003EA successful try result must not be interpreted as an authenticated application session.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022tryandcommit-interactive-login\u0022\u003ETryAndCommit: Interactive Login\u003C/h3\u003E\n\u003Cp\u003EUse \u003Ccode\u003ETryAndCommit\u003C/code\u003E to combine structured validation with authentication commit.\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.Flows.TryLoginAsync(\n new LoginRequest\n {\n Identifier = email,\n Secret = password\n },\n UAuthSubmitMode.TryAndCommit,\n returnUrl: \u0026quot;/dashboard\u0026quot;);\n\nif (!result.IsSuccess)\n{\n // Show validation feedback.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWhen the try stage succeeds, the client proceeds to the commit operation through its configured transport.\u003C/p\u003E\n\u003Cp\u003EThis can involve navigation, so application code should not assume that execution always continues within the same component instance.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022directcommit-single-commit-submission\u0022\u003EDirectCommit: Single Commit Submission\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\u003Eawait UAuthClient.Flows.TryLoginAsync(\n new LoginRequest\n {\n Identifier = email,\n Secret = password\n },\n UAuthSubmitMode.DirectCommit);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EIn this mode, the SDK submits the commit operation directly.\u003C/p\u003E\n\u003Cp\u003EThe method returns a \u003Ccode\u003ETryLoginResult\u003C/code\u003E with \u003Ccode\u003EIsSuccess = true\u003C/code\u003E after the navigation submission completes, but this is not a structured server-side authentication success confirmation.\u003C/p\u003E\n\u003Cp\u003EIf you need structured credential validation feedback, prefer \u003Ccode\u003ETryAndCommit\u003C/code\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022handling-login-results\u0022\u003E5. Handling Login Results\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003ETryLoginAsync\u003C/code\u003E returns \u003Ccode\u003ETryLoginResult\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EIts available properties include:\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\u003EMeaning\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIsSuccess\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EWhether the try operation succeeded\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EReason\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional authentication failure reason\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERemainingAttempts\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERemaining attempts, when provided\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELockoutUntilUtc\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELockout expiration, when provided\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERequiresMfa\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EWhether additional authentication is required\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPreviewReceipt\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional receipt for subsequent flow handling\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EExample:\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.Flows.TryLoginAsync(request, UAuthSubmitMode.TryOnly);\n\nif (!result.IsSuccess)\n{\n if (result.LockoutUntilUtc is not null)\n {\n errorMessage = $\u0026quot;Login temporarily unavailable until {result.LockoutUntilUtc:u}\u0026quot;;\n }\n else\n {\n errorMessage = \u0026quot;Unable to sign in.\u0026quot;;\n }\n\n return;\n}\n\nif (result.RequiresMfa)\n{\n // Continue with the supported additional authentication flow.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EDo not expose sensitive internal authentication details unnecessarily.\u003C/p\u003E\n\u003Cp\u003EFor public-facing login forms, generic failure messages may be preferable to displaying every failure reason.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022handling-transport-and-protocol-errors\u0022\u003EHandling Transport and Protocol Errors\u003C/h3\u003E\n\u003Cp\u003EA structured authentication failure is different from a network or protocol failure.\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\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Etry\n{\n var result = await UAuthClient.Flows.TryLoginAsync(request, UAuthSubmitMode.TryAndCommit);\n\n if (!result.IsSuccess)\n {\n errorMessage = \u0026quot;Unable to sign in.\u0026quot;;\n }\n}\ncatch (UAuthTransportException)\n{\n errorMessage = \u0026quot;Authentication service is unavailable.\u0026quot;;\n}\ncatch (UAuthProtocolException)\n{\n errorMessage = \u0026quot;An authentication protocol error occurred.\u0026quot;;\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUse the corresponding UltimateAuth client error namespace when referencing these exception types.\u003C/p\u003E\n\u003Cp\u003EA failed authentication attempt should normally be handled through the structured try result where supported. Transport and protocol exceptions indicate different failure categories.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022pkce-authentication\u0022\u003E6. PKCE Authentication\u003C/h2\u003E\n\u003Cp\u003EPKCE provides a proof mechanism for authorization-code-based authentication flows.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth supports PKCE using an S256 code challenge.\u003C/p\u003E\n\u003Cp\u003EThe SDK generates a cryptographically random verifier, derives its challenge, and coordinates the authorization flow.\u003C/p\u003E\n\u003Cp\u003EPublic clients such as Blazor WebAssembly should use PKCE rather than direct credential posting.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022start-pkce\u0022\u003EStart PKCE\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\u003Eawait UAuthClient.Flows.BeginPkceAsync();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EYou can provide a return URL:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eawait UAuthClient.Flows.BeginPkceAsync(returnUrl: \u0026quot;/dashboard\u0026quot;);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe client performs the following operations:\u003C/p\u003E\n\u003Col\u003E\n\u003Cli\u003EGenerates a PKCE code verifier.\u003C/li\u003E\n\u003Cli\u003EDerives an S256 code challenge.\u003C/li\u003E\n\u003Cli\u003ESends the authorization request.\u003C/li\u003E\n\u003Cli\u003EReceives an authorization code.\u003C/li\u003E\n\u003Cli\u003EInvokes the configured authorization callback, if any.\u003C/li\u003E\n\u003Cli\u003ENavigates to the configured UAuthHub login flow when automatic redirection is enabled.\u003C/li\u003E\n\u003C/ol\u003E\n\u003Cp\u003EThe authorization code and verifier are used later to complete the flow.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configure-pkce\u0022\u003EConfigure PKCE\u003C/h3\u003E\n\u003Cp\u003EThe relevant client options include:\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.Pkce.Enabled = true;\noptions.Pkce.AutoRedirect = true;\noptions.Pkce.ReturnUrl = \u0026quot;/dashboard\u0026quot;;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EPKCE is enabled by default in \u003Ccode\u003EUAuthClientPkceLoginFlowOptions\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EAutomatic redirection is also enabled by default.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022observe-authorization\u0022\u003EObserve Authorization\u003C/h3\u003E\n\u003Cp\u003EYou can register a callback that runs after the authorization code is received and before the automatic Hub redirect.\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.Pkce.OnAuthorized = async response =\u0026gt;\n{\n // Observe the authorization response.\n await Task.CompletedTask;\n};\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis callback is useful for application-specific coordination.\u003C/p\u003E\n\u003Cp\u003EDo not log authorization codes or expose them to unrelated components.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022disable-automatic-redirection\u0022\u003EDisable Automatic Redirection\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.Pkce.AutoRedirect = false;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWith this configuration, \u003Ccode\u003EBeginPkceAsync\u003C/code\u003E still performs the authorization request and invokes \u003Ccode\u003EOnAuthorized\u003C/code\u003E, but it does not automatically navigate to the Hub login endpoint.\u003C/p\u003E\n\u003Cp\u003EThe application becomes responsible for coordinating the remaining flow.\u003C/p\u003E\n\u003Cp\u003EThis is an advanced integration option.\u003C/p\u003E\n\u003Cp\u003EA custom implementation must preserve the authorization code, verifier, and relevant context securely and complete the flow using the supported protocol.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022complete-pkce-login\u0022\u003E7. Complete PKCE Login\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003ECompletePkceLoginAsync\u003C/code\u003E submits the PKCE completion request.\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 request = new PkceCompleteRequest\n{\n AuthorizationCode = authorizationCode,\n CodeVerifier = codeVerifier,\n Identifier = email,\n Secret = password,\n ReturnUrl = \u0026quot;/dashboard\u0026quot;\n};\n\nawait UAuthClient.Flows.CompletePkceLoginAsync(request);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe request model includes:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EPurpose\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAuthorizationCode\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECode obtained from the authorization step\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECodeVerifier\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOriginal PKCE verifier\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdentifier\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELogin identifier\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESecret\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELogin credential\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EReturnUrl\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional post-login destination\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EHubSessionId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional Hub session context\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe authorization code and verifier must correspond to the same PKCE flow.\u003C/p\u003E\n\u003Cp\u003EDo not generate a new verifier when completing an existing authorization.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022interactive-pkce-completion\u0022\u003EInteractive PKCE Completion\u003C/h3\u003E\n\u003Cp\u003EFor structured feedback, use \u003Ccode\u003ETryCompletePkceLoginAsync\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.Flows.TryCompletePkceLoginAsync(\n request,\n UAuthSubmitMode.TryAndCommit);\n\nif (!result.IsSuccess)\n{\n // Display PKCE login feedback.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe same submit modes are supported:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003ETryOnly\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EDirectCommit\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003ETryAndCommit\u003C/code\u003E\u003C/li\u003E\n\u003C/ul\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022try-pkce-without-commit\u0022\u003ETry PKCE Without Commit\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.Flows.TryCompletePkceLoginAsync(request, UAuthSubmitMode.TryOnly);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis returns a \u003Ccode\u003ETryPkceLoginResult\u003C/code\u003E without committing a successful login.\u003C/p\u003E\n\u003Cp\u003EThe result 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\u003Eresult.IsSuccess;\nresult.Reason;\nresult.RemainingAttempts;\nresult.LockoutUntilUtc;\nresult.RequiresMfa;\nresult.RetryWithNewPkce;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022retrywithnewpkce\u0022\u003ERetryWithNewPkce\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003ERetryWithNewPkce\u003C/code\u003E indicates that the client should begin a new PKCE authorization sequence rather than continuing with the existing authorization material.\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\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 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.RetryWithNewPkce)\n{\n await UAuthClient.Flows.BeginPkceAsync(\u0026quot;/login\u0026quot;);\n return;\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis is useful when the current PKCE context can no longer be used.\u003C/p\u003E\n\u003Cp\u003EThe application should not repeatedly resubmit the same invalid authorization code and verifier.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022direct-credential-posting-restrictions\u0022\u003E8. Direct Credential Posting Restrictions\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth restricts direct credential posting according to the configured client profile.\u003C/p\u003E\n\u003Cp\u003EThe client 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.Login.AllowCredentialPost = false;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis is the default.\u003C/p\u003E\n\u003Cp\u003EIf the current profile is not permitted to post credentials directly and the option has not enabled that capability, direct login methods throw an \u003Ccode\u003EInvalidOperationException\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EFor example, attempting direct login from a public client may produce an error indicating that PKCE is required.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022trusted-server-hosted-clients\u0022\u003ETrusted Server-Hosted Clients\u003C/h3\u003E\n\u003Cp\u003EThe option can be explicitly configured for a trusted server-hosted integration where direct credential posting is appropriate:\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.Login.AllowCredentialPost = true;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis must not be used to bypass PKCE requirements in a public browser application.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EDo not enable direct credential posting in production Blazor WebAssembly or SPA clients.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EA client profile is an integration descriptor, not cryptographic proof that the caller is trusted.\u003C/p\u003E\n\u003Cp\u003EServer-side security requirements remain authoritative.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022refresh-authentication\u0022\u003E9. Refresh Authentication\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003ERefreshAsync\u003C/code\u003E requests continuation of the current authentication state.\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.Flows.RefreshAsync();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns \u003Ccode\u003ERefreshResult\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\u003Epublic sealed record RefreshResult\n{\n public bool IsSuccess { get; init; }\n public int Status { get; init; }\n public RefreshOutcome Outcome { get; init; }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ERefresh behavior depends on the authentication mode.\u003C/p\u003E\n\u003Cp\u003EA refresh operation may update session activity, rotate authentication material, or determine that reauthentication is required.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022handling-refresh-results\u0022\u003EHandling Refresh Results\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.Flows.RefreshAsync();\n\nif (result.IsSuccess)\n{\n // Authentication continuation succeeded.\n}\nelse if (result.Outcome == RefreshOutcome.ReauthRequired)\n{\n // The current authentication can no longer continue.\n // Direct the user to the appropriate login flow.\n}\nelse\n{\n // Handle another unsuccessful refresh result.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EA \u003Ccode\u003E401 Unauthorized\u003C/code\u003E refresh response is handled as a normal reauthentication outcome.\u003C/p\u003E\n\u003Cp\u003EIt 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\u003Enew RefreshResult\n{\n IsSuccess = false,\n Status = 401,\n Outcome = RefreshOutcome.ReauthRequired\n};\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ENetwork and unexpected protocol failures can still produce exceptions.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022refresh-outcomes\u0022\u003ERefresh Outcomes\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003ERefreshOutcome\u003C/code\u003E defines the following values:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EOutcome\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\u003ENone\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENo specific semantic outcome\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESuccess\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EGeneric successful refresh\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ENoOp\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENo refresh state change was necessary\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETouched\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EApplicable session activity was updated\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERotated\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERotation occurred\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EReauthRequired\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication continuation is no longer permitted\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\u003Evar result = await UAuthClient.Flows.RefreshAsync();\n\nif (!result.IsSuccess)\n{\n return;\n}\n\nswitch (result.Outcome)\n{\n case RefreshOutcome.Rotated:\n // Authentication material was rotated.\n break;\n\n case RefreshOutcome.Touched:\n // Applicable session activity was updated.\n break;\n\n case RefreshOutcome.NoOp:\n // Successful refresh and no state change was required.\n break;\n\n case RefreshOutcome.Success:\n // Refresh succeeded without a more specific outcome.\n break;\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022detailed-refresh-diagnostics\u0022\u003EDetailed Refresh Diagnostics\u003C/h3\u003E\n\u003Cp\u003EThe server may return a generic success outcome rather than exposing the internal refresh action.\u003C/p\u003E\n\u003Cp\u003EWhere supported, detailed refresh outcomes can be enabled through server diagnostics:\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.Diagnostics.EnableRefreshDetails = true;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis setting is intended for diagnostics and observability.\u003C/p\u003E\n\u003Cp\u003EApplications should not require a specific successful outcome such as \u003Ccode\u003ERotated\u003C/code\u003E or \u003Ccode\u003ETouched\u003C/code\u003E to consider refresh successful.\u003C/p\u003E\n\u003Cp\u003EUse \u003Ccode\u003EIsSuccess\u003C/code\u003E for the primary success decision.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022manual-and-automatic-refresh\u0022\u003EManual and Automatic Refresh\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003ERefreshAsync\u003C/code\u003E has an optional \u003Ccode\u003EisAuto\u003C/code\u003E argument.\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// Manual refresh\nawait UAuthClient.Flows.RefreshAsync();\n\n// Refresh initiated by automatic coordination\nawait UAuthClient.Flows.RefreshAsync(isAuto: true);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe parameter distinguishes automatic refresh activity for client diagnostics.\u003C/p\u003E\n\u003Cp\u003EIt does not select a different authentication mode or override the server\u0027s refresh policy.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022automatic-refresh-configuration\u0022\u003E10. Automatic Refresh Configuration\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth exposes client-side automatic refresh options through \u003Ccode\u003EAutoRefresh\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\u003Eoptions.AutoRefresh.Enabled = true;\noptions.AutoRefresh.Interval = TimeSpan.FromMinutes(5);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EEnabled\u003C/code\u003E controls background refresh coordination.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EInterval\u003C/code\u003E specifies the interval used for background refresh attempts.\u003C/p\u003E\n\u003Cp\u003EThe effective defaults and registration behavior depend on the client integration. The options documentation identifies Blazor Server as the environment where background refresh is enabled by default.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022automatic-refresh-is-not-a-security-guarantee\u0022\u003EAutomatic Refresh Is Not a Security Guarantee\u003C/h3\u003E\n\u003Cp\u003EAutomatic refresh is a client-side convenience mechanism.\u003C/p\u003E\n\u003Cp\u003EIt does not guarantee that a session will remain valid indefinitely.\u003C/p\u003E\n\u003Cp\u003EAuthentication may still end because of:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EExpiration.\u003C/li\u003E\n\u003Cli\u003ERevocation.\u003C/li\u003E\n\u003Cli\u003EAccount security changes.\u003C/li\u003E\n\u003Cli\u003EInvalid refresh credentials.\u003C/li\u003E\n\u003Cli\u003EServer-side continuation restrictions.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThe client must remain prepared to handle \u003Ccode\u003EReauthRequired\u003C/code\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022validate-current-authentication\u0022\u003E11. Validate Current Authentication\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EValidateAsync\u003C/code\u003E to ask the server to validate the current authentication state.\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.Flows.ValidateAsync();\n\nif (result.IsValid)\n{\n // Current authentication is valid.\n}\nelse\n{\n // Authentication is not currently valid.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns \u003Ccode\u003EAuthValidationResult\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EIts properties include:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EPurpose\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EState\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECurrent session state\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIsValid\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EWhether the state is \u003Ccode\u003EActive\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESnapshot\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional authentication state snapshot\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EChainId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional associated session chain\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERootId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional session root\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EBoundDeviceId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional bound device 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=\u0022inspect-session-context\u0022\u003EInspect Session Context\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.Flows.ValidateAsync();\n\nif (result.IsValid \u0026amp;\u0026amp; result.ChainId is { } chainId)\n{\n // Use chainId for supported session-management operations.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ENot every authentication mode necessarily provides all session hierarchy fields.\u003C/p\u003E\n\u003Cp\u003ETreat optional values as optional.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022validateasync-versus-ui-authentication-state\u0022\u003EValidateAsync Versus UI Authentication State\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EValidateAsync\u003C/code\u003E performs an explicit server validation operation.\u003C/p\u003E\n\u003Cp\u003EIt should not be called on every UI render merely to determine whether a component should display authenticated content.\u003C/p\u003E\n\u003Cp\u003EFor UI authentication state, use the supported UltimateAuth state management integration, including \u003Ccode\u003EIUAuthStateManager\u003C/code\u003E where appropriate.\u003C/p\u003E\n\u003Cp\u003EExplicit validation is useful when an application needs to check current server-recognized authentication state.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022logout\u0022\u003E12. Logout\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003ELogoutAsync\u003C/code\u003E performs the current logout flow.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eawait UAuthClient.Flows.LogoutAsync();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method uses the configured logout endpoint and navigation transport.\u003C/p\u003E\n\u003Cp\u003EDepending on the client profile and configuration, logout may involve navigation and authentication state changes.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022logout-button\u0022\u003ELogout Button\u003C/h3\u003E\n\u003Cp\u003EA simple Blazor 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\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\n\u0026lt;button @onclick=\u0026quot;LogoutAsync\u0026quot;\u0026gt;Logout\u0026lt;/button\u0026gt;\n\n@code {\n private async Task LogoutAsync()\n {\n await UAuthClient.Flows.LogoutAsync();\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ELogout is distinct from clearing local UI state.\u003C/p\u003E\n\u003Cp\u003ERemoving a local authentication indicator does not necessarily revoke authoritative server-managed authentication state.\u003C/p\u003E\n\u003Cp\u003EUse the supported logout flow to terminate authentication.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022logout-a-specific-device\u0022\u003E13. Logout a Specific Device\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth supports revoking the session chain associated with a particular device context.\u003C/p\u003E\n\u003Cp\u003EThe relevant method 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\u003ELogoutMyDeviceAsync(LogoutDeviceRequest request)\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003ELogoutDeviceRequest\u003C/code\u003E requires a \u003Ccode\u003ESessionChainId\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 request = new LogoutDeviceRequest\n{\n ChainId = chainId\n};\n\nvar result = await UAuthClient.Flows.LogoutMyDeviceAsync(request);\n\nif (result.IsSuccess)\n{\n // The requested device logout operation succeeded.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis operation targets a session chain, not an arbitrary individual session ID.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022current-device-behavior\u0022\u003ECurrent Device Behavior\u003C/h3\u003E\n\u003Cp\u003EThe returned \u003Ccode\u003ERevokeResult\u003C/code\u003E can indicate whether the affected chain is the current chain.\u003C/p\u003E\n\u003Cp\u003EWhen the operation succeeds and \u003Ccode\u003ECurrentChain\u003C/code\u003E is true, the client publishes a logout-related state event.\u003C/p\u003E\n\u003Cp\u003EThis allows the application to react when the currently authenticated device context has been revoked.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022logout-other-devices\u0022\u003E14. Logout Other Devices\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003ELogoutMyOtherDevicesAsync\u003C/code\u003E to revoke other device-associated sessions while retaining the current one.\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.Flows.LogoutMyOtherDevicesAsync();\n\nif (result.IsSuccess)\n{\n // Other devices were logged out.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis is useful for account security pages that provide a \u003Cstrong\u003ELog out other devices\u003C/strong\u003E action.\u003C/p\u003E\n\u003Cp\u003EThe current authentication context is intended to remain active.\u003C/p\u003E\n\u003Cp\u003EThe server remains responsible for identifying the current session or chain and enforcing the appropriate revocation scope.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022logout-all-devices\u0022\u003E15. Logout All Devices\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003ELogoutAllMyDevicesAsync\u003C/code\u003E to revoke all applicable sessions for the current 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.Flows.LogoutAllMyDevicesAsync();\n\nif (result.IsSuccess)\n{\n // All applicable device sessions were revoked.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EOn success, the client publishes a logout-related state event.\u003C/p\u003E\n\u003Cp\u003EThis operation is useful when a user suspects account compromise or wants to terminate existing authentication sessions across devices.\u003C/p\u003E\n\u003Cp\u003EIn JWT-based modes, previously issued access tokens may remain cryptographically valid until expiration unless additional revocation enforcement is configured.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022administrative-device-logout\u0022\u003E16. Administrative Device Logout\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth also exposes administrative session revocation operations.\u003C/p\u003E\n\u003Cp\u003EThese methods act on a specified user and require appropriate server-side authorization.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022logout-one-user-device\u0022\u003ELogout One User Device\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.Flows.LogoutUserDeviceAsync(\n userKey,\n new LogoutDeviceRequest\n {\n ChainId = chainId\n });\n\nif (!result.IsSuccess)\n{\n // Handle the failure.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022logout-other-devices-for-a-user\u0022\u003ELogout Other Devices for a User\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.Flows.LogoutUserOtherDevicesAsync(userKey, request);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EHere, \u003Ccode\u003Erequest\u003C/code\u003E is a \u003Ccode\u003ELogoutOtherDevicesRequest\u003C/code\u003E identifying the chain to retain.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022logout-all-devices-for-a-user\u0022\u003ELogout All Devices for a User\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.Flows.LogoutAllUserDevicesAsync(userKey);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAdministrative methods do not grant administrative privileges by themselves.\u003C/p\u003E\n\u003Cp\u003EThe server must authorize the actor and validate the target user and requested revocation scope.\u003C/p\u003E\n\u003Cp\u003EThese operations are particularly relevant to administration panels and account security workflows.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022building-login-forms-with-uauthloginform\u0022\u003E17. Building Login Forms with UAuthLoginForm\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth provides \u003Ccode\u003EUAuthLoginForm\u003C/code\u003E, a reusable Blazor component that handles authentication form submission without requiring developers to implement the underlying flow orchestration themselves.\u003C/p\u003E\n\u003Cp\u003EThe component supports password-based login, PKCE completion through UAuthHub, configurable submission modes, structured validation feedback, and customizable UI content.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EYou design the login interface. UltimateAuth handles the authentication submission workflow.\u003C/strong\u003E\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022basic-usage\u0022\u003EBasic Usage\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthLoginForm\u003C/code\u003E provides a complete, ready-to-use login form out of the box. No additional markup or credential bindings are required.\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\u0026lt;UAuthLoginForm /\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThat\u0027s it. UltimateAuth automatically renders the identifier field, password field, and submit button, and handles the authentication submission workflow.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003ECustomizing the Form\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EWhen you need a custom layout or styling, simply provide your own form content:\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.Blazor\n\n\u0026lt;UAuthLoginForm\u0026gt;\n \u0026lt;input name=\u0026quot;Identifier\u0026quot; placeholder=\u0026quot;Email\u0026quot; autocomplete=\u0026quot;username\u0026quot; /\u0026gt;\n \u0026lt;input name=\u0026quot;Secret\u0026quot; type=\u0026quot;password\u0026quot; placeholder=\u0026quot;Password\u0026quot; autocomplete=\u0026quot;current-password\u0026quot; /\u0026gt;\n \u0026lt;button type=\u0026quot;submit\u0026quot;\u0026gt;Sign in\u0026lt;/button\u0026gt;\n\u0026lt;/UAuthLoginForm\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUltimateAuth handles the authentication workflow in both cases. Your custom fields only need the correct HTML \u003Ccode\u003Ename\u003C/code\u003E attributes; there is no need to bind credentials to Blazor component state.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022native-credential-submission\u0022\u003ENative Credential Submission\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthLoginForm\u003C/code\u003E reads credentials directly from native browser form fields. It does not accept \u003Ccode\u003EIdentifier\u003C/code\u003E or \u003Ccode\u003ESecret\u003C/code\u003E component parameters.\u003C/p\u003E\n\u003Cp\u003EWhen providing custom \u003Ccode\u003EChildContent\u003C/code\u003E, the form must contain enabled input elements with the exact names \u003Ccode\u003EIdentifier\u003C/code\u003E and \u003Ccode\u003ESecret\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003ECredential values should not be bound to Blazor component state solely for submission. This is particularly important in Blazor Server, where interactive component bindings can transmit credential values over the SignalR connection.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EImportant: Do Not Disable Credential Fields During Submission\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EHTML inputs marked as \u003Ccode\u003Edisabled\u003C/code\u003E are excluded from \u003Ccode\u003EFormData\u003C/code\u003E and native form submissions.\u003C/p\u003E\n\u003Cp\u003EDo not set \u003Ccode\u003EDisabled=\u0026quot;@isSubmitting\u0026quot;\u003C/code\u003E on the username or password fields. A login preview may succeed, but the subsequent native commit can fail because the credential fields are missing from the submitted form.\u003C/p\u003E\n\u003Cp\u003EInstead, disable the submit button while leaving the credential inputs enabled.\u003C/p\u003E\n\u003Cp\u003EIf you need to prevent editing during submission, consider \u003Ccode\u003Ereadonly\u003C/code\u003E where supported, while accounting for component behavior, browser autofill, and accessibility.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003ECustom Form Requirements\u003C/strong\u003E\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EUse \u003Ccode\u003Ename=\u0026quot;Identifier\u0026quot;\u003C/code\u003E and \u003Ccode\u003Ename=\u0026quot;Secret\u0026quot;\u003C/code\u003E exactly.\u003C/li\u003E\n\u003Cli\u003EKeep both credential inputs enabled throughout submission.\u003C/li\u003E\n\u003Cli\u003EAvoid duplicate inputs with these names.\u003C/li\u003E\n\u003Cli\u003EUse \u003Ccode\u003Etype=\u0026quot;password\u0026quot;\u003C/code\u003E and \u003Ccode\u003Eautocomplete=\u0026quot;current-password\u0026quot;\u003C/code\u003E for password fields.\u003C/li\u003E\n\u003Cli\u003EKeep custom submit controls inside \u003Ccode\u003EUAuthLoginForm\u003C/code\u003E.\u003C/li\u003E\n\u003Cli\u003EUse \u003Ccode\u003EOnSubmittingChanged\u003C/code\u003E to manage loading indicators and submit-button state.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthLoginForm\u003C/code\u003E handles the try and commit orchestration. The native commit includes the browser form fields, device information, and antiforgery data required by the configured authentication flow.\u003C/p\u003E\n\u003Cp\u003EThe component automatically uses \u003Ccode\u003ETryAndCommit\u003C/code\u003E as its default submission mode.\u003C/p\u003E\n\u003Cp\u003EWhen submitted, it:\u003C/p\u003E\n\u003Col\u003E\n\u003Cli\u003EValidates that the identifier and secret are present.\u003C/li\u003E\n\u003Cli\u003ECreates the appropriate authentication request.\u003C/li\u003E\n\u003Cli\u003ECalls the UltimateAuth client flow.\u003C/li\u003E\n\u003Cli\u003EPerforms the configured try/commit behavior.\u003C/li\u003E\n\u003Cli\u003EExposes structured results through \u003Ccode\u003EOnTryResult\u003C/code\u003E.\u003C/li\u003E\n\u003C/ol\u003E\n\u003Cp\u003EThe example assumes that UltimateAuth\u0027s Blazor services have already been registered and that the hosting environment permits the selected login flow.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022customizing-the-login-ui\u0022\u003ECustomizing the Login UI\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthLoginForm\u003C/code\u003E uses \u003Ccode\u003EChildContent\u003C/code\u003E, allowing developers to use standard HTML elements or their preferred Blazor component library.\u003C/p\u003E\n\u003Cp\u003EFor example, with MudBlazor:\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.Blazor\n@using CodeBeam.UltimateAuth.Client\n\n\u0026lt;UAuthLoginForm SubmitMode=\u0026quot;UAuthSubmitMode.TryAndCommit\u0026quot; OnTryResult=\u0026quot;HandleLoginResult\u0026quot; OnSubmittingChanged=\u0026quot;HandleSubmittingChanged\u0026quot;\u0026gt;\n \u0026lt;MudStack Spacing=\u0026quot;3\u0026quot;\u0026gt;\n \u0026lt;MudText Typo=\u0026quot;Typo.h5\u0026quot;\u0026gt;Sign in\u0026lt;/MudText\u0026gt;\n \u0026lt;MudTextField name=\u0026quot;Identifier\u0026quot; Label=\u0026quot;Email\u0026quot; Variant=\u0026quot;Variant.Outlined\u0026quot; /\u0026gt;\n \u0026lt;MudTextField name=\u0026quot;Secret\u0026quot; Label=\u0026quot;Password\u0026quot; InputType=\u0026quot;InputType.Password\u0026quot; Variant=\u0026quot;Variant.Outlined\u0026quot; /\u0026gt;\n\n \u0026lt;MudButton ButtonType=\u0026quot;ButtonType.Submit\u0026quot; Disabled=\u0026quot;@_isSubmitting\u0026quot; Variant=\u0026quot;Variant.Filled\u0026quot; Color=\u0026quot;Color.Primary\u0026quot; FullWidth\u0026gt;Sign in\u0026lt;/MudButton\u0026gt;\n\n @if (!string.IsNullOrWhiteSpace(error))\n {\n \u0026lt;MudAlert Severity=\u0026quot;Severity.Error\u0026quot;\u0026gt;@error\u0026lt;/MudAlert\u0026gt;\n }\n \u0026lt;/MudStack\u0026gt;\n\u0026lt;/UAuthLoginForm\u0026gt;\n\n@code {\n private bool _isSubmitting;\n private string? error;\n\n private Task HandleSubmittingChanged(bool submitting)\n {\n _isSubmitting = submitting;\n return Task.CompletedTask;\n }\n\n private Task HandleLoginResult(IUAuthTryResult result)\n {\n error = result.IsSuccess ? null : \u0026quot;Unable to sign in.\u0026quot;;\n return Task.CompletedTask;\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe form controls remain under your control.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth does not require a particular visual design, CSS framework, or component library.\u003C/p\u003E\n\u003Cp\u003EBecause the component renders an HTML form internally, place the submit button inside its child content and avoid nesting another HTML form within it.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022submission-modes\u0022\u003ESubmission Modes\u003C/h3\u003E\n\u003Cp\u003EUse \u003Ccode\u003ESubmitMode\u003C/code\u003E to control how the authentication attempt is executed.\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\u0026lt;UAuthLoginForm SubmitMode=\u0026quot;UAuthSubmitMode.TryAndCommit\u0026quot;\u0026gt;\n ...\n\u0026lt;/UAuthLoginForm\u0026gt;\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\u003EMode\u003C/th\u003E\n\u003Cth\u003EBehavior\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETryOnly\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EValidates the authentication attempt without committing login\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETryAndCommit\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAttempts authentication and commits on success\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDirectCommit\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESubmits directly to the commit flow\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003ETryAndCommit\u003C/code\u003E is the default.\u003C/p\u003E\n\u003Cp\u003EFor interactive login forms that need validation feedback, it is generally the recommended choice.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EDirectCommit\u003C/code\u003E does not invoke \u003Ccode\u003EOnTryResult\u003C/code\u003E, because it does not perform the structured try operation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022handling-authentication-results\u0022\u003EHandling Authentication Results\u003C/h3\u003E\n\u003Cp\u003EThe component exposes an \u003Ccode\u003EOnTryResult\u003C/code\u003E callback:\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\u0026lt;UAuthLoginForm OnTryResult=\u0026quot;HandleResult\u0026quot;\u0026gt;\n ...\n\u0026lt;/UAuthLoginForm\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe callback receives \u003Ccode\u003EIUAuthTryResult\u003C/code\u003E, allowing the same component to report password login and PKCE completion results.\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\u003Eprivate Task HandleResult(IUAuthTryResult result)\n{\n if (!result.IsSuccess)\n {\n errorMessage = \u0026quot;Authentication failed.\u0026quot;;\n }\n\n return Task.CompletedTask;\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EFor flow-specific details, inspect the concrete result 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\u003Eprivate Task HandleResult(IUAuthTryResult result)\n{\n if (result is TryLoginResult loginResult)\n {\n if (loginResult.LockoutUntilUtc is not null)\n {\n errorMessage = \u0026quot;Your account is temporarily locked.\u0026quot;;\n }\n else if (loginResult.RequiresMfa)\n {\n // Continue the supported additional authentication workflow.\n }\n else if (!loginResult.IsSuccess)\n {\n errorMessage = \u0026quot;Unable to sign in.\u0026quot;;\n }\n }\n else if (result is TryPkceLoginResult pkceResult)\n {\n if (pkceResult.RetryWithNewPkce)\n {\n errorMessage = \u0026quot;Please restart the authentication flow.\u0026quot;;\n }\n else if (!pkceResult.IsSuccess)\n {\n errorMessage = \u0026quot;Unable to complete authentication.\u0026quot;;\n }\n }\n\n return Task.CompletedTask;\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe callback reports the structured try result. It does not replace authentication state management or server-side validation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022using-uauthloginform-with-pkce\u0022\u003EUsing UAuthLoginForm with PKCE\u003C/h3\u003E\n\u003Cp\u003EThe same component also supports PKCE login completion when hosted within a supported UAuthHub environment.\u003C/p\u003E\n\u003Cp\u003ESet \u003Ccode\u003ELoginType\u003C/code\u003E to \u003Ccode\u003EPkce\u003C/code\u003E:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E\u0026lt;UAuthLoginForm LoginType=\u0026quot;UAuthLoginType.Pkce\u0026quot; SubmitMode=\u0026quot;UAuthSubmitMode.TryAndCommit\u0026quot; OnTryResult=\u0026quot;HandleLoginResult\u0026quot;\u0026gt;\n \u0026lt;input name=\u0026quot;Identifier\u0026quot; placeholder=\u0026quot;Email\u0026quot; /\u0026gt;\n \u0026lt;input name=\u0026quot;Secret\u0026quot; type=\u0026quot;password\u0026quot; placeholder=\u0026quot;Password\u0026quot; /\u0026gt;\n \u0026lt;button type=\u0026quot;submit\u0026quot;\u0026gt;Continue\u0026lt;/button\u0026gt;\n\u0026lt;/UAuthLoginForm\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EIn PKCE mode, the component:\u003C/p\u003E\n\u003Col\u003E\n\u003Cli\u003EResolves the relevant UAuthHub session context.\u003C/li\u003E\n\u003Cli\u003ERetrieves the authorization code and code verifier through the configured Hub services.\u003C/li\u003E\n\u003Cli\u003EBuilds a \u003Ccode\u003EPkceCompleteRequest\u003C/code\u003E.\u003C/li\u003E\n\u003Cli\u003EInvokes the corresponding PKCE completion flow.\u003C/li\u003E\n\u003Cli\u003EReports the structured result when a try-enabled submission mode is used.\u003C/li\u003E\n\u003C/ol\u003E\n\u003Cp\u003EThe application does not need to manually construct the PKCE completion request for this form.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EImportant:\u003C/strong\u003E PKCE mode requires a supported server-side UAuthHub environment. \u003Ccode\u003EUAuthLoginForm\u003C/code\u003E checks the available Hub capabilities and throws when PKCE login is requested in an unsupported client profile.\u003C/p\u003E\n\u003Cp\u003EA Blazor WebAssembly application should initiate PKCE using \u003Ccode\u003EBeginPkceAsync\u003C/code\u003E; the Hub-hosted login form handles the completion stage.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022return-url-handling\u0022\u003EReturn URL Handling\u003C/h3\u003E\n\u003Cp\u003EYou can provide an explicit return URL:\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\u0026lt;UAuthLoginForm ReturnUrl=\u0026quot;/dashboard\u0026quot;\u0026gt;\n ...\n\u0026lt;/UAuthLoginForm\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe component resolves the effective return URL using the following precedence:\u003C/p\u003E\n\u003Col\u003E\n\u003Cli\u003EExplicit \u003Ccode\u003EReturnUrl\u003C/code\u003E parameter.\u003C/li\u003E\n\u003Cli\u003EUAuthHub flow return URL when using PKCE.\u003C/li\u003E\n\u003Cli\u003ECurrent navigation URL.\u003C/li\u003E\n\u003C/ol\u003E\n\u003Cp\u003EThis allows a shared login component to participate in different authentication journeys without hardcoding navigation logic into the form.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022custom-login-endpoints\u0022\u003ECustom Login Endpoints\u003C/h3\u003E\n\u003Cp\u003EFor advanced integrations, the \u003Ccode\u003EEndpoint\u003C/code\u003E parameter allows the form\u0027s HTML action endpoint to be overridden:\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\u0026lt;UAuthLoginForm Endpoint=\u0026quot;/custom-login\u0026quot; SubmitMode=\u0026quot;UAuthSubmitMode.DirectCommit\u0026quot;\u0026gt;\n ...\n\u0026lt;/UAuthLoginForm\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe endpoint is resolved relative to the configured UltimateAuth endpoint base path and multi-tenant settings.\u003C/p\u003E\n\u003Cp\u003ENote that \u003Ccode\u003EEndpoint\u003C/code\u003E affects the form\u0027s HTML action. Try-based submissions invoke the configured \u003Ccode\u003EUAuthClient.Flows\u003C/code\u003E endpoints, so changing \u003Ccode\u003EEndpoint\u003C/code\u003E alone does not redirect those SDK requests to a custom endpoint.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022submit-the-form-programmatically\u0022\u003ESubmit the Form Programmatically\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthLoginForm\u003C/code\u003E exposes \u003Ccode\u003ESubmitAsync\u003C/code\u003E, allowing submission through a component reference.\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\u0026lt;UAuthLoginForm @ref=\u0026quot;loginForm\u0026quot; AllowEnterKeyToSubmit=\u0026quot;false\u0026quot;\u0026gt;\n \u0026lt;input name=\u0026quot;Identifier\u0026quot; placeholder=\u0026quot;Email\u0026quot; /\u0026gt;\n \u0026lt;input name=\u0026quot;Secret\u0026quot; type=\u0026quot;password\u0026quot; /\u0026gt;\n\u0026lt;/UAuthLoginForm\u0026gt;\n\n\u0026lt;button type=\u0026quot;button\u0026quot; @onclick=\u0026quot;SubmitLoginAsync\u0026quot;\u0026gt;\n Sign in\n\u0026lt;/button\u0026gt;\n\n@code {\n private UAuthLoginForm? loginForm;\n\n private async Task SubmitLoginAsync()\n {\n if (loginForm is not null)\n {\n await loginForm.SubmitAsync();\n }\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003ESubmitAsync\u003C/code\u003E must be called after the component has rendered.\u003C/p\u003E\n\u003Cp\u003EThis is useful when submission is initiated from a custom button, dialog, or application-specific interaction.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022controlling-enter-key-submission\u0022\u003EControlling Enter-Key Submission\u003C/h3\u003E\n\u003Cp\u003EBy default, the component supports Enter-key submission.\u003C/p\u003E\n\u003Cp\u003EYou can disable its built-in hidden submit button:\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\u0026lt;UAuthLoginForm AllowEnterKeyToSubmit=\u0026quot;false\u0026quot;\u0026gt;\n ...\n\u0026lt;/UAuthLoginForm\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis option controls the component\u0027s built-in Enter-key submit mechanism. Other submit buttons inside the form may still trigger normal browser form submission behavior.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022reloading-pkce-context\u0022\u003EReloading PKCE Context\u003C/h3\u003E\n\u003Cp\u003EThe component exposes \u003Ccode\u003EReloadAsync\u003C/code\u003E to reload its Hub credentials and flow state.\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\u0026lt;UAuthLoginForm @ref=\u0026quot;loginForm\u0026quot; LoginType=\u0026quot;UAuthLoginType.Pkce\u0026quot;\u0026gt;\n ...\n\u0026lt;/UAuthLoginForm\u0026gt;\n\n@code {\n private UAuthLoginForm? loginForm;\n\n private async Task ReloadLoginContextAsync()\n {\n if (loginForm is not null)\n {\n await loginForm.ReloadAsync();\n }\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis is an advanced capability intended for scenarios where the associated Hub state changes and the component must refresh its internal context.\u003C/p\u003E\n\u003Cp\u003EIt does not initiate a new PKCE authorization flow by itself.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022component-parameters\u0022\u003EComponent Parameters\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EParameter\u003C/th\u003E\n\u003Cth\u003EType\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\u003ELoginType\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthLoginType\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EPassword\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPassword or PKCE login\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESubmitMode\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthSubmitMode\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003ETryAndCommit\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESubmission strategy\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EReturnUrl\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Estring?\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERedirect destination\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEndpoint\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Estring?\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECustom HTML form endpoint\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EHubSessionId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EHubSessionId?\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EExplicit Hub session context\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAllowEnterKeyToSubmit\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Ebool\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EBuilt-in Enter-key submit support\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EOnTryResult\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EEventCallback\u0026lt;IUAuthTryResult\u0026gt;\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u2014\u003C/td\u003E\n\u003Ctd\u003EStructured try result callback\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EOnSubmittingChanged\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EEventCallback\u0026lt;bool\u0026gt;\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u2014\u003C/td\u003E\n\u003Ctd\u003ESubmission state change callback\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EChildContent\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003ERenderFragment?\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u2014\u003C/td\u003E\n\u003Ctd\u003ECustom form content\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=\u0022when-to-use-uauthloginform\u0022\u003EWhen to Use UAuthLoginForm\u003C/h3\u003E\n\u003Cp\u003EUse \u003Ccode\u003EUAuthLoginForm\u003C/code\u003E when building a Blazor login interface that should integrate directly with UltimateAuth\u0027s supported authentication flows.\u003C/p\u003E\n\u003Cp\u003EIt is particularly useful when:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EYou want to use your own component library without implementing the authentication submission logic.\u003C/li\u003E\n\u003Cli\u003EYou need structured login validation feedback.\u003C/li\u003E\n\u003Cli\u003EYou want to switch between try-only and commit behavior.\u003C/li\u003E\n\u003Cli\u003EYou are building a UAuthHub login page that completes PKCE.\u003C/li\u003E\n\u003Cli\u003EYou want to share login UI across supported Blazor hosting environments.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EFor application-specific workflows that cannot be expressed through the component, use \u003Ccode\u003EUAuthClient.Flows\u003C/code\u003E directly.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EThe component handles the login workflow, while your application retains control over presentation, user experience, and surrounding application logic.\u003C/strong\u003E\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022common-integration-mistakes\u0022\u003E18. Common Integration Mistakes\u003C/h2\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022calling-direct-login-from-a-public-client\u0022\u003ECalling Direct Login from a Public Client\u003C/h3\u003E\n\u003Cp\u003E\u003Cstrong\u003EProblem:\u003C/strong\u003E \u003Ccode\u003ELoginAsync\u003C/code\u003E throws because direct credential posting is disabled.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003ESolution:\u003C/strong\u003E Use \u003Ccode\u003EBeginPkceAsync\u003C/code\u003E and the supported PKCE completion flow. Do not enable \u003Ccode\u003EAllowCredentialPost\u003C/code\u003E to bypass the public-client restriction.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022treating-tryonly-as-authentication\u0022\u003ETreating TryOnly as Authentication\u003C/h3\u003E\n\u003Cp\u003E\u003Cstrong\u003EProblem:\u003C/strong\u003E The application receives \u003Ccode\u003EIsSuccess = true\u003C/code\u003E from \u003Ccode\u003ETryOnly\u003C/code\u003E and assumes the user is logged in.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003ESolution:\u003C/strong\u003E A successful try result does not commit authentication. Use \u003Ccode\u003ETryAndCommit\u003C/code\u003E or the appropriate commit flow.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022assuming-directcommit-skips-server-validation\u0022\u003EAssuming DirectCommit Skips Server Validation\u003C/h3\u003E\n\u003Cp\u003E\u003Cstrong\u003EProblem:\u003C/strong\u003E DirectCommit is interpreted as a faster but less secure authentication mode.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003ESolution:\u003C/strong\u003E DirectCommit skips the separate client try stage. The server still validates the actual authentication request.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022assuming-every-refresh-rotates-tokens\u0022\u003EAssuming Every Refresh Rotates Tokens\u003C/h3\u003E\n\u003Cp\u003E\u003Cstrong\u003EProblem:\u003C/strong\u003E The application requires \u003Ccode\u003ERefreshOutcome.Rotated\u003C/code\u003E for refresh success.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003ESolution:\u003C/strong\u003E Check \u003Ccode\u003ERefreshResult.IsSuccess\u003C/code\u003E. Rotation, touch, and no-op behavior depend on the authentication mode and refresh strategy.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022using-a-session-id-for-device-logout\u0022\u003EUsing a Session ID for Device Logout\u003C/h3\u003E\n\u003Cp\u003E\u003Cstrong\u003EProblem:\u003C/strong\u003E A session identifier is passed to a device logout operation.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003ESolution:\u003C/strong\u003E \u003Ccode\u003ELogoutMyDeviceAsync\u003C/code\u003E requires \u003Ccode\u003ELogoutDeviceRequest\u003C/code\u003E with a \u003Ccode\u003ESessionChainId\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022assuming-logout-immediately-invalidates-all-jwts\u0022\u003EAssuming Logout Immediately Invalidates All JWTs\u003C/h3\u003E\n\u003Cp\u003E\u003Cstrong\u003EProblem:\u003C/strong\u003E An application expects every issued JWT access token to become unusable immediately after server-side logout.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003ESolution:\u003C/strong\u003E Understand the enforcement behavior of SemiHybrid and PureJwt. Local JWT validation may continue accepting previously issued tokens until expiration.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022using-validateasync-on-every-render\u0022\u003EUsing ValidateAsync on Every Render\u003C/h3\u003E\n\u003Cp\u003E\u003Cstrong\u003EProblem:\u003C/strong\u003E Components repeatedly make validation requests just to display authenticated UI.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003ESolution:\u003C/strong\u003E Use the appropriate authentication state integration for UI rendering, and reserve explicit validation for operations that require it.\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-sdk/session-management\u0022\u003ESession Management\u003C/a\u003E \u2014 Inspect and manage session and device information.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/security/token-security\u0022\u003EToken Security\u003C/a\u003E \u2014 Understand token validity, refresh behavior, and revocation.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/security/refresh-rotation\u0022\u003ERefresh Rotation \u0026amp; Replay Protection\u003C/a\u003E \u2014 Explore refresh concurrency and reuse security.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/fundamentals/auth-modes\u0022\u003EAuthentication Modes\u003C/a\u003E \u2014 Compare authentication behavior across modes.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/sessions-devices/device-management\u0022\u003EDevice Management\u003C/a\u003E \u2014 Understand device-associated session chains.\u003C/li\u003E\n\u003C/ul\u003E\n",
"Headings": [
{
"Id": "accessing-the-authentication-client",
@@ -228,6 +228,11 @@
"Text": "Basic Usage",
"Level": 1
},
+ {
+ "Id": "native-credential-submission",
+ "Text": "Native Credential Submission",
+ "Level": 1
+ },
{
"Id": "customizing-the-login-ui",
"Text": "Customizing the Login UI",
@@ -329,6 +334,6 @@
"Level": 0
}
],
- "SourceHash": "2A94010322C7A85EC49E84E84F1E73737F414AE2A5FCCED1BAED1E3C3FD11729",
+ "SourceHash": "6F1CE929DE0E49957E2BF9BA2B7CD4397971D3A91CFB941977BE600C09786639",
"BuilderVersion": "1"
}
\ No newline at end of file
diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/getting-started/quickstart.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/getting-started/quickstart.json
index 16eb652d..04230bc9 100644
--- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/getting-started/quickstart.json
+++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/getting-started/quickstart.json
@@ -1,7 +1,7 @@
{
"Slug": "getting-started/quickstart",
"Title": "QuickStart",
- "Html": "\n\u003Cp\u003E\u003Cstrong\u003EYour first UltimateAuth application, in just a few steps.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EThis guide walks you through creating a Blazor Server application, adding UltimateAuth with in-memory storage, and signing in with a sample account.\u003C/p\u003E\n\u003Cp\u003EYou\u0027ll start with a minimal setup. No database configuration, migrations, or custom authentication infrastructure required.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022prerequisites\u0022\u003EPrerequisites\u003C/h2\u003E\n\u003Cp\u003EYou\u0027ll need:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E.NET SDK (8 and higher, latest recommended)\u003C/li\u003E\n\u003Cli\u003EA code editor such as Visual Studio, VS Code, or Rider\u003C/li\u003E\n\u003Cli\u003EBasic familiarity with Blazor\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThis guide uses \u003Cstrong\u003EBlazor Web App with Interactive Server rendering\u003C/strong\u003E and UltimateAuth\u0027s in-memory persistence bundle.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022create-a-blazor-application\u0022\u003E1. Create a Blazor Application\u003C/h2\u003E\n\u003Cp\u003EOpen a terminal and create a new Blazor Interactive Server project:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EBash\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-bash\u0022\u003Edotnet new blazor -n UltimateAuthDemo -int Server -ai\ncd UltimateAuthDemo\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis creates a Blazor Web App with Interactive Server components.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022install-ultimateauth\u0022\u003E2. Install UltimateAuth\u003C/h2\u003E\n\u003Cp\u003EInstall the Blazor client adapter and the in-memory bundle:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EBash\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-bash\u0022\u003Edotnet add package CodeBeam.UltimateAuth.Client.Blazor\ndotnet add package CodeBeam.UltimateAuth.InMemory.Bundle\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EFor this tutorial, also install the sample seed package:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EBash\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-bash\u0022\u003Edotnet add package CodeBeam.UltimateAuth.Sample.Seed\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe packages serve different purposes:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EPackage\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\u003EClient.Blazor\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EBlazor components, authentication state, and client integration\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EInMemory.Bundle\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIn-memory implementations for authentication domains\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESample.Seed\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EDevelopment accounts and sample authorization data\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe in-memory bundle includes the relevant persistence implementations, so you do not need to register each store individually.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022configure-ultimateauth-services\u0022\u003E3. Configure UltimateAuth Services\u003C/h2\u003E\n\u003Cp\u003EOpen \u003Ccode\u003EProgram.cs\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EAdd the following namespaces:\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\u003Eusing CodeBeam.UltimateAuth.Client.Blazor;\nusing CodeBeam.UltimateAuth.Client.Blazor.Extensions;\nusing CodeBeam.UltimateAuth.InMemory;\nusing CodeBeam.UltimateAuth.Sample.Seed.Extensions;\nusing CodeBeam.UltimateAuth.Server.Extensions;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAfter creating the builder, register UltimateAuth:\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 .AddUltimateAuthInMemory();\n\nbuilder.Services.AddUltimateAuthClientBlazor();\n\nbuilder.Services.AddUltimateAuthSampleSeed();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThat\u0027s the core service configuration.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EAddUltimateAuthServer()\u003C/code\u003E registers the authentication server.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EAddUltimateAuthInMemory()\u003C/code\u003E provides in-memory stores.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EAddUltimateAuthClientBlazor()\u003C/code\u003E registers the Blazor client integration.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EAddUltimateAuthSampleSeed()\u003C/code\u003E prepares the development seed services.\u003C/p\u003E\n\u003Cp\u003EYou don\u0027t need to call \u003Ccode\u003EAddUltimateAuth()\u003C/code\u003E or \u003Ccode\u003EAddUltimateAuthClient()\u003C/code\u003E separately.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022configure-the-http-pipeline\u0022\u003E4. Configure the HTTP Pipeline\u003C/h2\u003E\n\u003Cp\u003EIn \u003Ccode\u003EProgram.cs\u003C/code\u003E, configure the application pipeline:\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 app = builder.Build();\n\nif (!app.Environment.IsDevelopment())\n{\n app.UseExceptionHandler(\u0026quot;/Error\u0026quot;, createScopeForErrors: true);\n app.UseHsts();\n}\n\napp.UseHttpsRedirection();\napp.UseStaticFiles();\n\napp.UseUltimateAuthWithAspNetCore();\napp.UseAntiforgery();\napp.MapUltimateAuthEndpoints();\n\napp.MapRazorComponents\u0026lt;App\u0026gt;()\n .AddInteractiveServerRenderMode()\n .AddUltimateAuthRoutes(UAuthAssemblies.BlazorClient());\n\nif (app.Environment.IsDevelopment())\n{\n await app.SeedUltimateAuthAsync();\n}\n\napp.Run();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EKeep the \u003Ccode\u003EAddRazorComponents().AddInteractiveServerComponents()\u003C/code\u003E registration generated by the Blazor project template.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EUseUltimateAuthWithAspNetCore()\u003C/code\u003E integrates UltimateAuth\u0027s middleware with ASP.NET Core authentication and authorization.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EMapUltimateAuthEndpoints()\u003C/code\u003E registers the authentication endpoints.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EAddUltimateAuthRoutes()\u003C/code\u003E includes UltimateAuth\u0027s Blazor client route assemblies in endpoint discovery.\u003C/p\u003E\n\u003Cp\u003EThe development-only seed call creates the sample users before the application begins accepting requests.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022add-the-ultimateauth-script\u0022\u003E5. Add the UltimateAuth Script\u003C/h2\u003E\n\u003Cp\u003EOpen \u003Ccode\u003EComponents/App.razor\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EKeep the existing Blazor script and add UltimateAuth\u0027s JavaScript file:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EHTML\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-html\u0022\u003E\u0026lt;script src=\u0026quot;_content/CodeBeam.UltimateAuth.Client.Blazor/uauth.min.js\u0026quot;\u0026gt;\u0026lt;/script\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EPlace these near the end of the document body.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth\u0027s Blazor integration uses this script for browser-side functionality.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022add-usings\u0022\u003E6. Add Usings\u003C/h2\u003E\n\u003Cp\u003EAdd this in \u003Ccode\u003E_Imports.razor\u003C/code\u003E\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@using CodeBeam.UltimateAuth.Client\n@using CodeBeam.UltimateAuth.Client.Blazor\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022initialize-the-ultimateauth-application\u0022\u003E7. Initialize the UltimateAuth Application\u003C/h2\u003E\n\u003Cp\u003EOpen \u003Ccode\u003EComponents/Routes.razor\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EReplace its contents with:\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\u0026lt;UAuthApp UseBuiltInRouter=\u0026quot;true\u0026quot; AppAssembly=\u0026quot;typeof(Program).Assembly\u0026quot; DefaultLayout=\u0026quot;typeof(Layout.MainLayout)\u0026quot; /\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthApp\u003C/code\u003E is the root integration component for UltimateAuth in Blazor.\u003C/p\u003E\n\u003Cp\u003EIt initializes the client runtime, provides authentication state to child components, and coordinates the authenticated session lifecycle.\u003C/p\u003E\n\u003Cp\u003EWith \u003Ccode\u003EUseBuiltInRouter=\u0026quot;true\u0026quot;\u003C/code\u003E, it also provides the application\u0027s router.\u003C/p\u003E\n\u003Cp\u003EYou don\u0027t need to create a separate \u003Ccode\u003ERouter\u003C/code\u003E or \u003Ccode\u003ECascadingAuthenticationState\u003C/code\u003E component for this setup.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022create-your-login-page\u0022\u003E8. Create Your Login Page\u003C/h2\u003E\n\u003Cp\u003EChange \u003Ccode\u003EComponents/Pages/Home.razor\u003C/code\u003E:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/\u0026quot;\n@page \u0026quot;/login\u0026quot;\n@attribute [UAuthLoginPage]\n@inherits UAuthPageBase\n\n\u0026lt;div class=\u0026quot;container mt-5\u0026quot; style=\u0026quot;max-width: 400px;\u0026quot;\u0026gt;\n \u0026lt;h3\u0026gt;Sign in\u0026lt;/h3\u0026gt;\n \u0026lt;UAuthLoginForm Identifier=\u0026quot;@_identifier\u0026quot; Secret=\u0026quot;@_password\u0026quot; ReturnUrl=\u0026quot;/dashboard\u0026quot;\u0026gt;\n \u0026lt;div class=\u0026quot;d-block mb-3\u0026quot;\u0026gt;\n \u0026lt;label\u0026gt;Username\u0026lt;/label\u0026gt;\n \u0026lt;input @bind=\u0026quot;_identifier\u0026quot; class=\u0026quot;form-control w-100\u0026quot; autocomplete=\u0026quot;username\u0026quot; /\u0026gt;\n \u0026lt;/div\u0026gt;\n\n \u0026lt;div class=\u0026quot;d-block mb-3\u0026quot;\u0026gt;\n \u0026lt;label\u0026gt;Password\u0026lt;/label\u0026gt;\n \u0026lt;input type=\u0026quot;password\u0026quot; @bind=\u0026quot;_password\u0026quot; class=\u0026quot;form-control w-100\u0026quot; autocomplete=\u0026quot;current-password\u0026quot; /\u0026gt;\n \u0026lt;/div\u0026gt;\n\n \u0026lt;button class=\u0026quot;btn btn-primary w-100\u0026quot; type=\u0026quot;submit\u0026quot;\u0026gt;Sign in\u0026lt;/button\u0026gt;\n \u0026lt;/UAuthLoginForm\u0026gt;\n\u0026lt;/div\u0026gt;\n\n@code {\n private string _identifier = \u0026quot;\u0026quot;;\n private string _password = \u0026quot;\u0026quot;;\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthLoginForm\u003C/code\u003E handles the login submission through UltimateAuth\u0027s client flow.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003E[UAuthLoginPage]\u003C/code\u003E identifies this component as the application\u0027s login page.\u003C/p\u003E\n\u003Cp\u003EOnly designate one login page for this purpose.\u003C/p\u003E\n\u003Cp\u003EThe form above is intentionally minimal. You can replace the inputs with your preferred UI components later.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022add-a-protected-page\u0022\u003E9. Add a Protected Page\u003C/h2\u003E\n\u003Cp\u003ECreate \u003Ccode\u003EComponents/Pages/Dashboard.razor\u003C/code\u003E:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/dashboard\u0026quot;\n@attribute [UAuthAuthorize]\n@inherits UAuthPageBase\n@inject IUAuthClient UAuthClient\n\n\u0026lt;h3\u0026gt;Dashboard\u0026lt;/h3\u0026gt;\n\n\u0026lt;p\u0026gt;You have reached an authenticated page.\u0026lt;/p\u0026gt;\n\u0026lt;UAuthStateView Roles=\u0026quot;Admin\u0026quot;\u0026gt;You have admin privileges.\u0026lt;/UAuthStateView\u0026gt;\n\u0026lt;p\u0026gt;User: @AuthState?.Identity?.DisplayName\u0026lt;/p\u0026gt;\n\n\u0026lt;button @onclick=\u0026quot;Logout\u0026quot; class=\u0026quot;btn btn-primary\u0026quot;\u0026gt;Logout\u0026lt;/button\u0026gt;\n\n@code {\n private async Task Logout()\n {\n await UAuthClient.Flows.LogoutAsync();\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E declares an UltimateAuth authorization requirement for the page.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EImportant:\u003C/strong\u003E Client-side route protection is not a substitute for server-side authorization. Protected APIs and resources must enforce authorization on the server.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022run-the-application\u0022\u003E10. Run the Application\u003C/h2\u003E\n\u003Cp\u003EStart 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\u003EBash\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-bash\u0022\u003Edotnet run\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EOpen the HTTPS address displayed in your terminal, then navigate to:\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/login\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe sample seed package creates two development accounts:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EUsername\u003C/th\u003E\n\u003Cth\u003EPassword\u003C/th\u003E\n\u003Cth\u003ERole\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003Eadmin\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Eadmin\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAdministrator\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003Euser\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Euser\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EStandard user\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003ESign in with:\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\u003EUsername: admin\nPassword: admin\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAfter successful authentication, navigate to:\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/dashboard\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EYou now have a Blazor application using UltimateAuth authentication.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022your-first-ultimateauth-application-is-running\u0022\u003E\uD83C\uDF89 Your First UltimateAuth Application Is Running\u003C/h2\u003E\n\u003Cp\u003EWith this setup, you have:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EAn authentication server integrated into ASP.NET Core.\u003C/li\u003E\n\u003Cli\u003EIn-memory user, credential, session, token, and authorization storage.\u003C/li\u003E\n\u003Cli\u003EA Blazor authentication client.\u003C/li\u003E\n\u003Cli\u003EA working login form.\u003C/li\u003E\n\u003Cli\u003EAuthentication state initialization.\u003C/li\u003E\n\u003Cli\u003ESession lifecycle coordination.\u003C/li\u003E\n\u003Cli\u003ESample users and authorization data.\u003C/li\u003E\n\u003Cli\u003EA page declaring an authentication requirement.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EYou did not need to create your own password hashing, token issuance, session validation, or authentication middleware implementations.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022what-happened-when-you-logged-in\u0022\u003EWhat Happened When You Logged In?\u003C/h2\u003E\n\u003Cp\u003EFor this Blazor Server scenario, UltimateAuth uses the applicable session-oriented authentication model.\u003C/p\u003E\n\u003Cp\u003EThe login flow validates the credentials and establishes authentication state on the server.\u003C/p\u003E\n\u003Cp\u003EWhere a new session is created, it participates in UltimateAuth\u0027s root, chain, and session lifecycle.\u003C/p\u003E\n\u003Cp\u003EThe Blazor client then coordinates authentication state for the application.\u003C/p\u003E\n\u003Cp\u003EYou can explore these operations in detail later through the Authentication Flows and Session Management documentation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022important-in-memory-storage\u0022\u003EImportant: In-Memory Storage\u003C/h2\u003E\n\u003Cp\u003EThis QuickStart intentionally uses in-memory persistence.\u003C/p\u003E\n\u003Cp\u003EIt is ideal for:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003ELearning UltimateAuth.\u003C/li\u003E\n\u003Cli\u003ELocal development.\u003C/li\u003E\n\u003Cli\u003ETesting authentication flows.\u003C/li\u003E\n\u003Cli\u003EBuilding prototypes.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003E\u003Cstrong\u003EIn-memory authentication data is not durable.\u003C/strong\u003E Users, credentials, sessions, and other stored state may be lost when the application restarts.\u003C/p\u003E\n\u003Cp\u003EThe sample credentials are also for development only. Do not deploy the seeded accounts or their example passwords in a production application.\u003C/p\u003E\n\u003Cp\u003EFor production use, configure durable persistence, appropriate secrets and signing keys, HTTPS, and the security settings required by your deployment.\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\u003Cstrong\u003E\u003Ca href=\u0022/docs/getting-started/ecosystem\u0022\u003EEcosystem\u003C/a\u003E\u003C/strong\u003E \u2014 Explore packages, integrations, and supported environments.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003E\u003Ca href=\u0022/docs/configuration/server-options\u0022\u003EServer Options\u003C/a\u003E\u003C/strong\u003E \u2014 Configure sessions, tokens, login policies, and security behavior.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003E\u003Ca href=\u0022/docs/configuration/client-options\u0022\u003EClient Options\u003C/a\u003E\u003C/strong\u003E \u2014 Customize Blazor integration and authentication flows.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003E\u003Ca href=\u0022/docs/auth-flows/overview\u0022\u003EAuthentication Flows\u003C/a\u003E\u003C/strong\u003E \u2014 Understand login, refresh, logout, and revocation.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003E\u003Ca href=\u0022/docs/getting-started/real-world-setup#persistent-storage-with-entity-framework-core\u0022\u003EEntity Framework Core Setup\u003C/a\u003E\u003C/strong\u003E \u2014 Move from in-memory storage to persistent authentication data.\u003C/li\u003E\n\u003C/ul\u003E\n",
+ "Html": "\n\u003Cp\u003E\u003Cstrong\u003EYour first UltimateAuth application, in just a few steps.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EThis guide walks you through creating a Blazor Server application, adding UltimateAuth with in-memory storage, and signing in with a sample account.\u003C/p\u003E\n\u003Cp\u003EYou\u0027ll start with a minimal setup. No database configuration, migrations, or custom authentication infrastructure required.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022prerequisites\u0022\u003EPrerequisites\u003C/h2\u003E\n\u003Cp\u003EYou\u0027ll need:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E.NET SDK (8 and higher, latest recommended)\u003C/li\u003E\n\u003Cli\u003EA code editor such as Visual Studio, VS Code, or Rider\u003C/li\u003E\n\u003Cli\u003EBasic familiarity with Blazor\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThis guide uses \u003Cstrong\u003EBlazor Web App with Interactive Server rendering\u003C/strong\u003E and UltimateAuth\u0027s in-memory persistence bundle.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022create-a-blazor-application\u0022\u003E1. Create a Blazor Application\u003C/h2\u003E\n\u003Cp\u003EOpen a terminal and create a new Blazor Interactive Server project:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EBash\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-bash\u0022\u003Edotnet new blazor -n UltimateAuthDemo -int Server -ai\ncd UltimateAuthDemo\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis creates a Blazor Web App with Interactive Server components.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022install-ultimateauth\u0022\u003E2. Install UltimateAuth\u003C/h2\u003E\n\u003Cp\u003EInstall the Blazor client adapter and the in-memory bundle:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EBash\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-bash\u0022\u003Edotnet add package CodeBeam.UltimateAuth.Client.Blazor\ndotnet add package CodeBeam.UltimateAuth.InMemory.Bundle\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EFor this tutorial, also install the sample seed package:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EBash\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-bash\u0022\u003Edotnet add package CodeBeam.UltimateAuth.Sample.Seed\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe packages serve different purposes:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EPackage\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\u003EClient.Blazor\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EBlazor components, authentication state, and client integration\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EInMemory.Bundle\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIn-memory implementations for authentication domains\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESample.Seed\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EDevelopment accounts and sample authorization data\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe in-memory bundle includes the relevant persistence implementations, so you do not need to register each store individually.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022configure-ultimateauth-services\u0022\u003E3. Configure UltimateAuth Services\u003C/h2\u003E\n\u003Cp\u003EOpen \u003Ccode\u003EProgram.cs\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EAdd the following namespaces:\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\u003Eusing CodeBeam.UltimateAuth.Client.Blazor;\nusing CodeBeam.UltimateAuth.Client.Blazor.Extensions;\nusing CodeBeam.UltimateAuth.InMemory;\nusing CodeBeam.UltimateAuth.Sample.Seed.Extensions;\nusing CodeBeam.UltimateAuth.Server.Extensions;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAfter creating the builder, register UltimateAuth:\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 .AddUltimateAuthInMemory();\n\nbuilder.Services.AddUltimateAuthClientBlazor();\n\nbuilder.Services.AddUltimateAuthSampleSeed();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThat\u0027s the core service configuration.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EAddUltimateAuthServer()\u003C/code\u003E registers the authentication server.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EAddUltimateAuthInMemory()\u003C/code\u003E provides in-memory stores.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EAddUltimateAuthClientBlazor()\u003C/code\u003E registers the Blazor client integration.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EAddUltimateAuthSampleSeed()\u003C/code\u003E prepares the development seed services.\u003C/p\u003E\n\u003Cp\u003EYou don\u0027t need to call \u003Ccode\u003EAddUltimateAuth()\u003C/code\u003E or \u003Ccode\u003EAddUltimateAuthClient()\u003C/code\u003E separately.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022configure-the-http-pipeline\u0022\u003E4. Configure the HTTP Pipeline\u003C/h2\u003E\n\u003Cp\u003EIn \u003Ccode\u003EProgram.cs\u003C/code\u003E, configure the application pipeline:\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 app = builder.Build();\n\nif (!app.Environment.IsDevelopment())\n{\n app.UseExceptionHandler(\u0026quot;/Error\u0026quot;, createScopeForErrors: true);\n app.UseHsts();\n}\n\napp.UseHttpsRedirection();\napp.UseStaticFiles();\n\napp.UseUltimateAuthWithAspNetCore();\napp.UseAntiforgery();\napp.MapUltimateAuthEndpoints();\n\napp.MapRazorComponents\u0026lt;App\u0026gt;()\n .AddInteractiveServerRenderMode()\n .AddUltimateAuthRoutes(UAuthAssemblies.BlazorClient());\n\nif (app.Environment.IsDevelopment())\n{\n await app.SeedUltimateAuthAsync();\n}\n\napp.Run();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EKeep the \u003Ccode\u003EAddRazorComponents().AddInteractiveServerComponents()\u003C/code\u003E registration generated by the Blazor project template.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EUseUltimateAuthWithAspNetCore()\u003C/code\u003E integrates UltimateAuth\u0027s middleware with ASP.NET Core authentication and authorization.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EMapUltimateAuthEndpoints()\u003C/code\u003E registers the authentication endpoints.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EAddUltimateAuthRoutes()\u003C/code\u003E includes UltimateAuth\u0027s Blazor client route assemblies in endpoint discovery.\u003C/p\u003E\n\u003Cp\u003EThe development-only seed call creates the sample users before the application begins accepting requests.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022add-the-ultimateauth-script\u0022\u003E5. Add the UltimateAuth Script\u003C/h2\u003E\n\u003Cp\u003EOpen \u003Ccode\u003EComponents/App.razor\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EKeep the existing Blazor script and add UltimateAuth\u0027s JavaScript file:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EHTML\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-html\u0022\u003E\u0026lt;script src=\u0026quot;_content/CodeBeam.UltimateAuth.Client.Blazor/uauth.min.js\u0026quot;\u0026gt;\u0026lt;/script\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EPlace these near the end of the document body.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth\u0027s Blazor integration uses this script for browser-side functionality.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022add-usings\u0022\u003E6. Add Usings\u003C/h2\u003E\n\u003Cp\u003EAdd this in \u003Ccode\u003E_Imports.razor\u003C/code\u003E\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@using CodeBeam.UltimateAuth.Client\n@using CodeBeam.UltimateAuth.Client.Blazor\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022initialize-the-ultimateauth-application\u0022\u003E7. Initialize the UltimateAuth Application\u003C/h2\u003E\n\u003Cp\u003EOpen \u003Ccode\u003EComponents/Routes.razor\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EReplace its contents with:\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\u0026lt;UAuthApp UseBuiltInRouter=\u0026quot;true\u0026quot; AppAssembly=\u0026quot;typeof(Program).Assembly\u0026quot; DefaultLayout=\u0026quot;typeof(Layout.MainLayout)\u0026quot; /\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthApp\u003C/code\u003E is the root integration component for UltimateAuth in Blazor.\u003C/p\u003E\n\u003Cp\u003EIt initializes the client runtime, provides authentication state to child components, and coordinates the authenticated session lifecycle.\u003C/p\u003E\n\u003Cp\u003EWith \u003Ccode\u003EUseBuiltInRouter=\u0026quot;true\u0026quot;\u003C/code\u003E, it also provides the application\u0027s router.\u003C/p\u003E\n\u003Cp\u003EYou don\u0027t need to create a separate \u003Ccode\u003ERouter\u003C/code\u003E or \u003Ccode\u003ECascadingAuthenticationState\u003C/code\u003E component for this setup.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022create-your-login-page\u0022\u003E8. Create Your Login Page\u003C/h2\u003E\n\u003Cp\u003EChange \u003Ccode\u003EComponents/Pages/Home.razor\u003C/code\u003E:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/\u0026quot;\n@page \u0026quot;/login\u0026quot;\n@attribute [UAuthLoginPage]\n@inherits UAuthPageBase\n\n\u0026lt;div class=\u0026quot;container mt-5\u0026quot; style=\u0026quot;max-width: 400px;\u0026quot;\u0026gt;\n \u0026lt;h3\u0026gt;Sign in\u0026lt;/h3\u0026gt;\n \u0026lt;UAuthLoginForm ReturnUrl=\u0026quot;/dashboard\u0026quot;\u0026gt;\n \u0026lt;div class=\u0026quot;d-block mb-3\u0026quot;\u0026gt;\n \u0026lt;label\u0026gt;Username\u0026lt;/label\u0026gt;\n \u0026lt;input name=\u0026quot;Identifier\u0026quot; class=\u0026quot;form-control w-100\u0026quot; autocomplete=\u0026quot;username\u0026quot; /\u0026gt;\n \u0026lt;/div\u0026gt;\n\n \u0026lt;div class=\u0026quot;d-block mb-3\u0026quot;\u0026gt;\n \u0026lt;label\u0026gt;Password\u0026lt;/label\u0026gt;\n \u0026lt;input type=\u0026quot;password\u0026quot; name=\u0026quot;Secret\u0026quot; class=\u0026quot;form-control w-100\u0026quot; autocomplete=\u0026quot;current-password\u0026quot; /\u0026gt;\n \u0026lt;/div\u0026gt;\n\n \u0026lt;button class=\u0026quot;btn btn-primary w-100\u0026quot; type=\u0026quot;submit\u0026quot;\u0026gt;Sign in\u0026lt;/button\u0026gt;\n \u0026lt;/UAuthLoginForm\u0026gt;\n\u0026lt;/div\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthLoginForm\u003C/code\u003E handles the login submission through UltimateAuth\u0027s client flow.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003E[UAuthLoginPage]\u003C/code\u003E identifies this component as the application\u0027s login page.\u003C/p\u003E\n\u003Cp\u003EOnly designate one login page for this purpose.\u003C/p\u003E\n\u003Cp\u003EThe form above is intentionally minimal. You can replace the inputs with your preferred UI components later.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022add-a-protected-page\u0022\u003E9. Add a Protected Page\u003C/h2\u003E\n\u003Cp\u003ECreate \u003Ccode\u003EComponents/Pages/Dashboard.razor\u003C/code\u003E:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/dashboard\u0026quot;\n@attribute [UAuthAuthorize]\n@inherits UAuthPageBase\n@inject IUAuthClient UAuthClient\n\n\u0026lt;h3\u0026gt;Dashboard\u0026lt;/h3\u0026gt;\n\n\u0026lt;p\u0026gt;You have reached an authenticated page.\u0026lt;/p\u0026gt;\n\u0026lt;UAuthStateView Roles=\u0026quot;Admin\u0026quot;\u0026gt;You have admin privileges.\u0026lt;/UAuthStateView\u0026gt;\n\u0026lt;p\u0026gt;User: @AuthState?.Identity?.DisplayName\u0026lt;/p\u0026gt;\n\n\u0026lt;button @onclick=\u0026quot;Logout\u0026quot; class=\u0026quot;btn btn-primary\u0026quot;\u0026gt;Logout\u0026lt;/button\u0026gt;\n\n@code {\n private async Task Logout()\n {\n await UAuthClient.Flows.LogoutAsync();\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E declares an UltimateAuth authorization requirement for the page.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EImportant:\u003C/strong\u003E Client-side route protection is not a substitute for server-side authorization. Protected APIs and resources must enforce authorization on the server.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022run-the-application\u0022\u003E10. Run the Application\u003C/h2\u003E\n\u003Cp\u003EStart 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\u003EBash\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-bash\u0022\u003Edotnet run\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EOpen the HTTPS address displayed in your terminal, then navigate to:\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/login\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe sample seed package creates two development accounts:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EUsername\u003C/th\u003E\n\u003Cth\u003EPassword\u003C/th\u003E\n\u003Cth\u003ERole\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003Eadmin\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Eadmin\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAdministrator\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003Euser\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Euser\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EStandard user\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003ESign in with:\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\u003EUsername: admin\nPassword: admin\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAfter successful authentication, navigate to:\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/dashboard\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EYou now have a Blazor application using UltimateAuth authentication.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022your-first-ultimateauth-application-is-running\u0022\u003E\uD83C\uDF89 Your First UltimateAuth Application Is Running\u003C/h2\u003E\n\u003Cp\u003EWith this setup, you have:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EAn authentication server integrated into ASP.NET Core.\u003C/li\u003E\n\u003Cli\u003EIn-memory user, credential, session, token, and authorization storage.\u003C/li\u003E\n\u003Cli\u003EA Blazor authentication client.\u003C/li\u003E\n\u003Cli\u003EA working login form.\u003C/li\u003E\n\u003Cli\u003EAuthentication state initialization.\u003C/li\u003E\n\u003Cli\u003ESession lifecycle coordination.\u003C/li\u003E\n\u003Cli\u003ESample users and authorization data.\u003C/li\u003E\n\u003Cli\u003EA page declaring an authentication requirement.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EYou did not need to create your own password hashing, token issuance, session validation, or authentication middleware implementations.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022what-happened-when-you-logged-in\u0022\u003EWhat Happened When You Logged In?\u003C/h2\u003E\n\u003Cp\u003EFor this Blazor Server scenario, UltimateAuth uses the applicable session-oriented authentication model.\u003C/p\u003E\n\u003Cp\u003EThe login flow validates the credentials and establishes authentication state on the server.\u003C/p\u003E\n\u003Cp\u003EWhere a new session is created, it participates in UltimateAuth\u0027s root, chain, and session lifecycle.\u003C/p\u003E\n\u003Cp\u003EThe Blazor client then coordinates authentication state for the application.\u003C/p\u003E\n\u003Cp\u003EYou can explore these operations in detail later through the Authentication Flows and Session Management documentation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022important-in-memory-storage\u0022\u003EImportant: In-Memory Storage\u003C/h2\u003E\n\u003Cp\u003EThis QuickStart intentionally uses in-memory persistence.\u003C/p\u003E\n\u003Cp\u003EIt is ideal for:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003ELearning UltimateAuth.\u003C/li\u003E\n\u003Cli\u003ELocal development.\u003C/li\u003E\n\u003Cli\u003ETesting authentication flows.\u003C/li\u003E\n\u003Cli\u003EBuilding prototypes.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003E\u003Cstrong\u003EIn-memory authentication data is not durable.\u003C/strong\u003E Users, credentials, sessions, and other stored state may be lost when the application restarts.\u003C/p\u003E\n\u003Cp\u003EThe sample credentials are also for development only. Do not deploy the seeded accounts or their example passwords in a production application.\u003C/p\u003E\n\u003Cp\u003EFor production use, configure durable persistence, appropriate secrets and signing keys, HTTPS, and the security settings required by your deployment.\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\u003Cstrong\u003E\u003Ca href=\u0022/docs/getting-started/ecosystem\u0022\u003EEcosystem\u003C/a\u003E\u003C/strong\u003E \u2014 Explore packages, integrations, and supported environments.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003E\u003Ca href=\u0022/docs/configuration/server-options\u0022\u003EServer Options\u003C/a\u003E\u003C/strong\u003E \u2014 Configure sessions, tokens, login policies, and security behavior.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003E\u003Ca href=\u0022/docs/configuration/client-options\u0022\u003EClient Options\u003C/a\u003E\u003C/strong\u003E \u2014 Customize Blazor integration and authentication flows.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003E\u003Ca href=\u0022/docs/auth-flows/overview\u0022\u003EAuthentication Flows\u003C/a\u003E\u003C/strong\u003E \u2014 Understand login, refresh, logout, and revocation.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003E\u003Ca href=\u0022/docs/getting-started/real-world-setup#persistent-storage-with-entity-framework-core\u0022\u003EEntity Framework Core Setup\u003C/a\u003E\u003C/strong\u003E \u2014 Move from in-memory storage to persistent authentication data.\u003C/li\u003E\n\u003C/ul\u003E\n",
"Headings": [
{
"Id": "prerequisites",
@@ -79,6 +79,6 @@
"Level": 0
}
],
- "SourceHash": "CB240CCA978FE45AD65EA09C882CE905FF7424CB6531541B1DEDAE55948572FE",
+ "SourceHash": "D8DF1C0A171AAE73FC58756D8AB78D2B340FC49EADAC831B4B78BAE16B8FD607",
"BuilderVersion": "1"
}
\ No newline at end of file
diff --git a/samples/CodeBeam.UltimateAuth.Sample.Shared.Hub/Components/Page/SampleHubHome.razor b/samples/CodeBeam.UltimateAuth.Sample.Shared.Hub/Components/Page/SampleHubHome.razor
index da02651b..f2dcb777 100644
--- a/samples/CodeBeam.UltimateAuth.Sample.Shared.Hub/Components/Page/SampleHubHome.razor
+++ b/samples/CodeBeam.UltimateAuth.Sample.Shared.Hub/Components/Page/SampleHubHome.razor
@@ -38,7 +38,7 @@
-
+
@@ -46,8 +46,8 @@
Sign In
-
-
+
+
Login
@if (_isLocked)
{
diff --git a/samples/CodeBeam.UltimateAuth.Sample.Shared/Components/Page/SampleLogin.razor b/samples/CodeBeam.UltimateAuth.Sample.Shared/Components/Page/SampleLogin.razor
index 6e81cc6e..f1710db6 100644
--- a/samples/CodeBeam.UltimateAuth.Sample.Shared/Components/Page/SampleLogin.razor
+++ b/samples/CodeBeam.UltimateAuth.Sample.Shared/Components/Page/SampleLogin.razor
@@ -71,7 +71,7 @@
-
+
@@ -79,8 +79,8 @@
Sign In
-
-
+
+
Login
@if (_isLocked)
{
diff --git a/samples/CodeBeam.UltimateAuth.Sample.Shared/Components/Page/SampleLogin.razor.cs b/samples/CodeBeam.UltimateAuth.Sample.Shared/Components/Page/SampleLogin.razor.cs
index 334cd26e..391a7a54 100644
--- a/samples/CodeBeam.UltimateAuth.Sample.Shared/Components/Page/SampleLogin.razor.cs
+++ b/samples/CodeBeam.UltimateAuth.Sample.Shared/Components/Page/SampleLogin.razor.cs
@@ -9,8 +9,6 @@ namespace CodeBeam.UltimateAuth.Sample.Shared.Components.Page;
public partial class SampleLogin : UAuthPageBase
{
- private string? _username;
- private string? _password;
private UAuthClientProductInfo? _productInfo;
private MudTextField _usernameField = default!;
diff --git a/samples/CodeBeam.UltimateAuth.Sample.Shared/Components/Page/SamplePkceLogin.razor b/samples/CodeBeam.UltimateAuth.Sample.Shared/Components/Page/SamplePkceLogin.razor
index 031b5196..8126273f 100644
--- a/samples/CodeBeam.UltimateAuth.Sample.Shared/Components/Page/SamplePkceLogin.razor
+++ b/samples/CodeBeam.UltimateAuth.Sample.Shared/Components/Page/SamplePkceLogin.razor
@@ -84,10 +84,10 @@
Instead of use PKCE flow.
This section only demonstrates that direct login flow is working, but not suggested.
-
+
-
-
+
+
Login
@if (_isLocked)
{
diff --git a/samples/CodeBeam.UltimateAuth.Sample.Shared/Components/Page/SamplePkceLogin.razor.cs b/samples/CodeBeam.UltimateAuth.Sample.Shared/Components/Page/SamplePkceLogin.razor.cs
index 4e473d18..2aeff54d 100644
--- a/samples/CodeBeam.UltimateAuth.Sample.Shared/Components/Page/SamplePkceLogin.razor.cs
+++ b/samples/CodeBeam.UltimateAuth.Sample.Shared/Components/Page/SamplePkceLogin.razor.cs
@@ -9,8 +9,6 @@ namespace CodeBeam.UltimateAuth.Sample.Shared.Components.Page;
public partial class SamplePkceLogin : UAuthPageBase
{
- private string? _username;
- private string? _password;
private UAuthClientProductInfo? _productInfo;
private MudTextField _usernameField = default!;
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/PkceCompleteRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/PkceCompleteRequest.cs
index e004fb60..3d44a000 100644
--- a/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/PkceCompleteRequest.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/PkceCompleteRequest.cs
@@ -1,4 +1,5 @@
-using System.Text.Json.Serialization;
+using CodeBeam.UltimateAuth.Core.Defaults;
+using System.Text.Json.Serialization;
namespace CodeBeam.UltimateAuth.Core.Contracts;
@@ -14,7 +15,7 @@ public sealed record PkceCompleteRequest
public required string Secret { get; init; }
- [JsonPropertyName("return_url")]
+ [JsonPropertyName(UAuthConstants.Form.ReturnUrl)]
public string? ReturnUrl { get; init; }
[JsonPropertyName("hub_session_id")]
diff --git a/src/CodeBeam.UltimateAuth.Core/Defaults/UAuthConstants.cs b/src/CodeBeam.UltimateAuth.Core/Defaults/UAuthConstants.cs
index 761c7766..b18e2025 100644
--- a/src/CodeBeam.UltimateAuth.Core/Defaults/UAuthConstants.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Defaults/UAuthConstants.cs
@@ -29,7 +29,7 @@ public static class HttpItems
public static class Form
{
- public const string ReturnUrl = "return_url";
+ public const string ReturnUrl = "__uauth_return_url";
public const string Device = "__uauth_device";
public const string ClientProfile = "__uauth_client_profile";
public const string FormCacheKey = "__uauth_form";
diff --git a/src/client/CodeBeam.UltimateAuth.Client.Blazor/Components/UAuthLoginForm.razor b/src/client/CodeBeam.UltimateAuth.Client.Blazor/Components/UAuthLoginForm.razor
index 6a57f226..b2342bba 100644
--- a/src/client/CodeBeam.UltimateAuth.Client.Blazor/Components/UAuthLoginForm.razor
+++ b/src/client/CodeBeam.UltimateAuth.Client.Blazor/Components/UAuthLoginForm.razor
@@ -1,39 +1,59 @@
-@* TODO: Optional double-submit prevention for native form submit *@
-@namespace CodeBeam.UltimateAuth.Client.Blazor
+@namespace CodeBeam.UltimateAuth.Client.Blazor
-@using CodeBeam.UltimateAuth.Client.Device
@using CodeBeam.UltimateAuth.Client.Options
-@using CodeBeam.UltimateAuth.Core.Abstractions
@using CodeBeam.UltimateAuth.Core.Defaults
@using CodeBeam.UltimateAuth.Core.Contracts
-@using CodeBeam.UltimateAuth.Core.Options
@using Microsoft.Extensions.Options
+
@inject IJSRuntime JS
@inject IOptions Options
@inject NavigationManager Navigation
-
diff --git a/src/client/CodeBeam.UltimateAuth.Client.Blazor/Components/UAuthLoginForm.razor.cs b/src/client/CodeBeam.UltimateAuth.Client.Blazor/Components/UAuthLoginForm.razor.cs
index d9c54663..76f7ec0c 100644
--- a/src/client/CodeBeam.UltimateAuth.Client.Blazor/Components/UAuthLoginForm.razor.cs
+++ b/src/client/CodeBeam.UltimateAuth.Client.Blazor/Components/UAuthLoginForm.razor.cs
@@ -1,26 +1,34 @@
-using CodeBeam.UltimateAuth.Client.Infrastructure;
+using CodeBeam.UltimateAuth.Client.Errors;
+using CodeBeam.UltimateAuth.Client.Infrastructure;
using CodeBeam.UltimateAuth.Core.Abstractions;
using CodeBeam.UltimateAuth.Core.Contracts;
using CodeBeam.UltimateAuth.Core.Defaults;
using CodeBeam.UltimateAuth.Core.Domain;
-using CodeBeam.UltimateAuth.Core.Errors;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.JSInterop;
+using System.Text.Json;
namespace CodeBeam.UltimateAuth.Client.Blazor;
///
-/// Represents a Blazor component that provides a login form for UltimateAuth authentication.
+/// Provides a ready-to-use Blazor login form with native browser credential submission.
///
+///
+/// When no child content is supplied, the component renders a complete login form automatically.
+///
+/// Custom forms must provide enabled inputs named
+/// Identifier and Secret .
+/// Credential binding to Blazor component state is not required.
+///
+/// Do not disable credential inputs during submission, as disabled fields are excluded from native form submissions.
+/// Disable the submit button instead.
+///
public partial class UAuthLoginForm
{
[Inject]
IDeviceIdProvider DeviceIdProvider { get; set; } = null!;
- [Inject]
- IUAuthClient UAuthClient { get; set; } = null!;
-
[Inject]
IHubCredentialResolver HubCredentialResolver { get; set; } = null!;
@@ -31,100 +39,103 @@ public partial class UAuthLoginForm
IHubCapabilities HubCapabilities { get; set; } = null!;
///
- /// Gets or sets the unique login identifier associated with this component instance.
- ///
- [Parameter]
- public string? Identifier { get; set; }
-
- ///
- /// Gets or sets the secret value used for authentication or configuration.
- ///
- [Parameter]
- public string? Secret { get; set; }
-
- ///
- /// Gets or sets the endpoint URL (login path) used to connect to the target service. If not set, endpoint is set by options.
+ /// Gets or sets the login commit endpoint.
+ /// When omitted, the configured endpoint is used.
///
[Parameter]
public string? Endpoint { get; set; }
///
- /// Gets or sets the URL to which the user is redirected after a successful operation. If not set, return url is set by hub flow state's return url.
+ /// Gets or sets the URL to return to after successful login.
///
[Parameter]
public string? ReturnUrl { get; set; }
///
- /// Gets or sets the associated UAuthHub flow id. If not set, value is set from query.
+ /// Gets or sets the UAuthHub session associated with a PKCE login.
+ /// When omitted, the session is resolved from the current URL.
///
[Parameter]
public HubSessionId? HubSessionId { get; set; }
///
- /// Gets or sets the type of login method to use for authentication.
+ /// Gets or sets the login type. Defaults to password login.
///
[Parameter]
public UAuthLoginType LoginType { get; set; } = UAuthLoginType.Password;
///
- /// Gets or sets the mode used to submit authentication requests. Default is TryAndCommit.
+ /// Gets or sets the submission mode.
+ /// Defaults to .
///
[Parameter]
public UAuthSubmitMode SubmitMode { get; set; } = UAuthSubmitMode.TryAndCommit;
///
- /// Gets or sets the content to be rendered inside the component.
+ /// Gets or sets custom form content.
+ /// When omitted, a complete login form is rendered automatically.
///
- /// Use this property to specify child markup or components that will be rendered within the body
- /// of this component. Typically set automatically when the component is used with child content in Razor
- /// syntax.
+ ///
+ /// Custom content must include enabled inputs named
+ /// Identifier and Secret .
+ /// Their values are read directly from the browser form.
+ ///
[Parameter]
public RenderFragment? ChildContent { get; set; }
///
- /// Gets or sets a value indicating whether pressing the Enter key submits the form.
+ /// Gets or sets whether the component renders a hidden submit button for Enter-key submission in custom forms.
///
+ ///
+ /// Defaults to .
+ /// This does not prevent other submit controls from submitting the form.
+ ///
[Parameter]
public bool AllowEnterKeyToSubmit { get; set; } = true;
///
- /// Gets or sets the callback that is invoked when a try result event occurs. Does not fire on DirectCommit submit mode.
+ /// Occurs when a structured login attempt result is available.
+ /// Not invoked for direct commit submissions.
///
- /// Use this property to handle the result of an authentication attempt. The callback receives an
- /// object that provides details about the outcome of the operation.
[Parameter]
public EventCallback OnTryResult { get; set; }
- private bool _isSubmitting;
-
+ ///
+ /// Occurs when the form submission state changes.
+ /// Receives when submission begins
+ /// and when it finishes.
+ ///
[Parameter]
public EventCallback OnSubmittingChanged { get; set; }
+ ///
+ /// Gets whether a login submission is currently in progress.
+ ///
public bool IsSubmitting => _isSubmitting;
-
+ private bool _isSubmitting;
private ElementReference _form;
private HubCredentials? _credentials;
private HubFlowState? _flow;
private DeviceId? _deviceId;
- ///
+ ///
protected override async Task OnParametersSetAsync()
{
await base.OnParametersSetAsync();
- await ReloadCredentialsAsync();
- await ReloadStateAsync();
-
- if (LoginType == UAuthLoginType.Pkce && !HubCapabilities.SupportsPkce)
+ if (LoginType == UAuthLoginType.Pkce &&
+ !HubCapabilities.SupportsPkce)
{
- throw new InvalidOperationException("PKCE login requires UAuthHub (Blazor Server). " +
- "PKCE is not supported in this client profile." +
- "Change LoginType to password or place this component to a server-side project.");
+ throw new InvalidOperationException(
+ "PKCE login requires UAuthHub (Blazor Server).");
}
+
+ await ReloadCredentialsAsync();
+ await ReloadStateAsync();
}
- ///
+ ///
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender)
@@ -134,39 +145,33 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
StateHasChanged();
}
- ///
- /// Asynchronously reloads the credentials associated with the current hub session, if applicable.
- ///
- ///
protected async Task ReloadCredentialsAsync()
{
- if (LoginType != UAuthLoginType.Pkce)
+ if (LoginType != UAuthLoginType.Pkce ||
+ EffectiveHubSessionId is null)
return;
- if (HubCredentialResolver is null || EffectiveHubSessionId is null)
- return;
-
- _credentials = await HubCredentialResolver.ResolveAsync(EffectiveHubSessionId.Value);
+ _credentials = await HubCredentialResolver.ResolveAsync(
+ EffectiveHubSessionId.Value);
}
- ///
- /// Asynchronously reloads the state associated with the current hub session, if applicable.
- ///
- ///
protected async Task ReloadStateAsync()
{
- if (LoginType != UAuthLoginType.Pkce || EffectiveHubSessionId is null || HubFlowReader is null)
+ if (LoginType != UAuthLoginType.Pkce ||
+ EffectiveHubSessionId is null)
return;
- _flow = await HubFlowReader.GetStateAsync(EffectiveHubSessionId.Value);
+ _flow = await HubFlowReader.GetStateAsync(
+ EffectiveHubSessionId.Value);
}
///
- /// Asynchronously reloads credentials and state, and updates the component state.
+ /// Reloads the associated UAuthHub credentials and flow state.
///
- /// Call this method to refresh the component's credentials and state. This method is typically
- /// used when external changes require the component to update its internal data and UI.
- /// A task that represents the asynchronous reload operation.
+ ///
+ /// Intended for PKCE scenarios where the Hub context changes.
+ /// Does not initiate a new PKCE authorization flow.
+ ///
public async Task ReloadAsync()
{
await ReloadCredentialsAsync();
@@ -175,10 +180,17 @@ public async Task ReloadAsync()
}
///
- /// Submits the login form asynchronously using the configured authentication method.
+ /// Submits the native browser login form using the configured authentication flow.
///
- /// A task that represents the asynchronous submit operation.
- /// Thrown if the form has not been rendered. Call this method only after the OnAfterRender lifecycle event.
+ ///
+ /// Credentials are read from native form fields without being passed as Blazor component parameters or JS interop arguments.
+ ///
+ /// The component must have completed its initial render.
+ /// Concurrent submissions are ignored.
+ ///
+ ///
+ /// Thrown when the form has not rendered or required PKCE credentials are unavailable.
+ ///
public async Task SubmitAsync()
{
if (_isSubmitting)
@@ -187,20 +199,75 @@ public async Task SubmitAsync()
if (_form.Context is null)
throw new InvalidOperationException("Form is not yet rendered. Call SubmitAsync after OnAfterRender.");
+ if (LoginType == UAuthLoginType.Pkce && _credentials is null)
+ throw new InvalidOperationException("Missing PKCE credentials.");
+
_isSubmitting = true;
try
{
await OnSubmittingChanged.InvokeAsync(true);
- if (LoginType == UAuthLoginType.Pkce)
+ var result = await JS.InvokeAsync(
+ "uauth.submitLoginForm",
+ _form,
+ new
+ {
+ mode = SubmitMode.ToString(),
+ tryUrl = ResolvedTryEndpoint,
+ commitUrl = ResolvedCommitEndpoint,
+ clientProfile = ClientProfileValue,
+ trustedOrigin = TrustedOrigin,
+ isPkce = LoginType == UAuthLoginType.Pkce
+ });
+
+ if (SubmitMode == UAuthSubmitMode.DirectCommit)
+ return;
+
+ if (result.ValueKind != JsonValueKind.Object)
+ throw new UAuthProtocolException("Invalid browser login response.");
+
+ IUAuthTryResult parsed;
+
+ if (result.TryGetProperty("status", out var status) &&
+ status.ValueKind == JsonValueKind.Number &&
+ status.GetInt32() == 429)
{
- await SubmitPkceAsync();
+ var retryAfter = result.TryGetProperty("retryAfter", out var retry)
+ ? retry.GetString()
+ : null;
+
+ var delay = UAuthRateLimit.ParseRetryAfter(
+ retryAfter, DateTimeOffset.UtcNow);
+
+ parsed = LoginType == UAuthLoginType.Pkce
+ ? new TryPkceLoginResult
+ {
+ Status = 429,
+ ErrorCode = "rate_limited",
+ RetryAfter = delay
+ }
+ : new TryLoginResult
+ {
+ Status = 429,
+ ErrorCode = "rate_limited",
+ RetryAfter = delay
+ };
}
else
{
- await SubmitPasswordAsync();
+ var options = new JsonSerializerOptions(
+ JsonSerializerDefaults.Web);
+
+ parsed = LoginType == UAuthLoginType.Pkce
+ ? result.Deserialize(options)
+ ?? throw new UAuthProtocolException("Invalid PKCE try result.")
+ : result.Deserialize(options)
+ ?? throw new UAuthProtocolException("Invalid password try result.");
}
+
+ if (OnTryResult.HasDelegate)
+ await OnTryResult.InvokeAsync(parsed);
}
finally
{
@@ -209,137 +276,75 @@ public async Task SubmitAsync()
}
}
- private async Task SubmitPasswordAsync()
- {
- if (string.IsNullOrWhiteSpace(Identifier) || string.IsNullOrWhiteSpace(Secret))
- {
- throw new UAuthValidationException("Identifier and Secret are required.");
- }
-
- var request = new LoginRequest
- {
- Identifier = Identifier,
- Secret = Secret,
- };
-
- switch (SubmitMode)
- {
- case UAuthSubmitMode.DirectCommit:
- await JS.InvokeVoidAsync("uauth.submitForm", _form);
- break;
-
- case UAuthSubmitMode.TryOnly:
- {
- var result = await UAuthClient.Flows.TryLoginAsync(request, UAuthSubmitMode.TryOnly);
- await EmitResultAsync(result);
- break;
- }
-
- case UAuthSubmitMode.TryAndCommit:
- default:
- {
- var result = await UAuthClient.Flows.TryLoginAsync(request, UAuthSubmitMode.TryAndCommit, EffectiveReturnUrl);
- await EmitResultAsync(result);
- break;
- }
- }
- }
+ private string ClientProfileValue => Options.Value.ClientProfile.ToString();
- private async Task SubmitPkceAsync()
+ private string? TrustedOrigin
{
- if (_credentials is null)
- throw new InvalidOperationException("Missing PKCE credentials.");
-
- if (string.IsNullOrWhiteSpace(Identifier) || string.IsNullOrWhiteSpace(Secret))
- {
- throw new UAuthValidationException("Identifier and Secret are required.");
- }
-
- var request = new PkceCompleteRequest
- {
- Identifier = Identifier,
- Secret = Secret,
- AuthorizationCode = _credentials.AuthorizationCode,
- CodeVerifier = _credentials.CodeVerifier,
- ReturnUrl = EffectiveReturnUrl ?? string.Empty,
- HubSessionId = EffectiveHubSessionId?.Value ?? string.Empty
- };
-
- switch (SubmitMode)
+ get
{
- case UAuthSubmitMode.DirectCommit:
- {
- await UAuthClient.Flows.CompletePkceLoginAsync(request);
- break;
- }
+ var basePath = Options.Value.Endpoints.BasePath;
- case UAuthSubmitMode.TryOnly:
- {
- var result = await UAuthClient.Flows.TryCompletePkceLoginAsync(request, UAuthSubmitMode.TryOnly);
- await EmitResultAsync(result);
- break;
- }
+ if (Uri.TryCreate(basePath, UriKind.Absolute, out var uri) && uri.Scheme is "http" or "https")
+ {
+ return uri.GetLeftPart(UriPartial.Authority);
+ }
- case UAuthSubmitMode.TryAndCommit:
- default:
- {
- var result = await UAuthClient.Flows.TryCompletePkceLoginAsync(request, UAuthSubmitMode.TryAndCommit);
- await EmitResultAsync(result);
- break;
- }
+ return null;
}
}
- private async Task EmitResultAsync(IUAuthTryResult result)
+ private string BuildEndpoint(string path)
{
- if (OnTryResult.HasDelegate)
- await OnTryResult.InvokeAsync(result);
+ return UAuthUrlBuilder.Build(Options.Value.Endpoints.BasePath, path, Options.Value.MultiTenant);
}
- private string ClientProfileValue => Options.Value.ClientProfile.ToString();
-
- private string EffectiveEndpoint => LoginType == UAuthLoginType.Pkce
- ? Options.Value.Endpoints.PkceTryComplete
- : Options.Value.Endpoints.Login;
-
+ private string ResolvedTryEndpoint =>
+ BuildEndpoint(
+ LoginType == UAuthLoginType.Pkce
+ ? Options.Value.Endpoints.PkceTryComplete
+ : Options.Value.Endpoints.TryLogin);
- private string ResolvedEndpoint
+ private string ResolvedCommitEndpoint
{
get
{
- var loginPath = string.IsNullOrWhiteSpace(Endpoint)
- ? EffectiveEndpoint
- : Endpoint;
+ var path = !string.IsNullOrWhiteSpace(Endpoint)
+ ? Endpoint
+ : LoginType == UAuthLoginType.Pkce
+ ? Options.Value.Endpoints.PkceComplete
+ : Options.Value.Endpoints.Login;
- var baseUrl = UAuthUrlBuilder.Build(Options.Value.Endpoints.BasePath, loginPath, Options.Value.MultiTenant);
- var returnUrl = EffectiveReturnUrl;
-
- if (string.IsNullOrWhiteSpace(returnUrl))
- return baseUrl;
+ var url = BuildEndpoint(path);
var query = new List();
- if (_credentials != null && EffectiveHubSessionId is not null)
+ if (LoginType == UAuthLoginType.Pkce && EffectiveHubSessionId is not null)
{
query.Add(
$"{UAuthConstants.Query.Hub}=" +
- $"{Uri.EscapeDataString(EffectiveHubSessionId.Value.Value)}");
+ Uri.EscapeDataString(
+ EffectiveHubSessionId.Value.Value));
}
- if (!string.IsNullOrWhiteSpace(returnUrl))
+ if (!string.IsNullOrWhiteSpace(EffectiveReturnUrl))
{
- query.Add($"{UAuthConstants.Query.ReturnUrl}={Uri.EscapeDataString(returnUrl)}");
+ query.Add(
+ $"{UAuthConstants.Query.ReturnUrl}=" +
+ Uri.EscapeDataString(EffectiveReturnUrl));
}
return query.Count == 0
- ? baseUrl
- : $"{baseUrl}?{string.Join("&", query)}";
+ ? url
+ : $"{url}{(url.Contains('?') ? "&" : "?")}{string.Join("&", query)}";
}
}
- private string? EffectiveReturnUrl => !string.IsNullOrWhiteSpace(ReturnUrl)
- ? ReturnUrl
- : LoginType == UAuthLoginType.Pkce ? _flow?.ReturnUrl : Navigation.Uri;
+ private string? EffectiveReturnUrl =>
+ !string.IsNullOrWhiteSpace(ReturnUrl)
+ ? ReturnUrl
+ : LoginType == UAuthLoginType.Pkce
+ ? _flow?.ReturnUrl
+ : Navigation.Uri;
private HubSessionId? EffectiveHubSessionId
{
@@ -351,7 +356,8 @@ private HubSessionId? EffectiveHubSessionId
var uri = Navigation.ToAbsoluteUri(Navigation.Uri);
var query = QueryHelpers.ParseQuery(uri.Query);
- if (query.TryGetValue(UAuthConstants.Query.Hub, out var hubValue) && CodeBeam.UltimateAuth.Core.Domain.HubSessionId.TryParse(hubValue, out var parsed))
+ if (query.TryGetValue(UAuthConstants.Query.Hub, out var value) &&
+ CodeBeam.UltimateAuth.Core.Domain.HubSessionId.TryParse(value, out var parsed))
{
return parsed;
}
diff --git a/src/client/CodeBeam.UltimateAuth.Client.Blazor/Components/UAuthLoginForm.razor.css b/src/client/CodeBeam.UltimateAuth.Client.Blazor/Components/UAuthLoginForm.razor.css
new file mode 100644
index 00000000..f99aa0cc
--- /dev/null
+++ b/src/client/CodeBeam.UltimateAuth.Client.Blazor/Components/UAuthLoginForm.razor.css
@@ -0,0 +1,54 @@
+.uauth-login-fields {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ width: 100%;
+ max-width: 420px;
+}
+
+.uauth-login-field {
+ display: flex;
+ flex-direction: column;
+ gap: 0.375rem;
+}
+
+ .uauth-login-field label {
+ font-size: 0.875rem;
+ font-weight: 500;
+ }
+
+.uauth-login-input {
+ width: 100%;
+ box-sizing: border-box;
+ padding: 0.75rem;
+ border: 1px solid #a1a1aa;
+ border-radius: 0.375rem;
+ background: transparent;
+ color: inherit;
+ font: inherit;
+}
+
+ .uauth-login-input:focus-visible {
+ outline: 2px solid #2563eb;
+ outline-offset: 2px;
+ }
+
+.uauth-login-submit {
+ padding: 0.75rem 1rem;
+ border: none;
+ border-radius: 0.375rem;
+ background: #2563eb;
+ color: white;
+ font: inherit;
+ font-weight: 600;
+ cursor: pointer;
+}
+
+ .uauth-login-submit:hover {
+ background: #1d4ed8;
+ }
+
+ .uauth-login-submit:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ }
diff --git a/src/client/CodeBeam.UltimateAuth.Client.Blazor/TScripts/uauth.js b/src/client/CodeBeam.UltimateAuth.Client.Blazor/TScripts/uauth.js
index 237bb830..eccde40a 100644
--- a/src/client/CodeBeam.UltimateAuth.Client.Blazor/TScripts/uauth.js
+++ b/src/client/CodeBeam.UltimateAuth.Client.Blazor/TScripts/uauth.js
@@ -712,6 +712,164 @@ window.uauth.antiforgery = {
}
};
+window.uauth.submitLoginForm = async function (form, options) {
+ if (!(form instanceof HTMLFormElement)) {
+ throw new Error("A native HTML form is required.");
+ }
+
+ const {
+ mode,
+ tryUrl,
+ commitUrl,
+ clientProfile,
+ trustedOrigin,
+ isPkce
+ } = options;
+
+ if (!["TryOnly", "TryAndCommit", "DirectCommit"].includes(mode)) {
+ throw new Error("Unsupported UAuth submit mode.");
+ }
+
+ if (!window.uauth.deviceId) {
+ throw new Error("UAuth deviceId is not initialized.");
+ }
+
+ const validatedCommitUrl = window.uauth.validateAuthUrl(
+ commitUrl,
+ trustedOrigin
+ );
+
+ const validatedTryUrl = mode === "DirectCommit"
+ ? null
+ : window.uauth.validateAuthUrl(tryUrl, trustedOrigin);
+
+ // Read credentials from the original browser inputs.
+ // Never return them to Blazor or copy them to hidden inputs.
+ const fields = new FormData(form);
+
+ const identifiers = fields.getAll("Identifier");
+ const secrets = fields.getAll("Secret");
+
+ if (identifiers.length !== 1 || secrets.length !== 1) {
+ throw new Error(
+ "Login form requires exactly one Identifier and one Secret."
+ );
+ }
+
+ const identifier = identifiers[0];
+ const secret = secrets[0];
+
+ if (typeof identifier !== "string" ||
+ typeof secret !== "string" ||
+ !identifier.trim() ||
+ !secret.trim()) {
+ throw new Error("Identifier and Secret are required.");
+ }
+
+ if (isPkce) {
+ for (const name of ["authorization_code", "code_verifier"]) {
+ const values = fields.getAll(name);
+
+ if (values.length !== 1 ||
+ typeof values[0] !== "string" ||
+ !values[0]) {
+ throw new Error(`Missing or duplicate PKCE field: ${name}`);
+ }
+ }
+ }
+
+ // Commit always uses the original form.
+ // No second form and no hidden password copy.
+ form.action = validatedCommitUrl;
+
+ if (mode === "DirectCommit") {
+ await window.uauth.submitForm(form, trustedOrigin);
+ return null;
+ }
+
+ const payload = {
+ Identifier: identifier,
+ Secret: secret
+ };
+
+ if (isPkce) {
+ payload.authorization_code = fields.get("authorization_code");
+ payload.code_verifier = fields.get("code_verifier");
+ payload.__uauth_return_url = fields.get("__uauth_return_url") ?? "";
+ payload.hub_session_id = fields.get("hub_session_id") ?? "";
+ } else {
+ payload.ReturnUrl = fields.get("__uauth_return_url") ?? "";
+ }
+
+ const response = await window.uauth.postJson({
+ url: validatedTryUrl,
+ payload,
+ clientProfile,
+ trustedOrigin
+ });
+
+ if (!response || !Number.isInteger(response.status)) {
+ throw new Error("Invalid UAuth transport response.");
+ }
+
+ if (response.status === 429) {
+ return {
+ isSuccess: false,
+ status: 429,
+ errorCode: "rate_limited",
+ retryAfter: response.retryAfter ?? null
+ };
+ }
+
+ const isHttpSuccess =
+ response.status >= 200 && response.status < 300;
+
+ const isClientError =
+ response.status >= 400 && response.status < 500;
+
+ if (!isHttpSuccess && !isClientError) {
+ throw new Error(
+ `Unexpected UAuth try response: HTTP ${response.status}.`
+ );
+ }
+
+ const result = response.body;
+
+ if (!result ||
+ typeof result !== "object" ||
+ Array.isArray(result) ||
+ typeof result.isSuccess !== "boolean") {
+ throw new Error("Invalid UAuth try response body.");
+ }
+
+ if (isClientError && result.isSuccess) {
+ throw new Error(
+ "Successful authentication result returned with HTTP error."
+ );
+ }
+
+ if (mode === "TryAndCommit" && result.isSuccess) {
+ if (result.previewReceipt) {
+ let receipt = Array.from(form.elements)
+ .find(element => element.name === "PreviewReceipt");
+
+ if (!receipt) {
+ receipt = document.createElement("input");
+ receipt.type = "hidden";
+ receipt.name = "PreviewReceipt";
+ form.appendChild(receipt);
+ }
+
+ receipt.value = result.previewReceipt;
+ }
+
+ await window.uauth.submitForm(form, trustedOrigin);
+ }
+
+ // Only the non-credential authentication result crosses JS interop.
+ return result;
+};
+
window.uauth.setDeviceId = function (value) {
window.uauth.deviceId = value;
};
diff --git a/src/client/CodeBeam.UltimateAuth.Client.Blazor/wwwroot/uauth.min.js b/src/client/CodeBeam.UltimateAuth.Client.Blazor/wwwroot/uauth.min.js
index 5c205de1..e952d90a 100644
--- a/src/client/CodeBeam.UltimateAuth.Client.Blazor/wwwroot/uauth.min.js
+++ b/src/client/CodeBeam.UltimateAuth.Client.Blazor/wwwroot/uauth.min.js
@@ -1 +1 @@
-window.uauth=window.uauth||{};window.uauth.storage={set:function(n,t,i){const r=n==="local"?window.localStorage:window.sessionStorage;r.setItem(t,i)},get:function(n,t){const i=n==="local"?window.localStorage:window.sessionStorage;return i.getItem(t)},remove:function(n,t){const i=n==="local"?window.localStorage:window.sessionStorage;i.removeItem(t)},exists:function(n,t){const i=n==="local"?window.localStorage:window.sessionStorage;return i.getItem(t)!==null}};window.uauth.submitForm=async function(n,t=null){if(!n)return;if(!window.uauth.deviceId)throw new Error("UAuth deviceId is not initialized.");const r=window.uauth.validateAuthUrl(n.action,t);n.action=r;let i=n.querySelector("input[name='__uauth_device']");i||(i=document.createElement("input"),i.type="hidden",i.name="__uauth_device",n.appendChild(i));i.value=window.uauth.deviceId;await window.uauth.antiforgery.appendToForm(n,t);n.submit()};window.uauth.tryAndCommit=async function(n){const{tryUrl:c,commitUrl:l,data:e,clientProfile:o,trustedOrigin:u}=n,a=window.uauth.validateAuthUrl(c,u),v=window.uauth.validateAuthUrl(l,u),s=window.uauth.deviceId;if(!s)throw new Error("UAuth deviceId is not initialized.");const r=await window.uauth.postJson({url:a,payload:e,clientProfile:o,trustedOrigin:u});if(!r||!Number.isInteger(r.status))throw new Error("Invalid UAuth transport response.");const i=r.status;if(i===429)return{isSuccess:!1,status:429,errorCode:"rate_limited",retryAfter:r.retryAfter??null};const y=i>=200&&i<300,h=i>=400&&i<500;if(!y&&!h)throw new Error(`Unexpected UAuth try response: HTTP ${i}.`);const t=r.body;if(!t||typeof t!="object"||Array.isArray(t)||typeof t.isSuccess!="boolean")throw new Error("Invalid UAuth try response body.");if(h&&t.isSuccess)throw new Error("Successful authentication result returned with HTTP error.");const f={isSuccess:t.isSuccess,reason:t.reason??null,remainingAttempts:t.remainingAttempts??null,lockoutUntilUtc:t.lockoutUntilUtc??null,requiresMfa:t.requiresMfa??!1,retryWithNewPkce:t.retryWithNewPkce??!1,previewReceipt:t.previewReceipt??null};if(f.isSuccess){const n=document.createElement("form");n.method="POST";n.action=v;for(const t of Object.keys(e)){const i=document.createElement("input");i.type="hidden";i.name=t;i.value=e[t]??"";n.appendChild(i)}if(f.previewReceipt){const t=document.createElement("input");t.type="hidden";t.name="PreviewReceipt";t.value=f.previewReceipt;n.appendChild(t)}const t=document.createElement("input");t.type="hidden";t.name="__uauth_client_profile";t.value=o??"";n.appendChild(t);const i=document.createElement("input");i.type="hidden";i.name="__uauth_device";i.value=s;n.appendChild(i);document.body.appendChild(n);try{await window.uauth.submitForm(n,u)}catch(r){n.remove();throw r;}}return f};window.uauth.post=async function(n){const{url:e,mode:t,data:i,clientProfile:u,trustedOrigin:r}=n,f=window.uauth.validateAuthUrl(e,r);if(t==="navigate"){const n=document.createElement("form");n.method="POST";n.action=f;const t=document.createElement("input");t.type="hidden";t.name="__uauth_client_profile";t.value=u??"";n.appendChild(t);const e=document.createElement("input");if(e.type="hidden",e.name="__uauth_device",e.value=window.uauth.deviceId,n.appendChild(e),i)for(const t in i){const r=document.createElement("input");r.type="hidden";r.name=t;r.value=i[t];n.appendChild(r)}document.body.appendChild(n);try{await window.uauth.submitForm(n,r)}catch(o){n.remove();throw o;}return null}const o=t==="logout"||t==="logout-observe";return window.uauth.request({url:f,method:"POST",payload:i,clientProfile:u,contentType:"form",credentials:"include",allowCrossOriginCredentials:!0,antiforgery:!0,trustedOrigin:r,navigateOnSuccess:t==="logout",headers:o?{"X-UAuth-Logout-Mode":"observe"}:{}})};window.uauth.postJson=function(n){const t=window.uauth.validateAuthUrl(n.url,n.trustedOrigin);return window.uauth.request({url:t,method:"POST",payload:n.payload,clientProfile:n.clientProfile,contentType:"json",credentials:"include",allowCrossOriginCredentials:!0,antiforgery:!0,trustedOrigin:n.trustedOrigin})};window.uauth.request=async function(n){const{followRedirects:a=!1,navigateOnSuccess:v=!1,url:h,method:y="GET",payload:r=null,clientProfile:p="",contentType:c="json",credentials:s="same-origin",headers:w={}}=n;if(!h)throw new Error("Request URL is required.");const u=new URL(h,document.baseURI);if(!["http:","https:"].includes(u.protocol))throw new Error("Unsupported request URL protocol.");const i=y.toUpperCase();if(!["GET","POST","PUT","PATCH","DELETE"].includes(i))throw new Error(`Unsupported HTTP method: ${i}`);if(!["same-origin","include","omit"].includes(s))throw new Error("Invalid credentials policy.");if(s==="include"&&u.origin!==window.location.origin&&!n.allowCrossOriginCredentials)throw new Error("Cross-origin credentials require explicit authorization.");if(!window.uauth.deviceId)throw new Error("UAuth deviceId is not initialized.");const f={...w,"X-UDID":window.uauth.deviceId,"X-UAuth-ClientProfile":p,"X-Requested-With":"UAuth"};let e=null;if(r!==null&&r!==undefined){if(i==="GET")throw new Error("GET requests cannot contain a body.");if(c==="json")f["Content-Type"]="application/json",e=JSON.stringify(r);else if(c==="form"){f["Content-Type"]="application/x-www-form-urlencoded";e=new URLSearchParams;for(const[n,t]of Object.entries(r))e.append(n,t??"")}else throw new Error("Unsupported content type.");}if(i!=="GET"&&n.antiforgery===!0){const i=window.uauth.validateAuthUrl(window.uauth.antiforgery.tokenUrl,n.trustedOrigin);if(new URL(i).origin!==u.origin)throw new Error("UAuth antiforgery token origin differs from request origin.");const t=await window.uauth.antiforgery.getToken(n.trustedOrigin);f[t.headerName]=t.requestToken}let t;try{t=await fetch(u.href,{method:i,credentials:s,redirect:a?"follow":"error",headers:f,body:e})}catch(l){if(l instanceof TypeError)return{ok:!1,status:0,refreshOutcome:null,body:null};throw l;}let o=null;if(t.status!==204&&t.status!==205){const n=await t.text();if(n)try{o=JSON.parse(n)}catch{o=n}}if(v&&t.ok){const n=o?.redirectUrl;if(typeof n=="string"&&n.length>0){const t=new URL(n,window.location.origin);if(!["http:","https:"].includes(t.protocol))throw new Error("Invalid logout redirect protocol.");window.location.assign(t.href)}}return{retryAfter:t.headers.get("Retry-After"),ok:t.ok,status:t.status,refreshOutcome:t.headers.get("X-UAuth-Refresh"),body:o}};window.uauth.resourceRequest=async function(n){const{url:o,baseAddress:h,method:v="GET",payload:f=null,clientProfile:y="",credentials:p="Auto",requestId:u}=n;if(!o||typeof o!="string")throw new Error("Resource request URL is required.");const i=h?new URL(h):new URL(window.location.origin+"/");if(!["http:","https:"].includes(i.protocol))throw new Error("Invalid Resource API protocol.");if(i.username||i.password||i.search||i.hash)throw new Error("Invalid Resource API base address.");const t=new URL(o,i);if(t.origin!==i.origin)throw new Error("Resource request target differs from configured API origin.");if(t.username||t.password)throw new Error("Resource URL cannot contain credentials.");if(!["http:","https:"].includes(t.protocol))throw new Error("Unsupported Resource API protocol.");const s=v.toUpperCase();if(!["GET","POST","PUT","PATCH","DELETE"].includes(s))throw new Error("Unsupported Resource API method.");if(s==="GET"&&f!==null)throw new Error("GET requests cannot contain a body.");let r;switch(p.toLowerCase()){case"auto":r=t.origin===window.location.origin?"same-origin":"include";break;case"sameorigin":r="same-origin";break;case"include":r="include";break;case"omit":r="omit";break;default:throw new Error("Invalid Resource credentials mode.");}if(r==="include"&&t.protocol!=="https:"&&!["localhost","127.0.0.1","[::1]"].includes(t.hostname))throw new Error("Cross-origin credential requests require HTTPS.");if(!window.uauth.deviceId)throw new Error("UAuth deviceId is not initialized.");const c={"X-UDID":window.uauth.deviceId,"X-UAuth-ClientProfile":y,"X-Requested-With":"UAuth"};let l=null;f!==null&&f!==undefined&&(c["Content-Type"]="application/json",l=JSON.stringify(f));const a=new AbortController;if(u){if(window.uauth.resourceRequests.has(u))throw new Error("Duplicate Resource request ID.");window.uauth.resourceRequests.set(u,a)}try{const n=await fetch(t.href,{method:s,credentials:r,redirect:"error",headers:c,body:l,signal:a.signal});let i=null;if(n.status!==204&&n.status!==205){const t=await n.text();t.length>0&&(i=t)}return{retryAfter:n.headers.get("Retry-After"),ok:n.ok,status:n.status,body:i,contentType:n.headers.get("Content-Type")}}catch(e){if(e?.name==="AbortError")throw e;if(e instanceof TypeError)return{ok:!1,status:0,body:null,contentType:null};throw e;}finally{u&&window.uauth.resourceRequests.delete(u)}};window.uauth.resourceRequests=new Map;window.uauth.cancelResourceRequest=function(n){const t=window.uauth.resourceRequests.get(n);return t?(t.abort(),!0):!1};window.uauth.validateAuthUrl=function(n,t=null){if(!n||typeof n!="string")throw new Error("Authentication URL is required.");const r=t?new URL(t,window.location.origin):new URL(window.location.origin),i=new URL(n,document.baseURI);if(!["http:","https:"].includes(r.protocol))throw new Error("Unsupported trusted origin protocol.");if(!["http:","https:"].includes(i.protocol))throw new Error("Unsupported authentication URL protocol.");if(r.username||r.password)throw new Error("Trusted origin cannot contain credentials.");if(i.username||i.password)throw new Error("Authentication URL cannot contain credentials.");if(i.origin!==r.origin)throw new Error("Authentication request target differs from the trusted UAuthHub origin.");return i.href};window.uauth.antiforgery={tokenUrl:null,configure:function(n){if(!n?.tokenUrl)throw new Error("UAuth antiforgery token URL is required.");this.tokenUrl=window.uauth.validateAuthUrl(n.tokenUrl,n.trustedOrigin)},getToken:async function(n){if(!this.tokenUrl)throw new Error("UAuth antiforgery is not configured.");const r=window.uauth.validateAuthUrl(this.tokenUrl,n),i=await fetch(r,{method:"GET",credentials:"include",cache:"no-store",redirect:"error",headers:{"X-UDID":window.uauth.deviceId}});if(!i.ok)throw new Error(`Unable to obtain UAuth antiforgery token: HTTP ${i.status}.`);const t=await i.json();if(!t?.requestToken||!t?.headerName||!t?.formFieldName)throw new Error("Invalid UAuth antiforgery response.");return t},appendToForm:async function(n,t){const r=await this.getToken(t);let i=Array.from(n.elements).find(n=>n.name===r.formFieldName);i||(i=document.createElement("input"),i.type="hidden",i.name=r.formFieldName,n.appendChild(i));i.value=r.requestToken}};window.uauth.setDeviceId=function(n){window.uauth.deviceId=n};window.uauth.getDeviceInfo=function(){return{userAgent:navigator.userAgent,platform:navigator.platform,language:navigator.language}}
\ No newline at end of file
+window.uauth=window.uauth||{};window.uauth.storage={set:function(n,t,i){const r=n==="local"?window.localStorage:window.sessionStorage;r.setItem(t,i)},get:function(n,t){const i=n==="local"?window.localStorage:window.sessionStorage;return i.getItem(t)},remove:function(n,t){const i=n==="local"?window.localStorage:window.sessionStorage;i.removeItem(t)},exists:function(n,t){const i=n==="local"?window.localStorage:window.sessionStorage;return i.getItem(t)!==null}};window.uauth.submitForm=async function(n,t=null){if(!n)return;if(!window.uauth.deviceId)throw new Error("UAuth deviceId is not initialized.");const r=window.uauth.validateAuthUrl(n.action,t);n.action=r;let i=n.querySelector("input[name='__uauth_device']");i||(i=document.createElement("input"),i.type="hidden",i.name="__uauth_device",n.appendChild(i));i.value=window.uauth.deviceId;await window.uauth.antiforgery.appendToForm(n,t);n.submit()};window.uauth.tryAndCommit=async function(n){const{tryUrl:c,commitUrl:l,data:e,clientProfile:o,trustedOrigin:u}=n,a=window.uauth.validateAuthUrl(c,u),v=window.uauth.validateAuthUrl(l,u),s=window.uauth.deviceId;if(!s)throw new Error("UAuth deviceId is not initialized.");const r=await window.uauth.postJson({url:a,payload:e,clientProfile:o,trustedOrigin:u});if(!r||!Number.isInteger(r.status))throw new Error("Invalid UAuth transport response.");const i=r.status;if(i===429)return{isSuccess:!1,status:429,errorCode:"rate_limited",retryAfter:r.retryAfter??null};const y=i>=200&&i<300,h=i>=400&&i<500;if(!y&&!h)throw new Error(`Unexpected UAuth try response: HTTP ${i}.`);const t=r.body;if(!t||typeof t!="object"||Array.isArray(t)||typeof t.isSuccess!="boolean")throw new Error("Invalid UAuth try response body.");if(h&&t.isSuccess)throw new Error("Successful authentication result returned with HTTP error.");const f={isSuccess:t.isSuccess,reason:t.reason??null,remainingAttempts:t.remainingAttempts??null,lockoutUntilUtc:t.lockoutUntilUtc??null,requiresMfa:t.requiresMfa??!1,retryWithNewPkce:t.retryWithNewPkce??!1,previewReceipt:t.previewReceipt??null};if(f.isSuccess){const n=document.createElement("form");n.method="POST";n.action=v;for(const t of Object.keys(e)){const i=document.createElement("input");i.type="hidden";i.name=t;i.value=e[t]??"";n.appendChild(i)}if(f.previewReceipt){const t=document.createElement("input");t.type="hidden";t.name="PreviewReceipt";t.value=f.previewReceipt;n.appendChild(t)}const t=document.createElement("input");t.type="hidden";t.name="__uauth_client_profile";t.value=o??"";n.appendChild(t);const i=document.createElement("input");i.type="hidden";i.name="__uauth_device";i.value=s;n.appendChild(i);document.body.appendChild(n);try{await window.uauth.submitForm(n,u)}catch(r){n.remove();throw r;}}return f};window.uauth.post=async function(n){const{url:e,mode:t,data:i,clientProfile:u,trustedOrigin:r}=n,f=window.uauth.validateAuthUrl(e,r);if(t==="navigate"){const n=document.createElement("form");n.method="POST";n.action=f;const t=document.createElement("input");t.type="hidden";t.name="__uauth_client_profile";t.value=u??"";n.appendChild(t);const e=document.createElement("input");if(e.type="hidden",e.name="__uauth_device",e.value=window.uauth.deviceId,n.appendChild(e),i)for(const t in i){const r=document.createElement("input");r.type="hidden";r.name=t;r.value=i[t];n.appendChild(r)}document.body.appendChild(n);try{await window.uauth.submitForm(n,r)}catch(o){n.remove();throw o;}return null}const o=t==="logout"||t==="logout-observe";return window.uauth.request({url:f,method:"POST",payload:i,clientProfile:u,contentType:"form",credentials:"include",allowCrossOriginCredentials:!0,antiforgery:!0,trustedOrigin:r,navigateOnSuccess:t==="logout",headers:o?{"X-UAuth-Logout-Mode":"observe"}:{}})};window.uauth.postJson=function(n){const t=window.uauth.validateAuthUrl(n.url,n.trustedOrigin);return window.uauth.request({url:t,method:"POST",payload:n.payload,clientProfile:n.clientProfile,contentType:"json",credentials:"include",allowCrossOriginCredentials:!0,antiforgery:!0,trustedOrigin:n.trustedOrigin})};window.uauth.request=async function(n){const{followRedirects:a=!1,navigateOnSuccess:v=!1,url:h,method:y="GET",payload:r=null,clientProfile:p="",contentType:c="json",credentials:s="same-origin",headers:w={}}=n;if(!h)throw new Error("Request URL is required.");const u=new URL(h,document.baseURI);if(!["http:","https:"].includes(u.protocol))throw new Error("Unsupported request URL protocol.");const i=y.toUpperCase();if(!["GET","POST","PUT","PATCH","DELETE"].includes(i))throw new Error(`Unsupported HTTP method: ${i}`);if(!["same-origin","include","omit"].includes(s))throw new Error("Invalid credentials policy.");if(s==="include"&&u.origin!==window.location.origin&&!n.allowCrossOriginCredentials)throw new Error("Cross-origin credentials require explicit authorization.");if(!window.uauth.deviceId)throw new Error("UAuth deviceId is not initialized.");const f={...w,"X-UDID":window.uauth.deviceId,"X-UAuth-ClientProfile":p,"X-Requested-With":"UAuth"};let e=null;if(r!==null&&r!==undefined){if(i==="GET")throw new Error("GET requests cannot contain a body.");if(c==="json")f["Content-Type"]="application/json",e=JSON.stringify(r);else if(c==="form"){f["Content-Type"]="application/x-www-form-urlencoded";e=new URLSearchParams;for(const[n,t]of Object.entries(r))e.append(n,t??"")}else throw new Error("Unsupported content type.");}if(i!=="GET"&&n.antiforgery===!0){const i=window.uauth.validateAuthUrl(window.uauth.antiforgery.tokenUrl,n.trustedOrigin);if(new URL(i).origin!==u.origin)throw new Error("UAuth antiforgery token origin differs from request origin.");const t=await window.uauth.antiforgery.getToken(n.trustedOrigin);f[t.headerName]=t.requestToken}let t;try{t=await fetch(u.href,{method:i,credentials:s,redirect:a?"follow":"error",headers:f,body:e})}catch(l){if(l instanceof TypeError)return{ok:!1,status:0,refreshOutcome:null,body:null};throw l;}let o=null;if(t.status!==204&&t.status!==205){const n=await t.text();if(n)try{o=JSON.parse(n)}catch{o=n}}if(v&&t.ok){const n=o?.redirectUrl;if(typeof n=="string"&&n.length>0){const t=new URL(n,window.location.origin);if(!["http:","https:"].includes(t.protocol))throw new Error("Invalid logout redirect protocol.");window.location.assign(t.href)}}return{retryAfter:t.headers.get("Retry-After"),ok:t.ok,status:t.status,refreshOutcome:t.headers.get("X-UAuth-Refresh"),body:o}};window.uauth.resourceRequest=async function(n){const{url:o,baseAddress:h,method:v="GET",payload:f=null,clientProfile:y="",credentials:p="Auto",requestId:u}=n;if(!o||typeof o!="string")throw new Error("Resource request URL is required.");const i=h?new URL(h):new URL(window.location.origin+"/");if(!["http:","https:"].includes(i.protocol))throw new Error("Invalid Resource API protocol.");if(i.username||i.password||i.search||i.hash)throw new Error("Invalid Resource API base address.");const t=new URL(o,i);if(t.origin!==i.origin)throw new Error("Resource request target differs from configured API origin.");if(t.username||t.password)throw new Error("Resource URL cannot contain credentials.");if(!["http:","https:"].includes(t.protocol))throw new Error("Unsupported Resource API protocol.");const s=v.toUpperCase();if(!["GET","POST","PUT","PATCH","DELETE"].includes(s))throw new Error("Unsupported Resource API method.");if(s==="GET"&&f!==null)throw new Error("GET requests cannot contain a body.");let r;switch(p.toLowerCase()){case"auto":r=t.origin===window.location.origin?"same-origin":"include";break;case"sameorigin":r="same-origin";break;case"include":r="include";break;case"omit":r="omit";break;default:throw new Error("Invalid Resource credentials mode.");}if(r==="include"&&t.protocol!=="https:"&&!["localhost","127.0.0.1","[::1]"].includes(t.hostname))throw new Error("Cross-origin credential requests require HTTPS.");if(!window.uauth.deviceId)throw new Error("UAuth deviceId is not initialized.");const c={"X-UDID":window.uauth.deviceId,"X-UAuth-ClientProfile":y,"X-Requested-With":"UAuth"};let l=null;f!==null&&f!==undefined&&(c["Content-Type"]="application/json",l=JSON.stringify(f));const a=new AbortController;if(u){if(window.uauth.resourceRequests.has(u))throw new Error("Duplicate Resource request ID.");window.uauth.resourceRequests.set(u,a)}try{const n=await fetch(t.href,{method:s,credentials:r,redirect:"error",headers:c,body:l,signal:a.signal});let i=null;if(n.status!==204&&n.status!==205){const t=await n.text();t.length>0&&(i=t)}return{retryAfter:n.headers.get("Retry-After"),ok:n.ok,status:n.status,body:i,contentType:n.headers.get("Content-Type")}}catch(e){if(e?.name==="AbortError")throw e;if(e instanceof TypeError)return{ok:!1,status:0,body:null,contentType:null};throw e;}finally{u&&window.uauth.resourceRequests.delete(u)}};window.uauth.resourceRequests=new Map;window.uauth.cancelResourceRequest=function(n){const t=window.uauth.resourceRequests.get(n);return t?(t.abort(),!0):!1};window.uauth.validateAuthUrl=function(n,t=null){if(!n||typeof n!="string")throw new Error("Authentication URL is required.");const r=t?new URL(t,window.location.origin):new URL(window.location.origin),i=new URL(n,document.baseURI);if(!["http:","https:"].includes(r.protocol))throw new Error("Unsupported trusted origin protocol.");if(!["http:","https:"].includes(i.protocol))throw new Error("Unsupported authentication URL protocol.");if(r.username||r.password)throw new Error("Trusted origin cannot contain credentials.");if(i.username||i.password)throw new Error("Authentication URL cannot contain credentials.");if(i.origin!==r.origin)throw new Error("Authentication request target differs from the trusted UAuthHub origin.");return i.href};window.uauth.antiforgery={tokenUrl:null,configure:function(n){if(!n?.tokenUrl)throw new Error("UAuth antiforgery token URL is required.");this.tokenUrl=window.uauth.validateAuthUrl(n.tokenUrl,n.trustedOrigin)},getToken:async function(n){if(!this.tokenUrl)throw new Error("UAuth antiforgery is not configured.");const r=window.uauth.validateAuthUrl(this.tokenUrl,n),i=await fetch(r,{method:"GET",credentials:"include",cache:"no-store",redirect:"error",headers:{"X-UDID":window.uauth.deviceId}});if(!i.ok)throw new Error(`Unable to obtain UAuth antiforgery token: HTTP ${i.status}.`);const t=await i.json();if(!t?.requestToken||!t?.headerName||!t?.formFieldName)throw new Error("Invalid UAuth antiforgery response.");return t},appendToForm:async function(n,t){const r=await this.getToken(t);let i=Array.from(n.elements).find(n=>n.name===r.formFieldName);i||(i=document.createElement("input"),i.type="hidden",i.name=r.formFieldName,n.appendChild(i));i.value=r.requestToken}};window.uauth.submitLoginForm=async function(n,t){if(!(n instanceof HTMLFormElement))throw new Error("A native HTML form is required.");const{mode:o,tryUrl:y,commitUrl:p,clientProfile:w,trustedOrigin:e,isPkce:c}=t;if(!["TryOnly","TryAndCommit","DirectCommit"].includes(o))throw new Error("Unsupported UAuth submit mode.");if(!window.uauth.deviceId)throw new Error("UAuth deviceId is not initialized.");const b=window.uauth.validateAuthUrl(p,e),k=o==="DirectCommit"?null:window.uauth.validateAuthUrl(y,e),u=new FormData(n),l=u.getAll("Identifier"),a=u.getAll("Secret");if(l.length!==1||a.length!==1)throw new Error("Login form requires exactly one Identifier and one Secret.");const s=l[0],h=a[0];if(typeof s!="string"||typeof h!="string"||!s.trim()||!h.trim())throw new Error("Identifier and Secret are required.");if(c)for(const n of["authorization_code","code_verifier"]){const t=u.getAll(n);if(t.length!==1||typeof t[0]!="string"||!t[0])throw new Error(`Missing or duplicate PKCE field: ${n}`);}if(n.action=b,o==="DirectCommit")return await window.uauth.submitForm(n,e),null;const f={Identifier:s,Secret:h};c?(f.authorization_code=u.get("authorization_code"),f.code_verifier=u.get("code_verifier"),f.__uauth_return_url=u.get("__uauth_return_url")??"",f.hub_session_id=u.get("hub_session_id")??""):f.ReturnUrl=u.get("__uauth_return_url")??"";const i=await window.uauth.postJson({url:k,payload:f,clientProfile:w,trustedOrigin:e});if(!i||!Number.isInteger(i.status))throw new Error("Invalid UAuth transport response.");if(i.status===429)return{isSuccess:!1,status:429,errorCode:"rate_limited",retryAfter:i.retryAfter??null};const d=i.status>=200&&i.status<300,v=i.status>=400&&i.status<500;if(!d&&!v)throw new Error(`Unexpected UAuth try response: HTTP ${i.status}.`);const r=i.body;if(!r||typeof r!="object"||Array.isArray(r)||typeof r.isSuccess!="boolean")throw new Error("Invalid UAuth try response body.");if(v&&r.isSuccess)throw new Error("Successful authentication result returned with HTTP error.");if(o==="TryAndCommit"&&r.isSuccess){if(r.previewReceipt){let t=Array.from(n.elements).find(n=>n.name==="PreviewReceipt");t||(t=document.createElement("input"),t.type="hidden",t.name="PreviewReceipt",n.appendChild(t));t.value=r.previewReceipt}await window.uauth.submitForm(n,e)}return r};window.uauth.setDeviceId=function(n){window.uauth.deviceId=n};window.uauth.getDeviceInfo=function(){return{userAgent:navigator.userAgent,platform:navigator.platform,language:navigator.language}}
\ No newline at end of file
diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Bunit/UAuthLoginFormTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Bunit/UAuthLoginFormTests.cs
index 46e6bd12..37f52eee 100644
--- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Bunit/UAuthLoginFormTests.cs
+++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Bunit/UAuthLoginFormTests.cs
@@ -1,214 +1,130 @@
-using Bunit;
+using AwesomeAssertions;
+using Bunit;
using CodeBeam.UltimateAuth.Client;
using CodeBeam.UltimateAuth.Client.Blazor;
using CodeBeam.UltimateAuth.Client.Options;
-using CodeBeam.UltimateAuth.Client.Services;
using CodeBeam.UltimateAuth.Core.Abstractions;
using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Core.Defaults;
using CodeBeam.UltimateAuth.Core.Domain;
-using CodeBeam.UltimateAuth.Core.Errors;
-using AwesomeAssertions;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Moq;
-namespace CodeBeam.UltimateAuth.Tests.Unit;
+namespace CodeBeam.UltimateAuth.Tests.Unit.Client.Blazor;
-public class UAuthLoginFormTests
+public sealed class UAuthLoginFormContextTests : BunitContext
{
- [Fact]
- public void Should_Render_Form()
- {
- using var ctx = new BunitContext();
-
- ctx.Services.AddSingleton(Mock.Of());
- ctx.Services.AddSingleton(Mock.Of());
- ctx.Services.AddSingleton(Mock.Of());
- ctx.Services.AddSingleton(Mock.Of());
- ctx.Services.AddSingleton(Mock.Of());
- ctx.Services.AddSingleton>(Options.Create(new UAuthClientOptions()));
+ private readonly Mock _credentials = new();
+ private readonly Mock _flows = new();
+ private readonly Mock _capabilities = new();
- var cut = ctx.Render();
-
- cut.Find("form").Should().NotBeNull();
+ public UAuthLoginFormContextTests()
+ {
+ JSInterop.Mode = JSRuntimeMode.Loose;
+ Services.AddSingleton(Mock.Of());
+ Services.AddSingleton(_credentials.Object);
+ Services.AddSingleton(_flows.Object);
+ Services.AddSingleton(_capabilities.Object);
+ Services.AddSingleton>(Options.Create(new UAuthClientOptions()));
}
[Fact]
- public void Should_Render_Identifier_And_Secret_Inputs()
+ public void Default_Form_Renders_Exactly_One_Native_Identifier_And_Secret_And_Submit()
{
- using var ctx = new BunitContext();
-
- ctx.Services.AddSingleton(Mock.Of());
- ctx.Services.AddSingleton(Mock.Of());
- ctx.Services.AddSingleton(Mock.Of());
- ctx.Services.AddSingleton(Mock.Of());
- ctx.Services.AddSingleton(Mock.Of());
- ctx.Services.AddSingleton>(Options.Create(new UAuthClientOptions()));
-
- var cut = ctx.Render(p => p
- .Add(x => x.Identifier, "user")
- .Add(x => x.Secret, "pass"));
-
- cut.Markup.Should().Contain("name=\"Identifier\"");
- cut.Markup.Should().Contain("name=\"Secret\"");
+ var cut = Render();
+ var form = cut.Find("form");
+
+ form.GetAttribute("method").Should().Be("post");
+ cut.FindAll("input[name='Identifier']").Should().ContainSingle();
+ cut.FindAll("input[name='Secret']").Should().ContainSingle();
+ cut.Find("input[name='Secret']").GetAttribute("type").Should().Be("password");
+ cut.Find("input[name='Secret']").GetAttribute("autocomplete").Should().Be("current-password");
+ cut.FindAll("button[type='submit']").Should().ContainSingle();
+ cut.Find("button[type='submit']").HasAttribute("hidden").Should().BeFalse();
}
[Fact]
- public async Task Submit_Should_Call_TryLogin()
+ public void Custom_Content_Replaces_Default_Fields_Without_Duplicates()
{
- using var ctx = new BunitContext();
-
- var flowMock = new Mock();
-
- flowMock.Setup(x => x.TryLoginAsync(
- It.IsAny(),
- It.IsAny(),
- It.IsAny()))
- .ReturnsAsync(new TryLoginResult { IsSuccess = true });
-
- var clientMock = new Mock();
- clientMock.Setup(x => x.Flows).Returns(flowMock.Object);
-
- ctx.Services.AddSingleton(clientMock.Object);
- ctx.Services.AddSingleton(Mock.Of());
- ctx.Services.AddSingleton(Mock.Of());
- ctx.Services.AddSingleton(Mock.Of());
- ctx.Services.AddSingleton(Mock.Of());
- ctx.Services.AddSingleton>(Options.Create(new UAuthClientOptions()));
-
- var cut = ctx.Render(p => p
- .Add(x => x.Identifier, "user")
- .Add(x => x.Secret, "pass"));
+ var cut = Render(p => p.Add(x => x.ChildContent, (RenderFragment)(builder =>
+ {
+ builder.OpenElement(0, "div");
+ builder.AddAttribute(1, "class", "custom-login");
+ builder.OpenElement(2, "input");
+ builder.AddAttribute(3, "name", "Identifier");
+ builder.CloseElement();
+ builder.OpenElement(4, "input");
+ builder.AddAttribute(5, "name", "Secret");
+ builder.AddAttribute(6, "type", "password");
+ builder.CloseElement();
+ builder.CloseElement();
+ })));
+
+ cut.FindAll(".custom-login").Should().ContainSingle();
+ cut.FindAll("input[name='Identifier']").Should().ContainSingle();
+ cut.FindAll("input[name='Secret']").Should().ContainSingle();
+ cut.FindAll(".uauth-login-fields").Should().BeEmpty();
+ }
- await cut.Instance.SubmitAsync();
+ [Theory]
+ [InlineData(true, 1)]
+ [InlineData(false, 0)]
+ public void Custom_Content_Respects_Enter_Submit_Fallback(bool allowEnter, int expectedButtons)
+ {
+ var cut = Render(p => p
+ .Add(x => x.AllowEnterKeyToSubmit, allowEnter)
+ .Add(x => x.ChildContent, (RenderFragment)(b => b.AddMarkupContent(0, " "))));
- flowMock.Verify(x =>
- x.TryLoginAsync(It.IsAny(), It.IsAny(), It.IsAny()),
- Times.Once);
+ cut.FindAll("button[type='submit']").Should().HaveCount(expectedButtons);
}
[Fact]
- public async Task Submit_Should_Throw_When_Missing_Credentials()
+ public void Default_Form_Does_Not_Expose_Credential_Values_As_Component_Parameters()
{
- using var ctx = new BunitContext();
+ var parameters = typeof(UAuthLoginForm).GetProperties()
+ .Where(p => p.IsDefined(typeof(ParameterAttribute), inherit: true))
+ .Select(p => p.Name);
- ctx.Services.AddSingleton(Mock.Of());
- ctx.Services.AddSingleton(Mock.Of());
- ctx.Services.AddSingleton(Mock.Of());
- ctx.Services.AddSingleton(Mock.Of());
- ctx.Services.AddSingleton(Mock.Of());
- ctx.Services.AddSingleton>(Options.Create(new UAuthClientOptions()));
-
- var cut = ctx.Render();
-
- Func act = async () => await cut.Instance.SubmitAsync();
-
- await act.Should().ThrowAsync();
+ parameters.Should().NotContain("Identifier").And.NotContain("Secret");
}
[Fact]
- public async Task SubmitPkce_Should_Call_TryCompletePkce()
+ public void Form_Renders_Metadata_And_Explicit_ReturnUrl()
{
- using var ctx = new BunitContext();
-
- var flowMock = new Mock();
-
- flowMock.Setup(x => x.TryCompletePkceLoginAsync(
- It.IsAny(),
- It.IsAny()))
- .ReturnsAsync(new TryPkceLoginResult { IsSuccess = true });
+ var cut = Render(p => p.Add(x => x.ReturnUrl, "/dashboard"));
- var clientMock = new Mock();
- clientMock.Setup(x => x.Flows).Returns(flowMock.Object);
-
- var credResolver = new Mock();
- credResolver.Setup(x => x.ResolveAsync(It.IsAny()))
- .ReturnsAsync(new HubCredentials
- {
- AuthorizationCode = "code",
- CodeVerifier = "verifier"
- });
-
- var capabilities = new Mock();
- capabilities.Setup(x => x.SupportsPkce).Returns(true);
-
- ctx.Services.AddSingleton(clientMock.Object);
- ctx.Services.AddSingleton(Mock.Of());
- ctx.Services.AddSingleton(credResolver.Object);
- ctx.Services.AddSingleton(Mock.Of());
- ctx.Services.AddSingleton(capabilities.Object);
- ctx.Services.AddSingleton>(Options.Create(new UAuthClientOptions()));
-
- var hubSessionId = HubSessionId.New();
-
- var cut = ctx.Render(p => p
- .Add(x => x.Identifier, "user")
- .Add(x => x.Secret, "pass")
- .Add(x => x.LoginType, UAuthLoginType.Pkce)
- .Add(x => x.HubSessionId, hubSessionId));
-
- await cut.InvokeAsync(() => Task.CompletedTask);
- await cut.Instance.SubmitAsync();
-
- flowMock.Verify(x =>
- x.TryCompletePkceLoginAsync(It.IsAny(), It.IsAny()),
- Times.Once);
+ cut.FindAll($"input[name='{UAuthConstants.Form.ClientProfile}']").Should().ContainSingle();
+ cut.FindAll($"input[name='{UAuthConstants.Form.Device}']").Should().ContainSingle();
+ cut.Find($"input[name='{UAuthConstants.Form.ReturnUrl}']")
+ .GetAttribute("value").Should().Be("/dashboard");
+ cut.Find("form").GetAttribute("action").Should().Contain("uauth_return_url=%2Fdashboard");
}
[Fact]
- public async Task Should_Invoke_OnTryResult()
+ public void Pkce_Renders_Hub_Credentials_And_Hub_Query()
{
- using var ctx = new BunitContext();
-
- var flowMock = new Mock();
-
- flowMock.Setup(x => x.TryLoginAsync(
- It.IsAny(),
- It.IsAny(),
- It.IsAny()))
- .ReturnsAsync(new TryLoginResult { IsSuccess = true });
+ var hubId = HubSessionId.New();
+ _capabilities.SetupGet(x => x.SupportsPkce).Returns(true);
+ _credentials.Setup(x => x.ResolveAsync(hubId, It.IsAny()))
+ .ReturnsAsync(new HubCredentials { AuthorizationCode = "auth-code", CodeVerifier = "verifier" });
- var clientMock = new Mock();
- clientMock.Setup(x => x.Flows).Returns(flowMock.Object);
-
- var invoked = false;
-
- ctx.Services.AddSingleton(clientMock.Object);
- ctx.Services.AddSingleton(Mock.Of());
- ctx.Services.AddSingleton(Mock.Of());
- ctx.Services.AddSingleton(Mock.Of());
- ctx.Services.AddSingleton(Mock.Of());
- ctx.Services.AddSingleton>(Options.Create(new UAuthClientOptions()));
-
- var cut = ctx.Render(p => p
- .Add(x => x.Identifier, "user")
- .Add(x => x.Secret, "pass")
- .Add(x => x.OnTryResult, EventCallback.Factory.Create(this, _ => invoked = true)));
-
- await cut.Instance.SubmitAsync();
+ var cut = Render(p => p
+ .Add(x => x.LoginType, UAuthLoginType.Pkce)
+ .Add(x => x.HubSessionId, hubId));
- invoked.Should().BeTrue();
+ cut.Find("input[name='authorization_code']").GetAttribute("value").Should().Be("auth-code");
+ cut.Find("input[name='code_verifier']").GetAttribute("value").Should().Be("verifier");
+ cut.Find("input[name='hub_session_id']").GetAttribute("value").Should().Be(hubId.Value);
+ cut.Find("form").GetAttribute("action").Should().Contain(Uri.EscapeDataString(hubId.Value));
}
[Fact]
- public void Should_Throw_When_Pkce_Not_Supported()
+ public void Pkce_Without_Hub_Support_Is_Rejected()
{
- using var ctx = new BunitContext();
-
- var capabilities = new Mock();
- capabilities.Setup(x => x.SupportsPkce).Returns(false);
-
- ctx.Services.AddSingleton(capabilities.Object);
- ctx.Services.AddSingleton(Mock.Of());
- ctx.Services.AddSingleton(Mock.Of());
- ctx.Services.AddSingleton(Mock.Of());
- ctx.Services.AddSingleton(Mock.Of());
- ctx.Services.AddSingleton>(Options.Create(new UAuthClientOptions()));
-
- Action act = () => ctx.Render(p => p
- .Add(x => x.LoginType, UAuthLoginType.Pkce));
-
- act.Should().Throw();
+ _capabilities.SetupGet(x => x.SupportsPkce).Returns(false);
+ Action act = () => Render(p => p.Add(x => x.LoginType, UAuthLoginType.Pkce));
+ act.Should().Throw().WithMessage("*PKCE login requires UAuthHub*");
}
}
diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/Blazor/UAuthLoginFormSubmissionTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/Blazor/UAuthLoginFormSubmissionTests.cs
index ec25c253..6767de08 100644
--- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/Blazor/UAuthLoginFormSubmissionTests.cs
+++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/Blazor/UAuthLoginFormSubmissionTests.cs
@@ -1,689 +1,256 @@
-using Bunit;
+using AwesomeAssertions;
+using Bunit;
using CodeBeam.UltimateAuth.Client;
using CodeBeam.UltimateAuth.Client.Blazor;
using CodeBeam.UltimateAuth.Client.Options;
-using CodeBeam.UltimateAuth.Client.Services;
using CodeBeam.UltimateAuth.Core.Abstractions;
using CodeBeam.UltimateAuth.Core.Contracts;
using CodeBeam.UltimateAuth.Core.Defaults;
using CodeBeam.UltimateAuth.Core.Domain;
-using CodeBeam.UltimateAuth.Core.Errors;
-using AwesomeAssertions;
-using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
+using Microsoft.JSInterop;
using Moq;
+using System.Text.Json;
namespace CodeBeam.UltimateAuth.Tests.Unit.Client.Blazor;
public sealed class UAuthLoginFormSubmissionTests : BunitContext
{
- private readonly Mock _deviceIdProvider = new();
- private readonly Mock _client = new();
- private readonly Mock _flows = new();
- private readonly Mock _credentialResolver = new();
- private readonly Mock _hubFlowReader = new();
- private readonly Mock _hubCapabilities = new();
+ private readonly Mock _credentials = new();
+ private readonly Mock _flows = new();
+ private readonly Mock _capabilities = new();
public UAuthLoginFormSubmissionTests()
{
- JSInterop.Mode = JSRuntimeMode.Loose;
-
- _client
- .SetupGet(x => x.Flows)
- .Returns(_flows.Object);
-
- _hubCapabilities
- .SetupGet(x => x.SupportsPkce)
- .Returns(true);
-
- Services.AddSingleton(_deviceIdProvider.Object);
- Services.AddSingleton(_client.Object);
- Services.AddSingleton(_credentialResolver.Object);
- Services.AddSingleton(_hubFlowReader.Object);
- Services.AddSingleton(_hubCapabilities.Object);
-
- Services.AddSingleton(
- Options.Create(new UAuthClientOptions()));
+ JSInterop.Mode = JSRuntimeMode.Strict;
+ Services.AddSingleton(Mock.Of());
+ Services.AddSingleton(_credentials.Object);
+ Services.AddSingleton(_flows.Object);
+ Services.AddSingleton(_capabilities.Object);
+ Services.AddSingleton>(Options.Create(new UAuthClientOptions()));
}
- // -----------------------------------------------------------------
- // Password validation
- // -----------------------------------------------------------------
-
- [Theory]
- [InlineData(null, "secret")]
- [InlineData("", "secret")]
- [InlineData(" ", "secret")]
- [InlineData("alice", null)]
- [InlineData("alice", "")]
- [InlineData("alice", " ")]
- public async Task PasswordSubmit_WhenCredentialsMissing_ThrowsValidationException(string? identifier, string? secret)
- {
- var cut = RenderPasswordForm(
- identifier,
- secret,
- UAuthSubmitMode.TryOnly);
-
- var act = () => cut.Instance.SubmitAsync();
-
- await act.Should().ThrowAsync();
- }
+ private static JsonElement Json(string value) => JsonDocument.Parse(value).RootElement.Clone();
- // -----------------------------------------------------------------
- // Password / TryOnly
- // -----------------------------------------------------------------
+ private IRenderedComponent RenderForm(UAuthSubmitMode mode = UAuthSubmitMode.TryAndCommit)
+ => Render(p => p.Add(x => x.SubmitMode, mode));
- [Fact]
- public async Task Password_TryOnly_SendsExpectedLoginRequest()
+ private void ConfigureResult(string json)
{
- LoginRequest? captured = null;
+ using var document = JsonDocument.Parse(json);
- var result = new TryLoginResult
- {
- IsSuccess = true
- };
-
- _client
- .Setup(x => x.Flows.TryLoginAsync(
- It.IsAny(),
- UAuthSubmitMode.TryOnly,
- It.IsAny()))
- .Callback(
- (request, _, _) => captured = request)
- .ReturnsAsync(result);
-
- var cut = RenderPasswordForm(
- "alice@example.com",
- "secret-123",
- UAuthSubmitMode.TryOnly);
-
- await cut.Instance.SubmitAsync();
-
- captured.Should().NotBeNull();
- captured!.Identifier.Should().Be("alice@example.com");
- captured.Secret.Should().Be("secret-123");
+ JSInterop
+ .Setup(
+ "uauth.submitLoginForm",
+ _ => true)
+ .SetResult(document.RootElement.Clone());
}
- [Fact]
- public async Task Password_TryOnly_UsesTryOnlyMode()
- {
- var result = CreateTryLoginResult();
-
- _client
- .Setup(x => x.Flows.TryLoginAsync(
- It.IsAny(),
- UAuthSubmitMode.TryOnly,
- It.IsAny()))
- .ReturnsAsync(result);
-
- var cut = RenderPasswordForm(
- "alice",
- "secret",
- UAuthSubmitMode.TryOnly);
-
- await cut.Instance.SubmitAsync();
-
- _client.Verify(
- x => x.Flows.TryLoginAsync(
- It.Is(r =>
- r.Identifier == "alice" &&
- r.Secret == "secret"),
- UAuthSubmitMode.TryOnly,
- It.IsAny()),
- Times.Once);
- }
-
- [Fact]
- public async Task Password_TryOnly_EmitsTryResult()
+ [Theory]
+ [InlineData(UAuthSubmitMode.TryOnly)]
+ [InlineData(UAuthSubmitMode.TryAndCommit)]
+ public async Task Password_Submit_Uses_One_Browser_Interop_Call_And_Emits_Try_Result(UAuthSubmitMode mode)
{
- var result = new TryLoginResult
+ ConfigureResult("""
{
- IsSuccess = true
- };
+ "isSuccess": true,
+ "previewReceipt": "receipt"
+ }
+ """);
- _client
- .Setup(x => x.Flows.TryLoginAsync(
- It.IsAny(),
- UAuthSubmitMode.TryOnly,
- It.IsAny()))
- .ReturnsAsync(result);
-
- IUAuthTryResult? emitted = null;
+ IUAuthTryResult? received = null;
var cut = Render(p => p
- .Add(x => x.Identifier, "alice")
- .Add(x => x.Secret, "secret")
- .Add(x => x.SubmitMode, UAuthSubmitMode.TryOnly)
- .Add(x => x.OnTryResult,
- r => emitted = r));
-
- await cut.Instance.SubmitAsync();
-
- emitted.Should().BeSameAs(result);
- }
-
- // -----------------------------------------------------------------
- // Password / TryAndCommit
- // -----------------------------------------------------------------
-
- [Fact]
- public async Task Password_TryAndCommit_SendsEffectiveReturnUrl()
- {
- string? capturedReturnUrl = null;
-
- var result = new TryLoginResult
- {
- IsSuccess = true
- };
-
- _client
- .Setup(x => x.Flows.TryLoginAsync(
- It.IsAny(),
- UAuthSubmitMode.TryAndCommit,
- It.IsAny()))
- .Callback(
- (_, _, returnUrl) =>
- capturedReturnUrl = returnUrl)
- .ReturnsAsync(result);
-
- var cut = Render(p => p
- .Add(x => x.Identifier, "alice")
- .Add(x => x.Secret, "secret")
+ .Add(x => x.SubmitMode, mode)
.Add(x => x.ReturnUrl, "/dashboard")
- .Add(x => x.SubmitMode, UAuthSubmitMode.TryAndCommit));
+ .Add(x => x.OnTryResult, result => received = result));
- await cut.Instance.SubmitAsync();
+ await cut.InvokeAsync(() => cut.Instance.SubmitAsync());
- capturedReturnUrl.Should().Be("/dashboard");
- }
+ received.Should().BeOfType();
+ ((TryLoginResult)received!).IsSuccess.Should().BeTrue();
- [Fact]
- public async Task Password_TryAndCommit_EmitsTryResult()
- {
- var result = CreateTryLoginResult();
+ JSInterop.VerifyInvoke("uauth.submitLoginForm", 1);
- _client
- .Setup(x => x.Flows.TryLoginAsync(
- It.IsAny(),
- UAuthSubmitMode.TryAndCommit,
- It.IsAny()))
- .ReturnsAsync(result);
+ var call = JSInterop.Invocations
+ .Single(x => x.Identifier == "uauth.submitLoginForm");
- IUAuthTryResult? emitted = null;
+ call.Arguments.Should().HaveCount(2);
- var cut = Render(p => p
- .Add(x => x.Identifier, "alice")
- .Add(x => x.Secret, "secret")
- .Add(x => x.ReturnUrl, "/home")
- .Add(x => x.SubmitMode, UAuthSubmitMode.TryAndCommit)
- .Add(x => x.OnTryResult,
- r => emitted = r));
+ var options = JsonSerializer.Serialize(call.Arguments[1]);
- await cut.Instance.SubmitAsync();
+ using var document = JsonDocument.Parse(options);
+ var root = document.RootElement;
- emitted.Should().BeSameAs(result);
- }
+ root.GetProperty("mode")
+ .GetString()
+ .Should().Be(mode.ToString());
- // -----------------------------------------------------------------
- // Password / DirectCommit
- // -----------------------------------------------------------------
+ root.GetProperty("isPkce")
+ .GetBoolean()
+ .Should().BeFalse();
- [Fact]
- public async Task Password_DirectCommit_UsesNativeFormSubmission()
- {
- var cut = RenderPasswordForm(
- "alice",
- "secret",
- UAuthSubmitMode.DirectCommit);
+ root.GetProperty("tryUrl")
+ .GetString()
+ .Should().Be("/auth/try-login");
- await cut.Instance.SubmitAsync();
+ root.GetProperty("commitUrl")
+ .GetString()
+ .Should().Contain(
+ $"{UAuthConstants.Query.ReturnUrl}={Uri.EscapeDataString("/dashboard")}");
- JSInterop.VerifyInvoke("uauth.submitForm");
+ options.Should()
+ .NotContain("\"Secret\"")
+ .And.NotContain("\"Identifier\"");
}
[Fact]
- public async Task Password_DirectCommit_DoesNotCallTryLogin()
+ public async Task DirectCommit_Does_Not_Emit_Try_Result()
{
- var cut = RenderPasswordForm(
- "alice",
- "secret",
- UAuthSubmitMode.DirectCommit);
-
- await cut.Instance.SubmitAsync();
-
- _client.Verify(
- x => x.Flows.TryLoginAsync(
- It.IsAny(),
- It.IsAny(),
- It.IsAny()),
- Times.Never);
- }
+ ConfigureResult("null");
- [Fact]
- public async Task Password_DirectCommit_DoesNotEmitTryResult()
- {
- var callbackCount = 0;
+ var count = 0;
var cut = Render(p => p
- .Add(x => x.Identifier, "alice")
- .Add(x => x.Secret, "secret")
.Add(x => x.SubmitMode, UAuthSubmitMode.DirectCommit)
- .Add(x => x.OnTryResult,
- _ => callbackCount++));
+ .Add(x => x.OnTryResult, _ => count++));
- await cut.Instance.SubmitAsync();
+ await cut.InvokeAsync(() => cut.Instance.SubmitAsync());
- callbackCount.Should().Be(0);
- }
+ count.Should().Be(0);
- [Fact]
- public async Task Password_DirectCommit_DoesNotCallFlowClient()
- {
- var cut = RenderPasswordForm(
- "alice",
- "secret",
- UAuthSubmitMode.DirectCommit);
-
- await cut.Instance.SubmitAsync();
-
- _client.Verify(
- x => x.Flows.TryLoginAsync(
- It.IsAny(),
- It.IsAny(),
- It.IsAny()),
- Times.Never);
-
- _client.Verify(
- x => x.Flows.LoginAsync(
- It.IsAny(),
- It.IsAny()),
- Times.Never);
- }
+ JSInterop.VerifyInvoke("uauth.submitLoginForm", 1);
- // -----------------------------------------------------------------
- // PKCE validation
- // -----------------------------------------------------------------
+ var call = JSInterop.Invocations
+ .Single(x => x.Identifier == "uauth.submitLoginForm");
- [Fact]
- public void Pkce_WhenHubDoesNotSupportPkce_Throws()
- {
- _hubCapabilities
- .SetupGet(x => x.SupportsPkce)
- .Returns(false);
+ call.Arguments.Should().HaveCount(2);
- var hubId = HubSessionId.New();
+ using var document = JsonDocument.Parse(
+ JsonSerializer.Serialize(call.Arguments[1]));
- var act = () => Render(p => p
- .Add(x => x.LoginType, UAuthLoginType.Pkce)
- .Add(x => x.HubSessionId, hubId));
+ document.RootElement.GetProperty("mode")
+ .GetString()
+ .Should().Be(nameof(UAuthSubmitMode.DirectCommit));
- act.Should()
- .Throw()
- .WithMessage("*PKCE login requires UAuthHub*");
+ cut.Instance.IsSubmitting.Should().BeFalse();
}
[Fact]
- public async Task Pkce_WhenCredentialsMissing_Throws()
+ public async Task Failed_Try_Result_Is_Forwarded()
{
- var hubId = HubSessionId.New();
-
- _credentialResolver
- .Setup(x => x.ResolveAsync(
- hubId,
- It.IsAny()))
- .ReturnsAsync((HubCredentials?)null);
-
- _hubFlowReader
- .Setup(x => x.GetStateAsync(
- hubId,
- It.IsAny()))
- .ReturnsAsync(CreateHubState(hubId));
-
- var cut = RenderPkceForm(
- hubId,
- "alice",
- "secret",
- UAuthSubmitMode.TryOnly);
+ ConfigureResult("""{"isSuccess":false,"reason":0,"remainingAttempts":2}""");
+ IUAuthTryResult? received = null;
+ var cut = Render(p => p
+ .Add(x => x.SubmitMode, UAuthSubmitMode.TryOnly)
+ .Add(x => x.OnTryResult, result => received = result));
- var act = () => cut.Instance.SubmitAsync();
+ await cut.InvokeAsync(() => cut.Instance.SubmitAsync());
- await act.Should()
- .ThrowAsync()
- .WithMessage("Missing PKCE credentials.");
+ received.Should().BeOfType();
+ ((TryLoginResult)received!).IsSuccess.Should().BeFalse();
}
- // -----------------------------------------------------------------
- // PKCE / TryOnly
- // -----------------------------------------------------------------
-
[Fact]
- public async Task Pkce_TryOnly_BuildsExpectedRequest()
+ public async Task Rate_Limit_Is_Converted_To_Try_Result()
{
- var hubId = HubSessionId.New();
-
- SetupPkce(
- hubId,
- authorizationCode: "auth-code",
- codeVerifier: "verifier",
- returnUrl: "/dashboard");
-
- PkceCompleteRequest? captured = null;
-
- var result = new TryPkceLoginResult
- {
- IsSuccess = true
- };
-
- _client
- .Setup(x => x.Flows.TryCompletePkceLoginAsync(
- It.IsAny(),
- UAuthSubmitMode.TryOnly))
- .Callback(
- (request, _) => captured = request)
- .ReturnsAsync(result);
-
- var cut = RenderPkceForm(
- hubId,
- "alice@example.com",
- "secret-123",
- UAuthSubmitMode.TryOnly);
-
- await cut.Instance.SubmitAsync();
-
- captured.Should().NotBeNull();
-
- captured!.Identifier.Should().Be("alice@example.com");
- captured.Secret.Should().Be("secret-123");
+ ConfigureResult("""{"status":429,"isSuccess":false,"errorCode":"rate_limited"}""");
+ IUAuthTryResult? received = null;
+ var cut = Render(p => p
+ .Add(x => x.SubmitMode, UAuthSubmitMode.TryOnly)
+ .Add(x => x.OnTryResult, result => received = result));
- captured.AuthorizationCode.Should().Be("auth-code");
- captured.CodeVerifier.Should().Be("verifier");
+ await cut.InvokeAsync(() => cut.Instance.SubmitAsync());
- captured.ReturnUrl.Should().Be("/dashboard");
- captured.HubSessionId.Should().Be(hubId.Value);
+ received.Should().BeOfType();
+ ((TryLoginResult)received!).Status.Should().Be(429);
}
[Fact]
- public async Task Pkce_TryOnly_EmitsTryResult()
+ public async Task Submitting_Changed_Is_Raised_And_Reset_After_Success()
{
- var hubId = HubSessionId.New();
-
- SetupPkce(hubId);
-
- var result = CreateTryPkceLoginResult();
-
- _client
- .Setup(x => x.Flows.TryCompletePkceLoginAsync(
- It.IsAny(),
- UAuthSubmitMode.TryOnly))
- .ReturnsAsync(result);
-
- IUAuthTryResult? emitted = null;
-
+ ConfigureResult("""{"isSuccess":true}""");
+ var states = new List();
var cut = Render(p => p
- .Add(x => x.LoginType, UAuthLoginType.Pkce)
- .Add(x => x.HubSessionId, hubId)
- .Add(x => x.Identifier, "alice")
- .Add(x => x.Secret, "secret")
- .Add(x => x.SubmitMode, UAuthSubmitMode.TryOnly)
- .Add(x => x.OnTryResult,
- r => emitted = r));
+ .Add(x => x.OnSubmittingChanged, state => states.Add(state)));
- await cut.Instance.SubmitAsync();
+ await cut.InvokeAsync(() => cut.Instance.SubmitAsync());
- emitted.Should().BeSameAs(result);
+ states.Should().Equal(true, false);
+ cut.Instance.IsSubmitting.Should().BeFalse();
}
- // -----------------------------------------------------------------
- // PKCE / TryAndCommit
- // -----------------------------------------------------------------
-
[Fact]
- public async Task Pkce_TryAndCommit_UsesTryAndCommitMode()
+ public async Task Submitting_Changed_Is_Reset_After_Interop_Failure()
{
- var hubId = HubSessionId.New();
-
- SetupPkce(hubId);
+ JSInterop
+ .Setup(
+ "uauth.submitLoginForm",
+ _ => true)
+ .SetException(new JSException("submit failed"));
- var result = new TryPkceLoginResult
- {
- IsSuccess = true
- };
-
- _client
- .Setup(x => x.Flows.TryCompletePkceLoginAsync(
- It.IsAny(),
- UAuthSubmitMode.TryAndCommit))
- .ReturnsAsync(result);
-
- var cut = RenderPkceForm(
- hubId,
- "alice",
- "secret",
- UAuthSubmitMode.TryAndCommit);
-
- await cut.Instance.SubmitAsync();
-
- _client.Verify(
- x => x.Flows.TryCompletePkceLoginAsync(
- It.Is(r =>
- r.Identifier == "alice" &&
- r.Secret == "secret" &&
- r.HubSessionId == hubId.Value),
- UAuthSubmitMode.TryAndCommit),
- Times.Once);
- }
+ var states = new List();
- // -----------------------------------------------------------------
- // PKCE / DirectCommit
- // -----------------------------------------------------------------
+ var cut = Render(p => p
+ .Add(x => x.OnSubmittingChanged, state => states.Add(state)));
- [Fact]
- public async Task Pkce_DirectCommit_CompletesPkceDirectly()
- {
- var hubId = HubSessionId.New();
+ var act = () => cut.InvokeAsync(() => cut.Instance.SubmitAsync());
- SetupPkce(hubId);
+ await act.Should()
+ .ThrowAsync()
+ .WithMessage("submit failed");
- _flows
- .Setup(x => x.CompletePkceLoginAsync(
- It.IsAny()))
- .Returns(Task.CompletedTask);
+ states.Should().Equal(true, false);
+ cut.Instance.IsSubmitting.Should().BeFalse();
- var cut = Render(p => p
- .Add(x => x.LoginType, UAuthLoginType.Pkce)
- .Add(x => x.HubSessionId, hubId)
- .Add(x => x.Identifier, "alice")
- .Add(x => x.Secret, "secret")
- .Add(x => x.ReturnUrl, "/home")
- .Add(x => x.SubmitMode, UAuthSubmitMode.DirectCommit));
-
- await cut.Instance.SubmitAsync();
-
- _flows.Verify(
- x => x.CompletePkceLoginAsync(
- It.Is(r =>
- r.Identifier == "alice" &&
- r.Secret == "secret" &&
- r.ReturnUrl == "/home")),
- Times.Once);
+ JSInterop.VerifyInvoke("uauth.submitLoginForm", 1);
}
[Fact]
- public async Task Pkce_DirectCommit_DoesNotUseNativeFormSubmission()
+ public async Task Invalid_Interop_Response_Is_Rejected()
{
- var hubId = HubSessionId.New();
-
- SetupPkce(hubId);
-
- _flows
- .Setup(x => x.CompletePkceLoginAsync(It.IsAny()))
- .Returns(Task.CompletedTask);
-
- var cut = RenderPkceForm(
- hubId,
- "alice",
- "secret",
- UAuthSubmitMode.DirectCommit);
-
- await cut.Instance.SubmitAsync();
-
- JSInterop.Invocations
- .Should()
- .NotContain(x => x.Identifier == "uauth.submitForm");
+ ConfigureResult("null");
+ var cut = RenderForm();
+ var act = () => cut.InvokeAsync(() => cut.Instance.SubmitAsync());
+ await act.Should().ThrowAsync();
}
[Fact]
- public async Task Pkce_DirectCommit_DoesNotEmitTryResult()
+ public async Task Pkce_Submit_Uses_Pkce_Interop_Options_And_Result()
{
var hubId = HubSessionId.New();
-
- SetupPkce(hubId);
-
- _flows
- .Setup(x => x.CompletePkceLoginAsync(It.IsAny()))
- .Returns(Task.CompletedTask);
-
- var callbackCount = 0;
-
+ _capabilities.SetupGet(x => x.SupportsPkce).Returns(true);
+ _credentials.Setup(x => x.ResolveAsync(hubId, It.IsAny()))
+ .ReturnsAsync(new HubCredentials { AuthorizationCode = "auth-code", CodeVerifier = "verifier" });
+ ConfigureResult("""{"isSuccess":true}""");
+ IUAuthTryResult? received = null;
var cut = Render(p => p
.Add(x => x.LoginType, UAuthLoginType.Pkce)
.Add(x => x.HubSessionId, hubId)
- .Add(x => x.Identifier, "alice")
- .Add(x => x.Secret, "secret")
- .Add(x => x.SubmitMode, UAuthSubmitMode.DirectCommit)
- .Add(x => x.OnTryResult,
- _ => callbackCount++));
+ .Add(x => x.SubmitMode, UAuthSubmitMode.TryOnly)
+ .Add(x => x.OnTryResult, result => received = result));
- await cut.Instance.SubmitAsync();
+ await cut.InvokeAsync(() => cut.Instance.SubmitAsync());
- callbackCount.Should().Be(0);
+ received.Should().BeOfType();
+ var options = JsonSerializer.Serialize(JSInterop.Invocations.Single(x => x.Identifier == "uauth.submitLoginForm").Arguments[1]);
+ options.Should().Contain("\"isPkce\":true");
+ options.Should().Contain(Uri.EscapeDataString(hubId.Value));
+ options.Should().NotContain("auth-code").And.NotContain("verifier");
}
- // -----------------------------------------------------------------
- // Hub resolution
- // -----------------------------------------------------------------
-
[Fact]
- public async Task Pkce_HubSessionIdFromQuery_IsUsed()
+ public async Task Pkce_Without_Resolved_Credentials_Cannot_Submit()
{
- var hubId = HubSessionId.New();
-
- SetupPkce(hubId);
-
- var nav = Services
- .GetRequiredService();
-
- nav.NavigateTo(
- $"/login?{UAuthConstants.Query.Hub}={Uri.EscapeDataString(hubId.Value)}");
-
- var result = CreateTryPkceLoginResult();
-
- _client
- .Setup(x => x.Flows.TryCompletePkceLoginAsync(
- It.IsAny(),
- UAuthSubmitMode.TryOnly))
- .ReturnsAsync(result);
-
+ _capabilities.SetupGet(x => x.SupportsPkce).Returns(true);
var cut = Render(p => p
.Add(x => x.LoginType, UAuthLoginType.Pkce)
- .Add(x => x.Identifier, "alice")
- .Add(x => x.Secret, "secret")
- .Add(x => x.SubmitMode, UAuthSubmitMode.TryOnly));
-
- await cut.Instance.SubmitAsync();
-
- _credentialResolver.Verify(
- x => x.ResolveAsync(
- hubId,
- It.IsAny()),
- Times.Once);
- }
-
- // -----------------------------------------------------------------
- // Helpers
- // -----------------------------------------------------------------
-
- private IRenderedComponent RenderPasswordForm(
- string? identifier,
- string? secret,
- UAuthSubmitMode mode)
- {
- return Render(p => p
- .Add(x => x.Identifier, identifier)
- .Add(x => x.Secret, secret)
- .Add(x => x.LoginType, UAuthLoginType.Password)
- .Add(x => x.SubmitMode, mode));
- }
-
- private IRenderedComponent RenderPkceForm(
- HubSessionId hubId,
- string identifier,
- string secret,
- UAuthSubmitMode mode)
- {
- return Render(p => p
- .Add(x => x.Identifier, identifier)
- .Add(x => x.Secret, secret)
- .Add(x => x.LoginType, UAuthLoginType.Pkce)
- .Add(x => x.HubSessionId, hubId)
- .Add(x => x.SubmitMode, mode));
- }
+ .Add(x => x.HubSessionId, HubSessionId.New()));
- private void SetupPkce(
- HubSessionId hubId,
- string authorizationCode = "authorization-code",
- string codeVerifier = "code-verifier",
- string returnUrl = "/home")
- {
- var credentials = new HubCredentials
- {
- AuthorizationCode = authorizationCode,
- CodeVerifier = codeVerifier
- };
-
- _credentialResolver
- .Setup(x => x.ResolveAsync(
- hubId,
- It.IsAny()))
- .ReturnsAsync(credentials);
-
- _hubFlowReader
- .Setup(x => x.GetStateAsync(
- hubId,
- It.IsAny()))
- .ReturnsAsync(
- CreateHubState(
- hubId,
- returnUrl));
- }
-
- private static HubFlowState CreateHubState(HubSessionId hubId, string returnUrl = "/home")
- {
- return new HubFlowState
- {
- HubSessionId = hubId,
- Exists = true,
- IsActive = true,
- IsExpired = false,
- IsCompleted = false,
- ReturnUrl = returnUrl
- };
- }
-
- private static TryLoginResult CreateTryLoginResult()
- {
- return new TryLoginResult
- {
- IsSuccess = true
- };
- }
-
- private static TryPkceLoginResult CreateTryPkceLoginResult()
- {
- return new TryPkceLoginResult
- {
- IsSuccess = true
- };
+ var act = () => cut.InvokeAsync(() => cut.Instance.SubmitAsync());
+ await act.Should().ThrowAsync()
+ .WithMessage("Missing PKCE credentials.");
+ JSInterop.Invocations.Should().NotContain(x => x.Identifier == "uauth.submitLoginForm");
}
}
diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/Blazor/UAuthLoginFormTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/Blazor/UAuthLoginFormTests.cs
index 4d17c72d..3f467d3a 100644
--- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/Blazor/UAuthLoginFormTests.cs
+++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/Blazor/UAuthLoginFormTests.cs
@@ -1,18 +1,15 @@
using Bunit;
using CodeBeam.UltimateAuth.Client;
using CodeBeam.UltimateAuth.Client.Blazor;
-using CodeBeam.UltimateAuth.Client.Device;
using CodeBeam.UltimateAuth.Client.Options;
using CodeBeam.UltimateAuth.Core.Abstractions;
using CodeBeam.UltimateAuth.Core.Contracts;
using CodeBeam.UltimateAuth.Core.Defaults;
using CodeBeam.UltimateAuth.Core.Domain;
-using CodeBeam.UltimateAuth.Core.Options;
using AwesomeAssertions;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
-using Microsoft.JSInterop;
using Moq;
namespace CodeBeam.UltimateAuth.Tests.Unit.Client.Blazor;
@@ -61,42 +58,55 @@ public void Render_RendersForm()
}
[Fact]
- public void Render_RendersIdentifierHiddenInput()
+ public void Render_RendersIdentifierInput()
{
- var cut = RenderLoginForm(
- identifier: "alice@example.com");
+ var cut = RenderLoginForm();
var input = cut.Find("input[name='Identifier']");
+ input.GetAttribute("type")
+ .Should()
+ .Be("text");
+
+ input.HasAttribute("hidden")
+ .Should()
+ .BeFalse();
+
input.GetAttribute("value")
.Should()
- .Be("alice@example.com");
+ .BeNullOrEmpty();
}
[Fact]
- public void Render_RendersSecretHiddenInput()
+ public void Render_RendersSecretInput()
{
- var cut = RenderLoginForm(
- secret: "super-secret");
+ var cut = RenderLoginForm();
var input = cut.Find("input[name='Secret']");
+ input.GetAttribute("type")
+ .Should()
+ .Be("password");
+
+ input.HasAttribute("hidden")
+ .Should()
+ .BeFalse();
+
input.GetAttribute("value")
.Should()
- .Be("super-secret");
+ .BeNullOrEmpty();
}
[Fact]
- public void Render_SecretInput_DisablesAutocomplete()
+ public void Render_SecretInput_UsesCurrentPasswordAutocomplete()
{
- var cut = RenderLoginForm(
- secret: "secret");
+ var cut = RenderLoginForm();
var input = cut.Find("input[name='Secret']");
input.GetAttribute("autocomplete")
.Should()
- .Be("off");
+ .Be("current-password");
}
// -----------------------------------------------------------------
@@ -143,17 +153,19 @@ public void DirectCommit_RendersReturnUrlUsingFormContractName()
[Theory]
[InlineData(UAuthSubmitMode.TryOnly)]
[InlineData(UAuthSubmitMode.TryAndCommit)]
- public void NonDirectCommit_DoesNotRenderReturnUrlFormField(
- UAuthSubmitMode submitMode)
+ [InlineData(UAuthSubmitMode.DirectCommit)]
+ public void AllSubmitModes_RenderReturnUrlFormField(UAuthSubmitMode submitMode)
{
var cut = RenderLoginForm(
returnUrl: "/home",
submitMode: submitMode);
- cut.FindAll(
- $"input[name='{UAuthConstants.Form.ReturnUrl}']")
+ var input = cut.Find(
+ $"input[name='{UAuthConstants.Form.ReturnUrl}']");
+
+ input.GetAttribute("value")
.Should()
- .BeEmpty();
+ .Be("/home");
}
// -----------------------------------------------------------------
@@ -192,10 +204,13 @@ public void PasswordLogin_DoesNotRenderCodeVerifier()
public void AllowEnterKeyToSubmitTrue_RendersHiddenSubmitButton()
{
var cut = RenderLoginForm(
- allowEnterKeyToSubmit: true);
+ allowEnterKeyToSubmit: true,
+ childContent: builder =>
+ builder.AddMarkupContent(
+ 0,
+ " "));
- var buttons = cut.FindAll(
- "button[type='submit']");
+ var buttons = cut.FindAll("button[type='submit']");
buttons.Should().ContainSingle();
@@ -209,10 +224,13 @@ public void AllowEnterKeyToSubmitTrue_RendersHiddenSubmitButton()
public void AllowEnterKeyToSubmitFalse_DoesNotRenderSubmitButton()
{
var cut = RenderLoginForm(
- allowEnterKeyToSubmit: false);
+ allowEnterKeyToSubmit: false,
+ childContent: builder =>
+ builder.AddMarkupContent(
+ 0,
+ " "));
- cut.FindAll(
- "button[type='submit']")
+ cut.FindAll("button[type='submit']")
.Should()
.BeEmpty();
}
@@ -649,13 +667,26 @@ public void PkceEndpoint_WithHub_UsesUAuthHubQueryContract()
.Contain($"{UAuthConstants.Query.Hub}=");
}
+ [Fact]
+ public void Render_WithChildContent_DoesNotRenderDefaultCredentialInputs()
+ {
+ var cut = RenderLoginForm(
+ childContent: builder =>
+ builder.AddMarkupContent(
+ 0,
+ "Custom login
"));
+
+ cut.Find("#custom-login").Should().NotBeNull();
+
+ cut.FindAll("input[name='Identifier']").Should().BeEmpty();
+ cut.FindAll("input[name='Secret']").Should().BeEmpty();
+ }
+
// -----------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------
private IRenderedComponent RenderLoginForm(
- string? identifier = "alice",
- string? secret = "secret",
string? endpoint = null,
string? returnUrl = null,
UAuthLoginType loginType = UAuthLoginType.Password,
@@ -666,13 +697,9 @@ private IRenderedComponent RenderLoginForm(
return Render(parameters =>
{
parameters
- .Add(x => x.Identifier, identifier)
- .Add(x => x.Secret, secret)
.Add(x => x.LoginType, loginType)
.Add(x => x.SubmitMode, submitMode)
- .Add(
- x => x.AllowEnterKeyToSubmit,
- allowEnterKeyToSubmit);
+ .Add(x => x.AllowEnterKeyToSubmit, allowEnterKeyToSubmit);
if (endpoint is not null)
parameters.Add(x => x.Endpoint, endpoint);
@@ -681,19 +708,17 @@ private IRenderedComponent RenderLoginForm(
parameters.Add(x => x.ReturnUrl, returnUrl);
if (childContent is not null)
- parameters.Add(
- x => x.ChildContent,
- childContent);
+ parameters.Add(x => x.ChildContent, childContent);
});
}
private void SetupPkceHub(
- HubSessionId hubId,
- string authorizationCode = "authorization-code",
- string codeVerifier = "code-verifier",
- string? returnUrl = "/home",
- bool exists = true,
- bool active = true)
+ HubSessionId hubId,
+ string authorizationCode = "authorization-code",
+ string codeVerifier = "code-verifier",
+ string? returnUrl = "/home",
+ bool exists = true,
+ bool active = true)
{
var credentials = new HubCredentials
{
@@ -730,8 +755,6 @@ private IRenderedComponent RenderPkceLoginForm(
return Render(parameters =>
{
parameters
- .Add(x => x.Identifier, "alice")
- .Add(x => x.Secret, "secret")
.Add(x => x.HubSessionId, hubId)
.Add(x => x.LoginType, UAuthLoginType.Pkce)
.Add(x => x.SubmitMode, submitMode);
diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/javascript/uauth.test.cjs b/tests/CodeBeam.UltimateAuth.Tests.Unit/javascript/uauth.test.cjs
index d3582c73..c5817c2c 100644
--- a/tests/CodeBeam.UltimateAuth.Tests.Unit/javascript/uauth.test.cjs
+++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/javascript/uauth.test.cjs
@@ -33,7 +33,12 @@ function createEnvironment(fetchImpl = async () => ({
};
const document = {
- baseURI: "https://localhost:6130/"
+ baseURI: "https://localhost:6130/",
+ createElement: () => ({
+ type: "",
+ name: "",
+ value: ""
+ })
};
const context = vm.createContext({
@@ -45,7 +50,9 @@ function createEnvironment(fetchImpl = async () => ({
AbortController,
Map,
Error,
- TypeError
+ TypeError,
+ HTMLFormElement: TestForm,
+ FormData: TestFormData
});
vm.runInContext(script, context);
@@ -664,3 +671,151 @@ test("authenticated logout uses the same Device ID for CSRF token and POST", asy
assert.equal(logoutDeviceId, deviceId);
assert.equal(tokenDeviceId, deviceId);
});
+
+class TestForm {
+ constructor(fields = []) {
+ this.elements = fields.map(field => ({
+ ...field,
+ disabled: field.disabled ?? false
+ }));
+ this.action = "";
+ this.submitCount = 0;
+ }
+
+ appendChild(input) {
+ this.elements.push(input);
+ }
+
+ querySelector(selector) {
+ const match = selector.match(/^input\[name='(.+)'\]$/);
+ return match
+ ? this.elements.find(x => x.name === match[1]) ?? null
+ : null;
+ }
+
+ submit() {
+ this.submitCount++;
+ }
+}
+
+class TestFormData {
+ constructor(form) {
+ this.fields = form.elements.filter(x => !x.disabled && x.name);
+ }
+
+ getAll(name) {
+ return this.fields
+ .filter(x => x.name === name)
+ .map(x => x.value);
+ }
+
+ get(name) {
+ return this.getAll(name)[0] ?? null;
+ }
+}
+
+function createLoginEnvironment() {
+ const uauth = createEnvironment();
+
+// The VM context used by createEnvironment needs these constructors.
+// Expose them in createEnvironment's vm.createContext object.
+ return uauth;
+}
+
+test("submitLoginForm TryOnly reads native credentials without committing", async () => {
+ const uauth = createEnvironment();
+
+ const form = new TestForm([
+ { name: "Identifier", value: "alice" },
+ { name: "Secret", value: "password123" }
+ ]);
+
+ let captured = null;
+
+ uauth.postJson = async options => {
+ captured = options;
+ return {
+ status: 200,
+ body: { isSuccess: true }
+ };
+ };
+
+ const result = await uauth.submitLoginForm(form, {
+ mode: "TryOnly",
+ tryUrl: "/auth/try-login",
+ commitUrl: "/auth/login",
+ clientProfile: "BlazorServer",
+ trustedOrigin: null,
+ isPkce: false
+ });
+
+ assert.equal(result.isSuccess, true);
+ assert.equal(captured.payload.Identifier, "alice");
+ assert.equal(captured.payload.Secret, "password123");
+ assert.equal(form.submitCount, 0);
+});
+
+test("submitLoginForm rejects disabled credential fields", async () => {
+ const uauth = createEnvironment();
+
+ const form = new TestForm([
+ { name: "Identifier", value: "alice", disabled: true },
+ { name: "Secret", value: "password123", disabled: true }
+ ]);
+
+ await assert.rejects(
+ uauth.submitLoginForm(form, {
+ mode: "TryAndCommit",
+ tryUrl: "/auth/try-login",
+ commitUrl: "/auth/login",
+ clientProfile: "BlazorServer",
+ trustedOrigin: null,
+ isPkce: false
+ }),
+ /exactly one Identifier and one Secret/
+ );
+
+ assert.equal(form.submitCount, 0);
+});
+
+test("submitLoginForm TryAndCommit reuses original form", async () => {
+ const uauth = createEnvironment();
+
+ const form = new TestForm([
+ { name: "Identifier", value: "alice" },
+ { name: "Secret", value: "password123" }
+ ]);
+
+ uauth.postJson = async () => ({
+ status: 200,
+ body: {
+ isSuccess: true,
+ previewReceipt: "receipt-123"
+ }
+ });
+
+ uauth.antiforgery.appendToForm = async () => { };
+
+ const result = await uauth.submitLoginForm(form, {
+ mode: "TryAndCommit",
+ tryUrl: "/auth/try-login",
+ commitUrl: "/auth/login",
+ clientProfile: "BlazorServer",
+ trustedOrigin: null,
+ isPkce: false
+ });
+
+ assert.equal(result.isSuccess, true);
+ assert.equal(form.submitCount, 1);
+ assert.equal(form.action, "https://localhost:6130/auth/login");
+
+ assert.equal(
+ form.elements.find(x => x.name === "PreviewReceipt")?.value,
+ "receipt-123"
+ );
+
+ assert.equal(
+ form.elements.filter(x => x.name === "Secret").length,
+ 1
+ );
+});