diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Program.cs b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Program.cs index efd4b905..50a134c8 100644 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Program.cs +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Program.cs @@ -1,3 +1,4 @@ +using CodeBeam.UltimateAuth.Client; using CodeBeam.UltimateAuth.Client.Blazor; using CodeBeam.UltimateAuth.Client.Blazor.Extensions; using CodeBeam.UltimateAuth.Core.Abstractions; @@ -13,11 +14,9 @@ using CodeBeam.UltimateAuth.IdentityLab.Policy; using CodeBeam.UltimateAuth.IdentityLab.Seed; using CodeBeam.UltimateAuth.IdentityLab.Services; -using CodeBeam.UltimateAuth.Sample.Seed.Extensions; using CodeBeam.UltimateAuth.Sample.Shared.Infrastructure; using CodeBeam.UltimateAuth.Server.Extensions; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; using MudBlazor.Services; using MudExtensions.Services; @@ -72,6 +71,7 @@ { //o.AutoRefresh.Interval = TimeSpan.FromSeconds(5); o.Reauth.Behavior = ReauthBehavior.RaiseEvent; + o.Resource.Credentials = UAuthResourceCredentialsMode.SameOrigin; }); @@ -95,7 +95,6 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Services/LabApiClient.cs b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Services/LabApiClient.cs deleted file mode 100644 index 4ccb22cd..00000000 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Services/LabApiClient.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Microsoft.JSInterop; - -namespace CodeBeam.UltimateAuth.IdentityLab.Services; - -public sealed class LabApiClient(IJSRuntime js) : IAsyncDisposable -{ - private IJSObjectReference? _module; - - private async Task GetModuleAsync() - { - return _module ??= await js.InvokeAsync( - "import", - "/js/lab-api.js"); - } - - public async Task> GetAsync( - string url, - CancellationToken ct = default) - { - var module = await GetModuleAsync(); - - return await module.InvokeAsync>( - "request", - ct, - "GET", - url, - null); - } - - public async ValueTask DisposeAsync() - { - if (_module is not null) - await _module.DisposeAsync(); - } -} - -public sealed class LabApiResponse -{ - public bool Ok { get; set; } - public int Status { get; set; } - public T? Body { get; set; } -} diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/wwwroot/js/lab-api.js b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/wwwroot/js/lab-api.js deleted file mode 100644 index 4c2f124b..00000000 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/wwwroot/js/lab-api.js +++ /dev/null @@ -1,35 +0,0 @@ -export async function request(method, url, payload = null) { - const deviceId = window.uauth?.deviceId; - - if (!deviceId) { - throw new Error("UltimateAuth device is not initialized."); - } - - const headers = { - "X-UDID": deviceId, - "X-Requested-With": "UAuth" - }; - - const options = { - method, - credentials: "same-origin", - headers - }; - - if (payload !== null) { - headers["Content-Type"] = "application/json"; - options.body = JSON.stringify(payload); - } - - const response = await fetch(url, options); - - const body = response.status === 204 - ? null - : await response.json().catch(() => null); - - return { - ok: response.ok, - status: response.status, - body - }; -} diff --git a/docs/content/client-sdk/overview.md b/docs/content/client-sdk/overview.md index 28441579..11744a08 100644 --- a/docs/content/client-sdk/overview.md +++ b/docs/content/client-sdk/overview.md @@ -27,6 +27,7 @@ It brings together several responsibilities: - **Authorization management** — Work with roles, permissions, and other exposed authorization operations. - **Structured results** — Receive operation results through the SDK's response contracts. - **Client state integration** — Coordinate supported authentication state changes and notifications. +- **Resource API requests** — Access protected application APIs through unified HTTP operations and structured results. The SDK helps applications interact with UltimateAuth consistently while keeping the server responsible for authentication and authorization enforcement. @@ -44,7 +45,7 @@ Conceptually: Application | v -UAuthClient +IUAuthClient | +-- Flows | Login, logout, refresh, PKCE @@ -62,7 +63,10 @@ UAuthClient | Credential management and recovery | +-- Authorization - Roles, permissions, and access management + | Roles, permissions, and access management + | + +-- Resource + Authenticated application API requests ``` These represent functional responsibilities. The precise available members and operations depend on the installed packages and supported SDK contracts. @@ -204,22 +208,32 @@ Applications should continue to treat the server as authoritative wherever the s ## Calling Protected Resource APIs -Authentication is often only the first step. +Authentication is only the beginning of an application. + +After signing in, users still need to retrieve data, create records, and interact with protected application resources. + +Without a unified integration, developers often configure authentication through one SDK and then build a separate HTTP abstraction for their application APIs. -Applications also need to call protected business APIs after the user has authenticated. +UltimateAuth addresses this through `IUAuthClient.Resource`. -UltimateAuth supports integration with protected resource requests, allowing the client application to use the authentication mechanisms appropriate to its configured environment. +```csharp +var result = await UAuthClient.Resource.GetAsync>("/api/products"); + +if (result.IsSuccess && result.Value is not null) +{ + products = result.Value; +} +``` -A protected resource request involves two distinct responsibilities: +Resource requests use the same client entry point and structured result model as other UltimateAuth operations. -1. The client sends the request with the appropriate authentication material. -2. The receiving server validates authentication and enforces authorization. +The Blazor integration provides browser-based transport, configurable resource origins, credential handling, and request cancellation without requiring application-specific JavaScript wrappers. -The Client SDK can help with the client-side responsibility. +The Resource Client supports GET, POST, PUT, PATCH, and DELETE operations. -It cannot grant access to an operation that the receiving server does not authorize. +It does not replace server-side authentication or authorization. The receiving Resource API remains responsible for validating requests and enforcing access policies. -When building applications with separate resource APIs, developers must configure the relevant API integration and server-side authentication enforcement. +For configuration, examples, security considerations, and supported behavior, see [Resource API Requests](/docs/client-sdk/resource-api-requests). ## Client SDK Guides @@ -233,7 +247,7 @@ The following guides cover the major client responsibilities with practical C# e | **Identifiers** | Manage email addresses, usernames, phone numbers, and verification workflows | | **Credentials** | Add, change, revoke, and recover credentials | | **Authorization** | Work with roles, permissions, and exposed authorization management operations | -| **Resource API Requests** | Call protected application APIs using the appropriate authentication integration | +| [Resource API Requests](/docs/client-sdk/resource-api-requests) | Call protected application APIs through the unified Resource Client | Each guide focuses on a specific developer task and includes the relevant SDK methods, request models, response handling, and implementation considerations. diff --git a/docs/content/client-sdk/resource-api-requests.md b/docs/content/client-sdk/resource-api-requests.md new file mode 100644 index 00000000..b9501058 --- /dev/null +++ b/docs/content/client-sdk/resource-api-requests.md @@ -0,0 +1,884 @@ +--- +title: Resource API Requests +order: 8 +group: client +--- + +# Resource API Requests + +**Authenticate once. Access your application resources through the same unified client.** + +Authentication is only the beginning of an application. + +Once a user signs in, the application still needs to retrieve data, create records, update resources, and communicate with protected APIs. + +Traditionally, this introduces another layer of integration work. + +Developers configure authentication through one library, then build a separate HTTP client to access their application APIs. They must handle credentials, browser restrictions, response parsing, errors, and differences between hosting environments. + +The result is often two separate programming models: one for authentication and another for everything that comes after it. + +**UltimateAuth brings these operations together through `IUAuthClient.Resource`.** + +Instead of introducing another client abstraction, applications can use the same UltimateAuth entry point for authentication, identity management, and protected resource requests. + +```csharp +var result = await UAuthClient.Resource + .GetAsync>("/api/products"); +``` + +No custom JavaScript wrapper. No application-specific response type. No separate HTTP client configuration for ordinary authenticated resource requests. + +The goal is straightforward: **make accessing protected application resources feel like a natural continuation of authentication.** + +--- + +## 1. The Problem: Authentication Ends, Integration Work Begins + +Consider a typical application with three components: + +```text +Blazor Application + | + +----> UAuthHub + | Authentication + | Sessions + | Identity + | + +----> Resource API + Products + Orders + Application Data +``` + +UltimateAuth handles authentication and identity operations. + +But the application also needs to communicate with its Resource API. + +Without a unified resource client, developers typically need to: + +1. Configure an additional HTTP client. +2. Determine how authentication credentials should accompany requests. +3. Handle browser credential policies and cross-origin restrictions. +4. Maintain device and client profile metadata where applicable. +5. Deserialize successful responses. +6. Translate unsuccessful HTTP responses into application-friendly results. +7. Implement similar request handling across multiple components. + +These responsibilities are not necessarily complicated individually. + +The problem is their repetition. + +A framework that simplifies authentication should not require developers to rebuild the surrounding integration layer every time they need application data. + +### A Unified Client Model + +UltimateAuth already exposes specialized operations through `IUAuthClient`: + +```csharp +UAuthClient.Flows +UAuthClient.Users +UAuthClient.Sessions +UAuthClient.Credentials +UAuthClient.Authorization +``` + +Resource requests follow the same model: + +```csharp +UAuthClient.Resource +``` + +This preserves a consistent entry point without mixing application-specific business operations into UltimateAuth's authentication services. + +UltimateAuth does not need to know what a `Product`, `Order`, or `Invoice` represents. + +It only needs to provide a predictable way to communicate with the API that owns those resources. + +--- + +## 2. Getting Started + +The Resource API client is available through the UltimateAuth Client SDK. + +For Blazor applications, register the Blazor client integration: + +```csharp +builder.Services.AddUltimateAuthClientBlazor(); +``` + +The registration includes the shared client services and the browser-specific resource transport. + +You do not need to register a separate Resource Client. + +### Inject IUAuthClient + +In a Blazor component: + +```razor +@using CodeBeam.UltimateAuth.Client +@inject IUAuthClient UAuthClient +``` + +Then call a protected application endpoint: + +```csharp +var result = await UAuthClient.Resource.GetAsync>("/api/products"); + +if (result.IsSuccess && result.Value is not null) +{ + products = result.Value; +} +``` + +The response uses the same structured result model found throughout UltimateAuth. + +### What Happens Behind the Call? + +For the Blazor integration, the request follows this path: + +```text +Blazor Component + | + v +IUAuthClient.Resource + | + v +Resource Client + | + v +UltimateAuth Browser Transport + | + v +Protected Resource API + | + v +UAuthResult +``` + +The browser transport handles request construction and browser-specific behavior. + +The Resource API remains responsible for validating authentication and enforcing authorization. + +**A successful client request does not bypass server-side security.** + +--- + +## 3. Supported HTTP Operations + +The Resource Client provides five standard HTTP operations. + +| Method | Purpose | Return type | +|---|---|---| +| `GetAsync` | Retrieve a resource | `UAuthResult` | +| `PostAsync` | Create or submit a resource | `UAuthResult` | +| `PutAsync` | Replace or update a resource | `UAuthResult` | +| `PatchAsync` | Partially update a resource | `UAuthResult` | +| `DeleteAsync` | Delete a resource | `UAuthResult` | + +All methods accept a relative endpoint and support cancellation. + +### GET + +Retrieve application data: + +```csharp +var result = await UAuthClient.Resource.GetAsync>("/api/products"); + +if (result.IsSuccess && result.Value is not null) +{ + products = result.Value; +} +``` + +Retrieve a single resource: + +```csharp +var result = await UAuthClient.Resource.GetAsync($"/api/products/{productId}"); +``` + +### POST + +Submit a new resource: + +```csharp +var product = new Product +{ + Name = "Example Product", + Price = 29.99m +}; + +var result = await UAuthClient.Resource.PostAsync("/api/products", product); + +if (result.IsSuccess) +{ + // Product created successfully. +} +``` + +The request payload is serialized for transport. + +### PUT + +Update a resource: + +```csharp +var result = await UAuthClient.Resource + .PutAsync($"/api/products/{product.Id}", product); +``` + +### PATCH + +Submit a partial update: + +```csharp +var changes = new +{ + Price = 24.99m +}; + +var result = await UAuthClient.Resource + .PatchAsync($"/api/products/{productId}", changes); +``` + +The Resource Client sends the request payload. + +The receiving API determines which partial-update formats and operations it supports. + +### DELETE + +Delete a resource: + +```csharp +var result = await UAuthClient.Resource.DeleteAsync($"/api/products/{productId}"); + +if (result.IsSuccess) +{ + // Resource deleted successfully. +} +``` + +Unlike the other methods, `DeleteAsync` returns the non-generic `UAuthResult` because the operation does not require a response value. + +--- + +## 4. Consistent Results with UAuthResult + +A common source of unnecessary complexity is response handling. + +One API client returns `HttpResponseMessage`. + +Another returns a custom response wrapper. + +A third throws exceptions for ordinary HTTP failures. + +Applications then need different handling logic for operations that are conceptually similar. + +UltimateAuth uses `UAuthResult` to provide a consistent response contract. + +```csharp +var result = await UAuthClient.Resource.GetAsync("/api/products/42"); + +if (result.IsSuccess) +{ + var product = result.Value; +} +else +{ + var status = result.Status; + var problem = result.Problem; +} +``` + +### Result Properties + +| Property | Description | +|---|---| +| `IsSuccess` | Indicates whether the HTTP operation succeeded | +| `Status` | HTTP response status code | +| `Value` | Deserialized response value for successful generic operations | +| `Problem` | Structured problem information when available | + +A failed HTTP response does not automatically mean a transport exception. + +For example, an API may return: + +- `400 Bad Request` +- `401 Unauthorized` +- `403 Forbidden` +- `404 Not Found` +- `409 Conflict` + +These responses can be represented through `UAuthResult`. + +The application can inspect the status and decide how to respond. + +### Structured Problems + +When an unsuccessful response contains a compatible UltimateAuth problem payload, the Resource Client attempts to deserialize it into `UAuthProblem`. + +```csharp +if (!result.IsSuccess) +{ + if (result.Problem is not null) + { + // Handle the structured API problem. + } + else + { + // Handle the HTTP status without a structured problem. + } +} +``` + +Not every API returns a compatible problem body. + +Therefore, `Problem` may be `null` even when the request failed. + +### Transport Failures Are Different + +Network failures, protocol errors, and request cancellation are different from ordinary unsuccessful HTTP responses. + +For example, an unreachable API does not necessarily produce an HTTP status code. + +Applications should distinguish transport exceptions from valid HTTP responses indicating failure. + +**`UAuthResult` standardizes HTTP response handling; it does not guarantee that every network or runtime failure becomes a result object.** + +--- + +## 5. Resource API Configuration + +Resource API configuration is part of `UAuthClientOptions`. + +```csharp +builder.Services.AddUltimateAuthClientBlazor(options => +{ + options.Resource.BaseAddress = "https://api.example.com"; + options.Resource.Credentials = UAuthResourceCredentialsMode.Auto; +}); +``` + +The `Resource` options are separate from the authentication endpoint configuration. + +This distinction allows applications to authenticate against one server while accessing resources hosted elsewhere. + +### BaseAddress + +`BaseAddress` identifies the Resource API origin. + +```csharp +options.Resource.BaseAddress = "https://api.example.com"; +``` + +If `BaseAddress` is not configured, requests use the application's current origin. + +```csharp +builder.Services.AddUltimateAuthClientBlazor(); +``` + +A call such as: + +```csharp +await UAuthClient.Resource.GetAsync>("/api/products"); +``` + +then targets the application's origin. + +### BaseAddress Validation + +UltimateAuth validates the configured Resource API address. + +The address must: + +- Be an absolute HTTP or HTTPS URL. +- Use HTTPS, except for loopback development addresses. +- Represent an origin without an additional path. +- Exclude embedded credentials, query strings, and fragments. + +Valid examples: + +```text +https://api.example.com +https://localhost:7001 +``` + +Invalid examples: + +```text +https://api.example.com/api +https://api.example.com?version=1 +http://api.example.com +``` + +Keep endpoint paths in individual Resource Client calls: + +```csharp +options.Resource.BaseAddress = "https://api.example.com"; +``` + +```csharp +await UAuthClient.Resource.GetAsync>("/api/products"); +``` + +This separates deployment configuration from application resource paths. + +--- + +## 6. Browser Credential Handling + +Browser-based applications do not have unrestricted control over authentication credentials. + +Cookies, origins, cross-origin requests, and browser security policies all influence how requests are sent. + +UltimateAuth exposes credential handling through `UAuthResourceCredentialsMode`. + +| Mode | Behavior | +|---|---| +| `Auto` | Let UltimateAuth select the appropriate browser credential policy | +| `SameOrigin` | Send browser credentials only for same-origin requests | +| `Include` | Include browser credentials for the configured Resource API origin | +| `Omit` | Do not send browser credentials | + +The default is: + +```csharp +options.Resource.Credentials = UAuthResourceCredentialsMode.Auto; +``` + +### Same-Origin Resource API + +When the application and Resource API share an origin: + +```text +https://app.example.com +https://app.example.com/api/products +``` + +you can use: + +```csharp +options.Resource.Credentials = UAuthResourceCredentialsMode.SameOrigin; +``` + +This is appropriate when the Resource API relies on browser credentials available to the application origin. + +### Separate Resource API + +Consider: + +```text +Application: https://app.example.com +Resource API: https://api.example.com +``` + +For a cross-origin API that requires browser cookies, configure: + +```csharp +builder.Services.AddUltimateAuthClientBlazor(options => +{ + options.Resource.BaseAddress = "https://api.example.com"; + options.Resource.Credentials = UAuthResourceCredentialsMode.Include; +}); +``` + +The receiving API must also support the relevant browser security requirements. + +For credentialed cross-origin requests, this typically includes: + +- Explicitly permitted origins. +- Appropriate CORS configuration. +- Compatible cookie attributes and browser policies. +- Server-side authentication and authorization. + +Configuring `Include` does not override browser restrictions or guarantee that a cookie is available for the destination. + +### When to Use Omit + +Some resource endpoints do not require browser cookies. + +For those cases: + +```csharp +options.Resource.Credentials = UAuthResourceCredentialsMode.Omit; +``` + +This prevents browser credentials from being included in the resource request. + +The API must still enforce whichever authentication mechanism it requires. + +**Credential transmission is not the same as authorization.** + +--- + +## 7. Separate UAuthHub and Resource API + +UltimateAuth supports architectures where authentication and application resources are hosted independently. + +For example: + +```text + Blazor Application + | + +------------+------------+ + | | + v v + UAuthHub Resource API + auth.example.com api.example.com + | | + v v + Authentication Application Data + Sessions Business Operations + Identity Authorization +``` + +The client can configure both destinations independently: + +```csharp +builder.Services.AddUltimateAuthClientBlazor(options => +{ + options.Endpoints.BasePath = "https://auth.example.com/auth"; + + options.Resource.BaseAddress = "https://api.example.com"; + + options.Resource.Credentials = UAuthResourceCredentialsMode.Auto; +}); +``` + +Authentication operations use the configured authentication endpoints. + +Resource operations use the configured Resource API origin. + +```csharp +var profile = await UAuthClient.Users.GetMeAsync(); + +var products = await UAuthClient.Resource.GetAsync>("/api/products"); +``` + +Both operations are accessed through `IUAuthClient`, but they serve different purposes. + +### Authentication Does Not Automatically Protect an API + +Configuring a Resource API address does not automatically enable authentication or authorization on that API. + +The Resource API must independently validate incoming requests and enforce its access rules. + +For separate deployments, the Resource API must also be configured to accept and validate the authentication material associated with the selected UltimateAuth authentication mode. + +The Resource Client simplifies communication. + +It does not replace the receiving server's security responsibilities. + +--- + +## 8. Device and Client Profile Metadata + +UltimateAuth maintains a distinction between user identity, device identity, and client environment. + +For browser resource requests, the transport can propagate UltimateAuth metadata, including: + +```text +X-UDID +X-UAuth-ClientProfile +X-Requested-With +``` + +This keeps resource requests consistent with the UltimateAuth client environment without requiring every application component to construct these headers manually. + +For example: + +```csharp +await UAuthClient.Resource.GetAsync>("/api/products"); +``` + +does not require the developer to manually retrieve and attach the device identifier. + +### Metadata Is Not Proof of Identity + +Device identifiers and client profile values describe request context. + +They are not independent authentication credentials. + +A receiving API must not grant access solely because a request declares a particular device identifier or client profile. + +The server remains responsible for validating the user's authentication state and permissions. + +For more information, see [Client Profiles](/docs/fundamentals/client-profiles). + +--- + +## 9. Browser Transport and Security + +The Blazor Resource Client uses UltimateAuth's browser transport. + +This allows UltimateAuth to apply consistent request behavior without requiring developers to write a separate JavaScript module for their application. + +### Origin Validation + +Resource requests are constrained by the configured Resource API origin. + +The transport validates request destinations rather than allowing arbitrary URLs to bypass the configured resource boundary. + +This helps prevent application code from unintentionally sending credentials or UltimateAuth request metadata to unrelated destinations. + +### Redirect Protection + +Resource requests use redirect protection. + +A request should not silently follow a redirect to an unexpected destination while carrying authentication-related context. + +Applications should handle unexpected redirects as transport failures rather than assuming the request succeeded. + +### Request Cancellation + +Resource operations support `CancellationToken`. + +```csharp +using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + +var result = await UAuthClient.Resource.GetAsync>("/api/products", cts.Token); +``` + +The Blazor transport coordinates cancellation with the underlying browser request. + +Cancellation is particularly useful when a user navigates away from a page or starts another operation before the previous request completes. + +### Browser Security Still Applies + +The Resource Client does not disable browser security mechanisms. + +Cross-origin requests remain subject to CORS and browser credential policies. + +Likewise, origin validation and redirect protection do not replace: + +- Authentication validation. +- Authorization policies. +- CSRF protection where applicable. +- Input validation. +- Server-side resource ownership checks. + +These responsibilities remain part of the receiving API's security model. + +--- + +## 10. Blazor Server vs. Blazor WebAssembly + +One of UltimateAuth's design goals is to provide a consistent client API across supported application environments. + +However, different hosting models have different execution characteristics. + +### Blazor WebAssembly + +Blazor WebAssembly executes application code in the browser. + +It cannot directly invoke server-side application services or access a server database. + +A typical application architecture is: + +```text +Blazor WebAssembly + | + v +IUAuthClient.Resource + | + v +Resource API + | + v +Application Services + | + v +Database +``` + +The Resource Client provides a natural way to access protected application data from this environment. + +### Blazor Server + +Blazor Server executes component logic on the server. + +When the application service is available in the same process, a direct service call is often more appropriate. + +```text +Blazor Server Component + | + v +Application Service + | + v +Database +``` + +For example: + +```csharp +var products = await productService.GetAllAsync(); +``` + +There is no requirement to introduce an HTTP request simply to use the Resource Client. + +However, when a Blazor Server application needs to communicate with a separate protected Resource API, it can use: + +```csharp +var result = await UAuthClient.Resource.GetAsync>("/api/products"); +``` + +**Important:** The current Blazor Resource Client uses browser-based transport, including in Blazor Server applications. + +The HTTP request is initiated by the user's browser, not by a server-side `HttpClient`. + +For server-to-server communication, use an appropriate server-side HTTP integration and authentication mechanism. + +### The Same Client API, Different Application Architectures + +UltimateAuth aims to make client integration consistent without forcing every application into the same deployment model. + +The goal is not to replace direct application service calls. + +The goal is to remove unnecessary integration work whenever an HTTP resource request is actually needed. + +--- + +## 11. Handling Unauthorized Responses + +An authenticated session may expire or become invalid while an application is running. + +When a Resource API returns `401 Unauthorized`, the Resource Client exposes the unsuccessful HTTP response through its standard result contract. + +```csharp +var result = await UAuthClient.Resource.GetAsync>("/api/products"); + +if (!result.IsSuccess && result.Status == 401) +{ + // Authentication may need to be renewed. +} +``` + +### Automatic Refresh and Retry + +The current Resource Client does not automatically refresh authentication and retry a request after receiving a `401` response. + +This behavior is intentionally outside the initial Resource API implementation. + +Applications should not assume that an unsuccessful request has been retried. + +Authentication renewal and retry policies require careful handling, particularly for requests that modify application state. + +Future improvements may provide additional coordination between resource requests and authentication lifecycle management. + +Until then, applications should handle unauthorized responses according to their authentication flow and UI requirements. + +--- + +## 12. Organizing Application Resource Services + +Although Resource Client operations can be called directly from components, larger applications often benefit from small, domain-specific services. + +For example: + +```csharp +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Core.Contracts; + +public sealed class ProductApiService(IUAuthClient client) +{ + public Task>> GetAllAsync() + => client.Resource.GetAsync>("/api/products"); + + public Task> GetAsync(int id) + => client.Resource.GetAsync($"/api/products/{id}"); + + public Task> CreateAsync(Product product) + => client.Resource.PostAsync("/api/products", product); + + public Task> UpdateAsync(int id, Product product) + => client.Resource.PutAsync($"/api/products/{id}", product); + + public Task DeleteAsync(int id) + => client.Resource.DeleteAsync($"/api/products/{id}"); +} +``` + +This approach keeps application-specific endpoints and operations in one place. + +The service does not need to implement: + +- A custom HTTP transport. +- Browser credential handling. +- Device metadata propagation. +- JSON response deserialization. +- A separate response wrapper. + +Those responsibilities remain with UltimateAuth. + +### Keep the Standard Result + +Notice that `ProductApiService` returns `UAuthResult` directly. + +There is no need to create another response type merely to wrap the same status, value, and problem information. + +This keeps the application aligned with the UltimateAuth SDK. + +### Application Services Still Own Business Logic + +The Resource Client does not attempt to understand application-specific business rules. + +A `ProductApiService` can organize product-related operations, but the Resource API remains responsible for validating product data, enforcing ownership, and authorizing changes. + +This separation keeps UltimateAuth focused on the infrastructure it can provide consistently. + +--- + +## 13. Design Philosophy + +The Resource Client follows the same principle as the rest of UltimateAuth: + +**Complexity should be handled by the framework when it is common, predictable, and independent of application business logic.** + +Developers should not need to repeatedly solve browser transport, credential configuration, device metadata, and response handling simply because their application has its own API. + +At the same time, a framework should not hide important security boundaries or take ownership of application-specific decisions. + +UltimateAuth therefore provides: + +- A unified client entry point. +- Familiar HTTP operations. +- Consistent result contracts. +- Configurable resource origins and credential policies. +- Browser-aware transport behavior. +- Explicit server-side security responsibilities. + +The result is a simpler developer experience without requiring applications to abandon their existing architecture. + +```csharp +// Authentication +await UAuthClient.Flows.RefreshAsync(); + +// Identity +var user = await UAuthClient.Users.GetMeAsync(); + +// Sessions +var sessions = await UAuthClient.Sessions.GetMyChainsAsync( + new PageRequest + { + PageNumber = 1, + PageSize = 10 + }); + +// Application resources +var products = await UAuthClient.Resource.GetAsync>("/api/products"); +``` + +Different responsibilities. One consistent client model. + +**UltimateAuth does not stop at authentication. It helps applications use authentication without rebuilding the integration layer around it.** + +--- + +## Related Documentation + +- [Client SDK Overview](/docs/client-sdk/overview) — Understand the unified client architecture. +- [Client Options](/docs/configuration/client-options) — Configure client profiles, authentication endpoints, and resource options. +- [Authentication](/docs/client-sdk/authentication) — Work with authentication flows and session renewal. +- [Client Profiles](/docs/fundamentals/client-profiles) — Understand runtime-specific authentication behavior. +- [Real-World Setup](/docs/getting-started/real-world-setup) — Configure Blazor applications, UAuthHub, and separate Resource APIs. \ No newline at end of file diff --git a/docs/content/configuration/client-options.md b/docs/content/configuration/client-options.md index b7d09678..929b5a64 100644 --- a/docs/content/configuration/client-options.md +++ b/docs/content/configuration/client-options.md @@ -102,6 +102,7 @@ The client controls the application-side integration. | `Reauth` | `UAuthClientReauthOptions` | Reauthentication behavior | | `MultiTenant` | `UAuthClientMultiTenantOptions` | Tenant propagation | | `StateEvents` | `UAuthStateEventOptions` | Authentication state event handling | +| `Resource` | `UAuthClientResourceOptions` | Protected Resource API origin and browser credential handling | These options are configured through the same `UAuthClientOptions` instance. @@ -321,6 +322,94 @@ When using a remote authentication server, also consider: A correct endpoint URL alone does not guarantee that cross-origin authentication will work. +### Resource API Configuration + +Authentication endpoints and application resource endpoints serve different purposes. + +An application may authenticate through UAuthHub while retrieving business data from a separately deployed Resource API. + +UltimateAuth provides `options.Resource` to configure these requests independently. + +```csharp +using CodeBeam.UltimateAuth.Client; + +builder.Services.AddUltimateAuthClientBlazor(options => +{ + options.Endpoints.BasePath = "https://auth.example.com/auth"; + + options.Resource.BaseAddress = "https://api.example.com"; + options.Resource.Credentials = UAuthResourceCredentialsMode.Auto; +}); +``` + +The Resource Client is available through `IUAuthClient.Resource`. + +```csharp +var result = await UAuthClient.Resource + .GetAsync>("/api/products"); +``` + +#### Resource Options + +| Property | Default | Description | +|---|---|---| +| `Resource.BaseAddress` | `null` | Resource API origin; `null` uses the application's origin | +| `Resource.Credentials` | `Auto` | Browser credential transmission policy | + +#### Base Address Validation + +When configured, `Resource.BaseAddress` must be an absolute HTTP or HTTPS origin. + +HTTPS is required except for loopback development addresses. + +The address must not contain an additional path, query string, fragment, or embedded credentials. + +For example: + +```csharp +options.Resource.BaseAddress = "https://api.example.com"; +``` + +Individual resource paths are supplied when making requests: + +```csharp +await UAuthClient.Resource + .GetAsync>("/api/products"); +``` + +#### Credential Modes + +`UAuthResourceCredentialsMode` supports four options: + +| Mode | Description | +|---|---| +| `Auto` | Automatically select the appropriate browser credential policy | +| `SameOrigin` | Include browser credentials only for same-origin requests | +| `Include` | Include browser credentials for the configured Resource API origin | +| `Omit` | Do not include browser credentials | + +For example, a separate Resource API that requires cross-origin cookies may use: + +```csharp +options.Resource.Credentials = UAuthResourceCredentialsMode.Include; +``` + +The Resource API must permit the relevant browser origin and support the required CORS and cookie configuration. + +Credential configuration does not bypass browser security restrictions or replace server-side authentication and authorization. + +#### Resource Requests and Authentication Lifecycle + +Resource requests use the browser transport in the Blazor integration. + +They support request cancellation, origin validation, and redirect protection. + +Resource requests return `UAuthResult` or `UAuthResult`, depending on the operation. + +The current Resource Client does not automatically refresh authentication and retry a request after receiving a `401 Unauthorized` response. + +For a complete explanation, see [Resource API Requests](/docs/client-sdk/resource-api-requests). + ## 6. Login Flow Options `options.Login` controls client-side login behavior. @@ -1136,6 +1225,8 @@ The current client registration validates options when they are resolved, rather | `MultiTenant.Tenant` | `null` | Tenant identifier | | `MultiTenant.Transport` | `None` | Tenant transport | | `StateEvents.HandlingMode` | `Patch` | State event handling | +| `Resource.BaseAddress` | `null` | Resource API origin; defaults to application origin | +| `Resource.Credentials` | `Auto` | Browser credential handling for resource requests | `AutoRefresh.Enabled` should be evaluated with the applicable platform and post-configuration behavior rather than inferred solely from its property initializer. diff --git a/docs/content/getting-started/real-world-setup.md b/docs/content/getting-started/real-world-setup.md index 4f98c524..4e8d16cd 100644 --- a/docs/content/getting-started/real-world-setup.md +++ b/docs/content/getting-started/real-world-setup.md @@ -779,56 +779,148 @@ This separation allows the API and authentication infrastructure to be deployed, ## 9. Calling a Resource API from Blazor -The current samples demonstrate calling a separate Resource API from Blazor applications. +UltimateAuth provides a unified Resource Client for accessing protected application APIs. -The client integration and the business API's HTTP client are separate concerns. +Instead of configuring a separate HTTP client and implementing browser credential handling for ordinary resource requests, applications can use `IUAuthClient.Resource`. -### Interactive WASM host +This keeps authentication, identity operations, and application resource requests accessible through the same client SDK. -In the ASP.NET Core host: +### Configure the Resource API + +For a separate Resource API, configure its origin through the Blazor client registration: ```csharp -builder.Services.AddHttpClient( - client => - { - client.BaseAddress = new Uri("https://localhost:6122"); - }); +using CodeBeam.UltimateAuth.Client; + +builder.Services.AddUltimateAuthClientBlazor(options => +{ + options.Endpoints.BasePath = "https://localhost:6112/auth"; + + options.Resource.BaseAddress = "https://localhost:6122"; + options.Resource.Credentials = UAuthResourceCredentialsMode.Auto; +}); ``` -### Interactive WASM client +`Endpoints.BasePath` identifies the authentication server. + +`Resource.BaseAddress` identifies the application Resource API. + +These addresses can point to different applications. + +If the Resource API shares the application's origin, `Resource.BaseAddress` can remain `null`. + +### Call Protected Resources + +Inject `IUAuthClient` into a Blazor component: + +```razor +@using CodeBeam.UltimateAuth.Client +@inject IUAuthClient UAuthClient +``` -Register the same application API service in the `.Client` project: +Then retrieve application data: ```csharp -builder.Services.AddHttpClient( - client => - { - client.BaseAddress = new Uri("https://localhost:6122"); - }); +var result = await UAuthClient.Resource.GetAsync>("/api/products"); + +if (result.IsSuccess && result.Value is not null) +{ + products = result.Value; +} ``` -The host and client each require the service when application components or services execute in their respective environments. +The Resource Client also supports POST, PUT, PATCH, and DELETE operations. -`SampleProductApiService` is an example application service, not a required UltimateAuth framework type. +### Application-Specific API Services -### Standalone WASM +Applications can organize resource operations into small domain-specific services. -A standalone WASM application can register an HTTP client for its Resource API: +For example: ```csharp -builder.Services.AddScoped(sp => new HttpClient +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Core.Contracts; + +public sealed class ProductApiService(IUAuthClient client) { - BaseAddress = new Uri("https://localhost:6120") + public Task>> GetAllAsync() + => client.Resource.GetAsync>("/api/products"); + + public Task> CreateAsync(Product product) + => client.Resource.PostAsync("/api/products",product); + + public Task DeleteAsync(int id) + => client.Resource.DeleteAsync($"/api/products/{id}"); +} +``` + +Register the application service: + +```csharp +builder.Services.AddScoped(); +``` + +The service uses UltimateAuth's transport and result contracts rather than implementing another HTTP abstraction. + +### Interactive WebAssembly + +In an Interactive WebAssembly application, configure the UltimateAuth Blazor client in each project where the Resource Client will be used. + +The host and `.Client` project have separate dependency injection containers. + +A shared application API service may be registered in both projects when components execute in both environments. + +The Resource Client uses browser-based transport, including when invoked from an interactive Blazor Server component. + +Browser interop must be available before making resource requests. Do not initiate browser-dependent resource requests during static prerendering. + +### Standalone WebAssembly + +Standalone Blazor WebAssembly applications use the same Resource Client API. + +```csharp +builder.Services.AddUltimateAuthClientBlazor(options => +{ + options.Endpoints.BasePath = "https://localhost:6112/auth"; + options.Resource.BaseAddress = "https://localhost:6122"; }); ``` -If your application has multiple HTTP clients, prefer explicitly named or typed clients to avoid ambiguity. +Once configured, application components can call protected resources through `IUAuthClient.Resource`. + +A separate application-specific `HttpClient` is not required for these requests. + +### Browser Credentials and CORS + +For cross-origin requests, the browser and receiving API must agree on credential handling. + +The Resource Client supports `Auto`, `SameOrigin`, `Include`, and `Omit` credential policies. + +For cross-origin cookie-based authentication, configure the appropriate credential policy and ensure that the Resource API supports the required CORS and cookie settings. + +The Resource Client does not automatically make an API authenticated merely because the user has signed in through UAuthHub. + +The receiving API must validate the authentication material associated with the configured authentication mode and enforce its authorization policies. + +### Blazor Server and Direct Service Calls + +Blazor Server applications can often access application services directly when those services are hosted in the same process. + +For example: + +```csharp +var products = await productService.GetAllAsync(); +``` + +There is no need to introduce an HTTP request solely to use the Resource Client. + +Use `IUAuthClient.Resource` when the application actually needs to communicate with a protected Resource API through the browser. -The current samples also demonstrate browser request credentials handling for cookie-based requests. +For server-to-server requests, use an appropriate server-side HTTP client and authentication mechanism. -For cross-origin cookie authentication, the browser request, API CORS policy, cookie attributes, and Hub configuration must all be compatible. +### Learn More -Do not assume that registering an `HttpClient` automatically forwards authentication credentials to every remote API. +See [Resource API Requests](/docs/client-sdk/resource-api-requests) for supported operations, result handling, credential configuration, security considerations, and request cancellation. ## 10. Authentication Modes and Client Profiles diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/client-sdk/overview.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/client-sdk/overview.json index 1b79ad60..d1540231 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/client-sdk/overview.json +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/client-sdk/overview.json @@ -1,7 +1,7 @@ { "Slug": "client-sdk/overview", "Title": "Client SDK Overview", - "Html": "\n\u003Cp\u003EThe UltimateAuth Client SDK provides a unified way to integrate authentication, session management, user operations, credentials, and authorization into .NET applications.\u003C/p\u003E\n\u003Cp\u003EInstead of manually constructing requests for every authentication endpoint, applications can use dedicated client services that understand UltimateAuth\u0027s API contracts and authentication workflows.\u003C/p\u003E\n\u003Cp\u003EThe SDK is designed for applications that need to interact with an UltimateAuth server, including Blazor WebAssembly, Blazor Server, and other supported .NET client environments.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EThis section is the practical developer guide to using UltimateAuth from application code.\u003C/strong\u003E\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022what-the-client-sdk-provides\u0022\u003EWhat the Client SDK Provides\u003C/h2\u003E\n\u003Cp\u003EThe Client SDK is more than a collection of HTTP methods.\u003C/p\u003E\n\u003Cp\u003EIt brings together several responsibilities:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Cstrong\u003EAuthentication workflows\u003C/strong\u003E \u2014 Start login, refresh, logout, and other supported authentication operations.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003ESession management\u003C/strong\u003E \u2014 Interact with available session and device management capabilities.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EUser management\u003C/strong\u003E \u2014 Retrieve and manage user information through the appropriate API contracts.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003ECredential operations\u003C/strong\u003E \u2014 Manage credentials and initiate supported recovery workflows.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EAuthorization management\u003C/strong\u003E \u2014 Work with roles, permissions, and other exposed authorization operations.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EStructured results\u003C/strong\u003E \u2014 Receive operation results through the SDK\u0027s response contracts.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EClient state integration\u003C/strong\u003E \u2014 Coordinate supported authentication state changes and notifications.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThe SDK helps applications interact with UltimateAuth consistently while keeping the server responsible for authentication and authorization enforcement.\u003C/p\u003E\n\u003Cp\u003EIt does not replace server-side security checks.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022the-client-architecture\u0022\u003EThe Client Architecture\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth separates client responsibilities into specialized services.\u003C/p\u003E\n\u003Cp\u003EThe main client entry point is \u003Ccode\u003EUAuthClient\u003C/code\u003E, which provides access to the SDK\u0027s functional areas.\u003C/p\u003E\n\u003Cp\u003EConceptually:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EApplication\n |\n v\nUAuthClient\n |\n \u002B-- Flows\n | Login, logout, refresh, PKCE\n |\n \u002B-- Sessions\n | Sessions and device-associated chains\n |\n \u002B-- Users\n | User information and lifecycle\n |\n \u002B-- Identifiers\n | User identifiers and verification\n |\n \u002B-- Credentials\n | Credential management and recovery\n |\n \u002B-- Authorization\n Roles, permissions, and access management\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThese represent functional responsibilities. The precise available members and operations depend on the installed packages and supported SDK contracts.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022why-specialized-clients\u0022\u003EWhy Specialized Clients?\u003C/h3\u003E\n\u003Cp\u003EAuthentication-related operations have different requirements.\u003C/p\u003E\n\u003Cp\u003EA login flow may establish authentication state and involve redirects. A user management operation may retrieve or update account information. A credential recovery operation may require additional verification and security handling.\u003C/p\u003E\n\u003Cp\u003EKeeping these responsibilities separate provides a clearer programming model and makes individual APIs easier to discover.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022getting-started-with-uauthclient\u0022\u003EGetting Started with UAuthClient\u003C/h2\u003E\n\u003Cp\u003EIn a supported dependency injection environment, application components can consume the configured UltimateAuth client.\u003C/p\u003E\n\u003Cp\u003EFor example, a Blazor component can receive the client through dependency injection:\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@inject UAuthClient UAuthClient\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EOnce configured, the application uses the appropriate client service for the operation it needs.\u003C/p\u003E\n\u003Cp\u003EThe following illustrates the intended programming model:\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 user = await UAuthClient.Users.GetMeAsync();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe SDK handles the corresponding client operation and returns its structured result.\u003C/p\u003E\n\u003Cp\u003EBefore using the client, the application must register the appropriate UltimateAuth services and configure its connection to the authentication server.\u003C/p\u003E\n\u003Cp\u003EFor a complete setup, follow the Getting Started guide for your application environment.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022choose-your-application-environment\u0022\u003EChoose Your Application Environment\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth supports different application architectures, but their authentication and transport requirements are not identical.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022blazor-webassembly\u0022\u003EBlazor WebAssembly\u003C/h3\u003E\n\u003Cp\u003EBlazor WebAssembly executes application code in the browser.\u003C/p\u003E\n\u003Cp\u003EThe client communicates with the authentication server and protected APIs through browser-compatible HTTP requests.\u003C/p\u003E\n\u003Cp\u003EBrowser security rules, credential handling, CORS configuration, and authentication state synchronization are relevant to this environment.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022blazor-server\u0022\u003EBlazor Server\u003C/h3\u003E\n\u003Cp\u003EBlazor Server executes application code on the server while maintaining an interactive connection to the browser.\u003C/p\u003E\n\u003Cp\u003EAuthentication state, cookie behavior, and server-side execution require different considerations from a browser-hosted WebAssembly application.\u003C/p\u003E\n\u003Cp\u003EServer-side code may also invoke application services directly rather than making an HTTP request.\u003C/p\u003E\n\u003Cp\u003ESuch operations still require appropriate server-side authorization enforcement.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022interactive-web-applications\u0022\u003EInteractive Web Applications\u003C/h3\u003E\n\u003Cp\u003EApplications combining server rendering and WebAssembly interactivity may have multiple execution environments.\u003C/p\u003E\n\u003Cp\u003EDevelopers should distinguish between operations performed on the server and operations performed in the browser.\u003C/p\u003E\n\u003Cp\u003EShared UI components do not automatically imply identical transport or credential handling.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022other.net-clients\u0022\u003EOther .NET Clients\u003C/h3\u003E\n\u003Cp\u003EOther supported .NET application environments can use the relevant client SDK capabilities according to their platform requirements.\u003C/p\u003E\n\u003Cp\u003ECredential storage, HTTP transport, redirect handling, and authentication state integration may differ from browser applications.\u003C/p\u003E\n\u003Cp\u003EThe SDK\u0027s shared contracts help maintain consistency, while platform-specific integration handles the environment\u0027s requirements.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022working-with-authentication-state\u0022\u003EWorking with Authentication State\u003C/h2\u003E\n\u003Cp\u003EAuthentication is not simply a method call that returns a token.\u003C/p\u003E\n\u003Cp\u003EThe client may need to coordinate several related concerns:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003ECompleting the selected authentication flow.\u003C/li\u003E\n\u003Cli\u003EHandling the resulting authentication state.\u003C/li\u003E\n\u003Cli\u003ERenewing authentication when continuation is supported.\u003C/li\u003E\n\u003Cli\u003EReacting to logout or session invalidation.\u003C/li\u003E\n\u003Cli\u003EUpdating application components when relevant state changes.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EUltimateAuth provides client-side abstractions and notifications for supported operations.\u003C/p\u003E\n\u003Cp\u003EFor example, user or credential changes may publish SDK events after a successful operation.\u003C/p\u003E\n\u003Cp\u003EApplications can use the available state integration mechanisms to update their UI without treating every API response as a complete authentication state transition.\u003C/p\u003E\n\u003Cp\u003EThe exact behavior depends on the client environment and configured integration.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022understanding-sdk-results\u0022\u003EUnderstanding SDK Results\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth client operations use structured response contracts.\u003C/p\u003E\n\u003Cp\u003EApplications should inspect the result of an operation rather than assuming that completing an asynchronous call means the requested action succeeded.\u003C/p\u003E\n\u003Cp\u003EA typical application workflow is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003ECall SDK operation\n |\n v\nReceive structured result\n |\n \u002B-- Success\n | |\n | v\n | Continue application workflow\n |\n \u002B-- Failure\n |\n v\n Handle error\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EDifferent operations may return different result types.\u003C/p\u003E\n\u003Cp\u003EThe relevant guide for each client service explains its request and response contracts, including how to handle failures.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022client-events-and-ui-updates\u0022\u003EClient Events and UI Updates\u003C/h2\u003E\n\u003Cp\u003ESome SDK operations can publish notifications when a successful operation changes application-relevant state.\u003C/p\u003E\n\u003Cp\u003EFor example, credential or user management operations may notify subscribers that related information has changed.\u003C/p\u003E\n\u003Cp\u003EThese notifications help decouple an operation from the UI components that react to it.\u003C/p\u003E\n\u003Cp\u003EA component can respond by refreshing its data rather than requiring the caller to manually update every affected view.\u003C/p\u003E\n\u003Cp\u003EHowever, client events are not a security authority.\u003C/p\u003E\n\u003Cp\u003EAn event does not replace server-side validation, and the absence of an event does not prove that the underlying security state has remained unchanged.\u003C/p\u003E\n\u003Cp\u003EApplications should continue to treat the server as authoritative wherever the selected authentication mode requires it.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022calling-protected-resource-apis\u0022\u003ECalling Protected Resource APIs\u003C/h2\u003E\n\u003Cp\u003EAuthentication is often only the first step.\u003C/p\u003E\n\u003Cp\u003EApplications also need to call protected business APIs after the user has authenticated.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth supports integration with protected resource requests, allowing the client application to use the authentication mechanisms appropriate to its configured environment.\u003C/p\u003E\n\u003Cp\u003EA protected resource request involves two distinct responsibilities:\u003C/p\u003E\n\u003Col\u003E\n\u003Cli\u003EThe client sends the request with the appropriate authentication material.\u003C/li\u003E\n\u003Cli\u003EThe receiving server validates authentication and enforces authorization.\u003C/li\u003E\n\u003C/ol\u003E\n\u003Cp\u003EThe Client SDK can help with the client-side responsibility.\u003C/p\u003E\n\u003Cp\u003EIt cannot grant access to an operation that the receiving server does not authorize.\u003C/p\u003E\n\u003Cp\u003EWhen building applications with separate resource APIs, developers must configure the relevant API integration and server-side authentication enforcement.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022client-sdk-guides\u0022\u003EClient SDK Guides\u003C/h2\u003E\n\u003Cp\u003EThe following guides cover the major client responsibilities with practical C# examples.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EGuide\u003C/th\u003E\n\u003Cth\u003EWhat you will learn\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EAuthentication\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003ELogin, logout, refresh, PKCE, and authentication state handling\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003ESession Management\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003ERetrieve, inspect, and manage sessions and device-associated chains\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EUser Management\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003ERetrieve user information and perform supported user lifecycle operations\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EIdentifiers\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EManage email addresses, usernames, phone numbers, and verification workflows\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003ECredentials\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EAdd, change, revoke, and recover credentials\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EAuthorization\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EWork with roles, permissions, and exposed authorization management operations\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EResource API Requests\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003ECall protected application APIs using the appropriate authentication integration\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EEach guide focuses on a specific developer task and includes the relevant SDK methods, request models, response handling, and implementation considerations.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022recommended-learning-path\u0022\u003ERecommended Learning Path\u003C/h3\u003E\n\u003Cp\u003EIf you are integrating UltimateAuth for the first time, start with \u003Cstrong\u003EAuthentication\u003C/strong\u003E.\u003C/p\u003E\n\u003Cp\u003EOnce login and authentication state handling work correctly, continue with \u003Cstrong\u003EResource API Requests\u003C/strong\u003E to connect authentication to your application\u0027s protected functionality.\u003C/p\u003E\n\u003Cp\u003EAfter that, use the domain-specific guides for the features your application needs.\u003C/p\u003E\n\u003Cp\u003EFor example, an account settings page may require the User Management, Identifiers, and Credentials guides, while an administration panel may additionally require Authorization and Session Management.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022client-sdk-and-server-responsibilities\u0022\u003EClient SDK and Server Responsibilities\u003C/h2\u003E\n\u003Cp\u003EThe Client SDK simplifies integration, but it does not independently enforce the security of server-side operations.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EClient responsibility\u003C/th\u003E\n\u003Cth\u003EServer responsibility\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ESubmit authentication requests\u003C/td\u003E\n\u003Ctd\u003EValidate authentication requirements\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EManage supported client state\u003C/td\u003E\n\u003Ctd\u003EEstablish and validate authoritative session state\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ESend protected resource requests\u003C/td\u003E\n\u003Ctd\u003EAuthenticate and authorize protected operations\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EHandle structured responses\u003C/td\u003E\n\u003Ctd\u003EEnforce domain rules and security policies\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EReact to state notifications\u003C/td\u003E\n\u003Ctd\u003EApply revocation and account security controls\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThis separation is particularly important for applications that share UI components across different hosting models.\u003C/p\u003E\n\u003Cp\u003EA shared component can use the same SDK abstractions, while the hosting environment and server determine how authentication material is transported and validated.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022when-to-use-the-client-sdk\u0022\u003EWhen to Use the Client SDK\u003C/h2\u003E\n\u003Cp\u003EUse the Client SDK when building an application that interacts with UltimateAuth\u0027s exposed authentication and management APIs.\u003C/p\u003E\n\u003Cp\u003EIt is generally preferable to manually reproducing the corresponding HTTP contracts because it provides a consistent integration surface and reduces duplicated request-handling code.\u003C/p\u003E\n\u003Cp\u003EDirect endpoint integration may still be appropriate for clients that cannot use the .NET SDK or for specialized interoperability requirements.\u003C/p\u003E\n\u003Cp\u003ERegardless of the client implementation, the same server-side authentication and authorization requirements apply.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022next-steps\u0022\u003ENext Steps\u003C/h2\u003E\n\u003Cp\u003EContinue with the Client SDK guides to implement real application workflows.\u003C/p\u003E\n\u003Cp\u003EStart with \u003Cstrong\u003EAuthentication\u003C/strong\u003E to configure and use login, refresh, and logout operations.\u003C/p\u003E\n\u003Cp\u003EThen explore session management, user operations, credentials, authorization, and protected resource API integration according to your application\u0027s requirements.\u003C/p\u003E\n\u003Cp\u003EFor the concepts behind these operations, refer to \u003Ca href=\u0022/docs/fundamentals/auth-modes\u0022\u003EAuthentication Modes\u003C/a\u003E and \u003Ca href=\u0022/docs/plugin-domains/overview\u0022\u003EPlugin Domains\u003C/a\u003E.\u003C/p\u003E\n", + "Html": "\n\u003Cp\u003EThe UltimateAuth Client SDK provides a unified way to integrate authentication, session management, user operations, credentials, and authorization into .NET applications.\u003C/p\u003E\n\u003Cp\u003EInstead of manually constructing requests for every authentication endpoint, applications can use dedicated client services that understand UltimateAuth\u0027s API contracts and authentication workflows.\u003C/p\u003E\n\u003Cp\u003EThe SDK is designed for applications that need to interact with an UltimateAuth server, including Blazor WebAssembly, Blazor Server, and other supported .NET client environments.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EThis section is the practical developer guide to using UltimateAuth from application code.\u003C/strong\u003E\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022what-the-client-sdk-provides\u0022\u003EWhat the Client SDK Provides\u003C/h2\u003E\n\u003Cp\u003EThe Client SDK is more than a collection of HTTP methods.\u003C/p\u003E\n\u003Cp\u003EIt brings together several responsibilities:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Cstrong\u003EAuthentication workflows\u003C/strong\u003E \u2014 Start login, refresh, logout, and other supported authentication operations.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003ESession management\u003C/strong\u003E \u2014 Interact with available session and device management capabilities.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EUser management\u003C/strong\u003E \u2014 Retrieve and manage user information through the appropriate API contracts.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003ECredential operations\u003C/strong\u003E \u2014 Manage credentials and initiate supported recovery workflows.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EAuthorization management\u003C/strong\u003E \u2014 Work with roles, permissions, and other exposed authorization operations.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EStructured results\u003C/strong\u003E \u2014 Receive operation results through the SDK\u0027s response contracts.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EClient state integration\u003C/strong\u003E \u2014 Coordinate supported authentication state changes and notifications.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EResource API requests\u003C/strong\u003E \u2014 Access protected application APIs through unified HTTP operations and structured results.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThe SDK helps applications interact with UltimateAuth consistently while keeping the server responsible for authentication and authorization enforcement.\u003C/p\u003E\n\u003Cp\u003EIt does not replace server-side security checks.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022the-client-architecture\u0022\u003EThe Client Architecture\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth separates client responsibilities into specialized services.\u003C/p\u003E\n\u003Cp\u003EThe main client entry point is \u003Ccode\u003EUAuthClient\u003C/code\u003E, which provides access to the SDK\u0027s functional areas.\u003C/p\u003E\n\u003Cp\u003EConceptually:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EApplication\n |\n v\nIUAuthClient\n |\n \u002B-- Flows\n | Login, logout, refresh, PKCE\n |\n \u002B-- Sessions\n | Sessions and device-associated chains\n |\n \u002B-- Users\n | User information and lifecycle\n |\n \u002B-- Identifiers\n | User identifiers and verification\n |\n \u002B-- Credentials\n | Credential management and recovery\n |\n \u002B-- Authorization\n | Roles, permissions, and access management\n |\n \u002B-- Resource\n Authenticated application API requests\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThese represent functional responsibilities. The precise available members and operations depend on the installed packages and supported SDK contracts.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022why-specialized-clients\u0022\u003EWhy Specialized Clients?\u003C/h3\u003E\n\u003Cp\u003EAuthentication-related operations have different requirements.\u003C/p\u003E\n\u003Cp\u003EA login flow may establish authentication state and involve redirects. A user management operation may retrieve or update account information. A credential recovery operation may require additional verification and security handling.\u003C/p\u003E\n\u003Cp\u003EKeeping these responsibilities separate provides a clearer programming model and makes individual APIs easier to discover.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022getting-started-with-uauthclient\u0022\u003EGetting Started with UAuthClient\u003C/h2\u003E\n\u003Cp\u003EIn a supported dependency injection environment, application components can consume the configured UltimateAuth client.\u003C/p\u003E\n\u003Cp\u003EFor example, a Blazor component can receive the client through dependency injection:\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@inject UAuthClient UAuthClient\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EOnce configured, the application uses the appropriate client service for the operation it needs.\u003C/p\u003E\n\u003Cp\u003EThe following illustrates the intended programming model:\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 user = await UAuthClient.Users.GetMeAsync();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe SDK handles the corresponding client operation and returns its structured result.\u003C/p\u003E\n\u003Cp\u003EBefore using the client, the application must register the appropriate UltimateAuth services and configure its connection to the authentication server.\u003C/p\u003E\n\u003Cp\u003EFor a complete setup, follow the Getting Started guide for your application environment.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022choose-your-application-environment\u0022\u003EChoose Your Application Environment\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth supports different application architectures, but their authentication and transport requirements are not identical.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022blazor-webassembly\u0022\u003EBlazor WebAssembly\u003C/h3\u003E\n\u003Cp\u003EBlazor WebAssembly executes application code in the browser.\u003C/p\u003E\n\u003Cp\u003EThe client communicates with the authentication server and protected APIs through browser-compatible HTTP requests.\u003C/p\u003E\n\u003Cp\u003EBrowser security rules, credential handling, CORS configuration, and authentication state synchronization are relevant to this environment.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022blazor-server\u0022\u003EBlazor Server\u003C/h3\u003E\n\u003Cp\u003EBlazor Server executes application code on the server while maintaining an interactive connection to the browser.\u003C/p\u003E\n\u003Cp\u003EAuthentication state, cookie behavior, and server-side execution require different considerations from a browser-hosted WebAssembly application.\u003C/p\u003E\n\u003Cp\u003EServer-side code may also invoke application services directly rather than making an HTTP request.\u003C/p\u003E\n\u003Cp\u003ESuch operations still require appropriate server-side authorization enforcement.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022interactive-web-applications\u0022\u003EInteractive Web Applications\u003C/h3\u003E\n\u003Cp\u003EApplications combining server rendering and WebAssembly interactivity may have multiple execution environments.\u003C/p\u003E\n\u003Cp\u003EDevelopers should distinguish between operations performed on the server and operations performed in the browser.\u003C/p\u003E\n\u003Cp\u003EShared UI components do not automatically imply identical transport or credential handling.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022other.net-clients\u0022\u003EOther .NET Clients\u003C/h3\u003E\n\u003Cp\u003EOther supported .NET application environments can use the relevant client SDK capabilities according to their platform requirements.\u003C/p\u003E\n\u003Cp\u003ECredential storage, HTTP transport, redirect handling, and authentication state integration may differ from browser applications.\u003C/p\u003E\n\u003Cp\u003EThe SDK\u0027s shared contracts help maintain consistency, while platform-specific integration handles the environment\u0027s requirements.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022working-with-authentication-state\u0022\u003EWorking with Authentication State\u003C/h2\u003E\n\u003Cp\u003EAuthentication is not simply a method call that returns a token.\u003C/p\u003E\n\u003Cp\u003EThe client may need to coordinate several related concerns:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003ECompleting the selected authentication flow.\u003C/li\u003E\n\u003Cli\u003EHandling the resulting authentication state.\u003C/li\u003E\n\u003Cli\u003ERenewing authentication when continuation is supported.\u003C/li\u003E\n\u003Cli\u003EReacting to logout or session invalidation.\u003C/li\u003E\n\u003Cli\u003EUpdating application components when relevant state changes.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EUltimateAuth provides client-side abstractions and notifications for supported operations.\u003C/p\u003E\n\u003Cp\u003EFor example, user or credential changes may publish SDK events after a successful operation.\u003C/p\u003E\n\u003Cp\u003EApplications can use the available state integration mechanisms to update their UI without treating every API response as a complete authentication state transition.\u003C/p\u003E\n\u003Cp\u003EThe exact behavior depends on the client environment and configured integration.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022understanding-sdk-results\u0022\u003EUnderstanding SDK Results\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth client operations use structured response contracts.\u003C/p\u003E\n\u003Cp\u003EApplications should inspect the result of an operation rather than assuming that completing an asynchronous call means the requested action succeeded.\u003C/p\u003E\n\u003Cp\u003EA typical application workflow is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003ECall SDK operation\n |\n v\nReceive structured result\n |\n \u002B-- Success\n | |\n | v\n | Continue application workflow\n |\n \u002B-- Failure\n |\n v\n Handle error\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EDifferent operations may return different result types.\u003C/p\u003E\n\u003Cp\u003EThe relevant guide for each client service explains its request and response contracts, including how to handle failures.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022client-events-and-ui-updates\u0022\u003EClient Events and UI Updates\u003C/h2\u003E\n\u003Cp\u003ESome SDK operations can publish notifications when a successful operation changes application-relevant state.\u003C/p\u003E\n\u003Cp\u003EFor example, credential or user management operations may notify subscribers that related information has changed.\u003C/p\u003E\n\u003Cp\u003EThese notifications help decouple an operation from the UI components that react to it.\u003C/p\u003E\n\u003Cp\u003EA component can respond by refreshing its data rather than requiring the caller to manually update every affected view.\u003C/p\u003E\n\u003Cp\u003EHowever, client events are not a security authority.\u003C/p\u003E\n\u003Cp\u003EAn event does not replace server-side validation, and the absence of an event does not prove that the underlying security state has remained unchanged.\u003C/p\u003E\n\u003Cp\u003EApplications should continue to treat the server as authoritative wherever the selected authentication mode requires it.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022calling-protected-resource-apis\u0022\u003ECalling Protected Resource APIs\u003C/h2\u003E\n\u003Cp\u003EAuthentication is only the beginning of an application.\u003C/p\u003E\n\u003Cp\u003EAfter signing in, users still need to retrieve data, create records, and interact with protected application resources.\u003C/p\u003E\n\u003Cp\u003EWithout a unified integration, developers often configure authentication through one SDK and then build a separate HTTP abstraction for their application APIs.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth addresses this through \u003Ccode\u003EIUAuthClient.Resource\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.Resource.GetAsync\u0026lt;List\u0026lt;Product\u0026gt;\u0026gt;(\u0026quot;/api/products\u0026quot;);\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is not null)\n{\n products = result.Value;\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EResource requests use the same client entry point and structured result model as other UltimateAuth operations.\u003C/p\u003E\n\u003Cp\u003EThe Blazor integration provides browser-based transport, configurable resource origins, credential handling, and request cancellation without requiring application-specific JavaScript wrappers.\u003C/p\u003E\n\u003Cp\u003EThe Resource Client supports GET, POST, PUT, PATCH, and DELETE operations.\u003C/p\u003E\n\u003Cp\u003EIt does not replace server-side authentication or authorization. The receiving Resource API remains responsible for validating requests and enforcing access policies.\u003C/p\u003E\n\u003Cp\u003EFor configuration, examples, security considerations, and supported behavior, see \u003Ca href=\u0022/docs/client-sdk/resource-api-requests\u0022\u003EResource API Requests\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022client-sdk-guides\u0022\u003EClient SDK Guides\u003C/h2\u003E\n\u003Cp\u003EThe following guides cover the major client responsibilities with practical C# examples.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EGuide\u003C/th\u003E\n\u003Cth\u003EWhat you will learn\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EAuthentication\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003ELogin, logout, refresh, PKCE, and authentication state handling\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003ESession Management\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003ERetrieve, inspect, and manage sessions and device-associated chains\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EUser Management\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003ERetrieve user information and perform supported user lifecycle operations\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EIdentifiers\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EManage email addresses, usernames, phone numbers, and verification workflows\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003ECredentials\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EAdd, change, revoke, and recover credentials\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EAuthorization\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EWork with roles, permissions, and exposed authorization management operations\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ca href=\u0022/docs/client-sdk/resource-api-requests\u0022\u003EResource API Requests\u003C/a\u003E\u003C/td\u003E\n\u003Ctd\u003ECall protected application APIs through the unified Resource Client\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EEach guide focuses on a specific developer task and includes the relevant SDK methods, request models, response handling, and implementation considerations.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022recommended-learning-path\u0022\u003ERecommended Learning Path\u003C/h3\u003E\n\u003Cp\u003EIf you are integrating UltimateAuth for the first time, start with \u003Cstrong\u003EAuthentication\u003C/strong\u003E.\u003C/p\u003E\n\u003Cp\u003EOnce login and authentication state handling work correctly, continue with \u003Cstrong\u003EResource API Requests\u003C/strong\u003E to connect authentication to your application\u0027s protected functionality.\u003C/p\u003E\n\u003Cp\u003EAfter that, use the domain-specific guides for the features your application needs.\u003C/p\u003E\n\u003Cp\u003EFor example, an account settings page may require the User Management, Identifiers, and Credentials guides, while an administration panel may additionally require Authorization and Session Management.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022client-sdk-and-server-responsibilities\u0022\u003EClient SDK and Server Responsibilities\u003C/h2\u003E\n\u003Cp\u003EThe Client SDK simplifies integration, but it does not independently enforce the security of server-side operations.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EClient responsibility\u003C/th\u003E\n\u003Cth\u003EServer responsibility\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ESubmit authentication requests\u003C/td\u003E\n\u003Ctd\u003EValidate authentication requirements\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EManage supported client state\u003C/td\u003E\n\u003Ctd\u003EEstablish and validate authoritative session state\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ESend protected resource requests\u003C/td\u003E\n\u003Ctd\u003EAuthenticate and authorize protected operations\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EHandle structured responses\u003C/td\u003E\n\u003Ctd\u003EEnforce domain rules and security policies\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EReact to state notifications\u003C/td\u003E\n\u003Ctd\u003EApply revocation and account security controls\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThis separation is particularly important for applications that share UI components across different hosting models.\u003C/p\u003E\n\u003Cp\u003EA shared component can use the same SDK abstractions, while the hosting environment and server determine how authentication material is transported and validated.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022when-to-use-the-client-sdk\u0022\u003EWhen to Use the Client SDK\u003C/h2\u003E\n\u003Cp\u003EUse the Client SDK when building an application that interacts with UltimateAuth\u0027s exposed authentication and management APIs.\u003C/p\u003E\n\u003Cp\u003EIt is generally preferable to manually reproducing the corresponding HTTP contracts because it provides a consistent integration surface and reduces duplicated request-handling code.\u003C/p\u003E\n\u003Cp\u003EDirect endpoint integration may still be appropriate for clients that cannot use the .NET SDK or for specialized interoperability requirements.\u003C/p\u003E\n\u003Cp\u003ERegardless of the client implementation, the same server-side authentication and authorization requirements apply.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022next-steps\u0022\u003ENext Steps\u003C/h2\u003E\n\u003Cp\u003EContinue with the Client SDK guides to implement real application workflows.\u003C/p\u003E\n\u003Cp\u003EStart with \u003Cstrong\u003EAuthentication\u003C/strong\u003E to configure and use login, refresh, and logout operations.\u003C/p\u003E\n\u003Cp\u003EThen explore session management, user operations, credentials, authorization, and protected resource API integration according to your application\u0027s requirements.\u003C/p\u003E\n\u003Cp\u003EFor the concepts behind these operations, refer to \u003Ca href=\u0022/docs/fundamentals/auth-modes\u0022\u003EAuthentication Modes\u003C/a\u003E and \u003Ca href=\u0022/docs/plugin-domains/overview\u0022\u003EPlugin Domains\u003C/a\u003E.\u003C/p\u003E\n", "Headings": [ { "Id": "what-the-client-sdk-provides", @@ -94,6 +94,6 @@ "Level": 0 } ], - "SourceHash": "152FDA6808038B8192736EF34354F65C42565CF545396E6B52A7B831E8213610", + "SourceHash": "28EB536E721D2C3528E8A2287BCC4F7589B732AC2A192C2EAC862DC31E2154FF", "BuilderVersion": "1" } \ No newline at end of file diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/client-sdk/resource-api-requests.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/client-sdk/resource-api-requests.json new file mode 100644 index 00000000..e33f60ce --- /dev/null +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/client-sdk/resource-api-requests.json @@ -0,0 +1,219 @@ +{ + "Slug": "client-sdk/resource-api-requests", + "Title": "Resource API Requests", + "Html": "\n\u003Cp\u003E\u003Cstrong\u003EAuthenticate once. Access your application resources through the same unified client.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EAuthentication is only the beginning of an application.\u003C/p\u003E\n\u003Cp\u003EOnce a user signs in, the application still needs to retrieve data, create records, update resources, and communicate with protected APIs.\u003C/p\u003E\n\u003Cp\u003ETraditionally, this introduces another layer of integration work.\u003C/p\u003E\n\u003Cp\u003EDevelopers configure authentication through one library, then build a separate HTTP client to access their application APIs. They must handle credentials, browser restrictions, response parsing, errors, and differences between hosting environments.\u003C/p\u003E\n\u003Cp\u003EThe result is often two separate programming models: one for authentication and another for everything that comes after it.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EUltimateAuth brings these operations together through \u003Ccode\u003EIUAuthClient.Resource\u003C/code\u003E.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EInstead of introducing another client abstraction, applications can use the same UltimateAuth entry point for authentication, identity management, and protected resource requests.\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.Resource\n .GetAsync\u0026lt;List\u0026lt;Product\u0026gt;\u0026gt;(\u0026quot;/api/products\u0026quot;);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ENo custom JavaScript wrapper. No application-specific response type. No separate HTTP client configuration for ordinary authenticated resource requests.\u003C/p\u003E\n\u003Cp\u003EThe goal is straightforward: \u003Cstrong\u003Emake accessing protected application resources feel like a natural continuation of authentication.\u003C/strong\u003E\u003C/p\u003E\n\u003Chr\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022the-problem-authentication-ends-integration-work-begins\u0022\u003E1. The Problem: Authentication Ends, Integration Work Begins\u003C/h2\u003E\n\u003Cp\u003EConsider a typical application with three components:\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\u003EBlazor Application\n |\n \u002B----\u0026gt; UAuthHub\n | Authentication\n | Sessions\n | Identity\n |\n \u002B----\u0026gt; Resource API\n Products\n Orders\n Application Data\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUltimateAuth handles authentication and identity operations.\u003C/p\u003E\n\u003Cp\u003EBut the application also needs to communicate with its Resource API.\u003C/p\u003E\n\u003Cp\u003EWithout a unified resource client, developers typically need to:\u003C/p\u003E\n\u003Col\u003E\n\u003Cli\u003EConfigure an additional HTTP client.\u003C/li\u003E\n\u003Cli\u003EDetermine how authentication credentials should accompany requests.\u003C/li\u003E\n\u003Cli\u003EHandle browser credential policies and cross-origin restrictions.\u003C/li\u003E\n\u003Cli\u003EMaintain device and client profile metadata where applicable.\u003C/li\u003E\n\u003Cli\u003EDeserialize successful responses.\u003C/li\u003E\n\u003Cli\u003ETranslate unsuccessful HTTP responses into application-friendly results.\u003C/li\u003E\n\u003Cli\u003EImplement similar request handling across multiple components.\u003C/li\u003E\n\u003C/ol\u003E\n\u003Cp\u003EThese responsibilities are not necessarily complicated individually.\u003C/p\u003E\n\u003Cp\u003EThe problem is their repetition.\u003C/p\u003E\n\u003Cp\u003EA framework that simplifies authentication should not require developers to rebuild the surrounding integration layer every time they need application data.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022a-unified-client-model\u0022\u003EA Unified Client Model\u003C/h3\u003E\n\u003Cp\u003EUltimateAuth already exposes specialized operations through \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\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthClient.Flows\nUAuthClient.Users\nUAuthClient.Sessions\nUAuthClient.Credentials\nUAuthClient.Authorization\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EResource requests follow the same model:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthClient.Resource\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis preserves a consistent entry point without mixing application-specific business operations into UltimateAuth\u0027s authentication services.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth does not need to know what a \u003Ccode\u003EProduct\u003C/code\u003E, \u003Ccode\u003EOrder\u003C/code\u003E, or \u003Ccode\u003EInvoice\u003C/code\u003E represents.\u003C/p\u003E\n\u003Cp\u003EIt only needs to provide a predictable way to communicate with the API that owns those resources.\u003C/p\u003E\n\u003Chr\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022getting-started\u0022\u003E2. Getting Started\u003C/h2\u003E\n\u003Cp\u003EThe Resource API client is available through the UltimateAuth Client SDK.\u003C/p\u003E\n\u003Cp\u003EFor Blazor applications, register the Blazor client integration:\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.AddUltimateAuthClientBlazor();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe registration includes the shared client services and the browser-specific resource transport.\u003C/p\u003E\n\u003Cp\u003EYou do not need to register a separate Resource Client.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022inject-iuauthclient\u0022\u003EInject IUAuthClient\u003C/h3\u003E\n\u003Cp\u003EIn a Blazor component:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@using CodeBeam.UltimateAuth.Client\n@inject IUAuthClient UAuthClient\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThen call a protected application endpoint:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Resource.GetAsync\u0026lt;List\u0026lt;Product\u0026gt;\u0026gt;(\u0026quot;/api/products\u0026quot;);\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is not null)\n{\n products = result.Value;\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe response uses the same structured result model found throughout UltimateAuth.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022what-happens-behind-the-call\u0022\u003EWhat Happens Behind the Call?\u003C/h3\u003E\n\u003Cp\u003EFor the Blazor integration, the request follows this path:\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\u003EBlazor Component\n |\n v\nIUAuthClient.Resource\n |\n v\nResource Client\n |\n v\nUltimateAuth Browser Transport\n |\n v\nProtected Resource API\n |\n v\nUAuthResult\u0026lt;T\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe browser transport handles request construction and browser-specific behavior.\u003C/p\u003E\n\u003Cp\u003EThe Resource API remains responsible for validating authentication and enforcing authorization.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EA successful client request does not bypass server-side security.\u003C/strong\u003E\u003C/p\u003E\n\u003Chr\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022supported-http-operations\u0022\u003E3. Supported HTTP Operations\u003C/h2\u003E\n\u003Cp\u003EThe Resource Client provides five standard HTTP operations.\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\u003Cth\u003EReturn type\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EGetAsync\u0026lt;T\u0026gt;\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERetrieve a resource\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthResult\u0026lt;T\u0026gt;\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPostAsync\u0026lt;T\u0026gt;\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECreate or submit a resource\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthResult\u0026lt;T\u0026gt;\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPutAsync\u0026lt;T\u0026gt;\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReplace or update a resource\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthResult\u0026lt;T\u0026gt;\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPatchAsync\u0026lt;T\u0026gt;\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPartially update a resource\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthResult\u0026lt;T\u0026gt;\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDeleteAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EDelete a resource\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthResult\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EAll methods accept a relative endpoint and support cancellation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022get\u0022\u003EGET\u003C/h3\u003E\n\u003Cp\u003ERetrieve application data:\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.Resource.GetAsync\u0026lt;List\u0026lt;Product\u0026gt;\u0026gt;(\u0026quot;/api/products\u0026quot;);\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is not null)\n{\n products = result.Value;\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ERetrieve a single resource:\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.Resource.GetAsync\u0026lt;Product\u0026gt;($\u0026quot;/api/products/{productId}\u0026quot;);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022post\u0022\u003EPOST\u003C/h3\u003E\n\u003Cp\u003ESubmit a new resource:\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 product = new Product\n{\n Name = \u0026quot;Example Product\u0026quot;,\n Price = 29.99m\n};\n\nvar result = await UAuthClient.Resource.PostAsync\u0026lt;Product\u0026gt;(\u0026quot;/api/products\u0026quot;, product);\n\nif (result.IsSuccess)\n{\n // Product created successfully.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe request payload is serialized for transport.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022put\u0022\u003EPUT\u003C/h3\u003E\n\u003Cp\u003EUpdate a resource:\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.Resource\n .PutAsync\u0026lt;Product\u0026gt;($\u0026quot;/api/products/{product.Id}\u0026quot;, product);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022patch\u0022\u003EPATCH\u003C/h3\u003E\n\u003Cp\u003ESubmit a partial update:\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 changes = new\n{\n Price = 24.99m\n};\n\nvar result = await UAuthClient.Resource\n .PatchAsync\u0026lt;Product\u0026gt;($\u0026quot;/api/products/{productId}\u0026quot;, changes);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe Resource Client sends the request payload.\u003C/p\u003E\n\u003Cp\u003EThe receiving API determines which partial-update formats and operations it supports.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022delete\u0022\u003EDELETE\u003C/h3\u003E\n\u003Cp\u003EDelete a resource:\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.Resource.DeleteAsync($\u0026quot;/api/products/{productId}\u0026quot;);\n\nif (result.IsSuccess)\n{\n // Resource deleted successfully.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUnlike the other methods, \u003Ccode\u003EDeleteAsync\u003C/code\u003E returns the non-generic \u003Ccode\u003EUAuthResult\u003C/code\u003E because the operation does not require a response value.\u003C/p\u003E\n\u003Chr\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022consistent-results-with-uauthresult\u0022\u003E4. Consistent Results with UAuthResult\u003C/h2\u003E\n\u003Cp\u003EA common source of unnecessary complexity is response handling.\u003C/p\u003E\n\u003Cp\u003EOne API client returns \u003Ccode\u003EHttpResponseMessage\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EAnother returns a custom response wrapper.\u003C/p\u003E\n\u003Cp\u003EA third throws exceptions for ordinary HTTP failures.\u003C/p\u003E\n\u003Cp\u003EApplications then need different handling logic for operations that are conceptually similar.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth uses \u003Ccode\u003EUAuthResult\u0026lt;T\u0026gt;\u003C/code\u003E to provide a consistent response contract.\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.Resource.GetAsync\u0026lt;Product\u0026gt;(\u0026quot;/api/products/42\u0026quot;);\n\nif (result.IsSuccess)\n{\n var product = result.Value;\n}\nelse\n{\n var status = result.Status;\n var problem = result.Problem;\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022result-properties\u0022\u003EResult Properties\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIsSuccess\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIndicates whether the HTTP operation succeeded\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EStatus\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EHTTP response status code\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EValue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EDeserialized response value for successful generic operations\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EProblem\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EStructured problem information when available\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EA failed HTTP response does not automatically mean a transport exception.\u003C/p\u003E\n\u003Cp\u003EFor example, an API may return:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003E400 Bad Request\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003E401 Unauthorized\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003E403 Forbidden\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003E404 Not Found\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003E409 Conflict\u003C/code\u003E\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThese responses can be represented through \u003Ccode\u003EUAuthResult\u0026lt;T\u0026gt;\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe application can inspect the status and decide how to respond.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022structured-problems\u0022\u003EStructured Problems\u003C/h3\u003E\n\u003Cp\u003EWhen an unsuccessful response contains a compatible UltimateAuth problem payload, the Resource Client attempts to deserialize it into \u003Ccode\u003EUAuthProblem\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\u003Eif (!result.IsSuccess)\n{\n if (result.Problem is not null)\n {\n // Handle the structured API problem.\n }\n else\n {\n // Handle the HTTP status without a structured problem.\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ENot every API returns a compatible problem body.\u003C/p\u003E\n\u003Cp\u003ETherefore, \u003Ccode\u003EProblem\u003C/code\u003E may be \u003Ccode\u003Enull\u003C/code\u003E even when the request failed.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022transport-failures-are-different\u0022\u003ETransport Failures Are Different\u003C/h3\u003E\n\u003Cp\u003ENetwork failures, protocol errors, and request cancellation are different from ordinary unsuccessful HTTP responses.\u003C/p\u003E\n\u003Cp\u003EFor example, an unreachable API does not necessarily produce an HTTP status code.\u003C/p\u003E\n\u003Cp\u003EApplications should distinguish transport exceptions from valid HTTP responses indicating failure.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003E\u003Ccode\u003EUAuthResult\u0026lt;T\u0026gt;\u003C/code\u003E standardizes HTTP response handling; it does not guarantee that every network or runtime failure becomes a result object.\u003C/strong\u003E\u003C/p\u003E\n\u003Chr\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022resource-api-configuration\u0022\u003E5. Resource API Configuration\u003C/h2\u003E\n\u003Cp\u003EResource API configuration is part of \u003Ccode\u003EUAuthClientOptions\u003C/code\u003E.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Resource.BaseAddress = \u0026quot;https://api.example.com\u0026quot;;\n options.Resource.Credentials = UAuthResourceCredentialsMode.Auto;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe \u003Ccode\u003EResource\u003C/code\u003E options are separate from the authentication endpoint configuration.\u003C/p\u003E\n\u003Cp\u003EThis distinction allows applications to authenticate against one server while accessing resources hosted elsewhere.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022baseaddress\u0022\u003EBaseAddress\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EBaseAddress\u003C/code\u003E identifies the Resource API origin.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.Resource.BaseAddress = \u0026quot;https://api.example.com\u0026quot;;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EIf \u003Ccode\u003EBaseAddress\u003C/code\u003E is not configured, requests use the application\u0027s current origin.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EA call such as:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 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.Resource.GetAsync\u0026lt;List\u0026lt;Product\u0026gt;\u0026gt;(\u0026quot;/api/products\u0026quot;);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Ethen targets the application\u0027s origin.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022baseaddress-validation\u0022\u003EBaseAddress Validation\u003C/h3\u003E\n\u003Cp\u003EUltimateAuth validates the configured Resource API address.\u003C/p\u003E\n\u003Cp\u003EThe address must:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EBe an absolute HTTP or HTTPS URL.\u003C/li\u003E\n\u003Cli\u003EUse HTTPS, except for loopback development addresses.\u003C/li\u003E\n\u003Cli\u003ERepresent an origin without an additional path.\u003C/li\u003E\n\u003Cli\u003EExclude embedded credentials, query strings, and fragments.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EValid examples:\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\u003Ehttps://api.example.com\nhttps://localhost:7001\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EInvalid examples:\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\u003Ehttps://api.example.com/api\nhttps://api.example.com?version=1\nhttp://api.example.com\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EKeep endpoint paths in individual Resource Client calls:\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.Resource.BaseAddress = \u0026quot;https://api.example.com\u0026quot;;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 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.Resource.GetAsync\u0026lt;List\u0026lt;Product\u0026gt;\u0026gt;(\u0026quot;/api/products\u0026quot;);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis separates deployment configuration from application resource paths.\u003C/p\u003E\n\u003Chr\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022browser-credential-handling\u0022\u003E6. Browser Credential Handling\u003C/h2\u003E\n\u003Cp\u003EBrowser-based applications do not have unrestricted control over authentication credentials.\u003C/p\u003E\n\u003Cp\u003ECookies, origins, cross-origin requests, and browser security policies all influence how requests are sent.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth exposes credential handling through \u003Ccode\u003EUAuthResourceCredentialsMode\u003C/code\u003E.\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\u003EAuto\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELet UltimateAuth select the appropriate browser credential policy\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESameOrigin\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESend browser credentials only for same-origin requests\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EInclude\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EInclude browser credentials for the configured Resource API origin\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EOmit\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EDo not send browser credentials\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe default is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.Resource.Credentials = UAuthResourceCredentialsMode.Auto;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022same-origin-resource-api\u0022\u003ESame-Origin Resource API\u003C/h3\u003E\n\u003Cp\u003EWhen the application and Resource API share an origin:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\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\u003Ehttps://app.example.com\nhttps://app.example.com/api/products\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Eyou can use:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.Resource.Credentials = UAuthResourceCredentialsMode.SameOrigin;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis is appropriate when the Resource API relies on browser credentials available to the application origin.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022separate-resource-api\u0022\u003ESeparate Resource API\u003C/h3\u003E\n\u003Cp\u003EConsider:\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\u003EApplication: https://app.example.com\nResource API: https://api.example.com\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EFor a cross-origin API that requires browser cookies, configure:\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.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Resource.BaseAddress = \u0026quot;https://api.example.com\u0026quot;;\n options.Resource.Credentials = UAuthResourceCredentialsMode.Include;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe receiving API must also support the relevant browser security requirements.\u003C/p\u003E\n\u003Cp\u003EFor credentialed cross-origin requests, this typically includes:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EExplicitly permitted origins.\u003C/li\u003E\n\u003Cli\u003EAppropriate CORS configuration.\u003C/li\u003E\n\u003Cli\u003ECompatible cookie attributes and browser policies.\u003C/li\u003E\n\u003Cli\u003EServer-side authentication and authorization.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EConfiguring \u003Ccode\u003EInclude\u003C/code\u003E does not override browser restrictions or guarantee that a cookie is available for the destination.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022when-to-use-omit\u0022\u003EWhen to Use Omit\u003C/h3\u003E\n\u003Cp\u003ESome resource endpoints do not require browser cookies.\u003C/p\u003E\n\u003Cp\u003EFor those cases:\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.Resource.Credentials = UAuthResourceCredentialsMode.Omit;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis prevents browser credentials from being included in the resource request.\u003C/p\u003E\n\u003Cp\u003EThe API must still enforce whichever authentication mechanism it requires.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003ECredential transmission is not the same as authorization.\u003C/strong\u003E\u003C/p\u003E\n\u003Chr\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022separate-uauthhub-and-resource-api\u0022\u003E7. Separate UAuthHub and Resource API\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth supports architectures where authentication and application resources are hosted independently.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003E Blazor Application\n |\n \u002B------------\u002B------------\u002B\n | |\n v v\n UAuthHub Resource API\n auth.example.com api.example.com\n | |\n v v\n Authentication Application Data\n Sessions Business Operations\n Identity Authorization\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe client can configure both destinations independently:\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.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Endpoints.BasePath = \u0026quot;https://auth.example.com/auth\u0026quot;;\n\n options.Resource.BaseAddress = \u0026quot;https://api.example.com\u0026quot;;\n\n options.Resource.Credentials = UAuthResourceCredentialsMode.Auto;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAuthentication operations use the configured authentication endpoints.\u003C/p\u003E\n\u003Cp\u003EResource operations use the configured Resource API origin.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar profile = await UAuthClient.Users.GetMeAsync();\n\nvar products = await UAuthClient.Resource.GetAsync\u0026lt;List\u0026lt;Product\u0026gt;\u0026gt;(\u0026quot;/api/products\u0026quot;);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EBoth operations are accessed through \u003Ccode\u003EIUAuthClient\u003C/code\u003E, but they serve different purposes.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022authentication-does-not-automatically-protect-an-api\u0022\u003EAuthentication Does Not Automatically Protect an API\u003C/h3\u003E\n\u003Cp\u003EConfiguring a Resource API address does not automatically enable authentication or authorization on that API.\u003C/p\u003E\n\u003Cp\u003EThe Resource API must independently validate incoming requests and enforce its access rules.\u003C/p\u003E\n\u003Cp\u003EFor separate deployments, the Resource API must also be configured to accept and validate the authentication material associated with the selected UltimateAuth authentication mode.\u003C/p\u003E\n\u003Cp\u003EThe Resource Client simplifies communication.\u003C/p\u003E\n\u003Cp\u003EIt does not replace the receiving server\u0027s security responsibilities.\u003C/p\u003E\n\u003Chr\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022device-and-client-profile-metadata\u0022\u003E8. Device and Client Profile Metadata\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth maintains a distinction between user identity, device identity, and client environment.\u003C/p\u003E\n\u003Cp\u003EFor browser resource requests, the transport can propagate UltimateAuth metadata, including:\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\u003EX-UDID\nX-UAuth-ClientProfile\nX-Requested-With\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis keeps resource requests consistent with the UltimateAuth client environment without requiring every application component to construct these headers manually.\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\u003Eawait UAuthClient.Resource.GetAsync\u0026lt;List\u0026lt;Product\u0026gt;\u0026gt;(\u0026quot;/api/products\u0026quot;);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Edoes not require the developer to manually retrieve and attach the device identifier.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022metadata-is-not-proof-of-identity\u0022\u003EMetadata Is Not Proof of Identity\u003C/h3\u003E\n\u003Cp\u003EDevice identifiers and client profile values describe request context.\u003C/p\u003E\n\u003Cp\u003EThey are not independent authentication credentials.\u003C/p\u003E\n\u003Cp\u003EA receiving API must not grant access solely because a request declares a particular device identifier or client profile.\u003C/p\u003E\n\u003Cp\u003EThe server remains responsible for validating the user\u0027s authentication state and permissions.\u003C/p\u003E\n\u003Cp\u003EFor more information, see \u003Ca href=\u0022/docs/fundamentals/client-profiles\u0022\u003EClient Profiles\u003C/a\u003E.\u003C/p\u003E\n\u003Chr\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022browser-transport-and-security\u0022\u003E9. Browser Transport and Security\u003C/h2\u003E\n\u003Cp\u003EThe Blazor Resource Client uses UltimateAuth\u0027s browser transport.\u003C/p\u003E\n\u003Cp\u003EThis allows UltimateAuth to apply consistent request behavior without requiring developers to write a separate JavaScript module for their application.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022origin-validation\u0022\u003EOrigin Validation\u003C/h3\u003E\n\u003Cp\u003EResource requests are constrained by the configured Resource API origin.\u003C/p\u003E\n\u003Cp\u003EThe transport validates request destinations rather than allowing arbitrary URLs to bypass the configured resource boundary.\u003C/p\u003E\n\u003Cp\u003EThis helps prevent application code from unintentionally sending credentials or UltimateAuth request metadata to unrelated destinations.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022redirect-protection\u0022\u003ERedirect Protection\u003C/h3\u003E\n\u003Cp\u003EResource requests use redirect protection.\u003C/p\u003E\n\u003Cp\u003EA request should not silently follow a redirect to an unexpected destination while carrying authentication-related context.\u003C/p\u003E\n\u003Cp\u003EApplications should handle unexpected redirects as transport failures rather than assuming the request succeeded.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022request-cancellation\u0022\u003ERequest Cancellation\u003C/h3\u003E\n\u003Cp\u003EResource operations support \u003Ccode\u003ECancellationToken\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\u003Eusing var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));\n\nvar result = await UAuthClient.Resource.GetAsync\u0026lt;List\u0026lt;Product\u0026gt;\u0026gt;(\u0026quot;/api/products\u0026quot;, cts.Token);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe Blazor transport coordinates cancellation with the underlying browser request.\u003C/p\u003E\n\u003Cp\u003ECancellation is particularly useful when a user navigates away from a page or starts another operation before the previous request completes.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022browser-security-still-applies\u0022\u003EBrowser Security Still Applies\u003C/h3\u003E\n\u003Cp\u003EThe Resource Client does not disable browser security mechanisms.\u003C/p\u003E\n\u003Cp\u003ECross-origin requests remain subject to CORS and browser credential policies.\u003C/p\u003E\n\u003Cp\u003ELikewise, origin validation and redirect protection do not replace:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EAuthentication validation.\u003C/li\u003E\n\u003Cli\u003EAuthorization policies.\u003C/li\u003E\n\u003Cli\u003ECSRF protection where applicable.\u003C/li\u003E\n\u003Cli\u003EInput validation.\u003C/li\u003E\n\u003Cli\u003EServer-side resource ownership checks.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThese responsibilities remain part of the receiving API\u0027s security model.\u003C/p\u003E\n\u003Chr\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022blazor-server-vs.blazor-webassembly\u0022\u003E10. Blazor Server vs. Blazor WebAssembly\u003C/h2\u003E\n\u003Cp\u003EOne of UltimateAuth\u0027s design goals is to provide a consistent client API across supported application environments.\u003C/p\u003E\n\u003Cp\u003EHowever, different hosting models have different execution characteristics.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022blazor-webassembly\u0022\u003EBlazor WebAssembly\u003C/h3\u003E\n\u003Cp\u003EBlazor WebAssembly executes application code in the browser.\u003C/p\u003E\n\u003Cp\u003EIt cannot directly invoke server-side application services or access a server database.\u003C/p\u003E\n\u003Cp\u003EA typical application architecture is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EBlazor WebAssembly\n |\n v\nIUAuthClient.Resource\n |\n v\nResource API\n |\n v\nApplication Services\n |\n v\nDatabase\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe Resource Client provides a natural way to access protected application data from this environment.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022blazor-server\u0022\u003EBlazor Server\u003C/h3\u003E\n\u003Cp\u003EBlazor Server executes component logic on the server.\u003C/p\u003E\n\u003Cp\u003EWhen the application service is available in the same process, a direct service call is often more 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\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\u003EBlazor Server Component\n |\n v\nApplication Service\n |\n v\nDatabase\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 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 products = await productService.GetAllAsync();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThere is no requirement to introduce an HTTP request simply to use the Resource Client.\u003C/p\u003E\n\u003Cp\u003EHowever, when a Blazor Server application needs to communicate with a separate protected Resource API, it can use:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Resource.GetAsync\u0026lt;List\u0026lt;Product\u0026gt;\u0026gt;(\u0026quot;/api/products\u0026quot;);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Cstrong\u003EImportant:\u003C/strong\u003E The current Blazor Resource Client uses browser-based transport, including in Blazor Server applications.\u003C/p\u003E\n\u003Cp\u003EThe HTTP request is initiated by the user\u0027s browser, not by a server-side \u003Ccode\u003EHttpClient\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EFor server-to-server communication, use an appropriate server-side HTTP integration and authentication mechanism.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022the-same-client-api-different-application-architectures\u0022\u003EThe Same Client API, Different Application Architectures\u003C/h3\u003E\n\u003Cp\u003EUltimateAuth aims to make client integration consistent without forcing every application into the same deployment model.\u003C/p\u003E\n\u003Cp\u003EThe goal is not to replace direct application service calls.\u003C/p\u003E\n\u003Cp\u003EThe goal is to remove unnecessary integration work whenever an HTTP resource request is actually needed.\u003C/p\u003E\n\u003Chr\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022handling-unauthorized-responses\u0022\u003E11. Handling Unauthorized Responses\u003C/h2\u003E\n\u003Cp\u003EAn authenticated session may expire or become invalid while an application is running.\u003C/p\u003E\n\u003Cp\u003EWhen a Resource API returns \u003Ccode\u003E401 Unauthorized\u003C/code\u003E, the Resource Client exposes the unsuccessful HTTP response through its standard result contract.\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.Resource.GetAsync\u0026lt;List\u0026lt;Product\u0026gt;\u0026gt;(\u0026quot;/api/products\u0026quot;);\n\nif (!result.IsSuccess \u0026amp;\u0026amp; result.Status == 401)\n{\n // Authentication may need to be renewed.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022automatic-refresh-and-retry\u0022\u003EAutomatic Refresh and Retry\u003C/h3\u003E\n\u003Cp\u003EThe current Resource Client does not automatically refresh authentication and retry a request after receiving a \u003Ccode\u003E401\u003C/code\u003E response.\u003C/p\u003E\n\u003Cp\u003EThis behavior is intentionally outside the initial Resource API implementation.\u003C/p\u003E\n\u003Cp\u003EApplications should not assume that an unsuccessful request has been retried.\u003C/p\u003E\n\u003Cp\u003EAuthentication renewal and retry policies require careful handling, particularly for requests that modify application state.\u003C/p\u003E\n\u003Cp\u003EFuture improvements may provide additional coordination between resource requests and authentication lifecycle management.\u003C/p\u003E\n\u003Cp\u003EUntil then, applications should handle unauthorized responses according to their authentication flow and UI requirements.\u003C/p\u003E\n\u003Chr\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022organizing-application-resource-services\u0022\u003E12. Organizing Application Resource Services\u003C/h2\u003E\n\u003Cp\u003EAlthough Resource Client operations can be called directly from components, larger applications often benefit from small, domain-specific services.\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\u003Eusing CodeBeam.UltimateAuth.Client;\nusing CodeBeam.UltimateAuth.Core.Contracts;\n\npublic sealed class ProductApiService(IUAuthClient client)\n{\n public Task\u0026lt;UAuthResult\u0026lt;List\u0026lt;Product\u0026gt;\u0026gt;\u0026gt; GetAllAsync()\n =\u0026gt; client.Resource.GetAsync\u0026lt;List\u0026lt;Product\u0026gt;\u0026gt;(\u0026quot;/api/products\u0026quot;);\n\n public Task\u0026lt;UAuthResult\u0026lt;Product\u0026gt;\u0026gt; GetAsync(int id)\n =\u0026gt; client.Resource.GetAsync\u0026lt;Product\u0026gt;($\u0026quot;/api/products/{id}\u0026quot;);\n\n public Task\u0026lt;UAuthResult\u0026lt;Product\u0026gt;\u0026gt; CreateAsync(Product product)\n =\u0026gt; client.Resource.PostAsync\u0026lt;Product\u0026gt;(\u0026quot;/api/products\u0026quot;, product);\n\n public Task\u0026lt;UAuthResult\u0026lt;Product\u0026gt;\u0026gt; UpdateAsync(int id, Product product)\n =\u0026gt; client.Resource.PutAsync\u0026lt;Product\u0026gt;($\u0026quot;/api/products/{id}\u0026quot;, product);\n\n public Task\u0026lt;UAuthResult\u0026gt; DeleteAsync(int id)\n =\u0026gt; client.Resource.DeleteAsync($\u0026quot;/api/products/{id}\u0026quot;);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis approach keeps application-specific endpoints and operations in one place.\u003C/p\u003E\n\u003Cp\u003EThe service does not need to implement:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EA custom HTTP transport.\u003C/li\u003E\n\u003Cli\u003EBrowser credential handling.\u003C/li\u003E\n\u003Cli\u003EDevice metadata propagation.\u003C/li\u003E\n\u003Cli\u003EJSON response deserialization.\u003C/li\u003E\n\u003Cli\u003EA separate response wrapper.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThose responsibilities remain with UltimateAuth.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022keep-the-standard-result\u0022\u003EKeep the Standard Result\u003C/h3\u003E\n\u003Cp\u003ENotice that \u003Ccode\u003EProductApiService\u003C/code\u003E returns \u003Ccode\u003EUAuthResult\u0026lt;T\u0026gt;\u003C/code\u003E directly.\u003C/p\u003E\n\u003Cp\u003EThere is no need to create another response type merely to wrap the same status, value, and problem information.\u003C/p\u003E\n\u003Cp\u003EThis keeps the application aligned with the UltimateAuth SDK.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022application-services-still-own-business-logic\u0022\u003EApplication Services Still Own Business Logic\u003C/h3\u003E\n\u003Cp\u003EThe Resource Client does not attempt to understand application-specific business rules.\u003C/p\u003E\n\u003Cp\u003EA \u003Ccode\u003EProductApiService\u003C/code\u003E can organize product-related operations, but the Resource API remains responsible for validating product data, enforcing ownership, and authorizing changes.\u003C/p\u003E\n\u003Cp\u003EThis separation keeps UltimateAuth focused on the infrastructure it can provide consistently.\u003C/p\u003E\n\u003Chr\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022design-philosophy\u0022\u003E13. Design Philosophy\u003C/h2\u003E\n\u003Cp\u003EThe Resource Client follows the same principle as the rest of UltimateAuth:\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EComplexity should be handled by the framework when it is common, predictable, and independent of application business logic.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EDevelopers should not need to repeatedly solve browser transport, credential configuration, device metadata, and response handling simply because their application has its own API.\u003C/p\u003E\n\u003Cp\u003EAt the same time, a framework should not hide important security boundaries or take ownership of application-specific decisions.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth therefore provides:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EA unified client entry point.\u003C/li\u003E\n\u003Cli\u003EFamiliar HTTP operations.\u003C/li\u003E\n\u003Cli\u003EConsistent result contracts.\u003C/li\u003E\n\u003Cli\u003EConfigurable resource origins and credential policies.\u003C/li\u003E\n\u003Cli\u003EBrowser-aware transport behavior.\u003C/li\u003E\n\u003Cli\u003EExplicit server-side security responsibilities.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThe result is a simpler developer experience without requiring applications to abandon their existing architecture.\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// Authentication\nawait UAuthClient.Flows.RefreshAsync();\n\n// Identity\nvar user = await UAuthClient.Users.GetMeAsync();\n\n// Sessions\nvar sessions = await UAuthClient.Sessions.GetMyChainsAsync(\n new PageRequest\n {\n PageNumber = 1,\n PageSize = 10\n });\n\n// Application resources\nvar products = await UAuthClient.Resource.GetAsync\u0026lt;List\u0026lt;Product\u0026gt;\u0026gt;(\u0026quot;/api/products\u0026quot;);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EDifferent responsibilities. One consistent client model.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EUltimateAuth does not stop at authentication. It helps applications use authentication without rebuilding the integration layer around it.\u003C/strong\u003E\u003C/p\u003E\n\u003Chr\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022related-documentation\u0022\u003ERelated Documentation\u003C/h2\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/client-sdk/overview\u0022\u003EClient SDK Overview\u003C/a\u003E \u2014 Understand the unified client architecture.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/configuration/client-options\u0022\u003EClient Options\u003C/a\u003E \u2014 Configure client profiles, authentication endpoints, and resource options.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/client-sdk/authentication\u0022\u003EAuthentication\u003C/a\u003E \u2014 Work with authentication flows and session renewal.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/fundamentals/client-profiles\u0022\u003EClient Profiles\u003C/a\u003E \u2014 Understand runtime-specific authentication behavior.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/getting-started/real-world-setup\u0022\u003EReal-World Setup\u003C/a\u003E \u2014 Configure Blazor applications, UAuthHub, and separate Resource APIs.\u003C/li\u003E\n\u003C/ul\u003E\n", + "Headings": [ + { + "Id": "the-problem-authentication-ends-integration-work-begins", + "Text": "1. The Problem: Authentication Ends, Integration Work Begins", + "Level": 0 + }, + { + "Id": "a-unified-client-model", + "Text": "A Unified Client Model", + "Level": 1 + }, + { + "Id": "getting-started", + "Text": "2. Getting Started", + "Level": 0 + }, + { + "Id": "inject-iuauthclient", + "Text": "Inject IUAuthClient", + "Level": 1 + }, + { + "Id": "what-happens-behind-the-call", + "Text": "What Happens Behind the Call?", + "Level": 1 + }, + { + "Id": "supported-http-operations", + "Text": "3. Supported HTTP Operations", + "Level": 0 + }, + { + "Id": "get", + "Text": "GET", + "Level": 1 + }, + { + "Id": "post", + "Text": "POST", + "Level": 1 + }, + { + "Id": "put", + "Text": "PUT", + "Level": 1 + }, + { + "Id": "patch", + "Text": "PATCH", + "Level": 1 + }, + { + "Id": "delete", + "Text": "DELETE", + "Level": 1 + }, + { + "Id": "consistent-results-with-uauthresult", + "Text": "4. Consistent Results with UAuthResult", + "Level": 0 + }, + { + "Id": "result-properties", + "Text": "Result Properties", + "Level": 1 + }, + { + "Id": "structured-problems", + "Text": "Structured Problems", + "Level": 1 + }, + { + "Id": "transport-failures-are-different", + "Text": "Transport Failures Are Different", + "Level": 1 + }, + { + "Id": "resource-api-configuration", + "Text": "5. Resource API Configuration", + "Level": 0 + }, + { + "Id": "baseaddress", + "Text": "BaseAddress", + "Level": 1 + }, + { + "Id": "baseaddress-validation", + "Text": "BaseAddress Validation", + "Level": 1 + }, + { + "Id": "browser-credential-handling", + "Text": "6. Browser Credential Handling", + "Level": 0 + }, + { + "Id": "same-origin-resource-api", + "Text": "Same-Origin Resource API", + "Level": 1 + }, + { + "Id": "separate-resource-api", + "Text": "Separate Resource API", + "Level": 1 + }, + { + "Id": "when-to-use-omit", + "Text": "When to Use Omit", + "Level": 1 + }, + { + "Id": "separate-uauthhub-and-resource-api", + "Text": "7. Separate UAuthHub and Resource API", + "Level": 0 + }, + { + "Id": "authentication-does-not-automatically-protect-an-api", + "Text": "Authentication Does Not Automatically Protect an API", + "Level": 1 + }, + { + "Id": "device-and-client-profile-metadata", + "Text": "8. Device and Client Profile Metadata", + "Level": 0 + }, + { + "Id": "metadata-is-not-proof-of-identity", + "Text": "Metadata Is Not Proof of Identity", + "Level": 1 + }, + { + "Id": "browser-transport-and-security", + "Text": "9. Browser Transport and Security", + "Level": 0 + }, + { + "Id": "origin-validation", + "Text": "Origin Validation", + "Level": 1 + }, + { + "Id": "redirect-protection", + "Text": "Redirect Protection", + "Level": 1 + }, + { + "Id": "request-cancellation", + "Text": "Request Cancellation", + "Level": 1 + }, + { + "Id": "browser-security-still-applies", + "Text": "Browser Security Still Applies", + "Level": 1 + }, + { + "Id": "blazor-server-vs.blazor-webassembly", + "Text": "10. Blazor Server vs. Blazor WebAssembly", + "Level": 0 + }, + { + "Id": "blazor-webassembly", + "Text": "Blazor WebAssembly", + "Level": 1 + }, + { + "Id": "blazor-server", + "Text": "Blazor Server", + "Level": 1 + }, + { + "Id": "the-same-client-api-different-application-architectures", + "Text": "The Same Client API, Different Application Architectures", + "Level": 1 + }, + { + "Id": "handling-unauthorized-responses", + "Text": "11. Handling Unauthorized Responses", + "Level": 0 + }, + { + "Id": "automatic-refresh-and-retry", + "Text": "Automatic Refresh and Retry", + "Level": 1 + }, + { + "Id": "organizing-application-resource-services", + "Text": "12. Organizing Application Resource Services", + "Level": 0 + }, + { + "Id": "keep-the-standard-result", + "Text": "Keep the Standard Result", + "Level": 1 + }, + { + "Id": "application-services-still-own-business-logic", + "Text": "Application Services Still Own Business Logic", + "Level": 1 + }, + { + "Id": "design-philosophy", + "Text": "13. Design Philosophy", + "Level": 0 + }, + { + "Id": "related-documentation", + "Text": "Related Documentation", + "Level": 0 + } + ], + "SourceHash": "693B2C9C5D37C80F519FFABBB88A6D89477FA66160733A68725E043991325317", + "BuilderVersion": "1" +} \ No newline at end of file diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/configuration/client-options.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/configuration/client-options.json index 800caaef..d5a6794b 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/configuration/client-options.json +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/configuration/client-options.json @@ -1,7 +1,7 @@ { "Slug": "configuration/client-options", "Title": "Client Options", - "Html": "\n\u003Cp\u003E\u003Ccode\u003EUAuthClientOptions\u003C/code\u003E controls how an UltimateAuth client communicates with the authentication server, executes authentication flows, maintains client-side authentication state, and responds when authentication must be renewed.\u003C/p\u003E\n\u003Cp\u003EThe client configuration is intentionally separate from server security configuration.\u003C/p\u003E\n\u003Cp\u003EThe server determines which authentication operations are permitted, how credentials are validated, and when sessions expire. The client determines how those operations are initiated and how the application responds to their outcomes.\u003C/p\u003E\n\u003Cp\u003EThis guide covers the current client configuration API, platform integration, defaults, practical examples, and important security considerations.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022register-the-ultimateauth-client\u0022\u003E1. Register the UltimateAuth Client\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth provides a platform-independent client SDK and platform-specific adapters.\u003C/p\u003E\n\u003Cp\u003EFor Blazor applications, use:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.AutoDetectClientProfile = true;\n options.DefaultReturnUrl = \u0026quot;/\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EAddUltimateAuthClientBlazor()\u003C/code\u003E registers the underlying client services and the Blazor-specific infrastructure, including browser transport, storage, device identification, and authentication state integration.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022core-client-registration\u0022\u003ECore Client Registration\u003C/h3\u003E\n\u003Cp\u003EThe platform-independent registration 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\u003Ebuilder.Services.AddUltimateAuthClient(options =\u0026gt;\n{\n options.AutoDetectClientProfile = true;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis registers the shared client SDK.\u003C/p\u003E\n\u003Cp\u003EHowever, it does not provide every platform-specific implementation needed to run authentication flows.\u003C/p\u003E\n\u003Cp\u003EApplications should use an appropriate platform adapter rather than relying on the core client registration alone.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022do-not-register-both-client-methods\u0022\u003EDo Not Register Both Client Methods\u003C/h3\u003E\n\u003Cp\u003EWhen using:\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.AddUltimateAuthClientBlazor();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Edo not also call:\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.AddUltimateAuthClient();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe Blazor registration already includes the core client services.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022server-and-client-in-one-application\u0022\u003EServer and Client in One Application\u003C/h3\u003E\n\u003Cp\u003EA Blazor Server application can register both UltimateAuth Server and the Blazor client adapter:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Session.Lifetime = TimeSpan.FromDays(7);\n});\n\nbuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.ClientProfile = UAuthClientProfile.BlazorServer;\n options.AutoDetectClientProfile = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThese registrations configure different components.\u003C/p\u003E\n\u003Cp\u003EThe server controls authentication security and lifecycle rules.\u003C/p\u003E\n\u003Cp\u003EThe client controls the application-side integration.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022client-options-architecture\u0022\u003E2. Client Options Architecture\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthClientOptions\u003C/code\u003E contains 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\u003EType\u003C/th\u003E\n\u003Cth\u003EPurpose\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EClientProfile\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthClientProfile\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifies the client platform\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAutoDetectClientProfile\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Ebool\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEnables automatic profile detection\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDefaultReturnUrl\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Estring?\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EDefault navigation destination after authentication\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEndpoints\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthClientEndpointOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication endpoint paths\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELogin\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthClientLoginFlowOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELogin flow configuration\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPkce\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthClientPkceLoginFlowOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPKCE flow configuration\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAutoRefresh\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthClientAutoRefreshOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EBackground refresh coordination\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EReauth\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthClientReauthOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReauthentication behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMultiTenant\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthClientMultiTenantOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETenant propagation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EStateEvents\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthStateEventOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication state event handling\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThese options are configured through the same \u003Ccode\u003EUAuthClientOptions\u003C/code\u003E instance.\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\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.DefaultReturnUrl = \u0026quot;/dashboard\u0026quot;;\n options.Pkce.Enabled = true;\n options.AutoRefresh.Interval = TimeSpan.FromMinutes(5);\n options.StateEvents.HandlingMode = UAuthStateEventHandlingMode.Patch;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022client-profiles\u0022\u003E3. Client Profiles\u003C/h2\u003E\n\u003Cp\u003EThe client profile identifies the application environment and influences authentication flow behavior.\u003C/p\u003E\n\u003Cp\u003EConfigure it through:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.ClientProfile\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUltimateAuth defines profiles including:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProfile\u003C/th\u003E\n\u003Cth\u003ETypical application\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EBlazorServer\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EInteractive Blazor Server applications\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EBlazorWasm\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EBrowser-hosted Blazor WebAssembly\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaui\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E.NET MAUI applications\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EWebServer\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETraditional ASP.NET Core web applications\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EApi\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAPI-oriented clients\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthHub\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUltimateAuth Hub\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ENotSpecified\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EProfile not explicitly selected\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EDifferent client profiles may result in different authentication modes on the server.\u003C/p\u003E\n\u003Cp\u003EFor example, the default server mode resolver maps Blazor Server to \u003Ccode\u003EPureOpaque\u003C/code\u003E and Blazor WebAssembly to \u003Ccode\u003EHybrid\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe client profile does not itself authorize an authentication mode. The server must support and permit the resolved mode.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022automatic-client-profile-detection\u0022\u003E4. Automatic Client Profile Detection\u003C/h2\u003E\n\u003Cp\u003EAutomatic profile detection is enabled by 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.AutoDetectClientProfile = true;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWhen enabled, UltimateAuth detects the client environment if \u003Ccode\u003EClientProfile\u003C/code\u003E remains \u003Ccode\u003ENotSpecified\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe current detector checks for:\u003C/p\u003E\n\u003Col\u003E\n\u003Cli\u003EUAuthHub registration.\u003C/li\u003E\n\u003Cli\u003E.NET MAUI.\u003C/li\u003E\n\u003Cli\u003EBlazor WebAssembly.\u003C/li\u003E\n\u003Cli\u003EBlazor Server.\u003C/li\u003E\n\u003Cli\u003EA fallback to \u003Ccode\u003EWebServer\u003C/code\u003E.\u003C/li\u003E\n\u003C/ol\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022default-configuration\u0022\u003EDefault Configuration\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.AutoDetectClientProfile = true;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ENo explicit profile is required for ordinary supported environments where detection succeeds.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022manual-configuration\u0022\u003EManual Configuration\u003C/h3\u003E\n\u003Cp\u003EFor applications that need predictable behavior independent of assembly-based detection:\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.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.AutoDetectClientProfile = false;\n options.ClientProfile = UAuthClientProfile.BlazorWasm;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EManual configuration can be useful for:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003ESpecialized hosting environments.\u003C/li\u003E\n\u003Cli\u003EIntegration tests.\u003C/li\u003E\n\u003Cli\u003EApplications with multiple hosting models.\u003C/li\u003E\n\u003Cli\u003ECases where assembly-based detection is ambiguous.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022important-validation-rule\u0022\u003EImportant Validation Rule\u003C/h3\u003E\n\u003Cp\u003EThis configuration is invalid:\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.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.AutoDetectClientProfile = false;\n options.ClientProfile = UAuthClientProfile.NotSpecified;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWhen automatic detection is disabled, an explicit client profile must be provided.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022explicit-profiles-take-precedence\u0022\u003EExplicit Profiles Take Precedence\u003C/h3\u003E\n\u003Cp\u003EThe post-configuration detector does not replace a profile that has already been explicitly selected.\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\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.ClientProfile = UAuthClientProfile.BlazorServer;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe explicit profile is retained even when automatic detection remains enabled.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022detection-limitations\u0022\u003EDetection Limitations\u003C/h3\u003E\n\u003Cp\u003EThe current detector uses service registration and loaded assemblies to identify the environment.\u003C/p\u003E\n\u003Cp\u003EAssembly-based detection may not be completely reliable in every hosting arrangement.\u003C/p\u003E\n\u003Cp\u003EFor complex Blazor Web App hosting scenarios, explicitly configuring the profile can make authentication behavior easier to reason about.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022endpoint-configuration\u0022\u003E5. Endpoint Configuration\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Endpoints\u003C/code\u003E defines the endpoint paths used by the client SDK.\u003C/p\u003E\n\u003Cp\u003EThe default base path is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003E/auth\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configure-a-remote-authentication-server\u0022\u003EConfigure a Remote Authentication Server\u003C/h3\u003E\n\u003Cp\u003EFor a client communicating with a separately hosted UAuthHub:\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.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Endpoints.BasePath = \u0026quot;https://auth.example.com/auth\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EFor an application using same-origin authentication endpoints:\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.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Endpoints.BasePath = \u0026quot;/auth\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe base path must match the deployed authentication server\u0027s routing configuration.\u003C/p\u003E\n\u003Cp\u003EChanging a client endpoint does not create or expose the corresponding server endpoint.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022default-endpoint-paths\u0022\u003EDefault Endpoint Paths\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EBasePath\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E/auth\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELogin\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E/login\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETryLogin\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E/try-login\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELogout\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E/logout\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERefresh\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E/refresh\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EReauth\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E/reauth\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EValidate\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E/validate\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPkceAuthorize\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E/pkce/authorize\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPkceTryComplete\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E/pkce/try-complete\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPkceComplete\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E/pkce/complete\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EHubLoginPath\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E/uauthhub/entry\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022customize-individual-paths\u0022\u003ECustomize Individual Paths\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Endpoints.BasePath = \u0026quot;/auth\u0026quot;;\n options.Endpoints.Login = \u0026quot;/login\u0026quot;;\n options.Endpoints.Logout = \u0026quot;/logout\u0026quot;;\n options.Endpoints.Refresh = \u0026quot;/refresh\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThese values should reflect the actual routes registered by UltimateAuth Server.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022required-endpoint-validation\u0022\u003ERequired Endpoint Validation\u003C/h3\u003E\n\u003Cp\u003EThe client options validator requires:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003EEndpoints.BasePath\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EEndpoints.Login\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EEndpoints.Logout\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EEndpoints.Refresh\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EEndpoints.Validate\u003C/code\u003E\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThese properties must not be null, empty, or whitespace-only.\u003C/p\u003E\n\u003Cp\u003EThe current validator does not apply the same required-value checks to every optional endpoint path.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022remote-clients-and-browser-security\u0022\u003ERemote Clients and Browser Security\u003C/h3\u003E\n\u003Cp\u003EWhen using a remote authentication server, also consider:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EHTTPS.\u003C/li\u003E\n\u003Cli\u003ECross-origin request configuration.\u003C/li\u003E\n\u003Cli\u003ECookie \u003Ccode\u003ESameSite\u003C/code\u003E and \u003Ccode\u003ESecure\u003C/code\u003E attributes.\u003C/li\u003E\n\u003Cli\u003EAllowed client origins.\u003C/li\u003E\n\u003Cli\u003EPKCE redirects.\u003C/li\u003E\n\u003Cli\u003EBrowser credential policies.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EA correct endpoint URL alone does not guarantee that cross-origin authentication will work.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022login-flow-options\u0022\u003E6. Login Flow Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Login\u003C/code\u003E controls client-side login behavior.\u003C/p\u003E\n\u003Cp\u003EThe available properties are:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003Cth\u003EPurpose\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EReturnUrl\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELogin-specific return destination\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAllowCredentialPost\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAllow direct credential posting\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=\u0022configure-login-navigation\u0022\u003EConfigure Login Navigation\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Login.ReturnUrl = \u0026quot;/dashboard\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe login-specific return URL is used when the login flow does not provide a more specific destination.\u003C/p\u003E\n\u003Cp\u003EIf it is not set, UltimateAuth can fall back to \u003Ccode\u003EDefaultReturnUrl\u003C/code\u003E or the current location according to the flow.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022direct-credential-posting\u0022\u003EDirect Credential Posting\u003C/h3\u003E\n\u003Cp\u003EThe default is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.Login.AllowCredentialPost = false;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis is an important security setting.\u003C/p\u003E\n\u003Cp\u003EDirectly posting user credentials is not appropriate for public browser clients such as Blazor WebAssembly applications.\u003C/p\u003E\n\u003Cp\u003EPublic clients should use the supported PKCE-based login flow.\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\u003EA trusted server-hosted application may explicitly enable direct credential posting where its authentication architecture requires it:\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.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.ClientProfile = UAuthClientProfile.BlazorServer;\n options.Login.AllowCredentialPost = true;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EDo not enable this setting for a public client merely to simplify login implementation.\u003C/p\u003E\n\u003Cp\u003EThe server still determines whether the requested authentication operation is permitted.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022pkce-configuration\u0022\u003E7. PKCE Configuration\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Pkce\u003C/code\u003E controls client-side PKCE login behavior.\u003C/p\u003E\n\u003Cp\u003EPKCE allows a public client to complete an authorization flow without relying on a confidential client secret.\u003C/p\u003E\n\u003Cp\u003EThe current options are:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnabled\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEnable client PKCE support\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\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPost-login return destination\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAutoRedirect\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAutomatically navigate after beginning PKCE\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EOnAuthorized\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECallback after the authorization response is issued\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=\u0022basic-configuration\u0022\u003EBasic Configuration\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Pkce.Enabled = true;\n options.Pkce.ReturnUrl = \u0026quot;/dashboard\u0026quot;;\n options.Pkce.AutoRedirect = true;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022disable-automatic-redirect\u0022\u003EDisable Automatic Redirect\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Pkce.AutoRedirect = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWhen \u003Ccode\u003EAutoRedirect\u003C/code\u003E is disabled, \u003Ccode\u003EBeginPkceAsync\u003C/code\u003E does not automatically navigate to the next location.\u003C/p\u003E\n\u003Cp\u003EThe application is responsible for handling navigation.\u003C/p\u003E\n\u003Cp\u003EThis can be useful when integrating UltimateAuth into an existing routing or navigation system.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022authorization-callback\u0022\u003EAuthorization Callback\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Pkce.OnAuthorized = response =\u0026gt;\n {\n // Inspect or process the authorization response.\n return Task.CompletedTask;\n };\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe callback receives:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EPkceAuthorizeResponse\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EIt runs after the authorization response is issued and before the normal Hub navigation.\u003C/p\u003E\n\u003Cp\u003EUse the callback for application-specific integration, not to bypass PKCE verification.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022client-and-server-pkce-settings-are-different\u0022\u003EClient and Server PKCE Settings Are Different\u003C/h3\u003E\n\u003Cp\u003EClient configuration:\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.AutoRedirect = true;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EServer configuration:\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.AuthorizationCodeLifetimeSeconds = 120;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe client controls how the flow is initiated and navigated.\u003C/p\u003E\n\u003Cp\u003EThe server controls the authorization code\u0027s validity and verification.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022return-url-configuration\u0022\u003E8. Return URL Configuration\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth supports several return URL configuration points.\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\u003EDefaultReturnUrl\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EGlobal default for interactive authentication\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELogin.ReturnUrl\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELogin-specific default\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPkce.ReturnUrl\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPKCE-specific default\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=\u0022global-default\u0022\u003EGlobal Default\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.DefaultReturnUrl = \u0026quot;/dashboard\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022flow-specific-defaults\u0022\u003EFlow-Specific Defaults\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.DefaultReturnUrl = \u0026quot;/\u0026quot;;\n options.Login.ReturnUrl = \u0026quot;/account\u0026quot;;\n options.Pkce.ReturnUrl = \u0026quot;/dashboard\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EFlow-specific values allow different authentication paths to use different destinations.\u003C/p\u003E\n\u003Cp\u003EAn explicitly supplied return URL for an individual operation may take precedence over these defaults.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-considerations\u0022\u003ESecurity Considerations\u003C/h3\u003E\n\u003Cp\u003EReturn URLs are navigation destinations and must be treated carefully.\u003C/p\u003E\n\u003Cp\u003EDo not redirect users to arbitrary external URLs without appropriate validation.\u003C/p\u003E\n\u003Cp\u003EOpen redirects can be abused for phishing and other attacks.\u003C/p\u003E\n\u003Cp\u003EPrefer local application routes unless cross-origin navigation is an intentional and validated part of the authentication architecture.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022automatic-refresh\u0022\u003E9. Automatic Refresh\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.AutoRefresh\u003C/code\u003E controls background refresh coordination.\u003C/p\u003E\n\u003Cp\u003EThe available properties are:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnabled\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEnables or disables automatic background refresh\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EInterval\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EInterval between background refresh attempts\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=\u0022configure-automatic-refresh\u0022\u003EConfigure Automatic Refresh\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.AutoRefresh.Enabled = true;\n options.AutoRefresh.Interval = TimeSpan.FromMinutes(5);\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022disable-automatic-refresh\u0022\u003EDisable Automatic Refresh\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.AutoRefresh.Enabled = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022default-interval\u0022\u003EDefault Interval\u003C/h3\u003E\n\u003Cp\u003EIf \u003Ccode\u003EInterval\u003C/code\u003E is not supplied, the client registration applies:\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.Interval ??= TimeSpan.FromMinutes(5);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe resulting default interval is five minutes.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022platform-behavior\u0022\u003EPlatform Behavior\u003C/h3\u003E\n\u003Cp\u003EThe effective \u003Ccode\u003EEnabled\u003C/code\u003E setting should be considered together with the platform\u0027s post-configuration and session coordination behavior.\u003C/p\u003E\n\u003Cp\u003EThe current Blazor session coordinator checks \u003Ccode\u003EAutoRefresh.Enabled\u003C/code\u003E before starting its background loop.\u003C/p\u003E\n\u003Cp\u003EWhen enabled, it periodically invokes:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthClient.Flows.RefreshAsync(true);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022automatic-refresh-is-not-a-security-policy\u0022\u003EAutomatic Refresh Is Not a Security Policy\u003C/h3\u003E\n\u003Cp\u003EAutomatic refresh does not control:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EServer session lifetime.\u003C/li\u003E\n\u003Cli\u003ERefresh token lifetime.\u003C/li\u003E\n\u003Cli\u003ESession revocation.\u003C/li\u003E\n\u003Cli\u003ECredential recovery policies.\u003C/li\u003E\n\u003Cli\u003ERefresh token replay protection.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThese remain server-side responsibilities.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022automatic-refresh-does-not-guarantee-session-continuity\u0022\u003EAutomatic Refresh Does Not Guarantee Session Continuity\u003C/h3\u003E\n\u003Cp\u003EBackground refresh may fail because:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EThe session has expired.\u003C/li\u003E\n\u003Cli\u003EThe refresh credential has been revoked.\u003C/li\u003E\n\u003Cli\u003EThe browser is suspended.\u003C/li\u003E\n\u003Cli\u003ENetwork connectivity is unavailable.\u003C/li\u003E\n\u003Cli\u003EThe server requires reauthentication.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EAutomatic refresh is a convenience and continuity mechanism, not a guarantee that authentication remains valid indefinitely.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022coordinator-lifecycle\u0022\u003ECoordinator Lifecycle\u003C/h3\u003E\n\u003Cp\u003EThe Blazor adapter provides an \u003Ccode\u003EISessionCoordinator\u003C/code\u003E implementation.\u003C/p\u003E\n\u003Cp\u003EThe coordinator must be started by the application\u0027s supported initialization lifecycle.\u003C/p\u003E\n\u003Cp\u003ESetting \u003Ccode\u003EAutoRefresh.Enabled = true\u003C/code\u003E does not mean that a background task automatically runs without the coordinator being started.\u003C/p\u003E\n\u003Cp\u003EApplications should follow the client initialization guidance for their hosting model.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022reauthentication-options\u0022\u003E10. Reauthentication Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Reauth\u003C/code\u003E controls how the Blazor session coordinator responds when refresh reports that reauthentication is required.\u003C/p\u003E\n\u003Cp\u003EThe available properties are:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EBehavior\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EReauthBehavior.Redirect\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERedirectPath\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-behaviors\u0022\u003EAvailable Behaviors\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\u003EReauthBehavior.Redirect\nReauthBehavior.None\nReauthBehavior.RaiseEvent\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022redirect\u0022\u003ERedirect\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Reauth.Behavior = ReauthBehavior.Redirect;\n options.Reauth.RedirectPath = \u0026quot;/login\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWhen the coordinator receives a \u003Ccode\u003EReauthRequired\u003C/code\u003E refresh outcome, it navigates to the configured redirect path.\u003C/p\u003E\n\u003Cp\u003EIf \u003Ccode\u003ERedirectPath\u003C/code\u003E is null, the coordinator falls back to \u003Ccode\u003EEndpoints.Login\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe current Blazor coordinator uses forced navigation for this redirect.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022raise-an-event\u0022\u003ERaise an Event\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Reauth.Behavior = ReauthBehavior.RaiseEvent;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis allows an application to handle reauthentication through the coordinator\u0027s \u003Ccode\u003EReauthRequired\u003C/code\u003E event.\u003C/p\u003E\n\u003Cp\u003EFor example, an application may display a reauthentication dialog rather than immediately navigating.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022no-automatic-action\u0022\u003ENo Automatic Action\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Reauth.Behavior = ReauthBehavior.None;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWith \u003Ccode\u003ENone\u003C/code\u003E, the coordinator does not automatically redirect or raise its reauthentication event.\u003C/p\u003E\n\u003Cp\u003EThe application must still handle unauthenticated states appropriately.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022important\u0022\u003EImportant\u003C/h3\u003E\n\u003Cp\u003EReauthentication behavior does not make an expired or revoked session valid.\u003C/p\u003E\n\u003Cp\u003EIt only controls how the client responds when authentication must be reestablished.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022multi-tenant-client-configuration\u0022\u003E11. Multi-Tenant Client Configuration\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.MultiTenant\u003C/code\u003E controls tenant propagation from the client to the authentication server.\u003C/p\u003E\n\u003Cp\u003EThe client does not independently authorize or resolve tenant ownership.\u003C/p\u003E\n\u003Cp\u003EIt carries tenant context using the configured transport.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-properties\u0022\u003EAvailable Properties\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnabled\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEnable tenant propagation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETenant\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETenant identifier to propagate\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETransport\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003ETenantTransport.None\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETenant transport mechanism\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=\u0022available-transports\u0022\u003EAvailable Transports\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\u003ETenantTransport.None\nTenantTransport.Header\nTenantTransport.Route\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022header-based-tenant-propagation\u0022\u003EHeader-Based Tenant Propagation\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.MultiTenant.Enabled = true;\n options.MultiTenant.Tenant = \u0026quot;tenant-a\u0026quot;;\n options.MultiTenant.Transport = TenantTransport.Header;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe server must be configured to resolve tenant information from the corresponding transport.\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.MultiTenant.Enabled = true;\n options.MultiTenant.EnableHeader = true;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022route-based-tenant-propagation\u0022\u003ERoute-Based Tenant Propagation\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.MultiTenant.Enabled = true;\n options.MultiTenant.Tenant = \u0026quot;tenant-a\u0026quot;;\n options.MultiTenant.Transport = TenantTransport.Route;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe server must expose routes compatible with tenant-based routing.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-boundary\u0022\u003ESecurity Boundary\u003C/h3\u003E\n\u003Cp\u003EA client-provided tenant identifier is not proof of tenant membership.\u003C/p\u003E\n\u003Cp\u003EThe server must validate the resolved tenant and enforce tenant isolation.\u003C/p\u003E\n\u003Cp\u003ENever rely on client-side tenant selection as an authorization boundary.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022authentication-state-events\u0022\u003E12. Authentication State Events\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.StateEvents\u003C/code\u003E controls how the client processes authentication-related state events.\u003C/p\u003E\n\u003Cp\u003EThe available property is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.StateEvents.HandlingMode\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EIts default is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthStateEventHandlingMode.Patch\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-modes\u0022\u003EAvailable Modes\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EMode\u003C/th\u003E\n\u003Cth\u003EBehavior\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPatch\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EApply a local authentication state update\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EValidate\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERevalidate authentication state after the event\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ENone\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EDo not update authentication state in response to the event\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=\u0022patch\u0022\u003EPatch\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.StateEvents.HandlingMode = UAuthStateEventHandlingMode.Patch;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EPatch\u003C/code\u003E is the default.\u003C/p\u003E\n\u003Cp\u003EIt allows the client to update local state without performing a full authentication validation for every event.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022validate\u0022\u003EValidate\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.StateEvents.HandlingMode = UAuthStateEventHandlingMode.Validate;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EValidate\u003C/code\u003E causes the client to revalidate authentication state in response to events.\u003C/p\u003E\n\u003Cp\u003EThis may introduce additional requests but can be appropriate when the application prefers server-confirmed state updates.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022none\u0022\u003ENone\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.StateEvents.HandlingMode = UAuthStateEventHandlingMode.None;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003ENone\u003C/code\u003E disables automatic authentication state updates in response to these events.\u003C/p\u003E\n\u003Cp\u003EApplications using this mode must provide any required state synchronization through other mechanisms.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022local-events-are-not-distributed-notifications\u0022\u003ELocal Events Are Not Distributed Notifications\u003C/h3\u003E\n\u003Cp\u003EClient state events do not automatically notify every browser, device, or application instance.\u003C/p\u003E\n\u003Cp\u003EFor example, an administrator changing a user\u0027s role does not automatically push an event to that user\u0027s open browser.\u003C/p\u003E\n\u003Cp\u003EDistributed synchronization requires additional application infrastructure.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022configuration-from-appsettings.json\u0022\u003E13. Configuration from appsettings.json\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth Client binds the configuration section:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EUltimateAuth:Client\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EJSON\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-json\u0022\u003E{\n \u0026quot;UltimateAuth\u0026quot;: {\n \u0026quot;Client\u0026quot;: {\n \u0026quot;AutoDetectClientProfile\u0026quot;: true,\n \u0026quot;DefaultReturnUrl\u0026quot;: \u0026quot;/dashboard\u0026quot;,\n \u0026quot;Endpoints\u0026quot;: {\n \u0026quot;BasePath\u0026quot;: \u0026quot;https://auth.example.com/auth\u0026quot;\n },\n \u0026quot;Pkce\u0026quot;: {\n \u0026quot;Enabled\u0026quot;: true,\n \u0026quot;ReturnUrl\u0026quot;: \u0026quot;/dashboard\u0026quot;,\n \u0026quot;AutoRedirect\u0026quot;: true\n },\n \u0026quot;AutoRefresh\u0026quot;: {\n \u0026quot;Enabled\u0026quot;: true,\n \u0026quot;Interval\u0026quot;: \u0026quot;00:05:00\u0026quot;\n },\n \u0026quot;Reauth\u0026quot;: {\n \u0026quot;Behavior\u0026quot;: \u0026quot;Redirect\u0026quot;,\n \u0026quot;RedirectPath\u0026quot;: \u0026quot;/login\u0026quot;\n },\n \u0026quot;StateEvents\u0026quot;: {\n \u0026quot;HandlingMode\u0026quot;: \u0026quot;Patch\u0026quot;\n }\n }\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe example uses the standard ASP.NET Core configuration binding conventions.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configuration-precedence\u0022\u003EConfiguration Precedence\u003C/h3\u003E\n\u003Cp\u003EThe registration pipeline applies:\u003C/p\u003E\n\u003Col\u003E\n\u003Cli\u003EOption type defaults.\u003C/li\u003E\n\u003Cli\u003EThe configuration delegate.\u003C/li\u003E\n\u003Cli\u003EValues from \u003Ccode\u003EUltimateAuth:Client\u003C/code\u003E.\u003C/li\u003E\n\u003Cli\u003EPost-configuration processing.\u003C/li\u003E\n\u003Cli\u003ERegistered options validation when the options are resolved.\u003C/li\u003E\n\u003C/ol\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.DefaultReturnUrl = \u0026quot;/\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWith:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EJSON\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-json\u0022\u003E{\n \u0026quot;UltimateAuth\u0026quot;: {\n \u0026quot;Client\u0026quot;: {\n \u0026quot;DefaultReturnUrl\u0026quot;: \u0026quot;/dashboard\u0026quot;\n }\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe configured return URL becomes \u003Ccode\u003E/dashboard\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022environment-variables\u0022\u003EEnvironment Variables\u003C/h3\u003E\n\u003Cp\u003EASP.NET Core supports double underscores for hierarchical configuration keys.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EUltimateAuth__Client__Endpoints__BasePath=https://auth.example.com/auth\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis corresponds 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\u003EUltimateAuth:Client:Endpoints:BasePath\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUse environment-specific configuration for deployment-dependent values such as authentication server addresses and application return paths.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022client-options-validation\u0022\u003E14. Client Options Validation\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth registers validators for:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthClientOptions\nUAuthClientEndpointOptions\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022client-profile-validation\u0022\u003EClient Profile Validation\u003C/h3\u003E\n\u003Cp\u003EThe validator rejects a configuration where:\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.AutoDetectClientProfile = false;\noptions.ClientProfile = UAuthClientProfile.NotSpecified;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAn explicit profile is required when detection is disabled.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022endpoint-validation\u0022\u003EEndpoint Validation\u003C/h3\u003E\n\u003Cp\u003EThe following endpoint properties must be present:\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\u003EEndpoints.BasePath\nEndpoints.Login\nEndpoints.Logout\nEndpoints.Refresh\nEndpoints.Validate\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EFor example, this configuration is invalid:\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.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Endpoints.BasePath = \u0026quot;\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022validation-timing\u0022\u003EValidation Timing\u003C/h3\u003E\n\u003Cp\u003EUnlike the server registration, the current client registration does not explicitly call \u003Ccode\u003EValidateOnStart()\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EValidation therefore occurs through the normal options resolution lifecycle.\u003C/p\u003E\n\u003Cp\u003EApplications that require early validation can arrange for the configured options to be resolved during startup.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022validation-scope\u0022\u003EValidation Scope\u003C/h3\u003E\n\u003Cp\u003EThe current validators check essential client profile and endpoint requirements.\u003C/p\u003E\n\u003Cp\u003EThey do not automatically validate every possible combination of client flow, server deployment, cookie behavior, and authentication mode.\u003C/p\u003E\n\u003Cp\u003EApplications should test their actual authentication flows against the deployed server configuration.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022complete-blazor-webassembly-example\u0022\u003E15. Complete Blazor WebAssembly Example\u003C/h2\u003E\n\u003Cp\u003EThe following example configures a Blazor WebAssembly client using a remote authentication server.\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.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n // Client profile\n options.AutoDetectClientProfile = false;\n options.ClientProfile = UAuthClientProfile.BlazorWasm;\n\n // Authentication server\n options.Endpoints.BasePath = \u0026quot;https://auth.example.com/auth\u0026quot;;\n\n // Navigation\n options.DefaultReturnUrl = \u0026quot;/\u0026quot;;\n options.Login.ReturnUrl = \u0026quot;/dashboard\u0026quot;;\n\n // Public client security\n options.Login.AllowCredentialPost = false;\n\n // PKCE\n options.Pkce.Enabled = true;\n options.Pkce.ReturnUrl = \u0026quot;/dashboard\u0026quot;;\n options.Pkce.AutoRedirect = true;\n\n // Background refresh\n options.AutoRefresh.Enabled = false;\n\n // Reauthentication\n options.Reauth.Behavior = ReauthBehavior.Redirect;\n options.Reauth.RedirectPath = \u0026quot;/login\u0026quot;;\n\n // State updates\n options.StateEvents.HandlingMode = UAuthStateEventHandlingMode.Patch;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis example emphasizes PKCE-based authentication and avoids direct credential posting from a public browser client.\u003C/p\u003E\n\u003Cp\u003EThe remote server must be configured with compatible endpoints, origins, authentication modes, and credential delivery policies.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022complete-blazor-server-example\u0022\u003E16. Complete Blazor Server Example\u003C/h2\u003E\n\u003Cp\u003EThe following example configures a Blazor Server client.\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.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n // Client profile\n options.AutoDetectClientProfile = false;\n options.ClientProfile = UAuthClientProfile.BlazorServer;\n\n // Same-origin authentication endpoints\n options.Endpoints.BasePath = \u0026quot;/auth\u0026quot;;\n\n // Navigation\n options.DefaultReturnUrl = \u0026quot;/\u0026quot;;\n options.Login.ReturnUrl = \u0026quot;/dashboard\u0026quot;;\n\n // Background refresh\n options.AutoRefresh.Enabled = true;\n options.AutoRefresh.Interval = TimeSpan.FromMinutes(5);\n\n // Reauthentication\n options.Reauth.Behavior = ReauthBehavior.Redirect;\n options.Reauth.RedirectPath = \u0026quot;/login\u0026quot;;\n\n // Authentication state\n options.StateEvents.HandlingMode = UAuthStateEventHandlingMode.Patch;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EBlazor Server applications can use session-oriented authentication and the corresponding server-side security policies.\u003C/p\u003E\n\u003Cp\u003EThe application must still initialize the client and session coordinator through its supported lifecycle.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022server-and-client-configuration-together\u0022\u003E17. Server and Client Configuration Together\u003C/h2\u003E\n\u003Cp\u003EAn application that hosts both UltimateAuth Server and the Blazor client adapter can configure them independently.\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// Server: authentication and security policies\nbuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.AllowedModes =\n [\n UAuthMode.PureOpaque,\n UAuthMode.Hybrid\n ];\n\n options.Session.Lifetime = TimeSpan.FromDays(7);\n options.Login.MaxFailedAttempts = 5;\n});\n\n// Client: platform integration and flow behavior\nbuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.ClientProfile = UAuthClientProfile.BlazorServer;\n options.AutoDetectClientProfile = false;\n options.Endpoints.BasePath = \u0026quot;/auth\u0026quot;;\n options.AutoRefresh.Enabled = true;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe two configuration objects have different responsibilities.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003ERequirement\u003C/th\u003E\n\u003Cth\u003EConfigure on\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ESession lifetime\u003C/td\u003E\n\u003Ctd\u003EServer\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAccess token lifetime\u003C/td\u003E\n\u003Ctd\u003EServer\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ERefresh token replay policy\u003C/td\u003E\n\u003Ctd\u003EServer\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ECredential recovery rules\u003C/td\u003E\n\u003Ctd\u003EServer\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAllowed authentication modes\u003C/td\u003E\n\u003Ctd\u003EServer\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EClient profile\u003C/td\u003E\n\u003Ctd\u003EClient\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EPKCE navigation\u003C/td\u003E\n\u003Ctd\u003EClient\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAutomatic background refresh\u003C/td\u003E\n\u003Ctd\u003EClient\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EReauthentication navigation\u003C/td\u003E\n\u003Ctd\u003EClient\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAuthentication state event handling\u003C/td\u003E\n\u003Ctd\u003EClient\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ETenant validation\u003C/td\u003E\n\u003Ctd\u003EServer\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ETenant propagation\u003C/td\u003E\n\u003Ctd\u003EClient\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe client cannot override server-side security decisions.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022common-configuration-mistakes\u0022\u003E18. Common Configuration Mistakes\u003C/h2\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022registering-the-core-client-and-blazor-adapter-separately\u0022\u003ERegistering the Core Client and Blazor Adapter Separately\u003C/h3\u003E\n\u003Cp\u003EUse \u003Ccode\u003EAddUltimateAuthClientBlazor()\u003C/code\u003E for Blazor applications.\u003C/p\u003E\n\u003Cp\u003EIt already registers the underlying client SDK.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022disabling-profile-detection-without-selecting-a-profile\u0022\u003EDisabling Profile Detection Without Selecting a Profile\u003C/h3\u003E\n\u003Cp\u003EWhen \u003Ccode\u003EAutoDetectClientProfile\u003C/code\u003E is false, set \u003Ccode\u003EClientProfile\u003C/code\u003E explicitly.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022relying-on-automatic-detection-in-an-ambiguous-hosting-environment\u0022\u003ERelying on Automatic Detection in an Ambiguous Hosting Environment\u003C/h3\u003E\n\u003Cp\u003EFor complex hosting arrangements, consider specifying the client profile explicitly.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022enabling-direct-credential-posting-in-a-public-client\u0022\u003EEnabling Direct Credential Posting in a Public Client\u003C/h3\u003E\n\u003Cp\u003EDo not enable \u003Ccode\u003EAllowCredentialPost\u003C/code\u003E for browser-hosted public clients.\u003C/p\u003E\n\u003Cp\u003EUse PKCE.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022assuming-client-auto-refresh-extends-server-lifetimes\u0022\u003EAssuming Client Auto-Refresh Extends Server Lifetimes\u003C/h3\u003E\n\u003Cp\u003EAutomatic refresh only initiates refresh operations.\u003C/p\u003E\n\u003Cp\u003EThe server controls whether refresh is permitted.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022assuming-auto-refresh-starts-automatically\u0022\u003EAssuming Auto-Refresh Starts Automatically\u003C/h3\u003E\n\u003Cp\u003EThe Blazor session coordinator must be started through the supported application lifecycle.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configuring-only-the-client-endpoint\u0022\u003EConfiguring Only the Client Endpoint\u003C/h3\u003E\n\u003Cp\u003EThe server must expose compatible routes and allow the intended cross-origin interaction.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022treating-tenant-propagation-as-authorization\u0022\u003ETreating Tenant Propagation as Authorization\u003C/h3\u003E\n\u003Cp\u003EA tenant identifier supplied by the client does not grant access to that tenant.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022using-state-events-as-distributed-notifications\u0022\u003EUsing State Events as Distributed Notifications\u003C/h3\u003E\n\u003Cp\u003EClient events update local client state; they do not automatically synchronize all connected devices.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022assuming-every-client-setting-is-validated-at-startup\u0022\u003EAssuming Every Client Setting Is Validated at Startup\u003C/h3\u003E\n\u003Cp\u003EThe current client registration validates options when they are resolved, rather than explicitly requiring startup validation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022client-options-quick-reference\u0022\u003E19. Client Options Quick Reference\u003C/h2\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EOption\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EClientProfile\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003ENotSpecified\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EClient environment\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAutoDetectClientProfile\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAutomatic profile detection\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDefaultReturnUrl\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EGlobal return URL\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEndpoints.BasePath\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E/auth\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication endpoint base\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELogin.ReturnUrl\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELogin return URL\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELogin.AllowCredentialPost\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EDirect credential posting\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPkce.Enabled\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPKCE flow support\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPkce.ReturnUrl\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPKCE return URL\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPkce.AutoRedirect\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAutomatic PKCE navigation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPkce.OnAuthorized\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPKCE authorization callback\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAutoRefresh.Interval\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E5 minutes after post-configuration\u003C/td\u003E\n\u003Ctd\u003EBackground refresh interval\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EReauth.Behavior\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003ERedirect\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReauthentication handling\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EReauth.RedirectPath\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReauthentication redirect\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMultiTenant.Enabled\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETenant propagation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMultiTenant.Tenant\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETenant identifier\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMultiTenant.Transport\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003ENone\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETenant transport\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EStateEvents.HandlingMode\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EPatch\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EState event handling\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EAutoRefresh.Enabled\u003C/code\u003E should be evaluated with the applicable platform and post-configuration behavior rather than inferred solely from its property initializer.\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\u003EConfiguration Overview\u003C/strong\u003E \u2014 Understand the configuration architecture and source precedence.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EServer Options\u003C/strong\u003E \u2014 Configure authentication security, sessions, tokens, and endpoints.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EAdvanced Configuration\u003C/strong\u003E \u2014 Explore effective options, mode resolution, and extension points.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EClient Authentication\u003C/strong\u003E \u2014 Implement login, PKCE, refresh, and logout flows.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EClient Session Management\u003C/strong\u003E \u2014 Manage authenticated sessions and revocation.\u003C/li\u003E\n\u003C/ul\u003E\n", + "Html": "\n\u003Cp\u003E\u003Ccode\u003EUAuthClientOptions\u003C/code\u003E controls how an UltimateAuth client communicates with the authentication server, executes authentication flows, maintains client-side authentication state, and responds when authentication must be renewed.\u003C/p\u003E\n\u003Cp\u003EThe client configuration is intentionally separate from server security configuration.\u003C/p\u003E\n\u003Cp\u003EThe server determines which authentication operations are permitted, how credentials are validated, and when sessions expire. The client determines how those operations are initiated and how the application responds to their outcomes.\u003C/p\u003E\n\u003Cp\u003EThis guide covers the current client configuration API, platform integration, defaults, practical examples, and important security considerations.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022register-the-ultimateauth-client\u0022\u003E1. Register the UltimateAuth Client\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth provides a platform-independent client SDK and platform-specific adapters.\u003C/p\u003E\n\u003Cp\u003EFor Blazor applications, use:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.AutoDetectClientProfile = true;\n options.DefaultReturnUrl = \u0026quot;/\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EAddUltimateAuthClientBlazor()\u003C/code\u003E registers the underlying client services and the Blazor-specific infrastructure, including browser transport, storage, device identification, and authentication state integration.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022core-client-registration\u0022\u003ECore Client Registration\u003C/h3\u003E\n\u003Cp\u003EThe platform-independent registration 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\u003Ebuilder.Services.AddUltimateAuthClient(options =\u0026gt;\n{\n options.AutoDetectClientProfile = true;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis registers the shared client SDK.\u003C/p\u003E\n\u003Cp\u003EHowever, it does not provide every platform-specific implementation needed to run authentication flows.\u003C/p\u003E\n\u003Cp\u003EApplications should use an appropriate platform adapter rather than relying on the core client registration alone.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022do-not-register-both-client-methods\u0022\u003EDo Not Register Both Client Methods\u003C/h3\u003E\n\u003Cp\u003EWhen using:\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.AddUltimateAuthClientBlazor();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Edo not also call:\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.AddUltimateAuthClient();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe Blazor registration already includes the core client services.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022server-and-client-in-one-application\u0022\u003EServer and Client in One Application\u003C/h3\u003E\n\u003Cp\u003EA Blazor Server application can register both UltimateAuth Server and the Blazor client adapter:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Session.Lifetime = TimeSpan.FromDays(7);\n});\n\nbuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.ClientProfile = UAuthClientProfile.BlazorServer;\n options.AutoDetectClientProfile = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThese registrations configure different components.\u003C/p\u003E\n\u003Cp\u003EThe server controls authentication security and lifecycle rules.\u003C/p\u003E\n\u003Cp\u003EThe client controls the application-side integration.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022client-options-architecture\u0022\u003E2. Client Options Architecture\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthClientOptions\u003C/code\u003E contains 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\u003EType\u003C/th\u003E\n\u003Cth\u003EPurpose\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EClientProfile\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthClientProfile\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifies the client platform\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAutoDetectClientProfile\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Ebool\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEnables automatic profile detection\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDefaultReturnUrl\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Estring?\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EDefault navigation destination after authentication\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEndpoints\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthClientEndpointOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication endpoint paths\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELogin\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthClientLoginFlowOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELogin flow configuration\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPkce\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthClientPkceLoginFlowOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPKCE flow configuration\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAutoRefresh\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthClientAutoRefreshOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EBackground refresh coordination\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EReauth\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthClientReauthOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReauthentication behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMultiTenant\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthClientMultiTenantOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETenant propagation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EStateEvents\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthStateEventOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication state event handling\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EResource\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthClientResourceOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EProtected Resource API origin and browser credential handling\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThese options are configured through the same \u003Ccode\u003EUAuthClientOptions\u003C/code\u003E instance.\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\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.DefaultReturnUrl = \u0026quot;/dashboard\u0026quot;;\n options.Pkce.Enabled = true;\n options.AutoRefresh.Interval = TimeSpan.FromMinutes(5);\n options.StateEvents.HandlingMode = UAuthStateEventHandlingMode.Patch;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022client-profiles\u0022\u003E3. Client Profiles\u003C/h2\u003E\n\u003Cp\u003EThe client profile identifies the application environment and influences authentication flow behavior.\u003C/p\u003E\n\u003Cp\u003EConfigure it through:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.ClientProfile\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUltimateAuth defines profiles including:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProfile\u003C/th\u003E\n\u003Cth\u003ETypical application\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EBlazorServer\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EInteractive Blazor Server applications\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EBlazorWasm\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EBrowser-hosted Blazor WebAssembly\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaui\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E.NET MAUI applications\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EWebServer\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETraditional ASP.NET Core web applications\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EApi\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAPI-oriented clients\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthHub\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUltimateAuth Hub\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ENotSpecified\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EProfile not explicitly selected\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EDifferent client profiles may result in different authentication modes on the server.\u003C/p\u003E\n\u003Cp\u003EFor example, the default server mode resolver maps Blazor Server to \u003Ccode\u003EPureOpaque\u003C/code\u003E and Blazor WebAssembly to \u003Ccode\u003EHybrid\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe client profile does not itself authorize an authentication mode. The server must support and permit the resolved mode.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022automatic-client-profile-detection\u0022\u003E4. Automatic Client Profile Detection\u003C/h2\u003E\n\u003Cp\u003EAutomatic profile detection is enabled by 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.AutoDetectClientProfile = true;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWhen enabled, UltimateAuth detects the client environment if \u003Ccode\u003EClientProfile\u003C/code\u003E remains \u003Ccode\u003ENotSpecified\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe current detector checks for:\u003C/p\u003E\n\u003Col\u003E\n\u003Cli\u003EUAuthHub registration.\u003C/li\u003E\n\u003Cli\u003E.NET MAUI.\u003C/li\u003E\n\u003Cli\u003EBlazor WebAssembly.\u003C/li\u003E\n\u003Cli\u003EBlazor Server.\u003C/li\u003E\n\u003Cli\u003EA fallback to \u003Ccode\u003EWebServer\u003C/code\u003E.\u003C/li\u003E\n\u003C/ol\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022default-configuration\u0022\u003EDefault Configuration\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.AutoDetectClientProfile = true;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ENo explicit profile is required for ordinary supported environments where detection succeeds.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022manual-configuration\u0022\u003EManual Configuration\u003C/h3\u003E\n\u003Cp\u003EFor applications that need predictable behavior independent of assembly-based detection:\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.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.AutoDetectClientProfile = false;\n options.ClientProfile = UAuthClientProfile.BlazorWasm;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EManual configuration can be useful for:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003ESpecialized hosting environments.\u003C/li\u003E\n\u003Cli\u003EIntegration tests.\u003C/li\u003E\n\u003Cli\u003EApplications with multiple hosting models.\u003C/li\u003E\n\u003Cli\u003ECases where assembly-based detection is ambiguous.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022important-validation-rule\u0022\u003EImportant Validation Rule\u003C/h3\u003E\n\u003Cp\u003EThis configuration is invalid:\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.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.AutoDetectClientProfile = false;\n options.ClientProfile = UAuthClientProfile.NotSpecified;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWhen automatic detection is disabled, an explicit client profile must be provided.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022explicit-profiles-take-precedence\u0022\u003EExplicit Profiles Take Precedence\u003C/h3\u003E\n\u003Cp\u003EThe post-configuration detector does not replace a profile that has already been explicitly selected.\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\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.ClientProfile = UAuthClientProfile.BlazorServer;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe explicit profile is retained even when automatic detection remains enabled.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022detection-limitations\u0022\u003EDetection Limitations\u003C/h3\u003E\n\u003Cp\u003EThe current detector uses service registration and loaded assemblies to identify the environment.\u003C/p\u003E\n\u003Cp\u003EAssembly-based detection may not be completely reliable in every hosting arrangement.\u003C/p\u003E\n\u003Cp\u003EFor complex Blazor Web App hosting scenarios, explicitly configuring the profile can make authentication behavior easier to reason about.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022endpoint-configuration\u0022\u003E5. Endpoint Configuration\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Endpoints\u003C/code\u003E defines the endpoint paths used by the client SDK.\u003C/p\u003E\n\u003Cp\u003EThe default base path is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003E/auth\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configure-a-remote-authentication-server\u0022\u003EConfigure a Remote Authentication Server\u003C/h3\u003E\n\u003Cp\u003EFor a client communicating with a separately hosted UAuthHub:\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.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Endpoints.BasePath = \u0026quot;https://auth.example.com/auth\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EFor an application using same-origin authentication endpoints:\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.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Endpoints.BasePath = \u0026quot;/auth\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe base path must match the deployed authentication server\u0027s routing configuration.\u003C/p\u003E\n\u003Cp\u003EChanging a client endpoint does not create or expose the corresponding server endpoint.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022default-endpoint-paths\u0022\u003EDefault Endpoint Paths\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EBasePath\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E/auth\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELogin\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E/login\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETryLogin\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E/try-login\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELogout\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E/logout\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERefresh\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E/refresh\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EReauth\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E/reauth\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EValidate\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E/validate\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPkceAuthorize\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E/pkce/authorize\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPkceTryComplete\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E/pkce/try-complete\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPkceComplete\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E/pkce/complete\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EHubLoginPath\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E/uauthhub/entry\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022customize-individual-paths\u0022\u003ECustomize Individual Paths\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Endpoints.BasePath = \u0026quot;/auth\u0026quot;;\n options.Endpoints.Login = \u0026quot;/login\u0026quot;;\n options.Endpoints.Logout = \u0026quot;/logout\u0026quot;;\n options.Endpoints.Refresh = \u0026quot;/refresh\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThese values should reflect the actual routes registered by UltimateAuth Server.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022required-endpoint-validation\u0022\u003ERequired Endpoint Validation\u003C/h3\u003E\n\u003Cp\u003EThe client options validator requires:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003EEndpoints.BasePath\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EEndpoints.Login\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EEndpoints.Logout\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EEndpoints.Refresh\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EEndpoints.Validate\u003C/code\u003E\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThese properties must not be null, empty, or whitespace-only.\u003C/p\u003E\n\u003Cp\u003EThe current validator does not apply the same required-value checks to every optional endpoint path.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022remote-clients-and-browser-security\u0022\u003ERemote Clients and Browser Security\u003C/h3\u003E\n\u003Cp\u003EWhen using a remote authentication server, also consider:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EHTTPS.\u003C/li\u003E\n\u003Cli\u003ECross-origin request configuration.\u003C/li\u003E\n\u003Cli\u003ECookie \u003Ccode\u003ESameSite\u003C/code\u003E and \u003Ccode\u003ESecure\u003C/code\u003E attributes.\u003C/li\u003E\n\u003Cli\u003EAllowed client origins.\u003C/li\u003E\n\u003Cli\u003EPKCE redirects.\u003C/li\u003E\n\u003Cli\u003EBrowser credential policies.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EA correct endpoint URL alone does not guarantee that cross-origin authentication will work.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022resource-api-configuration\u0022\u003EResource API Configuration\u003C/h3\u003E\n\u003Cp\u003EAuthentication endpoints and application resource endpoints serve different purposes.\u003C/p\u003E\n\u003Cp\u003EAn application may authenticate through UAuthHub while retrieving business data from a separately deployed Resource API.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth provides \u003Ccode\u003Eoptions.Resource\u003C/code\u003E to configure these requests independently.\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;\n\nbuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Endpoints.BasePath = \u0026quot;https://auth.example.com/auth\u0026quot;;\n\n options.Resource.BaseAddress = \u0026quot;https://api.example.com\u0026quot;;\n options.Resource.Credentials = UAuthResourceCredentialsMode.Auto;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe Resource Client is available through \u003Ccode\u003EIUAuthClient.Resource\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.Resource\n .GetAsync\u0026lt;List\u0026lt;Product\u0026gt;\u0026gt;(\u0026quot;/api/products\u0026quot;);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch4 id=\u0022resource-options\u0022\u003EResource Options\u003C/h4\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EResource.BaseAddress\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EResource API origin; \u003Ccode\u003Enull\u003C/code\u003E uses the application\u0027s origin\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EResource.Credentials\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EAuto\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EBrowser credential transmission policy\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch4 id=\u0022base-address-validation\u0022\u003EBase Address Validation\u003C/h4\u003E\n\u003Cp\u003EWhen configured, \u003Ccode\u003EResource.BaseAddress\u003C/code\u003E must be an absolute HTTP or HTTPS origin.\u003C/p\u003E\n\u003Cp\u003EHTTPS is required except for loopback development addresses.\u003C/p\u003E\n\u003Cp\u003EThe address must not contain an additional path, query string, fragment, or embedded credentials.\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\u003Eoptions.Resource.BaseAddress = \u0026quot;https://api.example.com\u0026quot;;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EIndividual resource paths are supplied when making requests:\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.Resource\n .GetAsync\u0026lt;List\u0026lt;Product\u0026gt;\u0026gt;(\u0026quot;/api/products\u0026quot;);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch4 id=\u0022credential-modes\u0022\u003ECredential Modes\u003C/h4\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthResourceCredentialsMode\u003C/code\u003E supports four options:\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\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAuto\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAutomatically select the appropriate browser credential policy\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESameOrigin\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EInclude browser credentials only for same-origin requests\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EInclude\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EInclude browser credentials for the configured Resource API origin\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EOmit\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EDo not include browser credentials\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EFor example, a separate Resource API that requires cross-origin cookies may use:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.Resource.Credentials = UAuthResourceCredentialsMode.Include;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe Resource API must permit the relevant browser origin and support the required CORS and cookie configuration.\u003C/p\u003E\n\u003Cp\u003ECredential configuration does not bypass browser security restrictions or replace server-side authentication and authorization.\u003C/p\u003E\n\u003Ch4 id=\u0022resource-requests-and-authentication-lifecycle\u0022\u003EResource Requests and Authentication Lifecycle\u003C/h4\u003E\n\u003Cp\u003EResource requests use the browser transport in the Blazor integration.\u003C/p\u003E\n\u003Cp\u003EThey support request cancellation, origin validation, and redirect protection.\u003C/p\u003E\n\u003Cp\u003EResource requests return \u003Ccode\u003EUAuthResult\u0026lt;T\u0026gt;\u003C/code\u003E or \u003Ccode\u003EUAuthResult\u003C/code\u003E, depending on the operation.\u003C/p\u003E\n\u003Cp\u003EThe current Resource Client does not automatically refresh authentication and retry a request after receiving a \u003Ccode\u003E401 Unauthorized\u003C/code\u003E response.\u003C/p\u003E\n\u003Cp\u003EFor a complete explanation, see \u003Ca href=\u0022/docs/client-sdk/resource-api-requests\u0022\u003EResource API Requests\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022login-flow-options\u0022\u003E6. Login Flow Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Login\u003C/code\u003E controls client-side login behavior.\u003C/p\u003E\n\u003Cp\u003EThe available properties are:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003Cth\u003EPurpose\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EReturnUrl\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELogin-specific return destination\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAllowCredentialPost\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAllow direct credential posting\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=\u0022configure-login-navigation\u0022\u003EConfigure Login Navigation\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Login.ReturnUrl = \u0026quot;/dashboard\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe login-specific return URL is used when the login flow does not provide a more specific destination.\u003C/p\u003E\n\u003Cp\u003EIf it is not set, UltimateAuth can fall back to \u003Ccode\u003EDefaultReturnUrl\u003C/code\u003E or the current location according to the flow.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022direct-credential-posting\u0022\u003EDirect Credential Posting\u003C/h3\u003E\n\u003Cp\u003EThe default is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.Login.AllowCredentialPost = false;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis is an important security setting.\u003C/p\u003E\n\u003Cp\u003EDirectly posting user credentials is not appropriate for public browser clients such as Blazor WebAssembly applications.\u003C/p\u003E\n\u003Cp\u003EPublic clients should use the supported PKCE-based login flow.\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\u003EA trusted server-hosted application may explicitly enable direct credential posting where its authentication architecture requires it:\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.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.ClientProfile = UAuthClientProfile.BlazorServer;\n options.Login.AllowCredentialPost = true;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EDo not enable this setting for a public client merely to simplify login implementation.\u003C/p\u003E\n\u003Cp\u003EThe server still determines whether the requested authentication operation is permitted.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022pkce-configuration\u0022\u003E7. PKCE Configuration\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Pkce\u003C/code\u003E controls client-side PKCE login behavior.\u003C/p\u003E\n\u003Cp\u003EPKCE allows a public client to complete an authorization flow without relying on a confidential client secret.\u003C/p\u003E\n\u003Cp\u003EThe current options are:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnabled\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEnable client PKCE support\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\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPost-login return destination\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAutoRedirect\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAutomatically navigate after beginning PKCE\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EOnAuthorized\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECallback after the authorization response is issued\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=\u0022basic-configuration\u0022\u003EBasic Configuration\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Pkce.Enabled = true;\n options.Pkce.ReturnUrl = \u0026quot;/dashboard\u0026quot;;\n options.Pkce.AutoRedirect = true;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022disable-automatic-redirect\u0022\u003EDisable Automatic Redirect\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Pkce.AutoRedirect = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWhen \u003Ccode\u003EAutoRedirect\u003C/code\u003E is disabled, \u003Ccode\u003EBeginPkceAsync\u003C/code\u003E does not automatically navigate to the next location.\u003C/p\u003E\n\u003Cp\u003EThe application is responsible for handling navigation.\u003C/p\u003E\n\u003Cp\u003EThis can be useful when integrating UltimateAuth into an existing routing or navigation system.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022authorization-callback\u0022\u003EAuthorization Callback\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Pkce.OnAuthorized = response =\u0026gt;\n {\n // Inspect or process the authorization response.\n return Task.CompletedTask;\n };\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe callback receives:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EPkceAuthorizeResponse\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EIt runs after the authorization response is issued and before the normal Hub navigation.\u003C/p\u003E\n\u003Cp\u003EUse the callback for application-specific integration, not to bypass PKCE verification.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022client-and-server-pkce-settings-are-different\u0022\u003EClient and Server PKCE Settings Are Different\u003C/h3\u003E\n\u003Cp\u003EClient configuration:\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.AutoRedirect = true;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EServer configuration:\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.AuthorizationCodeLifetimeSeconds = 120;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe client controls how the flow is initiated and navigated.\u003C/p\u003E\n\u003Cp\u003EThe server controls the authorization code\u0027s validity and verification.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022return-url-configuration\u0022\u003E8. Return URL Configuration\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth supports several return URL configuration points.\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\u003EDefaultReturnUrl\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EGlobal default for interactive authentication\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELogin.ReturnUrl\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELogin-specific default\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPkce.ReturnUrl\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPKCE-specific default\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=\u0022global-default\u0022\u003EGlobal Default\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.DefaultReturnUrl = \u0026quot;/dashboard\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022flow-specific-defaults\u0022\u003EFlow-Specific Defaults\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.DefaultReturnUrl = \u0026quot;/\u0026quot;;\n options.Login.ReturnUrl = \u0026quot;/account\u0026quot;;\n options.Pkce.ReturnUrl = \u0026quot;/dashboard\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EFlow-specific values allow different authentication paths to use different destinations.\u003C/p\u003E\n\u003Cp\u003EAn explicitly supplied return URL for an individual operation may take precedence over these defaults.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-considerations\u0022\u003ESecurity Considerations\u003C/h3\u003E\n\u003Cp\u003EReturn URLs are navigation destinations and must be treated carefully.\u003C/p\u003E\n\u003Cp\u003EDo not redirect users to arbitrary external URLs without appropriate validation.\u003C/p\u003E\n\u003Cp\u003EOpen redirects can be abused for phishing and other attacks.\u003C/p\u003E\n\u003Cp\u003EPrefer local application routes unless cross-origin navigation is an intentional and validated part of the authentication architecture.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022automatic-refresh\u0022\u003E9. Automatic Refresh\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.AutoRefresh\u003C/code\u003E controls background refresh coordination.\u003C/p\u003E\n\u003Cp\u003EThe available properties are:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnabled\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEnables or disables automatic background refresh\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EInterval\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EInterval between background refresh attempts\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=\u0022configure-automatic-refresh\u0022\u003EConfigure Automatic Refresh\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.AutoRefresh.Enabled = true;\n options.AutoRefresh.Interval = TimeSpan.FromMinutes(5);\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022disable-automatic-refresh\u0022\u003EDisable Automatic Refresh\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.AutoRefresh.Enabled = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022default-interval\u0022\u003EDefault Interval\u003C/h3\u003E\n\u003Cp\u003EIf \u003Ccode\u003EInterval\u003C/code\u003E is not supplied, the client registration applies:\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.Interval ??= TimeSpan.FromMinutes(5);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe resulting default interval is five minutes.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022platform-behavior\u0022\u003EPlatform Behavior\u003C/h3\u003E\n\u003Cp\u003EThe effective \u003Ccode\u003EEnabled\u003C/code\u003E setting should be considered together with the platform\u0027s post-configuration and session coordination behavior.\u003C/p\u003E\n\u003Cp\u003EThe current Blazor session coordinator checks \u003Ccode\u003EAutoRefresh.Enabled\u003C/code\u003E before starting its background loop.\u003C/p\u003E\n\u003Cp\u003EWhen enabled, it periodically invokes:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthClient.Flows.RefreshAsync(true);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022automatic-refresh-is-not-a-security-policy\u0022\u003EAutomatic Refresh Is Not a Security Policy\u003C/h3\u003E\n\u003Cp\u003EAutomatic refresh does not control:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EServer session lifetime.\u003C/li\u003E\n\u003Cli\u003ERefresh token lifetime.\u003C/li\u003E\n\u003Cli\u003ESession revocation.\u003C/li\u003E\n\u003Cli\u003ECredential recovery policies.\u003C/li\u003E\n\u003Cli\u003ERefresh token replay protection.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThese remain server-side responsibilities.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022automatic-refresh-does-not-guarantee-session-continuity\u0022\u003EAutomatic Refresh Does Not Guarantee Session Continuity\u003C/h3\u003E\n\u003Cp\u003EBackground refresh may fail because:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EThe session has expired.\u003C/li\u003E\n\u003Cli\u003EThe refresh credential has been revoked.\u003C/li\u003E\n\u003Cli\u003EThe browser is suspended.\u003C/li\u003E\n\u003Cli\u003ENetwork connectivity is unavailable.\u003C/li\u003E\n\u003Cli\u003EThe server requires reauthentication.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EAutomatic refresh is a convenience and continuity mechanism, not a guarantee that authentication remains valid indefinitely.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022coordinator-lifecycle\u0022\u003ECoordinator Lifecycle\u003C/h3\u003E\n\u003Cp\u003EThe Blazor adapter provides an \u003Ccode\u003EISessionCoordinator\u003C/code\u003E implementation.\u003C/p\u003E\n\u003Cp\u003EThe coordinator must be started by the application\u0027s supported initialization lifecycle.\u003C/p\u003E\n\u003Cp\u003ESetting \u003Ccode\u003EAutoRefresh.Enabled = true\u003C/code\u003E does not mean that a background task automatically runs without the coordinator being started.\u003C/p\u003E\n\u003Cp\u003EApplications should follow the client initialization guidance for their hosting model.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022reauthentication-options\u0022\u003E10. Reauthentication Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Reauth\u003C/code\u003E controls how the Blazor session coordinator responds when refresh reports that reauthentication is required.\u003C/p\u003E\n\u003Cp\u003EThe available properties are:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EBehavior\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EReauthBehavior.Redirect\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERedirectPath\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-behaviors\u0022\u003EAvailable Behaviors\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\u003EReauthBehavior.Redirect\nReauthBehavior.None\nReauthBehavior.RaiseEvent\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022redirect\u0022\u003ERedirect\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Reauth.Behavior = ReauthBehavior.Redirect;\n options.Reauth.RedirectPath = \u0026quot;/login\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWhen the coordinator receives a \u003Ccode\u003EReauthRequired\u003C/code\u003E refresh outcome, it navigates to the configured redirect path.\u003C/p\u003E\n\u003Cp\u003EIf \u003Ccode\u003ERedirectPath\u003C/code\u003E is null, the coordinator falls back to \u003Ccode\u003EEndpoints.Login\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe current Blazor coordinator uses forced navigation for this redirect.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022raise-an-event\u0022\u003ERaise an Event\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Reauth.Behavior = ReauthBehavior.RaiseEvent;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis allows an application to handle reauthentication through the coordinator\u0027s \u003Ccode\u003EReauthRequired\u003C/code\u003E event.\u003C/p\u003E\n\u003Cp\u003EFor example, an application may display a reauthentication dialog rather than immediately navigating.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022no-automatic-action\u0022\u003ENo Automatic Action\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Reauth.Behavior = ReauthBehavior.None;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWith \u003Ccode\u003ENone\u003C/code\u003E, the coordinator does not automatically redirect or raise its reauthentication event.\u003C/p\u003E\n\u003Cp\u003EThe application must still handle unauthenticated states appropriately.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022important\u0022\u003EImportant\u003C/h3\u003E\n\u003Cp\u003EReauthentication behavior does not make an expired or revoked session valid.\u003C/p\u003E\n\u003Cp\u003EIt only controls how the client responds when authentication must be reestablished.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022multi-tenant-client-configuration\u0022\u003E11. Multi-Tenant Client Configuration\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.MultiTenant\u003C/code\u003E controls tenant propagation from the client to the authentication server.\u003C/p\u003E\n\u003Cp\u003EThe client does not independently authorize or resolve tenant ownership.\u003C/p\u003E\n\u003Cp\u003EIt carries tenant context using the configured transport.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-properties\u0022\u003EAvailable Properties\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnabled\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEnable tenant propagation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETenant\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETenant identifier to propagate\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETransport\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003ETenantTransport.None\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETenant transport mechanism\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=\u0022available-transports\u0022\u003EAvailable Transports\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\u003ETenantTransport.None\nTenantTransport.Header\nTenantTransport.Route\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022header-based-tenant-propagation\u0022\u003EHeader-Based Tenant Propagation\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.MultiTenant.Enabled = true;\n options.MultiTenant.Tenant = \u0026quot;tenant-a\u0026quot;;\n options.MultiTenant.Transport = TenantTransport.Header;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe server must be configured to resolve tenant information from the corresponding transport.\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.MultiTenant.Enabled = true;\n options.MultiTenant.EnableHeader = true;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022route-based-tenant-propagation\u0022\u003ERoute-Based Tenant Propagation\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.MultiTenant.Enabled = true;\n options.MultiTenant.Tenant = \u0026quot;tenant-a\u0026quot;;\n options.MultiTenant.Transport = TenantTransport.Route;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe server must expose routes compatible with tenant-based routing.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-boundary\u0022\u003ESecurity Boundary\u003C/h3\u003E\n\u003Cp\u003EA client-provided tenant identifier is not proof of tenant membership.\u003C/p\u003E\n\u003Cp\u003EThe server must validate the resolved tenant and enforce tenant isolation.\u003C/p\u003E\n\u003Cp\u003ENever rely on client-side tenant selection as an authorization boundary.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022authentication-state-events\u0022\u003E12. Authentication State Events\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.StateEvents\u003C/code\u003E controls how the client processes authentication-related state events.\u003C/p\u003E\n\u003Cp\u003EThe available property is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.StateEvents.HandlingMode\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EIts default is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthStateEventHandlingMode.Patch\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-modes\u0022\u003EAvailable Modes\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EMode\u003C/th\u003E\n\u003Cth\u003EBehavior\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPatch\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EApply a local authentication state update\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EValidate\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERevalidate authentication state after the event\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ENone\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EDo not update authentication state in response to the event\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=\u0022patch\u0022\u003EPatch\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.StateEvents.HandlingMode = UAuthStateEventHandlingMode.Patch;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EPatch\u003C/code\u003E is the default.\u003C/p\u003E\n\u003Cp\u003EIt allows the client to update local state without performing a full authentication validation for every event.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022validate\u0022\u003EValidate\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.StateEvents.HandlingMode = UAuthStateEventHandlingMode.Validate;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EValidate\u003C/code\u003E causes the client to revalidate authentication state in response to events.\u003C/p\u003E\n\u003Cp\u003EThis may introduce additional requests but can be appropriate when the application prefers server-confirmed state updates.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022none\u0022\u003ENone\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.StateEvents.HandlingMode = UAuthStateEventHandlingMode.None;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003ENone\u003C/code\u003E disables automatic authentication state updates in response to these events.\u003C/p\u003E\n\u003Cp\u003EApplications using this mode must provide any required state synchronization through other mechanisms.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022local-events-are-not-distributed-notifications\u0022\u003ELocal Events Are Not Distributed Notifications\u003C/h3\u003E\n\u003Cp\u003EClient state events do not automatically notify every browser, device, or application instance.\u003C/p\u003E\n\u003Cp\u003EFor example, an administrator changing a user\u0027s role does not automatically push an event to that user\u0027s open browser.\u003C/p\u003E\n\u003Cp\u003EDistributed synchronization requires additional application infrastructure.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022configuration-from-appsettings.json\u0022\u003E13. Configuration from appsettings.json\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth Client binds the configuration section:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EUltimateAuth:Client\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EJSON\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-json\u0022\u003E{\n \u0026quot;UltimateAuth\u0026quot;: {\n \u0026quot;Client\u0026quot;: {\n \u0026quot;AutoDetectClientProfile\u0026quot;: true,\n \u0026quot;DefaultReturnUrl\u0026quot;: \u0026quot;/dashboard\u0026quot;,\n \u0026quot;Endpoints\u0026quot;: {\n \u0026quot;BasePath\u0026quot;: \u0026quot;https://auth.example.com/auth\u0026quot;\n },\n \u0026quot;Pkce\u0026quot;: {\n \u0026quot;Enabled\u0026quot;: true,\n \u0026quot;ReturnUrl\u0026quot;: \u0026quot;/dashboard\u0026quot;,\n \u0026quot;AutoRedirect\u0026quot;: true\n },\n \u0026quot;AutoRefresh\u0026quot;: {\n \u0026quot;Enabled\u0026quot;: true,\n \u0026quot;Interval\u0026quot;: \u0026quot;00:05:00\u0026quot;\n },\n \u0026quot;Reauth\u0026quot;: {\n \u0026quot;Behavior\u0026quot;: \u0026quot;Redirect\u0026quot;,\n \u0026quot;RedirectPath\u0026quot;: \u0026quot;/login\u0026quot;\n },\n \u0026quot;StateEvents\u0026quot;: {\n \u0026quot;HandlingMode\u0026quot;: \u0026quot;Patch\u0026quot;\n }\n }\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe example uses the standard ASP.NET Core configuration binding conventions.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configuration-precedence\u0022\u003EConfiguration Precedence\u003C/h3\u003E\n\u003Cp\u003EThe registration pipeline applies:\u003C/p\u003E\n\u003Col\u003E\n\u003Cli\u003EOption type defaults.\u003C/li\u003E\n\u003Cli\u003EThe configuration delegate.\u003C/li\u003E\n\u003Cli\u003EValues from \u003Ccode\u003EUltimateAuth:Client\u003C/code\u003E.\u003C/li\u003E\n\u003Cli\u003EPost-configuration processing.\u003C/li\u003E\n\u003Cli\u003ERegistered options validation when the options are resolved.\u003C/li\u003E\n\u003C/ol\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.DefaultReturnUrl = \u0026quot;/\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWith:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EJSON\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-json\u0022\u003E{\n \u0026quot;UltimateAuth\u0026quot;: {\n \u0026quot;Client\u0026quot;: {\n \u0026quot;DefaultReturnUrl\u0026quot;: \u0026quot;/dashboard\u0026quot;\n }\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe configured return URL becomes \u003Ccode\u003E/dashboard\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022environment-variables\u0022\u003EEnvironment Variables\u003C/h3\u003E\n\u003Cp\u003EASP.NET Core supports double underscores for hierarchical configuration keys.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EUltimateAuth__Client__Endpoints__BasePath=https://auth.example.com/auth\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis corresponds 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\u003EUltimateAuth:Client:Endpoints:BasePath\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUse environment-specific configuration for deployment-dependent values such as authentication server addresses and application return paths.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022client-options-validation\u0022\u003E14. Client Options Validation\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth registers validators for:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthClientOptions\nUAuthClientEndpointOptions\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022client-profile-validation\u0022\u003EClient Profile Validation\u003C/h3\u003E\n\u003Cp\u003EThe validator rejects a configuration where:\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.AutoDetectClientProfile = false;\noptions.ClientProfile = UAuthClientProfile.NotSpecified;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAn explicit profile is required when detection is disabled.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022endpoint-validation\u0022\u003EEndpoint Validation\u003C/h3\u003E\n\u003Cp\u003EThe following endpoint properties must be present:\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\u003EEndpoints.BasePath\nEndpoints.Login\nEndpoints.Logout\nEndpoints.Refresh\nEndpoints.Validate\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EFor example, this configuration is invalid:\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.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Endpoints.BasePath = \u0026quot;\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022validation-timing\u0022\u003EValidation Timing\u003C/h3\u003E\n\u003Cp\u003EUnlike the server registration, the current client registration does not explicitly call \u003Ccode\u003EValidateOnStart()\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EValidation therefore occurs through the normal options resolution lifecycle.\u003C/p\u003E\n\u003Cp\u003EApplications that require early validation can arrange for the configured options to be resolved during startup.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022validation-scope\u0022\u003EValidation Scope\u003C/h3\u003E\n\u003Cp\u003EThe current validators check essential client profile and endpoint requirements.\u003C/p\u003E\n\u003Cp\u003EThey do not automatically validate every possible combination of client flow, server deployment, cookie behavior, and authentication mode.\u003C/p\u003E\n\u003Cp\u003EApplications should test their actual authentication flows against the deployed server configuration.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022complete-blazor-webassembly-example\u0022\u003E15. Complete Blazor WebAssembly Example\u003C/h2\u003E\n\u003Cp\u003EThe following example configures a Blazor WebAssembly client using a remote authentication server.\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.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n // Client profile\n options.AutoDetectClientProfile = false;\n options.ClientProfile = UAuthClientProfile.BlazorWasm;\n\n // Authentication server\n options.Endpoints.BasePath = \u0026quot;https://auth.example.com/auth\u0026quot;;\n\n // Navigation\n options.DefaultReturnUrl = \u0026quot;/\u0026quot;;\n options.Login.ReturnUrl = \u0026quot;/dashboard\u0026quot;;\n\n // Public client security\n options.Login.AllowCredentialPost = false;\n\n // PKCE\n options.Pkce.Enabled = true;\n options.Pkce.ReturnUrl = \u0026quot;/dashboard\u0026quot;;\n options.Pkce.AutoRedirect = true;\n\n // Background refresh\n options.AutoRefresh.Enabled = false;\n\n // Reauthentication\n options.Reauth.Behavior = ReauthBehavior.Redirect;\n options.Reauth.RedirectPath = \u0026quot;/login\u0026quot;;\n\n // State updates\n options.StateEvents.HandlingMode = UAuthStateEventHandlingMode.Patch;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis example emphasizes PKCE-based authentication and avoids direct credential posting from a public browser client.\u003C/p\u003E\n\u003Cp\u003EThe remote server must be configured with compatible endpoints, origins, authentication modes, and credential delivery policies.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022complete-blazor-server-example\u0022\u003E16. Complete Blazor Server Example\u003C/h2\u003E\n\u003Cp\u003EThe following example configures a Blazor Server client.\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.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n // Client profile\n options.AutoDetectClientProfile = false;\n options.ClientProfile = UAuthClientProfile.BlazorServer;\n\n // Same-origin authentication endpoints\n options.Endpoints.BasePath = \u0026quot;/auth\u0026quot;;\n\n // Navigation\n options.DefaultReturnUrl = \u0026quot;/\u0026quot;;\n options.Login.ReturnUrl = \u0026quot;/dashboard\u0026quot;;\n\n // Background refresh\n options.AutoRefresh.Enabled = true;\n options.AutoRefresh.Interval = TimeSpan.FromMinutes(5);\n\n // Reauthentication\n options.Reauth.Behavior = ReauthBehavior.Redirect;\n options.Reauth.RedirectPath = \u0026quot;/login\u0026quot;;\n\n // Authentication state\n options.StateEvents.HandlingMode = UAuthStateEventHandlingMode.Patch;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EBlazor Server applications can use session-oriented authentication and the corresponding server-side security policies.\u003C/p\u003E\n\u003Cp\u003EThe application must still initialize the client and session coordinator through its supported lifecycle.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022server-and-client-configuration-together\u0022\u003E17. Server and Client Configuration Together\u003C/h2\u003E\n\u003Cp\u003EAn application that hosts both UltimateAuth Server and the Blazor client adapter can configure them independently.\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// Server: authentication and security policies\nbuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.AllowedModes =\n [\n UAuthMode.PureOpaque,\n UAuthMode.Hybrid\n ];\n\n options.Session.Lifetime = TimeSpan.FromDays(7);\n options.Login.MaxFailedAttempts = 5;\n});\n\n// Client: platform integration and flow behavior\nbuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.ClientProfile = UAuthClientProfile.BlazorServer;\n options.AutoDetectClientProfile = false;\n options.Endpoints.BasePath = \u0026quot;/auth\u0026quot;;\n options.AutoRefresh.Enabled = true;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe two configuration objects have different responsibilities.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003ERequirement\u003C/th\u003E\n\u003Cth\u003EConfigure on\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ESession lifetime\u003C/td\u003E\n\u003Ctd\u003EServer\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAccess token lifetime\u003C/td\u003E\n\u003Ctd\u003EServer\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ERefresh token replay policy\u003C/td\u003E\n\u003Ctd\u003EServer\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ECredential recovery rules\u003C/td\u003E\n\u003Ctd\u003EServer\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAllowed authentication modes\u003C/td\u003E\n\u003Ctd\u003EServer\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EClient profile\u003C/td\u003E\n\u003Ctd\u003EClient\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EPKCE navigation\u003C/td\u003E\n\u003Ctd\u003EClient\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAutomatic background refresh\u003C/td\u003E\n\u003Ctd\u003EClient\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EReauthentication navigation\u003C/td\u003E\n\u003Ctd\u003EClient\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAuthentication state event handling\u003C/td\u003E\n\u003Ctd\u003EClient\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ETenant validation\u003C/td\u003E\n\u003Ctd\u003EServer\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ETenant propagation\u003C/td\u003E\n\u003Ctd\u003EClient\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe client cannot override server-side security decisions.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022common-configuration-mistakes\u0022\u003E18. Common Configuration Mistakes\u003C/h2\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022registering-the-core-client-and-blazor-adapter-separately\u0022\u003ERegistering the Core Client and Blazor Adapter Separately\u003C/h3\u003E\n\u003Cp\u003EUse \u003Ccode\u003EAddUltimateAuthClientBlazor()\u003C/code\u003E for Blazor applications.\u003C/p\u003E\n\u003Cp\u003EIt already registers the underlying client SDK.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022disabling-profile-detection-without-selecting-a-profile\u0022\u003EDisabling Profile Detection Without Selecting a Profile\u003C/h3\u003E\n\u003Cp\u003EWhen \u003Ccode\u003EAutoDetectClientProfile\u003C/code\u003E is false, set \u003Ccode\u003EClientProfile\u003C/code\u003E explicitly.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022relying-on-automatic-detection-in-an-ambiguous-hosting-environment\u0022\u003ERelying on Automatic Detection in an Ambiguous Hosting Environment\u003C/h3\u003E\n\u003Cp\u003EFor complex hosting arrangements, consider specifying the client profile explicitly.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022enabling-direct-credential-posting-in-a-public-client\u0022\u003EEnabling Direct Credential Posting in a Public Client\u003C/h3\u003E\n\u003Cp\u003EDo not enable \u003Ccode\u003EAllowCredentialPost\u003C/code\u003E for browser-hosted public clients.\u003C/p\u003E\n\u003Cp\u003EUse PKCE.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022assuming-client-auto-refresh-extends-server-lifetimes\u0022\u003EAssuming Client Auto-Refresh Extends Server Lifetimes\u003C/h3\u003E\n\u003Cp\u003EAutomatic refresh only initiates refresh operations.\u003C/p\u003E\n\u003Cp\u003EThe server controls whether refresh is permitted.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022assuming-auto-refresh-starts-automatically\u0022\u003EAssuming Auto-Refresh Starts Automatically\u003C/h3\u003E\n\u003Cp\u003EThe Blazor session coordinator must be started through the supported application lifecycle.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configuring-only-the-client-endpoint\u0022\u003EConfiguring Only the Client Endpoint\u003C/h3\u003E\n\u003Cp\u003EThe server must expose compatible routes and allow the intended cross-origin interaction.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022treating-tenant-propagation-as-authorization\u0022\u003ETreating Tenant Propagation as Authorization\u003C/h3\u003E\n\u003Cp\u003EA tenant identifier supplied by the client does not grant access to that tenant.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022using-state-events-as-distributed-notifications\u0022\u003EUsing State Events as Distributed Notifications\u003C/h3\u003E\n\u003Cp\u003EClient events update local client state; they do not automatically synchronize all connected devices.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022assuming-every-client-setting-is-validated-at-startup\u0022\u003EAssuming Every Client Setting Is Validated at Startup\u003C/h3\u003E\n\u003Cp\u003EThe current client registration validates options when they are resolved, rather than explicitly requiring startup validation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022client-options-quick-reference\u0022\u003E19. Client Options Quick Reference\u003C/h2\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EOption\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EClientProfile\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003ENotSpecified\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EClient environment\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAutoDetectClientProfile\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAutomatic profile detection\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDefaultReturnUrl\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EGlobal return URL\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEndpoints.BasePath\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E/auth\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication endpoint base\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELogin.ReturnUrl\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELogin return URL\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELogin.AllowCredentialPost\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EDirect credential posting\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPkce.Enabled\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPKCE flow support\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPkce.ReturnUrl\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPKCE return URL\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPkce.AutoRedirect\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAutomatic PKCE navigation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPkce.OnAuthorized\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPKCE authorization callback\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAutoRefresh.Interval\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E5 minutes after post-configuration\u003C/td\u003E\n\u003Ctd\u003EBackground refresh interval\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EReauth.Behavior\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003ERedirect\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReauthentication handling\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EReauth.RedirectPath\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReauthentication redirect\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMultiTenant.Enabled\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETenant propagation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMultiTenant.Tenant\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETenant identifier\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMultiTenant.Transport\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003ENone\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETenant transport\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EStateEvents.HandlingMode\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EPatch\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EState event handling\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EResource.BaseAddress\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EResource API origin; defaults to application origin\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EResource.Credentials\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EAuto\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EBrowser credential handling for resource requests\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EAutoRefresh.Enabled\u003C/code\u003E should be evaluated with the applicable platform and post-configuration behavior rather than inferred solely from its property initializer.\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\u003EConfiguration Overview\u003C/strong\u003E \u2014 Understand the configuration architecture and source precedence.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EServer Options\u003C/strong\u003E \u2014 Configure authentication security, sessions, tokens, and endpoints.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EAdvanced Configuration\u003C/strong\u003E \u2014 Explore effective options, mode resolution, and extension points.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EClient Authentication\u003C/strong\u003E \u2014 Implement login, PKCE, refresh, and logout flows.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EClient Session Management\u003C/strong\u003E \u2014 Manage authenticated sessions and revocation.\u003C/li\u003E\n\u003C/ul\u003E\n", "Headings": [ { "Id": "register-the-ultimateauth-client", @@ -93,6 +93,11 @@ "Text": "Remote Clients and Browser Security", "Level": 1 }, + { + "Id": "resource-api-configuration", + "Text": "Resource API Configuration", + "Level": 1 + }, { "Id": "login-flow-options", "Text": "6. Login Flow Options", @@ -409,6 +414,6 @@ "Level": 0 } ], - "SourceHash": "B983498C65C1D5EC6AF5BDD7D78AD78955164848475FF72ADC1D6495E1A1183B", + "SourceHash": "FE74B982178EDB99198EC90811ED91660E95FD2EE3D445361B61236F850B608A", "BuilderVersion": "1" } \ No newline at end of file diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/docs-index.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/docs-index.json index bb77726e..a2d1d0fc 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/docs-index.json +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/docs-index.json @@ -202,6 +202,13 @@ "Group": "client", "GroupOrder": 6 }, + { + "Title": "Resource API Requests", + "Slug": "client-sdk/resource-api-requests", + "Order": 8, + "Group": "client", + "GroupOrder": 6 + }, { "Title": "Configuration Overview", "Slug": "configuration/overview", diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/getting-started/real-world-setup.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/getting-started/real-world-setup.json index d828b7b9..65894848 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/getting-started/real-world-setup.json +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/getting-started/real-world-setup.json @@ -1,7 +1,7 @@ { "Slug": "getting-started/real-world-setup", "Title": "Real World Setup", - "Html": "\n\u003Cp\u003E\u003Cstrong\u003EOne authentication framework. Different deployment models.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EThe \u003Ca href=\u0022/docs/getting-started/quickstart\u0022\u003EQuickStart\u003C/a\u003E demonstrates UltimateAuth with Blazor Server and in-memory storage.\u003C/p\u003E\n\u003Cp\u003EReal applications may require persistent data, a separate authentication server, browser-based clients, or independently deployed APIs.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth supports these scenarios through different service registrations and hosting integrations. You can choose the architecture that fits your application without configuring every available component.\u003C/p\u003E\n\u003Cp\u003EThis guide covers the setup patterns demonstrated by the UltimateAuth samples.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022choose-your-architecture\u0022\u003E1. Choose Your Architecture\u003C/h2\u003E\n\u003Cp\u003EStart by identifying where authentication is hosted and how your application communicates with it.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EApplication architecture\u003C/th\u003E\n\u003Cth\u003EAuthentication host\u003C/th\u003E\n\u003Cth\u003EApplication registration\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EBlazor Server, single project\u003C/td\u003E\n\u003Ctd\u003ESame application\u003C/td\u003E\n\u003Ctd\u003EServer \u002B persistence \u002B Blazor client\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EBlazor Server with EF Core\u003C/td\u003E\n\u003Ctd\u003ESame application\u003C/td\u003E\n\u003Ctd\u003EServer \u002B EF Core \u002B Blazor client\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EDedicated UAuthHub\u003C/td\u003E\n\u003Ctd\u003ESeparate authentication application\u003C/td\u003E\n\u003Ctd\u003EServer \u002B persistence \u002B Hub\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EStandalone Blazor WASM\u003C/td\u003E\n\u003Ctd\u003ERemote UAuthHub\u003C/td\u003E\n\u003Ctd\u003EBlazor client\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EBlazor Web App, Interactive WASM\u003C/td\u003E\n\u003Ctd\u003ERemote UAuthHub\u003C/td\u003E\n\u003Ctd\u003EHost integration \u002B WASM client\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ESeparate Resource API\u003C/td\u003E\n\u003Ctd\u003ERemote UAuthHub\u003C/td\u003E\n\u003Ctd\u003EResource API integration\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThese models can be combined.\u003C/p\u003E\n\u003Cp\u003EFor example, an application might use an EF Core-backed UAuthHub, a standalone Blazor WASM frontend, and a separate Resource API.\u003C/p\u003E\n\u003Cp\u003EThe frontend handles user interaction, UAuthHub manages authentication, and the Resource API enforces access to protected resources.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022persistent-storage-with-entity-framework-core\u0022\u003E2. Persistent Storage with Entity Framework Core\u003C/h2\u003E\n\u003Cp\u003EFor applications that need authentication state to survive restarts, use a persistent storage provider.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth provides an Entity Framework Core bundle that registers its reference domain implementations and EF Core-backed stores.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022install-packages\u0022\u003EInstall packages\u003C/h3\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.EntityFrameworkCore.Bundle\ndotnet add package Microsoft.EntityFrameworkCore.Sqlite\ndotnet add package Microsoft.EntityFrameworkCore.Design\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe SQLite provider is used here for simplicity. You can use another compatible EF Core database provider.\u003C/p\u003E\n\u003Cp\u003EWhen using the EF Core bundle, you do not need the in-memory bundle.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022register-the-services\u0022\u003ERegister the services\u003C/h3\u003E\n\u003Cp\u003EIn \u003Ccode\u003EProgram.cs\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\u003Eusing CodeBeam.UltimateAuth.EntityFrameworkCore;\nusing CodeBeam.UltimateAuth.Server.Extensions;\nusing Microsoft.EntityFrameworkCore;\n\nbuilder.Services.AddUltimateAuthServer()\n .AddUltimateAuthEntityFrameworkCore(db =\u0026gt;\n {\n db.UseSqlite(\u0026quot;Data Source=uauth.db\u0026quot;, sqlite =\u0026gt; sqlite.MigrationsAssembly(typeof(Program).Assembly.GetName().Name!));\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EAddUltimateAuthEntityFrameworkCore()\u003C/code\u003E registers the EF Core stores and reference domain implementations.\u003C/p\u003E\n\u003Cp\u003EThe bundle provides a unified \u003Ccode\u003EUAuthDbContext\u003C/code\u003E for the supported authentication domains.\u003C/p\u003E\n\u003Cp\u003EYou do not need to register each user, session, credential, token, or authorization store individually.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configure-the-database-connection\u0022\u003EConfigure the database connection\u003C/h3\u003E\n\u003Cp\u003EFor a deployed application, prefer configuration over hardcoded connection strings.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003Eappsettings.json\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\u003EJSON\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-json\u0022\u003E{\n \u0026quot;ConnectionStrings\u0026quot;: {\n \u0026quot;UltimateAuth\u0026quot;: \u0026quot;Data Source=uauth.db\u0026quot;\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EProgram.cs\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 connectionString = builder.Configuration.GetConnectionString(\u0026quot;UltimateAuth\u0026quot;)\n ?? throw new InvalidOperationException(\u0026quot;UltimateAuth connection string is missing.\u0026quot;);\n\nbuilder.Services.AddUltimateAuthServer()\n .AddUltimateAuthEntityFrameworkCore(db =\u0026gt;\n {\n db.UseSqlite(connectionString);\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EFor production databases, use your deployment environment\u0027s secret management facilities rather than storing credentials in source control.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022database-migrations\u0022\u003EDatabase migrations\u003C/h3\u003E\n\u003Cp\u003EUltimateAuth does not automatically create or apply migrations when the EF Core bundle is registered.\u003C/p\u003E\n\u003Cp\u003EYour application owns the database migration lifecycle.\u003C/p\u003E\n\u003Cp\u003EInstall the EF Core CLI if 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\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 tool install --global dotnet-ef\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ECreate the initial migration:\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 ef migrations add InitUAuth --context UAuthDbContext\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EApply it:\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 ef database update --context UAuthDbContext\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EFor Visual Studio Package Manager Console:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EPOWERSHELL\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-powershell\u0022\u003EAdd-Migration InitUAuth -Context UAuthDbContext\nUpdate-Database -Context UAuthDbContext\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EInstall \u003Ccode\u003EMicrosoft.EntityFrameworkCore.Tools\u003C/code\u003E if required.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022applying-migrations-programmatically\u0022\u003EApplying migrations programmatically\u003C/h3\u003E\n\u003Cp\u003EThe EF Core samples demonstrate initialization through a scoped \u003Ccode\u003EUAuthDbContext\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EFor a controlled development environment:\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.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore;\n\nusing (var scope = app.Services.CreateScope())\n{\n var db = scope.ServiceProvider.GetRequiredService\u0026lt;UAuthDbContext\u0026gt;();\n await db.Database.MigrateAsync();\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis applies pending migrations. It does not generate migration files.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EProduction guidance:\u003C/strong\u003E Prefer applying reviewed migrations as a separate deployment step, particularly when multiple application instances share a database.\u003C/p\u003E\n\u003Cp\u003EDo not copy the sample initializer\u0027s \u003Ccode\u003Ereset: true\u003C/code\u003E behavior into production. The samples use database reset functionality for repeatable demonstrations, not as a safe deployment strategy.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022optional-sample-accounts-with-ef-core\u0022\u003EOptional: sample accounts with EF Core\u003C/h3\u003E\n\u003Cp\u003EThe sample seed package can also work with EF Core persistence.\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\u003ERegister the scoped seed services:\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.AddScopedUltimateAuthSampleSeed();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAfter preparing the database:\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 (app.Environment.IsDevelopment())\n{\n await app.SeedUltimateAuthAsync();\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe difference matters:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003EAddUltimateAuthSampleSeed()\u003C/code\u003E is used by the in-memory samples.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EAddScopedUltimateAuthSampleSeed()\u003C/code\u003E is used by the EF Core samples.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EDo not deploy the demonstration accounts and passwords to production.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022blazor-server-integrated-authentication\u0022\u003E3. Blazor Server \u2014 Integrated Authentication\u003C/h2\u003E\n\u003Cp\u003E\u003Cstrong\u003EBest for:\u003C/strong\u003E A Blazor Server application that wants to host its own authentication system.\u003C/p\u003E\n\u003Cp\u003EThis is the simplest deployment model for a server-side Blazor application.\u003C/p\u003E\n\u003Cp\u003EThe application hosts UltimateAuth Server and uses the Blazor client adapter in the same project.\u003C/p\u003E\n\u003Cp\u003EA separate UAuthHub is not required.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022packages\u0022\u003EPackages\u003C/h3\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.EntityFrameworkCore.Bundle\ndotnet add package Microsoft.EntityFrameworkCore.Sqlite\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022services\u0022\u003EServices\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\u003Eusing CodeBeam.UltimateAuth.Client.Blazor.Extensions;\nusing CodeBeam.UltimateAuth.EntityFrameworkCore;\nusing CodeBeam.UltimateAuth.Server.Extensions;\nusing Microsoft.EntityFrameworkCore;\n\nbuilder.Services\n .AddRazorComponents()\n .AddInteractiveServerComponents();\n\nbuilder.Services.AddUltimateAuthServer()\n .AddUltimateAuthEntityFrameworkCore(db =\u0026gt;\n {\n db.UseSqlite(\u0026quot;Data Source=uauth.db\u0026quot;);\n });\n\nbuilder.Services.AddUltimateAuthClientBlazor();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022pipeline\u0022\u003EPipeline\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\u003Eapp.UseHttpsRedirection();\napp.UseStaticFiles();\n\napp.UseUltimateAuthWithAspNetCore();\napp.UseAntiforgery();\n\napp.MapUltimateAuthEndpoints();\n\napp.MapRazorComponents\u0026lt;App\u0026gt;()\n .AddInteractiveServerRenderMode()\n .AddUltimateAuthRoutes(UAuthAssemblies.BlazorClient());\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAlso configure the \u003Ccode\u003EUAuthApp\u003C/code\u003E root component, login page, and UltimateAuth JavaScript file as demonstrated in QuickStart.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022authentication-behavior\u0022\u003EAuthentication behavior\u003C/h3\u003E\n\u003Cp\u003EThe default client profile resolution selects \u003Ccode\u003EPureOpaque\u003C/code\u003E for Blazor Server.\u003C/p\u003E\n\u003Cp\u003EThis uses session-oriented authentication rather than requiring the application to manage access and refresh tokens manually.\u003C/p\u003E\n\u003Cp\u003EYou can configure session lifetimes, login policies, credential policies, and other behavior through \u003Ccode\u003EAddUltimateAuthServer()\u003C/code\u003E.\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Login.MaxFailedAttempts = 5;\n options.Login.LockoutDuration = TimeSpan.FromMinutes(15);\n options.Session.Lifetime = TimeSpan.FromDays(7);\n options.Identifiers.Behavior.AllowMultipleUsernames = true;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe effective authentication mode is resolved at runtime. The application does not need to select a single mode globally.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022uauthhub-dedicated-authentication-server\u0022\u003E4. UAuthHub \u2014 Dedicated Authentication Server\u003C/h2\u003E\n\u003Cp\u003E\u003Cstrong\u003EBest for:\u003C/strong\u003E Applications where multiple clients share one authentication authority.\u003C/p\u003E\n\u003Cp\u003EUAuthHub hosts UltimateAuth\u0027s authentication services separately from your frontend applications.\u003C/p\u003E\n\u003Cp\u003EIt is particularly relevant for standalone WASM applications, Interactive WASM applications, and systems with separate Resource APIs.\u003C/p\u003E\n\u003Cp\u003EA typical architecture looks like this:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003E Standalone WASM Interactive WASM\n \u2502 \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u25BC\n UAuthHub\n Authentication\n Sessions \u0026amp; Users\n \u2502\n \u25BC\n Persistence\n\n Resource API \u2500\u2500\u2500\u25BA UAuthHub\n (validation)\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022create-the-hub-application\u0022\u003ECreate the Hub application\u003C/h3\u003E\n\u003Cp\u003EA UAuthHub can be hosted in an ASP.NET Core Blazor Server application.\u003C/p\u003E\n\u003Cp\u003EThe current samples use Blazor Server to provide the Hub\u0027s login UI.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022in-memory-uauthhub\u0022\u003EIn-memory UAuthHub\u003C/h3\u003E\n\u003Cp\u003EFor development:\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\u003ERegister:\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.Extensions;\nusing CodeBeam.UltimateAuth.InMemory;\nusing CodeBeam.UltimateAuth.Server.Extensions;\n\nbuilder.Services.AddUltimateAuthServer()\n .AddUltimateAuthInMemory()\n .AddUAuthHub(options =\u0026gt;\n {\n options.AllowedClientOrigins.Add(\u0026quot;https://localhost:6130\u0026quot;);\n });\n\nbuilder.Services.AddUltimateAuthClientBlazor();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe Hub\u0027s Blazor client registration is needed when the Hub itself hosts UltimateAuth-powered Blazor pages and login UI.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022ef-core-uauthhub\u0022\u003EEF Core UAuthHub\u003C/h3\u003E\n\u003Cp\u003EFor persistent storage:\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.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore;\n\nbuilder.Services.AddUltimateAuthServer()\n .AddUltimateAuthEntityFrameworkCore(db =\u0026gt;\n {\n db.UseSqlite(\u0026quot;Data Source=uauthhub.db\u0026quot;);\n })\n .AddUAuthHub(options =\u0026gt;\n {\n options.AllowedClientOrigins.Add(\u0026quot;https://localhost:6132\u0026quot;);\n });\n\nbuilder.Services.AddUltimateAuthClientBlazor();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe \u003Ccode\u003EAllowedClientOrigins\u003C/code\u003E collection identifies browser origins permitted to interact with the Hub.\u003C/p\u003E\n\u003Cp\u003EUse exact origins, including scheme and port. Do not include paths such as \u003Ccode\u003E/home\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022hub-pipeline\u0022\u003EHub pipeline\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\u003Eapp.UseHttpsRedirection();\n\napp.UseUltimateAuthWithAspNetCore();\napp.UseAntiforgery();\n\napp.MapUltimateAuthEndpoints();\napp.MapUAuthHub();\n\napp.MapStaticAssets();\n\napp.MapRazorComponents\u0026lt;App\u0026gt;()\n .AddInteractiveServerRenderMode()\n .AddUltimateAuthRoutes(UAuthAssemblies.BlazorClient());\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EMapUltimateAuthEndpoints()\u003C/code\u003E maps the authentication endpoints.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EMapUAuthHub()\u003C/code\u003E maps the Hub-specific entry endpoint.\u003C/p\u003E\n\u003Cp\u003EBoth registrations are used by the current UAuthHub samples.\u003C/p\u003E\n\u003Cp\u003EThe Hub also needs its login page and Blazor application lifecycle components.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022choosing-origins\u0022\u003EChoosing origins\u003C/h3\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EApplication\u003C/th\u003E\n\u003Cth\u003EURL\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EUAuthHub\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Ehttps://localhost:6112\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EStandalone WASM\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Ehttps://localhost:6130\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EInteractive WASM\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Ehttps://localhost:6132\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EConfigure the Hub\u0027s allowed origins for the actual frontend applications it serves.\u003C/p\u003E\n\u003Cp\u003EThese localhost addresses are illustrative and must match your deployment.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022blazor-standalone-webassembly\u0022\u003E5. Blazor Standalone WebAssembly\u003C/h2\u003E\n\u003Cp\u003E\u003Cstrong\u003EBest for:\u003C/strong\u003E A browser-hosted application with a separate UAuthHub.\u003C/p\u003E\n\u003Cp\u003EStandalone Blazor WASM runs in the browser. It does not host UltimateAuth Server or database persistence.\u003C/p\u003E\n\u003Cp\u003EAuthentication is handled by the remote UAuthHub.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022client-packages\u0022\u003EClient packages\u003C/h3\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\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configure-services\u0022\u003EConfigure services\u003C/h3\u003E\n\u003Cp\u003EIn the standalone WASM application\u0027s \u003Ccode\u003EProgram.cs\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\u003Eusing CodeBeam.UltimateAuth.Client.Blazor.Extensions;\nusing CodeBeam.UltimateAuth.Core.Domain;\n\nvar builder = WebAssemblyHostBuilder.CreateDefault(args);\n\nbuilder.RootComponents.Add\u0026lt;App\u0026gt;(\u0026quot;#app\u0026quot;);\nbuilder.RootComponents.Add\u0026lt;HeadOutlet\u0026gt;(\u0026quot;head::after\u0026quot;);\n\nbuilder.Services.AddScoped(sp =\u0026gt; new HttpClient\n{\n BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)\n});\n\nbuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Endpoints.BasePath = \u0026quot;https://localhost:6110/auth\u0026quot;;\n options.Pkce.ReturnUrl = \u0026quot;https://localhost:6130/home\u0026quot;;\n options.Login.AllowCredentialPost = true;\n options.Reauth.Behavior = ReauthBehavior.RaiseEvent;\n});\n\nawait builder.Build().RunAsync();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe current standalone WASM sample explicitly configures \u003Ccode\u003ELogin.AllowCredentialPost\u003C/code\u003E and the PKCE return URL.\u003C/p\u003E\n\u003Cp\u003EThese are client-side settings. The UAuthHub must be configured independently.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022what-the-urls-mean\u0022\u003EWhat the URLs mean\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EEndpoints.BasePath\u003C/code\u003E points to the remote authentication endpoints.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EPkce.ReturnUrl\u003C/code\u003E identifies the frontend destination used by the relevant PKCE flow.\u003C/p\u003E\n\u003Cp\u003EThe Hub must recognize the frontend\u0027s origin, and any return URL used in an authentication flow must be validated against the application\u0027s security policy.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022root-component\u0022\u003ERoot component\u003C/h3\u003E\n\u003Cp\u003EA standalone WASM application should initialize UltimateAuth through \u003Ccode\u003EUAuthApp\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EFor example, its root application or routing component can contain:\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;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\u003EInclude the UltimateAuth JavaScript asset in the application\u0027s HTML host page.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022authentication-modes\u0022\u003EAuthentication modes\u003C/h3\u003E\n\u003Cp\u003EThe default \u003Ccode\u003EBlazorWasm\u003C/code\u003E profile resolves to \u003Ccode\u003EHybrid\u003C/code\u003E in the current mode resolver.\u003C/p\u003E\n\u003Cp\u003EDo not manually force \u003Ccode\u003EPureOpaque\u003C/code\u003E for a standalone browser client simply because a Blazor Server application uses it.\u003C/p\u003E\n\u003Cp\u003EThe transport and authentication lifecycle must match the client environment.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022blazor-web-app-interactive-webassembly\u0022\u003E6. Blazor Web App \u2014 Interactive WebAssembly\u003C/h2\u003E\n\u003Cp\u003E\u003Cstrong\u003EBest for:\u003C/strong\u003E A Blazor Web App with an ASP.NET Core host and a separate \u003Ccode\u003E.Client\u003C/code\u003E project.\u003C/p\u003E\n\u003Cp\u003EThis scenario differs from standalone WASM because components may participate in both server-side rendering and browser-side interactivity.\u003C/p\u003E\n\u003Cp\u003EThe current UltimateAuth sample registers the Blazor client integration in \u003Cstrong\u003Eboth projects\u003C/strong\u003E.\u003C/p\u003E\n\u003Cp\u003EIt also adds a separate ASP.NET Core compatibility registration in the host.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022project-structure\u0022\u003EProject structure\u003C/h3\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\u003EMyApplication/\n\u251C\u2500\u2500 MyApplication/\n\u2502 \u251C\u2500\u2500 Program.cs\n\u2502 \u2514\u2500\u2500 Components/\n\u2502 \u2514\u2500\u2500 App.razor\n\u2502\n\u2514\u2500\u2500 MyApplication.Client/\n \u251C\u2500\u2500 Program.cs\n \u2514\u2500\u2500 Pages/\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe host serves the application.\u003C/p\u003E\n\u003Cp\u003EThe \u003Ccode\u003E.Client\u003C/code\u003E project contains browser-executed components and services.\u003C/p\u003E\n\u003Cp\u003EA remote UAuthHub manages authentication.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022host-project-packages\u0022\u003E6.1 Host project packages\u003C/h3\u003E\n\u003Cp\u003EInstall the Blazor client integration and ASP.NET Core compatibility package in the host:\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.Client.AspNetCore\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022host-project-services\u0022\u003E6.2 Host project services\u003C/h3\u003E\n\u003Cp\u003EIn the host \u003Ccode\u003EProgram.cs\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\u003Eusing CodeBeam.UltimateAuth.Client.AspNetCore;\nusing CodeBeam.UltimateAuth.Client.Blazor.Extensions;\nusing CodeBeam.UltimateAuth.Core.Domain;\n\nbuilder.Services\n .AddRazorComponents()\n .AddInteractiveWebAssemblyComponents();\n\nbuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Endpoints.BasePath = \u0026quot;https://localhost:6112/auth\u0026quot;;\n options.Pkce.ReturnUrl = \u0026quot;https://localhost:6132/home\u0026quot;;\n options.Login.AllowCredentialPost = true;\n options.Reauth.Behavior = ReauthBehavior.RaiseEvent;\n});\n\nbuilder.Services.AddUltimateAuthAspNetCoreCompatibility();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EAddUltimateAuthAspNetCoreCompatibility()\u003C/code\u003E registers the compatibility authentication scheme and ASP.NET Core authorization services used by this hosting pattern.\u003C/p\u003E\n\u003Cp\u003EIt does \u003Cstrong\u003Enot\u003C/strong\u003E turn the host into an UltimateAuth authentication server.\u003C/p\u003E\n\u003Cp\u003EIt is also not equivalent to \u003Ccode\u003EAddUltimateAuthResourceApi()\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022host-project-pipeline\u0022\u003E6.3 Host project pipeline\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\u003Eapp.UseHttpsRedirection();\n\napp.UseAntiforgery();\n\napp.MapStaticAssets();\n\napp.MapRazorComponents\u0026lt;App\u0026gt;()\n .AddInteractiveWebAssemblyRenderMode()\n .AddAdditionalAssemblies(typeof(MyApplication.Client._Imports).Assembly, UAuthAssemblies.BlazorClient().First());\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EReplace \u003Ccode\u003EMyApplication.Client._Imports\u003C/code\u003E with the corresponding type from your client project.\u003C/p\u003E\n\u003Cp\u003EThe additional assemblies allow the host to discover routable components from the WASM client project and UltimateAuth\u0027s Blazor client assembly.\u003C/p\u003E\n\u003Cp\u003EThe sample does not register \u003Ccode\u003EAddUltimateAuthServer()\u003C/code\u003E in this host because authentication is delegated to a separate UAuthHub.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022client-project-packages\u0022\u003E6.4 Client project packages\u003C/h3\u003E\n\u003Cp\u003EInstall the Blazor client adapter in the \u003Ccode\u003E.Client\u003C/code\u003E 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 add package CodeBeam.UltimateAuth.Client.Blazor\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022client-project-services\u0022\u003E6.5 Client project services\u003C/h3\u003E\n\u003Cp\u003EIn \u003Ccode\u003EMyApplication.Client/Program.cs\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\u003Eusing CodeBeam.UltimateAuth.Client.Blazor.Extensions;\nusing CodeBeam.UltimateAuth.Core.Domain;\n\nvar builder = WebAssemblyHostBuilder.CreateDefault(args);\n\nbuilder.Services.AddScoped(sp =\u0026gt; new HttpClient\n{\n BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)\n});\n\nbuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Endpoints.BasePath = \u0026quot;https://localhost:6112/auth\u0026quot;;\n options.Pkce.ReturnUrl = \u0026quot;https://localhost:6132/home\u0026quot;;\n options.Login.AllowCredentialPost = true;\n options.Reauth.Behavior = ReauthBehavior.RaiseEvent;\n});\n\nawait builder.Build().RunAsync();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe host and client configurations should agree on the remote UAuthHub URL and authentication flow settings.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022why-both-registrations-are-necessary\u0022\u003EWhy both registrations are necessary\u003C/h3\u003E\n\u003Cp\u003EThe host and browser client have separate dependency injection containers.\u003C/p\u003E\n\u003Cp\u003ERegistering \u003Ccode\u003EAddUltimateAuthClientBlazor()\u003C/code\u003E in the host does not automatically register those services inside the WebAssembly runtime.\u003C/p\u003E\n\u003Cp\u003ELikewise, registering the client adapter only in the \u003Ccode\u003E.Client\u003C/code\u003E project does not supply the services required during server-side integration.\u003C/p\u003E\n\u003Cp\u003EThe additional ASP.NET Core compatibility registration addresses the host\u0027s ASP.NET Core authentication and authorization integration requirements.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EImportant:\u003C/strong\u003E This compatibility layer is not a replacement for server-side authorization of your protected business APIs.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022resource-api-separate-protected-backend\u0022\u003E7. Resource API \u2014 Separate Protected Backend\u003C/h2\u003E\n\u003Cp\u003E\u003Cstrong\u003EBest for:\u003C/strong\u003E An API that needs UltimateAuth authentication validation but does not host the authentication system itself.\u003C/p\u003E\n\u003Cp\u003EA Resource API can be an ASP.NET Core Web API, MVC application, or another supported server-side application.\u003C/p\u003E\n\u003Cp\u003EIt does not need to be a Blazor application.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022architecture\u0022\u003EArchitecture\u003C/h3\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 Blazor Client\n \u2502\n \u250C\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2510\n \u25BC \u25BC\n UAuthHub Resource API\n \u25B2 \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n Authentication\n validation\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe Resource API uses the configured UAuthHub for remote authentication and session validation behavior.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022install-the-server-integration-package\u0022\u003EInstall the server integration package\u003C/h3\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.Server\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configure-services-1\u0022\u003EConfigure services\u003C/h3\u003E\n\u003Cp\u003EIn the Resource API\u0027s \u003Ccode\u003EProgram.cs\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\u003Eusing CodeBeam.UltimateAuth.Server.Extensions;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nbuilder.Services.AddControllers();\n\nbuilder.Services.AddUltimateAuthResourceApi(options =\u0026gt;\n{\n options.UAuthHubBaseUrl = \u0026quot;https://localhost:6112\u0026quot;;\n options.AllowedClientOrigins.Add(\u0026quot;https://localhost:6132\u0026quot;);\n options.CorsPolicyName = \u0026quot;resourceApi\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthHubBaseUrl\u003C/code\u003E is the base address of the authentication server.\u003C/p\u003E\n\u003Cp\u003ENotice that this example uses the Hub origin without the \u003Ccode\u003E/auth\u003C/code\u003E suffix, matching the current Resource API samples.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EAllowedClientOrigins\u003C/code\u003E controls the browser origins permitted by the Resource API\u0027s CORS configuration.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configure-the-pipeline\u0022\u003EConfigure the pipeline\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 app = builder.Build();\n\napp.UseHttpsRedirection();\n\napp.UseUltimateAuthResourceApiWithAspNetCore();\n\napp.MapControllers();\n\napp.Run();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EUseUltimateAuthResourceApiWithAspNetCore()\u003C/code\u003E installs the Resource API integration middleware together with ASP.NET Core authentication and authorization middleware.\u003C/p\u003E\n\u003Cp\u003EYou do not need to register a local UltimateAuth Server or its persistence bundle in the Resource API.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022protecting-endpoints\u0022\u003EProtecting endpoints\u003C/h3\u003E\n\u003Cp\u003EUse ASP.NET Core authorization attributes for endpoints that require authentication:\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 Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\n[ApiController]\n[Route(\u0026quot;api/products\u0026quot;)]\npublic class ProductsController : ControllerBase\n{\n [HttpGet]\n [Authorize]\n public IActionResult GetProducts()\n {\n return Ok(new[]\n {\n new { Id = 1, Name = \u0026quot;Example product\u0026quot; }\n });\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe authorization attribute is enforced by ASP.NET Core on the server.\u003C/p\u003E\n\u003Cp\u003EFor permission-specific rules, configure and apply the corresponding authorization policies.\u003C/p\u003E\n\u003Cp\u003EDo not rely on Blazor UI authorization checks to protect Resource API endpoints.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022cors-configuration\u0022\u003ECORS configuration\u003C/h3\u003E\n\u003Cp\u003EThe current Resource API integration registers CORS from the configured allowed origins.\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\u003Eoptions.AllowedClientOrigins.Add(\u0026quot;https://localhost:6132\u0026quot;);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EOnly add origins that should be allowed to call your API from a browser.\u003C/p\u003E\n\u003Cp\u003ECORS is a browser access control mechanism, not an authentication or authorization mechanism.\u003C/p\u003E\n\u003Cp\u003EA request must still pass the applicable authentication and authorization checks.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022resource-api-vs-uauthhub\u0022\u003EResource API vs UAuthHub\u003C/h3\u003E\n\u003Cp\u003EThese are different responsibilities.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EUAuthHub\u003C/th\u003E\n\u003Cth\u003EResource API\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EHosts authentication flows\u003C/td\u003E\n\u003Ctd\u003EHosts application resources\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EManages authentication sessions\u003C/td\u003E\n\u003Ctd\u003EValidates remote authentication context\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EUses authentication persistence\u003C/td\u003E\n\u003Ctd\u003EDoes not require local authentication stores\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EProvides login and related endpoints\u003C/td\u003E\n\u003Ctd\u003EProvides application endpoints\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EServes authentication clients\u003C/td\u003E\n\u003Ctd\u003EServes authorized API consumers\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EUse a Resource API when you want independent deployment of business endpoints without duplicating the authentication server.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022resource-api-with-an-ef-core-backed-uauthhub\u0022\u003E8. Resource API with an EF Core-Backed UAuthHub\u003C/h2\u003E\n\u003Cp\u003EThe Resource API does not need to use EF Core just because UAuthHub does.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EInteractive WASM\n \u2502\n \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25BA Resource API\n \u2502\n \u25BC\n UAuthHub\n \u2502\n \u25BC\n EF Core Database\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe Hub stores users, credentials, sessions, and related security state in its database.\u003C/p\u003E\n\u003Cp\u003EThe Resource API communicates with the Hub through the configured remote validation integration.\u003C/p\u003E\n\u003Cp\u003EIts registration remains:\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.AddUltimateAuthResourceApi(options =\u0026gt;\n{\n options.UAuthHubBaseUrl = \u0026quot;https://localhost:6112\u0026quot;;\n options.AllowedClientOrigins.Add(\u0026quot;https://localhost:6132\u0026quot;);\n options.CorsPolicyName = \u0026quot;resourceApi\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis separation allows the API and authentication infrastructure to be deployed, scaled, and maintained independently.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022calling-a-resource-api-from-blazor\u0022\u003E9. Calling a Resource API from Blazor\u003C/h2\u003E\n\u003Cp\u003EThe current samples demonstrate calling a separate Resource API from Blazor applications.\u003C/p\u003E\n\u003Cp\u003EThe client integration and the business API\u0027s HTTP client are separate concerns.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022interactive-wasm-host\u0022\u003EInteractive WASM host\u003C/h3\u003E\n\u003Cp\u003EIn the ASP.NET Core host:\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.AddHttpClient\u0026lt;SampleProductApiService\u0026gt;(\n client =\u0026gt;\n {\n client.BaseAddress = new Uri(\u0026quot;https://localhost:6122\u0026quot;);\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022interactive-wasm-client\u0022\u003EInteractive WASM client\u003C/h3\u003E\n\u003Cp\u003ERegister the same application API service in the \u003Ccode\u003E.Client\u003C/code\u003E 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\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 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.AddHttpClient\u0026lt;SampleProductApiService\u0026gt;(\n client =\u0026gt;\n {\n client.BaseAddress = new Uri(\u0026quot;https://localhost:6122\u0026quot;);\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe host and client each require the service when application components or services execute in their respective environments.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003ESampleProductApiService\u003C/code\u003E is an example application service, not a required UltimateAuth framework type.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022standalone-wasm\u0022\u003EStandalone WASM\u003C/h3\u003E\n\u003Cp\u003EA standalone WASM application can register an HTTP client for its Resource API:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddScoped(sp =\u0026gt; new HttpClient\n{\n BaseAddress = new Uri(\u0026quot;https://localhost:6120\u0026quot;)\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EIf your application has multiple HTTP clients, prefer explicitly named or typed clients to avoid ambiguity.\u003C/p\u003E\n\u003Cp\u003EThe current samples also demonstrate browser request credentials handling for cookie-based requests.\u003C/p\u003E\n\u003Cp\u003EFor cross-origin cookie authentication, the browser request, API CORS policy, cookie attributes, and Hub configuration must all be compatible.\u003C/p\u003E\n\u003Cp\u003EDo not assume that registering an \u003Ccode\u003EHttpClient\u003C/code\u003E automatically forwards authentication credentials to every remote API.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022authentication-modes-and-client-profiles\u0022\u003E10. Authentication Modes and Client Profiles\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth separates \u003Cstrong\u003Eclient profile detection\u003C/strong\u003E from \u003Cstrong\u003Eauthentication mode enforcement\u003C/strong\u003E.\u003C/p\u003E\n\u003Cp\u003EThe default resolver maps client profiles to authentication modes.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EClient profile\u003C/th\u003E\n\u003Cth\u003EDefault resolved mode\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EBlazor Server\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EPureOpaque\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EBlazor WASM\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EHybrid\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EWeb Server\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EHybrid\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EMAUI\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EHybrid\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAPI\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EPureJwt\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThis mapping describes the resolver\u0027s behavior, not a guarantee that every mode is currently fully supported by the server.\u003C/p\u003E\n\u003Cp\u003EIn particular, \u003Ccode\u003EPureJwt\u003C/code\u003E and \u003Ccode\u003ESemiHybrid\u003C/code\u003E have implementation limitations in the current release.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022restrict-allowed-modes\u0022\u003ERestrict allowed modes\u003C/h3\u003E\n\u003Cp\u003EOn the authentication server:\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.AllowedModes =\n [\n UAuthMode.PureOpaque,\n UAuthMode.Hybrid\n ];\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EAllowedModes\u003C/code\u003E is a guardrail.\u003C/p\u003E\n\u003Cp\u003EIt does not select a mode for every request.\u003C/p\u003E\n\u003Cp\u003EThe effective mode is resolved from the client profile and then checked against the server\u0027s allowed-mode policy.\u003C/p\u003E\n\u003Cp\u003EFor most applications, use the default mode resolution unless you have a specific integration requirement.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022server-options-for-real-applications\u0022\u003E11. Server Options for Real Applications\u003C/h2\u003E\n\u003Cp\u003EProduction configuration should reflect your application\u0027s security requirements.\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Login.MaxFailedAttempts = 5;\n options.Login.LockoutDuration = TimeSpan.FromMinutes(15);\n options.Identifiers.Behavior.AllowMultipleUsernames = true;\n options.UserProfile.EnableMultiProfile = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EServer options also compose the Core-defined login, session, token, PKCE, and multi-tenant options.\u003C/p\u003E\n\u003Cp\u003EConfigure those through \u003Ccode\u003EAddUltimateAuthServer()\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EDo not independently configure Core options and assume the server will automatically use them.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configuration-files\u0022\u003EConfiguration files\u003C/h3\u003E\n\u003Cp\u003EUltimateAuth also supports configuration through \u003Ccode\u003Eappsettings.json\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\u003EJSON\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-json\u0022\u003E{\n \u0026quot;UltimateAuth\u0026quot;: {\n \u0026quot;Server\u0026quot;: {\n \u0026quot;Login\u0026quot;: {\n \u0026quot;MaxFailedAttempts\u0026quot;: 5,\n \u0026quot;LockoutDuration\u0026quot;: \u0026quot;00:15:00\u0026quot;\n }\n }\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe server options registration binds the \u003Ccode\u003EUltimateAuth:Server\u003C/code\u003E configuration section.\u003C/p\u003E\n\u003Cp\u003ERemember that effective mode defaults and mode-specific overrides are applied after the base options have been configured.\u003C/p\u003E\n\u003Cp\u003EFor detailed configuration behavior, see Server Options and Advanced Configuration.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022development-and-production-differences\u0022\u003E12. Development and Production Differences\u003C/h2\u003E\n\u003Cp\u003EThe samples intentionally prioritize visibility and repeatability.\u003C/p\u003E\n\u003Cp\u003EA production application needs additional safeguards.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EArea\u003C/th\u003E\n\u003Cth\u003ESample or development\u003C/th\u003E\n\u003Cth\u003EProduction\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EPersistence\u003C/td\u003E\n\u003Ctd\u003EInMemory or local SQLite\u003C/td\u003E\n\u003Ctd\u003EDurable database\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ESeed accounts\u003C/td\u003E\n\u003Ctd\u003EExample users and passwords\u003C/td\u003E\n\u003Ctd\u003EReal provisioning\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EDatabase initialization\u003C/td\u003E\n\u003Ctd\u003EMay reset sample data\u003C/td\u003E\n\u003Ctd\u003EReviewed migrations\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EHTTPS\u003C/td\u003E\n\u003Ctd\u003ELocal development certificate\u003C/td\u003E\n\u003Ctd\u003EProper TLS termination\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ECORS\u003C/td\u003E\n\u003Ctd\u003ELocalhost origins\u003C/td\u003E\n\u003Ctd\u003EExact trusted origins\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ESigning keys\u003C/td\u003E\n\u003Ctd\u003EDevelopment defaults\u003C/td\u003E\n\u003Ctd\u003EManaged, persistent keys\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EDiagnostics\u003C/td\u003E\n\u003Ctd\u003EDetailed debugging\u003C/td\u003E\n\u003Ctd\u003ERestrict sensitive diagnostics\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ECredential recovery\u003C/td\u003E\n\u003Ctd\u003EDevelopment notifier\u003C/td\u003E\n\u003Ctd\u003EReal secure notification channel\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EIdentifier verification\u003C/td\u003E\n\u003Ctd\u003EDevelopment bypass\u003C/td\u003E\n\u003Ctd\u003EProof-based verification\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ESecrets\u003C/td\u003E\n\u003Ctd\u003ELocal settings\u003C/td\u003E\n\u003Ctd\u003ESecure configuration provider\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe sample projects include development-only implementations for credential reset notifications and identifier verification.\u003C/p\u003E\n\u003Cp\u003EThose implementations demonstrate extension points and should not be treated as production verification systems.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022credential-recovery\u0022\u003ECredential recovery\u003C/h3\u003E\n\u003Cp\u003EIf your application exposes credential recovery, configure a real implementation of the credential reset notifier.\u003C/p\u003E\n\u003Cp\u003ERecovery secrets must be delivered through an appropriate channel and must not be exposed directly through anonymous API responses.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022identifier-verification\u0022\u003EIdentifier verification\u003C/h3\u003E\n\u003Cp\u003EIf your application supports verified email addresses, phone numbers, or other identifiers, use an implementation that verifies ownership through suitable proof.\u003C/p\u003E\n\u003Cp\u003EDo not use a development bypass verifier in production.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022signing-keys\u0022\u003ESigning keys\u003C/h3\u003E\n\u003Cp\u003EConfigure persistent signing keys for authentication modes that issue JWTs.\u003C/p\u003E\n\u003Cp\u003EDevelopment signing key providers are not a substitute for production key management.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022deployment-checklist\u0022\u003E13. Deployment Checklist\u003C/h2\u003E\n\u003Cp\u003EBefore deploying, verify the following:\u003C/p\u003E\n\u003Cul class=\u0022contains-task-list\u0022\u003E\n\u003Cli class=\u0022task-list-item\u0022\u003E\u003Cinput disabled=\u0022disabled\u0022 type=\u0022checkbox\u0022\u003E The selected architecture matches the actual client hosting model.\u003C/li\u003E\n\u003Cli class=\u0022task-list-item\u0022\u003E\u003Cinput disabled=\u0022disabled\u0022 type=\u0022checkbox\u0022\u003E The authentication server uses appropriate persistent storage.\u003C/li\u003E\n\u003Cli class=\u0022task-list-item\u0022\u003E\u003Cinput disabled=\u0022disabled\u0022 type=\u0022checkbox\u0022\u003E Database migrations have been reviewed and applied.\u003C/li\u003E\n\u003Cli class=\u0022task-list-item\u0022\u003E\u003Cinput disabled=\u0022disabled\u0022 type=\u0022checkbox\u0022\u003E Sample users and development-only verification services are disabled.\u003C/li\u003E\n\u003Cli class=\u0022task-list-item\u0022\u003E\u003Cinput disabled=\u0022disabled\u0022 type=\u0022checkbox\u0022\u003E Production signing keys and secrets are configured.\u003C/li\u003E\n\u003Cli class=\u0022task-list-item\u0022\u003E\u003Cinput disabled=\u0022disabled\u0022 type=\u0022checkbox\u0022\u003E HTTPS is enforced at the appropriate boundary.\u003C/li\u003E\n\u003Cli class=\u0022task-list-item\u0022\u003E\u003Cinput disabled=\u0022disabled\u0022 type=\u0022checkbox\u0022\u003E UAuthHub and Resource API origins are explicitly configured.\u003C/li\u003E\n\u003Cli class=\u0022task-list-item\u0022\u003E\u003Cinput disabled=\u0022disabled\u0022 type=\u0022checkbox\u0022\u003E Authentication endpoints and application endpoints are protected appropriately.\u003C/li\u003E\n\u003Cli class=\u0022task-list-item\u0022\u003E\u003Cinput disabled=\u0022disabled\u0022 type=\u0022checkbox\u0022\u003E Client and server endpoint URLs match the deployment.\u003C/li\u003E\n\u003Cli class=\u0022task-list-item\u0022\u003E\u003Cinput disabled=\u0022disabled\u0022 type=\u0022checkbox\u0022\u003E Session expiration, refresh, logout, and revocation have been tested.\u003C/li\u003E\n\u003Cli class=\u0022task-list-item\u0022\u003E\u003Cinput disabled=\u0022disabled\u0022 type=\u0022checkbox\u0022\u003E Protected resources enforce server-side authorization.\u003C/li\u003E\n\u003Cli class=\u0022task-list-item\u0022\u003E\u003Cinput disabled=\u0022disabled\u0022 type=\u0022checkbox\u0022\u003E Authentication state and persistence survive the expected application lifecycle.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022choosing-your-architecture\u0022\u003E14. Choosing Your Architecture\u003C/h2\u003E\n\u003Cp\u003E\u003Cstrong\u003EStart with one server and one client. Connect more clients whenever you need them.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EUltimateAuth is built around two fundamental responsibilities:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Cstrong\u003EUltimateAuth Server\u003C/strong\u003E \u2014 Manages authentication, users, credentials, sessions, authorization, and the security lifecycle.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EUltimateAuth Client\u003C/strong\u003E \u2014 Integrates an application with the authentication system using the appropriate client profile and authentication flows.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EEvery application needs access to an authentication server, but that server does not have to be a separate application. It can be hosted within your existing ASP.NET Core application or deployed independently as UAuthHub.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022single-application-the-simplest-setup\u0022\u003ESingle Application \u2014 The Simplest Setup\u003C/h3\u003E\n\u003Cp\u003EFor a traditional ASP.NET Core or Blazor Server application, UltimateAuth Server and Client can run together.\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\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Your Application \u2502\n\u2502 \u2502\n\u2502 UltimateAuth Client \u2502\n\u2502 \u2502 \u2502\n\u2502 UltimateAuth Server \u2502\n\u2502 \u2502 \u2502\n\u2502 Database \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis approach is ideal when you want authentication without introducing another service or deployment.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022multiple-applications-one-authentication-server\u0022\u003EMultiple Applications \u2014 One Authentication Server\u003C/h3\u003E\n\u003Cp\u003EAs your system grows, you can connect multiple client applications to the same UltimateAuth Server.\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 Blazor Server Blazor WASM\n \u2502 \u2502\n \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n \u2502 \u2502 \u2502\n Razor Pages MAUI Other .NET Clients\n \u2502 \u2502 \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u25BC\n UltimateAuth Server\n UAuthHub\n \u2502\n \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u25BC \u25BC \u25BC\n Users Sessions Authorization\n \u2502\n \u25BC\n Persistence\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EEach client can have its own application structure, hosting model, and authentication requirements while sharing the same authentication authority.\u003C/p\u003E\n\u003Cp\u003EFor example, a business platform might use Blazor Server for administration, Blazor WASM for its customer portal, MAUI for mobile access, and Razor Pages for an internal application.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EAdding another client does not require replacing the existing clients or creating another UltimateAuth Server.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EThe same users, credentials, security policies, and session infrastructure can serve multiple applications, subject to the authentication modes and client integrations supported by the installed version.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022distributed-applications-separate-resource-apis\u0022\u003EDistributed Applications \u2014 Separate Resource APIs\u003C/h3\u003E\n\u003Cp\u003EWhen your architecture includes independently deployed backend services, Resource APIs can integrate with the same authentication authority.\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 Multiple Client Applications\n \u2502\n \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u25BC \u25BC\n UAuthHub Resource APIs\n \u25B2 \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n Authentication\n Validation\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EResource APIs protect application resources without becoming additional authentication servers.\u003C/p\u003E\n\u003Cp\u003EThis lets you separate business services from authentication infrastructure while maintaining a shared security model.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022one-foundation-different-scales\u0022\u003EOne Foundation, Different Scales\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EYour scenario\u003C/th\u003E\n\u003Cth\u003ERecommended approach\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EA simple Blazor Server application\u003C/td\u003E\n\u003Ctd\u003EIntegrated Server \u002B Client\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EA traditional ASP.NET Core application\u003C/td\u003E\n\u003Ctd\u003EServer hosted within the application\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EA standalone Blazor WASM application\u003C/td\u003E\n\u003Ctd\u003EClient \u002B remote UAuthHub\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAn Interactive WASM application\u003C/td\u003E\n\u003Ctd\u003EHost \u002B WASM Client \u002B remote UAuthHub\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EMultiple web, desktop, or mobile applications\u003C/td\u003E\n\u003Ctd\u003EMultiple Clients \u002B shared UAuthHub\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EMultiple backend services\u003C/td\u003E\n\u003Ctd\u003EShared UAuthHub \u002B Resource APIs\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Cstrong\u003EUltimateAuth does not require a complex architecture to get started, and it does not restrict you to a single client as your application grows.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EStart with the simplest deployment that meets your needs. Add new client applications, persistent storage, or separate Resource APIs as your requirements evolve.\u003C/p\u003E\n\u003Cp\u003EThe authentication foundation remains consistent.\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/getting-started/quickstart\u0022\u003EQuickStart\u003C/a\u003E \u2014 Build the minimal in-memory application.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/getting-started/ecosystem\u0022\u003EEcosystem\u003C/a\u003E \u2014 Explore packages and supported integrations.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/configuration/server-options\u0022\u003EServer Options\u003C/a\u003E \u2014 Configure server authentication and security behavior.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/configuration/client-options\u0022\u003EClient Options\u003C/a\u003E \u2014 Configure client profiles, endpoints, refresh, and reauthentication.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/configuration/advanced-configuration\u0022\u003EAdvanced Configuration\u003C/a\u003E \u2014 Explore effective options, dependency injection, and extension points.\u003C/li\u003E\n\u003C/ul\u003E\n", + "Html": "\n\u003Cp\u003E\u003Cstrong\u003EOne authentication framework. Different deployment models.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EThe \u003Ca href=\u0022/docs/getting-started/quickstart\u0022\u003EQuickStart\u003C/a\u003E demonstrates UltimateAuth with Blazor Server and in-memory storage.\u003C/p\u003E\n\u003Cp\u003EReal applications may require persistent data, a separate authentication server, browser-based clients, or independently deployed APIs.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth supports these scenarios through different service registrations and hosting integrations. You can choose the architecture that fits your application without configuring every available component.\u003C/p\u003E\n\u003Cp\u003EThis guide covers the setup patterns demonstrated by the UltimateAuth samples.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022choose-your-architecture\u0022\u003E1. Choose Your Architecture\u003C/h2\u003E\n\u003Cp\u003EStart by identifying where authentication is hosted and how your application communicates with it.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EApplication architecture\u003C/th\u003E\n\u003Cth\u003EAuthentication host\u003C/th\u003E\n\u003Cth\u003EApplication registration\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EBlazor Server, single project\u003C/td\u003E\n\u003Ctd\u003ESame application\u003C/td\u003E\n\u003Ctd\u003EServer \u002B persistence \u002B Blazor client\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EBlazor Server with EF Core\u003C/td\u003E\n\u003Ctd\u003ESame application\u003C/td\u003E\n\u003Ctd\u003EServer \u002B EF Core \u002B Blazor client\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EDedicated UAuthHub\u003C/td\u003E\n\u003Ctd\u003ESeparate authentication application\u003C/td\u003E\n\u003Ctd\u003EServer \u002B persistence \u002B Hub\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EStandalone Blazor WASM\u003C/td\u003E\n\u003Ctd\u003ERemote UAuthHub\u003C/td\u003E\n\u003Ctd\u003EBlazor client\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EBlazor Web App, Interactive WASM\u003C/td\u003E\n\u003Ctd\u003ERemote UAuthHub\u003C/td\u003E\n\u003Ctd\u003EHost integration \u002B WASM client\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ESeparate Resource API\u003C/td\u003E\n\u003Ctd\u003ERemote UAuthHub\u003C/td\u003E\n\u003Ctd\u003EResource API integration\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThese models can be combined.\u003C/p\u003E\n\u003Cp\u003EFor example, an application might use an EF Core-backed UAuthHub, a standalone Blazor WASM frontend, and a separate Resource API.\u003C/p\u003E\n\u003Cp\u003EThe frontend handles user interaction, UAuthHub manages authentication, and the Resource API enforces access to protected resources.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022persistent-storage-with-entity-framework-core\u0022\u003E2. Persistent Storage with Entity Framework Core\u003C/h2\u003E\n\u003Cp\u003EFor applications that need authentication state to survive restarts, use a persistent storage provider.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth provides an Entity Framework Core bundle that registers its reference domain implementations and EF Core-backed stores.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022install-packages\u0022\u003EInstall packages\u003C/h3\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.EntityFrameworkCore.Bundle\ndotnet add package Microsoft.EntityFrameworkCore.Sqlite\ndotnet add package Microsoft.EntityFrameworkCore.Design\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe SQLite provider is used here for simplicity. You can use another compatible EF Core database provider.\u003C/p\u003E\n\u003Cp\u003EWhen using the EF Core bundle, you do not need the in-memory bundle.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022register-the-services\u0022\u003ERegister the services\u003C/h3\u003E\n\u003Cp\u003EIn \u003Ccode\u003EProgram.cs\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\u003Eusing CodeBeam.UltimateAuth.EntityFrameworkCore;\nusing CodeBeam.UltimateAuth.Server.Extensions;\nusing Microsoft.EntityFrameworkCore;\n\nbuilder.Services.AddUltimateAuthServer()\n .AddUltimateAuthEntityFrameworkCore(db =\u0026gt;\n {\n db.UseSqlite(\u0026quot;Data Source=uauth.db\u0026quot;, sqlite =\u0026gt; sqlite.MigrationsAssembly(typeof(Program).Assembly.GetName().Name!));\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EAddUltimateAuthEntityFrameworkCore()\u003C/code\u003E registers the EF Core stores and reference domain implementations.\u003C/p\u003E\n\u003Cp\u003EThe bundle provides a unified \u003Ccode\u003EUAuthDbContext\u003C/code\u003E for the supported authentication domains.\u003C/p\u003E\n\u003Cp\u003EYou do not need to register each user, session, credential, token, or authorization store individually.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configure-the-database-connection\u0022\u003EConfigure the database connection\u003C/h3\u003E\n\u003Cp\u003EFor a deployed application, prefer configuration over hardcoded connection strings.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003Eappsettings.json\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\u003EJSON\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-json\u0022\u003E{\n \u0026quot;ConnectionStrings\u0026quot;: {\n \u0026quot;UltimateAuth\u0026quot;: \u0026quot;Data Source=uauth.db\u0026quot;\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EProgram.cs\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 connectionString = builder.Configuration.GetConnectionString(\u0026quot;UltimateAuth\u0026quot;)\n ?? throw new InvalidOperationException(\u0026quot;UltimateAuth connection string is missing.\u0026quot;);\n\nbuilder.Services.AddUltimateAuthServer()\n .AddUltimateAuthEntityFrameworkCore(db =\u0026gt;\n {\n db.UseSqlite(connectionString);\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EFor production databases, use your deployment environment\u0027s secret management facilities rather than storing credentials in source control.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022database-migrations\u0022\u003EDatabase migrations\u003C/h3\u003E\n\u003Cp\u003EUltimateAuth does not automatically create or apply migrations when the EF Core bundle is registered.\u003C/p\u003E\n\u003Cp\u003EYour application owns the database migration lifecycle.\u003C/p\u003E\n\u003Cp\u003EInstall the EF Core CLI if 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\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 tool install --global dotnet-ef\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ECreate the initial migration:\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 ef migrations add InitUAuth --context UAuthDbContext\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EApply it:\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 ef database update --context UAuthDbContext\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EFor Visual Studio Package Manager Console:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EPOWERSHELL\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-powershell\u0022\u003EAdd-Migration InitUAuth -Context UAuthDbContext\nUpdate-Database -Context UAuthDbContext\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EInstall \u003Ccode\u003EMicrosoft.EntityFrameworkCore.Tools\u003C/code\u003E if required.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022applying-migrations-programmatically\u0022\u003EApplying migrations programmatically\u003C/h3\u003E\n\u003Cp\u003EThe EF Core samples demonstrate initialization through a scoped \u003Ccode\u003EUAuthDbContext\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EFor a controlled development environment:\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.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore;\n\nusing (var scope = app.Services.CreateScope())\n{\n var db = scope.ServiceProvider.GetRequiredService\u0026lt;UAuthDbContext\u0026gt;();\n await db.Database.MigrateAsync();\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis applies pending migrations. It does not generate migration files.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EProduction guidance:\u003C/strong\u003E Prefer applying reviewed migrations as a separate deployment step, particularly when multiple application instances share a database.\u003C/p\u003E\n\u003Cp\u003EDo not copy the sample initializer\u0027s \u003Ccode\u003Ereset: true\u003C/code\u003E behavior into production. The samples use database reset functionality for repeatable demonstrations, not as a safe deployment strategy.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022optional-sample-accounts-with-ef-core\u0022\u003EOptional: sample accounts with EF Core\u003C/h3\u003E\n\u003Cp\u003EThe sample seed package can also work with EF Core persistence.\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\u003ERegister the scoped seed services:\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.AddScopedUltimateAuthSampleSeed();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAfter preparing the database:\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 (app.Environment.IsDevelopment())\n{\n await app.SeedUltimateAuthAsync();\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe difference matters:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003EAddUltimateAuthSampleSeed()\u003C/code\u003E is used by the in-memory samples.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EAddScopedUltimateAuthSampleSeed()\u003C/code\u003E is used by the EF Core samples.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EDo not deploy the demonstration accounts and passwords to production.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022blazor-server-integrated-authentication\u0022\u003E3. Blazor Server \u2014 Integrated Authentication\u003C/h2\u003E\n\u003Cp\u003E\u003Cstrong\u003EBest for:\u003C/strong\u003E A Blazor Server application that wants to host its own authentication system.\u003C/p\u003E\n\u003Cp\u003EThis is the simplest deployment model for a server-side Blazor application.\u003C/p\u003E\n\u003Cp\u003EThe application hosts UltimateAuth Server and uses the Blazor client adapter in the same project.\u003C/p\u003E\n\u003Cp\u003EA separate UAuthHub is not required.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022packages\u0022\u003EPackages\u003C/h3\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.EntityFrameworkCore.Bundle\ndotnet add package Microsoft.EntityFrameworkCore.Sqlite\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022services\u0022\u003EServices\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\u003Eusing CodeBeam.UltimateAuth.Client.Blazor.Extensions;\nusing CodeBeam.UltimateAuth.EntityFrameworkCore;\nusing CodeBeam.UltimateAuth.Server.Extensions;\nusing Microsoft.EntityFrameworkCore;\n\nbuilder.Services\n .AddRazorComponents()\n .AddInteractiveServerComponents();\n\nbuilder.Services.AddUltimateAuthServer()\n .AddUltimateAuthEntityFrameworkCore(db =\u0026gt;\n {\n db.UseSqlite(\u0026quot;Data Source=uauth.db\u0026quot;);\n });\n\nbuilder.Services.AddUltimateAuthClientBlazor();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022pipeline\u0022\u003EPipeline\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\u003Eapp.UseHttpsRedirection();\napp.UseStaticFiles();\n\napp.UseUltimateAuthWithAspNetCore();\napp.UseAntiforgery();\n\napp.MapUltimateAuthEndpoints();\n\napp.MapRazorComponents\u0026lt;App\u0026gt;()\n .AddInteractiveServerRenderMode()\n .AddUltimateAuthRoutes(UAuthAssemblies.BlazorClient());\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAlso configure the \u003Ccode\u003EUAuthApp\u003C/code\u003E root component, login page, and UltimateAuth JavaScript file as demonstrated in QuickStart.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022authentication-behavior\u0022\u003EAuthentication behavior\u003C/h3\u003E\n\u003Cp\u003EThe default client profile resolution selects \u003Ccode\u003EPureOpaque\u003C/code\u003E for Blazor Server.\u003C/p\u003E\n\u003Cp\u003EThis uses session-oriented authentication rather than requiring the application to manage access and refresh tokens manually.\u003C/p\u003E\n\u003Cp\u003EYou can configure session lifetimes, login policies, credential policies, and other behavior through \u003Ccode\u003EAddUltimateAuthServer()\u003C/code\u003E.\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Login.MaxFailedAttempts = 5;\n options.Login.LockoutDuration = TimeSpan.FromMinutes(15);\n options.Session.Lifetime = TimeSpan.FromDays(7);\n options.Identifiers.Behavior.AllowMultipleUsernames = true;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe effective authentication mode is resolved at runtime. The application does not need to select a single mode globally.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022uauthhub-dedicated-authentication-server\u0022\u003E4. UAuthHub \u2014 Dedicated Authentication Server\u003C/h2\u003E\n\u003Cp\u003E\u003Cstrong\u003EBest for:\u003C/strong\u003E Applications where multiple clients share one authentication authority.\u003C/p\u003E\n\u003Cp\u003EUAuthHub hosts UltimateAuth\u0027s authentication services separately from your frontend applications.\u003C/p\u003E\n\u003Cp\u003EIt is particularly relevant for standalone WASM applications, Interactive WASM applications, and systems with separate Resource APIs.\u003C/p\u003E\n\u003Cp\u003EA typical architecture looks like this:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003E Standalone WASM Interactive WASM\n \u2502 \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u25BC\n UAuthHub\n Authentication\n Sessions \u0026amp; Users\n \u2502\n \u25BC\n Persistence\n\n Resource API \u2500\u2500\u2500\u25BA UAuthHub\n (validation)\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022create-the-hub-application\u0022\u003ECreate the Hub application\u003C/h3\u003E\n\u003Cp\u003EA UAuthHub can be hosted in an ASP.NET Core Blazor Server application.\u003C/p\u003E\n\u003Cp\u003EThe current samples use Blazor Server to provide the Hub\u0027s login UI.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022in-memory-uauthhub\u0022\u003EIn-memory UAuthHub\u003C/h3\u003E\n\u003Cp\u003EFor development:\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\u003ERegister:\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.Extensions;\nusing CodeBeam.UltimateAuth.InMemory;\nusing CodeBeam.UltimateAuth.Server.Extensions;\n\nbuilder.Services.AddUltimateAuthServer()\n .AddUltimateAuthInMemory()\n .AddUAuthHub(options =\u0026gt;\n {\n options.AllowedClientOrigins.Add(\u0026quot;https://localhost:6130\u0026quot;);\n });\n\nbuilder.Services.AddUltimateAuthClientBlazor();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe Hub\u0027s Blazor client registration is needed when the Hub itself hosts UltimateAuth-powered Blazor pages and login UI.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022ef-core-uauthhub\u0022\u003EEF Core UAuthHub\u003C/h3\u003E\n\u003Cp\u003EFor persistent storage:\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.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore;\n\nbuilder.Services.AddUltimateAuthServer()\n .AddUltimateAuthEntityFrameworkCore(db =\u0026gt;\n {\n db.UseSqlite(\u0026quot;Data Source=uauthhub.db\u0026quot;);\n })\n .AddUAuthHub(options =\u0026gt;\n {\n options.AllowedClientOrigins.Add(\u0026quot;https://localhost:6132\u0026quot;);\n });\n\nbuilder.Services.AddUltimateAuthClientBlazor();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe \u003Ccode\u003EAllowedClientOrigins\u003C/code\u003E collection identifies browser origins permitted to interact with the Hub.\u003C/p\u003E\n\u003Cp\u003EUse exact origins, including scheme and port. Do not include paths such as \u003Ccode\u003E/home\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022hub-pipeline\u0022\u003EHub pipeline\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\u003Eapp.UseHttpsRedirection();\n\napp.UseUltimateAuthWithAspNetCore();\napp.UseAntiforgery();\n\napp.MapUltimateAuthEndpoints();\napp.MapUAuthHub();\n\napp.MapStaticAssets();\n\napp.MapRazorComponents\u0026lt;App\u0026gt;()\n .AddInteractiveServerRenderMode()\n .AddUltimateAuthRoutes(UAuthAssemblies.BlazorClient());\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EMapUltimateAuthEndpoints()\u003C/code\u003E maps the authentication endpoints.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EMapUAuthHub()\u003C/code\u003E maps the Hub-specific entry endpoint.\u003C/p\u003E\n\u003Cp\u003EBoth registrations are used by the current UAuthHub samples.\u003C/p\u003E\n\u003Cp\u003EThe Hub also needs its login page and Blazor application lifecycle components.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022choosing-origins\u0022\u003EChoosing origins\u003C/h3\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EApplication\u003C/th\u003E\n\u003Cth\u003EURL\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EUAuthHub\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Ehttps://localhost:6112\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EStandalone WASM\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Ehttps://localhost:6130\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EInteractive WASM\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Ehttps://localhost:6132\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EConfigure the Hub\u0027s allowed origins for the actual frontend applications it serves.\u003C/p\u003E\n\u003Cp\u003EThese localhost addresses are illustrative and must match your deployment.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022blazor-standalone-webassembly\u0022\u003E5. Blazor Standalone WebAssembly\u003C/h2\u003E\n\u003Cp\u003E\u003Cstrong\u003EBest for:\u003C/strong\u003E A browser-hosted application with a separate UAuthHub.\u003C/p\u003E\n\u003Cp\u003EStandalone Blazor WASM runs in the browser. It does not host UltimateAuth Server or database persistence.\u003C/p\u003E\n\u003Cp\u003EAuthentication is handled by the remote UAuthHub.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022client-packages\u0022\u003EClient packages\u003C/h3\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\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configure-services\u0022\u003EConfigure services\u003C/h3\u003E\n\u003Cp\u003EIn the standalone WASM application\u0027s \u003Ccode\u003EProgram.cs\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\u003Eusing CodeBeam.UltimateAuth.Client.Blazor.Extensions;\nusing CodeBeam.UltimateAuth.Core.Domain;\n\nvar builder = WebAssemblyHostBuilder.CreateDefault(args);\n\nbuilder.RootComponents.Add\u0026lt;App\u0026gt;(\u0026quot;#app\u0026quot;);\nbuilder.RootComponents.Add\u0026lt;HeadOutlet\u0026gt;(\u0026quot;head::after\u0026quot;);\n\nbuilder.Services.AddScoped(sp =\u0026gt; new HttpClient\n{\n BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)\n});\n\nbuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Endpoints.BasePath = \u0026quot;https://localhost:6110/auth\u0026quot;;\n options.Pkce.ReturnUrl = \u0026quot;https://localhost:6130/home\u0026quot;;\n options.Login.AllowCredentialPost = true;\n options.Reauth.Behavior = ReauthBehavior.RaiseEvent;\n});\n\nawait builder.Build().RunAsync();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe current standalone WASM sample explicitly configures \u003Ccode\u003ELogin.AllowCredentialPost\u003C/code\u003E and the PKCE return URL.\u003C/p\u003E\n\u003Cp\u003EThese are client-side settings. The UAuthHub must be configured independently.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022what-the-urls-mean\u0022\u003EWhat the URLs mean\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EEndpoints.BasePath\u003C/code\u003E points to the remote authentication endpoints.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EPkce.ReturnUrl\u003C/code\u003E identifies the frontend destination used by the relevant PKCE flow.\u003C/p\u003E\n\u003Cp\u003EThe Hub must recognize the frontend\u0027s origin, and any return URL used in an authentication flow must be validated against the application\u0027s security policy.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022root-component\u0022\u003ERoot component\u003C/h3\u003E\n\u003Cp\u003EA standalone WASM application should initialize UltimateAuth through \u003Ccode\u003EUAuthApp\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EFor example, its root application or routing component can contain:\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;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\u003EInclude the UltimateAuth JavaScript asset in the application\u0027s HTML host page.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022authentication-modes\u0022\u003EAuthentication modes\u003C/h3\u003E\n\u003Cp\u003EThe default \u003Ccode\u003EBlazorWasm\u003C/code\u003E profile resolves to \u003Ccode\u003EHybrid\u003C/code\u003E in the current mode resolver.\u003C/p\u003E\n\u003Cp\u003EDo not manually force \u003Ccode\u003EPureOpaque\u003C/code\u003E for a standalone browser client simply because a Blazor Server application uses it.\u003C/p\u003E\n\u003Cp\u003EThe transport and authentication lifecycle must match the client environment.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022blazor-web-app-interactive-webassembly\u0022\u003E6. Blazor Web App \u2014 Interactive WebAssembly\u003C/h2\u003E\n\u003Cp\u003E\u003Cstrong\u003EBest for:\u003C/strong\u003E A Blazor Web App with an ASP.NET Core host and a separate \u003Ccode\u003E.Client\u003C/code\u003E project.\u003C/p\u003E\n\u003Cp\u003EThis scenario differs from standalone WASM because components may participate in both server-side rendering and browser-side interactivity.\u003C/p\u003E\n\u003Cp\u003EThe current UltimateAuth sample registers the Blazor client integration in \u003Cstrong\u003Eboth projects\u003C/strong\u003E.\u003C/p\u003E\n\u003Cp\u003EIt also adds a separate ASP.NET Core compatibility registration in the host.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022project-structure\u0022\u003EProject structure\u003C/h3\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\u003EMyApplication/\n\u251C\u2500\u2500 MyApplication/\n\u2502 \u251C\u2500\u2500 Program.cs\n\u2502 \u2514\u2500\u2500 Components/\n\u2502 \u2514\u2500\u2500 App.razor\n\u2502\n\u2514\u2500\u2500 MyApplication.Client/\n \u251C\u2500\u2500 Program.cs\n \u2514\u2500\u2500 Pages/\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe host serves the application.\u003C/p\u003E\n\u003Cp\u003EThe \u003Ccode\u003E.Client\u003C/code\u003E project contains browser-executed components and services.\u003C/p\u003E\n\u003Cp\u003EA remote UAuthHub manages authentication.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022host-project-packages\u0022\u003E6.1 Host project packages\u003C/h3\u003E\n\u003Cp\u003EInstall the Blazor client integration and ASP.NET Core compatibility package in the host:\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.Client.AspNetCore\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022host-project-services\u0022\u003E6.2 Host project services\u003C/h3\u003E\n\u003Cp\u003EIn the host \u003Ccode\u003EProgram.cs\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\u003Eusing CodeBeam.UltimateAuth.Client.AspNetCore;\nusing CodeBeam.UltimateAuth.Client.Blazor.Extensions;\nusing CodeBeam.UltimateAuth.Core.Domain;\n\nbuilder.Services\n .AddRazorComponents()\n .AddInteractiveWebAssemblyComponents();\n\nbuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Endpoints.BasePath = \u0026quot;https://localhost:6112/auth\u0026quot;;\n options.Pkce.ReturnUrl = \u0026quot;https://localhost:6132/home\u0026quot;;\n options.Login.AllowCredentialPost = true;\n options.Reauth.Behavior = ReauthBehavior.RaiseEvent;\n});\n\nbuilder.Services.AddUltimateAuthAspNetCoreCompatibility();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EAddUltimateAuthAspNetCoreCompatibility()\u003C/code\u003E registers the compatibility authentication scheme and ASP.NET Core authorization services used by this hosting pattern.\u003C/p\u003E\n\u003Cp\u003EIt does \u003Cstrong\u003Enot\u003C/strong\u003E turn the host into an UltimateAuth authentication server.\u003C/p\u003E\n\u003Cp\u003EIt is also not equivalent to \u003Ccode\u003EAddUltimateAuthResourceApi()\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022host-project-pipeline\u0022\u003E6.3 Host project pipeline\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\u003Eapp.UseHttpsRedirection();\n\napp.UseAntiforgery();\n\napp.MapStaticAssets();\n\napp.MapRazorComponents\u0026lt;App\u0026gt;()\n .AddInteractiveWebAssemblyRenderMode()\n .AddAdditionalAssemblies(typeof(MyApplication.Client._Imports).Assembly, UAuthAssemblies.BlazorClient().First());\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EReplace \u003Ccode\u003EMyApplication.Client._Imports\u003C/code\u003E with the corresponding type from your client project.\u003C/p\u003E\n\u003Cp\u003EThe additional assemblies allow the host to discover routable components from the WASM client project and UltimateAuth\u0027s Blazor client assembly.\u003C/p\u003E\n\u003Cp\u003EThe sample does not register \u003Ccode\u003EAddUltimateAuthServer()\u003C/code\u003E in this host because authentication is delegated to a separate UAuthHub.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022client-project-packages\u0022\u003E6.4 Client project packages\u003C/h3\u003E\n\u003Cp\u003EInstall the Blazor client adapter in the \u003Ccode\u003E.Client\u003C/code\u003E 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 add package CodeBeam.UltimateAuth.Client.Blazor\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022client-project-services\u0022\u003E6.5 Client project services\u003C/h3\u003E\n\u003Cp\u003EIn \u003Ccode\u003EMyApplication.Client/Program.cs\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\u003Eusing CodeBeam.UltimateAuth.Client.Blazor.Extensions;\nusing CodeBeam.UltimateAuth.Core.Domain;\n\nvar builder = WebAssemblyHostBuilder.CreateDefault(args);\n\nbuilder.Services.AddScoped(sp =\u0026gt; new HttpClient\n{\n BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)\n});\n\nbuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Endpoints.BasePath = \u0026quot;https://localhost:6112/auth\u0026quot;;\n options.Pkce.ReturnUrl = \u0026quot;https://localhost:6132/home\u0026quot;;\n options.Login.AllowCredentialPost = true;\n options.Reauth.Behavior = ReauthBehavior.RaiseEvent;\n});\n\nawait builder.Build().RunAsync();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe host and client configurations should agree on the remote UAuthHub URL and authentication flow settings.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022why-both-registrations-are-necessary\u0022\u003EWhy both registrations are necessary\u003C/h3\u003E\n\u003Cp\u003EThe host and browser client have separate dependency injection containers.\u003C/p\u003E\n\u003Cp\u003ERegistering \u003Ccode\u003EAddUltimateAuthClientBlazor()\u003C/code\u003E in the host does not automatically register those services inside the WebAssembly runtime.\u003C/p\u003E\n\u003Cp\u003ELikewise, registering the client adapter only in the \u003Ccode\u003E.Client\u003C/code\u003E project does not supply the services required during server-side integration.\u003C/p\u003E\n\u003Cp\u003EThe additional ASP.NET Core compatibility registration addresses the host\u0027s ASP.NET Core authentication and authorization integration requirements.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EImportant:\u003C/strong\u003E This compatibility layer is not a replacement for server-side authorization of your protected business APIs.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022resource-api-separate-protected-backend\u0022\u003E7. Resource API \u2014 Separate Protected Backend\u003C/h2\u003E\n\u003Cp\u003E\u003Cstrong\u003EBest for:\u003C/strong\u003E An API that needs UltimateAuth authentication validation but does not host the authentication system itself.\u003C/p\u003E\n\u003Cp\u003EA Resource API can be an ASP.NET Core Web API, MVC application, or another supported server-side application.\u003C/p\u003E\n\u003Cp\u003EIt does not need to be a Blazor application.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022architecture\u0022\u003EArchitecture\u003C/h3\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 Blazor Client\n \u2502\n \u250C\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2510\n \u25BC \u25BC\n UAuthHub Resource API\n \u25B2 \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n Authentication\n validation\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe Resource API uses the configured UAuthHub for remote authentication and session validation behavior.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022install-the-server-integration-package\u0022\u003EInstall the server integration package\u003C/h3\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.Server\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configure-services-1\u0022\u003EConfigure services\u003C/h3\u003E\n\u003Cp\u003EIn the Resource API\u0027s \u003Ccode\u003EProgram.cs\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\u003Eusing CodeBeam.UltimateAuth.Server.Extensions;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nbuilder.Services.AddControllers();\n\nbuilder.Services.AddUltimateAuthResourceApi(options =\u0026gt;\n{\n options.UAuthHubBaseUrl = \u0026quot;https://localhost:6112\u0026quot;;\n options.AllowedClientOrigins.Add(\u0026quot;https://localhost:6132\u0026quot;);\n options.CorsPolicyName = \u0026quot;resourceApi\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthHubBaseUrl\u003C/code\u003E is the base address of the authentication server.\u003C/p\u003E\n\u003Cp\u003ENotice that this example uses the Hub origin without the \u003Ccode\u003E/auth\u003C/code\u003E suffix, matching the current Resource API samples.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EAllowedClientOrigins\u003C/code\u003E controls the browser origins permitted by the Resource API\u0027s CORS configuration.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configure-the-pipeline\u0022\u003EConfigure the pipeline\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 app = builder.Build();\n\napp.UseHttpsRedirection();\n\napp.UseUltimateAuthResourceApiWithAspNetCore();\n\napp.MapControllers();\n\napp.Run();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EUseUltimateAuthResourceApiWithAspNetCore()\u003C/code\u003E installs the Resource API integration middleware together with ASP.NET Core authentication and authorization middleware.\u003C/p\u003E\n\u003Cp\u003EYou do not need to register a local UltimateAuth Server or its persistence bundle in the Resource API.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022protecting-endpoints\u0022\u003EProtecting endpoints\u003C/h3\u003E\n\u003Cp\u003EUse ASP.NET Core authorization attributes for endpoints that require authentication:\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 Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\n[ApiController]\n[Route(\u0026quot;api/products\u0026quot;)]\npublic class ProductsController : ControllerBase\n{\n [HttpGet]\n [Authorize]\n public IActionResult GetProducts()\n {\n return Ok(new[]\n {\n new { Id = 1, Name = \u0026quot;Example product\u0026quot; }\n });\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe authorization attribute is enforced by ASP.NET Core on the server.\u003C/p\u003E\n\u003Cp\u003EFor permission-specific rules, configure and apply the corresponding authorization policies.\u003C/p\u003E\n\u003Cp\u003EDo not rely on Blazor UI authorization checks to protect Resource API endpoints.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022cors-configuration\u0022\u003ECORS configuration\u003C/h3\u003E\n\u003Cp\u003EThe current Resource API integration registers CORS from the configured allowed origins.\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\u003Eoptions.AllowedClientOrigins.Add(\u0026quot;https://localhost:6132\u0026quot;);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EOnly add origins that should be allowed to call your API from a browser.\u003C/p\u003E\n\u003Cp\u003ECORS is a browser access control mechanism, not an authentication or authorization mechanism.\u003C/p\u003E\n\u003Cp\u003EA request must still pass the applicable authentication and authorization checks.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022resource-api-vs-uauthhub\u0022\u003EResource API vs UAuthHub\u003C/h3\u003E\n\u003Cp\u003EThese are different responsibilities.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EUAuthHub\u003C/th\u003E\n\u003Cth\u003EResource API\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EHosts authentication flows\u003C/td\u003E\n\u003Ctd\u003EHosts application resources\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EManages authentication sessions\u003C/td\u003E\n\u003Ctd\u003EValidates remote authentication context\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EUses authentication persistence\u003C/td\u003E\n\u003Ctd\u003EDoes not require local authentication stores\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EProvides login and related endpoints\u003C/td\u003E\n\u003Ctd\u003EProvides application endpoints\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EServes authentication clients\u003C/td\u003E\n\u003Ctd\u003EServes authorized API consumers\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EUse a Resource API when you want independent deployment of business endpoints without duplicating the authentication server.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022resource-api-with-an-ef-core-backed-uauthhub\u0022\u003E8. Resource API with an EF Core-Backed UAuthHub\u003C/h2\u003E\n\u003Cp\u003EThe Resource API does not need to use EF Core just because UAuthHub does.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EInteractive WASM\n \u2502\n \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25BA Resource API\n \u2502\n \u25BC\n UAuthHub\n \u2502\n \u25BC\n EF Core Database\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe Hub stores users, credentials, sessions, and related security state in its database.\u003C/p\u003E\n\u003Cp\u003EThe Resource API communicates with the Hub through the configured remote validation integration.\u003C/p\u003E\n\u003Cp\u003EIts registration remains:\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.AddUltimateAuthResourceApi(options =\u0026gt;\n{\n options.UAuthHubBaseUrl = \u0026quot;https://localhost:6112\u0026quot;;\n options.AllowedClientOrigins.Add(\u0026quot;https://localhost:6132\u0026quot;);\n options.CorsPolicyName = \u0026quot;resourceApi\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis separation allows the API and authentication infrastructure to be deployed, scaled, and maintained independently.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022calling-a-resource-api-from-blazor\u0022\u003E9. Calling a Resource API from Blazor\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth provides a unified Resource Client for accessing protected application APIs.\u003C/p\u003E\n\u003Cp\u003EInstead of configuring a separate HTTP client and implementing browser credential handling for ordinary resource requests, applications can use \u003Ccode\u003EIUAuthClient.Resource\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThis keeps authentication, identity operations, and application resource requests accessible through the same client SDK.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configure-the-resource-api\u0022\u003EConfigure the Resource API\u003C/h3\u003E\n\u003Cp\u003EFor a separate Resource API, configure its origin through the Blazor client registration:\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;\n\nbuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Endpoints.BasePath = \u0026quot;https://localhost:6112/auth\u0026quot;;\n\n options.Resource.BaseAddress = \u0026quot;https://localhost:6122\u0026quot;;\n options.Resource.Credentials = UAuthResourceCredentialsMode.Auto;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EEndpoints.BasePath\u003C/code\u003E identifies the authentication server.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EResource.BaseAddress\u003C/code\u003E identifies the application Resource API.\u003C/p\u003E\n\u003Cp\u003EThese addresses can point to different applications.\u003C/p\u003E\n\u003Cp\u003EIf the Resource API shares the application\u0027s origin, \u003Ccode\u003EResource.BaseAddress\u003C/code\u003E can remain \u003Ccode\u003Enull\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022call-protected-resources\u0022\u003ECall Protected Resources\u003C/h3\u003E\n\u003Cp\u003EInject \u003Ccode\u003EIUAuthClient\u003C/code\u003E into a Blazor component:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@using CodeBeam.UltimateAuth.Client\n@inject IUAuthClient UAuthClient\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThen retrieve application data:\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.Resource.GetAsync\u0026lt;List\u0026lt;Product\u0026gt;\u0026gt;(\u0026quot;/api/products\u0026quot;);\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is not null)\n{\n products = result.Value;\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe Resource Client also supports POST, PUT, PATCH, and DELETE operations.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022application-specific-api-services\u0022\u003EApplication-Specific API Services\u003C/h3\u003E\n\u003Cp\u003EApplications can organize resource operations into small domain-specific services.\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\u003Eusing CodeBeam.UltimateAuth.Client;\nusing CodeBeam.UltimateAuth.Core.Contracts;\n\npublic sealed class ProductApiService(IUAuthClient client)\n{\n public Task\u0026lt;UAuthResult\u0026lt;List\u0026lt;Product\u0026gt;\u0026gt;\u0026gt; GetAllAsync()\n =\u0026gt; client.Resource.GetAsync\u0026lt;List\u0026lt;Product\u0026gt;\u0026gt;(\u0026quot;/api/products\u0026quot;);\n\n public Task\u0026lt;UAuthResult\u0026lt;Product\u0026gt;\u0026gt; CreateAsync(Product product)\n =\u0026gt; client.Resource.PostAsync\u0026lt;Product\u0026gt;(\u0026quot;/api/products\u0026quot;,product);\n\n public Task\u0026lt;UAuthResult\u0026gt; DeleteAsync(int id)\n =\u0026gt; client.Resource.DeleteAsync($\u0026quot;/api/products/{id}\u0026quot;);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ERegister the application service:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddScoped\u0026lt;ProductApiService\u0026gt;();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe service uses UltimateAuth\u0027s transport and result contracts rather than implementing another HTTP abstraction.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022interactive-webassembly\u0022\u003EInteractive WebAssembly\u003C/h3\u003E\n\u003Cp\u003EIn an Interactive WebAssembly application, configure the UltimateAuth Blazor client in each project where the Resource Client will be used.\u003C/p\u003E\n\u003Cp\u003EThe host and \u003Ccode\u003E.Client\u003C/code\u003E project have separate dependency injection containers.\u003C/p\u003E\n\u003Cp\u003EA shared application API service may be registered in both projects when components execute in both environments.\u003C/p\u003E\n\u003Cp\u003EThe Resource Client uses browser-based transport, including when invoked from an interactive Blazor Server component.\u003C/p\u003E\n\u003Cp\u003EBrowser interop must be available before making resource requests. Do not initiate browser-dependent resource requests during static prerendering.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022standalone-webassembly\u0022\u003EStandalone WebAssembly\u003C/h3\u003E\n\u003Cp\u003EStandalone Blazor WebAssembly applications use the same Resource Client API.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor(options =\u0026gt;\n{\n options.Endpoints.BasePath = \u0026quot;https://localhost:6112/auth\u0026quot;;\n options.Resource.BaseAddress = \u0026quot;https://localhost:6122\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EOnce configured, application components can call protected resources through \u003Ccode\u003EIUAuthClient.Resource\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EA separate application-specific \u003Ccode\u003EHttpClient\u003C/code\u003E is not required for these requests.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022browser-credentials-and-cors\u0022\u003EBrowser Credentials and CORS\u003C/h3\u003E\n\u003Cp\u003EFor cross-origin requests, the browser and receiving API must agree on credential handling.\u003C/p\u003E\n\u003Cp\u003EThe Resource Client supports \u003Ccode\u003EAuto\u003C/code\u003E, \u003Ccode\u003ESameOrigin\u003C/code\u003E, \u003Ccode\u003EInclude\u003C/code\u003E, and \u003Ccode\u003EOmit\u003C/code\u003E credential policies.\u003C/p\u003E\n\u003Cp\u003EFor cross-origin cookie-based authentication, configure the appropriate credential policy and ensure that the Resource API supports the required CORS and cookie settings.\u003C/p\u003E\n\u003Cp\u003EThe Resource Client does not automatically make an API authenticated merely because the user has signed in through UAuthHub.\u003C/p\u003E\n\u003Cp\u003EThe receiving API must validate the authentication material associated with the configured authentication mode and enforce its authorization policies.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022blazor-server-and-direct-service-calls\u0022\u003EBlazor Server and Direct Service Calls\u003C/h3\u003E\n\u003Cp\u003EBlazor Server applications can often access application services directly when those services are hosted in the same process.\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\u003Evar products = await productService.GetAllAsync();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThere is no need to introduce an HTTP request solely to use the Resource Client.\u003C/p\u003E\n\u003Cp\u003EUse \u003Ccode\u003EIUAuthClient.Resource\u003C/code\u003E when the application actually needs to communicate with a protected Resource API through the browser.\u003C/p\u003E\n\u003Cp\u003EFor server-to-server requests, use an appropriate server-side HTTP client and authentication mechanism.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022learn-more\u0022\u003ELearn More\u003C/h3\u003E\n\u003Cp\u003ESee \u003Ca href=\u0022/docs/client-sdk/resource-api-requests\u0022\u003EResource API Requests\u003C/a\u003E for supported operations, result handling, credential configuration, security considerations, and request cancellation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022authentication-modes-and-client-profiles\u0022\u003E10. Authentication Modes and Client Profiles\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth separates \u003Cstrong\u003Eclient profile detection\u003C/strong\u003E from \u003Cstrong\u003Eauthentication mode enforcement\u003C/strong\u003E.\u003C/p\u003E\n\u003Cp\u003EThe default resolver maps client profiles to authentication modes.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EClient profile\u003C/th\u003E\n\u003Cth\u003EDefault resolved mode\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EBlazor Server\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EPureOpaque\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EBlazor WASM\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EHybrid\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EWeb Server\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EHybrid\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EMAUI\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EHybrid\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAPI\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EPureJwt\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThis mapping describes the resolver\u0027s behavior, not a guarantee that every mode is currently fully supported by the server.\u003C/p\u003E\n\u003Cp\u003EIn particular, \u003Ccode\u003EPureJwt\u003C/code\u003E and \u003Ccode\u003ESemiHybrid\u003C/code\u003E have implementation limitations in the current release.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022restrict-allowed-modes\u0022\u003ERestrict allowed modes\u003C/h3\u003E\n\u003Cp\u003EOn the authentication server:\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.AllowedModes =\n [\n UAuthMode.PureOpaque,\n UAuthMode.Hybrid\n ];\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EAllowedModes\u003C/code\u003E is a guardrail.\u003C/p\u003E\n\u003Cp\u003EIt does not select a mode for every request.\u003C/p\u003E\n\u003Cp\u003EThe effective mode is resolved from the client profile and then checked against the server\u0027s allowed-mode policy.\u003C/p\u003E\n\u003Cp\u003EFor most applications, use the default mode resolution unless you have a specific integration requirement.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022server-options-for-real-applications\u0022\u003E11. Server Options for Real Applications\u003C/h2\u003E\n\u003Cp\u003EProduction configuration should reflect your application\u0027s security requirements.\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\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Login.MaxFailedAttempts = 5;\n options.Login.LockoutDuration = TimeSpan.FromMinutes(15);\n options.Identifiers.Behavior.AllowMultipleUsernames = true;\n options.UserProfile.EnableMultiProfile = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EServer options also compose the Core-defined login, session, token, PKCE, and multi-tenant options.\u003C/p\u003E\n\u003Cp\u003EConfigure those through \u003Ccode\u003EAddUltimateAuthServer()\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EDo not independently configure Core options and assume the server will automatically use them.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configuration-files\u0022\u003EConfiguration files\u003C/h3\u003E\n\u003Cp\u003EUltimateAuth also supports configuration through \u003Ccode\u003Eappsettings.json\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\u003EJSON\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-json\u0022\u003E{\n \u0026quot;UltimateAuth\u0026quot;: {\n \u0026quot;Server\u0026quot;: {\n \u0026quot;Login\u0026quot;: {\n \u0026quot;MaxFailedAttempts\u0026quot;: 5,\n \u0026quot;LockoutDuration\u0026quot;: \u0026quot;00:15:00\u0026quot;\n }\n }\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe server options registration binds the \u003Ccode\u003EUltimateAuth:Server\u003C/code\u003E configuration section.\u003C/p\u003E\n\u003Cp\u003ERemember that effective mode defaults and mode-specific overrides are applied after the base options have been configured.\u003C/p\u003E\n\u003Cp\u003EFor detailed configuration behavior, see Server Options and Advanced Configuration.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022development-and-production-differences\u0022\u003E12. Development and Production Differences\u003C/h2\u003E\n\u003Cp\u003EThe samples intentionally prioritize visibility and repeatability.\u003C/p\u003E\n\u003Cp\u003EA production application needs additional safeguards.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EArea\u003C/th\u003E\n\u003Cth\u003ESample or development\u003C/th\u003E\n\u003Cth\u003EProduction\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EPersistence\u003C/td\u003E\n\u003Ctd\u003EInMemory or local SQLite\u003C/td\u003E\n\u003Ctd\u003EDurable database\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ESeed accounts\u003C/td\u003E\n\u003Ctd\u003EExample users and passwords\u003C/td\u003E\n\u003Ctd\u003EReal provisioning\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EDatabase initialization\u003C/td\u003E\n\u003Ctd\u003EMay reset sample data\u003C/td\u003E\n\u003Ctd\u003EReviewed migrations\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EHTTPS\u003C/td\u003E\n\u003Ctd\u003ELocal development certificate\u003C/td\u003E\n\u003Ctd\u003EProper TLS termination\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ECORS\u003C/td\u003E\n\u003Ctd\u003ELocalhost origins\u003C/td\u003E\n\u003Ctd\u003EExact trusted origins\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ESigning keys\u003C/td\u003E\n\u003Ctd\u003EDevelopment defaults\u003C/td\u003E\n\u003Ctd\u003EManaged, persistent keys\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EDiagnostics\u003C/td\u003E\n\u003Ctd\u003EDetailed debugging\u003C/td\u003E\n\u003Ctd\u003ERestrict sensitive diagnostics\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ECredential recovery\u003C/td\u003E\n\u003Ctd\u003EDevelopment notifier\u003C/td\u003E\n\u003Ctd\u003EReal secure notification channel\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EIdentifier verification\u003C/td\u003E\n\u003Ctd\u003EDevelopment bypass\u003C/td\u003E\n\u003Ctd\u003EProof-based verification\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ESecrets\u003C/td\u003E\n\u003Ctd\u003ELocal settings\u003C/td\u003E\n\u003Ctd\u003ESecure configuration provider\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe sample projects include development-only implementations for credential reset notifications and identifier verification.\u003C/p\u003E\n\u003Cp\u003EThose implementations demonstrate extension points and should not be treated as production verification systems.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022credential-recovery\u0022\u003ECredential recovery\u003C/h3\u003E\n\u003Cp\u003EIf your application exposes credential recovery, configure a real implementation of the credential reset notifier.\u003C/p\u003E\n\u003Cp\u003ERecovery secrets must be delivered through an appropriate channel and must not be exposed directly through anonymous API responses.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022identifier-verification\u0022\u003EIdentifier verification\u003C/h3\u003E\n\u003Cp\u003EIf your application supports verified email addresses, phone numbers, or other identifiers, use an implementation that verifies ownership through suitable proof.\u003C/p\u003E\n\u003Cp\u003EDo not use a development bypass verifier in production.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022signing-keys\u0022\u003ESigning keys\u003C/h3\u003E\n\u003Cp\u003EConfigure persistent signing keys for authentication modes that issue JWTs.\u003C/p\u003E\n\u003Cp\u003EDevelopment signing key providers are not a substitute for production key management.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022deployment-checklist\u0022\u003E13. Deployment Checklist\u003C/h2\u003E\n\u003Cp\u003EBefore deploying, verify the following:\u003C/p\u003E\n\u003Cul class=\u0022contains-task-list\u0022\u003E\n\u003Cli class=\u0022task-list-item\u0022\u003E\u003Cinput disabled=\u0022disabled\u0022 type=\u0022checkbox\u0022\u003E The selected architecture matches the actual client hosting model.\u003C/li\u003E\n\u003Cli class=\u0022task-list-item\u0022\u003E\u003Cinput disabled=\u0022disabled\u0022 type=\u0022checkbox\u0022\u003E The authentication server uses appropriate persistent storage.\u003C/li\u003E\n\u003Cli class=\u0022task-list-item\u0022\u003E\u003Cinput disabled=\u0022disabled\u0022 type=\u0022checkbox\u0022\u003E Database migrations have been reviewed and applied.\u003C/li\u003E\n\u003Cli class=\u0022task-list-item\u0022\u003E\u003Cinput disabled=\u0022disabled\u0022 type=\u0022checkbox\u0022\u003E Sample users and development-only verification services are disabled.\u003C/li\u003E\n\u003Cli class=\u0022task-list-item\u0022\u003E\u003Cinput disabled=\u0022disabled\u0022 type=\u0022checkbox\u0022\u003E Production signing keys and secrets are configured.\u003C/li\u003E\n\u003Cli class=\u0022task-list-item\u0022\u003E\u003Cinput disabled=\u0022disabled\u0022 type=\u0022checkbox\u0022\u003E HTTPS is enforced at the appropriate boundary.\u003C/li\u003E\n\u003Cli class=\u0022task-list-item\u0022\u003E\u003Cinput disabled=\u0022disabled\u0022 type=\u0022checkbox\u0022\u003E UAuthHub and Resource API origins are explicitly configured.\u003C/li\u003E\n\u003Cli class=\u0022task-list-item\u0022\u003E\u003Cinput disabled=\u0022disabled\u0022 type=\u0022checkbox\u0022\u003E Authentication endpoints and application endpoints are protected appropriately.\u003C/li\u003E\n\u003Cli class=\u0022task-list-item\u0022\u003E\u003Cinput disabled=\u0022disabled\u0022 type=\u0022checkbox\u0022\u003E Client and server endpoint URLs match the deployment.\u003C/li\u003E\n\u003Cli class=\u0022task-list-item\u0022\u003E\u003Cinput disabled=\u0022disabled\u0022 type=\u0022checkbox\u0022\u003E Session expiration, refresh, logout, and revocation have been tested.\u003C/li\u003E\n\u003Cli class=\u0022task-list-item\u0022\u003E\u003Cinput disabled=\u0022disabled\u0022 type=\u0022checkbox\u0022\u003E Protected resources enforce server-side authorization.\u003C/li\u003E\n\u003Cli class=\u0022task-list-item\u0022\u003E\u003Cinput disabled=\u0022disabled\u0022 type=\u0022checkbox\u0022\u003E Authentication state and persistence survive the expected application lifecycle.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022choosing-your-architecture\u0022\u003E14. Choosing Your Architecture\u003C/h2\u003E\n\u003Cp\u003E\u003Cstrong\u003EStart with one server and one client. Connect more clients whenever you need them.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EUltimateAuth is built around two fundamental responsibilities:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Cstrong\u003EUltimateAuth Server\u003C/strong\u003E \u2014 Manages authentication, users, credentials, sessions, authorization, and the security lifecycle.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EUltimateAuth Client\u003C/strong\u003E \u2014 Integrates an application with the authentication system using the appropriate client profile and authentication flows.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EEvery application needs access to an authentication server, but that server does not have to be a separate application. It can be hosted within your existing ASP.NET Core application or deployed independently as UAuthHub.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022single-application-the-simplest-setup\u0022\u003ESingle Application \u2014 The Simplest Setup\u003C/h3\u003E\n\u003Cp\u003EFor a traditional ASP.NET Core or Blazor Server application, UltimateAuth Server and Client can run together.\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\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Your Application \u2502\n\u2502 \u2502\n\u2502 UltimateAuth Client \u2502\n\u2502 \u2502 \u2502\n\u2502 UltimateAuth Server \u2502\n\u2502 \u2502 \u2502\n\u2502 Database \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis approach is ideal when you want authentication without introducing another service or deployment.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022multiple-applications-one-authentication-server\u0022\u003EMultiple Applications \u2014 One Authentication Server\u003C/h3\u003E\n\u003Cp\u003EAs your system grows, you can connect multiple client applications to the same UltimateAuth Server.\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 Blazor Server Blazor WASM\n \u2502 \u2502\n \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n \u2502 \u2502 \u2502\n Razor Pages MAUI Other .NET Clients\n \u2502 \u2502 \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u25BC\n UltimateAuth Server\n UAuthHub\n \u2502\n \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u25BC \u25BC \u25BC\n Users Sessions Authorization\n \u2502\n \u25BC\n Persistence\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EEach client can have its own application structure, hosting model, and authentication requirements while sharing the same authentication authority.\u003C/p\u003E\n\u003Cp\u003EFor example, a business platform might use Blazor Server for administration, Blazor WASM for its customer portal, MAUI for mobile access, and Razor Pages for an internal application.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EAdding another client does not require replacing the existing clients or creating another UltimateAuth Server.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EThe same users, credentials, security policies, and session infrastructure can serve multiple applications, subject to the authentication modes and client integrations supported by the installed version.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022distributed-applications-separate-resource-apis\u0022\u003EDistributed Applications \u2014 Separate Resource APIs\u003C/h3\u003E\n\u003Cp\u003EWhen your architecture includes independently deployed backend services, Resource APIs can integrate with the same authentication authority.\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 Multiple Client Applications\n \u2502\n \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u25BC \u25BC\n UAuthHub Resource APIs\n \u25B2 \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n Authentication\n Validation\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EResource APIs protect application resources without becoming additional authentication servers.\u003C/p\u003E\n\u003Cp\u003EThis lets you separate business services from authentication infrastructure while maintaining a shared security model.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022one-foundation-different-scales\u0022\u003EOne Foundation, Different Scales\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EYour scenario\u003C/th\u003E\n\u003Cth\u003ERecommended approach\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EA simple Blazor Server application\u003C/td\u003E\n\u003Ctd\u003EIntegrated Server \u002B Client\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EA traditional ASP.NET Core application\u003C/td\u003E\n\u003Ctd\u003EServer hosted within the application\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EA standalone Blazor WASM application\u003C/td\u003E\n\u003Ctd\u003EClient \u002B remote UAuthHub\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAn Interactive WASM application\u003C/td\u003E\n\u003Ctd\u003EHost \u002B WASM Client \u002B remote UAuthHub\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EMultiple web, desktop, or mobile applications\u003C/td\u003E\n\u003Ctd\u003EMultiple Clients \u002B shared UAuthHub\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EMultiple backend services\u003C/td\u003E\n\u003Ctd\u003EShared UAuthHub \u002B Resource APIs\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Cstrong\u003EUltimateAuth does not require a complex architecture to get started, and it does not restrict you to a single client as your application grows.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EStart with the simplest deployment that meets your needs. Add new client applications, persistent storage, or separate Resource APIs as your requirements evolve.\u003C/p\u003E\n\u003Cp\u003EThe authentication foundation remains consistent.\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/getting-started/quickstart\u0022\u003EQuickStart\u003C/a\u003E \u2014 Build the minimal in-memory application.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/getting-started/ecosystem\u0022\u003EEcosystem\u003C/a\u003E \u2014 Explore packages and supported integrations.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/configuration/server-options\u0022\u003EServer Options\u003C/a\u003E \u2014 Configure server authentication and security behavior.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/configuration/client-options\u0022\u003EClient Options\u003C/a\u003E \u2014 Configure client profiles, endpoints, refresh, and reauthentication.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/configuration/advanced-configuration\u0022\u003EAdvanced Configuration\u003C/a\u003E \u2014 Explore effective options, dependency injection, and extension points.\u003C/li\u003E\n\u003C/ul\u003E\n", "Headings": [ { "Id": "choose-your-architecture", @@ -219,18 +219,43 @@ "Level": 0 }, { - "Id": "interactive-wasm-host", - "Text": "Interactive WASM host", + "Id": "configure-the-resource-api", + "Text": "Configure the Resource API", "Level": 1 }, { - "Id": "interactive-wasm-client", - "Text": "Interactive WASM client", + "Id": "call-protected-resources", + "Text": "Call Protected Resources", "Level": 1 }, { - "Id": "standalone-wasm", - "Text": "Standalone WASM", + "Id": "application-specific-api-services", + "Text": "Application-Specific API Services", + "Level": 1 + }, + { + "Id": "interactive-webassembly", + "Text": "Interactive WebAssembly", + "Level": 1 + }, + { + "Id": "standalone-webassembly", + "Text": "Standalone WebAssembly", + "Level": 1 + }, + { + "Id": "browser-credentials-and-cors", + "Text": "Browser Credentials and CORS", + "Level": 1 + }, + { + "Id": "blazor-server-and-direct-service-calls", + "Text": "Blazor Server and Direct Service Calls", + "Level": 1 + }, + { + "Id": "learn-more", + "Text": "Learn More", "Level": 1 }, { @@ -309,6 +334,6 @@ "Level": 0 } ], - "SourceHash": "B62B0756A775A119E94E9873797C6AADF4B14F3BDC983D1788EC0FE33C63A08A", + "SourceHash": "770C2ACEAC642791FB3C618D54F7EB1108DF9651ABFBF28CD7D5700EB25EF255", "BuilderVersion": "1" } \ No newline at end of file diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/how-to-guides/define-authorization-policies.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/how-to-guides/define-authorization-policies.json index 04bab453..ddce4b09 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/how-to-guides/define-authorization-policies.json +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/how-to-guides/define-authorization-policies.json @@ -59,6 +59,6 @@ "Level": 0 } ], - "SourceHash": "A6232510BA9CDB76716D20766348E869ED6E509C5E292088D8B6D8BECE7171B9", + "SourceHash": "8BBBF846734CC54FDA1AB844283F5C607DED9109339C07DAFABFACA3B3E3B03B", "BuilderVersion": "1" } \ No newline at end of file diff --git a/samples/CodeBeam.UltimateAuth.Sample.Shared/Components/Dialogs/ResourceApiDialog.razor b/samples/CodeBeam.UltimateAuth.Sample.Shared/Components/Dialogs/ResourceApiDialog.razor index db13e910..6a22d9ea 100644 --- a/samples/CodeBeam.UltimateAuth.Sample.Shared/Components/Dialogs/ResourceApiDialog.razor +++ b/samples/CodeBeam.UltimateAuth.Sample.Shared/Components/Dialogs/ResourceApiDialog.razor @@ -225,9 +225,8 @@ - Resource operations are performed through - SampleProductApiService. Authorization is enforced - by the Resource API, not by this dialog. + Resource operations are performed through IUAuthClient.Resource. + Authentication is handled by UltimateAuth, while authorization is enforced by the Resource API. @@ -235,10 +234,7 @@
- + Close
diff --git a/samples/CodeBeam.UltimateAuth.Sample.Shared/ResourceApi/SampleProductApiService.cs b/samples/CodeBeam.UltimateAuth.Sample.Shared/ResourceApi/SampleProductApiService.cs index fb7c954f..937cf1e6 100644 --- a/samples/CodeBeam.UltimateAuth.Sample.Shared/ResourceApi/SampleProductApiService.cs +++ b/samples/CodeBeam.UltimateAuth.Sample.Shared/ResourceApi/SampleProductApiService.cs @@ -1,81 +1,22 @@ -using CodeBeam.UltimateAuth.Core.Contracts; -using Microsoft.AspNetCore.Components.WebAssembly.Http; -using System.Net.Http.Json; +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Core.Contracts; namespace CodeBeam.UltimateAuth.Sample.Shared.ResourceApi; -public class SampleProductApiService +public sealed class SampleProductApiService(IUAuthClient client) { - private readonly HttpClient _http; - - public SampleProductApiService(HttpClient factory) - { - _http = factory; - } - - private HttpRequestMessage CreateRequest(HttpMethod method, string url, object? body = null) - { - var request = new HttpRequestMessage(method, url); - if (OperatingSystem.IsBrowser()) - { - request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include); - } - - if (body is not null) - { - request.Content = JsonContent.Create(body); - } - - return request; - } - public Task>> GetAllAsync() - => SendAsync>(CreateRequest(HttpMethod.Get, "/api/products")); + => client.Resource.GetAsync>("/api/products"); public Task> GetAsync(int id) - => SendAsync(CreateRequest(HttpMethod.Get, $"/api/products/{id}")); + => client.Resource.GetAsync($"/api/products/{id}"); public Task> CreateAsync(SampleProduct product) - => SendAsync(CreateRequest(HttpMethod.Post, $"/api/products", product)); + => client.Resource.PostAsync("/api/products", product); public Task> UpdateAsync(int id, SampleProduct product) - => SendAsync(CreateRequest(HttpMethod.Put, $"/api/products/{id}", product)); - - public Task> DeleteAsync(int id) - => SendAsync(CreateRequest(HttpMethod.Delete, $"/api/products/{id}")); - - private async Task> SendAsync(HttpRequestMessage request) - { - var response = await _http.SendAsync(request); - - var result = new UAuthResult - { - Status = (int)response.StatusCode, - IsSuccess = response.IsSuccessStatusCode - }; - - if (response.IsSuccessStatusCode) - { - result.Value = await response.Content.ReadFromJsonAsync(); - return result; - } - - result.Problem = await TryReadProblem(response); - return result; - } + => client.Resource.PutAsync($"/api/products/{id}", product); - private async Task TryReadProblem(HttpResponseMessage response) - { - try - { - return await response.Content.ReadFromJsonAsync(); - } - catch - { - return new UAuthProblem - { - Title = response.ReasonPhrase - }; - } - } + public Task DeleteAsync(int id) + => client.Resource.DeleteAsync($"/api/products/{id}"); } diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db-shm b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db-shm index 5eaf9289..82b7d9dd 100644 Binary files a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db-shm and b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db-shm differ diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db-wal b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db-wal index 97609168..f799821e 100644 Binary files a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db-wal and b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db-wal differ diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Program.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Program.cs index 3648af62..9dc2ee6b 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Program.cs +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Program.cs @@ -76,7 +76,7 @@ } builder.Services.AddScopedUltimateAuthSampleSeed(); -//builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.Configure(options => { diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-shm b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-shm index 08d31fd8..cec5825e 100644 Binary files a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-shm and b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-shm differ diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-wal b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-wal index b13e8d5d..80ea6afe 100644 Binary files a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-wal and b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-wal differ diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs index 07ae777a..e58f9b28 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs @@ -70,10 +70,12 @@ { //o.AutoRefresh.Interval = TimeSpan.FromSeconds(5); o.Reauth.Behavior = ReauthBehavior.RaiseEvent; + o.Resource.BaseAddress = "https://localhost:6120"; //o.UAuthStateRefreshMode = UAuthStateRefreshMode.Validate; }); builder.Services.AddUltimateAuthSampleSeed(); +builder.Services.AddScoped(); //builder.Services.AddHttpClient(client => //{ diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Program.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Program.cs index 4fb69e88..5d9b36ed 100644 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Program.cs +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Program.cs @@ -24,6 +24,8 @@ builder.Services.AddUltimateAuthClientBlazor(o => { o.Endpoints.BasePath = "https://localhost:6110/auth"; // UAuthHub URL + o.Resource.BaseAddress = "https://localhost:6120"; // Resource API URL + o.Reauth.Behavior = ReauthBehavior.RaiseEvent; o.Login.AllowCredentialPost = true; o.Pkce.ReturnUrl = "https://localhost:6130/home"; // This application domain + path @@ -33,12 +35,12 @@ builder.Services.AddScoped(); -builder.Services.AddScoped(sp => -{ - return new HttpClient - { - BaseAddress = new Uri("https://localhost:6120") // Resource API URL - }; -}); +//builder.Services.AddScoped(sp => +//{ +// return new HttpClient +// { +// BaseAddress = new Uri("https://localhost:6120") // Resource API URL +// }; +//}); await builder.Build().RunAsync(); diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/wwwroot/index.html b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/wwwroot/index.html index ed5354ac..0061dda8 100644 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/wwwroot/index.html +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/wwwroot/index.html @@ -9,7 +9,7 @@ - + diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Program.cs b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Program.cs index 272cafb4..9452b151 100644 --- a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Program.cs +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Program.cs @@ -16,14 +16,12 @@ builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); -builder.Services.AddHttpClient(client => -{ - client.BaseAddress = new Uri("https://localhost:6122"); // EFCore Resource API URL -}); +builder.Services.AddScoped(); builder.Services.AddUltimateAuthClientBlazor(o => { o.Endpoints.BasePath = "https://localhost:6112/auth"; // UAuthHub EFCore URL + o.Resource.BaseAddress = "https://localhost:6122"; o.Reauth.Behavior = ReauthBehavior.RaiseEvent; o.Login.AllowCredentialPost = true; o.Pkce.ReturnUrl = "https://localhost:6132/home"; // This application domain + path diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Components/App.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Components/App.razor index 6eccdef2..e14b88a2 100644 --- a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Components/App.razor +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Components/App.razor @@ -9,7 +9,7 @@ - + diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Program.cs b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Program.cs index 10c609ad..bff64bcf 100644 --- a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Program.cs +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Program.cs @@ -28,6 +28,7 @@ builder.Services.AddUltimateAuthClientBlazor(o => { o.Endpoints.BasePath = "https://localhost:6112/auth"; // UAuthHub EFCore URL + o.Resource.BaseAddress = "https://localhost:6122"; o.Reauth.Behavior = ReauthBehavior.RaiseEvent; o.Login.AllowCredentialPost = true; o.Pkce.ReturnUrl = "https://localhost:6132/home"; // This application domain + path @@ -39,10 +40,7 @@ // It gives full AspNetCore compatibility but decreases performance significantly. //builder.Services.AddUltimateAuthResourceApi(o => o.UAuthHubBaseUrl = "https://localhost:6112/auth"); -builder.Services.AddHttpClient(client => -{ - client.BaseAddress = new Uri("https://localhost:6122"); // EFCore Resource API URL -}); +builder.Services.AddScoped(); var app = builder.Build(); diff --git a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi/Program.cs b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi/Program.cs index 0b595429..250c466b 100644 --- a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi/Program.cs +++ b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi/Program.cs @@ -9,7 +9,13 @@ { // TODO: Make multiple UAuthHub support via resolver, then different client apps can use different UAuthHub instances if needed. o.UAuthHubBaseUrl = "https://localhost:6110"; + // o.UAuthHubBaseUrl = "https://localhost:7213"; // Comment upper line and uncomment this if you want to try blazor server sample. + + // Standalone WASM o.AllowedClientOrigins.Add("https://localhost:6130"); + + // Blazor Server + o.AllowedClientOrigins.Add("https://localhost:7213"); }); var app = builder.Build(); diff --git a/src/client/CodeBeam.UltimateAuth.Client.Blazor/Extensions/ServiceCollectionExtensions.cs b/src/client/CodeBeam.UltimateAuth.Client.Blazor/Extensions/ServiceCollectionExtensions.cs index 2191e695..b2f782ae 100644 --- a/src/client/CodeBeam.UltimateAuth.Client.Blazor/Extensions/ServiceCollectionExtensions.cs +++ b/src/client/CodeBeam.UltimateAuth.Client.Blazor/Extensions/ServiceCollectionExtensions.cs @@ -40,6 +40,7 @@ public static IServiceCollection AddUltimateAuthClientBlazor(this IServiceCollec private static IServiceCollection AddUltimateAuthClientBlazorInternal(this IServiceCollection services) { services.TryAddScoped(); + services.TryAddScoped(); services.TryAddScoped(); diff --git a/src/client/CodeBeam.UltimateAuth.Client.Blazor/Infrastructure/UAuthLoginPageDiscovery.cs b/src/client/CodeBeam.UltimateAuth.Client.Blazor/Infrastructure/UAuthLoginPageDiscovery.cs index 84cc3af5..44991391 100644 --- a/src/client/CodeBeam.UltimateAuth.Client.Blazor/Infrastructure/UAuthLoginPageDiscovery.cs +++ b/src/client/CodeBeam.UltimateAuth.Client.Blazor/Infrastructure/UAuthLoginPageDiscovery.cs @@ -31,23 +31,31 @@ public static string Resolve() if (_cached is not null) return _cached; - var candidates = AppDomain.CurrentDomain - .GetAssemblies() + var resolved = ResolveFromAssemblies(AppDomain.CurrentDomain.GetAssemblies()); + + return _cached = resolved; + } + + internal static string ResolveFromAssemblies( + IEnumerable assemblies) + { + ArgumentNullException.ThrowIfNull(assemblies); + + var candidates = assemblies .SelectMany(GetLoadableTypes) .Where(HasLoginPageAttribute) .ToArray(); if (candidates.Length == 0) - return _cached = DefaultLoginRoute; + return DefaultLoginRoute; if (candidates.Length > 1) { - throw new InvalidOperationException( - "Multiple [UAuthLoginPage] components were found. " + + throw new InvalidOperationException("Multiple [UAuthLoginPage] components were found. " + "Make sure only one component is marked as the UltimateAuth login page."); } - return _cached = ResolveRoute(candidates[0]); + return ResolveRoute(candidates[0]); } internal static string ResolveRoute(Type componentType) diff --git a/src/client/CodeBeam.UltimateAuth.Client.Blazor/Infrastructure/UAuthRequestClient.cs b/src/client/CodeBeam.UltimateAuth.Client.Blazor/Infrastructure/UAuthRequestClient.cs index 510acac1..f35488ff 100644 --- a/src/client/CodeBeam.UltimateAuth.Client.Blazor/Infrastructure/UAuthRequestClient.cs +++ b/src/client/CodeBeam.UltimateAuth.Client.Blazor/Infrastructure/UAuthRequestClient.cs @@ -34,7 +34,8 @@ public async Task NavigateAsync(string endpoint, IDictionary? fo url = endpoint, mode = "navigate", data = form, - clientProfile = _options.ClientProfile.ToString() + clientProfile = _options.ClientProfile.ToString(), + trustedOrigin = ResolveTrustedOrigin() }); } @@ -49,7 +50,8 @@ public async Task SendFormAsync(string endpoint, IDictiona url = endpoint, mode = "fetch", data = form, - clientProfile = _options.ClientProfile.ToString() + clientProfile = _options.ClientProfile.ToString(), + trustedOrigin = ResolveTrustedOrigin() }); if (result == null) @@ -74,7 +76,8 @@ public async Task SendJsonAsync(string endpoint, object? p { url = endpoint, payload = payload, - clientProfile = _options.ClientProfile.ToString() + clientProfile = _options.ClientProfile.ToString(), + trustedOrigin = ResolveTrustedOrigin() }); if (result == null) @@ -100,7 +103,8 @@ public async Task TryAndCommitAsync(string tryEndpoint, tryUrl = tryEndpoint, commitUrl = commitEndpoint, data = request, - clientProfile = _options.ClientProfile.ToString() + clientProfile = _options.ClientProfile.ToString(), + trustedOrigin = ResolveTrustedOrigin() }); if (response is null) @@ -108,4 +112,15 @@ public async Task TryAndCommitAsync(string tryEndpoint, return response; } + + private string? ResolveTrustedOrigin() + { + var basePath = _options.Endpoints.BasePath; + + if (Uri.TryCreate(basePath, UriKind.Absolute, out var uri)) + return uri.GetLeftPart(UriPartial.Authority); + + // Relative BasePath: JS automatically uses the current origin. + return null; + } } diff --git a/src/client/CodeBeam.UltimateAuth.Client.Blazor/Infrastructure/UAuthResourceRequestClient.cs b/src/client/CodeBeam.UltimateAuth.Client.Blazor/Infrastructure/UAuthResourceRequestClient.cs new file mode 100644 index 00000000..0efa6e96 --- /dev/null +++ b/src/client/CodeBeam.UltimateAuth.Client.Blazor/Infrastructure/UAuthResourceRequestClient.cs @@ -0,0 +1,85 @@ +using CodeBeam.UltimateAuth.Client.Contracts; +using CodeBeam.UltimateAuth.Client.Errors; +using CodeBeam.UltimateAuth.Client.Infrastructure; +using CodeBeam.UltimateAuth.Client.Options; +using Microsoft.Extensions.Options; +using Microsoft.JSInterop; + +namespace CodeBeam.UltimateAuth.Client.Blazor.Infrastructure; + +internal sealed class UAuthResourceRequestClient : IUAuthResourceRequestClient +{ + private readonly IJSRuntime _js; + private readonly IUAuthClientBootstrapper _bootstrapper; + private readonly UAuthClientOptions _options; + + public UAuthResourceRequestClient(IJSRuntime js, IUAuthClientBootstrapper bootstrapper, IOptions options) + { + _js = js; + _bootstrapper = bootstrapper; + _options = options.Value; + } + + public async Task SendAsync(string method, string endpoint, object? payload = null, CancellationToken ct = default) + { + ct.ThrowIfCancellationRequested(); + + await _bootstrapper.EnsureStartedAsync(); + + ct.ThrowIfCancellationRequested(); + + var requestId = Guid.NewGuid().ToString("N"); + + using var registration = ct.Register(() => + { + _ = CancelRequestAsync(requestId); + }); + + try + { + var result = await _js.InvokeAsync("uauth.resourceRequest", ct, + new + { + url = endpoint, + baseAddress = _options.Resource.BaseAddress, + method, + payload, + clientProfile = _options.ClientProfile.ToString(), + credentials = _options.Resource.Credentials.ToString(), + requestId + }); + + if (result is null) + { + throw new UAuthProtocolException("Invalid Resource API transport response."); + } + + if (result.Status == 0) + { + throw new UAuthTransportException("Resource API network error."); + } + + return result; + } + catch (OperationCanceledException) when (ct.IsCancellationRequested) + { + throw; + } + } + + private async Task CancelRequestAsync(string requestId) + { + try + { + await _js.InvokeVoidAsync("uauth.cancelResourceRequest", requestId); + } + catch (JSDisconnectedException) + { + // Browser circuit is no longer available. + } + catch (JSException) + { + // Cancellation is best-effort. + } + } +} diff --git a/src/client/CodeBeam.UltimateAuth.Client.Blazor/TScripts/uauth.js b/src/client/CodeBeam.UltimateAuth.Client.Blazor/TScripts/uauth.js index 8cdc2053..9cae6ca0 100644 --- a/src/client/CodeBeam.UltimateAuth.Client.Blazor/TScripts/uauth.js +++ b/src/client/CodeBeam.UltimateAuth.Client.Blazor/TScripts/uauth.js @@ -55,7 +55,23 @@ window.uauth.submitForm = function (form) { }; window.uauth.tryAndCommit = async function (options) { - const { tryUrl, commitUrl, data, clientProfile } = options; + const { + tryUrl, + commitUrl, + data, + clientProfile, + trustedOrigin + } = options; + + const validatedTryUrl = window.uauth.validateAuthUrl( + tryUrl, + trustedOrigin + ); + + const validatedCommitUrl = window.uauth.validateAuthUrl( + commitUrl, + trustedOrigin + ); const deviceId = window.uauth.deviceId; @@ -64,9 +80,10 @@ window.uauth.tryAndCommit = async function (options) { } const tryResponse = await window.uauth.postJson({ - url: tryUrl, + url: validatedTryUrl, payload: data, - clientProfile + clientProfile, + trustedOrigin }); if (!tryResponse || !Number.isInteger(tryResponse.status)) { @@ -107,7 +124,7 @@ window.uauth.tryAndCommit = async function (options) { if (normalized.isSuccess) { const form = document.createElement("form"); form.method = "POST"; - form.action = commitUrl; + form.action = validatedCommitUrl; for (const key of Object.keys(data)) { const input = document.createElement("input"); @@ -149,13 +166,19 @@ window.uauth.post = async function (options) { url, mode, data, - clientProfile + clientProfile, + trustedOrigin } = options; + const validatedUrl = window.uauth.validateAuthUrl( + url, + trustedOrigin + ); + if (mode === "navigate") { const form = document.createElement("form"); form.method = "POST"; - form.action = url; + form.action = validatedUrl; const cp = document.createElement("input"); cp.type = "hidden"; @@ -184,35 +207,119 @@ window.uauth.post = async function (options) { return null; } - let body = null; + return window.uauth.request({ + url: validatedUrl, + method: "POST", + payload: data, + clientProfile, + contentType: "form", + credentials: "include", + allowCrossOriginCredentials: true + }); +}; + +window.uauth.postJson = function (options) { + const url = window.uauth.validateAuthUrl( + options.url, + options.trustedOrigin + ); + + return window.uauth.request({ + url, + method: "POST", + payload: options.payload, + clientProfile: options.clientProfile, + contentType: "json", + credentials: "include", + allowCrossOriginCredentials: true + }); +}; + +window.uauth.request = async function (options) { + const { + url, + method = "GET", + payload = null, + clientProfile = "", + contentType = "json", + credentials = "same-origin", + headers: customHeaders = {} + } = options; + + if (!url) { + throw new Error("Request URL is required."); + } + + const requestUrl = new URL(url, document.baseURI); + + if (!["http:", "https:"].includes(requestUrl.protocol)) { + throw new Error("Unsupported request URL protocol."); + } + + const requestMethod = method.toUpperCase(); + + if (!["GET", "POST", "PUT", "PATCH", "DELETE"].includes(requestMethod)) { + throw new Error(`Unsupported HTTP method: ${requestMethod}`); + } + + if (!["same-origin", "include", "omit"].includes(credentials)) { + throw new Error("Invalid credentials policy."); + } + + if (credentials === "include" && + requestUrl.origin !== window.location.origin && + !options.allowCrossOriginCredentials) { + throw new Error("Cross-origin credentials require explicit authorization."); + } + if (!window.uauth.deviceId) { throw new Error("UAuth deviceId is not initialized."); } + const headers = { + ...customHeaders, "X-UDID": window.uauth.deviceId, "X-UAuth-ClientProfile": clientProfile, "X-Requested-With": "UAuth" }; - if (data) { - body = new URLSearchParams(); - for (const key in data) { - body.append(key, data[key]); + let body = null; + + if (payload !== null && payload !== undefined) { + if (requestMethod === "GET") { + throw new Error("GET requests cannot contain a body."); } - headers["Content-Type"] = "application/x-www-form-urlencoded"; + if (contentType === "json") { + headers["Content-Type"] = "application/json"; + body = JSON.stringify(payload); + } + else if (contentType === "form") { + headers["Content-Type"] = "application/x-www-form-urlencoded"; + + body = new URLSearchParams(); + + for (const [key, value] of Object.entries(payload)) { + body.append(key, value ?? ""); + } + } + else { + throw new Error("Unsupported content type."); + } } let response; try { - response = await fetch(url, { - method: "POST", - credentials: "include", - headers: headers, - body: body + response = await fetch(requestUrl.href, { + method: requestMethod, + credentials, + redirect: "error", + headers, + body }); - } catch (error) { + } + catch (error) { if (error instanceof TypeError) { return { ok: false, @@ -226,10 +333,18 @@ window.uauth.post = async function (options) { } let responseBody = null; - try { - responseBody = await response.json(); - } catch { - responseBody = null; + + if (response.status !== 204 && response.status !== 205) { + const text = await response.text(); + + if (text) { + try { + responseBody = JSON.parse(text); + } + catch { + responseBody = text; + } + } } return { @@ -240,59 +355,215 @@ window.uauth.post = async function (options) { }; }; -window.uauth.postJson = async function (options) { +window.uauth.resourceRequest = async function (options) { const { url, - payload, - clientProfile + baseAddress, + method = "GET", + payload = null, + clientProfile = "", + credentials = "Auto", + requestId } = options; + if (!url || typeof url !== "string") { + throw new Error("Resource request URL is required."); + } + + const baseUrl = baseAddress + ? new URL(baseAddress) + : new URL(window.location.origin + "/"); + + if (!["http:", "https:"].includes(baseUrl.protocol)) { + throw new Error("Invalid Resource API protocol."); + } + + if (baseUrl.username || baseUrl.password || + baseUrl.search || baseUrl.hash) { + throw new Error("Invalid Resource API base address."); + } + + const requestUrl = new URL(url, baseUrl); + + if (requestUrl.origin !== baseUrl.origin) { + throw new Error("Resource request target differs from configured API origin."); + } + + if (requestUrl.username || requestUrl.password) { + throw new Error("Resource URL cannot contain credentials."); + } + + if (!["http:", "https:"].includes(requestUrl.protocol)) { + throw new Error("Unsupported Resource API protocol."); + } + + const requestMethod = method.toUpperCase(); + + if (!["GET", "POST", "PUT", "PATCH", "DELETE"].includes(requestMethod)) { + throw new Error("Unsupported Resource API method."); + } + + if ((requestMethod === "GET") && payload !== null) { + throw new Error("GET requests cannot contain a body."); + } + + let resolvedCredentials; + + switch (credentials.toLowerCase()) { + case "auto": + resolvedCredentials = + requestUrl.origin === window.location.origin + ? "same-origin" + : "include"; + break; + + case "sameorigin": + resolvedCredentials = "same-origin"; + break; + + case "include": + resolvedCredentials = "include"; + break; + + case "omit": + resolvedCredentials = "omit"; + break; + + default: + throw new Error("Invalid Resource credentials mode."); + } + + if (resolvedCredentials === "include" && + requestUrl.protocol !== "https:" && + !["localhost", "127.0.0.1", "[::1]"].includes(requestUrl.hostname)) { + throw new Error( + "Cross-origin credential requests require HTTPS."); + } + if (!window.uauth.deviceId) { throw new Error("UAuth deviceId is not initialized."); } const headers = { - "Content-Type": "application/json", "X-UDID": window.uauth.deviceId, - "X-UAuth-ClientProfile": clientProfile ?? "", + "X-UAuth-ClientProfile": clientProfile, "X-Requested-With": "UAuth" }; + let body = null; + + if (payload !== null && payload !== undefined) { + headers["Content-Type"] = "application/json"; + body = JSON.stringify(payload); + } + let response; + const controller = new AbortController(); + + if (requestId) { + if (window.uauth.resourceRequests.has(requestId)) { + throw new Error("Duplicate Resource request ID."); + } + + window.uauth.resourceRequests.set(requestId, controller); + } + try { - response = await fetch(url, { - method: "POST", - credentials: "include", - headers: headers, - body: payload ? JSON.stringify(payload) : null + const response = await fetch(requestUrl.href, { + method: requestMethod, + credentials: resolvedCredentials, + redirect: "error", + headers, + body, + signal: controller.signal }); - } catch (error) { + + let responseBody = null; + + if (response.status !== 204 && response.status !== 205) { + const text = await response.text(); + + if (text.length > 0) { + responseBody = text; + } + } + + return { + ok: response.ok, + status: response.status, + body: responseBody, + contentType: response.headers.get("Content-Type") + }; + } + catch (error) { + if (error?.name === "AbortError") { + throw error; + } + if (error instanceof TypeError) { return { ok: false, status: 0, - refreshOutcome: null, - body: null + body: null, + contentType: null }; } throw error; } + finally { + if (requestId) { + window.uauth.resourceRequests.delete(requestId); + } + } +}; - let responseBody = null; - try { - responseBody = await response.json(); - } catch { - responseBody = null; +window.uauth.resourceRequests = new Map(); + +window.uauth.cancelResourceRequest = function (requestId) { + const controller = window.uauth.resourceRequests.get(requestId); + + if (!controller) { + return false; } - return { - ok: response.ok, - status: response.status, - refreshOutcome: response.headers.get("X-UAuth-Refresh"), - body: responseBody - }; + controller.abort(); + return true; +}; + +window.uauth.validateAuthUrl = function (url, trustedOrigin = null) { + if (!url || typeof url !== "string") { + throw new Error("Authentication URL is required."); + } + + const origin = trustedOrigin + ? new URL(trustedOrigin, window.location.origin) + : new URL(window.location.origin); + + const target = new URL(url, document.baseURI); + + if (!["http:", "https:"].includes(origin.protocol)) { + throw new Error("Unsupported trusted origin protocol."); + } + + if (!["http:", "https:"].includes(target.protocol)) { + throw new Error("Unsupported authentication URL protocol."); + } + + if (origin.username || origin.password) { + throw new Error("Trusted origin cannot contain credentials."); + } + + if (target.username || target.password) { + throw new Error("Authentication URL cannot contain credentials."); + } + + if (target.origin !== origin.origin) { + throw new Error("Authentication request target differs from the trusted UAuthHub origin."); + } + + return target.href; }; window.uauth.setDeviceId = function (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 361da1fb..c0c82b59 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=function(n){if(!n)return;if(!window.uauth.deviceId)throw new Error("UAuth deviceId is not initialized.");let t=n.querySelector("input[name='__uauth_device']");t||(t=document.createElement("input"),t.type="hidden",t.name="__uauth_device",n.appendChild(t));t.value=window.uauth.deviceId;n.submit()};window.uauth.tryAndCommit=async function(n){const{tryUrl:h,commitUrl:c,data:f,clientProfile:e}=n,o=window.uauth.deviceId;if(!o)throw new Error("UAuth deviceId is not initialized.");const r=await window.uauth.postJson({url:h,payload:f,clientProfile:e});if(!r||!Number.isInteger(r.status))throw new Error("Invalid UAuth transport response.");const i=r.status,l=i>=200&&i<300,s=i>=400&&i<500;if(!l&&!s)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(s&&t.isSuccess)throw new Error("Successful authentication result returned with HTTP error.");const u={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(u.isSuccess){const n=document.createElement("form");n.method="POST";n.action=c;for(const t of Object.keys(f)){const i=document.createElement("input");i.type="hidden";i.name=t;i.value=f[t]??"";n.appendChild(i)}if(u.previewReceipt){const t=document.createElement("input");t.type="hidden";t.name="PreviewReceipt";t.value=u.previewReceipt;n.appendChild(t)}const t=document.createElement("input");t.type="hidden";t.name="__uauth_client_profile";t.value=e??"";n.appendChild(t);const i=document.createElement("input");i.type="hidden";i.name="__uauth_device";i.value=o;n.appendChild(i);document.body.appendChild(n);n.submit()}return u};window.uauth.post=async function(n){const{url:f,mode:h,data:t,clientProfile:e}=n;if(h==="navigate"){const n=document.createElement("form");n.method="POST";n.action=f;const i=document.createElement("input");i.type="hidden";i.name="__uauth_client_profile";i.value=e??"";n.appendChild(i);const r=document.createElement("input");if(r.type="hidden",r.name="__uauth_device",r.value=window.uauth.deviceId,n.appendChild(r),t)for(const i in t){const r=document.createElement("input");r.type="hidden";r.name=i;r.value=t[i];n.appendChild(r)}return document.body.appendChild(n),n.submit(),null}let r=null;if(!window.uauth.deviceId)throw new Error("UAuth deviceId is not initialized.");const o={"X-UDID":window.uauth.deviceId,"X-UAuth-ClientProfile":e,"X-Requested-With":"UAuth"};if(t){r=new URLSearchParams;for(const n in t)r.append(n,t[n]);o["Content-Type"]="application/x-www-form-urlencoded"}let i;try{i=await fetch(f,{method:"POST",credentials:"include",headers:o,body:r})}catch(s){if(s instanceof TypeError)return{ok:!1,status:0,refreshOutcome:null,body:null};throw s;}let u=null;try{u=await i.json()}catch{u=null}return{ok:i.ok,status:i.status,refreshOutcome:i.headers.get("X-UAuth-Refresh"),body:u}};window.uauth.postJson=async function(n){const{url:f,payload:r,clientProfile:e}=n;if(!window.uauth.deviceId)throw new Error("UAuth deviceId is not initialized.");const o={"Content-Type":"application/json","X-UDID":window.uauth.deviceId,"X-UAuth-ClientProfile":e??"","X-Requested-With":"UAuth"};let t;try{t=await fetch(f,{method:"POST",credentials:"include",headers:o,body:r?JSON.stringify(r):null})}catch(u){if(u instanceof TypeError)return{ok:!1,status:0,refreshOutcome:null,body:null};throw u;}let i=null;try{i=await t.json()}catch{i=null}return{ok:t.ok,status:t.status,refreshOutcome:t.headers.get("X-UAuth-Refresh"),body:i}};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=function(n){if(!n)return;if(!window.uauth.deviceId)throw new Error("UAuth deviceId is not initialized.");let t=n.querySelector("input[name='__uauth_device']");t||(t=document.createElement("input"),t.type="hidden",t.name="__uauth_device",n.appendChild(t));t.value=window.uauth.deviceId;n.submit()};window.uauth.tryAndCommit=async function(n){const{tryUrl:c,commitUrl:l,data:f,clientProfile:o,trustedOrigin:e}=n,a=window.uauth.validateAuthUrl(c,e),v=window.uauth.validateAuthUrl(l,e),s=window.uauth.deviceId;if(!s)throw new Error("UAuth deviceId is not initialized.");const r=await window.uauth.postJson({url:a,payload:f,clientProfile:o,trustedOrigin:e});if(!r||!Number.isInteger(r.status))throw new Error("Invalid UAuth transport response.");const i=r.status,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 u={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(u.isSuccess){const n=document.createElement("form");n.method="POST";n.action=v;for(const t of Object.keys(f)){const i=document.createElement("input");i.type="hidden";i.name=t;i.value=f[t]??"";n.appendChild(i)}if(u.previewReceipt){const t=document.createElement("input");t.type="hidden";t.name="PreviewReceipt";t.value=u.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);n.submit()}return u};window.uauth.post=async function(n){const{url:u,mode:f,data:t,clientProfile:i,trustedOrigin:e}=n,r=window.uauth.validateAuthUrl(u,e);if(f==="navigate"){const n=document.createElement("form");n.method="POST";n.action=r;const u=document.createElement("input");u.type="hidden";u.name="__uauth_client_profile";u.value=i??"";n.appendChild(u);const f=document.createElement("input");if(f.type="hidden",f.name="__uauth_device",f.value=window.uauth.deviceId,n.appendChild(f),t)for(const i in t){const r=document.createElement("input");r.type="hidden";r.name=i;r.value=t[i];n.appendChild(r)}return document.body.appendChild(n),n.submit(),null}return window.uauth.request({url:r,method:"POST",payload:t,clientProfile:i,contentType:"form",credentials:"include",allowCrossOriginCredentials:!0})};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})};window.uauth.request=async function(n){const{url:h,method:a="GET",payload:i=null,clientProfile:v="",contentType:c="json",credentials:f="same-origin",headers:y={}}=n;if(!h)throw new Error("Request URL is required.");const e=new URL(h,document.baseURI);if(!["http:","https:"].includes(e.protocol))throw new Error("Unsupported request URL protocol.");const r=a.toUpperCase();if(!["GET","POST","PUT","PATCH","DELETE"].includes(r))throw new Error(`Unsupported HTTP method: ${r}`);if(!["same-origin","include","omit"].includes(f))throw new Error("Invalid credentials policy.");if(f==="include"&&e.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 o={...y,"X-UDID":window.uauth.deviceId,"X-UAuth-ClientProfile":v,"X-Requested-With":"UAuth"};let u=null;if(i!==null&&i!==undefined){if(r==="GET")throw new Error("GET requests cannot contain a body.");if(c==="json")o["Content-Type"]="application/json",u=JSON.stringify(i);else if(c==="form"){o["Content-Type"]="application/x-www-form-urlencoded";u=new URLSearchParams;for(const[n,t]of Object.entries(i))u.append(n,t??"")}else throw new Error("Unsupported content type.");}let t;try{t=await fetch(e.href,{method:r,credentials:f,redirect:"error",headers:o,body:u})}catch(l){if(l instanceof TypeError)return{ok:!1,status:0,refreshOutcome:null,body:null};throw l;}let s=null;if(t.status!==204&&t.status!==205){const n=await t.text();if(n)try{s=JSON.parse(n)}catch{s=n}}return{ok:t.ok,status:t.status,refreshOutcome:t.headers.get("X-UAuth-Refresh"),body:s}};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{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.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/src/client/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateManager.cs b/src/client/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateManager.cs index 26399d5d..df208996 100644 --- a/src/client/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateManager.cs +++ b/src/client/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateManager.cs @@ -31,7 +31,7 @@ public Task EnsureAsync(bool force = false, CancellationToken ct = default) lock (_ensureLock) { - if (_ensureTask != null) + if (_ensureTask is { IsCompleted: false }) return _ensureTask; _ensureTask = EnsureInternalAsync(ct); @@ -45,13 +45,15 @@ private async Task EnsureInternalAsync(CancellationToken ct) { var result = await _client.Flows.ValidateAsync(); - if (!result.IsValid || result.Snapshot == null) + if (!result.IsValid) { - if (State.IsAuthenticated) - State.MarkStale(); - else - State.Clear(); + State.Clear(); + return; + } + if (result.Snapshot is null) + { + State.MarkStale(); return; } diff --git a/src/client/CodeBeam.UltimateAuth.Client/Contracts/UAuthResourceCredentialsMode.cs b/src/client/CodeBeam.UltimateAuth.Client/Contracts/UAuthResourceCredentialsMode.cs new file mode 100644 index 00000000..593e4627 --- /dev/null +++ b/src/client/CodeBeam.UltimateAuth.Client/Contracts/UAuthResourceCredentialsMode.cs @@ -0,0 +1,27 @@ +namespace CodeBeam.UltimateAuth.Client; + +/// +/// Controls browser credential transmission for resource requests. +/// +public enum UAuthResourceCredentialsMode +{ + /// + /// Automatically selects the appropriate browser credentials policy. + /// + Auto = 0, + + /// + /// Send browser credentials only for same-origin requests. + /// + SameOrigin = 10, + + /// + /// Include browser credentials for the configured resource origin. + /// + Include = 20, + + /// + /// Do not send browser credentials. + /// + Omit = 30 +} diff --git a/src/client/CodeBeam.UltimateAuth.Client/Contracts/UAuthResourceTransportResult.cs b/src/client/CodeBeam.UltimateAuth.Client/Contracts/UAuthResourceTransportResult.cs new file mode 100644 index 00000000..73defa21 --- /dev/null +++ b/src/client/CodeBeam.UltimateAuth.Client/Contracts/UAuthResourceTransportResult.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace CodeBeam.UltimateAuth.Client.Contracts; + +/// +/// Represents an HTTP response from an application resource. +/// +public sealed class UAuthResourceTransportResult +{ + [JsonPropertyName("ok")] + public bool Ok { get; init; } + + [JsonPropertyName("status")] + public int Status { get; init; } + + [JsonPropertyName("body")] + public string? Body { get; init; } + + [JsonPropertyName("contentType")] + public string? ContentType { get; init; } +} diff --git a/src/client/CodeBeam.UltimateAuth.Client/Extensions/ServiceCollectionExtensions.cs b/src/client/CodeBeam.UltimateAuth.Client/Extensions/ServiceCollectionExtensions.cs index 42431a3b..e4df3e3d 100644 --- a/src/client/CodeBeam.UltimateAuth.Client/Extensions/ServiceCollectionExtensions.cs +++ b/src/client/CodeBeam.UltimateAuth.Client/Extensions/ServiceCollectionExtensions.cs @@ -91,6 +91,7 @@ private static IServiceCollection AddUltimateAuthClientInternal(this IServiceCol services.AddSingleton, UAuthClientOptionsValidator>(); services.AddSingleton, UAuthClientEndpointOptionsValidator>(); + services.AddSingleton, UAuthClientResourceOptionsValidator>(); services.AddSingleton(); services.AddSingleton, UAuthClientOptionsPostConfigure>(); @@ -109,8 +110,8 @@ private static IServiceCollection AddUltimateAuthClientInternal(this IServiceCol services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); + services.TryAddScoped(); - services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); diff --git a/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/IUAuthResourceRequestClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/IUAuthResourceRequestClient.cs new file mode 100644 index 00000000..cc072aa0 --- /dev/null +++ b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/IUAuthResourceRequestClient.cs @@ -0,0 +1,8 @@ +using CodeBeam.UltimateAuth.Client.Contracts; + +namespace CodeBeam.UltimateAuth.Client.Infrastructure; + +public interface IUAuthResourceRequestClient +{ + Task SendAsync(string method, string endpoint, object? payload = null, CancellationToken ct = default); +} diff --git a/src/client/CodeBeam.UltimateAuth.Client/Options/UAuthClientOptions.cs b/src/client/CodeBeam.UltimateAuth.Client/Options/UAuthClientOptions.cs index f08a09a3..0fe51f34 100644 --- a/src/client/CodeBeam.UltimateAuth.Client/Options/UAuthClientOptions.cs +++ b/src/client/CodeBeam.UltimateAuth.Client/Options/UAuthClientOptions.cs @@ -115,4 +115,9 @@ public sealed class UAuthClientOptions /// Controls how tenant information is resolved and included in requests. /// public UAuthClientMultiTenantOptions MultiTenant { get; set; } = new(); + + /// + /// Configures requests to UltimateAuth-protected application resources. + /// + public UAuthClientResourceOptions Resource { get; set; } = new(); } diff --git a/src/client/CodeBeam.UltimateAuth.Client/Options/UAuthClientResourceOptions.cs b/src/client/CodeBeam.UltimateAuth.Client/Options/UAuthClientResourceOptions.cs new file mode 100644 index 00000000..37f1b687 --- /dev/null +++ b/src/client/CodeBeam.UltimateAuth.Client/Options/UAuthClientResourceOptions.cs @@ -0,0 +1,18 @@ +namespace CodeBeam.UltimateAuth.Client.Options; + +/// +/// Configures client requests to an UltimateAuth-protected Resource API. +/// +public sealed class UAuthClientResourceOptions +{ + /// + /// Base address of the Resource API. + /// When null, requests use the application's origin. + /// + public string? BaseAddress { get; set; } + + /// + /// Controls browser credential transmission. + /// + public UAuthResourceCredentialsMode Credentials { get; set; } = UAuthResourceCredentialsMode.Auto; +} diff --git a/src/client/CodeBeam.UltimateAuth.Client/Options/Validators/UAuthClientResourceOptionsValidator.cs b/src/client/CodeBeam.UltimateAuth.Client/Options/Validators/UAuthClientResourceOptionsValidator.cs new file mode 100644 index 00000000..c21b6761 --- /dev/null +++ b/src/client/CodeBeam.UltimateAuth.Client/Options/Validators/UAuthClientResourceOptionsValidator.cs @@ -0,0 +1,55 @@ +using Microsoft.Extensions.Options; + +namespace CodeBeam.UltimateAuth.Client.Options; + +public sealed class UAuthClientResourceOptionsValidator : IValidateOptions +{ + public ValidateOptionsResult Validate(string? name, UAuthClientOptions options) + { + var resource = options.Resource; + + if (resource is null) + { + return ValidateOptionsResult.Fail("Resource options cannot be null."); + } + + if (!Enum.IsDefined(resource.Credentials)) + { + return ValidateOptionsResult.Fail("Invalid Resource credentials mode."); + } + + if (string.IsNullOrWhiteSpace(resource.BaseAddress)) + { + return ValidateOptionsResult.Success; + } + + if (!Uri.TryCreate(resource.BaseAddress, UriKind.Absolute, out var uri)) + { + return ValidateOptionsResult.Fail("Resource BaseAddress must be an absolute URL."); + } + + if (uri.Scheme != Uri.UriSchemeHttps && uri.Scheme != Uri.UriSchemeHttp) + { + return ValidateOptionsResult.Fail("Resource BaseAddress must use HTTP or HTTPS."); + } + + if (!string.IsNullOrEmpty(uri.UserInfo) || + !string.IsNullOrEmpty(uri.Query) || + !string.IsNullOrEmpty(uri.Fragment)) + { + return ValidateOptionsResult.Fail("Resource BaseAddress cannot contain credentials, query or fragment."); + } + + if (uri.Scheme == Uri.UriSchemeHttp && !uri.IsLoopback) + { + return ValidateOptionsResult.Fail("Resource BaseAddress must use HTTPS except for loopback development addresses."); + } + + if (uri.AbsolutePath != "/") + { + return ValidateOptionsResult.Fail("Resource BaseAddress must contain only the API origin."); + } + + return ValidateOptionsResult.Success; + } +} diff --git a/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IResourceClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IResourceClient.cs new file mode 100644 index 00000000..a08f00ab --- /dev/null +++ b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IResourceClient.cs @@ -0,0 +1,19 @@ +using CodeBeam.UltimateAuth.Core.Contracts; + +namespace CodeBeam.UltimateAuth.Client.Services; + +/// +/// Provides authenticated HTTP operations for application resources. +/// +public interface IResourceClient +{ + Task> GetAsync(string endpoint, CancellationToken ct = default); + + Task> PostAsync(string endpoint, object? request, CancellationToken ct = default); + + Task> PutAsync(string endpoint, object? request, CancellationToken ct = default); + + Task> PatchAsync(string endpoint, object? request, CancellationToken ct = default); + + Task DeleteAsync(string endpoint, CancellationToken ct = default); +} diff --git a/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUAuthClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUAuthClient.cs index 54cabf73..bb5a4ccc 100644 --- a/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUAuthClient.cs +++ b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUAuthClient.cs @@ -64,4 +64,9 @@ public interface IUAuthClient /// Provides authorization and policy evaluation operations. /// IAuthorizationClient Authorization { get; } + + /// + /// Provides authenticated requests to application Resource APIs. + /// + IResourceClient Resource { get; } } diff --git a/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthClient.cs index 6df7e74d..ce8e3aaa 100644 --- a/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthClient.cs +++ b/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthClient.cs @@ -10,8 +10,9 @@ internal sealed class UAuthClient : IUAuthClient public IUserIdentifierClient Identifiers { get; } public ICredentialClient Credentials { get; } public IAuthorizationClient Authorization { get; } + public IResourceClient Resource { get; } - public UAuthClient(IFlowClient flows, ISessionClient session, IUserClient users, IUserIdentifierClient identifiers, ICredentialClient credentials, IAuthorizationClient authorization) + public UAuthClient(IFlowClient flows, ISessionClient session, IUserClient users, IUserIdentifierClient identifiers, ICredentialClient credentials, IAuthorizationClient authorization, IResourceClient resource) { Flows = flows; Sessions = session; @@ -19,5 +20,6 @@ public UAuthClient(IFlowClient flows, ISessionClient session, IUserClient users, Identifiers = identifiers; Credentials = credentials; Authorization = authorization; + Resource = resource; } } diff --git a/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthResourceClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthResourceClient.cs new file mode 100644 index 00000000..5e9eb4c1 --- /dev/null +++ b/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthResourceClient.cs @@ -0,0 +1,78 @@ +using System.Text.Json; +using CodeBeam.UltimateAuth.Client.Contracts; +using CodeBeam.UltimateAuth.Client.Infrastructure; +using CodeBeam.UltimateAuth.Core.Contracts; + +namespace CodeBeam.UltimateAuth.Client.Services; + +internal sealed class UAuthResourceClient : IResourceClient +{ + private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); + + private readonly IUAuthResourceRequestClient _transport; + + public UAuthResourceClient(IUAuthResourceRequestClient transport) + { + _transport = transport; + } + + public Task> GetAsync(string endpoint, CancellationToken ct = default) + => SendAsync("GET", endpoint, null, ct); + + public Task> PostAsync(string endpoint, object? request, CancellationToken ct = default) + => SendAsync("POST", endpoint, request, ct); + + public Task> PutAsync(string endpoint, object? request, CancellationToken ct = default) + => SendAsync("PUT", endpoint, request, ct); + + public Task> PatchAsync(string endpoint, object? request, CancellationToken ct = default) + => SendAsync("PATCH", endpoint, request, ct); + + public async Task DeleteAsync(string endpoint, CancellationToken ct = default) + { + var response = await _transport.SendAsync("DELETE", endpoint, null, ct); + + return new UAuthResult + { + IsSuccess = response.Ok, + Status = response.Status, + Problem = response.Ok ? null : ReadProblem(response) + }; + } + + private async Task> SendAsync(string method, string endpoint, object? request, CancellationToken ct) + { + var response = await _transport.SendAsync(method, endpoint, request, ct); + + return new UAuthResult + { + IsSuccess = response.Ok, + Status = response.Status, + Value = response.Ok ? ReadValue(response) : default, + Problem = response.Ok ? null : ReadProblem(response) + }; + } + + private static T? ReadValue(UAuthResourceTransportResult response) + { + if (string.IsNullOrWhiteSpace(response.Body)) + return default; + + return JsonSerializer.Deserialize(response.Body, JsonOptions); + } + + private static UAuthProblem? ReadProblem(UAuthResourceTransportResult response) + { + if (string.IsNullOrWhiteSpace(response.Body)) + return null; + + try + { + return JsonSerializer.Deserialize(response.Body, JsonOptions); + } + catch (JsonException) + { + return null; + } + } +} diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserIdentifierStoreFactory.cs b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserIdentifierStoreFactory.cs index c6145e2a..2b800d7b 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserIdentifierStoreFactory.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserIdentifierStoreFactory.cs @@ -19,7 +19,6 @@ public IUserIdentifierStore Create(TenantKey tenant) { return _stores.GetOrAdd(tenant, t => { - Console.WriteLine("New Store Added"); var tenantContext = new TenantExecutionContext(tenant); return ActivatorUtilities.CreateInstance(_provider, tenantContext); }); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/Blazor/UAuthAppLifecycleTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/Blazor/UAuthAppLifecycleTests.cs index 1883ef1d..41fddac1 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/Blazor/UAuthAppLifecycleTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/Blazor/UAuthAppLifecycleTests.cs @@ -2,7 +2,6 @@ using CodeBeam.UltimateAuth.Client; using CodeBeam.UltimateAuth.Client.Abstractions; using CodeBeam.UltimateAuth.Client.Blazor; -using CodeBeam.UltimateAuth.Client.Contracts; using CodeBeam.UltimateAuth.Client.Infrastructure; using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; @@ -234,10 +233,89 @@ public async Task Dispose_WhenCoordinatorStartedAfterAuthentication_StopsCoordin Times.Once); } - private static UAuthState AuthenticatedState( - string userKey = "user-1", - string? userName = "alice", - SessionState sessionState = SessionState.Active) + [Fact] + public void ReactiveMode_StateChange_RerendersChildContent() + { + var renderCount = 0; + + var cut = Render(p => p + .Add(x => x.RenderMode, UAuthRenderMode.Reactive) + .Add(x => x.ChildContent, (RenderFragment)(builder => + { + renderCount++; + builder.AddContent(0, $"Render {renderCount}"); + }))); + + var initialRenderCount = renderCount; + + cut.InvokeAsync(() => ApplyAuthenticatedSnapshot(_state)); + + cut.WaitForAssertion(() => + renderCount.Should().BeGreaterThan(initialRenderCount)); + } + + [Fact] + public void ManualMode_StateChange_DoesNotAutomaticallyRerenderChildContent() + { + var renderCount = 0; + + var cut = Render(p => p + .Add(x => x.RenderMode, UAuthRenderMode.Manual) + .Add(x => x.ChildContent, (RenderFragment)(builder => + { + renderCount++; + builder.AddContent(0, $"Render {renderCount}"); + }))); + + var initialRenderCount = renderCount; + + cut.InvokeAsync(() => ApplyAuthenticatedSnapshot(_state)); + + renderCount.Should().Be(initialRenderCount); + } + + [Fact] + public async Task Dispose_UnsubscribesFromStateChanges() + { + var cut = Render(p => p + .AddChildContent("content")); + + await cut.Instance.DisposeAsync(); + + _coordinator.Invocations.Clear(); + + ApplyAuthenticatedSnapshot(_state); + + _coordinator.Verify( + x => x.StartAsync(), + Times.Never); + } + + [Fact] + public void ManualMode_Touch_RequestsRender() + { + var renderCount = 0; + + var cut = Render(p => p + .Add(x => x.RenderMode, UAuthRenderMode.Manual) + .Add(x => x.ChildContent, (RenderFragment)(builder => + { + renderCount++; + builder.AddContent(0, $"Render {renderCount}"); + }))); + + var initialRenderCount = renderCount; + + cut.InvokeAsync(() => _state.Touch(updateState: false)); + + cut.WaitForAssertion(() => + renderCount.Should().BeGreaterThan(initialRenderCount)); + + _state.IsStale.Should().BeFalse(); + } + + + private static UAuthState AuthenticatedState(string userKey = "user-1", string? userName = "alice", SessionState sessionState = SessionState.Active) { var state = UAuthState.Anonymous(); @@ -251,10 +329,7 @@ private static UAuthState AuthenticatedState( return state; } - private static AuthStateSnapshot CreateAuthSnapshot( - string userKey = "user-1", - string? userName = "alice", - SessionState sessionState = SessionState.Active) + private static AuthStateSnapshot CreateAuthSnapshot(string userKey = "user-1", string? userName = "alice", SessionState sessionState = SessionState.Active) { return new AuthStateSnapshot { @@ -274,8 +349,6 @@ private static AuthStateSnapshot CreateAuthSnapshot( private static void ApplyAuthenticatedSnapshot(UAuthState state) { - state.ApplySnapshot( - CreateAuthSnapshot(), - DateTimeOffset.UtcNow); + state.ApplySnapshot(CreateAuthSnapshot(), DateTimeOffset.UtcNow); } } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthClientResourceOptionsValidatorTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthClientResourceOptionsValidatorTests.cs new file mode 100644 index 00000000..63896893 --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthClientResourceOptionsValidatorTests.cs @@ -0,0 +1,95 @@ +using AwesomeAssertions; +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Client.Options; + +namespace CodeBeam.UltimateAuth.Tests.Unit; + +public class UAuthClientResourceOptionsValidatorTests +{ + private readonly UAuthClientResourceOptionsValidator _validator = new(); + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("https://api.example.com")] + [InlineData("https://api.example.com/")] + [InlineData("http://localhost:6120")] + [InlineData("http://127.0.0.1:6120")] + public void Valid_BaseAddress_Should_Pass(string? baseAddress) + { + var options = CreateOptions(baseAddress); + + var result = _validator.Validate(null, options); + + result.Succeeded.Should().BeTrue(); + } + + [Theory] + [InlineData("/api")] + [InlineData("api.example.com")] + [InlineData("ftp://api.example.com")] + [InlineData("http://api.example.com")] + [InlineData("https://api.example.com/path")] + [InlineData("https://api.example.com?key=value")] + [InlineData("https://api.example.com#fragment")] + [InlineData("https://user:password@api.example.com")] + public void Invalid_BaseAddress_Should_Fail(string baseAddress) + { + var options = CreateOptions(baseAddress); + + var result = _validator.Validate(null, options); + + result.Failed.Should().BeTrue(); + } + + [Fact] + public void Null_Resource_Options_Should_Fail() + { + var options = new UAuthClientOptions + { + Resource = null! + }; + + var result = _validator.Validate(null, options); + + result.Failed.Should().BeTrue(); + } + + [Fact] + public void Invalid_Credentials_Mode_Should_Fail() + { + var options = CreateOptions("https://api.example.com"); + + options.Resource.Credentials = + (UAuthResourceCredentialsMode)999; + + var result = _validator.Validate(null, options); + + result.Failed.Should().BeTrue(); + } + + [Theory] + [InlineData(UAuthResourceCredentialsMode.Auto)] + [InlineData(UAuthResourceCredentialsMode.SameOrigin)] + [InlineData(UAuthResourceCredentialsMode.Include)] + [InlineData(UAuthResourceCredentialsMode.Omit)] + public void Valid_Credentials_Mode_Should_Pass( + UAuthResourceCredentialsMode credentials) + { + var options = CreateOptions("https://api.example.com"); + options.Resource.Credentials = credentials; + + var result = _validator.Validate(null, options); + + result.Succeeded.Should().BeTrue(); + } + + private static UAuthClientOptions CreateOptions(string? baseAddress) + => new() + { + Resource = new UAuthClientResourceOptions + { + BaseAddress = baseAddress + } + }; +} diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthClientSessionTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthClientSessionTests.cs index 25e5e6c5..deaa0861 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthClientSessionTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthClientSessionTests.cs @@ -37,7 +37,8 @@ private IUAuthClient CreateClient() users: Mock.Of(), identifiers: Mock.Of(), credentials: Mock.Of(), - authorization: Mock.Of()); + authorization: Mock.Of(), + resource: Mock.Of()); } private static UAuthTransportResult Success() diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthFlowClientTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthFlowClientTests.cs index d979b72e..ee8b992c 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthFlowClientTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthFlowClientTests.cs @@ -22,7 +22,7 @@ public class UAuthFlowClientTests : UAuthClientTestBase { private readonly Mock _mockRequest = new(); - private UAuthFlowClient CreateClient(Mock? requestMock = null) + private UAuthFlowClient CreateClient(Mock? requestMock = null, UAuthClientDiagnostics? diagnostics = null) { var request = requestMock ?? new Mock(); @@ -54,15 +54,13 @@ private UAuthFlowClient CreateClient(Mock? requestMock = nu } }); - var diagnostics = new UAuthClientDiagnostics(); - return new UAuthFlowClient( request.Object, events.Object, deviceProvider.Object, returnUrlProvider.Object, options, - diagnostics); + diagnostics ?? new UAuthClientDiagnostics()); } private static UAuthTransportResult TryLoginResponse(bool success, AuthFailureReason? reason = null) @@ -810,8 +808,146 @@ public void UAuthClient_Should_Expose_FlowClient() Mock.Of(), Mock.Of(), Mock.Of(), - Mock.Of()); + Mock.Of(), + Mock.Of()); client.Flows.Should().Be(flow); } -} \ No newline at end of file + + [Theory] + [InlineData("no-op", RefreshOutcome.NoOp)] + [InlineData("touched", RefreshOutcome.Touched)] + [InlineData("rotated", RefreshOutcome.Rotated)] + [InlineData("reauth-required", RefreshOutcome.ReauthRequired)] + public async Task Refresh_Should_Record_Correct_Diagnostic_Outcome(string outcomeHeader, RefreshOutcome expectedOutcome) + { + var request = new Mock(); + var diagnostics = new UAuthClientDiagnostics(); + + request.Setup(x => x.SendFormAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync(new UAuthTransportResult + { + Ok = true, + Status = 200, + RefreshOutcome = outcomeHeader + }); + + var client = CreateClient(request, diagnostics); + + var result = await client.RefreshAsync(isAuto: true); + + result.IsSuccess.Should().BeTrue(); + result.Outcome.Should().Be(expectedOutcome); + + diagnostics.RefreshNoOpCount.Should() + .Be(expectedOutcome == RefreshOutcome.NoOp ? 1 : 0); + + diagnostics.RefreshTouchedCount.Should() + .Be(expectedOutcome == RefreshOutcome.Touched ? 1 : 0); + + diagnostics.RefreshRotatedCount.Should() + .Be(expectedOutcome == RefreshOutcome.Rotated ? 1 : 0); + + diagnostics.RefreshReauthRequiredCount.Should() + .Be(expectedOutcome == RefreshOutcome.ReauthRequired ? 1 : 0); + + diagnostics.RefreshSuccessCount.Should().Be(0); + + diagnostics.ManualRefreshCount.Should().Be(0); + } + + [Fact] + public async Task Refresh_Should_Record_Success_Diagnostic() + { + var request = new Mock(); + var diagnostics = new UAuthClientDiagnostics(); + + request.Setup(x => x.SendFormAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync(new UAuthTransportResult + { + Ok = true, + Status = 200, + RefreshOutcome = "success" + }); + + var client = CreateClient(request, diagnostics); + + var result = await client.RefreshAsync(); + + result.IsSuccess.Should().BeTrue(); + result.Outcome.Should().Be(RefreshOutcome.Success); + + diagnostics.RefreshSuccessCount.Should().Be(1); + diagnostics.ManualRefreshCount.Should().Be(1); + diagnostics.RefreshAttemptCount.Should().Be(1); + } + + [Theory] + [InlineData("no-op")] + [InlineData("touched")] + [InlineData("rotated")] + [InlineData("success")] + public async Task Refresh_Should_Not_Record_Successful_Outcome_When_Response_Fails(string outcomeHeader) + { + var request = new Mock(); + var diagnostics = new UAuthClientDiagnostics(); + + request.Setup(x => x.SendFormAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync(new UAuthTransportResult + { + Ok = false, + Status = 200, + RefreshOutcome = outcomeHeader + }); + + var client = CreateClient(request, diagnostics); + + var result = await client.RefreshAsync(isAuto: true); + + result.IsSuccess.Should().BeFalse(); + + diagnostics.RefreshNoOpCount.Should().Be(0); + diagnostics.RefreshTouchedCount.Should().Be(0); + diagnostics.RefreshRotatedCount.Should().Be(0); + diagnostics.RefreshSuccessCount.Should().Be(0); + } + + [Fact] + public async Task Refresh_Should_Record_ReauthRequired_On_Failed_Response() + { + var request = new Mock(); + var diagnostics = new UAuthClientDiagnostics(); + + request.Setup(x => x.SendFormAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync(new UAuthTransportResult + { + Ok = false, + Status = 400, + RefreshOutcome = "reauth-required" + }); + + var client = CreateClient(request, diagnostics); + + var result = await client.RefreshAsync(isAuto: true); + + result.IsSuccess.Should().BeFalse(); + result.Outcome.Should().Be(RefreshOutcome.ReauthRequired); + + diagnostics.RefreshReauthRequiredCount.Should().Be(1); + + diagnostics.RefreshSuccessCount.Should().Be(0); + diagnostics.RefreshRotatedCount.Should().Be(0); + } +} diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthLoginPageDiscoveryTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthLoginPageDiscoveryTests.cs index dd0b7bb5..9714f6b2 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthLoginPageDiscoveryTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthLoginPageDiscoveryTests.cs @@ -2,6 +2,7 @@ using CodeBeam.UltimateAuth.Client.Blazor.Infrastructure; using CodeBeam.UltimateAuth.Client.Infrastructure; using AwesomeAssertions; +using Microsoft.AspNetCore.Components; namespace CodeBeam.UltimateAuth.Tests.Unit.Client.Infrastructure; @@ -216,4 +217,45 @@ public void ResolveRoute_CustomFallback_ShouldBeIndependentOfDiscoveryOrder( result.Should().Be("/a"); } -} \ No newline at end of file + + [Fact] + public void ResolveRoute_Type_ShouldResolvePreferredRoute() + { + var result = UAuthLoginPageDiscovery.ResolveRoute( + typeof(TestLoginPage)); + + result.Should().Be("/sign-in"); + } + + [Fact] + public void ResolveRoute_Type_WithoutAttribute_ShouldThrow() + { + var act = () => UAuthLoginPageDiscovery.ResolveRoute( + typeof(TestPageWithoutLoginAttribute)); + + act.Should() + .Throw() + .WithMessage("*not decorated with [UAuthLoginPage]*"); + } + + [Fact] + public void ResolveRoute_Type_WhenNull_ShouldThrow() + { + var act = () => UAuthLoginPageDiscovery.ResolveRoute(null!); + + act.Should().Throw(); + } + + + [UAuthLoginPage("/sign-in")] + [Route("/")] + [Route("/sign-in")] + private sealed class TestLoginPage : ComponentBase + { + } + + [Route("/login")] + private sealed class TestPageWithoutLoginAttribute : ComponentBase + { + } +} diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthResourceClientTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthResourceClientTests.cs new file mode 100644 index 00000000..781e1b8f --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthResourceClientTests.cs @@ -0,0 +1,255 @@ +using AwesomeAssertions; +using CodeBeam.UltimateAuth.Client.Contracts; +using CodeBeam.UltimateAuth.Client.Infrastructure; +using CodeBeam.UltimateAuth.Client.Services; +using Moq; + +namespace CodeBeam.UltimateAuth.Tests.Unit; + +public class UAuthResourceClientTests +{ + private readonly Mock _transport = new(); + + private UAuthResourceClient CreateClient() + => new(_transport.Object); + + [Theory] + [InlineData("GET")] + [InlineData("POST")] + [InlineData("PUT")] + [InlineData("PATCH")] + public async Task Resource_Methods_Should_Forward_Correct_Method(string method) + { + var payload = new { Name = "Product" }; + + _transport + .Setup(x => x.SendAsync( + method, + "/api/products", + It.IsAny(), + It.IsAny())) + .ReturnsAsync(new UAuthResourceTransportResult + { + Ok = true, + Status = 200, + Body = "{\"id\":1,\"name\":\"Product\"}" + }); + + var client = CreateClient(); + + switch (method) + { + case "GET": + await client.GetAsync("/api/products"); + break; + + case "POST": + await client.PostAsync("/api/products", payload); + break; + + case "PUT": + await client.PutAsync("/api/products", payload); + break; + + case "PATCH": + await client.PatchAsync("/api/products", payload); + break; + } + + _transport.Verify(x => x.SendAsync( + method, + "/api/products", + It.Is(p => + method == "GET" ? p == null : ReferenceEquals(p, payload)), + It.IsAny()), Times.Once); + } + + [Fact] + public async Task Get_Should_Deserialize_Success_Response() + { + _transport + .Setup(x => x.SendAsync( + "GET", + "/api/products/1", + null, + It.IsAny())) + .ReturnsAsync(new UAuthResourceTransportResult + { + Ok = true, + Status = 200, + Body = "{\"id\":1,\"name\":\"Product\"}", + ContentType = "application/json" + }); + + var result = await CreateClient() + .GetAsync("/api/products/1"); + + result.IsSuccess.Should().BeTrue(); + result.Status.Should().Be(200); + result.Value.Should().NotBeNull(); + result.Value!.Id.Should().Be(1); + result.Value.Name.Should().Be("Product"); + } + + [Fact] + public async Task Get_Should_Return_Default_When_Body_Is_Empty() + { + _transport + .Setup(x => x.SendAsync( + "GET", + It.IsAny(), + null, + It.IsAny())) + .ReturnsAsync(new UAuthResourceTransportResult + { + Ok = true, + Status = 204, + Body = null + }); + + var result = await CreateClient() + .GetAsync("/api/products/1"); + + result.IsSuccess.Should().BeTrue(); + result.Status.Should().Be(204); + result.Value.Should().BeNull(); + } + + [Fact] + public async Task Get_Should_Parse_Problem_On_Failure() + { + _transport + .Setup(x => x.SendAsync( + "GET", + It.IsAny(), + null, + It.IsAny())) + .ReturnsAsync(new UAuthResourceTransportResult + { + Ok = false, + Status = 403, + Body = """ + { + "title": "Forbidden", + "status": 403, + "detail": "Access denied" + } + """ + }); + + var result = await CreateClient() + .GetAsync("/api/products"); + + result.IsSuccess.Should().BeFalse(); + result.Status.Should().Be(403); + result.Value.Should().BeNull(); + result.Problem.Should().NotBeNull(); + result.Problem!.Title.Should().Be("Forbidden"); + } + + [Fact] + public async Task Get_Should_Handle_Invalid_Problem_Json() + { + _transport + .Setup(x => x.SendAsync( + "GET", + It.IsAny(), + null, + It.IsAny())) + .ReturnsAsync(new UAuthResourceTransportResult + { + Ok = false, + Status = 500, + Body = "Server Error" + }); + + var result = await CreateClient() + .GetAsync("/api/products"); + + result.IsSuccess.Should().BeFalse(); + result.Status.Should().Be(500); + result.Problem.Should().BeNull(); + } + + [Fact] + public async Task Delete_Should_Return_Success_Without_Body() + { + _transport + .Setup(x => x.SendAsync( + "DELETE", + "/api/products/1", + null, + It.IsAny())) + .ReturnsAsync(new UAuthResourceTransportResult + { + Ok = true, + Status = 204 + }); + + var result = await CreateClient() + .DeleteAsync("/api/products/1"); + + result.IsSuccess.Should().BeTrue(); + result.Status.Should().Be(204); + result.Problem.Should().BeNull(); + } + + [Fact] + public async Task Delete_Should_Parse_Problem_On_Failure() + { + _transport + .Setup(x => x.SendAsync( + "DELETE", + "/api/products/1", + null, + It.IsAny())) + .ReturnsAsync(new UAuthResourceTransportResult + { + Ok = false, + Status = 404, + Body = """{"title":"Not Found","status":404}""" + }); + + var result = await CreateClient() + .DeleteAsync("/api/products/1"); + + result.IsSuccess.Should().BeFalse(); + result.Status.Should().Be(404); + result.Problem.Should().NotBeNull(); + result.Problem!.Title.Should().Be("Not Found"); + } + + [Fact] + public async Task Get_Should_Forward_CancellationToken() + { + using var cts = new CancellationTokenSource(); + + _transport + .Setup(x => x.SendAsync( + "GET", + "/api/products", + null, + cts.Token)) + .ReturnsAsync(new UAuthResourceTransportResult + { + Ok = true, + Status = 200, + Body = "[]" + }); + + await CreateClient() + .GetAsync>("/api/products", cts.Token); + + _transport.Verify(x => x.SendAsync( + "GET", + "/api/products", + null, + cts.Token), Times.Once); + } + + public sealed class TestProduct + { + public int Id { get; set; } + public string Name { get; set; } = ""; + } +} diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthResourceRequestClientTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthResourceRequestClientTests.cs new file mode 100644 index 00000000..06c5ede3 --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthResourceRequestClientTests.cs @@ -0,0 +1,341 @@ +using AwesomeAssertions; +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Client.Blazor.Infrastructure; +using CodeBeam.UltimateAuth.Client.Contracts; +using CodeBeam.UltimateAuth.Client.Errors; +using CodeBeam.UltimateAuth.Client.Infrastructure; +using CodeBeam.UltimateAuth.Client.Options; +using Microsoft.Extensions.Options; +using Microsoft.JSInterop; +using Moq; + +namespace CodeBeam.UltimateAuth.Tests.Unit; + +public class UAuthResourceRequestClientTests +{ + private readonly Mock _js = new(); + private readonly Mock _bootstrapper = new(); + + private UAuthResourceRequestClient CreateClient( + string? baseAddress = "https://localhost:6120", + UAuthResourceCredentialsMode credentials = + UAuthResourceCredentialsMode.Auto) + { + var options = Options.Create(new UAuthClientOptions + { + Resource = new UAuthClientResourceOptions + { + BaseAddress = baseAddress, + Credentials = credentials + } + }); + + return new UAuthResourceRequestClient( + _js.Object, + _bootstrapper.Object, + options); + } + + private void SetupResponse( + UAuthResourceTransportResult? response) + { + _js.Setup(x => x.InvokeAsync( + "uauth.resourceRequest", + It.IsAny(), + It.IsAny())) + .ReturnsAsync(response!); + } + + [Fact] + public async Task Send_Should_Ensure_Bootstrap_Before_JS_Request() + { + var bootstrapCompleted = + new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + _bootstrapper + .Setup(x => x.EnsureStartedAsync( + It.IsAny())) + .Returns(bootstrapCompleted.Task); + + SetupResponse(new UAuthResourceTransportResult + { + Ok = true, + Status = 200 + }); + + var client = CreateClient(); + + var task = client.SendAsync("GET", "/api/products"); + + _bootstrapper.Verify(x => x.EnsureStartedAsync( + It.IsAny()), Times.Once); + + _js.Verify(x => x.InvokeAsync( + It.IsAny(), + It.IsAny(), + It.IsAny()), Times.Never); + + bootstrapCompleted.SetResult(); + + await task; + + _js.Verify(x => x.InvokeAsync( + "uauth.resourceRequest", + It.IsAny(), + It.IsAny()), Times.Once); + } + + [Fact] + public async Task Send_Should_Pass_Correct_Arguments_To_JS() + { + object? capturedOptions = null; + + _js.Setup(x => x.InvokeAsync( + "uauth.resourceRequest", + It.IsAny(), + It.IsAny())) + .Callback( + (_, _, args) => capturedOptions = args[0]) + .ReturnsAsync(new UAuthResourceTransportResult + { + Ok = true, + Status = 201 + }); + + var payload = new { Name = "Product" }; + + var client = CreateClient( + "https://localhost:6120", + UAuthResourceCredentialsMode.Include); + + await client.SendAsync( + "POST", + "/api/products", + payload); + + capturedOptions.Should().NotBeNull(); + + var type = capturedOptions!.GetType(); + + type.GetProperty("url")! + .GetValue(capturedOptions) + .Should().Be("/api/products"); + + type.GetProperty("method")! + .GetValue(capturedOptions) + .Should().Be("POST"); + + type.GetProperty("baseAddress")! + .GetValue(capturedOptions) + .Should().Be("https://localhost:6120"); + + type.GetProperty("credentials")! + .GetValue(capturedOptions) + .Should().Be("Include"); + + type.GetProperty("payload")! + .GetValue(capturedOptions) + .Should().BeSameAs(payload); + + var requestId = type.GetProperty("requestId")! + .GetValue(capturedOptions) as string; + + requestId.Should().NotBeNullOrWhiteSpace(); + Guid.TryParseExact(requestId, "N", out _) + .Should().BeTrue(); + } + + [Fact] + public async Task Send_Should_Return_Success_Response() + { + var response = new UAuthResourceTransportResult + { + Ok = true, + Status = 200, + Body = """{"id":1}""", + ContentType = "application/json" + }; + + SetupResponse(response); + + var result = await CreateClient() + .SendAsync("GET", "/api/products/1"); + + result.Should().BeSameAs(response); + } + + [Fact] + public async Task Send_Should_Return_Http_Error_Response() + { + var response = new UAuthResourceTransportResult + { + Ok = false, + Status = 403, + Body = """{"title":"Forbidden"}""" + }; + + SetupResponse(response); + + var result = await CreateClient() + .SendAsync("GET", "/api/products"); + + result.Should().BeSameAs(response); + result.Status.Should().Be(403); + result.Ok.Should().BeFalse(); + } + + [Fact] + public async Task Send_Should_Throw_When_JS_Returns_Null() + { + SetupResponse(null); + + var client = CreateClient(); + + Func act = () => + client.SendAsync("GET", "/api/products"); + + await act.Should() + .ThrowAsync(); + } + + [Fact] + public async Task Send_Should_Throw_On_Network_Error() + { + SetupResponse(new UAuthResourceTransportResult + { + Ok = false, + Status = 0 + }); + + var client = CreateClient(); + + Func act = () => + client.SendAsync("GET", "/api/products"); + + await act.Should() + .ThrowAsync(); + } + + [Fact] + public async Task Send_Should_Not_Bootstrap_When_Already_Cancelled() + { + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + var client = CreateClient(); + + Func act = () => + client.SendAsync( + "GET", + "/api/products", + ct: cts.Token); + + await act.Should() + .ThrowAsync(); + + _bootstrapper.Verify(x => x.EnsureStartedAsync( + It.IsAny()), Times.Never); + + _js.Verify(x => x.InvokeAsync( + It.IsAny(), + It.IsAny(), + It.IsAny()), Times.Never); + } + + [Fact] + public async Task Send_Should_Not_Invoke_JS_When_Cancelled_During_Bootstrap() + { + var bootstrapCompleted = + new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + _bootstrapper + .Setup(x => x.EnsureStartedAsync( + It.IsAny())) + .Returns(bootstrapCompleted.Task); + + using var cts = new CancellationTokenSource(); + + var client = CreateClient(); + + var task = client.SendAsync( + "GET", + "/api/products", + ct: cts.Token); + + cts.Cancel(); + + bootstrapCompleted.SetResult(); + + Func act = () => task; + + await act.Should() + .ThrowAsync(); + + _js.Verify(x => x.InvokeAsync( + It.IsAny(), + It.IsAny(), + It.IsAny()), Times.Never); + } + + [Fact] + public async Task Send_Should_Propagate_JS_Cancellation() + { + using var cts = new CancellationTokenSource(); + + _js.Setup(x => x.InvokeAsync( + "uauth.resourceRequest", + cts.Token, + It.IsAny())) + .Returns( + (_, ct, _) => new ValueTask( + Task.FromCanceled(ct))); + + var client = CreateClient(); + + cts.Cancel(); + + // The pre-cancelled token is rejected before JS invocation. + Func act = () => + client.SendAsync( + "GET", + "/api/products", + ct: cts.Token); + + await act.Should() + .ThrowAsync(); + } + + [Fact] + public async Task Send_Should_Generate_Unique_RequestIds() + { + var requestIds = new List(); + + _js.Setup(x => x.InvokeAsync( + "uauth.resourceRequest", + It.IsAny(), + It.IsAny())) + .Callback( + (_, _, args) => + { + var options = args[0]!; + var requestId = options.GetType() + .GetProperty("requestId")! + .GetValue(options) as string; + + requestIds.Add(requestId!); + }) + .ReturnsAsync(new UAuthResourceTransportResult + { + Ok = true, + Status = 200 + }); + + var client = CreateClient(); + + await client.SendAsync("GET", "/api/products/1"); + await client.SendAsync("GET", "/api/products/2"); + + requestIds.Should().HaveCount(2); + requestIds.Distinct().Should().HaveCount(2); + } +} diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthStateManagerTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthStateManagerTests.cs index 0f4f8883..8f25e4a3 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthStateManagerTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthStateManagerTests.cs @@ -1,4 +1,5 @@ -using CodeBeam.UltimateAuth.Client; +using AwesomeAssertions; +using CodeBeam.UltimateAuth.Client; using CodeBeam.UltimateAuth.Client.Authentication; using CodeBeam.UltimateAuth.Client.Events; using CodeBeam.UltimateAuth.Client.Services; @@ -6,7 +7,7 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.MultiTenancy; -using AwesomeAssertions; +using CodeBeam.UltimateAuth.Users.Contracts; using Moq; namespace CodeBeam.UltimateAuth.Tests.Unit; @@ -51,32 +52,53 @@ public async Task EnsureAsync_should_not_validate_when_authenticated_and_not_sta } [Fact] - public async Task EnsureAsync_should_deduplicate_concurrent_calls() + public async Task EnsureAsync_Should_Deduplicate_InFlight_Calls_But_Allow_Later_Validation() { - var flows = new Mock(); + var (manager, flows, _, _) = CreateManager(); - flows.Setup(x => x.ValidateAsync()) - .Returns(async () => + using (manager) + { + var pendingValidation = + new TaskCompletionSource( + TaskCreationOptions.RunContinuationsAsynchronously); + + flows.SetupSequence(x => x.ValidateAsync()) + .Returns(pendingValidation.Task) + .ReturnsAsync(new AuthValidationResult + { + State = SessionState.Active, + Snapshot = CreateSnapshot() + }); + + var first = manager.EnsureAsync(force: true); + var second = manager.EnsureAsync(force: true); + var third = manager.EnsureAsync(force: true); + + first.IsCompleted.Should().BeFalse(); + + second.Should().BeSameAs(first); + third.Should().BeSameAs(first); + + flows.Verify( + x => x.ValidateAsync(), + Times.Once); + + pendingValidation.SetResult(new AuthValidationResult { - await Task.Delay(50); - return new AuthValidationResult { State = SessionState.Invalid }; + State = SessionState.Active, + Snapshot = CreateSnapshot() }); - var client = new Mock(); - client.SetupGet(x => x.Flows).Returns(flows.Object); - - var events = new Mock(); - var clock = new Mock(); + await Task.WhenAll(first, second, third); - var manager = new UAuthStateManager(client.Object, events.Object, clock.Object); + manager.State.IsAuthenticated.Should().BeTrue(); - await Task.WhenAll( - manager.EnsureAsync(true), - manager.EnsureAsync(true), - manager.EnsureAsync(true) - ); + await manager.EnsureAsync(force: true); - flows.Verify(x => x.ValidateAsync(), Times.Once); + flows.Verify( + x => x.ValidateAsync(), + Times.Exactly(2)); + } } [Fact] @@ -96,4 +118,474 @@ public async Task EnsureAsync_invalid_should_clear_state() await manager.EnsureAsync(); manager.State.IsAuthenticated.Should().BeFalse(); } + + [Fact] + public async Task OnLogin_Should_Mark_Authenticated_State_Stale() + { + var (manager, flows, _, _) = CreateManager(); + using (manager) + { + SetupValidSnapshot(flows); + + await manager.EnsureAsync(); + + manager.State.IsAuthenticated.Should().BeTrue(); + manager.State.IsStale.Should().BeFalse(); + + await manager.OnLoginAsync(); + + manager.State.IsStale.Should().BeTrue(); + manager.NeedsValidation.Should().BeTrue(); + } + } + + [Fact] + public async Task OnLogin_When_Anonymous_Should_Still_Require_Validation() + { + var (manager, _, _, _) = CreateManager(); + using (manager) + { + await manager.OnLoginAsync(); + + manager.State.IsAuthenticated.Should().BeFalse(); + manager.NeedsValidation.Should().BeTrue(); + } + } + + [Fact] + public async Task OnLogout_Should_Clear_Authenticated_State() + { + var (manager, flows, _, _) = CreateManager(); + using (manager) + { + SetupValidSnapshot(flows); + + await manager.EnsureAsync(); + await manager.OnLogoutAsync(); + + manager.State.IsAuthenticated.Should().BeFalse(); + manager.State.Identity.Should().BeNull(); + manager.State.IsStale.Should().BeFalse(); + manager.NeedsValidation.Should().BeTrue(); + } + } + + [Fact] + public async Task MarkStale_Should_Cause_Next_Ensure_To_Validate() + { + var (manager, flows, _, _) = CreateManager(); + using (manager) + { + SetupValidSnapshot(flows); + + await manager.EnsureAsync(); + + manager.MarkStale(); + + await manager.EnsureAsync(); + + flows.Verify(x => x.ValidateAsync(), Times.Exactly(2)); + manager.State.IsStale.Should().BeFalse(); + } + } + + [Fact] + public async Task Clear_Should_Require_Next_Validation() + { + var (manager, flows, _, _) = CreateManager(); + using (manager) + { + SetupValidSnapshot(flows); + + await manager.EnsureAsync(); + + manager.Clear(); + + manager.State.IsAuthenticated.Should().BeFalse(); + manager.NeedsValidation.Should().BeTrue(); + + await manager.EnsureAsync(); + + flows.Verify(x => x.ValidateAsync(), Times.Exactly(2)); + } + } + + [Theory] + [InlineData(UAuthStateEvent.SessionRevoked)] + [InlineData(UAuthStateEvent.CredentialsChanged)] + [InlineData(UAuthStateEvent.UserDeleted)] + [InlineData(UAuthStateEvent.LogoutVariant)] + public async Task State_Invalidating_Events_Should_Clear_State(UAuthStateEvent eventType) + { + var (manager, flows, events, _) = CreateManager(); + using (manager) + { + SetupValidSnapshot(flows); + + await manager.EnsureAsync(); + + await events.PublishAsync( + new UAuthStateEventArgsEmpty( + eventType, + UAuthStateEventHandlingMode.Patch)); + + manager.State.IsAuthenticated.Should().BeFalse(); + manager.State.Identity.Should().BeNull(); + } + } + + [Fact] + public async Task ProfileChanged_Should_Patch_Current_Identity() + { + var (manager, flows, events, _) = CreateManager(); + using (manager) + { + SetupValidSnapshot(flows); + + await manager.EnsureAsync(); + + await events.PublishAsync( + new UAuthStateEventArgs( + UAuthStateEvent.ProfileChanged, + UAuthStateEventHandlingMode.Patch, + new UpdateProfileRequest + { + DisplayName = "Updated Name" + })); + + manager.State.Identity!.DisplayName + .Should().Be("Updated Name"); + + flows.Verify(x => x.ValidateAsync(), Times.Once); + } + } + + [Fact] + public async Task UserStatusChanged_Should_Patch_Current_Identity() + { + var (manager, flows, events, _) = CreateManager(); + using (manager) + { + SetupValidSnapshot(flows); + + await manager.EnsureAsync(); + + await events.PublishAsync( + new UAuthStateEventArgs( + UAuthStateEvent.UserStatusChanged, + UAuthStateEventHandlingMode.Patch, + new ChangeUserStatusSelfRequest + { + NewStatus = SelfAssignableUserStatus.SelfSuspended + })); + + manager.State.Identity!.UserStatus + .Should().Be(UserStatus.SelfSuspended); + + flows.Verify(x => x.ValidateAsync(), Times.Once); + } + } + + [Fact] + public async Task RefreshMode_None_Should_Ignore_Event() + { + var (manager, flows, events, _) = CreateManager(); + using (manager) + { + SetupValidSnapshot(flows); + + await manager.EnsureAsync(); + + await events.PublishAsync( + new UAuthStateEventArgsEmpty( + UAuthStateEvent.SessionRevoked, + UAuthStateEventHandlingMode.None)); + + manager.State.IsAuthenticated.Should().BeTrue(); + manager.State.IsStale.Should().BeFalse(); + } + } + + [Fact] + public async Task RefreshMode_Validate_Should_Force_Validation() + { + var (manager, flows, events, _) = CreateManager(); + using (manager) + { + SetupValidSnapshot(flows); + + await manager.EnsureAsync(); + + await events.PublishAsync( + new UAuthStateEventArgsEmpty( + UAuthStateEvent.AuthorizationChanged, + UAuthStateEventHandlingMode.Validate)); + + flows.Verify(x => x.ValidateAsync(), Times.Exactly(2)); + } + } + + [Fact] + public async Task ValidationCalled_With_Patch_Should_Mark_Validated() + { + var (manager, flows, events, clock) = CreateManager(); + using (manager) + { + SetupValidSnapshot(flows); + + await manager.EnsureAsync(); + + manager.MarkStale(); + + var newTime = new DateTimeOffset( + 2026, 10, 26, 12, 5, 0, TimeSpan.Zero); + + clock.SetupGet(x => x.UtcNow).Returns(newTime); + + await events.PublishAsync( + new UAuthStateEventArgsEmpty( + UAuthStateEvent.ValidationCalled, + UAuthStateEventHandlingMode.Patch)); + + manager.State.IsStale.Should().BeFalse(); + manager.State.LastValidatedAt.Should().Be(newTime); + + flows.Verify(x => x.ValidateAsync(), Times.Once); + } + } + + [Fact] + public async Task AuthorizationChanged_With_Patch_Should_Mark_Stale() + { + var (manager, flows, events, _) = CreateManager(); + using (manager) + { + SetupValidSnapshot(flows); + + await manager.EnsureAsync(); + + await events.PublishAsync( + new UAuthStateEventArgsEmpty( + UAuthStateEvent.AuthorizationChanged, + UAuthStateEventHandlingMode.Patch)); + + manager.State.IsStale.Should().BeTrue(); + manager.NeedsValidation.Should().BeTrue(); + } + } + + [Fact] + public async Task Dispose_Should_Unsubscribe_From_State_Events() + { + var (manager, flows, events, _) = CreateManager(); + + SetupValidSnapshot(flows); + + await manager.EnsureAsync(); + + manager.Dispose(); + + await events.PublishAsync( + new UAuthStateEventArgsEmpty( + UAuthStateEvent.SessionRevoked, + UAuthStateEventHandlingMode.Patch)); + + manager.State.IsAuthenticated.Should().BeTrue(); + } + + [Fact] + public async Task EnsureAsync_Should_Allow_Revalidation_After_Synchronous_Completion() + { + var (manager, flows, _, _) = CreateManager(); + + using (manager) + { + SetupValidSnapshot(flows); + await manager.EnsureAsync(force: true); + await manager.EnsureAsync(force: true); + await manager.EnsureAsync(force: true); + flows.Verify(x => x.ValidateAsync(), Times.Exactly(3)); + } + } + + [Theory] + [InlineData(SessionState.Expired)] + [InlineData(SessionState.Revoked)] + [InlineData(SessionState.NotFound)] + [InlineData(SessionState.Invalid)] + [InlineData(SessionState.SecurityMismatch)] + [InlineData(SessionState.DeviceMismatch)] + [InlineData(SessionState.Unsupported)] + public async Task EnsureAsync_When_Session_Is_Not_Active_Should_Clear_State(SessionState sessionState) + { + var (manager, flows, _, _) = CreateManager(); + + using (manager) + { + SetupValidSnapshot(flows); + + await manager.EnsureAsync(); + + manager.State.IsAuthenticated.Should().BeTrue(); + + flows.Setup(x => x.ValidateAsync()) + .ReturnsAsync(new AuthValidationResult + { + State = sessionState + }); + + await manager.EnsureAsync(force: true); + + manager.State.IsAuthenticated.Should().BeFalse(); + manager.State.Identity.Should().BeNull(); + manager.State.IsStale.Should().BeFalse(); + manager.NeedsValidation.Should().BeTrue(); + + flows.Verify( + x => x.ValidateAsync(), + Times.Exactly(2)); + } + } + + [Fact] + public async Task EnsureAsync_Should_Allow_Retry_After_Validation_Exception() + { + var (manager, flows, _, _) = CreateManager(); + + using (manager) + { + flows.SetupSequence(x => x.ValidateAsync()) + .ThrowsAsync(new HttpRequestException("Network failure")) + .ReturnsAsync(new AuthValidationResult + { + State = SessionState.Active, + Snapshot = CreateSnapshot() + }); + + var act = async () => + await manager.EnsureAsync(force: true); + + await act.Should() + .ThrowAsync() + .WithMessage("Network failure"); + + manager.State.IsAuthenticated.Should().BeFalse(); + + await manager.EnsureAsync(force: true); + + manager.State.IsAuthenticated.Should().BeTrue(); + manager.State.IsStale.Should().BeFalse(); + + flows.Verify( + x => x.ValidateAsync(), + Times.Exactly(2)); + } + } + + [Fact] + public async Task EnsureAsync_When_Network_Fails_Should_Preserve_Authenticated_State() + { + var (manager, flows, _, _) = CreateManager(); + + using (manager) + { + SetupValidSnapshot(flows); + + await manager.EnsureAsync(); + + var originalIdentity = manager.State.Identity; + var originalValidatedAt = manager.State.LastValidatedAt; + + flows.Setup(x => x.ValidateAsync()) + .ThrowsAsync( + new HttpRequestException("Network unavailable")); + + var act = async () => + await manager.EnsureAsync(force: true); + + await act.Should().ThrowAsync(); + + manager.State.IsAuthenticated.Should().BeTrue(); + manager.State.Identity.Should().Be(originalIdentity); + manager.State.LastValidatedAt.Should().Be(originalValidatedAt); + } + } + + [Fact] + public async Task EnsureAsync_When_Active_Result_Has_No_Snapshot_Should_Mark_State_Stale() + { + var (manager, flows, _, _) = CreateManager(); + + using (manager) + { + SetupValidSnapshot(flows); + + await manager.EnsureAsync(); + + flows.Setup(x => x.ValidateAsync()) + .ReturnsAsync(new AuthValidationResult + { + State = SessionState.Active, + Snapshot = null + }); + + await manager.EnsureAsync(force: true); + + manager.State.IsAuthenticated.Should().BeTrue(); + manager.State.IsStale.Should().BeTrue(); + manager.State.NeedsValidation.Should().BeTrue(); + } + } + + + private static AuthStateSnapshot CreateSnapshot(string userKey = "user-1", string displayName = "Original Name") + { + return new AuthStateSnapshot + { + Identity = new AuthIdentitySnapshot + { + UserKey = UserKey.FromString(userKey), + Tenant = TenantKey.Single, + DisplayName = displayName + }, + Claims = ClaimsSnapshot.Empty + }; + } + + private static ( + UAuthStateManager Manager, + Mock Flows, + UAuthClientEvents Events, + Mock Clock) + CreateManager() + { + var flows = new Mock(); + + var client = new Mock(); + client.SetupGet(x => x.Flows).Returns(flows.Object); + + var events = new UAuthClientEvents(); + + var clock = new Mock(); + clock.SetupGet(x => x.UtcNow) + .Returns(new DateTimeOffset(2026, 10, 26, 12, 0, 0, TimeSpan.Zero)); + + return ( + new UAuthStateManager(client.Object, events, clock.Object), + flows, + events, + clock); + } + + private static void SetupValidSnapshot( + Mock flows, + AuthStateSnapshot? snapshot = null) + { + flows.Setup(x => x.ValidateAsync()) + .ReturnsAsync(new AuthValidationResult + { + State = SessionState.Active, + Snapshot = snapshot ?? CreateSnapshot() + }); + } } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthStateTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthStateTests.cs index 06dde860..b414894f 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthStateTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthStateTests.cs @@ -1,95 +1,410 @@ -using CodeBeam.UltimateAuth.Client; +using System.Security.Claims; +using AwesomeAssertions; +using CodeBeam.UltimateAuth.Client; using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.MultiTenancy; -using AwesomeAssertions; -using System.Security.Claims; -using Xunit; +using CodeBeam.UltimateAuth.Users.Contracts; namespace CodeBeam.UltimateAuth.Tests.Unit; public class UAuthStateTests { - private static AuthStateSnapshot CreateSnapshot() - { - var identity = new AuthIdentitySnapshot - { - UserKey = UserKey.FromGuid(Guid.NewGuid()), - Tenant = TenantKey.FromInternal("__single__"), - PrimaryUserName = "admin", - AuthenticatedAt = DateTimeOffset.UtcNow - }; + private static readonly DateTimeOffset Now = + new(2026, 10, 26, 12, 0, 0, TimeSpan.Zero); - var claims = ClaimsSnapshot.From((ClaimTypes.Role, "Admin"), ("uauth:permission", "*")); + private static AuthStateSnapshot Snapshot( + string userKey = "user-1", + string displayName = "Original Name", + params string[] permissions) + { + var claims = ClaimsSnapshot.Create() + .AddRole("Member") + .Add("custom:department", "engineering") + .AddMany("uauth:permission", permissions) + .Build(); return new AuthStateSnapshot { - Identity = identity, + Identity = new AuthIdentitySnapshot + { + UserKey = UserKey.FromString(userKey), + Tenant = TenantKey.Single, + PrimaryUserName = userKey, + DisplayName = displayName + }, Claims = claims }; } + private static UAuthState Authenticated( + params string[] permissions) + { + var state = UAuthState.Anonymous(); + state.ApplySnapshot(Snapshot(permissions: permissions), Now); + return state; + } + [Fact] - public void Anonymous_should_not_be_authenticated() + public void Anonymous_Should_Have_Empty_State() { var state = UAuthState.Anonymous(); state.IsAuthenticated.Should().BeFalse(); + state.NeedsValidation.Should().BeFalse(); + state.IsStale.Should().BeFalse(); state.Identity.Should().BeNull(); + state.LastValidatedAt.Should().BeNull(); + + state.HasPermission("users.read").Should().BeFalse(); + state.HasAnyPermission("users.read", "users.write").Should().BeFalse(); + state.IsInRole("Member").Should().BeFalse(); + state.HasClaim("custom:department", "engineering").Should().BeFalse(); + state.GetClaim("custom:department").Should().BeNull(); } [Fact] - public void ApplySnapshot_should_set_identity_and_claims() + public void ApplySnapshot_Should_Set_Identity_Claims_And_Validation_Time() { var state = UAuthState.Anonymous(); - var snapshot = CreateSnapshot(); + var reasons = new List(); + + state.Changed += reasons.Add; - state.ApplySnapshot(snapshot, DateTimeOffset.UtcNow); + state.ApplySnapshot( + Snapshot(permissions: ["users.read"]), + Now); state.IsAuthenticated.Should().BeTrue(); - state.Identity.Should().NotBeNull(); - state.Claims.Should().BeEquivalentTo(snapshot.Claims); state.IsStale.Should().BeFalse(); + state.NeedsValidation.Should().BeFalse(); + state.LastValidatedAt.Should().Be(Now); + + state.Identity!.DisplayName.Should().Be("Original Name"); + state.HasPermission("users.read").Should().BeTrue(); + state.IsInRole("Member").Should().BeTrue(); + state.HasClaim("custom:department", "engineering").Should().BeTrue(); + state.GetClaim("custom:department").Should().Be("engineering"); + + reasons.Should().ContainSingle() + .Which.Should().Be(UAuthStateChangeReason.Authenticated); + } + + [Fact] + public void UpdateProfile_Should_Update_DisplayName_And_Raise_Patched() + { + var state = Authenticated(); + var reasons = new List(); + + state.Changed += reasons.Add; + + state.UpdateProfile(new UpdateProfileRequest + { + DisplayName = "Updated Name" + }); + + state.Identity!.DisplayName.Should().Be("Updated Name"); + state.Identity.UserKey.Should().Be(UserKey.FromString("user-1")); + + reasons.Should().ContainSingle() + .Which.Should().Be(UAuthStateChangeReason.Patched); + } + + [Fact] + public void UpdateProfile_With_Null_DisplayName_Should_Preserve_Name() + { + var state = Authenticated(); + + state.UpdateProfile(new UpdateProfileRequest + { + FirstName = "Updated" + }); + + state.Identity!.DisplayName.Should().Be("Original Name"); + } + + [Fact] + public void UpdateProfile_Should_Do_Nothing_When_Anonymous() + { + var state = UAuthState.Anonymous(); + var reasons = new List(); + + state.Changed += reasons.Add; + + state.UpdateProfile(new UpdateProfileRequest + { + DisplayName = "New Name" + }); + + state.Identity.Should().BeNull(); + reasons.Should().BeEmpty(); + } + + [Fact] + public void UpdateUserStatus_Should_Raise_Patched() + { + var state = Authenticated(); + var reasons = new List(); + + state.Changed += reasons.Add; + + state.UpdateUserStatus(new ChangeUserStatusSelfRequest + { + NewStatus = SelfAssignableUserStatus.SelfSuspended + }); + + state.Identity!.UserStatus.Should().Be(UserStatus.SelfSuspended); + + reasons.Should().ContainSingle().Which.Should().Be(UAuthStateChangeReason.Patched); + } + + [Fact] + public void UpdateUserStatus_Should_Do_Nothing_When_Anonymous() + { + var state = UAuthState.Anonymous(); + var reasons = new List(); + + state.Changed += reasons.Add; + + state.UpdateUserStatus(new ChangeUserStatusSelfRequest + { + NewStatus = SelfAssignableUserStatus.SelfSuspended + }); + + state.Identity.Should().BeNull(); + reasons.Should().BeEmpty(); + } + + [Fact] + public void MarkStale_Should_Require_Validation() + { + var state = Authenticated(); + var reasons = new List(); + + state.Changed += reasons.Add; + + state.MarkStale(); + + state.IsStale.Should().BeTrue(); + state.NeedsValidation.Should().BeTrue(); + + reasons.Should().ContainSingle() + .Which.Should().Be(UAuthStateChangeReason.MarkedStale); + } + + [Fact] + public void MarkStale_Should_Do_Nothing_When_Anonymous() + { + var state = UAuthState.Anonymous(); + var reasons = new List(); + + state.Changed += reasons.Add; + + state.MarkStale(); + + state.IsStale.Should().BeFalse(); + state.NeedsValidation.Should().BeFalse(); + reasons.Should().BeEmpty(); } [Fact] - public void Clear_should_reset_state() + public void MarkValidated_Should_Reset_Stale_And_Update_Timestamp() + { + var state = Authenticated(); + state.MarkStale(); + + var validatedAt = Now.AddMinutes(5); + var reasons = new List(); + + state.Changed += reasons.Add; + + state.MarkValidated(validatedAt); + + state.IsStale.Should().BeFalse(); + state.NeedsValidation.Should().BeFalse(); + state.LastValidatedAt.Should().Be(validatedAt); + + reasons.Should().ContainSingle() + .Which.Should().Be(UAuthStateChangeReason.Validated); + } + + [Fact] + public void MarkValidated_Should_Do_Nothing_When_Anonymous() { var state = UAuthState.Anonymous(); - var snapshot = CreateSnapshot(); + var reasons = new List(); + + state.Changed += reasons.Add; + + state.MarkValidated(Now); + + state.LastValidatedAt.Should().BeNull(); + reasons.Should().BeEmpty(); + } + + [Fact] + public void Clear_Should_Remove_Identity_Claims_And_Permissions() + { + var state = Authenticated("users.read"); + state.MarkStale(); + + var reasons = new List(); + state.Changed += reasons.Add; - state.ApplySnapshot(snapshot, DateTimeOffset.UtcNow); state.Clear(); state.IsAuthenticated.Should().BeFalse(); state.Identity.Should().BeNull(); - state.Claims.Should().Be(ClaimsSnapshot.Empty); + state.IsStale.Should().BeFalse(); + state.NeedsValidation.Should().BeFalse(); + + state.HasPermission("users.read").Should().BeFalse(); + state.HasAnyPermission("users.read").Should().BeFalse(); + state.IsInRole("Member").Should().BeFalse(); + + reasons.Should().ContainSingle() + .Which.Should().Be(UAuthStateChangeReason.Cleared); } [Fact] - public void IsInRole_should_return_true_when_role_exists() + public void New_Snapshot_Should_Replace_Previous_User_Permissions() { var state = UAuthState.Anonymous(); - var snapshot = CreateSnapshot(); - state.ApplySnapshot(snapshot, DateTimeOffset.UtcNow); + state.ApplySnapshot( + Snapshot("user-1", permissions: ["users.delete"]), + Now); + + state.HasPermission("users.delete").Should().BeTrue(); + + state.Clear(); + + state.ApplySnapshot( + Snapshot("user-2", permissions: ["users.read"]), + Now.AddMinutes(1)); + + state.Identity!.UserKey.Should().Be(UserKey.FromString("user-2")); - state.IsInRole("Admin").Should().BeTrue(); - state.IsInRole("User").Should().BeFalse(); + state.HasPermission("users.read").Should().BeTrue(); + state.HasPermission("users.delete").Should().BeFalse(); + } + + [Theory] + [InlineData("users.read", true)] + [InlineData("users.write", false)] + [InlineData("users.delete", false)] + public void HasPermission_Should_Check_Exact_Permissions( + string permission, + bool expected) + { + var state = Authenticated("users.read"); + + state.HasPermission(permission).Should().Be(expected); } [Fact] - public void ToClaimsPrincipal_should_map_identity_correctly() + public void HasAnyPermission_Should_Return_True_If_Any_Matches() { - var state = UAuthState.Anonymous(); - var snapshot = CreateSnapshot(); + var state = Authenticated("users.read"); + + state.HasAnyPermission( + "users.delete", + "users.read", + "users.write").Should().BeTrue(); + } + + [Fact] + public void HasAnyPermission_Should_Return_False_When_None_Match() + { + var state = Authenticated("users.read"); + + state.HasAnyPermission( + "users.delete", + "users.write").Should().BeFalse(); + } + + [Fact] + public void HasAnyPermission_Should_Return_False_For_Empty_Input() + { + var state = Authenticated("users.read"); + + state.HasAnyPermission().Should().BeFalse(); + } + + [Fact] + public void Touch_Without_State_Update_Should_Only_Request_Render() + { + var state = Authenticated(); + var renders = 0; + var reasons = new List(); + + state.RequestRender = () => renders++; + state.Changed += reasons.Add; + + state.Touch(updateState: false); + + renders.Should().Be(1); + state.IsStale.Should().BeFalse(); + reasons.Should().BeEmpty(); + } + + [Fact] + public void Touch_With_State_Update_Should_Mark_Stale_And_Request_Render() + { + var state = Authenticated(); + var renders = 0; + + state.RequestRender = () => renders++; + + state.Touch(); - state.ApplySnapshot(snapshot, DateTimeOffset.UtcNow); + renders.Should().Be(1); + state.IsStale.Should().BeTrue(); + state.NeedsValidation.Should().BeTrue(); + } + + [Fact] + public void Anonymous_ToClaimsPrincipal_Should_Not_Be_Authenticated() + { + var principal = UAuthState.Anonymous().ToClaimsPrincipal(); + + principal.Identity!.IsAuthenticated.Should().BeFalse(); + principal.Claims.Should().BeEmpty(); + } + + [Fact] + public void Authenticated_ToClaimsPrincipal_Should_Contain_Identity_And_Roles() + { + var state = Authenticated("users.read"); var principal = state.ToClaimsPrincipal(); - principal.Identity!.Name.Should().Be("admin"); - principal.FindFirst(ClaimTypes.NameIdentifier)!.Value.Should().Be(snapshot.Identity.UserKey.Value); - principal.IsInRole("Admin").Should().BeTrue(); + principal.Identity!.IsAuthenticated.Should().BeTrue(); + + principal.FindFirst(ClaimTypes.NameIdentifier)!.Value + .Should().Be("user-1"); + + principal.FindFirst(ClaimTypes.Name)!.Value + .Should().Be("user-1"); + + principal.IsInRole("Member").Should().BeTrue(); + } + + [Theory] + [InlineData("users.read", true)] + [InlineData("users.create", true)] + [InlineData("users.delete", true)] + [InlineData("orders.read", false)] + public void HasPermission_Should_Resolve_Wildcard_Permissions(string permission, bool expected) + { + var state = Authenticated("users.*"); + state.HasPermission(permission).Should().Be(expected); + } + + [Fact] + public void HasAnyPermission_Should_Reject_Unrelated_Permissions() + { + var state = Authenticated("users.*"); + state.HasAnyPermission("orders.read", "products.create").Should().BeFalse(); } } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Helpers/UAuthClientTestBase.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Helpers/UAuthClientTestBase.cs index 7ceb6c4e..9d7903ce 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Helpers/UAuthClientTestBase.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Helpers/UAuthClientTestBase.cs @@ -16,6 +16,7 @@ namespace CodeBeam.UltimateAuth.Tests.Unit.Helpers; public abstract class UAuthClientTestBase { protected readonly Mock Request = new(); + protected readonly Mock ResourceRequest = new(); protected readonly Mock Events = new(); protected IUAuthClient CreateClient( @@ -24,7 +25,8 @@ protected IUAuthClient CreateClient( ICredentialClient? credentials = null, IAuthorizationClient? authorization = null, IFlowClient? flows = null, - IUserIdentifierClient? identifiers = null) + IUserIdentifierClient? identifiers = null, + IResourceClient? resource = null) { var options = Options.Create(new UAuthClientOptions { @@ -40,7 +42,8 @@ protected IUAuthClient CreateClient( users ?? new UAuthUserClient(Request.Object, Events.Object, options), identifiers ?? Mock.Of(), credentials ?? new UAuthCredentialClient(Request.Object, Events.Object, options), - authorization ?? new UAuthAuthorizationClient(Request.Object, Events.Object, options) + authorization ?? new UAuthAuthorizationClient(Request.Object, Events.Object, options), + resource ?? new UAuthResourceClient(ResourceRequest.Object) ); } @@ -105,7 +108,8 @@ protected IUAuthClient CreateCredentialClient() users: Mock.Of(), identifiers: Mock.Of(), credentials: new UAuthCredentialClient(Request.Object, Events.Object, options), - authorization: Mock.Of()); + authorization: Mock.Of(), + resource: Mock.Of()); } protected IUAuthClient CreateUserClient() @@ -124,7 +128,8 @@ protected IUAuthClient CreateUserClient() users: new UAuthUserClient(Request.Object, Events.Object, options), identifiers: Mock.Of(), credentials: Mock.Of(), - authorization: Mock.Of()); + authorization: Mock.Of(), + resource: Mock.Of()); } protected IUAuthClient CreateIdentifierClient() @@ -143,7 +148,8 @@ protected IUAuthClient CreateIdentifierClient() users: Mock.Of(), identifiers: new UAuthUserIdentifierClient(Request.Object, Events.Object, options), credentials: Mock.Of(), - authorization: Mock.Of()); + authorization: Mock.Of(), + resource: Mock.Of()); } protected static UAuthTransportResult Success() @@ -159,4 +165,4 @@ protected static UAuthTransportResult SuccessJson(T body) Status = 200, Body = JsonSerializer.SerializeToElement(body) }; -} \ No newline at end of file +} diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/javascript/uauth.test.cjs b/tests/CodeBeam.UltimateAuth.Tests.Unit/javascript/uauth.test.cjs new file mode 100644 index 00000000..c62d68dc --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/javascript/uauth.test.cjs @@ -0,0 +1,322 @@ +const test = require("node:test"); +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); +const vm = require("node:vm"); + +const scriptPath = path.resolve( + __dirname, + "../../../src/client/CodeBeam.UltimateAuth.Client.Blazor/TScripts/uauth.js" +); + +const script = fs.readFileSync(scriptPath, "utf8"); + +function createEnvironment(fetchImpl = async () => ({ + ok: true, + status: 200, + headers: { + get: () => "application/json" + }, + text: async () => '{"id":1}' +})) { + const window = { + location: { + origin: "https://localhost:6130" + } + }; + + const document = { + baseURI: "https://localhost:6130/" + }; + + const context = vm.createContext({ + window, + document, + fetch: fetchImpl, + URL, + URLSearchParams, + AbortController, + Map, + Error, + TypeError + }); + + vm.runInContext(script, context); + + window.uauth.setDeviceId("test-device"); + + return window.uauth; +} + +test("validateAuthUrl accepts trusted origin", () => { + const uauth = createEnvironment(); + + const result = uauth.validateAuthUrl( + "https://localhost:6110/auth/login", + "https://localhost:6110" + ); + + assert.equal( + result, + "https://localhost:6110/auth/login" + ); +}); + +test("validateAuthUrl rejects different origin", () => { + const uauth = createEnvironment(); + + assert.throws(() => { + uauth.validateAuthUrl( + "https://evil.example.com/auth/login", + "https://localhost:6110" + ); + }, /trusted UAuthHub origin/); +}); + +test("validateAuthUrl rejects javascript protocol", () => { + const uauth = createEnvironment(); + + assert.throws(() => { + uauth.validateAuthUrl( + "javascript:alert(1)" + ); + }, /protocol/); +}); + +test("validateAuthUrl rejects embedded credentials", () => { + const uauth = createEnvironment(); + + assert.throws(() => { + uauth.validateAuthUrl( + "https://user:password@localhost:6110/auth", + "https://localhost:6110" + ); + }, /credentials/); +}); + +test("resourceRequest rejects different API origin", async () => { + const uauth = createEnvironment(); + + await assert.rejects( + uauth.resourceRequest({ + url: "https://evil.example.com/api/products", + baseAddress: "https://localhost:6120", + method: "GET" + }), + /configured API origin/ + ); +}); + +test("resourceRequest accepts configured API origin", async () => { + let capturedUrl; + + const uauth = createEnvironment(async (url) => { + capturedUrl = url; + + return { + ok: true, + status: 200, + headers: { + get: () => "application/json" + }, + text: async () => "[]" + }; + }); + + const result = await uauth.resourceRequest({ + url: "/api/products", + baseAddress: "https://localhost:6120", + method: "GET" + }); + + assert.equal( + capturedUrl, + "https://localhost:6120/api/products" + ); + + assert.equal(result.ok, true); + assert.equal(result.status, 200); + assert.equal(result.body, "[]"); +}); + +test("resourceRequest rejects unsupported HTTP methods", async () => { + const uauth = createEnvironment(); + + await assert.rejects( + uauth.resourceRequest({ + url: "/api/products", + baseAddress: "https://localhost:6120", + method: "TRACE" + }), + /Unsupported Resource API method/ + ); +}); + +test("resourceRequest rejects GET with body", async () => { + const uauth = createEnvironment(); + + await assert.rejects( + uauth.resourceRequest({ + url: "/api/products", + baseAddress: "https://localhost:6120", + method: "GET", + payload: { id: 1 } + }), + /GET requests cannot contain a body/ + ); +}); + +test("resourceRequest uses include for cross-origin Auto credentials", async () => { + let capturedCredentials; + + const uauth = createEnvironment(async (_, options) => { + capturedCredentials = options.credentials; + + return { + ok: true, + status: 204, + headers: { + get: () => null + }, + text: async () => "" + }; + }); + + await uauth.resourceRequest({ + url: "/api/products", + baseAddress: "https://localhost:6120", + method: "GET", + credentials: "Auto" + }); + + assert.equal(capturedCredentials, "include"); +}); + +test("resourceRequest uses same-origin for same-origin Auto credentials", async () => { + let capturedCredentials; + + const uauth = createEnvironment(async (_, options) => { + capturedCredentials = options.credentials; + + return { + ok: true, + status: 204, + headers: { + get: () => null + }, + text: async () => "" + }; + }); + + await uauth.resourceRequest({ + url: "/api/products", + baseAddress: "https://localhost:6130", + method: "GET", + credentials: "Auto" + }); + + assert.equal(capturedCredentials, "same-origin"); +}); + +test("resourceRequest returns status zero on network error", async () => { + const uauth = createEnvironment(async () => { + throw new TypeError("Failed to fetch"); + }); + + const result = await uauth.resourceRequest({ + url: "/api/products", + baseAddress: "https://localhost:6120", + method: "GET" + }); + + assert.equal(result.ok, false); + assert.equal(result.status, 0); +}); + +test("resourceRequest removes request after completion", async () => { + const uauth = createEnvironment(); + + await uauth.resourceRequest({ + url: "/api/products", + baseAddress: "https://localhost:6120", + method: "GET", + requestId: "request-1" + }); + + assert.equal( + uauth.resourceRequests.has("request-1"), + false + ); +}); + +test("resourceRequest rejects duplicate active request IDs", async () => { + let releaseFetch; + + const pendingFetch = new Promise(resolve => { + releaseFetch = resolve; + }); + + const uauth = createEnvironment(() => pendingFetch); + + const options = { + url: "/api/products", + baseAddress: "https://localhost:6120", + method: "GET", + requestId: "duplicate-id" + }; + + const first = uauth.resourceRequest(options); + + await assert.rejects( + uauth.resourceRequest(options), + /Duplicate Resource request ID/ + ); + + releaseFetch({ + ok: true, + status: 204, + headers: { + get: () => null + }, + text: async () => "" + }); + + await first; +}); + +test("cancelResourceRequest aborts active request", async () => { + let capturedSignal; + + const uauth = createEnvironment((_, options) => { + capturedSignal = options.signal; + + return new Promise((_, reject) => { + options.signal.addEventListener("abort", () => { + const error = new Error("Aborted"); + error.name = "AbortError"; + reject(error); + }); + }); + }); + + const request = uauth.resourceRequest({ + url: "/api/products", + baseAddress: "https://localhost:6120", + method: "GET", + requestId: "cancel-1" + }); + + assert.equal( + uauth.cancelResourceRequest("cancel-1"), + true + ); + + await assert.rejects(request, /Aborted/); + + assert.equal(capturedSignal.aborted, true); + + assert.equal( + uauth.resourceRequests.has("cancel-1"), + false + ); +});