diff --git a/UltimateAuth.slnx b/UltimateAuth.slnx
index c1910ba6..cd8f9614 100644
--- a/UltimateAuth.slnx
+++ b/UltimateAuth.slnx
@@ -31,6 +31,7 @@
+
diff --git a/docs/content/configuration/client-options.md b/docs/content/configuration/client-options.md
index 929b5a64..6ea2e2b0 100644
--- a/docs/content/configuration/client-options.md
+++ b/docs/content/configuration/client-options.md
@@ -1236,4 +1236,4 @@ The current client registration validates options when they are resolved, rather
- **Server Options** — Configure authentication security, sessions, tokens, and endpoints.
- **Advanced Configuration** — Explore effective options, mode resolution, and extension points.
- **Client Authentication** — Implement login, PKCE, refresh, and logout flows.
-- **Client Session Management** — Manage authenticated sessions and revocation.
\ No newline at end of file
+- **Client Session Management** — Manage authenticated sessions and revocation.
diff --git a/docs/content/configuration/server-options.md b/docs/content/configuration/server-options.md
index 1f2c7dcd..ba10573b 100644
--- a/docs/content/configuration/server-options.md
+++ b/docs/content/configuration/server-options.md
@@ -14,6 +14,8 @@ UltimateAuth composes its Core-defined options directly into `UAuthServerOptions
This guide covers the available configuration groups, their defaults, practical examples, and the distinction between configured and effective runtime behavior.
+Built-in HTTP throttling is configured through `options.RateLimiting`. See [Rate Limiting](/docs/security/rate-limiting) for defaults, middleware ownership, proxy configuration and native-policy integration.
+
## 1. Register UltimateAuth Server
Configure the server through `AddUltimateAuthServer()`:
diff --git a/docs/content/getting-started/real-world-setup.md b/docs/content/getting-started/real-world-setup.md
index 4e8d16cd..aa295625 100644
--- a/docs/content/getting-started/real-world-setup.md
+++ b/docs/content/getting-started/real-world-setup.md
@@ -4,7 +4,7 @@ order: 4
group: getting-started
---
-# 🏗️ Real-World Setup
+# Real-World Setup
**One authentication framework. Different deployment models.**
@@ -1008,7 +1008,45 @@ Remember that effective mode defaults and mode-specific overrides are applied af
For detailed configuration behavior, see Server Options and Advanced Configuration.
-## 12. Development and Production Differences
+## 12. Client IP Address & Reverse Proxy
+
+UltimateAuth uses ASP.NET Core's resolved client IP address for device information and IP-based rate limiting. It does not directly trust `X-Forwarded-For` or `CF-Connecting-IP` headers.
+
+If your application runs behind Caddy, Nginx, or another reverse proxy, configure ASP.NET Core to trust that proxy.
+
+In `Program.cs`, before `builder.Build()`:
+
+```csharp
+using System.Net;
+using Microsoft.AspNetCore.HttpOverrides;
+
+builder.Services.Configure(options =>
+{
+ options.ForwardedHeaders =
+ ForwardedHeaders.XForwardedFor |
+ ForwardedHeaders.XForwardedProto;
+
+ // Replace with your actual Caddy or Nginx proxy IP.
+ options.KnownProxies.Add(IPAddress.Parse("172.20.0.5"));
+});
+```
+
+Then enable the middleware **before** UltimateAuth and HTTPS redirection:
+
+```csharp
+app.UseForwardedHeaders();
+
+app.UseHttpsRedirection();
+app.UseUltimateAuthWithAspNetCore();
+```
+
+The example IP `172.20.0.5` represents a trusted reverse proxy, not the visitor's IP address. Replace it with your actual proxy IP. For Docker deployments, ensure the proxy address is stable or configure a narrowly scoped trusted network.
+
+**Important:** Never trust arbitrary forwarded headers or clear the trusted proxy lists to accept all sources. If your application connects directly to Kestrel without a reverse proxy, no forwarded headers configuration is required.
+
+For multi-proxy deployments, configure the complete trusted proxy chain and forwarding behavior according to your infrastructure.
+
+## 13. Development and Production Differences
The samples intentionally prioritize visibility and repeatability.
@@ -1049,7 +1087,7 @@ Configure persistent signing keys for authentication modes that issue JWTs.
Development signing key providers are not a substitute for production key management.
-## 13. Deployment Checklist
+## 14. Deployment Checklist
Before deploying, verify the following:
@@ -1064,9 +1102,10 @@ Before deploying, verify the following:
- [ ] Client and server endpoint URLs match the deployment.
- [ ] Session expiration, refresh, logout, and revocation have been tested.
- [ ] Protected resources enforce server-side authorization.
+- [ ] Reverse proxy headers are configured correctly if applicable.
- [ ] Authentication state and persistence survive the expected application lifecycle.
-## 14. Choosing Your Architecture
+## 15. Choosing Your Architecture
**Start with one server and one client. Connect more clients whenever you need them.**
diff --git a/docs/content/security/rate-limiting.md b/docs/content/security/rate-limiting.md
new file mode 100644
index 00000000..6b1301b9
--- /dev/null
+++ b/docs/content/security/rate-limiting.md
@@ -0,0 +1,638 @@
+---
+title: Rate Limiting
+order: 8
+group: security
+---
+
+# Rate Limiting
+
+UltimateAuth provides built-in HTTP endpoint rate limiting using ASP.NET Core's native rate limiting infrastructure, together with client-side handling of HTTP 429 responses.
+
+Rate limiting helps reduce excessive authentication requests, credential attacks, recovery abuse, and unnecessary session traffic without introducing a separate persistence dependency.
+
+The server enforces request limits. UltimateAuth clients recognize throttled responses, preserve valid authentication state, and defer selected background operations when appropriate.
+
+No additional rate limiting package is required.
+
+## Getting started
+
+Rate limiting is enabled by default when using the integrated ASP.NET Core middleware helper.
+
+```csharp
+builder.Services.AddUltimateAuthServer(options =>
+{
+ options.RateLimiting.Standard.PermitLimit = 240;
+ options.RateLimiting.Login.PermitLimit = 40;
+
+ options.RateLimiting.Endpoints[UAuthEndpoint.Refresh] = new()
+ {
+ PermitLimit = 1200,
+ Window = TimeSpan.FromMinutes(1)
+ };
+});
+```
+
+Configure the middleware pipeline:
+
+```csharp
+app.UseForwardedHeaders();
+app.UseRouting();
+
+app.UseUltimateAuthWithAspNetCore();
+
+app.UseAntiforgery();
+
+app.MapUltimateAuthEndpoints();
+app.MapUAuthHub();
+```
+
+Import `CodeBeam.UltimateAuth.Server.RateLimiting` for rate limiting rules and endpoint identifiers.
+
+Configure forwarded headers only when appropriate for the deployment, using explicitly trusted proxies and networks.
+
+Rate limiting options are also available through the `UltimateAuth:Server:RateLimiting` configuration section. The existing server options binding behavior applies, including configuration-file precedence.
+
+Rate limiting settings are configured at startup. They are not automatically scoped to individual clients or authentication modes.
+
+## Middleware integration
+
+The integrated helper exposes a single method with two optional parameters:
+
+```csharp
+public static IApplicationBuilder UseUltimateAuthWithAspNetCore(
+ this IApplicationBuilder app,
+ bool? enableCors = null,
+ bool useRateLimiting = true)
+```
+
+The default middleware order is:
+
+1. CORS, when enabled or required by UAuthHub
+2. UltimateAuth context middleware
+3. ASP.NET Core authentication
+4. ASP.NET Core rate limiting
+5. ASP.NET Core authorization
+
+This places the rate limiter after authentication, allowing authenticated endpoint policies to use validated UltimateAuth actor information.
+
+### Configuration examples
+
+```csharp
+// Automatic CORS selection; rate limiting enabled
+app.UseUltimateAuthWithAspNetCore();
+
+// Explicit CORS; rate limiting enabled
+app.UseUltimateAuthWithAspNetCore(enableCors: true);
+
+// Automatic CORS; rate limiting disabled
+app.UseUltimateAuthWithAspNetCore(useRateLimiting: false);
+
+// Explicit configuration of both options
+app.UseUltimateAuthWithAspNetCore(enableCors: true, useRateLimiting: true);
+```
+
+`enableCors = null` automatically enables CORS when required by UAuthHub. Setting it to `false` does not remove the host's responsibility to configure CORS when needed.
+
+The standalone UltimateAuth middleware helper does not install ASP.NET Core authentication. If authentication has not already established a validated UltimateAuth actor, rate limiting partitioning falls back to the connection IP.
+
+### Existing application rate limiters
+
+If the application already installs its own `UseRateLimiter()` middleware, disable the helper-owned installation:
+
+```csharp
+app.UseRouting();
+app.UseAuthentication();
+
+app.UseUltimateAuthWithAspNetCore(useRateLimiting: false);
+
+app.UseRateLimiter();
+```
+
+This illustrates middleware ownership rather than a universal pipeline layout. Hosts must preserve the required routing, authentication, CORS, and authorization order and avoid duplicate middleware registration.
+
+UltimateAuth cannot detect a separately installed native rate limiting middleware instance.
+
+Repeated calls to the UltimateAuth helper install at most one helper-owned rate limiter on the same application builder. Other middleware installed by the helper is not deduplicated.
+
+An application builder created with `ApplicationBuilder.New()` has its own middleware installation lifecycle.
+
+Disabling UltimateAuth's built-in endpoint policies does not disable an application-owned global limiter or native application policies.
+
+## Rules and default limits
+
+UltimateAuth uses native sliding-window limiters with six segments and no request queue.
+
+The default rule groups are:
+
+| Rule | Default | Typical operations |
+|---|---|---|
+| Sensitive | 30 requests/minute | Sensitive credential and recovery operations |
+| Standard | 120 requests/minute | Authenticated management operations |
+| Frequent | 600 requests/minute | Validation, refresh, logout, authorization checks |
+| Login | 20 requests/minute/IP | Shared credential-verification budget |
+
+These are starting defaults, not recommended universal production limits. Adjust them based on application traffic, expected concurrency, and deployment conditions.
+
+Group rules act as templates for separate endpoint budgets. For example, validation and refresh do not share a counter merely because both use the Frequent group.
+
+### Shared login budget
+
+The Login rule is shared by:
+
+- Login
+- TryLogin
+- PkceComplete
+- PkceTryComplete
+
+These credential-verification operations share the same budget for a partition.
+
+Configure the shared limit through:
+
+```csharp
+options.RateLimiting.Login.PermitLimit = 40;
+```
+
+An explicit endpoint override replaces that endpoint's rule and separates it from the shared login budget.
+
+### Additional sensitive defaults
+
+Certain operations have stricter defaults than the Sensitive group:
+
+| Operation | Default |
+|---|---|
+| Anonymous registration | 10 requests/10 minutes/IP |
+| Anonymous credential reset initiation | 5 requests/10 minutes/IP |
+| Anonymous credential reset completion | 10 requests/10 minutes/IP |
+| Self identifier verification | 10 requests/10 minutes/IP |
+
+These numeric defaults take precedence over the Sensitive group's numeric settings.
+
+For example, increasing `Sensitive.PermitLimit` does not automatically increase the anonymous reset initiation limit.
+
+Use an explicit endpoint override to change these limits.
+
+## Per-endpoint configuration
+
+Individual endpoint rules can be configured using `RateLimiting.Endpoints`.
+
+For example, limit session validation to two requests per minute:
+
+```csharp
+builder.Services.AddUltimateAuthServer(options =>
+{
+ options.RateLimiting.Endpoints[UAuthEndpoint.Validate] = new()
+ {
+ PermitLimit = 2,
+ Window = TimeSpan.FromMinutes(1)
+ };
+});
+```
+
+The third request within the applicable sliding window receives HTTP 429 when the budget is exhausted.
+
+This example is useful for testing client behavior, but two validations per minute may be too restrictive for a real application with automatic session validation.
+
+Endpoint overrides are complete replacement rules, not partial changes merged with group numeric settings.
+
+### Policy precedence
+
+UltimateAuth resolves endpoint policies in the following order:
+
+1. Native application endpoint/group rate limiting metadata, including native disable metadata
+2. Explicit `Endpoints[id]` replacement
+3. Shared Login rule for credential-verification operations
+4. Special registration, reset, and verification defaults
+5. Endpoint group template
+
+An explicit endpoint override can replace a built-in rule, select an application-owned named policy, or disable the built-in policy for that endpoint.
+
+All UltimateAuth endpoint mappings have explicit classifications. Endpoint registration fails when a newly mapped endpoint is missing its required classification.
+
+## Disabling rate limiting
+
+To disable UltimateAuth's built-in endpoint policies globally:
+
+```csharp
+options.RateLimiting.Enabled = false;
+```
+
+To disable an individual built-in rule:
+
+```csharp
+options.RateLimiting.Endpoints[UAuthEndpoint.Validate] = new()
+{
+ Enabled = false
+};
+```
+
+A disabled UltimateAuth rule does not add native `DisableRateLimiting` metadata.
+
+Application-owned policies and global limiters may still apply.
+
+To prevent the integrated helper from installing the native middleware:
+
+```csharp
+app.UseUltimateAuthWithAspNetCore(useRateLimiting: false);
+```
+
+These controls have different responsibilities:
+
+- `RateLimiting.Enabled` controls UltimateAuth's built-in policies.
+- Individual rule `Enabled` settings control specific built-in rules.
+- `useRateLimiting` controls whether the integrated helper installs the middleware.
+
+## Using application-owned policies
+
+Applications can use their own native ASP.NET Core policies instead of an UltimateAuth built-in rule.
+
+```csharp
+builder.Services.AddRateLimiter(options =>
+{
+ options.AddPolicy("my-auth-policy");
+});
+
+builder.Services.AddUltimateAuthServer(options =>
+{
+ options.RateLimiting.Endpoints[UAuthEndpoint.UserCreateAnonymous] = new()
+ {
+ PolicyName = "my-auth-policy"
+ };
+});
+```
+
+Named policies must be registered with the native rate limiting options used by the middleware.
+
+UltimateAuth's built-in policies are inline native policies and do not reserve application policy names.
+
+Endpoint policies do not automatically compose. Native ASP.NET Core endpoint/group precedence applies.
+
+Native `DisableRateLimiting` metadata also follows ASP.NET Core semantics and can bypass an application global limiter.
+
+### Rejection callbacks
+
+Built-in UltimateAuth inline policies produce their own generic HTTP 429 response.
+
+They do not invoke the application's middleware-level `OnRejected` callback.
+
+Application-owned named policies can use native rejection behavior. When a named policy has no policy-level rejection callback, the effective native middleware `OnRejected` callback applies.
+
+If an application needs complete ownership of rejection responses for a particular endpoint, use an application-owned policy.
+
+UltimateAuth does not replace application global limiters or application rejection settings.
+
+## HTTP 429 responses
+
+When a built-in UltimateAuth policy rejects a request, the server returns:
+
+- HTTP status `429 Too Many Requests`
+- Problem Details content
+- Stable detail code `rate_limited`
+- `Cache-Control: no-store`
+- `Retry-After` when the native limiter supplies retry guidance
+
+A representative response is:
+
+```json
+{
+ "title": "Too Many Requests",
+ "status": 429,
+ "detail": "rate_limited"
+}
+```
+
+The exact Problem Details payload may include additional fields.
+
+`Retry-After` is optional. Applications must not assume that every HTTP 429 response contains it.
+
+Rejected requests do not execute their endpoint handlers. Consequently, a request rejected at this layer does not consume a reset or PKCE proof, rotate refresh credentials, clear logout cookies, or increment login failure counts through the rejected handler.
+
+Authentication and other middleware executing before the rate limiter may still perform work.
+
+### User-facing messages
+
+`rate_limited` is a stable machine-readable error code, not a localized user-facing message.
+
+Applications can map it to a message such as:
+
+> Too many requests. Please wait a moment before trying again.
+
+If a reliable retry delay is available, the application may display it.
+
+Automatic background operations should generally handle throttling silently rather than repeatedly notifying the user.
+
+## Client HTTP 429 handling
+
+UltimateAuth clients recognize HTTP 429 as a temporary request throttle.
+
+A 429 response does not establish that:
+
+- The user's credentials are invalid
+- The session has been revoked
+- A token has expired
+- The user is unauthorized
+- Reauthentication is required
+
+**A throttled request must not, by itself, clear a valid authenticated session.**
+
+UltimateAuth uses its existing client result and error contracts rather than introducing a separate rate limiting exception hierarchy.
+
+### Client result contracts
+
+The following contracts carry throttling information where applicable:
+
+- `UAuthTransportException`
+- `TryLoginResult`
+- `TryPkceLoginResult`
+- `RefreshResult`
+- `UAuthResult`
+- `UAuthProblem`
+
+The stable error code is `rate_limited`.
+
+Depending on the contract, it is exposed through `Code`, `ErrorCode`, or `Problem.Detail`.
+
+HTTP status remains 429.
+
+`UAuthResult.Http.IsTooManyRequests` distinguishes throttling from unauthorized and forbidden results.
+
+`RetryAfter` provides an optional `TimeSpan` where supported.
+
+Transport DTOs preserve the raw `Retry-After` header across the JavaScript bridge.
+
+The existing `IUAuthTryResult` interface provides default metadata properties so existing implementations are not required to add new members.
+
+The public `IFlowClient` method signatures remain unchanged.
+
+## Client behavior by operation
+
+| Operation | HTTP 429 behavior |
+|---|---|
+| Validation | Throws a transport exception with status 429 and throttling metadata |
+| Blazor authentication state | Preserves the current identity, claims, and last successful validation time |
+| Refresh | Returns an unsuccessful result with status 429 and `Outcome = None` |
+| Automatic refresh | Defers subsequent attempts without triggering reauthentication |
+| Login preview | Returns a distinguishable throttled result rather than invalid credentials |
+| PKCE preview | Returns a distinguishable throttled result |
+| Try-and-commit | Does not submit the final commit after a throttled preview |
+| Logout | Does not report successful logout or navigate on a confirmed 429 |
+| Authorization | Returns a throttled failure, not a Forbidden result |
+| Profile, user, credential, and recovery operations | Return unsuccessful results without publishing success events |
+| Resource requests | Preserve HTTP 429 information without changing authentication state |
+
+### Validation
+
+`ValidateAsync` throws `UAuthTransportException` for HTTP 429 before attempting to deserialize a successful authentication result.
+
+Blazor's `UAuthStateManager.EnsureAsync` recognizes this condition and preserves the current authentication snapshot.
+
+It does not:
+
+- Clear the current identity
+- Remove claims
+- Treat the user as anonymous
+- Mark the session freshly validated
+- Update the last successful validation timestamp
+
+Subsequent validation attempts, including forced validation calls, are deferred during the cooldown.
+
+A genuine invalid, expired, or revoked validation result continues to follow the normal state-clearing behavior.
+
+### Refresh
+
+A throttled refresh returns an unsuccessful `RefreshResult` with:
+
+```text
+StatusCode: 429
+ErrorCode: rate_limited
+Outcome: None
+```
+
+Misleading authentication-outcome headers are ignored on a throttled response.
+
+Repeated refresh calls during the cooldown return the throttled result without issuing another HTTP request.
+
+Automatic refresh scheduling respects both the configured refresh interval and the throttling deadline.
+
+HTTP 429 does not cause reauthentication events, logout, or automatic navigation.
+
+A genuine refresh HTTP 401 can still produce `ReauthRequired`.
+
+### Login and PKCE
+
+Login and PKCE preview operations expose throttling distinctly from invalid credentials.
+
+When a TryAndCommit preview is throttled, the client does not submit the final commit form.
+
+However, final browser-navigation form submissions retain their existing navigation semantics.
+
+For `LoginAsync`, DirectCommit, `CompletePkceLoginAsync`, and the final TryAndCommit submission, the browser owns the resulting HTTP response.
+
+If that final POST receives HTTP 429, the browser may display the server response page.
+
+The JavaScript client cannot reliably return the final navigated response's `Retry-After` metadata through the existing navigation contract.
+
+Preview success or form submission completion must not be interpreted as confirmation that a session was issued.
+
+### Logout
+
+Logout uses an observed form POST so the client can distinguish a confirmed successful response from HTTP 429.
+
+On HTTP 429:
+
+- No successful logout event is published.
+- No success navigation occurs.
+- The client does not clear authentication state solely because of throttling.
+
+On confirmed success, the existing logout event can update client authentication state.
+
+Logout navigation is different from login navigation: the observed request can follow a successful server redirect and then navigate without replaying the logout POST.
+
+Cross-origin logout still requires compatible browser credentials, CORS configuration, and redirect behavior.
+
+A network or redirect failure may leave the server-side logout outcome uncertain. The SDK reports an error rather than claiming success or automatically replaying the operation.
+
+Applications can explicitly revalidate when connectivity permits.
+
+### Custom client transports
+
+`IUAuthRequestClient` supports an observed form request overload:
+
+```csharp
+SendFormAsync(endpoint, form, navigateOnSuccess, ct)
+```
+
+The overload has a default implementation that delegates to the original form request method.
+
+Existing custom transport implementations therefore retain their original interface requirements.
+
+However, the default implementation does not provide navigation behavior automatically.
+
+Custom adapters that support logout navigation should implement the observed overload and preserve HTTP status and `Retry-After` metadata.
+
+## Retry-After and cooldown behavior
+
+UltimateAuth clients support standard `Retry-After` values expressed as:
+
+- Delta seconds
+- HTTP-date values
+
+Parsing uses invariant culture and UTC.
+
+Missing, malformed, or inaccessible values remain null in application results. Past dates produce a zero retry delay.
+
+For internal maintenance scheduling, the SDK uses:
+
+- A conservative 30-second fallback when retry guidance is unavailable
+- A one-second minimum to avoid immediate retry loops
+- Bounded timer intervals when waiting for long delays
+
+Long retry guidance is honored by the current implementation.
+
+The SDK does not automatically retry arbitrary application operations.
+
+In particular, it does not automatically replay login, logout, recovery, credential mutations, or administrative changes after throttling.
+
+For cross-origin browser clients, the server must expose `Retry-After` through CORS if JavaScript is expected to read that header.
+
+When the header is not accessible, the client follows its missing-header behavior.
+
+## Authentication state safety
+
+Rate limiting is deliberately separated from authentication validity.
+
+A throttled request does not:
+
+- Extend token expiration
+- Renew credentials
+- Update successful validation timestamps
+- Create a trusted identity
+- Revoke an existing session
+- Clear browser storage automatically
+- Publish successful mutation events
+
+`UAuthStateManager` remains responsible for authentication state transitions.
+
+`SessionCoordinator` remains responsible for automatic refresh scheduling and genuine reauthentication handling.
+
+`UAuthAuthenticationStateProvider` projects the retained state to Blazor.
+
+Explicit local actions such as `Clear` and `OnLogoutAsync`, and authoritative revocation or mutation events, retain their existing behavior.
+
+## Partitioning and trust boundaries
+
+UltimateAuth chooses rate limiting partitions using trusted server-side information.
+
+Credential entry, anonymous registration and recovery, identifier availability, PKCE, Hub entry, and self-proof verification use the connection IP even when a cookie is present.
+
+Other operations use validated UltimateAuth tenant and actor information when available, otherwise the connection IP.
+
+The following do not independently establish a trusted authenticated partition:
+
+- Mere presence of a session cookie
+- Usernames or target user identifiers
+- Unvalidated tokens
+- Device identifiers
+- Client profile hints
+- Raw tenant selectors
+- Principals from unrelated authentication authorities
+
+Authenticated actor partitions include validated tenant identity.
+
+Anonymous IP budgets span tenants, preventing a client from multiplying its quota simply by rotating tenant selectors.
+
+IPv4-mapped IPv6 addresses are normalized to IPv4.
+
+Requests without an available connection address share an unknown-address partition.
+
+UltimateAuth does not trust arbitrary forwarding headers.
+
+Configure trusted proxies, forwarded headers, and ingress restrictions at the application or infrastructure level.
+
+Applications using another identity authority should configure an appropriate native policy rather than assuming UltimateAuth can validate that authority's principal.
+
+## Persistence and distributed deployment
+
+Built-in counters are maintained in application memory.
+
+They are not stored in UltimateAuth's persistence providers, EF Core, or a database.
+
+This means:
+
+- Counters reset when the application process restarts.
+- Each application instance maintains its own counters.
+- Browser refreshes do not reset server counters.
+- Sliding windows replenish capacity over time.
+- Multiple application instances do not automatically share a global request budget.
+
+For distributed deployments requiring fleet-wide limits, use an ingress gateway, WAF, or custom native policy backed by shared infrastructure.
+
+Built-in native partitions have idle cleanup, but do not provide a hard partition-cardinality bound.
+
+Large-scale IP rotation, IPv6 address churn, NAT sharing, and botnet traffic require additional operational consideration.
+
+Rate limiting does not replace account lockout, proof-attempt limits, anti-enumeration responses, or other authentication security controls.
+
+Direct service calls and Blazor Server circuit operations are outside HTTP endpoint throttling unless the application protects them separately.
+
+## Testing rate limiting
+
+A simple development test is to temporarily limit validation to two requests per minute:
+
+```csharp
+builder.Services.AddUltimateAuthServer(options =>
+{
+ options.RateLimiting.Endpoints[UAuthEndpoint.Validate] = new()
+ {
+ PermitLimit = 2,
+ Window = TimeSpan.FromMinutes(1)
+ };
+});
+```
+
+After starting the application and authenticating, send repeated requests to:
+
+```http
+POST /auth/validate
+```
+
+The expected server behavior is:
+
+| Request | Expected result |
+|---|---|
+| First permitted request | Normal validation |
+| Second permitted request | Normal validation |
+| Request exceeding the budget | HTTP 429 |
+| Request after capacity replenishes | Normal validation |
+
+Actual results depend on the partition's existing traffic and sliding-window state.
+
+When testing through an UltimateAuth client, the client may defer additional validation requests after receiving HTTP 429. This is expected and should be tested separately from server enforcement.
+
+Also verify that:
+
+- Authentication state remains intact after throttling.
+- Other independently limited operations continue to work.
+- Automatic refresh does not enter an immediate retry loop.
+- Login preview does not confuse throttling with invalid credentials.
+- Logout does not report success after a confirmed 429.
+- No successful mutation event is published for a rejected operation.
+
+Remove temporary low-limit overrides before deploying to production.
+
+### Client IP Address & Reverse Proxies
+
+UltimateAuth uses ASP.NET Core's resolved client IP address for IP-based rate limiting. It does not directly trust forwarded IP headers.
+
+When deploying behind Caddy, Nginx, Cloudflare, or another reverse proxy, ensure that ASP.NET Core's Forwarded Headers Middleware is correctly configured with trusted proxies or networks.
+
+Without proper configuration, multiple users may be identified by the same proxy IP address and share rate limits.
+
+See [Client IP Address & Reverse Proxy Setup](/docs/getting-started/real-world-setup#10-client-ip-address--reverse-proxy) for configuration examples.
+
+## Related documentation
+
+- [Server Options](/docs/configuration/server-options)
+- [Client Options](/docs/configuration/client-options)
+- [Authentication Model](/docs/concepts/authentication-model)
+
+Rate limiting is one layer of UltimateAuth's security model. It should be combined with appropriate credential policies, session validation, trusted proxy configuration, and deployment-level protection.
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 d5a6794b..c11b7c67 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
@@ -414,6 +414,6 @@
"Level": 0
}
],
- "SourceHash": "FE74B982178EDB99198EC90811ED91660E95FD2EE3D445361B61236F850B608A",
+ "SourceHash": "8B65E4A762A493F3F5C6A4F6978EE5FCCCE7678BD388239B876754178E5EE3D0",
"BuilderVersion": "1"
}
\ No newline at end of file
diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/configuration/server-options.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/configuration/server-options.json
index a5f73fbd..841d876f 100644
--- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/configuration/server-options.json
+++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/configuration/server-options.json
@@ -1,7 +1,7 @@
{
"Slug": "configuration/server-options",
"Title": "Server Options",
- "Html": "\n\u003Cp\u003E\u003Ccode\u003EUAuthServerOptions\u003C/code\u003E is the primary configuration surface for UltimateAuth Server.\u003C/p\u003E\n\u003Cp\u003EIt controls authentication behavior, session lifecycles, token issuance, credential security, tenant resolution, endpoint exposure, and integration with UAuthHub.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth composes its Core-defined options directly into \u003Ccode\u003EUAuthServerOptions\u003C/code\u003E. When using UltimateAuth Server, you configure both shared authentication behavior and server-specific features through the same options object.\u003C/p\u003E\n\u003Cp\u003EThis guide covers the available configuration groups, their defaults, practical examples, and the distinction between configured and effective runtime behavior.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022register-ultimateauth-server\u0022\u003E1. Register UltimateAuth Server\u003C/h2\u003E\n\u003Cp\u003EConfigure the server through \u003Ccode\u003EAddUltimateAuthServer()\u003C/code\u003E:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Login.MaxFailedAttempts = 5;\n options.Session.Lifetime = TimeSpan.FromDays(7);\n options.Token.AccessTokenLifetime = TimeSpan.FromMinutes(10);\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe configuration delegate receives:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthServerOptions\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe server registration also supports an optional policy configuration delegate.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(\n configure: options =\u0026gt;\n {\n options.Login.MaxFailedAttempts = 5;\n },\n configurePolicies: policies =\u0026gt;\n {\n // Register application access policies.\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe server automatically registers its Core infrastructure. You do not need to call \u003Ccode\u003EAddUltimateAuth()\u003C/code\u003E separately.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configuration-from-appsettings.json\u0022\u003EConfiguration from appsettings.json\u003C/h3\u003E\n\u003Cp\u003EUltimateAuth Server binds the configuration section:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EUltimateAuth:Server\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EJSON\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-json\u0022\u003E{\n \u0026quot;UltimateAuth\u0026quot;: {\n \u0026quot;Server\u0026quot;: {\n \u0026quot;Login\u0026quot;: {\n \u0026quot;MaxFailedAttempts\u0026quot;: 5,\n \u0026quot;LockoutDuration\u0026quot;: \u0026quot;00:15:00\u0026quot;\n },\n \u0026quot;Session\u0026quot;: {\n \u0026quot;Lifetime\u0026quot;: \u0026quot;7.00:00:00\u0026quot;\n },\n \u0026quot;Token\u0026quot;: {\n \u0026quot;AccessTokenLifetime\u0026quot;: \u0026quot;00:10:00\u0026quot;\n }\n }\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EASP.NET Core represents \u003Ccode\u003ETimeSpan\u003C/code\u003E values using strings such as \u003Ccode\u003E00:15:00\u003C/code\u003E for fifteen minutes and \u003Ccode\u003E7.00:00:00\u003C/code\u003E for seven days.\u003C/p\u003E\n\u003Cp\u003EThe configuration section is bound after the registration delegate, so configuration provider values take precedence for matching properties.\u003C/p\u003E\n\u003Cp\u003EFor configuration source ordering and environment-specific overrides, see \u003Ca href=\u0022/docs/configuration/configuration\u0022\u003EConfiguration Overview\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022server-options-architecture\u0022\u003E2. Server Options Architecture\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthServerOptions\u003C/code\u003E contains two categories of configuration.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022core-defined-options\u0022\u003ECore-defined options\u003C/h3\u003E\n\u003Cp\u003EThese types are defined in UltimateAuth Core and composed into the server options:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EType\u003C/th\u003E\n\u003Cth\u003EPurpose\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELogin\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthLoginOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELogin attempts and lockout behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESession\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthSessionOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESession lifetime and lifecycle\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EToken\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthTokenOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAccess and refresh token behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPkce\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthPkceOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPKCE authorization code lifetime\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMultiTenant\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthMultiTenantOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETenant identification and resolution\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPagination\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthPaginationOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPagination defaults and limits\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEvents\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthEvents\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication lifecycle callbacks\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022server-specific-options\u0022\u003EServer-specific options\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EPurpose\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAllowedModes\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERestrict permitted authentication modes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EHubDeploymentMode\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EDescribe the relationship between UAuthHub and the application\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EHub\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EHub integration and flow settings\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECookie\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECookie names, security attributes, and lifetime behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESessionResolution\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESession identifier sources and precedence\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEndpoints\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEndpoint groups and disabled actions\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPrimaryCredential\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPrimary credential selection for UI and API requests\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAuthResponse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication credential delivery and redirect behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECredentialSessionPolicy\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESession invalidation after credential changes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EResetCredential\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECredential recovery code and token settings\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdentifiers\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifier behavior, normalization, and uniqueness\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdentifierValidation\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUsername, email, and phone validation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELoginIdentifiers\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifier types allowed for login\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserProfile\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUser profile capabilities\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ENavigation\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication navigation behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDiagnostics\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional diagnostic response details\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EAdvanced configuration hooks are also available, including \u003Ccode\u003EConfigureMode()\u003C/code\u003E and \u003Ccode\u003EOnConfigureEndpoints\u003C/code\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022login-options\u0022\u003E3. Login Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Login\u003C/code\u003E controls login attempt limits, lockout behavior, and related security settings.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Login.MaxFailedAttempts = 5;\n options.Login.LockoutDuration = TimeSpan.FromMinutes(15);\n options.Login.FailureWindow = TimeSpan.FromMinutes(15);\n options.Login.ExtendLockOnFailure = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-properties\u0022\u003EAvailable Properties\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxFailedAttempts\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E10\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EFailed login threshold; \u003Ccode\u003E0\u003C/code\u003E disables lockout\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELockoutDuration\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E15 minutes\u003C/td\u003E\n\u003Ctd\u003ELockout duration\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EFailureWindow\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E15 minutes\u003C/td\u003E\n\u003Ctd\u003EWindow for counting failed attempts\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EExtendLockOnFailure\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EExtend lockout when another attempt fails during lockout\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIncludeFailureDetails\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EInclude detailed authentication failure information\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETryLoginDuration\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E180 seconds\u003C/td\u003E\n\u003Ctd\u003EDuration associated with the try-login flow\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-restrict-login-failure-details\u0022\u003EExample: Restrict Login Failure Details\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Login.IncludeFailureDetails = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis setting is useful when an application does not want to expose detailed lockout information to callers.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022important\u0022\u003EImportant\u003C/h3\u003E\n\u003Cp\u003ELockout configuration is not equivalent to complete network-level rate limiting.\u003C/p\u003E\n\u003Cp\u003EProduction deployments should also consider infrastructure protections against abusive traffic and distributed login attempts.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022session-options\u0022\u003E4. Session Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Session\u003C/code\u003E controls session lifetime, expiration, sliding behavior, and session-chain limits.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Session.Lifetime = TimeSpan.FromDays(7);\n options.Session.SlidingExpiration = true;\n options.Session.IdleTimeout = TimeSpan.FromDays(3);\n options.Session.MaxLifetime = TimeSpan.FromDays(30);\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022session-lifetime-properties\u0022\u003ESession Lifetime Properties\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELifetime\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E7 days\u003C/td\u003E\n\u003Ctd\u003EStandard session lifetime\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxLifetime\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional absolute lifetime limit\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESlidingExpiration\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAllow eligible activity or refresh operations to extend expiration\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdleTimeout\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional inactivity timeout\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETouchInterval\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E5 minutes\u003C/td\u003E\n\u003Ctd\u003EMinimum interval between last-seen updates\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThese settings have different purposes.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003ELifetime\u003C/code\u003E determines the standard lifetime assigned to a session.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EMaxLifetime\u003C/code\u003E establishes an optional hard boundary that cannot be extended indefinitely.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EIdleTimeout\u003C/code\u003E limits how long a session can remain inactive.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003ETouchInterval\u003C/code\u003E reduces unnecessary persistence operations caused by frequent activity updates.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022session-chain-properties\u0022\u003ESession-Chain Properties\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxChainsPerUser\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E0\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReserved user-level chain limit\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxSessionsPerChain\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E100\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESession-chain rotation limit\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxChainsPerPlatform\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReserved platform-specific chain limits\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPlatformCategories\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReserved platform group definitions\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxChainsPerCategory\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReserved category-specific chain limits\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableIpBinding\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReserved IP binding setting\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableUserAgentBinding\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReserved User-Agent binding setting\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDeviceMismatchBehavior\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EReject\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReserved device mismatch behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Cstrong\u003ECurrent implementation limitation:\u003C/strong\u003E The source identifies several device limits and binding settings as reserved for future enforcement. Setting these properties must not be treated as proof that the corresponding protection is active.\u003C/p\u003E\n\u003Cp\u003EFor example, do not rely on:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.Session.MaxChainsPerUser = 1;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Eas a production guarantee that only one device chain can exist unless the current runtime implementation has been verified to enforce that restriction.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022mode-specific-session-defaults\u0022\u003EMode-Specific Session Defaults\u003C/h3\u003E\n\u003Cp\u003ESession settings can also be affected by authentication mode defaults.\u003C/p\u003E\n\u003Cp\u003EFor example, \u003Ccode\u003EPureOpaque\u003C/code\u003E supplies a longer idle timeout and touch interval when those values have not been configured.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EHybrid\u003C/code\u003E explicitly sets \u003Ccode\u003ETouchInterval\u003C/code\u003E to \u003Ccode\u003Enull\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003ESee \u003Ca href=\u0022/docs/configuration/advanced-configuration\u0022\u003EAdvanced Configuration\u003C/a\u003E for the effective-options resolution order.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022token-options\u0022\u003E5. Token Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Token\u003C/code\u003E configures access and refresh token behavior.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Token.Issuer = \u0026quot;MyApplication\u0026quot;;\n options.Token.Audience = \u0026quot;MyApplicationApi\u0026quot;;\n options.Token.AccessTokenLifetime = TimeSpan.FromMinutes(10);\n options.Token.RefreshTokenLifetime = TimeSpan.FromDays(7);\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-properties-1\u0022\u003EAvailable Properties\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIssueJwt\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEnable JWT issuance where supported by the effective mode\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIssueOpaque\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEnable opaque token issuance where supported\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIssueRefresh\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEnable refresh token issuance where supported\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAccessTokenLifetime\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E10 minutes\u003C/td\u003E\n\u003Ctd\u003EAccess token lifetime\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERefreshTokenLifetime\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E7 days\u003C/td\u003E\n\u003Ctd\u003ERefresh token lifetime\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EOpaqueIdBytes\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E32\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERandom byte length used for opaque token identifiers\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERefreshTokenConcurrentRequestWindow\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E2 seconds\u003C/td\u003E\n\u003Ctd\u003EConcurrent duplicate refresh classification window\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIssuer\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuth\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EJWT issuer\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAudience\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthClient\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EJWT audience\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAddJwtIdClaim\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EInclude a JWT identifier claim\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EKeyId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional signing key identifier\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022refresh-token-concurrency-window\u0022\u003ERefresh Token Concurrency Window\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Token.RefreshTokenConcurrentRequestWindow = TimeSpan.FromSeconds(2);\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis option controls how the server distinguishes a closely timed duplicate refresh request from a confirmed replay.\u003C/p\u003E\n\u003Cp\u003EA duplicate refresh request is still rejected.\u003C/p\u003E\n\u003Cp\u003EThe window affects the replay-response classification, including whether the token family should be revoked.\u003C/p\u003E\n\u003Cp\u003EIt does \u003Cstrong\u003Enot\u003C/strong\u003E allow the same consumed refresh token to be used successfully more than once.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022mode-defaults-may-override-token-values\u0022\u003EMode Defaults May Override Token Values\u003C/h3\u003E\n\u003Cp\u003EThe effective \u003Ccode\u003EHybrid\u003C/code\u003E configuration explicitly sets:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EToken.AccessTokenLifetime = TimeSpan.FromMinutes(10);\nToken.RefreshTokenLifetime = TimeSpan.FromDays(7);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ETherefore, a different value configured globally may be replaced when the effective mode is resolved.\u003C/p\u003E\n\u003Cp\u003ETo customize the lifetime for \u003Ccode\u003EHybrid\u003C/code\u003E, use:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.ConfigureMode(UAuthMode.Hybrid, mode =\u0026gt;\n {\n mode.Token.AccessTokenLifetime = TimeSpan.FromMinutes(5);\n });\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis distinction is essential when configuring token lifetimes.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022pkce-options\u0022\u003E6. PKCE Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Pkce\u003C/code\u003E configures the lifetime of PKCE authorization codes.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Pkce.AuthorizationCodeLifetimeSeconds = 120;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAuthorizationCodeLifetimeSeconds\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E120\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELifetime of a PKCE authorization code\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EPKCE is particularly important for browser-based clients such as Blazor WebAssembly.\u003C/p\u003E\n\u003Cp\u003EA shorter authorization code lifetime reduces the period during which an intercepted or exposed code could potentially be used.\u003C/p\u003E\n\u003Cp\u003EHowever, the value must remain long enough for legitimate authorization exchanges to complete.\u003C/p\u003E\n\u003Cp\u003EThe server\u0027s PKCE configuration is separate from the client\u0027s PKCE flow settings.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022authentication-modes\u0022\u003E7. Authentication Modes\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth supports authentication mode resolution based on the client profile.\u003C/p\u003E\n\u003Cp\u003EThe server exposes:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.AllowedModes\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis property restricts the modes the server is permitted to use.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022restrict-allowed-modes\u0022\u003ERestrict Allowed Modes\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.AllowedModes =\n [\n UAuthMode.PureOpaque,\n UAuthMode.Hybrid\n ];\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EAllowedModes\u003C/code\u003E is a restriction, not a mode selector.\u003C/p\u003E\n\u003Cp\u003EThe effective mode is resolved by \u003Ccode\u003EIEffectiveAuthModeResolver\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022default-mode-resolution\u0022\u003EDefault Mode Resolution\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EClient profile\u003C/th\u003E\n\u003Cth\u003EDefault resolved mode\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EBlazorServer\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EPureOpaque\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EWebServer\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EHybrid\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EBlazorWasm\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EHybrid\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaui\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EHybrid\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EApi\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EPureJwt\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe current implementation has mode templates for \u003Ccode\u003EPureOpaque\u003C/code\u003E, \u003Ccode\u003EHybrid\u003C/code\u003E, \u003Ccode\u003ESemiHybrid\u003C/code\u003E, and \u003Ccode\u003EPureJwt\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EHowever, \u003Ccode\u003ESemiHybrid\u003C/code\u003E and \u003Ccode\u003EPureJwt\u003C/code\u003E are not yet accepted as enabled modes by the current server options validator.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EDo not enable an unsupported mode merely because it exists in the enum or has a mode-default template.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EThe default \u003Ccode\u003EApi\u003C/code\u003E profile mapping to \u003Ccode\u003EPureJwt\u003C/code\u003E is a current compatibility limitation to consider when deploying API-profile authentication flows.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configure-mode-specific-behavior\u0022\u003EConfigure Mode-Specific Behavior\u003C/h3\u003E\n\u003Cp\u003EUse \u003Ccode\u003EConfigureMode()\u003C/code\u003E for behavior that should differ by mode.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.ConfigureMode(UAuthMode.PureOpaque, mode =\u0026gt;\n {\n mode.Session.IdleTimeout = TimeSpan.FromDays(7);\n });\n\n options.ConfigureMode(UAuthMode.Hybrid, mode =\u0026gt;\n {\n mode.Token.AccessTokenLifetime = TimeSpan.FromMinutes(5);\n });\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe mode override runs after the built-in mode defaults.\u003C/p\u003E\n\u003Cp\u003ESee \u003Ca href=\u0022/docs/configuration/advanced-configuration\u0022\u003EAdvanced Configuration\u003C/a\u003E for details.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022endpoint-options\u0022\u003E8. Endpoint Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Endpoints\u003C/code\u003E controls endpoint paths, feature groups, and individual action restrictions.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022base-path\u0022\u003EBase Path\u003C/h3\u003E\n\u003Cp\u003EThe default server endpoint base path is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003E/auth\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EConfigure it through:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Endpoints.BasePath = \u0026quot;/auth\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EChanging the base path can help avoid route conflicts when integrating UltimateAuth into an existing application.\u003C/p\u003E\n\u003Cp\u003EThe client must use compatible endpoint paths.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022endpoint-groups\u0022\u003EEndpoint Groups\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003Cth\u003EPurpose\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAuthentication\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication endpoints\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPkce\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPKCE endpoints\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESession\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESession management endpoints\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserLifecycle\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUser lifecycle endpoints\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserProfile\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUser profile endpoints\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserIdentifier\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifier management endpoints\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECredentials\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECredential management endpoints\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAuthorization\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthorization endpoints\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Endpoints.Authentication = true;\n options.Endpoints.Session = true;\n options.Endpoints.Authorization = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EDisabling an endpoint group controls endpoint exposure. It does not automatically remove the underlying domain services or invalidate previously issued credentials.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022disable-individual-actions\u0022\u003EDisable Individual Actions\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EDisabledActions\u003C/code\u003E is a collection of action identifiers:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Endpoints.DisabledActions.Add(\u0026quot;users.create.anonymous\u0026quot;);\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe string must match the action identifier used by the server\u0027s endpoint registration and action policy.\u003C/p\u003E\n\u003Cp\u003EDo not pass a C# constant name as a string unless that exact text is also the registered action value.\u003C/p\u003E\n\u003Cp\u003EFor example, these are not necessarily equivalent:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003E\u0026quot;UAuthActions.Users.Create.Anonymous\u0026quot;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Eand:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003E\u0026quot;users.create.anonymous\u0026quot;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUse the actual action value defined by the corresponding \u003Ccode\u003EUAuthActions\u003C/code\u003E constant.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022endpoint-hardening\u0022\u003EEndpoint Hardening\u003C/h3\u003E\n\u003Cp\u003EDisable endpoints that are not required by the application.\u003C/p\u003E\n\u003Cp\u003EFor example, an application that delegates user provisioning to administrators may choose to disable anonymous user creation.\u003C/p\u003E\n\u003Cp\u003EEndpoint exposure is a separate concern from authorization: an exposed endpoint must still enforce the applicable security policy.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022session-resolution\u0022\u003E9. Session Resolution\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.SessionResolution\u003C/code\u003E controls how incoming requests provide session identifiers.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth supports four resolution sources:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EBearer credentials.\u003C/li\u003E\n\u003Cli\u003ECustom headers.\u003C/li\u003E\n\u003Cli\u003ECookies.\u003C/li\u003E\n\u003Cli\u003EQuery parameters.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022default-configuration\u0022\u003EDefault Configuration\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableBearer\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableHeader\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableCookie\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableQuery\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EHeaderName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EX-UAuth-Session\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EQueryParameterName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Esession_id\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe default precedence order is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EBearer\nHeader\nCookie\nQuery\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configure-resolution-sources\u0022\u003EConfigure Resolution Sources\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.SessionResolution.EnableBearer = true;\n options.SessionResolution.EnableHeader = false;\n options.SessionResolution.EnableCookie = true;\n options.SessionResolution.EnableQuery = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis example disables custom-header and query-string session resolution.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022change-resolution-order\u0022\u003EChange Resolution Order\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.SessionResolution.Order =\n [\n \u0026quot;Cookie\u0026quot;,\n \u0026quot;Bearer\u0026quot;,\n \u0026quot;Header\u0026quot;,\n \u0026quot;Query\u0026quot;\n ];\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EResolution order matters when a request contains multiple candidate credentials.\u003C/p\u003E\n\u003Cp\u003EThe selected credential must still pass authentication and session validation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-recommendation\u0022\u003ESecurity Recommendation\u003C/h3\u003E\n\u003Cp\u003EAvoid query-string credentials unless the application has a specific, well-understood requirement.\u003C/p\u003E\n\u003Cp\u003EURLs may be recorded in browser history, reverse-proxy logs, analytics systems, and other infrastructure.\u003C/p\u003E\n\u003Cp\u003EFor most browser and API integrations, cookies or authorization headers are preferable.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022cookie-configuration\u0022\u003E10. Cookie Configuration\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Cookie\u003C/code\u003E controls cookies used for authentication credentials.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth defines three cookie groups:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.Cookie.Session\noptions.Cookie.AccessToken\noptions.Cookie.RefreshToken\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022default-cookie-names\u0022\u003EDefault Cookie Names\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003ECookie\u003C/th\u003E\n\u003Cth\u003EDefault name\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ESession\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Euas\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAccess token\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Euat\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ERefresh token\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Euar\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe default cookie options use:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003EHttpOnly = true\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003ESecurePolicy = CookieSecurePolicy.Always\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EPath = \u0026quot;/\u0026quot;\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EDomain = null\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EMaxAge = null\u003C/code\u003E\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003E\u003Ccode\u003ESameSite\u003C/code\u003E is nullable and can be resolved through the applicable cookie and deployment policies.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022customize-cookie-names\u0022\u003ECustomize Cookie Names\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Cookie.Session.Name = \u0026quot;myapp_session\u0026quot;;\n options.Cookie.RefreshToken.Name = \u0026quot;myapp_refresh\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configure-cookie-scope\u0022\u003EConfigure Cookie Scope\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Cookie.Session.Path = \u0026quot;/\u0026quot;;\n options.Cookie.Session.Domain = null;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EA null domain produces a host-only cookie.\u003C/p\u003E\n\u003Cp\u003ESetting a parent domain can broaden cookie availability to subdomains and should be done only when necessary.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022cookie-security\u0022\u003ECookie Security\u003C/h3\u003E\n\u003Cp\u003EAvoid disabling \u003Ccode\u003EHttpOnly\u003C/code\u003E or relaxing secure transport requirements for authentication cookies.\u003C/p\u003E\n\u003Cp\u003ECookie settings interact with:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EThe deployment mode.\u003C/li\u003E\n\u003Cli\u003ECross-site requests.\u003C/li\u003E\n\u003Cli\u003EBrowser cookie policies.\u003C/li\u003E\n\u003Cli\u003ECredential delivery configuration.\u003C/li\u003E\n\u003Cli\u003EHTTPS requirements.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EFor cross-site deployments, configure cookie behavior together with CORS and the relevant browser security requirements.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022hub-deployment-mode\u0022\u003E11. Hub Deployment Mode\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.HubDeploymentMode\u003C/code\u003E describes how UAuthHub is deployed relative to the application.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.HubDeploymentMode = UAuthHubDeploymentMode.Embedded;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-modes\u0022\u003EAvailable Modes\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EMode\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEmbedded\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EHub and application share the same origin\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIntegrated\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EHub is separate but within the same-site boundary\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EExternal\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EHub is hosted on a different site or domain\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe default is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthHubDeploymentMode.Integrated\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022embedded\u0022\u003EEmbedded\u003C/h3\u003E\n\u003Cp\u003ESuitable for applications hosting UltimateAuth within the same application and origin.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.HubDeploymentMode = UAuthHubDeploymentMode.Embedded;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022integrated\u0022\u003EIntegrated\u003C/h3\u003E\n\u003Cp\u003ESuitable for deployments such as:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003Eauth.example.com\napp.example.com\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Ewhere the authentication service and application are separate origins within the same site.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022external\u0022\u003EExternal\u003C/h3\u003E\n\u003Cp\u003ESuitable for a separate authentication provider domain.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.HubDeploymentMode = UAuthHubDeploymentMode.External;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EExternal deployment requires careful coordination of cookie policies, redirects, allowed origins, and browser restrictions.\u003C/p\u003E\n\u003Cp\u003EThe deployment mode influences authentication transport behavior; it is not merely descriptive metadata.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022hub-options\u0022\u003E12. Hub Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Hub\u003C/code\u003E configures UAuthHub integration.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-properties-2\u0022\u003EAvailable Properties\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EClientBaseAddress\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional client application base address\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAllowedClientOrigins\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEmpty set\u003C/td\u003E\n\u003Ctd\u003EAllowed client origins\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EFlowLifetime\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E5 minutes\u003C/td\u003E\n\u003Ctd\u003ELifetime of Hub flow artifacts\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELoginPath\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E/login\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EHub login page path\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example\u0022\u003EExample\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Hub.ClientBaseAddress = \u0026quot;https://app.example.com\u0026quot;;\n options.Hub.AllowedClientOrigins.Add(\u0026quot;https://app.example.com\u0026quot;);\n options.Hub.FlowLifetime = TimeSpan.FromMinutes(5);\n options.Hub.LoginPath = \u0026quot;/login\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe Hub flow lifetime is not the same as the authenticated session lifetime.\u003C/p\u003E\n\u003Cp\u003EHub artifacts support authentication orchestration and should generally be short-lived.\u003C/p\u003E\n\u003Cp\u003EOnly trusted origins should be included in \u003Ccode\u003EAllowedClientOrigins\u003C/code\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022primary-credential-policy\u0022\u003E13. Primary Credential Policy\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.PrimaryCredential\u003C/code\u003E controls which credential kind is preferred for UI and API requests.\u003C/p\u003E\n\u003Cp\u003EThe defaults are:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUi\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EPrimaryTokenKind.Session\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EApi\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EPrimaryTokenKind.AccessToken\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-1\u0022\u003EExample\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.PrimaryCredential.Ui = PrimaryTokenKind.Session;\n options.PrimaryCredential.Api = PrimaryTokenKind.AccessToken;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis allows UltimateAuth to distinguish between session-oriented browser interactions and access-token-oriented API interactions.\u003C/p\u003E\n\u003Cp\u003EThe primary credential policy does not bypass authentication validation.\u003C/p\u003E\n\u003Cp\u003EIt determines which credential kind should be preferred within the applicable request-processing rules.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022credential-session-policy\u0022\u003E14. Credential Session Policy\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.CredentialSessionPolicy\u003C/code\u003E determines what happens to active session chains after credential changes.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-properties-3\u0022\u003EAvailable Properties\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003Cth\u003EPurpose\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EOnSelfChange\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EPreserveCurrent\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUser changes their own credential\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EOnAdminChange\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003ERevokeAll\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAdministrator changes a user\u0027s credential\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EOnRecovery\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003ERevokeAll\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECredential recovery completes\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-2\u0022\u003EExample\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.CredentialSessionPolicy.OnSelfChange = CredentialSessionBehavior.PreserveCurrent;\n options.CredentialSessionPolicy.OnAdminChange = CredentialSessionBehavior.RevokeAll;\n options.CredentialSessionPolicy.OnRecovery = CredentialSessionBehavior.RevokeAll;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-behaviors\u0022\u003EAvailable Behaviors\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EBehavior\u003C/th\u003E\n\u003Cth\u003EEffect\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPreserveAll\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EKeep existing session chains\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPreserveCurrent\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EKeep the current chain and revoke other applicable chains\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERevokeAll\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERevoke all applicable session chains\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EFor recovery flows, \u003Ccode\u003EPreserveCurrent\u003C/code\u003E is not always meaningful because the user may not have an authenticated current session.\u003C/p\u003E\n\u003Cp\u003EThe current implementation treats this situation using the more restrictive \u003Ccode\u003ERevokeAll\u003C/code\u003E behavior.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-recommendation-1\u0022\u003ESecurity Recommendation\u003C/h3\u003E\n\u003Cp\u003EPreserving the current session after a normal password change can provide a reasonable user experience.\u003C/p\u003E\n\u003Cp\u003ERevoking all sessions after administrative credential changes or account recovery is generally more conservative.\u003C/p\u003E\n\u003Cp\u003EChoose the policy according to the application\u0027s security requirements.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022credential-reset-options\u0022\u003E15. Credential Reset Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.ResetCredential\u003C/code\u003E configures credential recovery proof lifetimes and attempt limits.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022defaults\u0022\u003EDefaults\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETokenValidity\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E30 minutes\u003C/td\u003E\n\u003Ctd\u003EReset token lifetime\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECodeValidity\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E10 minutes\u003C/td\u003E\n\u003Ctd\u003ENumeric reset code lifetime\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxAttempts\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E3\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EMaximum permitted recovery attempts\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECodeLength\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E6\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENumeric reset code length\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-3\u0022\u003EExample\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.ResetCredential.TokenValidity = TimeSpan.FromMinutes(20);\n options.ResetCredential.CodeValidity = TimeSpan.FromMinutes(5);\n options.ResetCredential.MaxAttempts = 3;\n options.ResetCredential.CodeLength = 6;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003ECodeLength\u003C/code\u003E applies to numeric reset codes, not token-based recovery proofs.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022important-security-boundary\u0022\u003EImportant Security Boundary\u003C/h3\u003E\n\u003Cp\u003EReset proofs must be delivered through a trusted server-side mechanism.\u003C/p\u003E\n\u003Cp\u003EDo not return recovery tokens to anonymous callers simply to simplify a client-side reset form.\u003C/p\u003E\n\u003Cp\u003EThe application should provide the appropriate \u003Ccode\u003EICredentialResetNotifier\u003C/code\u003E implementation for supported recovery flows.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022login-identifier-options\u0022\u003E16. Login Identifier Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.LoginIdentifiers\u003C/code\u003E determines which identifier types can be used for login.\u003C/p\u003E\n\u003Cp\u003EThe default allowed types are:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUserIdentifierType.Username\nUserIdentifierType.Email\nUserIdentifierType.Phone\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022restrict-login-identifier-types\u0022\u003ERestrict Login Identifier Types\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.LoginIdentifiers.AllowedTypes =\n new HashSet\u0026lt;UserIdentifierType\u0026gt;\n {\n UserIdentifierType.Email\n };\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis configures email as the allowed login identifier type.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022require-verification\u0022\u003ERequire Verification\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.LoginIdentifiers.RequireVerificationForEmail = true;\n options.LoginIdentifiers.RequireVerificationForPhone = true;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThese settings control whether the corresponding identifiers must be verified before being accepted for login.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022custom-resolvers\u0022\u003ECustom Resolvers\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableCustomResolvers\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECustomResolversFirst\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThese options control the participation and ordering of custom login identifier resolvers.\u003C/p\u003E\n\u003Cp\u003EAn identifier existing on a user account does not necessarily mean it is permitted for login.\u003C/p\u003E\n\u003Cp\u003EIdentifier management and login eligibility are separate concerns.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022identifier-validation\u0022\u003E17. Identifier Validation\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.IdentifierValidation\u003C/code\u003E provides rules for usernames, email addresses, and phone numbers.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022username-rules\u0022\u003EUsername Rules\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.IdentifierValidation.UserName.Enabled = true;\n options.IdentifierValidation.UserName.MinLength = 3;\n options.IdentifierValidation.UserName.MaxLength = 32;\n options.IdentifierValidation.UserName.AllowedRegex = \u0026quot;^[a-zA-Z0-9._-]\u002B$\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022default-validation-rules\u0022\u003EDefault Validation Rules\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EIdentifier\u003C/th\u003E\n\u003Cth\u003EEnabled\u003C/th\u003E\n\u003Cth\u003EMinimum length\u003C/th\u003E\n\u003Cth\u003EMaximum length\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EUsername\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E3\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E64\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EEmail\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E3\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E256\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EPhone\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E3\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E24\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EUsername validation additionally supports \u003Ccode\u003EAllowedRegex\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe default username expression is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EREGEX\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-regex\u0022\u003E^[a-zA-Z0-9._-]\u002B$\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022identifier-behavior\u0022\u003EIdentifier Behavior\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Identifiers\u003C/code\u003E contains additional groups:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.Identifiers.Behavior\noptions.Identifiers.Validation\noptions.Identifiers.Normalization\noptions.Identifiers.Uniqueness\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThese configure identifier behavior beyond the top-level validation rules.\u003C/p\u003E\n\u003Cp\u003EIdentifier normalization and uniqueness are particularly important because they affect how duplicate identifiers are detected.\u003C/p\u003E\n\u003Cp\u003EUse the provided options rather than implementing inconsistent normalization independently in the client.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022identifier-limits\u0022\u003EIdentifier Limits\u003C/h3\u003E\n\u003Cp\u003EUltimateAuth supports configurable limits on the number of identifiers associated with each user.\u003C/p\u003E\n\u003Cp\u003EBy default, a user can have up to \u003Cstrong\u003E10 active identifiers\u003C/strong\u003E, regardless of their type.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.Identifiers.Behavior.MaxIdentifiersPerUser = 10;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis is a global per-user limit. It applies to the combined number of usernames, email addresses, phone numbers, and custom identifiers.\u003C/p\u003E\n\u003Ch4 id=\u0022optional-per-type-limits\u0022\u003EOptional Per-Type Limits\u003C/h4\u003E\n\u003Cp\u003EApplications can optionally define limits for individual identifier types:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.Identifiers.Behavior.MaxIdentifiersPerUser = 10;\n\noptions.Identifiers.Behavior.MaxIdentifiersPerType = new()\n{\n [UserIdentifierType.Username] = 2,\n [UserIdentifierType.Email] = 5,\n [UserIdentifierType.Phone] = 3\n};\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Cstrong\u003EPer-type limits are not configured by default.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EWhen no per-type limit is specified, only the global limit applies.\u003C/p\u003E\n\u003Cp\u003EFor example, with the configuration above, a user may have up to five email addresses, but the combined number of all active identifiers cannot exceed ten.\u003C/p\u003E\n\u003Ch4 id=\u0022default-behavior\u0022\u003EDefault Behavior\u003C/h4\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EOption\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxIdentifiersPerUser\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E10\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EMaximum total active identifiers per user\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxIdentifiersPerType\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEmpty dictionary\u003C/td\u003E\n\u003Ctd\u003EOptional limits for individual identifier types\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe following rules apply:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EPrimary and non-primary identifiers count equally.\u003C/li\u003E\n\u003Cli\u003ESoft-deleted identifiers do not count toward active limits.\u003C/li\u003E\n\u003Cli\u003ESelf-service and administrative operations follow the same limits.\u003C/li\u003E\n\u003Cli\u003EPer-type limits do not override the global limit.\u003C/li\u003E\n\u003Cli\u003EExisting identifier behavior options remain applicable.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EFor example, \u003Ccode\u003EAllowMultipleUsernames = false\u003C/code\u003E still prevents multiple usernames, even when the configured username limit is greater than one.\u003C/p\u003E\n\u003Ch4 id=\u0022limit-validation\u0022\u003ELimit Validation\u003C/h4\u003E\n\u003Cp\u003EWhen an identifier addition exceeds a configured limit, the operation is rejected with a validation error.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EError Code\u003C/th\u003E\n\u003Cth\u003EMeaning\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003Eidentifier_total_limit_exceeded\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EThe user has reached the global identifier limit\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003Eidentifier_type_limit_exceeded\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EThe user has reached the configured limit for that identifier type\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EApplications should handle these errors and display appropriate feedback.\u003C/p\u003E\n\u003Cp\u003EIdentifier limits are enforced by the Users application layer rather than individual persistence stores.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022user-profile-options\u0022\u003E18. User Profile Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.UserProfile\u003C/code\u003E controls profile-related capabilities.\u003C/p\u003E\n\u003Cp\u003EThe current option is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.UserProfile.EnableMultiProfile\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EIts default is \u003Ccode\u003Efalse\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022enable-multiple-profiles\u0022\u003EEnable Multiple Profiles\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.UserProfile.EnableMultiProfile = true;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis allows applications to use UltimateAuth\u0027s multiple-profile capability where supported by the user domain.\u003C/p\u003E\n\u003Cp\u003EFor example, an application may maintain separate profile contexts for different purposes.\u003C/p\u003E\n\u003Cp\u003EMulti-profile support does not create separate user identities or independent authentication accounts.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022multi-tenant-options\u0022\u003E19. Multi-Tenant Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.MultiTenant\u003C/code\u003E controls whether tenant-aware behavior is enabled and how tenant identifiers are resolved.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-properties-4\u0022\u003EAvailable Properties\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnabled\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEnable multi-tenant behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ENormalizeToLowercase\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENormalize tenant identifiers\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableRoute\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EResolve tenants from routes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableHeader\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EResolve tenants from headers\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableDomain\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EResolve tenants from hosts\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EHeaderName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EX-Tenant\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETenant header name\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-header-based-tenant-resolution\u0022\u003EExample: Header-Based Tenant Resolution\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.MultiTenant.Enabled = true;\n options.MultiTenant.EnableHeader = true;\n options.MultiTenant.HeaderName = \u0026quot;X-Tenant\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-domain-based-resolution\u0022\u003EExample: Domain-Based Resolution\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.MultiTenant.Enabled = true;\n options.MultiTenant.EnableDomain = true;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EMultiple resolution mechanisms can be enabled.\u003C/p\u003E\n\u003Cp\u003EThe server composes the configured tenant resolvers according to its resolution infrastructure.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-boundary\u0022\u003ESecurity Boundary\u003C/h3\u003E\n\u003Cp\u003ETenant identification is not tenant authorization.\u003C/p\u003E\n\u003Cp\u003EA request providing another tenant identifier must not gain access to that tenant without appropriate authorization.\u003C/p\u003E\n\u003Cp\u003EThe server remains responsible for tenant isolation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022pagination-options\u0022\u003E20. Pagination Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Pagination\u003C/code\u003E controls pagination defaults and maximum page sizes.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDefaultPageSize\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E250\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxPageSize\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E1000\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-4\u0022\u003EExample\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Pagination.DefaultPageSize = 50;\n options.Pagination.MaxPageSize = 200;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThese settings affect paginated queries exposed by UltimateAuth domain services.\u003C/p\u003E\n\u003Cp\u003EFor administrative interfaces, choosing a moderate page size can reduce unnecessary data transfer and processing.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022navigation-options\u0022\u003E21. Navigation Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Navigation\u003C/code\u003E configures automatic authentication navigation behavior.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-properties-5\u0022\u003EAvailable Properties\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableAutomaticNavigationRedirect\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELoginResolver\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAccessDeniedResolver\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022disable-automatic-redirects\u0022\u003EDisable Automatic Redirects\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Navigation.EnableAutomaticNavigationRedirect = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022custom-login-redirect\u0022\u003ECustom Login Redirect\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Navigation.LoginResolver = context =\u0026gt;\n {\n return \u0026quot;/account/login\u0026quot;;\n };\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022custom-access-denied-redirect\u0022\u003ECustom Access Denied Redirect\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Navigation.AccessDeniedResolver = context =\u0026gt;\n {\n return \u0026quot;/account/access-denied\u0026quot;;\n };\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThese delegates receive the current \u003Ccode\u003EHttpContext\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe application should return valid, trusted navigation destinations.\u003C/p\u003E\n\u003Cp\u003EAvoid constructing redirects from unvalidated user-controlled URLs.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022authentication-response-options\u0022\u003E22. Authentication Response Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.AuthResponse\u003C/code\u003E controls how authentication credentials and redirect behavior are represented in responses.\u003C/p\u003E\n\u003Cp\u003EThe available groups are:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.AuthResponse.SessionIdDelivery\noptions.AuthResponse.AccessTokenDelivery\noptions.AuthResponse.RefreshTokenDelivery\n\noptions.AuthResponse.Login\noptions.AuthResponse.Logout\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThese settings govern credential delivery and authentication response behavior.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022important-mode-specific-response-defaults\u0022\u003EImportant: Mode-Specific Response Defaults\u003C/h3\u003E\n\u003Cp\u003EUltimateAuth provides response templates for authentication modes.\u003C/p\u003E\n\u003Cp\u003EFor example, \u003Ccode\u003EPureOpaque\u003C/code\u003E uses a session-oriented cookie response, while \u003Ccode\u003EHybrid\u003C/code\u003E combines session and token delivery behavior.\u003C/p\u003E\n\u003Cp\u003EThe effective response may therefore depend on the resolved authentication mode and flow.\u003C/p\u003E\n\u003Cp\u003EDo not assume that a single globally configured response delivery setting applies unchanged to every authentication mode.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-considerations\u0022\u003ESecurity Considerations\u003C/h3\u003E\n\u003Cp\u003ECredential delivery settings determine whether sensitive authentication material is exposed through cookies, headers, or other response mechanisms.\u003C/p\u003E\n\u003Cp\u003EOnly customize them when the application has a clear requirement and the selected transport is appropriate for the client platform.\u003C/p\u003E\n\u003Cp\u003EFor advanced response customization, see \u003Ca href=\u0022/docs/configuration/advanced-configuration\u0022\u003EAdvanced Configuration\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022diagnostics-options\u0022\u003E23. Diagnostics Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Diagnostics\u003C/code\u003E provides optional diagnostic behavior.\u003C/p\u003E\n\u003Cp\u003EThe current property is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.Diagnostics.EnableRefreshDetails\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EIts default is \u003Ccode\u003Efalse\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-5\u0022\u003EExample\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Diagnostics.EnableRefreshDetails = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWhen enabled, the option allows additional refresh-related diagnostic response information, such as the \u003Ccode\u003EX-UAuth-Refresh\u003C/code\u003E header.\u003C/p\u003E\n\u003Cp\u003EThis is primarily useful during development and debugging.\u003C/p\u003E\n\u003Cp\u003EAvoid enabling detailed authentication diagnostics in production unless the exposure is understood and justified.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022authentication-lifecycle-events\u0022\u003E24. Authentication Lifecycle Events\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Events\u003C/code\u003E exposes server-side authentication lifecycle callbacks.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-events\u0022\u003EAvailable Events\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.Events.OnAnyEvent\noptions.Events.OnUserLoggedIn\noptions.Events.OnUserLoggedOut\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-login-logging\u0022\u003EExample: Login Logging\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Events.OnUserLoggedIn = context =\u0026gt;\n {\n // Record the successful login.\n return Task.CompletedTask;\n };\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-global-event-handler\u0022\u003EExample: Global Event Handler\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Events.OnAnyEvent = context =\u0026gt;\n {\n // Process an authentication lifecycle event.\n return Task.CompletedTask;\n };\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ELifecycle events are intended for lightweight reactions such as logging, metrics, and notifications.\u003C/p\u003E\n\u003Cp\u003EThey should not replace security decisions, authentication validation, or application policies.\u003C/p\u003E\n\u003Cp\u003ESee \u003Ca href=\u0022/docs/configuration/advanced-configuration\u0022\u003EAdvanced Configuration\u003C/a\u003E for event and extension guidance.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022effective-server-options\u0022\u003E25. Effective Server Options\u003C/h2\u003E\n\u003Cp\u003EThe configured \u003Ccode\u003EUAuthServerOptions\u003C/code\u003E instance is the base configuration.\u003C/p\u003E\n\u003Cp\u003EAt runtime, UltimateAuth can resolve:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EEffectiveUAuthServerOptions\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe current resolution process is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EConfigured UAuthServerOptions\n |\n v\n Resolve auth mode\n |\n v\n Clone base options\n |\n v\n Apply mode defaults\n |\n v\n Apply ConfigureMode overrides\n |\n v\n EffectiveUAuthServerOptions\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis means that the effective settings used for a \u003Ccode\u003EHybrid\u003C/code\u003E client can differ from those used for a \u003Ccode\u003EPureOpaque\u003C/code\u003E client.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-6\u0022\u003EExample\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Session.Lifetime = TimeSpan.FromDays(7);\n\n options.ConfigureMode(UAuthMode.Hybrid, mode =\u0026gt;\n {\n mode.Token.AccessTokenLifetime = TimeSpan.FromMinutes(5);\n });\n\n options.ConfigureMode(UAuthMode.PureOpaque, mode =\u0026gt;\n {\n mode.Session.IdleTimeout = TimeSpan.FromDays(3);\n });\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe original options remain the common starting point.\u003C/p\u003E\n\u003Cp\u003EMode defaults and overrides determine the effective values for the resolved mode.\u003C/p\u003E\n\u003Cp\u003EFor implementation details and inspection of effective options, see \u003Ca href=\u0022/docs/configuration/advanced-configuration\u0022\u003EAdvanced Configuration\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022validation-and-configuration-safety\u0022\u003E26. Validation and Configuration Safety\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth Server registers multiple options validators and enables startup validation.\u003C/p\u003E\n\u003Cp\u003EThese validators cover areas including:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003ELogin settings.\u003C/li\u003E\n\u003Cli\u003ESession settings.\u003C/li\u003E\n\u003Cli\u003EToken settings.\u003C/li\u003E\n\u003Cli\u003EPKCE.\u003C/li\u003E\n\u003Cli\u003EMulti-tenancy.\u003C/li\u003E\n\u003Cli\u003EIdentifier configuration.\u003C/li\u003E\n\u003Cli\u003ESession resolution.\u003C/li\u003E\n\u003Cli\u003EPagination.\u003C/li\u003E\n\u003Cli\u003EGeneral server configuration.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-invalid-session-lifetime\u0022\u003EExample: Invalid Session Lifetime\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Session.Lifetime = TimeSpan.Zero;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe server rejects an invalid session lifetime under the applicable validation rules.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-invalid-base-path\u0022\u003EExample: Invalid Base Path\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Endpoints.BasePath = \u0026quot;//auth\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe general server validator rejects paths containing \u003Ccode\u003E//\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022unsupported-modes\u0022\u003EUnsupported Modes\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.AllowedModes =\n [\n UAuthMode.PureJwt\n ];\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe current validator rejects explicitly enabling \u003Ccode\u003EPureJwt\u003C/code\u003E because that mode is not yet implemented as a supported server mode.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022validation-limitations\u0022\u003EValidation Limitations\u003C/h3\u003E\n\u003Cp\u003EStartup validation protects the configured options according to the registered validators.\u003C/p\u003E\n\u003Cp\u003EIt does not guarantee that every custom service implementation or mode-specific override is secure.\u003C/p\u003E\n\u003Cp\u003EWhen using advanced customization, test the resulting effective configuration and authentication flows.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022complete-configuration-example\u0022\u003E27. Complete Configuration Example\u003C/h2\u003E\n\u003Cp\u003EThe following example combines several commonly used server settings.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n // Authentication modes\n options.AllowedModes =\n [\n UAuthMode.PureOpaque,\n UAuthMode.Hybrid\n ];\n\n // Login security\n options.Login.MaxFailedAttempts = 5;\n options.Login.LockoutDuration = TimeSpan.FromMinutes(15);\n options.Login.IncludeFailureDetails = false;\n\n // Sessions\n options.Session.Lifetime = TimeSpan.FromDays(7);\n options.Session.MaxLifetime = TimeSpan.FromDays(30);\n\n // Token identity\n options.Token.Issuer = \u0026quot;MyApplication\u0026quot;;\n options.Token.Audience = \u0026quot;MyApplicationApi\u0026quot;;\n\n // Hybrid-specific token lifetime\n options.ConfigureMode(UAuthMode.Hybrid, mode =\u0026gt;\n {\n mode.Token.AccessTokenLifetime = TimeSpan.FromMinutes(10);\n });\n\n // PKCE\n options.Pkce.AuthorizationCodeLifetimeSeconds = 120;\n\n // Hub deployment\n options.HubDeploymentMode = UAuthHubDeploymentMode.Integrated;\n options.Hub.ClientBaseAddress = \u0026quot;https://app.example.com\u0026quot;;\n options.Hub.AllowedClientOrigins.Add(\u0026quot;https://app.example.com\u0026quot;);\n\n // Credential security\n options.CredentialSessionPolicy.OnSelfChange = CredentialSessionBehavior.PreserveCurrent;\n options.CredentialSessionPolicy.OnAdminChange = CredentialSessionBehavior.RevokeAll;\n options.CredentialSessionPolicy.OnRecovery = CredentialSessionBehavior.RevokeAll;\n\n // Credential recovery\n options.ResetCredential.TokenValidity = TimeSpan.FromMinutes(30);\n options.ResetCredential.MaxAttempts = 3;\n\n // Identifier login rules\n options.LoginIdentifiers.RequireVerificationForEmail = true;\n\n // Session transport\n options.SessionResolution.EnableCookie = true;\n options.SessionResolution.EnableBearer = true;\n options.SessionResolution.EnableQuery = false;\n\n // Endpoint exposure\n options.Endpoints.Authentication = true;\n options.Endpoints.Session = true;\n options.Endpoints.Credentials = true;\n options.Endpoints.Authorization = true;\n\n // Pagination\n options.Pagination.DefaultPageSize = 50;\n options.Pagination.MaxPageSize = 200;\n\n // Diagnostics\n options.Diagnostics.EnableRefreshDetails = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis is an illustrative configuration, not a universal production preset.\u003C/p\u003E\n\u003Cp\u003EApplications should choose session lifetimes, authentication modes, credential transport, tenant behavior, and endpoint exposure according to their own deployment and security requirements.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022common-configuration-mistakes\u0022\u003E28. Common Configuration Mistakes\u003C/h2\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configuring-core-options-separately\u0022\u003EConfiguring Core Options Separately\u003C/h3\u003E\n\u003Cp\u003EWhen using \u003Ccode\u003EAddUltimateAuthServer()\u003C/code\u003E, configure Core-defined options through \u003Ccode\u003EUAuthServerOptions\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EA separate \u003Ccode\u003EUAuthOptions\u003C/code\u003E configuration does not automatically override the server\u0027s composed options.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022assuming-allowedmodes-selects-the-mode\u0022\u003EAssuming AllowedModes Selects the Mode\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EAllowedModes\u003C/code\u003E is a restriction.\u003C/p\u003E\n\u003Cp\u003EMode selection is performed by the authentication mode resolver.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022ignoring-mode-defaults\u0022\u003EIgnoring Mode Defaults\u003C/h3\u003E\n\u003Cp\u003EMode-specific defaults can replace globally configured values.\u003C/p\u003E\n\u003Cp\u003EUse \u003Ccode\u003EConfigureMode()\u003C/code\u003E for explicit mode-specific behavior.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022enabling-unsupported-modes\u0022\u003EEnabling Unsupported Modes\u003C/h3\u003E\n\u003Cp\u003EAn authentication mode appearing in an enum does not guarantee runtime support.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022treating-reserved-session-options-as-enforced-security-features\u0022\u003ETreating Reserved Session Options as Enforced Security Features\u003C/h3\u003E\n\u003Cp\u003ESome device and session-chain options are reserved for future enforcement.\u003C/p\u003E\n\u003Cp\u003EVerify actual runtime support before relying on them.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022exposing-unnecessary-endpoints\u0022\u003EExposing Unnecessary Endpoints\u003C/h3\u003E\n\u003Cp\u003EDisable endpoint groups or actions that the application does not require.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022allowing-query-string-credentials-without-a-specific-need\u0022\u003EAllowing Query-String Credentials Without a Specific Need\u003C/h3\u003E\n\u003Cp\u003EQuery-string credentials can leak through URLs and infrastructure logs.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022weakening-cookie-security\u0022\u003EWeakening Cookie Security\u003C/h3\u003E\n\u003Cp\u003EDo not relax secure transport or cookie protections without understanding the consequences.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022assuming-configuration-validation-covers-everything\u0022\u003EAssuming Configuration Validation Covers Everything\u003C/h3\u003E\n\u003Cp\u003EStartup validation does not replace testing, infrastructure security, or validation of custom extension implementations.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022server-options-quick-reference\u0022\u003E29. Server Options Quick Reference\u003C/h2\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EConfiguration group\u003C/th\u003E\n\u003Cth\u003ETypical use\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELogin\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELogin attempts and lockout\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESession\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESession expiration and lifecycle\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EToken\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EToken issuance and lifetime\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPkce\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPKCE code lifetime\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAllowedModes\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPermitted authentication modes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMultiTenant\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETenant resolution\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPagination\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EQuery page limits\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEndpoints\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEndpoint exposure\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESessionResolution\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECredential source precedence\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECookie\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECookie security and scope\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EHubDeploymentMode\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EHub deployment topology\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EHub\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EHub integration\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPrimaryCredential\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUI/API primary credential selection\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAuthResponse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECredential delivery\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECredentialSessionPolicy\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESession invalidation after credential changes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EResetCredential\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECredential recovery rules\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdentifiers\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifier behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdentifierValidation\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifier validation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELoginIdentifiers\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELogin identifier eligibility\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserProfile\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EMulti-profile capability\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ENavigation\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication redirects\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDiagnostics\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERefresh diagnostics\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEvents\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication lifecycle callbacks\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EConfigureMode()\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EMode-specific option overrides\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EOnConfigureEndpoints\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAdvanced endpoint customization\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022next-steps\u0022\u003ENext Steps\u003C/h2\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/configuration/configuration\u0022\u003EConfiguration Overview\u003C/a\u003E \u2014 Understand the configuration architecture and precedence.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/configuration/client-options\u0022\u003EClient Options\u003C/a\u003E \u2014 Configure client SDK behavior.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/configuration/advanced-configuration\u0022\u003EAdvanced Configuration\u003C/a\u003E \u2014 Explore effective options, mode resolution, and extension points.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/client/session-management\u0022\u003ESession Management\u003C/a\u003E \u2014 Manage sessions through the client.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/client/authentication\u0022\u003EAuthentication\u003C/a\u003E \u2014 Use the client authentication flows.\u003C/li\u003E\n\u003C/ul\u003E\n",
+ "Html": "\n\u003Cp\u003E\u003Ccode\u003EUAuthServerOptions\u003C/code\u003E is the primary configuration surface for UltimateAuth Server.\u003C/p\u003E\n\u003Cp\u003EIt controls authentication behavior, session lifecycles, token issuance, credential security, tenant resolution, endpoint exposure, and integration with UAuthHub.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth composes its Core-defined options directly into \u003Ccode\u003EUAuthServerOptions\u003C/code\u003E. When using UltimateAuth Server, you configure both shared authentication behavior and server-specific features through the same options object.\u003C/p\u003E\n\u003Cp\u003EThis guide covers the available configuration groups, their defaults, practical examples, and the distinction between configured and effective runtime behavior.\u003C/p\u003E\n\u003Cp\u003EBuilt-in HTTP throttling is configured through \u003Ccode\u003Eoptions.RateLimiting\u003C/code\u003E. See \u003Ca href=\u0022/docs/security/rate-limiting\u0022\u003ERate Limiting\u003C/a\u003E for defaults, middleware ownership, proxy configuration and native-policy integration.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022register-ultimateauth-server\u0022\u003E1. Register UltimateAuth Server\u003C/h2\u003E\n\u003Cp\u003EConfigure the server through \u003Ccode\u003EAddUltimateAuthServer()\u003C/code\u003E:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Login.MaxFailedAttempts = 5;\n options.Session.Lifetime = TimeSpan.FromDays(7);\n options.Token.AccessTokenLifetime = TimeSpan.FromMinutes(10);\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe configuration delegate receives:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthServerOptions\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe server registration also supports an optional policy configuration delegate.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(\n configure: options =\u0026gt;\n {\n options.Login.MaxFailedAttempts = 5;\n },\n configurePolicies: policies =\u0026gt;\n {\n // Register application access policies.\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe server automatically registers its Core infrastructure. You do not need to call \u003Ccode\u003EAddUltimateAuth()\u003C/code\u003E separately.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configuration-from-appsettings.json\u0022\u003EConfiguration from appsettings.json\u003C/h3\u003E\n\u003Cp\u003EUltimateAuth Server binds the configuration section:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EUltimateAuth:Server\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EJSON\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-json\u0022\u003E{\n \u0026quot;UltimateAuth\u0026quot;: {\n \u0026quot;Server\u0026quot;: {\n \u0026quot;Login\u0026quot;: {\n \u0026quot;MaxFailedAttempts\u0026quot;: 5,\n \u0026quot;LockoutDuration\u0026quot;: \u0026quot;00:15:00\u0026quot;\n },\n \u0026quot;Session\u0026quot;: {\n \u0026quot;Lifetime\u0026quot;: \u0026quot;7.00:00:00\u0026quot;\n },\n \u0026quot;Token\u0026quot;: {\n \u0026quot;AccessTokenLifetime\u0026quot;: \u0026quot;00:10:00\u0026quot;\n }\n }\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EASP.NET Core represents \u003Ccode\u003ETimeSpan\u003C/code\u003E values using strings such as \u003Ccode\u003E00:15:00\u003C/code\u003E for fifteen minutes and \u003Ccode\u003E7.00:00:00\u003C/code\u003E for seven days.\u003C/p\u003E\n\u003Cp\u003EThe configuration section is bound after the registration delegate, so configuration provider values take precedence for matching properties.\u003C/p\u003E\n\u003Cp\u003EFor configuration source ordering and environment-specific overrides, see \u003Ca href=\u0022/docs/configuration/configuration\u0022\u003EConfiguration Overview\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022server-options-architecture\u0022\u003E2. Server Options Architecture\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthServerOptions\u003C/code\u003E contains two categories of configuration.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022core-defined-options\u0022\u003ECore-defined options\u003C/h3\u003E\n\u003Cp\u003EThese types are defined in UltimateAuth Core and composed into the server options:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EType\u003C/th\u003E\n\u003Cth\u003EPurpose\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELogin\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthLoginOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELogin attempts and lockout behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESession\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthSessionOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESession lifetime and lifecycle\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EToken\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthTokenOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAccess and refresh token behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPkce\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthPkceOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPKCE authorization code lifetime\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMultiTenant\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthMultiTenantOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETenant identification and resolution\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPagination\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthPaginationOptions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPagination defaults and limits\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEvents\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthEvents\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication lifecycle callbacks\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022server-specific-options\u0022\u003EServer-specific options\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EPurpose\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAllowedModes\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERestrict permitted authentication modes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EHubDeploymentMode\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EDescribe the relationship between UAuthHub and the application\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EHub\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EHub integration and flow settings\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECookie\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECookie names, security attributes, and lifetime behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESessionResolution\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESession identifier sources and precedence\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEndpoints\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEndpoint groups and disabled actions\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPrimaryCredential\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPrimary credential selection for UI and API requests\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAuthResponse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication credential delivery and redirect behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECredentialSessionPolicy\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESession invalidation after credential changes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EResetCredential\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECredential recovery code and token settings\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdentifiers\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifier behavior, normalization, and uniqueness\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdentifierValidation\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUsername, email, and phone validation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELoginIdentifiers\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifier types allowed for login\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserProfile\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUser profile capabilities\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ENavigation\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication navigation behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDiagnostics\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional diagnostic response details\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EAdvanced configuration hooks are also available, including \u003Ccode\u003EConfigureMode()\u003C/code\u003E and \u003Ccode\u003EOnConfigureEndpoints\u003C/code\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022login-options\u0022\u003E3. Login Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Login\u003C/code\u003E controls login attempt limits, lockout behavior, and related security settings.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Login.MaxFailedAttempts = 5;\n options.Login.LockoutDuration = TimeSpan.FromMinutes(15);\n options.Login.FailureWindow = TimeSpan.FromMinutes(15);\n options.Login.ExtendLockOnFailure = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-properties\u0022\u003EAvailable Properties\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxFailedAttempts\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E10\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EFailed login threshold; \u003Ccode\u003E0\u003C/code\u003E disables lockout\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELockoutDuration\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E15 minutes\u003C/td\u003E\n\u003Ctd\u003ELockout duration\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EFailureWindow\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E15 minutes\u003C/td\u003E\n\u003Ctd\u003EWindow for counting failed attempts\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EExtendLockOnFailure\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EExtend lockout when another attempt fails during lockout\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIncludeFailureDetails\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EInclude detailed authentication failure information\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETryLoginDuration\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E180 seconds\u003C/td\u003E\n\u003Ctd\u003EDuration associated with the try-login flow\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-restrict-login-failure-details\u0022\u003EExample: Restrict Login Failure Details\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Login.IncludeFailureDetails = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis setting is useful when an application does not want to expose detailed lockout information to callers.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022important\u0022\u003EImportant\u003C/h3\u003E\n\u003Cp\u003ELockout configuration is not equivalent to complete network-level rate limiting.\u003C/p\u003E\n\u003Cp\u003EProduction deployments should also consider infrastructure protections against abusive traffic and distributed login attempts.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022session-options\u0022\u003E4. Session Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Session\u003C/code\u003E controls session lifetime, expiration, sliding behavior, and session-chain limits.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Session.Lifetime = TimeSpan.FromDays(7);\n options.Session.SlidingExpiration = true;\n options.Session.IdleTimeout = TimeSpan.FromDays(3);\n options.Session.MaxLifetime = TimeSpan.FromDays(30);\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022session-lifetime-properties\u0022\u003ESession Lifetime Properties\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELifetime\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E7 days\u003C/td\u003E\n\u003Ctd\u003EStandard session lifetime\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxLifetime\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional absolute lifetime limit\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESlidingExpiration\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAllow eligible activity or refresh operations to extend expiration\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdleTimeout\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional inactivity timeout\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETouchInterval\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E5 minutes\u003C/td\u003E\n\u003Ctd\u003EMinimum interval between last-seen updates\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThese settings have different purposes.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003ELifetime\u003C/code\u003E determines the standard lifetime assigned to a session.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EMaxLifetime\u003C/code\u003E establishes an optional hard boundary that cannot be extended indefinitely.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EIdleTimeout\u003C/code\u003E limits how long a session can remain inactive.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003ETouchInterval\u003C/code\u003E reduces unnecessary persistence operations caused by frequent activity updates.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022session-chain-properties\u0022\u003ESession-Chain Properties\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxChainsPerUser\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E0\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReserved user-level chain limit\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxSessionsPerChain\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E100\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESession-chain rotation limit\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxChainsPerPlatform\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReserved platform-specific chain limits\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPlatformCategories\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReserved platform group definitions\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxChainsPerCategory\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReserved category-specific chain limits\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableIpBinding\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReserved IP binding setting\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableUserAgentBinding\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReserved User-Agent binding setting\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDeviceMismatchBehavior\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EReject\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EReserved device mismatch behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Cstrong\u003ECurrent implementation limitation:\u003C/strong\u003E The source identifies several device limits and binding settings as reserved for future enforcement. Setting these properties must not be treated as proof that the corresponding protection is active.\u003C/p\u003E\n\u003Cp\u003EFor example, do not rely on:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.Session.MaxChainsPerUser = 1;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Eas a production guarantee that only one device chain can exist unless the current runtime implementation has been verified to enforce that restriction.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022mode-specific-session-defaults\u0022\u003EMode-Specific Session Defaults\u003C/h3\u003E\n\u003Cp\u003ESession settings can also be affected by authentication mode defaults.\u003C/p\u003E\n\u003Cp\u003EFor example, \u003Ccode\u003EPureOpaque\u003C/code\u003E supplies a longer idle timeout and touch interval when those values have not been configured.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EHybrid\u003C/code\u003E explicitly sets \u003Ccode\u003ETouchInterval\u003C/code\u003E to \u003Ccode\u003Enull\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003ESee \u003Ca href=\u0022/docs/configuration/advanced-configuration\u0022\u003EAdvanced Configuration\u003C/a\u003E for the effective-options resolution order.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022token-options\u0022\u003E5. Token Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Token\u003C/code\u003E configures access and refresh token behavior.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Token.Issuer = \u0026quot;MyApplication\u0026quot;;\n options.Token.Audience = \u0026quot;MyApplicationApi\u0026quot;;\n options.Token.AccessTokenLifetime = TimeSpan.FromMinutes(10);\n options.Token.RefreshTokenLifetime = TimeSpan.FromDays(7);\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-properties-1\u0022\u003EAvailable Properties\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIssueJwt\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEnable JWT issuance where supported by the effective mode\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIssueOpaque\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEnable opaque token issuance where supported\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIssueRefresh\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEnable refresh token issuance where supported\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAccessTokenLifetime\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E10 minutes\u003C/td\u003E\n\u003Ctd\u003EAccess token lifetime\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERefreshTokenLifetime\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E7 days\u003C/td\u003E\n\u003Ctd\u003ERefresh token lifetime\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EOpaqueIdBytes\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E32\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERandom byte length used for opaque token identifiers\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERefreshTokenConcurrentRequestWindow\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E2 seconds\u003C/td\u003E\n\u003Ctd\u003EConcurrent duplicate refresh classification window\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIssuer\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuth\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EJWT issuer\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAudience\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthClient\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EJWT audience\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAddJwtIdClaim\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EInclude a JWT identifier claim\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EKeyId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional signing key identifier\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022refresh-token-concurrency-window\u0022\u003ERefresh Token Concurrency Window\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Token.RefreshTokenConcurrentRequestWindow = TimeSpan.FromSeconds(2);\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis option controls how the server distinguishes a closely timed duplicate refresh request from a confirmed replay.\u003C/p\u003E\n\u003Cp\u003EA duplicate refresh request is still rejected.\u003C/p\u003E\n\u003Cp\u003EThe window affects the replay-response classification, including whether the token family should be revoked.\u003C/p\u003E\n\u003Cp\u003EIt does \u003Cstrong\u003Enot\u003C/strong\u003E allow the same consumed refresh token to be used successfully more than once.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022mode-defaults-may-override-token-values\u0022\u003EMode Defaults May Override Token Values\u003C/h3\u003E\n\u003Cp\u003EThe effective \u003Ccode\u003EHybrid\u003C/code\u003E configuration explicitly sets:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EToken.AccessTokenLifetime = TimeSpan.FromMinutes(10);\nToken.RefreshTokenLifetime = TimeSpan.FromDays(7);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ETherefore, a different value configured globally may be replaced when the effective mode is resolved.\u003C/p\u003E\n\u003Cp\u003ETo customize the lifetime for \u003Ccode\u003EHybrid\u003C/code\u003E, use:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.ConfigureMode(UAuthMode.Hybrid, mode =\u0026gt;\n {\n mode.Token.AccessTokenLifetime = TimeSpan.FromMinutes(5);\n });\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis distinction is essential when configuring token lifetimes.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022pkce-options\u0022\u003E6. PKCE Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Pkce\u003C/code\u003E configures the lifetime of PKCE authorization codes.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Pkce.AuthorizationCodeLifetimeSeconds = 120;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAuthorizationCodeLifetimeSeconds\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E120\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELifetime of a PKCE authorization code\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EPKCE is particularly important for browser-based clients such as Blazor WebAssembly.\u003C/p\u003E\n\u003Cp\u003EA shorter authorization code lifetime reduces the period during which an intercepted or exposed code could potentially be used.\u003C/p\u003E\n\u003Cp\u003EHowever, the value must remain long enough for legitimate authorization exchanges to complete.\u003C/p\u003E\n\u003Cp\u003EThe server\u0027s PKCE configuration is separate from the client\u0027s PKCE flow settings.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022authentication-modes\u0022\u003E7. Authentication Modes\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth supports authentication mode resolution based on the client profile.\u003C/p\u003E\n\u003Cp\u003EThe server exposes:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.AllowedModes\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis property restricts the modes the server is permitted to use.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022restrict-allowed-modes\u0022\u003ERestrict Allowed Modes\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.AllowedModes =\n [\n UAuthMode.PureOpaque,\n UAuthMode.Hybrid\n ];\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EAllowedModes\u003C/code\u003E is a restriction, not a mode selector.\u003C/p\u003E\n\u003Cp\u003EThe effective mode is resolved by \u003Ccode\u003EIEffectiveAuthModeResolver\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022default-mode-resolution\u0022\u003EDefault Mode Resolution\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EClient profile\u003C/th\u003E\n\u003Cth\u003EDefault resolved mode\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EBlazorServer\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EPureOpaque\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EWebServer\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EHybrid\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EBlazorWasm\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EHybrid\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaui\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EHybrid\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EApi\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EPureJwt\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe current implementation has mode templates for \u003Ccode\u003EPureOpaque\u003C/code\u003E, \u003Ccode\u003EHybrid\u003C/code\u003E, \u003Ccode\u003ESemiHybrid\u003C/code\u003E, and \u003Ccode\u003EPureJwt\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EHowever, \u003Ccode\u003ESemiHybrid\u003C/code\u003E and \u003Ccode\u003EPureJwt\u003C/code\u003E are not yet accepted as enabled modes by the current server options validator.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EDo not enable an unsupported mode merely because it exists in the enum or has a mode-default template.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EThe default \u003Ccode\u003EApi\u003C/code\u003E profile mapping to \u003Ccode\u003EPureJwt\u003C/code\u003E is a current compatibility limitation to consider when deploying API-profile authentication flows.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configure-mode-specific-behavior\u0022\u003EConfigure Mode-Specific Behavior\u003C/h3\u003E\n\u003Cp\u003EUse \u003Ccode\u003EConfigureMode()\u003C/code\u003E for behavior that should differ by mode.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.ConfigureMode(UAuthMode.PureOpaque, mode =\u0026gt;\n {\n mode.Session.IdleTimeout = TimeSpan.FromDays(7);\n });\n\n options.ConfigureMode(UAuthMode.Hybrid, mode =\u0026gt;\n {\n mode.Token.AccessTokenLifetime = TimeSpan.FromMinutes(5);\n });\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe mode override runs after the built-in mode defaults.\u003C/p\u003E\n\u003Cp\u003ESee \u003Ca href=\u0022/docs/configuration/advanced-configuration\u0022\u003EAdvanced Configuration\u003C/a\u003E for details.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022endpoint-options\u0022\u003E8. Endpoint Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Endpoints\u003C/code\u003E controls endpoint paths, feature groups, and individual action restrictions.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022base-path\u0022\u003EBase Path\u003C/h3\u003E\n\u003Cp\u003EThe default server endpoint base path is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003E/auth\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EConfigure it through:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Endpoints.BasePath = \u0026quot;/auth\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EChanging the base path can help avoid route conflicts when integrating UltimateAuth into an existing application.\u003C/p\u003E\n\u003Cp\u003EThe client must use compatible endpoint paths.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022endpoint-groups\u0022\u003EEndpoint Groups\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003Cth\u003EPurpose\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAuthentication\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication endpoints\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPkce\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPKCE endpoints\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESession\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESession management endpoints\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserLifecycle\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUser lifecycle endpoints\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserProfile\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUser profile endpoints\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserIdentifier\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifier management endpoints\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECredentials\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECredential management endpoints\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAuthorization\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthorization endpoints\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Endpoints.Authentication = true;\n options.Endpoints.Session = true;\n options.Endpoints.Authorization = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EDisabling an endpoint group controls endpoint exposure. It does not automatically remove the underlying domain services or invalidate previously issued credentials.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022disable-individual-actions\u0022\u003EDisable Individual Actions\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EDisabledActions\u003C/code\u003E is a collection of action identifiers:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Endpoints.DisabledActions.Add(\u0026quot;users.create.anonymous\u0026quot;);\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe string must match the action identifier used by the server\u0027s endpoint registration and action policy.\u003C/p\u003E\n\u003Cp\u003EDo not pass a C# constant name as a string unless that exact text is also the registered action value.\u003C/p\u003E\n\u003Cp\u003EFor example, these are not necessarily equivalent:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003E\u0026quot;UAuthActions.Users.Create.Anonymous\u0026quot;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Eand:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003E\u0026quot;users.create.anonymous\u0026quot;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUse the actual action value defined by the corresponding \u003Ccode\u003EUAuthActions\u003C/code\u003E constant.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022endpoint-hardening\u0022\u003EEndpoint Hardening\u003C/h3\u003E\n\u003Cp\u003EDisable endpoints that are not required by the application.\u003C/p\u003E\n\u003Cp\u003EFor example, an application that delegates user provisioning to administrators may choose to disable anonymous user creation.\u003C/p\u003E\n\u003Cp\u003EEndpoint exposure is a separate concern from authorization: an exposed endpoint must still enforce the applicable security policy.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022session-resolution\u0022\u003E9. Session Resolution\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.SessionResolution\u003C/code\u003E controls how incoming requests provide session identifiers.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth supports four resolution sources:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EBearer credentials.\u003C/li\u003E\n\u003Cli\u003ECustom headers.\u003C/li\u003E\n\u003Cli\u003ECookies.\u003C/li\u003E\n\u003Cli\u003EQuery parameters.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022default-configuration\u0022\u003EDefault Configuration\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableBearer\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableHeader\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableCookie\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableQuery\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EHeaderName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EX-UAuth-Session\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EQueryParameterName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Esession_id\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe default precedence order is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EBearer\nHeader\nCookie\nQuery\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configure-resolution-sources\u0022\u003EConfigure Resolution Sources\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.SessionResolution.EnableBearer = true;\n options.SessionResolution.EnableHeader = false;\n options.SessionResolution.EnableCookie = true;\n options.SessionResolution.EnableQuery = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis example disables custom-header and query-string session resolution.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022change-resolution-order\u0022\u003EChange Resolution Order\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.SessionResolution.Order =\n [\n \u0026quot;Cookie\u0026quot;,\n \u0026quot;Bearer\u0026quot;,\n \u0026quot;Header\u0026quot;,\n \u0026quot;Query\u0026quot;\n ];\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EResolution order matters when a request contains multiple candidate credentials.\u003C/p\u003E\n\u003Cp\u003EThe selected credential must still pass authentication and session validation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-recommendation\u0022\u003ESecurity Recommendation\u003C/h3\u003E\n\u003Cp\u003EAvoid query-string credentials unless the application has a specific, well-understood requirement.\u003C/p\u003E\n\u003Cp\u003EURLs may be recorded in browser history, reverse-proxy logs, analytics systems, and other infrastructure.\u003C/p\u003E\n\u003Cp\u003EFor most browser and API integrations, cookies or authorization headers are preferable.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022cookie-configuration\u0022\u003E10. Cookie Configuration\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Cookie\u003C/code\u003E controls cookies used for authentication credentials.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth defines three cookie groups:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.Cookie.Session\noptions.Cookie.AccessToken\noptions.Cookie.RefreshToken\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022default-cookie-names\u0022\u003EDefault Cookie Names\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003ECookie\u003C/th\u003E\n\u003Cth\u003EDefault name\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ESession\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Euas\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAccess token\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Euat\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ERefresh token\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Euar\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe default cookie options use:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003EHttpOnly = true\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003ESecurePolicy = CookieSecurePolicy.Always\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EPath = \u0026quot;/\u0026quot;\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EDomain = null\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EMaxAge = null\u003C/code\u003E\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003E\u003Ccode\u003ESameSite\u003C/code\u003E is nullable and can be resolved through the applicable cookie and deployment policies.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022customize-cookie-names\u0022\u003ECustomize Cookie Names\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Cookie.Session.Name = \u0026quot;myapp_session\u0026quot;;\n options.Cookie.RefreshToken.Name = \u0026quot;myapp_refresh\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configure-cookie-scope\u0022\u003EConfigure Cookie Scope\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Cookie.Session.Path = \u0026quot;/\u0026quot;;\n options.Cookie.Session.Domain = null;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EA null domain produces a host-only cookie.\u003C/p\u003E\n\u003Cp\u003ESetting a parent domain can broaden cookie availability to subdomains and should be done only when necessary.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022cookie-security\u0022\u003ECookie Security\u003C/h3\u003E\n\u003Cp\u003EAvoid disabling \u003Ccode\u003EHttpOnly\u003C/code\u003E or relaxing secure transport requirements for authentication cookies.\u003C/p\u003E\n\u003Cp\u003ECookie settings interact with:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EThe deployment mode.\u003C/li\u003E\n\u003Cli\u003ECross-site requests.\u003C/li\u003E\n\u003Cli\u003EBrowser cookie policies.\u003C/li\u003E\n\u003Cli\u003ECredential delivery configuration.\u003C/li\u003E\n\u003Cli\u003EHTTPS requirements.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EFor cross-site deployments, configure cookie behavior together with CORS and the relevant browser security requirements.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022hub-deployment-mode\u0022\u003E11. Hub Deployment Mode\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.HubDeploymentMode\u003C/code\u003E describes how UAuthHub is deployed relative to the application.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.HubDeploymentMode = UAuthHubDeploymentMode.Embedded;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-modes\u0022\u003EAvailable Modes\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EMode\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEmbedded\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EHub and application share the same origin\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIntegrated\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EHub is separate but within the same-site boundary\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EExternal\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EHub is hosted on a different site or domain\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe default is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthHubDeploymentMode.Integrated\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022embedded\u0022\u003EEmbedded\u003C/h3\u003E\n\u003Cp\u003ESuitable for applications hosting UltimateAuth within the same application and origin.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.HubDeploymentMode = UAuthHubDeploymentMode.Embedded;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022integrated\u0022\u003EIntegrated\u003C/h3\u003E\n\u003Cp\u003ESuitable for deployments such as:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003Eauth.example.com\napp.example.com\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Ewhere the authentication service and application are separate origins within the same site.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022external\u0022\u003EExternal\u003C/h3\u003E\n\u003Cp\u003ESuitable for a separate authentication provider domain.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.HubDeploymentMode = UAuthHubDeploymentMode.External;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EExternal deployment requires careful coordination of cookie policies, redirects, allowed origins, and browser restrictions.\u003C/p\u003E\n\u003Cp\u003EThe deployment mode influences authentication transport behavior; it is not merely descriptive metadata.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022hub-options\u0022\u003E12. Hub Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Hub\u003C/code\u003E configures UAuthHub integration.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-properties-2\u0022\u003EAvailable Properties\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EClientBaseAddress\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional client application base address\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAllowedClientOrigins\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEmpty set\u003C/td\u003E\n\u003Ctd\u003EAllowed client origins\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EFlowLifetime\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E5 minutes\u003C/td\u003E\n\u003Ctd\u003ELifetime of Hub flow artifacts\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELoginPath\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E/login\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EHub login page path\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example\u0022\u003EExample\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Hub.ClientBaseAddress = \u0026quot;https://app.example.com\u0026quot;;\n options.Hub.AllowedClientOrigins.Add(\u0026quot;https://app.example.com\u0026quot;);\n options.Hub.FlowLifetime = TimeSpan.FromMinutes(5);\n options.Hub.LoginPath = \u0026quot;/login\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe Hub flow lifetime is not the same as the authenticated session lifetime.\u003C/p\u003E\n\u003Cp\u003EHub artifacts support authentication orchestration and should generally be short-lived.\u003C/p\u003E\n\u003Cp\u003EOnly trusted origins should be included in \u003Ccode\u003EAllowedClientOrigins\u003C/code\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022primary-credential-policy\u0022\u003E13. Primary Credential Policy\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.PrimaryCredential\u003C/code\u003E controls which credential kind is preferred for UI and API requests.\u003C/p\u003E\n\u003Cp\u003EThe defaults are:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUi\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EPrimaryTokenKind.Session\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EApi\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EPrimaryTokenKind.AccessToken\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-1\u0022\u003EExample\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.PrimaryCredential.Ui = PrimaryTokenKind.Session;\n options.PrimaryCredential.Api = PrimaryTokenKind.AccessToken;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis allows UltimateAuth to distinguish between session-oriented browser interactions and access-token-oriented API interactions.\u003C/p\u003E\n\u003Cp\u003EThe primary credential policy does not bypass authentication validation.\u003C/p\u003E\n\u003Cp\u003EIt determines which credential kind should be preferred within the applicable request-processing rules.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022credential-session-policy\u0022\u003E14. Credential Session Policy\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.CredentialSessionPolicy\u003C/code\u003E determines what happens to active session chains after credential changes.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-properties-3\u0022\u003EAvailable Properties\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003Cth\u003EPurpose\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EOnSelfChange\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EPreserveCurrent\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUser changes their own credential\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EOnAdminChange\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003ERevokeAll\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAdministrator changes a user\u0027s credential\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EOnRecovery\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003ERevokeAll\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECredential recovery completes\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-2\u0022\u003EExample\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.CredentialSessionPolicy.OnSelfChange = CredentialSessionBehavior.PreserveCurrent;\n options.CredentialSessionPolicy.OnAdminChange = CredentialSessionBehavior.RevokeAll;\n options.CredentialSessionPolicy.OnRecovery = CredentialSessionBehavior.RevokeAll;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-behaviors\u0022\u003EAvailable Behaviors\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EBehavior\u003C/th\u003E\n\u003Cth\u003EEffect\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPreserveAll\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EKeep existing session chains\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPreserveCurrent\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EKeep the current chain and revoke other applicable chains\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERevokeAll\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERevoke all applicable session chains\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EFor recovery flows, \u003Ccode\u003EPreserveCurrent\u003C/code\u003E is not always meaningful because the user may not have an authenticated current session.\u003C/p\u003E\n\u003Cp\u003EThe current implementation treats this situation using the more restrictive \u003Ccode\u003ERevokeAll\u003C/code\u003E behavior.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-recommendation-1\u0022\u003ESecurity Recommendation\u003C/h3\u003E\n\u003Cp\u003EPreserving the current session after a normal password change can provide a reasonable user experience.\u003C/p\u003E\n\u003Cp\u003ERevoking all sessions after administrative credential changes or account recovery is generally more conservative.\u003C/p\u003E\n\u003Cp\u003EChoose the policy according to the application\u0027s security requirements.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022credential-reset-options\u0022\u003E15. Credential Reset Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.ResetCredential\u003C/code\u003E configures credential recovery proof lifetimes and attempt limits.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022defaults\u0022\u003EDefaults\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETokenValidity\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E30 minutes\u003C/td\u003E\n\u003Ctd\u003EReset token lifetime\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECodeValidity\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E10 minutes\u003C/td\u003E\n\u003Ctd\u003ENumeric reset code lifetime\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxAttempts\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E3\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EMaximum permitted recovery attempts\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECodeLength\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E6\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENumeric reset code length\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-3\u0022\u003EExample\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.ResetCredential.TokenValidity = TimeSpan.FromMinutes(20);\n options.ResetCredential.CodeValidity = TimeSpan.FromMinutes(5);\n options.ResetCredential.MaxAttempts = 3;\n options.ResetCredential.CodeLength = 6;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003ECodeLength\u003C/code\u003E applies to numeric reset codes, not token-based recovery proofs.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022important-security-boundary\u0022\u003EImportant Security Boundary\u003C/h3\u003E\n\u003Cp\u003EReset proofs must be delivered through a trusted server-side mechanism.\u003C/p\u003E\n\u003Cp\u003EDo not return recovery tokens to anonymous callers simply to simplify a client-side reset form.\u003C/p\u003E\n\u003Cp\u003EThe application should provide the appropriate \u003Ccode\u003EICredentialResetNotifier\u003C/code\u003E implementation for supported recovery flows.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022login-identifier-options\u0022\u003E16. Login Identifier Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.LoginIdentifiers\u003C/code\u003E determines which identifier types can be used for login.\u003C/p\u003E\n\u003Cp\u003EThe default allowed types are:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUserIdentifierType.Username\nUserIdentifierType.Email\nUserIdentifierType.Phone\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022restrict-login-identifier-types\u0022\u003ERestrict Login Identifier Types\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.LoginIdentifiers.AllowedTypes =\n new HashSet\u0026lt;UserIdentifierType\u0026gt;\n {\n UserIdentifierType.Email\n };\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis configures email as the allowed login identifier type.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022require-verification\u0022\u003ERequire Verification\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.LoginIdentifiers.RequireVerificationForEmail = true;\n options.LoginIdentifiers.RequireVerificationForPhone = true;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThese settings control whether the corresponding identifiers must be verified before being accepted for login.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022custom-resolvers\u0022\u003ECustom Resolvers\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableCustomResolvers\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECustomResolversFirst\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThese options control the participation and ordering of custom login identifier resolvers.\u003C/p\u003E\n\u003Cp\u003EAn identifier existing on a user account does not necessarily mean it is permitted for login.\u003C/p\u003E\n\u003Cp\u003EIdentifier management and login eligibility are separate concerns.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022identifier-validation\u0022\u003E17. Identifier Validation\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.IdentifierValidation\u003C/code\u003E provides rules for usernames, email addresses, and phone numbers.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022username-rules\u0022\u003EUsername Rules\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.IdentifierValidation.UserName.Enabled = true;\n options.IdentifierValidation.UserName.MinLength = 3;\n options.IdentifierValidation.UserName.MaxLength = 32;\n options.IdentifierValidation.UserName.AllowedRegex = \u0026quot;^[a-zA-Z0-9._-]\u002B$\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022default-validation-rules\u0022\u003EDefault Validation Rules\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EIdentifier\u003C/th\u003E\n\u003Cth\u003EEnabled\u003C/th\u003E\n\u003Cth\u003EMinimum length\u003C/th\u003E\n\u003Cth\u003EMaximum length\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EUsername\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E3\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E64\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EEmail\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E3\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E256\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EPhone\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E3\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E24\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EUsername validation additionally supports \u003Ccode\u003EAllowedRegex\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe default username expression is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EREGEX\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-regex\u0022\u003E^[a-zA-Z0-9._-]\u002B$\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022identifier-behavior\u0022\u003EIdentifier Behavior\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Identifiers\u003C/code\u003E contains additional groups:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.Identifiers.Behavior\noptions.Identifiers.Validation\noptions.Identifiers.Normalization\noptions.Identifiers.Uniqueness\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThese configure identifier behavior beyond the top-level validation rules.\u003C/p\u003E\n\u003Cp\u003EIdentifier normalization and uniqueness are particularly important because they affect how duplicate identifiers are detected.\u003C/p\u003E\n\u003Cp\u003EUse the provided options rather than implementing inconsistent normalization independently in the client.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022identifier-limits\u0022\u003EIdentifier Limits\u003C/h3\u003E\n\u003Cp\u003EUltimateAuth supports configurable limits on the number of identifiers associated with each user.\u003C/p\u003E\n\u003Cp\u003EBy default, a user can have up to \u003Cstrong\u003E10 active identifiers\u003C/strong\u003E, regardless of their type.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.Identifiers.Behavior.MaxIdentifiersPerUser = 10;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis is a global per-user limit. It applies to the combined number of usernames, email addresses, phone numbers, and custom identifiers.\u003C/p\u003E\n\u003Ch4 id=\u0022optional-per-type-limits\u0022\u003EOptional Per-Type Limits\u003C/h4\u003E\n\u003Cp\u003EApplications can optionally define limits for individual identifier types:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.Identifiers.Behavior.MaxIdentifiersPerUser = 10;\n\noptions.Identifiers.Behavior.MaxIdentifiersPerType = new()\n{\n [UserIdentifierType.Username] = 2,\n [UserIdentifierType.Email] = 5,\n [UserIdentifierType.Phone] = 3\n};\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Cstrong\u003EPer-type limits are not configured by default.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EWhen no per-type limit is specified, only the global limit applies.\u003C/p\u003E\n\u003Cp\u003EFor example, with the configuration above, a user may have up to five email addresses, but the combined number of all active identifiers cannot exceed ten.\u003C/p\u003E\n\u003Ch4 id=\u0022default-behavior\u0022\u003EDefault Behavior\u003C/h4\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EOption\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxIdentifiersPerUser\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E10\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EMaximum total active identifiers per user\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxIdentifiersPerType\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEmpty dictionary\u003C/td\u003E\n\u003Ctd\u003EOptional limits for individual identifier types\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe following rules apply:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EPrimary and non-primary identifiers count equally.\u003C/li\u003E\n\u003Cli\u003ESoft-deleted identifiers do not count toward active limits.\u003C/li\u003E\n\u003Cli\u003ESelf-service and administrative operations follow the same limits.\u003C/li\u003E\n\u003Cli\u003EPer-type limits do not override the global limit.\u003C/li\u003E\n\u003Cli\u003EExisting identifier behavior options remain applicable.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EFor example, \u003Ccode\u003EAllowMultipleUsernames = false\u003C/code\u003E still prevents multiple usernames, even when the configured username limit is greater than one.\u003C/p\u003E\n\u003Ch4 id=\u0022limit-validation\u0022\u003ELimit Validation\u003C/h4\u003E\n\u003Cp\u003EWhen an identifier addition exceeds a configured limit, the operation is rejected with a validation error.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EError Code\u003C/th\u003E\n\u003Cth\u003EMeaning\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003Eidentifier_total_limit_exceeded\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EThe user has reached the global identifier limit\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003Eidentifier_type_limit_exceeded\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EThe user has reached the configured limit for that identifier type\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EApplications should handle these errors and display appropriate feedback.\u003C/p\u003E\n\u003Cp\u003EIdentifier limits are enforced by the Users application layer rather than individual persistence stores.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022user-profile-options\u0022\u003E18. User Profile Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.UserProfile\u003C/code\u003E controls profile-related capabilities.\u003C/p\u003E\n\u003Cp\u003EThe current option is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.UserProfile.EnableMultiProfile\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EIts default is \u003Ccode\u003Efalse\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022enable-multiple-profiles\u0022\u003EEnable Multiple Profiles\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.UserProfile.EnableMultiProfile = true;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis allows applications to use UltimateAuth\u0027s multiple-profile capability where supported by the user domain.\u003C/p\u003E\n\u003Cp\u003EFor example, an application may maintain separate profile contexts for different purposes.\u003C/p\u003E\n\u003Cp\u003EMulti-profile support does not create separate user identities or independent authentication accounts.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022multi-tenant-options\u0022\u003E19. Multi-Tenant Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.MultiTenant\u003C/code\u003E controls whether tenant-aware behavior is enabled and how tenant identifiers are resolved.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-properties-4\u0022\u003EAvailable Properties\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnabled\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEnable multi-tenant behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ENormalizeToLowercase\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENormalize tenant identifiers\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableRoute\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EResolve tenants from routes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableHeader\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EResolve tenants from headers\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableDomain\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Efalse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EResolve tenants from hosts\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EHeaderName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EX-Tenant\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETenant header name\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-header-based-tenant-resolution\u0022\u003EExample: Header-Based Tenant Resolution\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.MultiTenant.Enabled = true;\n options.MultiTenant.EnableHeader = true;\n options.MultiTenant.HeaderName = \u0026quot;X-Tenant\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-domain-based-resolution\u0022\u003EExample: Domain-Based Resolution\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.MultiTenant.Enabled = true;\n options.MultiTenant.EnableDomain = true;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EMultiple resolution mechanisms can be enabled.\u003C/p\u003E\n\u003Cp\u003EThe server composes the configured tenant resolvers according to its resolution infrastructure.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-boundary\u0022\u003ESecurity Boundary\u003C/h3\u003E\n\u003Cp\u003ETenant identification is not tenant authorization.\u003C/p\u003E\n\u003Cp\u003EA request providing another tenant identifier must not gain access to that tenant without appropriate authorization.\u003C/p\u003E\n\u003Cp\u003EThe server remains responsible for tenant isolation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022pagination-options\u0022\u003E20. Pagination Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Pagination\u003C/code\u003E controls pagination defaults and maximum page sizes.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDefaultPageSize\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E250\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMaxPageSize\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E1000\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-4\u0022\u003EExample\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Pagination.DefaultPageSize = 50;\n options.Pagination.MaxPageSize = 200;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThese settings affect paginated queries exposed by UltimateAuth domain services.\u003C/p\u003E\n\u003Cp\u003EFor administrative interfaces, choosing a moderate page size can reduce unnecessary data transfer and processing.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022navigation-options\u0022\u003E21. Navigation Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Navigation\u003C/code\u003E configures automatic authentication navigation behavior.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-properties-5\u0022\u003EAvailable Properties\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEnableAutomaticNavigationRedirect\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Etrue\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELoginResolver\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAccessDeniedResolver\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Enull\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022disable-automatic-redirects\u0022\u003EDisable Automatic Redirects\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Navigation.EnableAutomaticNavigationRedirect = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022custom-login-redirect\u0022\u003ECustom Login Redirect\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Navigation.LoginResolver = context =\u0026gt;\n {\n return \u0026quot;/account/login\u0026quot;;\n };\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022custom-access-denied-redirect\u0022\u003ECustom Access Denied Redirect\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Navigation.AccessDeniedResolver = context =\u0026gt;\n {\n return \u0026quot;/account/access-denied\u0026quot;;\n };\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThese delegates receive the current \u003Ccode\u003EHttpContext\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe application should return valid, trusted navigation destinations.\u003C/p\u003E\n\u003Cp\u003EAvoid constructing redirects from unvalidated user-controlled URLs.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022authentication-response-options\u0022\u003E22. Authentication Response Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.AuthResponse\u003C/code\u003E controls how authentication credentials and redirect behavior are represented in responses.\u003C/p\u003E\n\u003Cp\u003EThe available groups are:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.AuthResponse.SessionIdDelivery\noptions.AuthResponse.AccessTokenDelivery\noptions.AuthResponse.RefreshTokenDelivery\n\noptions.AuthResponse.Login\noptions.AuthResponse.Logout\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThese settings govern credential delivery and authentication response behavior.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022important-mode-specific-response-defaults\u0022\u003EImportant: Mode-Specific Response Defaults\u003C/h3\u003E\n\u003Cp\u003EUltimateAuth provides response templates for authentication modes.\u003C/p\u003E\n\u003Cp\u003EFor example, \u003Ccode\u003EPureOpaque\u003C/code\u003E uses a session-oriented cookie response, while \u003Ccode\u003EHybrid\u003C/code\u003E combines session and token delivery behavior.\u003C/p\u003E\n\u003Cp\u003EThe effective response may therefore depend on the resolved authentication mode and flow.\u003C/p\u003E\n\u003Cp\u003EDo not assume that a single globally configured response delivery setting applies unchanged to every authentication mode.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-considerations\u0022\u003ESecurity Considerations\u003C/h3\u003E\n\u003Cp\u003ECredential delivery settings determine whether sensitive authentication material is exposed through cookies, headers, or other response mechanisms.\u003C/p\u003E\n\u003Cp\u003EOnly customize them when the application has a clear requirement and the selected transport is appropriate for the client platform.\u003C/p\u003E\n\u003Cp\u003EFor advanced response customization, see \u003Ca href=\u0022/docs/configuration/advanced-configuration\u0022\u003EAdvanced Configuration\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022diagnostics-options\u0022\u003E23. Diagnostics Options\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Diagnostics\u003C/code\u003E provides optional diagnostic behavior.\u003C/p\u003E\n\u003Cp\u003EThe current property is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.Diagnostics.EnableRefreshDetails\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EIts default is \u003Ccode\u003Efalse\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-5\u0022\u003EExample\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Diagnostics.EnableRefreshDetails = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWhen enabled, the option allows additional refresh-related diagnostic response information, such as the \u003Ccode\u003EX-UAuth-Refresh\u003C/code\u003E header.\u003C/p\u003E\n\u003Cp\u003EThis is primarily useful during development and debugging.\u003C/p\u003E\n\u003Cp\u003EAvoid enabling detailed authentication diagnostics in production unless the exposure is understood and justified.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022authentication-lifecycle-events\u0022\u003E24. Authentication Lifecycle Events\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003Eoptions.Events\u003C/code\u003E exposes server-side authentication lifecycle callbacks.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-events\u0022\u003EAvailable Events\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.Events.OnAnyEvent\noptions.Events.OnUserLoggedIn\noptions.Events.OnUserLoggedOut\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-login-logging\u0022\u003EExample: Login Logging\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Events.OnUserLoggedIn = context =\u0026gt;\n {\n // Record the successful login.\n return Task.CompletedTask;\n };\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-global-event-handler\u0022\u003EExample: Global Event Handler\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Events.OnAnyEvent = context =\u0026gt;\n {\n // Process an authentication lifecycle event.\n return Task.CompletedTask;\n };\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ELifecycle events are intended for lightweight reactions such as logging, metrics, and notifications.\u003C/p\u003E\n\u003Cp\u003EThey should not replace security decisions, authentication validation, or application policies.\u003C/p\u003E\n\u003Cp\u003ESee \u003Ca href=\u0022/docs/configuration/advanced-configuration\u0022\u003EAdvanced Configuration\u003C/a\u003E for event and extension guidance.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022effective-server-options\u0022\u003E25. Effective Server Options\u003C/h2\u003E\n\u003Cp\u003EThe configured \u003Ccode\u003EUAuthServerOptions\u003C/code\u003E instance is the base configuration.\u003C/p\u003E\n\u003Cp\u003EAt runtime, UltimateAuth can resolve:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EEffectiveUAuthServerOptions\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe current resolution process is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EConfigured UAuthServerOptions\n |\n v\n Resolve auth mode\n |\n v\n Clone base options\n |\n v\n Apply mode defaults\n |\n v\n Apply ConfigureMode overrides\n |\n v\n EffectiveUAuthServerOptions\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis means that the effective settings used for a \u003Ccode\u003EHybrid\u003C/code\u003E client can differ from those used for a \u003Ccode\u003EPureOpaque\u003C/code\u003E client.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-6\u0022\u003EExample\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Session.Lifetime = TimeSpan.FromDays(7);\n\n options.ConfigureMode(UAuthMode.Hybrid, mode =\u0026gt;\n {\n mode.Token.AccessTokenLifetime = TimeSpan.FromMinutes(5);\n });\n\n options.ConfigureMode(UAuthMode.PureOpaque, mode =\u0026gt;\n {\n mode.Session.IdleTimeout = TimeSpan.FromDays(3);\n });\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe original options remain the common starting point.\u003C/p\u003E\n\u003Cp\u003EMode defaults and overrides determine the effective values for the resolved mode.\u003C/p\u003E\n\u003Cp\u003EFor implementation details and inspection of effective options, see \u003Ca href=\u0022/docs/configuration/advanced-configuration\u0022\u003EAdvanced Configuration\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022validation-and-configuration-safety\u0022\u003E26. Validation and Configuration Safety\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth Server registers multiple options validators and enables startup validation.\u003C/p\u003E\n\u003Cp\u003EThese validators cover areas including:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003ELogin settings.\u003C/li\u003E\n\u003Cli\u003ESession settings.\u003C/li\u003E\n\u003Cli\u003EToken settings.\u003C/li\u003E\n\u003Cli\u003EPKCE.\u003C/li\u003E\n\u003Cli\u003EMulti-tenancy.\u003C/li\u003E\n\u003Cli\u003EIdentifier configuration.\u003C/li\u003E\n\u003Cli\u003ESession resolution.\u003C/li\u003E\n\u003Cli\u003EPagination.\u003C/li\u003E\n\u003Cli\u003EGeneral server configuration.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-invalid-session-lifetime\u0022\u003EExample: Invalid Session Lifetime\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Session.Lifetime = TimeSpan.Zero;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe server rejects an invalid session lifetime under the applicable validation rules.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022example-invalid-base-path\u0022\u003EExample: Invalid Base Path\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.Endpoints.BasePath = \u0026quot;//auth\u0026quot;;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe general server validator rejects paths containing \u003Ccode\u003E//\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022unsupported-modes\u0022\u003EUnsupported Modes\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.AllowedModes =\n [\n UAuthMode.PureJwt\n ];\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe current validator rejects explicitly enabling \u003Ccode\u003EPureJwt\u003C/code\u003E because that mode is not yet implemented as a supported server mode.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022validation-limitations\u0022\u003EValidation Limitations\u003C/h3\u003E\n\u003Cp\u003EStartup validation protects the configured options according to the registered validators.\u003C/p\u003E\n\u003Cp\u003EIt does not guarantee that every custom service implementation or mode-specific override is secure.\u003C/p\u003E\n\u003Cp\u003EWhen using advanced customization, test the resulting effective configuration and authentication flows.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022complete-configuration-example\u0022\u003E27. Complete Configuration Example\u003C/h2\u003E\n\u003Cp\u003EThe following example combines several commonly used server settings.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n // Authentication modes\n options.AllowedModes =\n [\n UAuthMode.PureOpaque,\n UAuthMode.Hybrid\n ];\n\n // Login security\n options.Login.MaxFailedAttempts = 5;\n options.Login.LockoutDuration = TimeSpan.FromMinutes(15);\n options.Login.IncludeFailureDetails = false;\n\n // Sessions\n options.Session.Lifetime = TimeSpan.FromDays(7);\n options.Session.MaxLifetime = TimeSpan.FromDays(30);\n\n // Token identity\n options.Token.Issuer = \u0026quot;MyApplication\u0026quot;;\n options.Token.Audience = \u0026quot;MyApplicationApi\u0026quot;;\n\n // Hybrid-specific token lifetime\n options.ConfigureMode(UAuthMode.Hybrid, mode =\u0026gt;\n {\n mode.Token.AccessTokenLifetime = TimeSpan.FromMinutes(10);\n });\n\n // PKCE\n options.Pkce.AuthorizationCodeLifetimeSeconds = 120;\n\n // Hub deployment\n options.HubDeploymentMode = UAuthHubDeploymentMode.Integrated;\n options.Hub.ClientBaseAddress = \u0026quot;https://app.example.com\u0026quot;;\n options.Hub.AllowedClientOrigins.Add(\u0026quot;https://app.example.com\u0026quot;);\n\n // Credential security\n options.CredentialSessionPolicy.OnSelfChange = CredentialSessionBehavior.PreserveCurrent;\n options.CredentialSessionPolicy.OnAdminChange = CredentialSessionBehavior.RevokeAll;\n options.CredentialSessionPolicy.OnRecovery = CredentialSessionBehavior.RevokeAll;\n\n // Credential recovery\n options.ResetCredential.TokenValidity = TimeSpan.FromMinutes(30);\n options.ResetCredential.MaxAttempts = 3;\n\n // Identifier login rules\n options.LoginIdentifiers.RequireVerificationForEmail = true;\n\n // Session transport\n options.SessionResolution.EnableCookie = true;\n options.SessionResolution.EnableBearer = true;\n options.SessionResolution.EnableQuery = false;\n\n // Endpoint exposure\n options.Endpoints.Authentication = true;\n options.Endpoints.Session = true;\n options.Endpoints.Credentials = true;\n options.Endpoints.Authorization = true;\n\n // Pagination\n options.Pagination.DefaultPageSize = 50;\n options.Pagination.MaxPageSize = 200;\n\n // Diagnostics\n options.Diagnostics.EnableRefreshDetails = false;\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis is an illustrative configuration, not a universal production preset.\u003C/p\u003E\n\u003Cp\u003EApplications should choose session lifetimes, authentication modes, credential transport, tenant behavior, and endpoint exposure according to their own deployment and security requirements.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022common-configuration-mistakes\u0022\u003E28. Common Configuration Mistakes\u003C/h2\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configuring-core-options-separately\u0022\u003EConfiguring Core Options Separately\u003C/h3\u003E\n\u003Cp\u003EWhen using \u003Ccode\u003EAddUltimateAuthServer()\u003C/code\u003E, configure Core-defined options through \u003Ccode\u003EUAuthServerOptions\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EA separate \u003Ccode\u003EUAuthOptions\u003C/code\u003E configuration does not automatically override the server\u0027s composed options.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022assuming-allowedmodes-selects-the-mode\u0022\u003EAssuming AllowedModes Selects the Mode\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EAllowedModes\u003C/code\u003E is a restriction.\u003C/p\u003E\n\u003Cp\u003EMode selection is performed by the authentication mode resolver.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022ignoring-mode-defaults\u0022\u003EIgnoring Mode Defaults\u003C/h3\u003E\n\u003Cp\u003EMode-specific defaults can replace globally configured values.\u003C/p\u003E\n\u003Cp\u003EUse \u003Ccode\u003EConfigureMode()\u003C/code\u003E for explicit mode-specific behavior.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022enabling-unsupported-modes\u0022\u003EEnabling Unsupported Modes\u003C/h3\u003E\n\u003Cp\u003EAn authentication mode appearing in an enum does not guarantee runtime support.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022treating-reserved-session-options-as-enforced-security-features\u0022\u003ETreating Reserved Session Options as Enforced Security Features\u003C/h3\u003E\n\u003Cp\u003ESome device and session-chain options are reserved for future enforcement.\u003C/p\u003E\n\u003Cp\u003EVerify actual runtime support before relying on them.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022exposing-unnecessary-endpoints\u0022\u003EExposing Unnecessary Endpoints\u003C/h3\u003E\n\u003Cp\u003EDisable endpoint groups or actions that the application does not require.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022allowing-query-string-credentials-without-a-specific-need\u0022\u003EAllowing Query-String Credentials Without a Specific Need\u003C/h3\u003E\n\u003Cp\u003EQuery-string credentials can leak through URLs and infrastructure logs.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022weakening-cookie-security\u0022\u003EWeakening Cookie Security\u003C/h3\u003E\n\u003Cp\u003EDo not relax secure transport or cookie protections without understanding the consequences.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022assuming-configuration-validation-covers-everything\u0022\u003EAssuming Configuration Validation Covers Everything\u003C/h3\u003E\n\u003Cp\u003EStartup validation does not replace testing, infrastructure security, or validation of custom extension implementations.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022server-options-quick-reference\u0022\u003E29. Server Options Quick Reference\u003C/h2\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EConfiguration group\u003C/th\u003E\n\u003Cth\u003ETypical use\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELogin\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELogin attempts and lockout\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESession\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESession expiration and lifecycle\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EToken\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EToken issuance and lifetime\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPkce\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPKCE code lifetime\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAllowedModes\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPermitted authentication modes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMultiTenant\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETenant resolution\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPagination\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EQuery page limits\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEndpoints\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEndpoint exposure\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESessionResolution\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECredential source precedence\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECookie\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECookie security and scope\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EHubDeploymentMode\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EHub deployment topology\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EHub\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EHub integration\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPrimaryCredential\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUI/API primary credential selection\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAuthResponse\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECredential delivery\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECredentialSessionPolicy\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ESession invalidation after credential changes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EResetCredential\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECredential recovery rules\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdentifiers\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifier behavior\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIdentifierValidation\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EIdentifier validation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ELoginIdentifiers\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELogin identifier eligibility\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserProfile\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EMulti-profile capability\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ENavigation\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication redirects\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDiagnostics\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERefresh diagnostics\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EEvents\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthentication lifecycle callbacks\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EConfigureMode()\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EMode-specific option overrides\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EOnConfigureEndpoints\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAdvanced endpoint customization\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022next-steps\u0022\u003ENext Steps\u003C/h2\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/configuration/configuration\u0022\u003EConfiguration Overview\u003C/a\u003E \u2014 Understand the configuration architecture and precedence.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/configuration/client-options\u0022\u003EClient Options\u003C/a\u003E \u2014 Configure client SDK behavior.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/configuration/advanced-configuration\u0022\u003EAdvanced Configuration\u003C/a\u003E \u2014 Explore effective options, mode resolution, and extension points.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/client/session-management\u0022\u003ESession Management\u003C/a\u003E \u2014 Manage sessions through the client.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/client/authentication\u0022\u003EAuthentication\u003C/a\u003E \u2014 Use the client authentication flows.\u003C/li\u003E\n\u003C/ul\u003E\n",
"Headings": [
{
"Id": "register-ultimateauth-server",
@@ -544,6 +544,6 @@
"Level": 0
}
],
- "SourceHash": "F71BFB8BC585AF45F096355BDE4989040EEE7545BB8A3B476585E7B77731C19D",
+ "SourceHash": "01AEC87897DF9AAC89F96EEC5B097505FB71A6F6140A7706A099932C15092C36",
"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 ab0acd2b..fde9e879 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
@@ -272,6 +272,13 @@
"Group": "security",
"GroupOrder": 8
},
+ {
+ "Title": "Rate Limiting",
+ "Slug": "security/rate-limiting",
+ "Order": 8,
+ "Group": "security",
+ "GroupOrder": 8
+ },
{
"Title": "How-to Guides",
"Slug": "how-to-guides/index",
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 65894848..17e02bae 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\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",
+ "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=\u0022client-ip-address-reverse-proxy\u0022\u003E12. Client IP Address \u0026amp; Reverse Proxy\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth uses ASP.NET Core\u0027s resolved client IP address for device information and IP-based rate limiting. It does not directly trust \u003Ccode\u003EX-Forwarded-For\u003C/code\u003E or \u003Ccode\u003ECF-Connecting-IP\u003C/code\u003E headers.\u003C/p\u003E\n\u003Cp\u003EIf your application runs behind Caddy, Nginx, or another reverse proxy, configure ASP.NET Core to trust that proxy.\u003C/p\u003E\n\u003Cp\u003EIn \u003Ccode\u003EProgram.cs\u003C/code\u003E, before \u003Ccode\u003Ebuilder.Build()\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 System.Net;\nusing Microsoft.AspNetCore.HttpOverrides;\n\nbuilder.Services.Configure\u0026lt;ForwardedHeadersOptions\u0026gt;(options =\u0026gt;\n{\n options.ForwardedHeaders =\n ForwardedHeaders.XForwardedFor |\n ForwardedHeaders.XForwardedProto;\n\n // Replace with your actual Caddy or Nginx proxy IP.\n options.KnownProxies.Add(IPAddress.Parse(\u0026quot;172.20.0.5\u0026quot;));\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThen enable the middleware \u003Cstrong\u003Ebefore\u003C/strong\u003E UltimateAuth and HTTPS redirection:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 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.UseForwardedHeaders();\n\napp.UseHttpsRedirection();\napp.UseUltimateAuthWithAspNetCore();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe example IP \u003Ccode\u003E172.20.0.5\u003C/code\u003E represents a trusted reverse proxy, not the visitor\u0027s IP address. Replace it with your actual proxy IP. For Docker deployments, ensure the proxy address is stable or configure a narrowly scoped trusted network.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EImportant:\u003C/strong\u003E Never trust arbitrary forwarded headers or clear the trusted proxy lists to accept all sources. If your application connects directly to Kestrel without a reverse proxy, no forwarded headers configuration is required.\u003C/p\u003E\n\u003Cp\u003EFor multi-proxy deployments, configure the complete trusted proxy chain and forwarding behavior according to your infrastructure.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022development-and-production-differences\u0022\u003E13. 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\u003E14. 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 Reverse proxy headers are configured correctly if applicable.\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\u003E15. 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",
@@ -278,9 +278,14 @@
"Text": "Configuration files",
"Level": 1
},
+ {
+ "Id": "client-ip-address-reverse-proxy",
+ "Text": "12. Client IP Address \u0026 Reverse Proxy",
+ "Level": 0
+ },
{
"Id": "development-and-production-differences",
- "Text": "12. Development and Production Differences",
+ "Text": "13. Development and Production Differences",
"Level": 0
},
{
@@ -300,12 +305,12 @@
},
{
"Id": "deployment-checklist",
- "Text": "13. Deployment Checklist",
+ "Text": "14. Deployment Checklist",
"Level": 0
},
{
"Id": "choosing-your-architecture",
- "Text": "14. Choosing Your Architecture",
+ "Text": "15. Choosing Your Architecture",
"Level": 0
},
{
@@ -334,6 +339,6 @@
"Level": 0
}
],
- "SourceHash": "770C2ACEAC642791FB3C618D54F7EB1108DF9651ABFBF28CD7D5700EB25EF255",
+ "SourceHash": "B1EA20A2729E11F13A9E2D7B12F6D1519AD7A45FE3DBFB4A9D79C01E06D0ED5A",
"BuilderVersion": "1"
}
\ No newline at end of file
diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/security/rate-limiting.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/security/rate-limiting.json
new file mode 100644
index 00000000..6ae687ac
--- /dev/null
+++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/security/rate-limiting.json
@@ -0,0 +1,154 @@
+{
+ "Slug": "security/rate-limiting",
+ "Title": "Rate Limiting",
+ "Html": "\n\u003Cp\u003EUltimateAuth provides built-in HTTP endpoint rate limiting using ASP.NET Core\u0027s native rate limiting infrastructure, together with client-side handling of HTTP 429 responses.\u003C/p\u003E\n\u003Cp\u003ERate limiting helps reduce excessive authentication requests, credential attacks, recovery abuse, and unnecessary session traffic without introducing a separate persistence dependency.\u003C/p\u003E\n\u003Cp\u003EThe server enforces request limits. UltimateAuth clients recognize throttled responses, preserve valid authentication state, and defer selected background operations when appropriate.\u003C/p\u003E\n\u003Cp\u003ENo additional rate limiting package is required.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022getting-started\u0022\u003EGetting started\u003C/h2\u003E\n\u003Cp\u003ERate limiting is enabled by default when using the integrated ASP.NET Core middleware helper.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 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.RateLimiting.Standard.PermitLimit = 240;\n options.RateLimiting.Login.PermitLimit = 40;\n\n options.RateLimiting.Endpoints[UAuthEndpoint.Refresh] = new()\n {\n PermitLimit = 1200,\n Window = TimeSpan.FromMinutes(1)\n };\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EConfigure the middleware pipeline:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eapp.UseForwardedHeaders();\napp.UseRouting();\n\napp.UseUltimateAuthWithAspNetCore();\n\napp.UseAntiforgery();\n\napp.MapUltimateAuthEndpoints();\napp.MapUAuthHub();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EImport \u003Ccode\u003ECodeBeam.UltimateAuth.Server.RateLimiting\u003C/code\u003E for rate limiting rules and endpoint identifiers.\u003C/p\u003E\n\u003Cp\u003EConfigure forwarded headers only when appropriate for the deployment, using explicitly trusted proxies and networks.\u003C/p\u003E\n\u003Cp\u003ERate limiting options are also available through the \u003Ccode\u003EUltimateAuth:Server:RateLimiting\u003C/code\u003E configuration section. The existing server options binding behavior applies, including configuration-file precedence.\u003C/p\u003E\n\u003Cp\u003ERate limiting settings are configured at startup. They are not automatically scoped to individual clients or authentication modes.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022middleware-integration\u0022\u003EMiddleware integration\u003C/h2\u003E\n\u003Cp\u003EThe integrated helper exposes a single method with two optional parameters:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Epublic static IApplicationBuilder UseUltimateAuthWithAspNetCore(\n this IApplicationBuilder app,\n bool? enableCors = null,\n bool useRateLimiting = true)\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe default middleware order is:\u003C/p\u003E\n\u003Col\u003E\n\u003Cli\u003ECORS, when enabled or required by UAuthHub\u003C/li\u003E\n\u003Cli\u003EUltimateAuth context middleware\u003C/li\u003E\n\u003Cli\u003EASP.NET Core authentication\u003C/li\u003E\n\u003Cli\u003EASP.NET Core rate limiting\u003C/li\u003E\n\u003Cli\u003EASP.NET Core authorization\u003C/li\u003E\n\u003C/ol\u003E\n\u003Cp\u003EThis places the rate limiter after authentication, allowing authenticated endpoint policies to use validated UltimateAuth actor information.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022configuration-examples\u0022\u003EConfiguration examples\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\u003E// Automatic CORS selection; rate limiting enabled\napp.UseUltimateAuthWithAspNetCore();\n\n// Explicit CORS; rate limiting enabled\napp.UseUltimateAuthWithAspNetCore(enableCors: true);\n\n// Automatic CORS; rate limiting disabled\napp.UseUltimateAuthWithAspNetCore(useRateLimiting: false);\n\n// Explicit configuration of both options\napp.UseUltimateAuthWithAspNetCore(enableCors: true, useRateLimiting: true);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EenableCors = null\u003C/code\u003E automatically enables CORS when required by UAuthHub. Setting it to \u003Ccode\u003Efalse\u003C/code\u003E does not remove the host\u0027s responsibility to configure CORS when needed.\u003C/p\u003E\n\u003Cp\u003EThe standalone UltimateAuth middleware helper does not install ASP.NET Core authentication. If authentication has not already established a validated UltimateAuth actor, rate limiting partitioning falls back to the connection IP.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022existing-application-rate-limiters\u0022\u003EExisting application rate limiters\u003C/h3\u003E\n\u003Cp\u003EIf the application already installs its own \u003Ccode\u003EUseRateLimiter()\u003C/code\u003E middleware, disable the helper-owned installation:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 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.UseRouting();\napp.UseAuthentication();\n\napp.UseUltimateAuthWithAspNetCore(useRateLimiting: false);\n\napp.UseRateLimiter();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis illustrates middleware ownership rather than a universal pipeline layout. Hosts must preserve the required routing, authentication, CORS, and authorization order and avoid duplicate middleware registration.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth cannot detect a separately installed native rate limiting middleware instance.\u003C/p\u003E\n\u003Cp\u003ERepeated calls to the UltimateAuth helper install at most one helper-owned rate limiter on the same application builder. Other middleware installed by the helper is not deduplicated.\u003C/p\u003E\n\u003Cp\u003EAn application builder created with \u003Ccode\u003EApplicationBuilder.New()\u003C/code\u003E has its own middleware installation lifecycle.\u003C/p\u003E\n\u003Cp\u003EDisabling UltimateAuth\u0027s built-in endpoint policies does not disable an application-owned global limiter or native application policies.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022rules-and-default-limits\u0022\u003ERules and default limits\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth uses native sliding-window limiters with six segments and no request queue.\u003C/p\u003E\n\u003Cp\u003EThe default rule groups are:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003ERule\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003Cth\u003ETypical operations\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ESensitive\u003C/td\u003E\n\u003Ctd\u003E30 requests/minute\u003C/td\u003E\n\u003Ctd\u003ESensitive credential and recovery operations\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EStandard\u003C/td\u003E\n\u003Ctd\u003E120 requests/minute\u003C/td\u003E\n\u003Ctd\u003EAuthenticated management operations\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EFrequent\u003C/td\u003E\n\u003Ctd\u003E600 requests/minute\u003C/td\u003E\n\u003Ctd\u003EValidation, refresh, logout, authorization checks\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ELogin\u003C/td\u003E\n\u003Ctd\u003E20 requests/minute/IP\u003C/td\u003E\n\u003Ctd\u003EShared credential-verification budget\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThese are starting defaults, not recommended universal production limits. Adjust them based on application traffic, expected concurrency, and deployment conditions.\u003C/p\u003E\n\u003Cp\u003EGroup rules act as templates for separate endpoint budgets. For example, validation and refresh do not share a counter merely because both use the Frequent group.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022shared-login-budget\u0022\u003EShared login budget\u003C/h3\u003E\n\u003Cp\u003EThe Login rule is shared by:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003ELogin\u003C/li\u003E\n\u003Cli\u003ETryLogin\u003C/li\u003E\n\u003Cli\u003EPkceComplete\u003C/li\u003E\n\u003Cli\u003EPkceTryComplete\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThese credential-verification operations share the same budget for a partition.\u003C/p\u003E\n\u003Cp\u003EConfigure the shared limit 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.RateLimiting.Login.PermitLimit = 40;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAn explicit endpoint override replaces that endpoint\u0027s rule and separates it from the shared login budget.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022additional-sensitive-defaults\u0022\u003EAdditional sensitive defaults\u003C/h3\u003E\n\u003Cp\u003ECertain operations have stricter defaults than the Sensitive group:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EOperation\u003C/th\u003E\n\u003Cth\u003EDefault\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAnonymous registration\u003C/td\u003E\n\u003Ctd\u003E10 requests/10 minutes/IP\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAnonymous credential reset initiation\u003C/td\u003E\n\u003Ctd\u003E5 requests/10 minutes/IP\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAnonymous credential reset completion\u003C/td\u003E\n\u003Ctd\u003E10 requests/10 minutes/IP\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ESelf identifier verification\u003C/td\u003E\n\u003Ctd\u003E10 requests/10 minutes/IP\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThese numeric defaults take precedence over the Sensitive group\u0027s numeric settings.\u003C/p\u003E\n\u003Cp\u003EFor example, increasing \u003Ccode\u003ESensitive.PermitLimit\u003C/code\u003E does not automatically increase the anonymous reset initiation limit.\u003C/p\u003E\n\u003Cp\u003EUse an explicit endpoint override to change these limits.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022per-endpoint-configuration\u0022\u003EPer-endpoint configuration\u003C/h2\u003E\n\u003Cp\u003EIndividual endpoint rules can be configured using \u003Ccode\u003ERateLimiting.Endpoints\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EFor example, limit session validation to two requests per minute:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 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.RateLimiting.Endpoints[UAuthEndpoint.Validate] = new()\n {\n PermitLimit = 2,\n Window = TimeSpan.FromMinutes(1)\n };\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe third request within the applicable sliding window receives HTTP 429 when the budget is exhausted.\u003C/p\u003E\n\u003Cp\u003EThis example is useful for testing client behavior, but two validations per minute may be too restrictive for a real application with automatic session validation.\u003C/p\u003E\n\u003Cp\u003EEndpoint overrides are complete replacement rules, not partial changes merged with group numeric settings.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022policy-precedence\u0022\u003EPolicy precedence\u003C/h3\u003E\n\u003Cp\u003EUltimateAuth resolves endpoint policies in the following order:\u003C/p\u003E\n\u003Col\u003E\n\u003Cli\u003ENative application endpoint/group rate limiting metadata, including native disable metadata\u003C/li\u003E\n\u003Cli\u003EExplicit \u003Ccode\u003EEndpoints[id]\u003C/code\u003E replacement\u003C/li\u003E\n\u003Cli\u003EShared Login rule for credential-verification operations\u003C/li\u003E\n\u003Cli\u003ESpecial registration, reset, and verification defaults\u003C/li\u003E\n\u003Cli\u003EEndpoint group template\u003C/li\u003E\n\u003C/ol\u003E\n\u003Cp\u003EAn explicit endpoint override can replace a built-in rule, select an application-owned named policy, or disable the built-in policy for that endpoint.\u003C/p\u003E\n\u003Cp\u003EAll UltimateAuth endpoint mappings have explicit classifications. Endpoint registration fails when a newly mapped endpoint is missing its required classification.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022disabling-rate-limiting\u0022\u003EDisabling rate limiting\u003C/h2\u003E\n\u003Cp\u003ETo disable UltimateAuth\u0027s built-in endpoint policies globally:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 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.RateLimiting.Enabled = false;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ETo disable an individual built-in rule:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 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.RateLimiting.Endpoints[UAuthEndpoint.Validate] = new()\n{\n Enabled = false\n};\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EA disabled UltimateAuth rule does not add native \u003Ccode\u003EDisableRateLimiting\u003C/code\u003E metadata.\u003C/p\u003E\n\u003Cp\u003EApplication-owned policies and global limiters may still apply.\u003C/p\u003E\n\u003Cp\u003ETo prevent the integrated helper from installing the native middleware:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 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.UseUltimateAuthWithAspNetCore(useRateLimiting: false);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThese controls have different responsibilities:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003ERateLimiting.Enabled\u003C/code\u003E controls UltimateAuth\u0027s built-in policies.\u003C/li\u003E\n\u003Cli\u003EIndividual rule \u003Ccode\u003EEnabled\u003C/code\u003E settings control specific built-in rules.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EuseRateLimiting\u003C/code\u003E controls whether the integrated helper installs the middleware.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022using-application-owned-policies\u0022\u003EUsing application-owned policies\u003C/h2\u003E\n\u003Cp\u003EApplications can use their own native ASP.NET Core policies instead of an UltimateAuth built-in rule.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 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.AddRateLimiter(options =\u0026gt;\n{\n options.AddPolicy\u0026lt;string, MyRateLimiterPolicy\u0026gt;(\u0026quot;my-auth-policy\u0026quot;);\n});\n\nbuilder.Services.AddUltimateAuthServer(options =\u0026gt;\n{\n options.RateLimiting.Endpoints[UAuthEndpoint.UserCreateAnonymous] = new()\n {\n PolicyName = \u0026quot;my-auth-policy\u0026quot;\n };\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ENamed policies must be registered with the native rate limiting options used by the middleware.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth\u0027s built-in policies are inline native policies and do not reserve application policy names.\u003C/p\u003E\n\u003Cp\u003EEndpoint policies do not automatically compose. Native ASP.NET Core endpoint/group precedence applies.\u003C/p\u003E\n\u003Cp\u003ENative \u003Ccode\u003EDisableRateLimiting\u003C/code\u003E metadata also follows ASP.NET Core semantics and can bypass an application global limiter.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022rejection-callbacks\u0022\u003ERejection callbacks\u003C/h3\u003E\n\u003Cp\u003EBuilt-in UltimateAuth inline policies produce their own generic HTTP 429 response.\u003C/p\u003E\n\u003Cp\u003EThey do not invoke the application\u0027s middleware-level \u003Ccode\u003EOnRejected\u003C/code\u003E callback.\u003C/p\u003E\n\u003Cp\u003EApplication-owned named policies can use native rejection behavior. When a named policy has no policy-level rejection callback, the effective native middleware \u003Ccode\u003EOnRejected\u003C/code\u003E callback applies.\u003C/p\u003E\n\u003Cp\u003EIf an application needs complete ownership of rejection responses for a particular endpoint, use an application-owned policy.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth does not replace application global limiters or application rejection settings.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022http-429-responses\u0022\u003EHTTP 429 responses\u003C/h2\u003E\n\u003Cp\u003EWhen a built-in UltimateAuth policy rejects a request, the server returns:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EHTTP status \u003Ccode\u003E429 Too Many Requests\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003EProblem Details content\u003C/li\u003E\n\u003Cli\u003EStable detail code \u003Ccode\u003Erate_limited\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003ECache-Control: no-store\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003ERetry-After\u003C/code\u003E when the native limiter supplies retry guidance\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EA representative response 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\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;title\u0026quot;: \u0026quot;Too Many Requests\u0026quot;,\n \u0026quot;status\u0026quot;: 429,\n \u0026quot;detail\u0026quot;: \u0026quot;rate_limited\u0026quot;\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe exact Problem Details payload may include additional fields.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003ERetry-After\u003C/code\u003E is optional. Applications must not assume that every HTTP 429 response contains it.\u003C/p\u003E\n\u003Cp\u003ERejected requests do not execute their endpoint handlers. Consequently, a request rejected at this layer does not consume a reset or PKCE proof, rotate refresh credentials, clear logout cookies, or increment login failure counts through the rejected handler.\u003C/p\u003E\n\u003Cp\u003EAuthentication and other middleware executing before the rate limiter may still perform work.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022user-facing-messages\u0022\u003EUser-facing messages\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003Erate_limited\u003C/code\u003E is a stable machine-readable error code, not a localized user-facing message.\u003C/p\u003E\n\u003Cp\u003EApplications can map it to a message such as:\u003C/p\u003E\n\u003Cblockquote\u003E\n\u003Cp\u003EToo many requests. Please wait a moment before trying again.\u003C/p\u003E\n\u003C/blockquote\u003E\n\u003Cp\u003EIf a reliable retry delay is available, the application may display it.\u003C/p\u003E\n\u003Cp\u003EAutomatic background operations should generally handle throttling silently rather than repeatedly notifying the user.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022client-http-429-handling\u0022\u003EClient HTTP 429 handling\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth clients recognize HTTP 429 as a temporary request throttle.\u003C/p\u003E\n\u003Cp\u003EA 429 response does not establish that:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EThe user\u0027s credentials are invalid\u003C/li\u003E\n\u003Cli\u003EThe session has been revoked\u003C/li\u003E\n\u003Cli\u003EA token has expired\u003C/li\u003E\n\u003Cli\u003EThe user is unauthorized\u003C/li\u003E\n\u003Cli\u003EReauthentication is required\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003E\u003Cstrong\u003EA throttled request must not, by itself, clear a valid authenticated session.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EUltimateAuth uses its existing client result and error contracts rather than introducing a separate rate limiting exception hierarchy.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022client-result-contracts\u0022\u003EClient result contracts\u003C/h3\u003E\n\u003Cp\u003EThe following contracts carry throttling information where applicable:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003EUAuthTransportException\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003ETryLoginResult\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003ETryPkceLoginResult\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003ERefreshResult\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EUAuthResult\u003C/code\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EUAuthProblem\u003C/code\u003E\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThe stable error code is \u003Ccode\u003Erate_limited\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EDepending on the contract, it is exposed through \u003Ccode\u003ECode\u003C/code\u003E, \u003Ccode\u003EErrorCode\u003C/code\u003E, or \u003Ccode\u003EProblem.Detail\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EHTTP status remains 429.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthResult.Http.IsTooManyRequests\u003C/code\u003E distinguishes throttling from unauthorized and forbidden results.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003ERetryAfter\u003C/code\u003E provides an optional \u003Ccode\u003ETimeSpan\u003C/code\u003E where supported.\u003C/p\u003E\n\u003Cp\u003ETransport DTOs preserve the raw \u003Ccode\u003ERetry-After\u003C/code\u003E header across the JavaScript bridge.\u003C/p\u003E\n\u003Cp\u003EThe existing \u003Ccode\u003EIUAuthTryResult\u003C/code\u003E interface provides default metadata properties so existing implementations are not required to add new members.\u003C/p\u003E\n\u003Cp\u003EThe public \u003Ccode\u003EIFlowClient\u003C/code\u003E method signatures remain unchanged.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022client-behavior-by-operation\u0022\u003EClient behavior by operation\u003C/h2\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EOperation\u003C/th\u003E\n\u003Cth\u003EHTTP 429 behavior\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EValidation\u003C/td\u003E\n\u003Ctd\u003EThrows a transport exception with status 429 and throttling metadata\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EBlazor authentication state\u003C/td\u003E\n\u003Ctd\u003EPreserves the current identity, claims, and last successful validation time\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ERefresh\u003C/td\u003E\n\u003Ctd\u003EReturns an unsuccessful result with status 429 and \u003Ccode\u003EOutcome = None\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAutomatic refresh\u003C/td\u003E\n\u003Ctd\u003EDefers subsequent attempts without triggering reauthentication\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ELogin preview\u003C/td\u003E\n\u003Ctd\u003EReturns a distinguishable throttled result rather than invalid credentials\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EPKCE preview\u003C/td\u003E\n\u003Ctd\u003EReturns a distinguishable throttled result\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ETry-and-commit\u003C/td\u003E\n\u003Ctd\u003EDoes not submit the final commit after a throttled preview\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ELogout\u003C/td\u003E\n\u003Ctd\u003EDoes not report successful logout or navigate on a confirmed 429\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAuthorization\u003C/td\u003E\n\u003Ctd\u003EReturns a throttled failure, not a Forbidden result\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EProfile, user, credential, and recovery operations\u003C/td\u003E\n\u003Ctd\u003EReturn unsuccessful results without publishing success events\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EResource requests\u003C/td\u003E\n\u003Ctd\u003EPreserve HTTP 429 information without changing authentication state\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=\u0022validation\u0022\u003EValidation\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EValidateAsync\u003C/code\u003E throws \u003Ccode\u003EUAuthTransportException\u003C/code\u003E for HTTP 429 before attempting to deserialize a successful authentication result.\u003C/p\u003E\n\u003Cp\u003EBlazor\u0027s \u003Ccode\u003EUAuthStateManager.EnsureAsync\u003C/code\u003E recognizes this condition and preserves the current authentication snapshot.\u003C/p\u003E\n\u003Cp\u003EIt does not:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EClear the current identity\u003C/li\u003E\n\u003Cli\u003ERemove claims\u003C/li\u003E\n\u003Cli\u003ETreat the user as anonymous\u003C/li\u003E\n\u003Cli\u003EMark the session freshly validated\u003C/li\u003E\n\u003Cli\u003EUpdate the last successful validation timestamp\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003ESubsequent validation attempts, including forced validation calls, are deferred during the cooldown.\u003C/p\u003E\n\u003Cp\u003EA genuine invalid, expired, or revoked validation result continues to follow the normal state-clearing behavior.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022refresh\u0022\u003ERefresh\u003C/h3\u003E\n\u003Cp\u003EA throttled refresh returns an unsuccessful \u003Ccode\u003ERefreshResult\u003C/code\u003E with:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EStatusCode: 429\nErrorCode: rate_limited\nOutcome: None\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EMisleading authentication-outcome headers are ignored on a throttled response.\u003C/p\u003E\n\u003Cp\u003ERepeated refresh calls during the cooldown return the throttled result without issuing another HTTP request.\u003C/p\u003E\n\u003Cp\u003EAutomatic refresh scheduling respects both the configured refresh interval and the throttling deadline.\u003C/p\u003E\n\u003Cp\u003EHTTP 429 does not cause reauthentication events, logout, or automatic navigation.\u003C/p\u003E\n\u003Cp\u003EA genuine refresh HTTP 401 can still produce \u003Ccode\u003EReauthRequired\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022login-and-pkce\u0022\u003ELogin and PKCE\u003C/h3\u003E\n\u003Cp\u003ELogin and PKCE preview operations expose throttling distinctly from invalid credentials.\u003C/p\u003E\n\u003Cp\u003EWhen a TryAndCommit preview is throttled, the client does not submit the final commit form.\u003C/p\u003E\n\u003Cp\u003EHowever, final browser-navigation form submissions retain their existing navigation semantics.\u003C/p\u003E\n\u003Cp\u003EFor \u003Ccode\u003ELoginAsync\u003C/code\u003E, DirectCommit, \u003Ccode\u003ECompletePkceLoginAsync\u003C/code\u003E, and the final TryAndCommit submission, the browser owns the resulting HTTP response.\u003C/p\u003E\n\u003Cp\u003EIf that final POST receives HTTP 429, the browser may display the server response page.\u003C/p\u003E\n\u003Cp\u003EThe JavaScript client cannot reliably return the final navigated response\u0027s \u003Ccode\u003ERetry-After\u003C/code\u003E metadata through the existing navigation contract.\u003C/p\u003E\n\u003Cp\u003EPreview success or form submission completion must not be interpreted as confirmation that a session was issued.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022logout\u0022\u003ELogout\u003C/h3\u003E\n\u003Cp\u003ELogout uses an observed form POST so the client can distinguish a confirmed successful response from HTTP 429.\u003C/p\u003E\n\u003Cp\u003EOn HTTP 429:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003ENo successful logout event is published.\u003C/li\u003E\n\u003Cli\u003ENo success navigation occurs.\u003C/li\u003E\n\u003Cli\u003EThe client does not clear authentication state solely because of throttling.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EOn confirmed success, the existing logout event can update client authentication state.\u003C/p\u003E\n\u003Cp\u003ELogout navigation is different from login navigation: the observed request can follow a successful server redirect and then navigate without replaying the logout POST.\u003C/p\u003E\n\u003Cp\u003ECross-origin logout still requires compatible browser credentials, CORS configuration, and redirect behavior.\u003C/p\u003E\n\u003Cp\u003EA network or redirect failure may leave the server-side logout outcome uncertain. The SDK reports an error rather than claiming success or automatically replaying the operation.\u003C/p\u003E\n\u003Cp\u003EApplications can explicitly revalidate when connectivity permits.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022custom-client-transports\u0022\u003ECustom client transports\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EIUAuthRequestClient\u003C/code\u003E supports an observed form request overload:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003ESendFormAsync(endpoint, form, navigateOnSuccess, ct)\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe overload has a default implementation that delegates to the original form request method.\u003C/p\u003E\n\u003Cp\u003EExisting custom transport implementations therefore retain their original interface requirements.\u003C/p\u003E\n\u003Cp\u003EHowever, the default implementation does not provide navigation behavior automatically.\u003C/p\u003E\n\u003Cp\u003ECustom adapters that support logout navigation should implement the observed overload and preserve HTTP status and \u003Ccode\u003ERetry-After\u003C/code\u003E metadata.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022retry-after-and-cooldown-behavior\u0022\u003ERetry-After and cooldown behavior\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth clients support standard \u003Ccode\u003ERetry-After\u003C/code\u003E values expressed as:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EDelta seconds\u003C/li\u003E\n\u003Cli\u003EHTTP-date values\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EParsing uses invariant culture and UTC.\u003C/p\u003E\n\u003Cp\u003EMissing, malformed, or inaccessible values remain null in application results. Past dates produce a zero retry delay.\u003C/p\u003E\n\u003Cp\u003EFor internal maintenance scheduling, the SDK uses:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EA conservative 30-second fallback when retry guidance is unavailable\u003C/li\u003E\n\u003Cli\u003EA one-second minimum to avoid immediate retry loops\u003C/li\u003E\n\u003Cli\u003EBounded timer intervals when waiting for long delays\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003ELong retry guidance is honored by the current implementation.\u003C/p\u003E\n\u003Cp\u003EThe SDK does not automatically retry arbitrary application operations.\u003C/p\u003E\n\u003Cp\u003EIn particular, it does not automatically replay login, logout, recovery, credential mutations, or administrative changes after throttling.\u003C/p\u003E\n\u003Cp\u003EFor cross-origin browser clients, the server must expose \u003Ccode\u003ERetry-After\u003C/code\u003E through CORS if JavaScript is expected to read that header.\u003C/p\u003E\n\u003Cp\u003EWhen the header is not accessible, the client follows its missing-header behavior.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022authentication-state-safety\u0022\u003EAuthentication state safety\u003C/h2\u003E\n\u003Cp\u003ERate limiting is deliberately separated from authentication validity.\u003C/p\u003E\n\u003Cp\u003EA throttled request does not:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EExtend token expiration\u003C/li\u003E\n\u003Cli\u003ERenew credentials\u003C/li\u003E\n\u003Cli\u003EUpdate successful validation timestamps\u003C/li\u003E\n\u003Cli\u003ECreate a trusted identity\u003C/li\u003E\n\u003Cli\u003ERevoke an existing session\u003C/li\u003E\n\u003Cli\u003EClear browser storage automatically\u003C/li\u003E\n\u003Cli\u003EPublish successful mutation events\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthStateManager\u003C/code\u003E remains responsible for authentication state transitions.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003ESessionCoordinator\u003C/code\u003E remains responsible for automatic refresh scheduling and genuine reauthentication handling.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthAuthenticationStateProvider\u003C/code\u003E projects the retained state to Blazor.\u003C/p\u003E\n\u003Cp\u003EExplicit local actions such as \u003Ccode\u003EClear\u003C/code\u003E and \u003Ccode\u003EOnLogoutAsync\u003C/code\u003E, and authoritative revocation or mutation events, retain their existing behavior.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022partitioning-and-trust-boundaries\u0022\u003EPartitioning and trust boundaries\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth chooses rate limiting partitions using trusted server-side information.\u003C/p\u003E\n\u003Cp\u003ECredential entry, anonymous registration and recovery, identifier availability, PKCE, Hub entry, and self-proof verification use the connection IP even when a cookie is present.\u003C/p\u003E\n\u003Cp\u003EOther operations use validated UltimateAuth tenant and actor information when available, otherwise the connection IP.\u003C/p\u003E\n\u003Cp\u003EThe following do not independently establish a trusted authenticated partition:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EMere presence of a session cookie\u003C/li\u003E\n\u003Cli\u003EUsernames or target user identifiers\u003C/li\u003E\n\u003Cli\u003EUnvalidated tokens\u003C/li\u003E\n\u003Cli\u003EDevice identifiers\u003C/li\u003E\n\u003Cli\u003EClient profile hints\u003C/li\u003E\n\u003Cli\u003ERaw tenant selectors\u003C/li\u003E\n\u003Cli\u003EPrincipals from unrelated authentication authorities\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EAuthenticated actor partitions include validated tenant identity.\u003C/p\u003E\n\u003Cp\u003EAnonymous IP budgets span tenants, preventing a client from multiplying its quota simply by rotating tenant selectors.\u003C/p\u003E\n\u003Cp\u003EIPv4-mapped IPv6 addresses are normalized to IPv4.\u003C/p\u003E\n\u003Cp\u003ERequests without an available connection address share an unknown-address partition.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth does not trust arbitrary forwarding headers.\u003C/p\u003E\n\u003Cp\u003EConfigure trusted proxies, forwarded headers, and ingress restrictions at the application or infrastructure level.\u003C/p\u003E\n\u003Cp\u003EApplications using another identity authority should configure an appropriate native policy rather than assuming UltimateAuth can validate that authority\u0027s principal.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022persistence-and-distributed-deployment\u0022\u003EPersistence and distributed deployment\u003C/h2\u003E\n\u003Cp\u003EBuilt-in counters are maintained in application memory.\u003C/p\u003E\n\u003Cp\u003EThey are not stored in UltimateAuth\u0027s persistence providers, EF Core, or a database.\u003C/p\u003E\n\u003Cp\u003EThis means:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003ECounters reset when the application process restarts.\u003C/li\u003E\n\u003Cli\u003EEach application instance maintains its own counters.\u003C/li\u003E\n\u003Cli\u003EBrowser refreshes do not reset server counters.\u003C/li\u003E\n\u003Cli\u003ESliding windows replenish capacity over time.\u003C/li\u003E\n\u003Cli\u003EMultiple application instances do not automatically share a global request budget.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EFor distributed deployments requiring fleet-wide limits, use an ingress gateway, WAF, or custom native policy backed by shared infrastructure.\u003C/p\u003E\n\u003Cp\u003EBuilt-in native partitions have idle cleanup, but do not provide a hard partition-cardinality bound.\u003C/p\u003E\n\u003Cp\u003ELarge-scale IP rotation, IPv6 address churn, NAT sharing, and botnet traffic require additional operational consideration.\u003C/p\u003E\n\u003Cp\u003ERate limiting does not replace account lockout, proof-attempt limits, anti-enumeration responses, or other authentication security controls.\u003C/p\u003E\n\u003Cp\u003EDirect service calls and Blazor Server circuit operations are outside HTTP endpoint throttling unless the application protects them separately.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022testing-rate-limiting\u0022\u003ETesting rate limiting\u003C/h2\u003E\n\u003Cp\u003EA simple development test is to temporarily limit validation to two requests per minute:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 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.RateLimiting.Endpoints[UAuthEndpoint.Validate] = new()\n {\n PermitLimit = 2,\n Window = TimeSpan.FromMinutes(1)\n };\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAfter starting the application and authenticating, send repeated requests 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\u003EHTTP\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-http\u0022\u003EPOST /auth/validate\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe expected server behavior is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003ERequest\u003C/th\u003E\n\u003Cth\u003EExpected result\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EFirst permitted request\u003C/td\u003E\n\u003Ctd\u003ENormal validation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ESecond permitted request\u003C/td\u003E\n\u003Ctd\u003ENormal validation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ERequest exceeding the budget\u003C/td\u003E\n\u003Ctd\u003EHTTP 429\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ERequest after capacity replenishes\u003C/td\u003E\n\u003Ctd\u003ENormal validation\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EActual results depend on the partition\u0027s existing traffic and sliding-window state.\u003C/p\u003E\n\u003Cp\u003EWhen testing through an UltimateAuth client, the client may defer additional validation requests after receiving HTTP 429. This is expected and should be tested separately from server enforcement.\u003C/p\u003E\n\u003Cp\u003EAlso verify that:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EAuthentication state remains intact after throttling.\u003C/li\u003E\n\u003Cli\u003EOther independently limited operations continue to work.\u003C/li\u003E\n\u003Cli\u003EAutomatic refresh does not enter an immediate retry loop.\u003C/li\u003E\n\u003Cli\u003ELogin preview does not confuse throttling with invalid credentials.\u003C/li\u003E\n\u003Cli\u003ELogout does not report success after a confirmed 429.\u003C/li\u003E\n\u003Cli\u003ENo successful mutation event is published for a rejected operation.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003ERemove temporary low-limit overrides before deploying to production.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022client-ip-address-reverse-proxies\u0022\u003EClient IP Address \u0026amp; Reverse Proxies\u003C/h3\u003E\n\u003Cp\u003EUltimateAuth uses ASP.NET Core\u0027s resolved client IP address for IP-based rate limiting. It does not directly trust forwarded IP headers.\u003C/p\u003E\n\u003Cp\u003EWhen deploying behind Caddy, Nginx, Cloudflare, or another reverse proxy, ensure that ASP.NET Core\u0027s Forwarded Headers Middleware is correctly configured with trusted proxies or networks.\u003C/p\u003E\n\u003Cp\u003EWithout proper configuration, multiple users may be identified by the same proxy IP address and share rate limits.\u003C/p\u003E\n\u003Cp\u003ESee \u003Ca href=\u0022/docs/getting-started/real-world-setup#10-client-ip-address--reverse-proxy\u0022\u003EClient IP Address \u0026amp; Reverse Proxy Setup\u003C/a\u003E for configuration examples.\u003C/p\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/configuration/server-options\u0022\u003EServer Options\u003C/a\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/configuration/client-options\u0022\u003EClient Options\u003C/a\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/concepts/authentication-model\u0022\u003EAuthentication Model\u003C/a\u003E\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003ERate limiting is one layer of UltimateAuth\u0027s security model. It should be combined with appropriate credential policies, session validation, trusted proxy configuration, and deployment-level protection.\u003C/p\u003E\n",
+ "Headings": [
+ {
+ "Id": "getting-started",
+ "Text": "Getting started",
+ "Level": 0
+ },
+ {
+ "Id": "middleware-integration",
+ "Text": "Middleware integration",
+ "Level": 0
+ },
+ {
+ "Id": "configuration-examples",
+ "Text": "Configuration examples",
+ "Level": 1
+ },
+ {
+ "Id": "existing-application-rate-limiters",
+ "Text": "Existing application rate limiters",
+ "Level": 1
+ },
+ {
+ "Id": "rules-and-default-limits",
+ "Text": "Rules and default limits",
+ "Level": 0
+ },
+ {
+ "Id": "shared-login-budget",
+ "Text": "Shared login budget",
+ "Level": 1
+ },
+ {
+ "Id": "additional-sensitive-defaults",
+ "Text": "Additional sensitive defaults",
+ "Level": 1
+ },
+ {
+ "Id": "per-endpoint-configuration",
+ "Text": "Per-endpoint configuration",
+ "Level": 0
+ },
+ {
+ "Id": "policy-precedence",
+ "Text": "Policy precedence",
+ "Level": 1
+ },
+ {
+ "Id": "disabling-rate-limiting",
+ "Text": "Disabling rate limiting",
+ "Level": 0
+ },
+ {
+ "Id": "using-application-owned-policies",
+ "Text": "Using application-owned policies",
+ "Level": 0
+ },
+ {
+ "Id": "rejection-callbacks",
+ "Text": "Rejection callbacks",
+ "Level": 1
+ },
+ {
+ "Id": "http-429-responses",
+ "Text": "HTTP 429 responses",
+ "Level": 0
+ },
+ {
+ "Id": "user-facing-messages",
+ "Text": "User-facing messages",
+ "Level": 1
+ },
+ {
+ "Id": "client-http-429-handling",
+ "Text": "Client HTTP 429 handling",
+ "Level": 0
+ },
+ {
+ "Id": "client-result-contracts",
+ "Text": "Client result contracts",
+ "Level": 1
+ },
+ {
+ "Id": "client-behavior-by-operation",
+ "Text": "Client behavior by operation",
+ "Level": 0
+ },
+ {
+ "Id": "validation",
+ "Text": "Validation",
+ "Level": 1
+ },
+ {
+ "Id": "refresh",
+ "Text": "Refresh",
+ "Level": 1
+ },
+ {
+ "Id": "login-and-pkce",
+ "Text": "Login and PKCE",
+ "Level": 1
+ },
+ {
+ "Id": "logout",
+ "Text": "Logout",
+ "Level": 1
+ },
+ {
+ "Id": "custom-client-transports",
+ "Text": "Custom client transports",
+ "Level": 1
+ },
+ {
+ "Id": "retry-after-and-cooldown-behavior",
+ "Text": "Retry-After and cooldown behavior",
+ "Level": 0
+ },
+ {
+ "Id": "authentication-state-safety",
+ "Text": "Authentication state safety",
+ "Level": 0
+ },
+ {
+ "Id": "partitioning-and-trust-boundaries",
+ "Text": "Partitioning and trust boundaries",
+ "Level": 0
+ },
+ {
+ "Id": "persistence-and-distributed-deployment",
+ "Text": "Persistence and distributed deployment",
+ "Level": 0
+ },
+ {
+ "Id": "testing-rate-limiting",
+ "Text": "Testing rate limiting",
+ "Level": 0
+ },
+ {
+ "Id": "client-ip-address-reverse-proxies",
+ "Text": "Client IP Address \u0026 Reverse Proxies",
+ "Level": 1
+ },
+ {
+ "Id": "related-documentation",
+ "Text": "Related documentation",
+ "Level": 0
+ }
+ ],
+ "SourceHash": "BFDB17C67297EBF0E46C4481A0221ECC54E04A67135EF223BA26DFE64B559B93",
+ "BuilderVersion": "1"
+}
\ No newline at end of file
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 9dc2ee6b..1b1cd3fa 100644
--- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Program.cs
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Program.cs
@@ -78,12 +78,16 @@
builder.Services.AddScopedUltimateAuthSampleSeed();
builder.Services.AddScoped();
-builder.Services.Configure(options =>
-{
- options.ForwardedHeaders =
- ForwardedHeaders.XForwardedFor |
- ForwardedHeaders.XForwardedProto;
-});
+// Reverse proxy deployments (Caddy, Nginx, etc.):
+// Configure forwarded headers only when running behind a trusted proxy.
+//
+// builder.Services.Configure(options =>
+// {
+// options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
+//
+// // Replace with the actual trusted proxy address.
+// options.KnownProxies.Add(IPAddress.Parse("192.0.2.10"));
+// });
var app = builder.Build();
@@ -103,7 +107,10 @@
}
await app.SeedUltimateAuthAsync();
}
-app.UseForwardedHeaders();
+
+// Enable only when trusted forwarded headers are configured.
+// Must run before authentication, rate limiting and HTTPS redirection.
+// app.UseForwardedHeaders();
app.UseHttpsRedirection();
app.UseStaticFiles();
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs
index e58f9b28..bedc9fb6 100644
--- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs
@@ -89,12 +89,16 @@
builder.Services.AddScoped();
}
-builder.Services.Configure(options =>
-{
- options.ForwardedHeaders =
- ForwardedHeaders.XForwardedFor |
- ForwardedHeaders.XForwardedProto;
-});
+// Reverse proxy deployments (Caddy, Nginx, etc.):
+// Configure forwarded headers only when running behind a trusted proxy.
+//
+// builder.Services.Configure(options =>
+// {
+// options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
+//
+// // Replace with the actual trusted proxy address.
+// options.KnownProxies.Add(IPAddress.Parse("192.0.2.10"));
+// });
var app = builder.Build();
@@ -111,7 +115,9 @@
await app.SeedUltimateAuthAsync();
}
-app.UseForwardedHeaders();
+// Enable only when trusted forwarded headers are configured.
+// Must run before authentication, rate limiting and HTTPS redirection.
+// app.UseForwardedHeaders();
app.UseHttpsRedirection();
app.UseStaticFiles();
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Common/IUAuthTryResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Common/IUAuthTryResult.cs
index daeda5ba..6c1f2cb3 100644
--- a/src/CodeBeam.UltimateAuth.Core/Contracts/Common/IUAuthTryResult.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Common/IUAuthTryResult.cs
@@ -4,6 +4,9 @@ namespace CodeBeam.UltimateAuth.Core.Contracts;
public interface IUAuthTryResult
{
+ int? Status => null;
+ string? ErrorCode => null;
+ TimeSpan? RetryAfter => null;
bool IsSuccess { get; }
AuthFailureReason? Reason { get; }
int? RemainingAttempts { get; }
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Common/UAuthProblem.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Common/UAuthProblem.cs
index a5f692f8..e0c6dcfb 100644
--- a/src/CodeBeam.UltimateAuth.Core/Contracts/Common/UAuthProblem.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Common/UAuthProblem.cs
@@ -7,6 +7,7 @@ public sealed class UAuthProblem
public string? Title { get; set; }
public int? Status { get; set; }
public string? Detail { get; set; }
+ public TimeSpan? RetryAfter { get; set; }
public string? TraceId { get; set; }
public Dictionary? Extensions { get; init; }
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Common/UAuthResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Common/UAuthResult.cs
index 2a674d30..198d540d 100644
--- a/src/CodeBeam.UltimateAuth.Core/Contracts/Common/UAuthResult.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Common/UAuthResult.cs
@@ -24,6 +24,7 @@ internal HttpStatusInfo(int status)
public bool IsBadRequest => _status == 400;
public bool IsUnauthorized => _status == 401;
+ public bool IsTooManyRequests => _status == 429;
public bool IsForbidden => _status == 403;
public bool IsConflict => _status == 409;
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/TryLoginResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/TryLoginResult.cs
index ce08a546..f7e8280f 100644
--- a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/TryLoginResult.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/TryLoginResult.cs
@@ -4,6 +4,9 @@ namespace CodeBeam.UltimateAuth.Core.Contracts;
public sealed record TryLoginResult : IUAuthTryResult
{
+ public int? Status { get; init; }
+ public string? ErrorCode { get; init; }
+ public TimeSpan? RetryAfter { get; init; }
public bool IsSuccess { get; init; }
public AuthFailureReason? Reason { get; init; }
public int? RemainingAttempts { get; init; }
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/TryPkceLoginResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/TryPkceLoginResult.cs
index 6efd7351..c2a8e826 100644
--- a/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/TryPkceLoginResult.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/TryPkceLoginResult.cs
@@ -4,6 +4,9 @@ namespace CodeBeam.UltimateAuth.Core.Contracts;
public sealed record TryPkceLoginResult : IUAuthTryResult
{
+ public int? Status { get; init; }
+ public string? ErrorCode { get; init; }
+ public TimeSpan? RetryAfter { get; init; }
public bool IsSuccess { get; init; }
public AuthFailureReason? Reason { get; init; }
public int? RemainingAttempts { get; init; }
diff --git a/src/CodeBeam.UltimateAuth.Server/Authentication/AspNetCore/UAuthAuthenticationHandler.cs b/src/CodeBeam.UltimateAuth.Server/Authentication/AspNetCore/UAuthAuthenticationHandler.cs
index fd8d351d..c4425397 100644
--- a/src/CodeBeam.UltimateAuth.Server/Authentication/AspNetCore/UAuthAuthenticationHandler.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Authentication/AspNetCore/UAuthAuthenticationHandler.cs
@@ -73,6 +73,8 @@ protected override async Task HandleAuthenticateAsync()
if (snapshot is null || snapshot.Identity is null)
return AuthenticateResult.NoResult();
+ RateLimiting.UAuthRateLimitPolicy.SetValidatedActor(Context, tenant.Value, result.UserKey.Value.Value);
+
var principal = snapshot.ToClaimsPrincipal(UAuthConstants.SchemeDefaults.GlobalScheme);
return AuthenticateResult.Success(new AuthenticationTicket(principal, UAuthConstants.SchemeDefaults.GlobalScheme));
}
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/LogoutEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/LogoutEndpointHandler.cs
index 175b07d3..e2fe123f 100644
--- a/src/CodeBeam.UltimateAuth.Server/Endpoints/LogoutEndpointHandler.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/LogoutEndpointHandler.cs
@@ -51,6 +51,17 @@ public async Task LogoutAsync(HttpContext ctx)
var decision = _redirectResolver.ResolveSuccess(authFlow, ctx);
+ var observedLogout = string.Equals(ctx.Request.Headers["X-UAuth-Logout-Mode"].ToString(), "observe" ,StringComparison.OrdinalIgnoreCase);
+
+ if (observedLogout)
+ {
+ return Results.Ok(new
+ {
+ IsSuccess = true,
+ RedirectUrl = decision.Enabled ? decision.TargetUrl : null
+ });
+ }
+
return decision.Enabled
? Results.Redirect(decision.TargetUrl!)
: Results.Ok(new LogoutResponse { IsSuccess = true });
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs
index a2dfbb16..6ba447e7 100644
--- a/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs
@@ -1,4 +1,5 @@
-using CodeBeam.UltimateAuth.Authorization.Contracts;
+using CodeBeam.UltimateAuth.Server.RateLimiting;
+using CodeBeam.UltimateAuth.Authorization.Contracts;
using CodeBeam.UltimateAuth.Core.Defaults;
using CodeBeam.UltimateAuth.Core.Domain;
using CodeBeam.UltimateAuth.Server.Auth;
@@ -37,6 +38,11 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options
: rootGroup.MapGroup("/" + basePrefix);
group.AddEndpointFilter();
+ ((IEndpointConventionBuilder)group).Finally(endpoint =>
+ {
+ if (!endpoint.Metadata.OfType().Any())
+ throw new InvalidOperationException("Every UltimateAuth endpoint must declare a rate limiting classification.");
+ });
//var user = group.MapGroup("");
var self = group.MapGroup("/me");
@@ -45,32 +51,32 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options
if (options.Endpoints.Authentication != false)
{
group.MapPost("/login", async ([FromServices] ILoginEndpointHandler h, HttpContext ctx)
- => await h.LoginAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Login));
+ => await h.LoginAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Login)).WithUAuthRateLimit(UAuthEndpoint.Login);
group.MapPost("/try-login", async ([FromServices] ILoginEndpointHandler h, HttpContext ctx)
- => await h.TryLoginAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Login));
+ => await h.TryLoginAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Login)).WithUAuthRateLimit(UAuthEndpoint.TryLogin);
group.MapPost("/validate", async ([FromServices] IValidateEndpointHandler h, HttpContext ctx)
- => await h.ValidateAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.ValidateSession));
+ => await h.ValidateAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.ValidateSession)).WithUAuthRateLimit(UAuthEndpoint.Validate);
if (Enabled(UAuthActions.Flows.LogoutSelf))
group.MapPost("/logout", async ([FromServices] ILogoutEndpointHandler h, HttpContext ctx)
- => await h.LogoutAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Logout));
+ => await h.LogoutAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Logout)).WithUAuthRateLimit(UAuthEndpoint.Logout);
if (Enabled(UAuthActions.Flows.LogoutDeviceSelf))
self.MapPost("/logout-device", async ([FromServices] ILogoutEndpointHandler h, HttpContext ctx)
- => await h.LogoutDeviceSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Logout));
+ => await h.LogoutDeviceSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Logout)).WithUAuthRateLimit(UAuthEndpoint.LogoutDeviceSelf);
if (Enabled(UAuthActions.Flows.LogoutOthersSelf))
self.MapPost("/logout-others", async ([FromServices] ILogoutEndpointHandler h, HttpContext ctx)
- => await h.LogoutOthersSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Logout));
+ => await h.LogoutOthersSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Logout)).WithUAuthRateLimit(UAuthEndpoint.LogoutOthersSelf);
if (Enabled(UAuthActions.Flows.LogoutAllSelf))
self.MapPost("/logout-all", async ([FromServices] ILogoutEndpointHandler h, HttpContext ctx)
- => await h.LogoutAllSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Logout));
+ => await h.LogoutAllSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Logout)).WithUAuthRateLimit(UAuthEndpoint.LogoutAllSelf);
group.MapPost("/refresh", async ([FromServices] IRefreshEndpointHandler h, HttpContext ctx)
- => await h.RefreshAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RefreshSession));
+ => await h.RefreshAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RefreshSession)).WithUAuthRateLimit(UAuthEndpoint.Refresh);
//group.MapPost("/reauth", async ([FromServices] IReauthEndpointHandler h, HttpContext ctx)
// => await h.ReauthAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Reauthentication));
@@ -78,15 +84,15 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options
if (Enabled(UAuthActions.Flows.LogoutDeviceAdmin))
admin.MapPost("/users/{userKey}/logout-device", async ([FromServices] ILogoutEndpointHandler h, UserKey userKey, HttpContext ctx)
- => await h.LogoutDeviceAdminAsync(ctx, userKey)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Logout));
+ => await h.LogoutDeviceAdminAsync(ctx, userKey)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Logout)).WithUAuthRateLimit(UAuthEndpoint.LogoutDeviceAdmin);
if (Enabled(UAuthActions.Flows.LogoutOthersAdmin))
admin.MapPost("/users/{userKey}/logout-others", async ([FromServices] ILogoutEndpointHandler h, UserKey userKey, HttpContext ctx)
- => await h.LogoutOthersAdminAsync(ctx, userKey)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Logout));
+ => await h.LogoutOthersAdminAsync(ctx, userKey)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Logout)).WithUAuthRateLimit(UAuthEndpoint.LogoutOthersAdmin);
if (Enabled(UAuthActions.Flows.LogoutAllAdmin))
admin.MapPost("/users/{userKey}/logout-all", async ([FromServices] ILogoutEndpointHandler h, UserKey userKey, HttpContext ctx)
- => await h.LogoutAllAdminAsync(ctx, userKey)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Logout));
+ => await h.LogoutAllAdminAsync(ctx, userKey)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Logout)).WithUAuthRateLimit(UAuthEndpoint.LogoutAllAdmin);
}
if (options.Endpoints.Pkce != false)
@@ -94,13 +100,13 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options
var pkce = group.MapGroup("/pkce");
pkce.MapPost("/authorize", async ([FromServices] IPkceEndpointHandler h, HttpContext ctx)
- => await h.AuthorizeAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Login));
+ => await h.AuthorizeAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Login)).WithUAuthRateLimit(UAuthEndpoint.PkceAuthorize);
pkce.MapPost("/complete", async ([FromServices] IPkceEndpointHandler h, HttpContext ctx)
- => await h.CompleteAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Login));
+ => await h.CompleteAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Login)).WithUAuthRateLimit(UAuthEndpoint.PkceComplete);
pkce.MapPost("/try-complete", async ([FromServices] IPkceEndpointHandler h, HttpContext ctx)
- => await h.TryCompleteAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Login));
+ => await h.TryCompleteAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Login)).WithUAuthRateLimit(UAuthEndpoint.PkceTryComplete);
}
//if (options.Endpoints.Token != false)
@@ -127,48 +133,48 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options
if (Enabled(UAuthActions.Sessions.ListChainsSelf))
selfSession.MapPost("/chains", async ([FromServices] ISessionEndpointHandler h, HttpContext ctx)
- => await h.GetMyChainsAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.QuerySession));
+ => await h.GetMyChainsAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.QuerySession)).WithUAuthRateLimit(UAuthEndpoint.GetMyChains);
if (Enabled(UAuthActions.Sessions.GetChainSelf))
selfSession.MapPost("/chains/{chainId}", async ([FromServices] ISessionEndpointHandler h, SessionChainId chainId, HttpContext ctx)
- => await h.GetMyChainDetailAsync(chainId, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.QuerySession));
+ => await h.GetMyChainDetailAsync(chainId, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.QuerySession)).WithUAuthRateLimit(UAuthEndpoint.GetMyChainDetail);
if (Enabled(UAuthActions.Sessions.RevokeChainSelf))
selfSession.MapPost("/chains/{chainId}/revoke", async ([FromServices] ISessionEndpointHandler h, SessionChainId chainId, HttpContext ctx)
- => await h.RevokeMyChainAsync(chainId, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeSession));
+ => await h.RevokeMyChainAsync(chainId, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeSession)).WithUAuthRateLimit(UAuthEndpoint.RevokeMyChain);
if (Enabled(UAuthActions.Sessions.RevokeOtherChainsSelf))
selfSession.MapPost("/revoke-others",async ([FromServices] ISessionEndpointHandler h, HttpContext ctx)
- => await h.RevokeOtherChainsAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeSession));
+ => await h.RevokeOtherChainsAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeSession)).WithUAuthRateLimit(UAuthEndpoint.RevokeOtherChains);
if (Enabled(UAuthActions.Sessions.RevokeAllChainsSelf))
selfSession.MapPost("/revoke-all", async ([FromServices] ISessionEndpointHandler h, HttpContext ctx)
- => await h.RevokeAllMyChainsAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeSession));
+ => await h.RevokeAllMyChainsAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeSession)).WithUAuthRateLimit(UAuthEndpoint.RevokeAllMyChains);
if (Enabled(UAuthActions.Sessions.ListChainsAdmin))
adminSession.MapPost("/chains", async ([FromServices] ISessionEndpointHandler h, UserKey userKey, HttpContext ctx)
- => await h.GetUserChainsAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.QuerySession));
+ => await h.GetUserChainsAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.QuerySession)).WithUAuthRateLimit(UAuthEndpoint.GetUserChains);
if (Enabled(UAuthActions.Sessions.GetChainAdmin))
adminSession.MapPost("/chains/{chainId}", async ([FromServices] ISessionEndpointHandler h, UserKey userKey, SessionChainId chainId, HttpContext ctx)
- => await h.GetUserChainDetailAsync(userKey, chainId, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.QuerySession));
+ => await h.GetUserChainDetailAsync(userKey, chainId, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.QuerySession)).WithUAuthRateLimit(UAuthEndpoint.GetUserChainDetail);
if (Enabled(UAuthActions.Sessions.RevokeSessionAdmin))
adminSession.MapPost("/{sessionId}/revoke", async ([FromServices] ISessionEndpointHandler h, UserKey userKey, AuthSessionId sessionId, HttpContext ctx)
- => await h.RevokeUserSessionAsync(userKey, sessionId, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeSession));
+ => await h.RevokeUserSessionAsync(userKey, sessionId, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeSession)).WithUAuthRateLimit(UAuthEndpoint.RevokeUserSession);
if (Enabled(UAuthActions.Sessions.RevokeChainAdmin))
adminSession.MapPost("/chains/{chainId}/revoke", async ([FromServices] ISessionEndpointHandler h, UserKey userKey, SessionChainId chainId, HttpContext ctx)
- => await h.RevokeUserChainAsync(userKey, chainId, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeSession));
+ => await h.RevokeUserChainAsync(userKey, chainId, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeSession)).WithUAuthRateLimit(UAuthEndpoint.RevokeUserChain);
if (Enabled(UAuthActions.Sessions.RevokeRootAdmin))
adminSession.MapPost("/revoke-root", async ([FromServices] ISessionEndpointHandler h, UserKey userKey, HttpContext ctx)
- => await h.RevokeRootAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeSession));
+ => await h.RevokeRootAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeSession)).WithUAuthRateLimit(UAuthEndpoint.RevokeRoot);
if (Enabled(UAuthActions.Sessions.RevokeAllChainsAdmin))
adminSession.MapPost("/revoke-all", async ([FromServices] ISessionEndpointHandler h, UserKey userKey, HttpContext ctx)
- => await h.RevokeAllChainsAsync(userKey, null, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeSession));
+ => await h.RevokeAllChainsAsync(userKey, null, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeSession)).WithUAuthRateLimit(UAuthEndpoint.RevokeAllChains);
}
//if (options.EnableUserInfoEndpoints != false)
@@ -189,136 +195,136 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options
{
if (Enabled(UAuthActions.Users.CreateAnonymous))
group.MapPost("/users/create", async ([FromServices] IUserEndpointHandler h, HttpContext ctx)
- => await h.CreateAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement));
+ => await h.CreateAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement)).WithUAuthRateLimit(UAuthEndpoint.UserCreateAnonymous);
if (Enabled(UAuthActions.Users.ChangeStatusSelf))
self.MapPost("/status", async ([FromServices] IUserEndpointHandler h, HttpContext ctx)
- => await h.ChangeStatusSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement));
+ => await h.ChangeStatusSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement)).WithUAuthRateLimit(UAuthEndpoint.ChangeStatusSelf);
if (Enabled(UAuthActions.Users.DeleteSelf))
self.MapPost("/delete", async ([FromServices] IUserEndpointHandler h, HttpContext ctx)
- => await h.DeleteMeAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement));
+ => await h.DeleteMeAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement)).WithUAuthRateLimit(UAuthEndpoint.UserDeleteSelf);
if (Enabled(UAuthActions.Users.QueryAdmin))
adminUsers.MapPost("/query", async ([FromServices] IUserEndpointHandler h, HttpContext ctx)
- => await h.QueryUsersAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement));
+ => await h.QueryUsersAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement)).WithUAuthRateLimit(UAuthEndpoint.QueryUsers);
if (Enabled(UAuthActions.Users.GetByKeysAdmin))
adminUsers.MapPost("/get-by-keys", async ([FromServices] IUserEndpointHandler h, HttpContext ctx)
- => await h.GetUsersByKeysAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement));
+ => await h.GetUsersByKeysAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement)).WithUAuthRateLimit(UAuthEndpoint.GetUsersByKeys);
if (Enabled(UAuthActions.Users.CreateAdmin))
adminUsers.MapPost("/create", async ([FromServices] IUserEndpointHandler h, HttpContext ctx)
- => await h.CreateAdminAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement));
+ => await h.CreateAdminAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement)).WithUAuthRateLimit(UAuthEndpoint.UserCreateAdmin);
if (Enabled(UAuthActions.Users.ChangeStatusAdmin))
adminUsers.MapPost("/{userKey}/status", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx)
- => await h.ChangeStatusAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement));
+ => await h.ChangeStatusAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement)).WithUAuthRateLimit(UAuthEndpoint.ChangeStatusAdmin);
if (Enabled(UAuthActions.Users.DeleteAdmin))
adminUsers.MapPost("/{userKey}/delete", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx)
- => await h.DeleteAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement));
+ => await h.DeleteAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement)).WithUAuthRateLimit(UAuthEndpoint.UserDeleteAdmin);
}
if (options.Endpoints.UserProfile != false)
{
if (Enabled(UAuthActions.UserProfiles.GetSelf))
self.MapPost("/profile/get", async ([FromServices] IUserEndpointHandler h, HttpContext ctx)
- => await h.GetMeAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement));
+ => await h.GetMeAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement)).WithUAuthRateLimit(UAuthEndpoint.ProfileGetSelf);
if (Enabled(UAuthActions.UserProfiles.CreateSelf))
self.MapPost("/profile/create", async (IUserEndpointHandler h, HttpContext ctx)
- => await h.CreateProfileSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement));
+ => await h.CreateProfileSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement)).WithUAuthRateLimit(UAuthEndpoint.CreateProfileSelf);
if (Enabled(UAuthActions.UserProfiles.UpdateSelf))
self.MapPost("/profile/update", async ([FromServices] IUserEndpointHandler h, HttpContext ctx)
- => await h.UpdateMeAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement));
+ => await h.UpdateMeAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement)).WithUAuthRateLimit(UAuthEndpoint.ProfileUpdateSelf);
if (Enabled(UAuthActions.UserProfiles.DeleteSelf))
self.MapPost("/profile/delete", async (IUserEndpointHandler h, HttpContext ctx)
- => await h.DeleteProfileSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement));
+ => await h.DeleteProfileSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement)).WithUAuthRateLimit(UAuthEndpoint.DeleteProfileSelf);
if (Enabled(UAuthActions.UserProfiles.GetAdmin))
adminUsers.MapPost("/{userKey}/profile/get", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx)
- => await h.GetUserAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement));
+ => await h.GetUserAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement)).WithUAuthRateLimit(UAuthEndpoint.ProfileGetAdmin);
if (Enabled(UAuthActions.UserProfiles.CreateAdmin))
adminUsers.MapPost("/{userKey}/profile/create", async (IUserEndpointHandler h, UserKey userKey, HttpContext ctx)
- => await h.CreateProfileAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement));
+ => await h.CreateProfileAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement)).WithUAuthRateLimit(UAuthEndpoint.CreateProfileAdmin);
if (Enabled(UAuthActions.UserProfiles.UpdateAdmin))
adminUsers.MapPost("/{userKey}/profile/update", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx)
- => await h.UpdateUserAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement));
+ => await h.UpdateUserAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement)).WithUAuthRateLimit(UAuthEndpoint.ProfileUpdateAdmin);
if (Enabled(UAuthActions.UserProfiles.DeleteAdmin))
adminUsers.MapPost("/{userKey}/profile/delete", async (IUserEndpointHandler h, UserKey userKey, HttpContext ctx)
- => await h.DeleteProfileAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement));
+ => await h.DeleteProfileAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement)).WithUAuthRateLimit(UAuthEndpoint.DeleteProfileAdmin);
}
if (options.Endpoints.UserIdentifier != false)
{
if (Enabled(UAuthActions.UserIdentifiers.GetSelf))
self.MapPost("/identifiers/get", async ([FromServices] IUserEndpointHandler h, HttpContext ctx)
- => await h.GetMyIdentifiersAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement));
+ => await h.GetMyIdentifiersAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)).WithUAuthRateLimit(UAuthEndpoint.GetMyIdentifiers);
if (Enabled(UAuthActions.UserIdentifiers.AddSelf))
self.MapPost("/identifiers/add", async ([FromServices] IUserEndpointHandler h, HttpContext ctx)
- => await h.AddUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement));
+ => await h.AddUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)).WithUAuthRateLimit(UAuthEndpoint.AddUserIdentifierSelf);
if (Enabled(UAuthActions.UserIdentifiers.UpdateSelf))
self.MapPost("/identifiers/update", async ([FromServices] IUserEndpointHandler h, HttpContext ctx)
- => await h.UpdateUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement));
+ => await h.UpdateUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)).WithUAuthRateLimit(UAuthEndpoint.UpdateUserIdentifierSelf);
if (Enabled(UAuthActions.UserIdentifiers.SetPrimarySelf))
self.MapPost("/identifiers/set-primary",async ([FromServices] IUserEndpointHandler h, HttpContext ctx)
- => await h.SetPrimaryUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement));
+ => await h.SetPrimaryUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)).WithUAuthRateLimit(UAuthEndpoint.SetPrimaryUserIdentifierSelf);
if (Enabled(UAuthActions.UserIdentifiers.UnsetPrimarySelf))
self.MapPost("/identifiers/unset-primary", async ([FromServices] IUserEndpointHandler h, HttpContext ctx)
- => await h.UnsetPrimaryUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement));
+ => await h.UnsetPrimaryUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)).WithUAuthRateLimit(UAuthEndpoint.UnsetPrimaryUserIdentifierSelf);
if (Enabled(UAuthActions.UserIdentifiers.VerifySelf))
self.MapPost("/identifiers/verify", async ([FromServices] IUserEndpointHandler h, HttpContext ctx)
- => await h.VerifyUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement));
+ => await h.VerifyUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)).WithUAuthRateLimit(UAuthEndpoint.VerifyUserIdentifierSelf);
if (Enabled(UAuthActions.UserIdentifiers.DeleteSelf))
self.MapPost("/identifiers/delete", async ([FromServices] IUserEndpointHandler h, HttpContext ctx)
- => await h.DeleteUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement));
+ => await h.DeleteUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)).WithUAuthRateLimit(UAuthEndpoint.DeleteUserIdentifierSelf);
if (Enabled(UAuthActions.UserIdentifiers.GetAdmin))
adminUsers.MapPost("/{userKey}/identifiers/get", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx)
- => await h.GetUserIdentifiersAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement));
+ => await h.GetUserIdentifiersAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)).WithUAuthRateLimit(UAuthEndpoint.GetUserIdentifiers);
if (Enabled(UAuthActions.UserIdentifiers.AddAdmin))
adminUsers.MapPost("/{userKey}/identifiers/add", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx)
- => await h.AddUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement));
+ => await h.AddUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)).WithUAuthRateLimit(UAuthEndpoint.AddUserIdentifierAdmin);
if (Enabled(UAuthActions.UserIdentifiers.UpdateAdmin))
adminUsers.MapPost("/{userKey}/identifiers/update", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx)
- => await h.UpdateUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement));
+ => await h.UpdateUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)).WithUAuthRateLimit(UAuthEndpoint.UpdateUserIdentifierAdmin);
if (Enabled(UAuthActions.UserIdentifiers.SetPrimaryAdmin))
adminUsers.MapPost("/{userKey}/identifiers/set-primary", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx)
- => await h.SetPrimaryUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement));
+ => await h.SetPrimaryUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)).WithUAuthRateLimit(UAuthEndpoint.SetPrimaryUserIdentifierAdmin);
if (Enabled(UAuthActions.UserIdentifiers.UnsetPrimaryAdmin))
adminUsers.MapPost("/{userKey}/identifiers/unset-primary", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx)
- => await h.UnsetPrimaryUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement));
+ => await h.UnsetPrimaryUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)).WithUAuthRateLimit(UAuthEndpoint.UnsetPrimaryUserIdentifierAdmin);
if (Enabled(UAuthActions.UserIdentifiers.VerifyAdmin))
adminUsers.MapPost("/{userKey}/identifiers/verify", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx)
- => await h.VerifyUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement));
+ => await h.VerifyUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)).WithUAuthRateLimit(UAuthEndpoint.VerifyUserIdentifierAdmin);
if (Enabled(UAuthActions.UserIdentifiers.DeleteAdmin))
adminUsers.MapPost("/{userKey}/identifiers/delete", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx)
- => await h.DeleteUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement));
+ => await h.DeleteUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)).WithUAuthRateLimit(UAuthEndpoint.DeleteUserIdentifierAdmin);
if (Enabled(UAuthActions.UserIdentifiers.CheckAvailability))
self.MapPost("/identifiers/check-availability", async ([FromServices] IUserEndpointHandler h, HttpContext ctx)
- => await h.CheckIdentifierAvailabilityAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement));
+ => await h.CheckIdentifierAvailabilityAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)).WithUAuthRateLimit(UAuthEndpoint.CheckIdentifierAvailability);
}
if (options.Endpoints.Credentials != false)
@@ -328,48 +334,48 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options
if (Enabled(UAuthActions.Credentials.AddSelf))
selfCredentials.MapPost("/add", async ([FromServices] ICredentialEndpointHandler h, HttpContext ctx)
- => await h.AddAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement));
+ => await h.AddAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)).WithUAuthRateLimit(UAuthEndpoint.CredentialAddSelf);
if (Enabled(UAuthActions.Credentials.ChangeSelf))
selfCredentials.MapPost("/change", async ([FromServices] ICredentialEndpointHandler h, HttpContext ctx)
- => await h.ChangeSecretAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement));
+ => await h.ChangeSecretAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)).WithUAuthRateLimit(UAuthEndpoint.CredentialChangeSelf);
if (Enabled(UAuthActions.Credentials.RevokeSelf))
selfCredentials.MapPost("/revoke", async ([FromServices] ICredentialEndpointHandler h, HttpContext ctx)
- => await h.RevokeAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement));
+ => await h.RevokeAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)).WithUAuthRateLimit(UAuthEndpoint.CredentialRevokeSelf);
if (Enabled(UAuthActions.Credentials.BeginResetAnonymous))
selfCredentials.MapPost("/reset/begin", async ([FromServices] ICredentialEndpointHandler h, HttpContext ctx)
- => await h.BeginResetAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement));
+ => await h.BeginResetAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)).WithUAuthRateLimit(UAuthEndpoint.CredentialResetBeginAnonymous);
if (Enabled(UAuthActions.Credentials.CompleteResetAnonymous))
selfCredentials.MapPost("/reset/complete", async ([FromServices] ICredentialEndpointHandler h, HttpContext ctx)
- => await h.CompleteResetAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement));
+ => await h.CompleteResetAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)).WithUAuthRateLimit(UAuthEndpoint.CredentialResetCompleteAnonymous);
if (Enabled(UAuthActions.Credentials.AddAdmin))
adminCredentials.MapPost("/add", async ([FromServices] ICredentialEndpointHandler h, UserKey userKey, HttpContext ctx)
- => await h.AddAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement));
+ => await h.AddAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)).WithUAuthRateLimit(UAuthEndpoint.CredentialAddAdmin);
if (Enabled(UAuthActions.Credentials.ChangeAdmin))
adminCredentials.MapPost("/change", async ([FromServices] ICredentialEndpointHandler h, UserKey userKey, HttpContext ctx)
- => await h.ChangeSecretAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement));
+ => await h.ChangeSecretAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)).WithUAuthRateLimit(UAuthEndpoint.CredentialChangeAdmin);
if (Enabled(UAuthActions.Credentials.RevokeAdmin))
adminCredentials.MapPost("/revoke", async ([FromServices] ICredentialEndpointHandler h, UserKey userKey, HttpContext ctx)
- => await h.RevokeAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement));
+ => await h.RevokeAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)).WithUAuthRateLimit(UAuthEndpoint.CredentialRevokeAdmin);
if (Enabled(UAuthActions.Credentials.BeginResetAdmin))
adminCredentials.MapPost("/reset/begin", async ([FromServices] ICredentialEndpointHandler h, UserKey userKey, HttpContext ctx)
- => await h.BeginResetAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement));
+ => await h.BeginResetAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)).WithUAuthRateLimit(UAuthEndpoint.CredentialResetBeginAdmin);
if (Enabled(UAuthActions.Credentials.CompleteResetAdmin))
adminCredentials.MapPost("/reset/complete", async ([FromServices] ICredentialEndpointHandler h, UserKey userKey, HttpContext ctx)
- => await h.CompleteResetAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement));
+ => await h.CompleteResetAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)).WithUAuthRateLimit(UAuthEndpoint.CredentialResetCompleteAdmin);
if (Enabled(UAuthActions.Credentials.DeleteAdmin))
adminCredentials.MapPost("/delete", async ([FromServices] ICredentialEndpointHandler h, UserKey userKey, HttpContext ctx)
- => await h.DeleteAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement));
+ => await h.DeleteAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)).WithUAuthRateLimit(UAuthEndpoint.CredentialDeleteAdmin);
}
if (options.Endpoints.Authorization != false)
@@ -379,48 +385,48 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options
// TODO: Add enabled actions here
selfAuthz.MapPost("/check", async ([FromServices] IAuthorizationEndpointHandler h, HttpContext ctx)
- => await h.CheckAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement));
+ => await h.CheckAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement)).WithUAuthRateLimit(UAuthEndpoint.AuthorizationCheck);
if (Enabled(UAuthActions.Authorization.Roles.GetSelf))
selfAuthz.MapPost("/roles/get", async ([FromServices] IAuthorizationEndpointHandler h, HttpContext ctx)
- => await h.GetMyRolesAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement));
+ => await h.GetMyRolesAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement)).WithUAuthRateLimit(UAuthEndpoint.GetMyRoles);
if (Enabled(UAuthActions.Authorization.Permissions.GetSelf))
selfAuthz.MapPost("/permissions/get", async ([FromServices] IAuthorizationEndpointHandler h, HttpContext ctx)
- => await h.GetMyPermissionsAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement));
+ => await h.GetMyPermissionsAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement)).WithUAuthRateLimit(UAuthEndpoint.GetMyPermissions);
if (Enabled(UAuthActions.Authorization.Roles.GetAdmin))
adminAuthz.MapPost("/users/{userKey}/roles/get", async ([FromServices] IAuthorizationEndpointHandler h, UserKey userKey, HttpContext ctx)
- => await h.GetUserRolesAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement));
+ => await h.GetUserRolesAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement)).WithUAuthRateLimit(UAuthEndpoint.GetUserRoles);
if (Enabled(UAuthActions.Authorization.Roles.AssignAdmin))
adminAuthz.MapPost("/users/{userKey}/roles/assign", async ([FromServices] IAuthorizationEndpointHandler h, UserKey userKey, HttpContext ctx)
- => await h.AssignRoleAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement));
+ => await h.AssignRoleAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement)).WithUAuthRateLimit(UAuthEndpoint.AssignRole);
if (Enabled(UAuthActions.Authorization.Roles.RemoveAdmin))
adminAuthz.MapPost("/users/{userKey}/roles/remove", async ([FromServices] IAuthorizationEndpointHandler h, UserKey userKey, HttpContext ctx)
- => await h.RemoveRoleAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement));
+ => await h.RemoveRoleAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement)).WithUAuthRateLimit(UAuthEndpoint.RemoveRole);
if (Enabled(UAuthActions.Authorization.Roles.CreateAdmin))
adminAuthz.MapPost("/roles/create",async ([FromServices] IAuthorizationEndpointHandler h, HttpContext ctx)
- => await h.CreateRoleAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement));
+ => await h.CreateRoleAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement)).WithUAuthRateLimit(UAuthEndpoint.CreateRole);
if (Enabled(UAuthActions.Authorization.Roles.QueryAdmin))
adminAuthz.MapPost("/roles/query", async ([FromServices] IAuthorizationEndpointHandler h, HttpContext ctx)
- => await h.QueryRolesAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement));
+ => await h.QueryRolesAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement)).WithUAuthRateLimit(UAuthEndpoint.QueryRoles);
if (Enabled(UAuthActions.Authorization.Roles.RenameAdmin))
adminAuthz.MapPost("/roles/{roleId}/rename", async ([FromServices] IAuthorizationEndpointHandler h, RoleId roleId, HttpContext ctx)
- => await h.RenameRoleAsync(roleId, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement));
+ => await h.RenameRoleAsync(roleId, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement)).WithUAuthRateLimit(UAuthEndpoint.RenameRole);
if (Enabled(UAuthActions.Authorization.Roles.SetPermissionsAdmin))
adminAuthz.MapPost("/roles/{roleId}/permissions", async ([FromServices] IAuthorizationEndpointHandler h, RoleId roleId, HttpContext ctx)
- => await h.SetRolePermissionsAsync(roleId, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement));
+ => await h.SetRolePermissionsAsync(roleId, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement)).WithUAuthRateLimit(UAuthEndpoint.SetRolePermissions);
if (Enabled(UAuthActions.Authorization.Roles.DeleteAdmin))
adminAuthz.MapPost("/roles/{roleId}/delete", async ([FromServices] IAuthorizationEndpointHandler h, RoleId roleId, HttpContext ctx)
- => await h.DeleteRoleAsync(roleId, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement));
+ => await h.DeleteRoleAsync(roleId, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement)).WithUAuthRateLimit(UAuthEndpoint.DeleteRole);
}
// IMPORTANT:
diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs
index 8b487a53..572cf606 100644
--- a/src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs
@@ -1,4 +1,5 @@
-using CodeBeam.UltimateAuth.Authorization;
+using Microsoft.AspNetCore.Builder;
+using CodeBeam.UltimateAuth.Authorization;
using CodeBeam.UltimateAuth.Core;
using CodeBeam.UltimateAuth.Core.Abstractions;
using CodeBeam.UltimateAuth.Core.Defaults;
@@ -66,6 +67,7 @@ public static IServiceCollection AddUltimateAuthServer(this IServiceCollection s
.ValidateOnStart();
services.AddUltimateAuthServerInternal();
+ services.AddRateLimiter(_ => { });
return services;
}
diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthApplicationBuilderExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthApplicationBuilderExtensions.cs
index 5d806c85..fb7c1886 100644
--- a/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthApplicationBuilderExtensions.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthApplicationBuilderExtensions.cs
@@ -10,21 +10,39 @@ namespace CodeBeam.UltimateAuth.Server.Extensions;
public static class UltimateAuthApplicationBuilderExtensions
{
- public static IApplicationBuilder UseUltimateAuth(this IApplicationBuilder app)
+ /// Installs UltimateAuth context middleware. Optional throttling requires routing first.
+ /// Do not combine helper-owned throttling with an independent UseRateLimiter call.
+ public static IApplicationBuilder UseUltimateAuth(this IApplicationBuilder app) =>
+ UseUltimateAuth(app, useRateLimiting: false);
+
+ public static IApplicationBuilder UseUltimateAuth(this IApplicationBuilder app, bool useRateLimiting)
{
app.UseUAuthExceptionHandling();
app.UseMiddleware();
app.UseMiddleware();
app.UseMiddleware();
+ if (useRateLimiting) UseOwnedRateLimiter(app);
return app;
}
- public static IApplicationBuilder UseUltimateAuthWithAspNetCore(this IApplicationBuilder app, bool? enableCors = null)
+ // Both arguments are required to preserve positional bool calls as the original enableCors argument.
+ ///
+ /// Configures the UltimateAuth ASP.NET Core middleware pipeline, including CORS, authentication, rate limiting, and authorization.
+ ///
+ /// The application builder.
+ ///
+ /// Controls CORS middleware registration. When null, CORS is enabled automatically if required by UAuthHub.
+ ///
+ ///
+ /// Enables UltimateAuth's built-in rate limiting middleware. Defaults to true.
+ ///
+ ///
+ /// The application builder.
+ ///
+ public static IApplicationBuilder UseUltimateAuthWithAspNetCore(this IApplicationBuilder app, bool? enableCors = null, bool useRateLimiting = true)
{
- var logger = app.ApplicationServices
- .GetRequiredService()
- .CreateLogger("UltimateAuth");
+ var logger = app.ApplicationServices.GetRequiredService().CreateLogger("UltimateAuth");
var marker = app.ApplicationServices.GetService();
var requiresCors = marker?.RequiresCors == true;
@@ -39,10 +57,24 @@ public static IApplicationBuilder UseUltimateAuthWithAspNetCore(this IApplicatio
app.UseUltimateAuth();
app.UseAuthentication();
+
+ if (useRateLimiting)
+ UseOwnedRateLimiter(app);
+
app.UseAuthorization();
+
return app;
}
+ private const string OwnedRateLimiterKey = "CodeBeam.UltimateAuth.RateLimiterInstalled";
+
+ private static void UseOwnedRateLimiter(IApplicationBuilder app)
+ {
+ if (app.Properties.TryGetValue(OwnedRateLimiterKey, out var owner) && ReferenceEquals(owner, app)) return;
+ app.Properties[OwnedRateLimiterKey] = app;
+ app.UseRateLimiter();
+ }
+
public static IApplicationBuilder UseUltimateAuthResourceApi(this IApplicationBuilder app)
{
var logger = app.ApplicationServices
diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthHubEndpointExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthHubEndpointExtensions.cs
index 7aef3c12..aa0af9a5 100644
--- a/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthHubEndpointExtensions.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthHubEndpointExtensions.cs
@@ -1,4 +1,5 @@
-using CodeBeam.UltimateAuth.Server.Infrastructure;
+using CodeBeam.UltimateAuth.Server.RateLimiting;
+using CodeBeam.UltimateAuth.Server.Infrastructure;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
@@ -9,7 +10,7 @@ public static class UAuthHubEndpointExtensions
public static IEndpointRouteBuilder MapUAuthHub(this IEndpointRouteBuilder endpoints)
{
var group = endpoints.MapGroup("/auth/uauthhub");
- group.MapPost("/entry", HandleHub.HandleHubEntry);
+ group.MapPost("/entry", HandleHub.HandleHubEntry).WithUAuthRateLimit(UAuthEndpoint.HubEntry);
return endpoints;
}
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/ClientIpAddress.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/ClientIpAddress.cs
new file mode 100644
index 00000000..4d66c17c
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/ClientIpAddress.cs
@@ -0,0 +1,14 @@
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.Infrastructure;
+
+// Forwarded headers are processed by the host, with its explicit proxy trust configuration.
+internal static class ClientIpAddress
+{
+ internal static string Resolve(HttpContext context)
+ {
+ var ip = context.Connection.RemoteIpAddress;
+ if (ip?.IsIPv4MappedToIPv6 == true) ip = ip.MapToIPv4();
+ return ip?.ToString() ?? "unknown";
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Device/DeviceResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Device/DeviceResolver.cs
index 8fa959b7..38672239 100644
--- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Device/DeviceResolver.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Device/DeviceResolver.cs
@@ -9,7 +9,7 @@
namespace CodeBeam.UltimateAuth.Server.Infrastructure;
-// TODO: Consider creating a seperate package with a library like UA Parser, WURFL or DeviceAtlas for more accurate device detection.
+// TODO: Consider creating a alternative package with a library like UA Parser, WURFL or DeviceAtlas for more accurate device detection.
public sealed class DeviceResolver : IDeviceResolver
{
private readonly IUserAgentParser _userAgentParser;
@@ -40,7 +40,7 @@ public async Task ResolveAsync(HttpContext context)
OperatingSystem = parsed.OperatingSystem,
Browser = parsed.Browser,
UserAgent = ua,
- IpAddress = ResolveIp(context)
+ IpAddress = ClientIpAddress.Resolve(context)
};
return deviceInfo;
@@ -68,14 +68,4 @@ public async Task ResolveAsync(HttpContext context)
return null;
}
-
- private static string? ResolveIp(HttpContext context)
- {
- var forwarded = context.Request.Headers["X-Forwarded-For"].FirstOrDefault();
-
- if (!string.IsNullOrWhiteSpace(forwarded))
- return forwarded.Split(',')[0].Trim();
-
- return context.Connection.RemoteIpAddress?.ToString();
- }
}
diff --git a/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptions.cs b/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptions.cs
index 1c04962c..c5474e56 100644
--- a/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptions.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptions.cs
@@ -52,6 +52,9 @@ public sealed class UAuthServerOptions
// SERVER-ONLY BEHAVIOR
// -------------------------------------------------------
+ /// Startup-only native endpoint throttling; application policies take precedence.
+ public RateLimiting.UAuthRateLimitingOptions RateLimiting { get; set; } = new();
+
///
/// Defines which authentication modes are allowed to be used by the server.
/// This is a safety guardrail, not a mode selection mechanism.
@@ -144,6 +147,7 @@ internal UAuthServerOptions Clone()
AllowedModes = AllowedModes?.ToArray(),
HubDeploymentMode = HubDeploymentMode,
+ RateLimiting = RateLimiting.Clone(),
Login = Login.Clone(),
Session = Session.Clone(),
Token = Token.Clone(),
diff --git a/src/CodeBeam.UltimateAuth.Server/Options/Validators/UAuthServerOptionsValidator.cs b/src/CodeBeam.UltimateAuth.Server/Options/Validators/UAuthServerOptionsValidator.cs
index e04e77ad..89caef50 100644
--- a/src/CodeBeam.UltimateAuth.Server/Options/Validators/UAuthServerOptionsValidator.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Options/Validators/UAuthServerOptionsValidator.cs
@@ -7,6 +7,9 @@ public sealed class UAuthServerOptionsValidator : IValidateOptionsStable identities of the built-in HTTP endpoints.
+public enum UAuthEndpoint
+{
+ Login,
+ TryLogin,
+ Validate,
+ Logout,
+ LogoutDeviceSelf,
+ LogoutOthersSelf,
+ LogoutAllSelf,
+ Refresh,
+ LogoutDeviceAdmin,
+ LogoutOthersAdmin,
+ LogoutAllAdmin,
+ PkceAuthorize,
+ PkceComplete,
+ PkceTryComplete,
+ GetMyChains,
+ GetMyChainDetail,
+ RevokeMyChain,
+ RevokeOtherChains,
+ RevokeAllMyChains,
+ GetUserChains,
+ GetUserChainDetail,
+ RevokeUserSession,
+ RevokeUserChain,
+ RevokeRoot,
+ RevokeAllChains,
+ UserCreateAnonymous,
+ ChangeStatusSelf,
+ UserDeleteSelf,
+ QueryUsers,
+ GetUsersByKeys,
+ UserCreateAdmin,
+ ChangeStatusAdmin,
+ UserDeleteAdmin,
+ ProfileGetSelf,
+ CreateProfileSelf,
+ ProfileUpdateSelf,
+ DeleteProfileSelf,
+ ProfileGetAdmin,
+ CreateProfileAdmin,
+ ProfileUpdateAdmin,
+ DeleteProfileAdmin,
+ GetMyIdentifiers,
+ AddUserIdentifierSelf,
+ UpdateUserIdentifierSelf,
+ SetPrimaryUserIdentifierSelf,
+ UnsetPrimaryUserIdentifierSelf,
+ VerifyUserIdentifierSelf,
+ DeleteUserIdentifierSelf,
+ GetUserIdentifiers,
+ AddUserIdentifierAdmin,
+ UpdateUserIdentifierAdmin,
+ SetPrimaryUserIdentifierAdmin,
+ UnsetPrimaryUserIdentifierAdmin,
+ VerifyUserIdentifierAdmin,
+ DeleteUserIdentifierAdmin,
+ CheckIdentifierAvailability,
+ CredentialAddSelf,
+ CredentialChangeSelf,
+ CredentialRevokeSelf,
+ CredentialResetBeginAnonymous,
+ CredentialResetCompleteAnonymous,
+ CredentialAddAdmin,
+ CredentialChangeAdmin,
+ CredentialRevokeAdmin,
+ CredentialResetBeginAdmin,
+ CredentialResetCompleteAdmin,
+ CredentialDeleteAdmin,
+ AuthorizationCheck,
+ GetMyRoles,
+ GetMyPermissions,
+ GetUserRoles,
+ AssignRole,
+ RemoveRole,
+ CreateRole,
+ QueryRoles,
+ RenameRole,
+ SetRolePermissions,
+ DeleteRole,
+ HubEntry,
+}
+
+internal enum UAuthRateLimitGroup { Sensitive, Standard, Frequent }
diff --git a/src/CodeBeam.UltimateAuth.Server/RateLimiting/UAuthEndpointClassification.cs b/src/CodeBeam.UltimateAuth.Server/RateLimiting/UAuthEndpointClassification.cs
new file mode 100644
index 00000000..d2ff588e
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/RateLimiting/UAuthEndpointClassification.cs
@@ -0,0 +1,90 @@
+namespace CodeBeam.UltimateAuth.Server.RateLimiting;
+
+internal sealed record UAuthRateLimitMetadata(UAuthEndpoint Endpoint, UAuthRateLimitGroup Group, bool IpOnly);
+
+internal static class UAuthEndpointClassification
+{
+ internal static UAuthRateLimitMetadata Get(UAuthEndpoint endpoint) => endpoint switch
+ {
+ UAuthEndpoint.Login => new(endpoint, UAuthRateLimitGroup.Sensitive, true),
+ UAuthEndpoint.TryLogin => new(endpoint, UAuthRateLimitGroup.Sensitive, true),
+ UAuthEndpoint.Validate => new(endpoint, UAuthRateLimitGroup.Frequent, false),
+ UAuthEndpoint.Logout => new(endpoint, UAuthRateLimitGroup.Frequent, false),
+ UAuthEndpoint.LogoutDeviceSelf => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.LogoutOthersSelf => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.LogoutAllSelf => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.Refresh => new(endpoint, UAuthRateLimitGroup.Frequent, false),
+ UAuthEndpoint.LogoutDeviceAdmin => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.LogoutOthersAdmin => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.LogoutAllAdmin => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.PkceAuthorize => new(endpoint, UAuthRateLimitGroup.Sensitive, true),
+ UAuthEndpoint.PkceComplete => new(endpoint, UAuthRateLimitGroup.Sensitive, true),
+ UAuthEndpoint.PkceTryComplete => new(endpoint, UAuthRateLimitGroup.Sensitive, true),
+ UAuthEndpoint.GetMyChains => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.GetMyChainDetail => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.RevokeMyChain => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.RevokeOtherChains => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.RevokeAllMyChains => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.GetUserChains => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.GetUserChainDetail => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.RevokeUserSession => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.RevokeUserChain => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.RevokeRoot => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.RevokeAllChains => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.UserCreateAnonymous => new(endpoint, UAuthRateLimitGroup.Sensitive, true),
+ UAuthEndpoint.ChangeStatusSelf => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.UserDeleteSelf => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.QueryUsers => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.GetUsersByKeys => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.UserCreateAdmin => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.ChangeStatusAdmin => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.UserDeleteAdmin => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.ProfileGetSelf => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.CreateProfileSelf => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.ProfileUpdateSelf => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.DeleteProfileSelf => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.ProfileGetAdmin => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.CreateProfileAdmin => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.ProfileUpdateAdmin => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.DeleteProfileAdmin => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.GetMyIdentifiers => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.AddUserIdentifierSelf => new(endpoint, UAuthRateLimitGroup.Sensitive, false),
+ UAuthEndpoint.UpdateUserIdentifierSelf => new(endpoint, UAuthRateLimitGroup.Sensitive, false),
+ UAuthEndpoint.SetPrimaryUserIdentifierSelf => new(endpoint, UAuthRateLimitGroup.Sensitive, false),
+ UAuthEndpoint.UnsetPrimaryUserIdentifierSelf => new(endpoint, UAuthRateLimitGroup.Sensitive, false),
+ UAuthEndpoint.VerifyUserIdentifierSelf => new(endpoint, UAuthRateLimitGroup.Sensitive, true),
+ UAuthEndpoint.DeleteUserIdentifierSelf => new(endpoint, UAuthRateLimitGroup.Sensitive, false),
+ UAuthEndpoint.GetUserIdentifiers => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.AddUserIdentifierAdmin => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.UpdateUserIdentifierAdmin => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.SetPrimaryUserIdentifierAdmin => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.UnsetPrimaryUserIdentifierAdmin => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.VerifyUserIdentifierAdmin => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.DeleteUserIdentifierAdmin => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.CheckIdentifierAvailability => new(endpoint, UAuthRateLimitGroup.Sensitive, true),
+ UAuthEndpoint.CredentialAddSelf => new(endpoint, UAuthRateLimitGroup.Sensitive, false),
+ UAuthEndpoint.CredentialChangeSelf => new(endpoint, UAuthRateLimitGroup.Sensitive, false),
+ UAuthEndpoint.CredentialRevokeSelf => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.CredentialResetBeginAnonymous => new(endpoint, UAuthRateLimitGroup.Sensitive, true),
+ UAuthEndpoint.CredentialResetCompleteAnonymous => new(endpoint, UAuthRateLimitGroup.Sensitive, true),
+ UAuthEndpoint.CredentialAddAdmin => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.CredentialChangeAdmin => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.CredentialRevokeAdmin => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.CredentialResetBeginAdmin => new(endpoint, UAuthRateLimitGroup.Sensitive, false),
+ UAuthEndpoint.CredentialResetCompleteAdmin => new(endpoint, UAuthRateLimitGroup.Sensitive, false),
+ UAuthEndpoint.CredentialDeleteAdmin => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.AuthorizationCheck => new(endpoint, UAuthRateLimitGroup.Frequent, false),
+ UAuthEndpoint.GetMyRoles => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.GetMyPermissions => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.GetUserRoles => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.AssignRole => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.RemoveRole => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.CreateRole => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.QueryRoles => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.RenameRole => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.SetRolePermissions => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.DeleteRole => new(endpoint, UAuthRateLimitGroup.Standard, false),
+ UAuthEndpoint.HubEntry => new(endpoint, UAuthRateLimitGroup.Sensitive, true),
+ _ => throw new ArgumentOutOfRangeException(nameof(endpoint))
+ };
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/RateLimiting/UAuthRateLimitPolicy.cs b/src/CodeBeam.UltimateAuth.Server/RateLimiting/UAuthRateLimitPolicy.cs
new file mode 100644
index 00000000..435e4f21
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/RateLimiting/UAuthRateLimitPolicy.cs
@@ -0,0 +1,107 @@
+using System.Globalization;
+using System.Threading.RateLimiting;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.RateLimiting;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using CodeBeam.UltimateAuth.Server.Options;
+
+namespace CodeBeam.UltimateAuth.Server.RateLimiting;
+
+internal readonly record struct UAuthPartitionKey(string Budget, string Identity);
+
+internal sealed class UAuthRateLimitPolicy : IRateLimiterPolicy
+{
+ private static readonly object ActorKey = new();
+ private readonly UAuthRateLimitMetadata _metadata;
+ private readonly UAuthRateLimitRule _rule;
+ private readonly string _budget;
+
+ internal UAuthRateLimitPolicy(UAuthRateLimitMetadata metadata, UAuthRateLimitRule rule, string budget)
+ {
+ _metadata = metadata; _rule = rule.Clone(); _budget = budget;
+ }
+
+ internal static void SetValidatedActor(HttpContext context, string tenant, string user) =>
+ context.Items[ActorKey] = (tenant, user);
+
+ public RateLimitPartition GetPartition(HttpContext context)
+ {
+ var identity = !_metadata.IpOnly && context.Items.TryGetValue(ActorKey, out var actor)
+ && actor is ValueTuple validated
+ ? $"user:{validated.Item1.Length}:{validated.Item1}:{validated.Item2}"
+ : "ip:" + Infrastructure.ClientIpAddress.Resolve(context);
+ return RateLimitPartition.GetSlidingWindowLimiter(new UAuthPartitionKey(_budget, identity), _ => new()
+ {
+ PermitLimit = _rule.PermitLimit,
+ Window = _rule.Window,
+ SegmentsPerWindow = 6,
+ QueueLimit = 0,
+ AutoReplenishment = true
+ });
+ }
+
+ public Func? OnRejected => RejectAsync;
+
+ private async ValueTask RejectAsync(OnRejectedContext rejected, CancellationToken cancellationToken)
+ {
+ var response = rejected.HttpContext.Response;
+ response.StatusCode = StatusCodes.Status429TooManyRequests;
+ response.Headers.CacheControl = "no-store";
+ if (rejected.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retry))
+ response.Headers.RetryAfter = Math.Max(1, Math.Ceiling(retry.TotalSeconds)).ToString(CultureInfo.InvariantCulture);
+ await Results.Problem(statusCode: 429, title: "Too many requests",
+ detail: "rate_limited", extensions: new Dictionary
+ { ["traceId"] = rejected.HttpContext.TraceIdentifier }).ExecuteAsync(rejected.HttpContext);
+ }
+}
+
+internal static class UAuthRateLimitEndpointExtensions
+{
+ internal static T WithUAuthRateLimit(this T builder, UAuthEndpoint endpoint) where T : IEndpointConventionBuilder
+ {
+ var metadata = UAuthEndpointClassification.Get(endpoint);
+ builder.Add(b => b.Metadata.Add(metadata));
+ builder.Finally(b =>
+ {
+ // Includes policies inherited from application route groups. Native Disable also wins.
+ if (b.Metadata.OfType().Any() ||
+ b.Metadata.OfType().Any()) return;
+ var options = b.ApplicationServices.GetRequiredService>().Value.RateLimiting;
+ var (rule, budget) = options.Resolve(metadata);
+ if (!options.Enabled || !rule.Enabled) return;
+ var immediate = new ImmediateConventionBuilder(b);
+ if (rule.PolicyName is not null) immediate.RequireRateLimiting(rule.PolicyName);
+ else immediate.RequireRateLimiting(new UAuthRateLimitPolicy(metadata, rule, budget));
+ });
+ return builder;
+ }
+
+ // Uses the public native extension to build inline-policy metadata during finalization.
+ private sealed class ImmediateConventionBuilder(EndpointBuilder endpoint) : IEndpointConventionBuilder
+ {
+ public void Add(Action convention) => convention(endpoint);
+ }
+}
+
+internal static class UAuthRateLimitValidation
+{
+ internal static string? Validate(UAuthRateLimitingOptions? options)
+ {
+ if (options is null || options.Endpoints is null) return "RateLimiting and its Endpoints cannot be null.";
+ if (options.Endpoints.Keys.Any(key => !Enum.IsDefined(typeof(UAuthEndpoint), key)))
+ return "RateLimiting.Endpoints contains an unknown endpoint.";
+ foreach (var rule in new[] { options.Sensitive, options.Standard, options.Frequent, options.Login }.Concat(options.Endpoints.Values))
+ {
+ if (rule is null) return "RateLimiting rules cannot be null.";
+ if (rule.PolicyName is not null)
+ {
+ if (string.IsNullOrWhiteSpace(rule.PolicyName)) return "RateLimiting.PolicyName cannot be blank.";
+ }
+ else if (rule.PermitLimit <= 0 || rule.Window.Ticks < 6)
+ return "RateLimiting requires a positive PermitLimit and a window of at least six ticks.";
+ }
+ return null;
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/RateLimiting/UAuthRateLimitingOptions.cs b/src/CodeBeam.UltimateAuth.Server/RateLimiting/UAuthRateLimitingOptions.cs
new file mode 100644
index 00000000..158aa062
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/RateLimiting/UAuthRateLimitingOptions.cs
@@ -0,0 +1,57 @@
+namespace CodeBeam.UltimateAuth.Server.RateLimiting;
+
+/// Startup-only endpoint rate limiting configuration. Counters are process-local.
+public sealed class UAuthRateLimitingOptions
+{
+ public bool Enabled { get; set; } = true;
+ public UAuthRateLimitRule Sensitive { get; set; } = new() { PermitLimit = 30 };
+ public UAuthRateLimitRule Standard { get; set; } = new() { PermitLimit = 120 };
+ public UAuthRateLimitRule Frequent { get; set; } = new() { PermitLimit = 600 };
+ /// Shared by Login, TryLogin, PkceComplete and PkceTryComplete unless explicitly overridden.
+ public UAuthRateLimitRule Login { get; set; } = new() { PermitLimit = 20 };
+ /// Complete replacement rules. Disabling removes only UltimateAuth's contribution.
+ public Dictionary Endpoints { get; set; } = new();
+
+ internal (UAuthRateLimitRule Rule, string Budget) Resolve(UAuthRateLimitMetadata metadata)
+ {
+ if (Endpoints.TryGetValue(metadata.Endpoint, out var rule))
+ return (rule, metadata.Endpoint.ToString());
+ if (metadata.Endpoint is UAuthEndpoint.Login or UAuthEndpoint.TryLogin or UAuthEndpoint.PkceComplete or UAuthEndpoint.PkceTryComplete)
+ return (Login, "login-verification");
+ rule = metadata.Group switch
+ {
+ UAuthRateLimitGroup.Sensitive => Sensitive,
+ UAuthRateLimitGroup.Standard => Standard,
+ _ => Frequent
+ };
+ // Explicit endpoint overrides replace these security defaults as well.
+ rule = metadata.Endpoint switch
+ {
+ UAuthEndpoint.UserCreateAnonymous => new() { PermitLimit = 10, Window = TimeSpan.FromMinutes(10), Enabled = rule.Enabled, PolicyName = rule.PolicyName },
+ UAuthEndpoint.CredentialResetBeginAnonymous => new() { PermitLimit = 5, Window = TimeSpan.FromMinutes(10), Enabled = rule.Enabled, PolicyName = rule.PolicyName },
+ UAuthEndpoint.CredentialResetCompleteAnonymous or UAuthEndpoint.VerifyUserIdentifierSelf => new() { PermitLimit = 10, Window = TimeSpan.FromMinutes(10), Enabled = rule.Enabled, PolicyName = rule.PolicyName },
+ _ => rule
+ };
+ return (rule, metadata.Endpoint.ToString());
+ }
+
+ internal UAuthRateLimitingOptions Clone() => new()
+ {
+ Enabled = Enabled,
+ Sensitive = Sensitive.Clone(),
+ Standard = Standard.Clone(),
+ Frequent = Frequent.Clone(),
+ Login = Login.Clone(),
+ Endpoints = Endpoints.ToDictionary(x => x.Key, x => x.Value.Clone())
+ };
+}
+
+public sealed class UAuthRateLimitRule
+{
+ public bool Enabled { get; set; } = true;
+ public int PermitLimit { get; set; } = 30;
+ public TimeSpan Window { get; set; } = TimeSpan.FromMinutes(1);
+ /// Optional application-owned named native policy; numeric settings are unused. Native policy/middleware rejection precedence applies. Built-in inline policies otherwise own their 429 response.
+ public string? PolicyName { get; set; }
+ internal UAuthRateLimitRule Clone() => (UAuthRateLimitRule)MemberwiseClone();
+}
diff --git a/src/client/CodeBeam.UltimateAuth.Client.Blazor/Infrastructure/SessionCoordinator.cs b/src/client/CodeBeam.UltimateAuth.Client.Blazor/Infrastructure/SessionCoordinator.cs
index 2c0ad536..96469232 100644
--- a/src/client/CodeBeam.UltimateAuth.Client.Blazor/Infrastructure/SessionCoordinator.cs
+++ b/src/client/CodeBeam.UltimateAuth.Client.Blazor/Infrastructure/SessionCoordinator.cs
@@ -1,5 +1,6 @@
using CodeBeam.UltimateAuth.Client.Abstractions;
using CodeBeam.UltimateAuth.Client.Contracts;
+using CodeBeam.UltimateAuth.Client.Infrastructure;
using CodeBeam.UltimateAuth.Client.Diagnostics;
using CodeBeam.UltimateAuth.Client.Options;
using CodeBeam.UltimateAuth.Core.Abstractions;
@@ -19,6 +20,7 @@ internal sealed class SessionCoordinator : ISessionCoordinator
private readonly IClock _clock;
private CancellationTokenSource? _cts;
+ private DateTimeOffset _refreshNotBefore;
public event Action? ReauthRequired;
@@ -53,8 +55,11 @@ private async Task RunAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
- _diagnostics.SetNextAutomaticRefresh(_clock.UtcNow.Add(interval));
- await Task.Delay(interval, ct);
+ var delay = _refreshNotBefore - _clock.UtcNow;
+ if (delay < interval) delay = interval;
+ _diagnostics.SetNextAutomaticRefresh(UAuthRateLimit.NotBefore(_clock.UtcNow, delay));
+ // Chunk very long guidance to stay within native timer bounds; TickAsync still checks the deadline.
+ await Task.Delay(delay > TimeSpan.FromDays(1) ? TimeSpan.FromDays(1) : delay, ct);
_diagnostics.SetNextAutomaticRefresh(null);
await TickAsync();
@@ -86,10 +91,17 @@ public async ValueTask DisposeAsync()
internal async Task TickAsync()
{
+ if (_clock.UtcNow < _refreshNotBefore) return;
_diagnostics.MarkAutomaticRefresh();
var result = await _client.Flows.RefreshAsync(true);
+ if (result.Status == 429)
+ {
+ _refreshNotBefore = UAuthRateLimit.NotBefore(_clock.UtcNow, result.RetryAfter);
+ return;
+ }
+
if (result.Outcome != RefreshOutcome.ReauthRequired)
return;
diff --git a/src/client/CodeBeam.UltimateAuth.Client.Blazor/Infrastructure/UAuthRequestClient.cs b/src/client/CodeBeam.UltimateAuth.Client.Blazor/Infrastructure/UAuthRequestClient.cs
index f35488ff..5c7b3ea1 100644
--- a/src/client/CodeBeam.UltimateAuth.Client.Blazor/Infrastructure/UAuthRequestClient.cs
+++ b/src/client/CodeBeam.UltimateAuth.Client.Blazor/Infrastructure/UAuthRequestClient.cs
@@ -6,6 +6,7 @@
using Microsoft.Extensions.Options;
using Microsoft.JSInterop;
using System.Net;
+using System.Text.Json;
// TODO: Add fluent helper API like RequiredOk
namespace CodeBeam.UltimateAuth.Client.Blazor.Infrastructure;
@@ -66,6 +67,24 @@ public async Task SendFormAsync(string endpoint, IDictiona
return result;
}
+ public async Task SendFormAsync(string endpoint, IDictionary? form, bool navigateOnSuccess, CancellationToken ct = default)
+ {
+ if (!navigateOnSuccess) return await SendFormAsync(endpoint, form, ct);
+ ct.ThrowIfCancellationRequested();
+ await _bootstrapper.EnsureStartedAsync();
+ var result = await _js.InvokeAsync("uauth.post", ct, new
+ {
+ url = endpoint,
+ mode = "logout",
+ data = form,
+ clientProfile = _options.ClientProfile.ToString(),
+ trustedOrigin = ResolveTrustedOrigin()
+ });
+ if (result is null) throw new UAuthProtocolException("Invalid logout transport response.");
+ if (result.Status == 0) throw new UAuthTransportException("Could not confirm server logout.");
+ return result;
+ }
+
public async Task SendJsonAsync(string endpoint, object? payload = default, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
@@ -95,7 +114,7 @@ public async Task TryAndCommitAsync(string tryEndpoint,
await _bootstrapper.EnsureStartedAsync();
- var response = await _js.InvokeAsync(
+ var response = await _js.InvokeAsync(
"uauth.tryAndCommit",
ct,
new
@@ -107,10 +126,16 @@ public async Task TryAndCommitAsync(string tryEndpoint,
trustedOrigin = ResolveTrustedOrigin()
});
- if (response is null)
+ if (response.ValueKind != JsonValueKind.Object)
throw new UAuthProtocolException("Invalid tryAndCommit response.");
-
- return response;
+ if (response.TryGetProperty("status", out var status) && status.ValueKind == JsonValueKind.Number && status.TryGetInt32(out var code) && code == 429)
+ throw UAuthRateLimit.Exception(response.TryGetProperty("retryAfter", out var retry) && retry.ValueKind == JsonValueKind.String ? retry.GetString() : null);
+ try
+ {
+ return response.Deserialize(new JsonSerializerOptions(JsonSerializerDefaults.Web))
+ ?? throw new UAuthProtocolException("Invalid tryAndCommit response.");
+ }
+ catch (JsonException ex) { throw new UAuthProtocolException("Invalid tryAndCommit response.", ex); }
}
private string? ResolveTrustedOrigin()
diff --git a/src/client/CodeBeam.UltimateAuth.Client.Blazor/TScripts/uauth.js b/src/client/CodeBeam.UltimateAuth.Client.Blazor/TScripts/uauth.js
index 9cae6ca0..8c81dbab 100644
--- a/src/client/CodeBeam.UltimateAuth.Client.Blazor/TScripts/uauth.js
+++ b/src/client/CodeBeam.UltimateAuth.Client.Blazor/TScripts/uauth.js
@@ -91,6 +91,9 @@ window.uauth.tryAndCommit = async function (options) {
}
const status = tryResponse.status;
+ if (status === 429) {
+ return { isSuccess: false, status: 429, errorCode: "rate_limited", retryAfter: tryResponse.retryAfter ?? null };
+ }
const isHttpSuccess = status >= 200 && status < 300;
const isClientError = status >= 400 && status < 500;
@@ -214,7 +217,9 @@ window.uauth.post = async function (options) {
clientProfile,
contentType: "form",
credentials: "include",
- allowCrossOriginCredentials: true
+ allowCrossOriginCredentials: true,
+ navigateOnSuccess: mode === "logout",
+ headers: mode === "logout" ? { "X-UAuth-Logout-Mode": "observe" } : {}
});
};
@@ -237,6 +242,8 @@ window.uauth.postJson = function (options) {
window.uauth.request = async function (options) {
const {
+ followRedirects = false,
+ navigateOnSuccess = false,
url,
method = "GET",
payload = null,
@@ -314,7 +321,7 @@ window.uauth.request = async function (options) {
response = await fetch(requestUrl.href, {
method: requestMethod,
credentials,
- redirect: "error",
+ redirect: followRedirects ? "follow" : "error",
headers,
body
});
@@ -347,7 +354,24 @@ window.uauth.request = async function (options) {
}
}
+ // Only the observed logout transport follows a successful server redirect.
+ // A rejected POST never navigates, clears storage, or causes a second POST.
+ if (navigateOnSuccess && response.ok) {
+ const redirectUrl = responseBody?.redirectUrl;
+
+ if (typeof redirectUrl === "string" && redirectUrl.length > 0) {
+ const target = new URL(redirectUrl, window.location.origin);
+
+ if (!["http:", "https:"].includes(target.protocol)) {
+ throw new Error("Invalid logout redirect protocol.");
+ }
+
+ window.location.assign(target.href);
+ }
+ }
+
return {
+ retryAfter: response.headers.get("Retry-After"),
ok: response.ok,
status: response.status,
refreshOutcome: response.headers.get("X-UAuth-Refresh"),
@@ -490,6 +514,7 @@ window.uauth.resourceRequest = async function (options) {
}
return {
+ retryAfter: response.headers.get("Retry-After"),
ok: response.ok,
status: response.status,
body: responseBody,
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 c0c82b59..85a8b886 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: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
+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;if(i===429)return{isSuccess:!1,status:429,errorCode:"rate_limited",retryAfter:r.retryAfter??null};const y=i>=200&&i<300,h=i>=400&&i<500;if(!y&&!h)throw new Error(`Unexpected UAuth try response: HTTP ${i}.`);const t=r.body;if(!t||typeof t!="object"||Array.isArray(t)||typeof t.isSuccess!="boolean")throw new Error("Invalid UAuth try response body.");if(h&&t.isSuccess)throw new Error("Successful authentication result returned with HTTP error.");const 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:f,mode:i,data:t,clientProfile:r,trustedOrigin:e}=n,u=window.uauth.validateAuthUrl(f,e);if(i==="navigate"){const n=document.createElement("form");n.method="POST";n.action=u;const i=document.createElement("input");i.type="hidden";i.name="__uauth_client_profile";i.value=r??"";n.appendChild(i);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:u,method:"POST",payload:t,clientProfile:r,contentType:"form",credentials:"include",allowCrossOriginCredentials:!0,navigateOnSuccess:i==="logout",headers:i==="logout"?{"X-UAuth-Logout-Mode":"observe"}:{}})};window.uauth.postJson=function(n){const t=window.uauth.validateAuthUrl(n.url,n.trustedOrigin);return window.uauth.request({url:t,method:"POST",payload:n.payload,clientProfile:n.clientProfile,contentType:"json",credentials:"include",allowCrossOriginCredentials:!0})};window.uauth.request=async function(n){const{followRedirects:a=!1,navigateOnSuccess:v=!1,url:h,method:y="GET",payload:i=null,clientProfile:p="",contentType:c="json",credentials:e="same-origin",headers:w={}}=n;if(!h)throw new Error("Request URL is required.");const o=new URL(h,document.baseURI);if(!["http:","https:"].includes(o.protocol))throw new Error("Unsupported request URL protocol.");const r=y.toUpperCase();if(!["GET","POST","PUT","PATCH","DELETE"].includes(r))throw new Error(`Unsupported HTTP method: ${r}`);if(!["same-origin","include","omit"].includes(e))throw new Error("Invalid credentials policy.");if(e==="include"&&o.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 s={...w,"X-UDID":window.uauth.deviceId,"X-UAuth-ClientProfile":p,"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")s["Content-Type"]="application/json",u=JSON.stringify(i);else if(c==="form"){s["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(o.href,{method:r,credentials:e,redirect:a?"follow":"error",headers:s,body:u})}catch(l){if(l instanceof TypeError)return{ok:!1,status:0,refreshOutcome:null,body:null};throw l;}let f=null;if(t.status!==204&&t.status!==205){const n=await t.text();if(n)try{f=JSON.parse(n)}catch{f=n}}if(v&&t.ok){const n=f?.redirectUrl;if(typeof n=="string"&&n.length>0){const t=new URL(n,window.location.origin);if(!["http:","https:"].includes(t.protocol))throw new Error("Invalid logout redirect protocol.");window.location.assign(t.href)}}return{retryAfter:t.headers.get("Retry-After"),ok:t.ok,status:t.status,refreshOutcome:t.headers.get("X-UAuth-Refresh"),body:f}};window.uauth.resourceRequest=async function(n){const{url:o,baseAddress:h,method:v="GET",payload:f=null,clientProfile:y="",credentials:p="Auto",requestId:u}=n;if(!o||typeof o!="string")throw new Error("Resource request URL is required.");const i=h?new URL(h):new URL(window.location.origin+"/");if(!["http:","https:"].includes(i.protocol))throw new Error("Invalid Resource API protocol.");if(i.username||i.password||i.search||i.hash)throw new Error("Invalid Resource API base address.");const t=new URL(o,i);if(t.origin!==i.origin)throw new Error("Resource request target differs from configured API origin.");if(t.username||t.password)throw new Error("Resource URL cannot contain credentials.");if(!["http:","https:"].includes(t.protocol))throw new Error("Unsupported Resource API protocol.");const s=v.toUpperCase();if(!["GET","POST","PUT","PATCH","DELETE"].includes(s))throw new Error("Unsupported Resource API method.");if(s==="GET"&&f!==null)throw new Error("GET requests cannot contain a body.");let r;switch(p.toLowerCase()){case"auto":r=t.origin===window.location.origin?"same-origin":"include";break;case"sameorigin":r="same-origin";break;case"include":r="include";break;case"omit":r="omit";break;default:throw new Error("Invalid Resource credentials mode.");}if(r==="include"&&t.protocol!=="https:"&&!["localhost","127.0.0.1","[::1]"].includes(t.hostname))throw new Error("Cross-origin credential requests require HTTPS.");if(!window.uauth.deviceId)throw new Error("UAuth deviceId is not initialized.");const c={"X-UDID":window.uauth.deviceId,"X-UAuth-ClientProfile":y,"X-Requested-With":"UAuth"};let l=null;f!==null&&f!==undefined&&(c["Content-Type"]="application/json",l=JSON.stringify(f));const a=new AbortController;if(u){if(window.uauth.resourceRequests.has(u))throw new Error("Duplicate Resource request ID.");window.uauth.resourceRequests.set(u,a)}try{const n=await fetch(t.href,{method:s,credentials:r,redirect:"error",headers:c,body:l,signal:a.signal});let i=null;if(n.status!==204&&n.status!==205){const t=await n.text();t.length>0&&(i=t)}return{retryAfter:n.headers.get("Retry-After"),ok:n.ok,status:n.status,body:i,contentType:n.headers.get("Content-Type")}}catch(e){if(e?.name==="AbortError")throw e;if(e instanceof TypeError)return{ok:!1,status:0,body:null,contentType:null};throw e;}finally{u&&window.uauth.resourceRequests.delete(u)}};window.uauth.resourceRequests=new Map;window.uauth.cancelResourceRequest=function(n){const t=window.uauth.resourceRequests.get(n);return t?(t.abort(),!0):!1};window.uauth.validateAuthUrl=function(n,t=null){if(!n||typeof n!="string")throw new Error("Authentication URL is required.");const r=t?new URL(t,window.location.origin):new URL(window.location.origin),i=new URL(n,document.baseURI);if(!["http:","https:"].includes(r.protocol))throw new Error("Unsupported trusted origin protocol.");if(!["http:","https:"].includes(i.protocol))throw new Error("Unsupported authentication URL protocol.");if(r.username||r.password)throw new Error("Trusted origin cannot contain credentials.");if(i.username||i.password)throw new Error("Authentication URL cannot contain credentials.");if(i.origin!==r.origin)throw new Error("Authentication request target differs from the trusted UAuthHub origin.");return i.href};window.uauth.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 df208996..1b8fd30b 100644
--- a/src/client/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateManager.cs
+++ b/src/client/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateManager.cs
@@ -1,4 +1,7 @@
using CodeBeam.UltimateAuth.Client.Events;
+using CodeBeam.UltimateAuth.Client.Errors;
+using CodeBeam.UltimateAuth.Client.Infrastructure;
+using System.Net;
using CodeBeam.UltimateAuth.Core.Abstractions;
using CodeBeam.UltimateAuth.Users.Contracts;
@@ -7,6 +10,7 @@ namespace CodeBeam.UltimateAuth.Client.Authentication;
internal sealed class UAuthStateManager : IUAuthStateManager, IDisposable
{
private Task? _ensureTask;
+ private DateTimeOffset _validationNotBefore;
private readonly object _ensureLock = new();
private readonly IUAuthClient _client;
@@ -26,6 +30,8 @@ public UAuthStateManager(IUAuthClient client, IUAuthClientEvents events, IClock
public Task EnsureAsync(bool force = false, CancellationToken ct = default)
{
+ ct.ThrowIfCancellationRequested();
+ if (_clock.UtcNow < _validationNotBefore) return Task.CompletedTask;
if (!force && State.IsAuthenticated && !State.IsStale)
return Task.CompletedTask;
@@ -59,6 +65,10 @@ private async Task EnsureInternalAsync(CancellationToken ct)
State.ApplySnapshot(result.Snapshot, _clock.UtcNow);
}
+ catch (UAuthTransportException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests)
+ {
+ _validationNotBefore = UAuthRateLimit.NotBefore(_clock.UtcNow, ex.RetryAfter);
+ }
finally
{
lock (_ensureLock)
diff --git a/src/client/CodeBeam.UltimateAuth.Client/Contracts/RefreshResult.cs b/src/client/CodeBeam.UltimateAuth.Client/Contracts/RefreshResult.cs
index 82206fe2..dfca87be 100644
--- a/src/client/CodeBeam.UltimateAuth.Client/Contracts/RefreshResult.cs
+++ b/src/client/CodeBeam.UltimateAuth.Client/Contracts/RefreshResult.cs
@@ -7,6 +7,8 @@ namespace CodeBeam.UltimateAuth.Client.Contracts;
///
public sealed record RefreshResult
{
+ public string? ErrorCode { get; init; }
+ public TimeSpan? RetryAfter { get; init; }
///
/// Gets a value indicating whether the refresh operation completed successfully.
///
diff --git a/src/client/CodeBeam.UltimateAuth.Client/Contracts/UAuthResourceTransportResult.cs b/src/client/CodeBeam.UltimateAuth.Client/Contracts/UAuthResourceTransportResult.cs
index 73defa21..b6fbfb99 100644
--- a/src/client/CodeBeam.UltimateAuth.Client/Contracts/UAuthResourceTransportResult.cs
+++ b/src/client/CodeBeam.UltimateAuth.Client/Contracts/UAuthResourceTransportResult.cs
@@ -10,6 +10,10 @@ public sealed class UAuthResourceTransportResult
[JsonPropertyName("ok")]
public bool Ok { get; init; }
+ /// Raw Retry-After header, when exposed by the server/CORS policy.
+ [JsonPropertyName("retryAfter")]
+ public string? RetryAfter { get; init; }
+
[JsonPropertyName("status")]
public int Status { get; init; }
diff --git a/src/client/CodeBeam.UltimateAuth.Client/Contracts/UAuthTransportResult.cs b/src/client/CodeBeam.UltimateAuth.Client/Contracts/UAuthTransportResult.cs
index 0e87ba67..4381fd98 100644
--- a/src/client/CodeBeam.UltimateAuth.Client/Contracts/UAuthTransportResult.cs
+++ b/src/client/CodeBeam.UltimateAuth.Client/Contracts/UAuthTransportResult.cs
@@ -18,6 +18,10 @@ public sealed class UAuthTransportResult
[JsonPropertyName("ok")]
public bool Ok { get; init; }
+ /// Raw Retry-After header, when exposed by the server/CORS policy.
+ [JsonPropertyName("retryAfter")]
+ public string? RetryAfter { get; init; }
+
///
/// Gets the HTTP status code returned by the request.
///
diff --git a/src/client/CodeBeam.UltimateAuth.Client/Errors/UAuthTransportException.cs b/src/client/CodeBeam.UltimateAuth.Client/Errors/UAuthTransportException.cs
index b41c0f6a..2c123769 100644
--- a/src/client/CodeBeam.UltimateAuth.Client/Errors/UAuthTransportException.cs
+++ b/src/client/CodeBeam.UltimateAuth.Client/Errors/UAuthTransportException.cs
@@ -5,6 +5,7 @@ namespace CodeBeam.UltimateAuth.Client.Errors;
public sealed class UAuthTransportException : UAuthClientException
{
public HttpStatusCode? StatusCode { get; }
+ public TimeSpan? RetryAfter { get; init; }
public UAuthTransportException(string message) : base("transport_error", message)
{
@@ -14,12 +15,12 @@ public UAuthTransportException(string message, Exception? inner) : base("transpo
{
}
- public UAuthTransportException(string message, HttpStatusCode statusCode) : base("transport_error", message)
+ public UAuthTransportException(string message, HttpStatusCode statusCode) : base(statusCode == HttpStatusCode.TooManyRequests ? "rate_limited" : "transport_error", message)
{
StatusCode = statusCode;
}
- public UAuthTransportException(string message, HttpStatusCode statusCode, Exception? inner) : base("transport_error", message, inner)
+ public UAuthTransportException(string message, HttpStatusCode statusCode, Exception? inner) : base(statusCode == HttpStatusCode.TooManyRequests ? "rate_limited" : "transport_error", message, inner)
{
StatusCode = statusCode;
}
diff --git a/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/IUAuthRequestClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/IUAuthRequestClient.cs
index b1c7d47b..521adeae 100644
--- a/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/IUAuthRequestClient.cs
+++ b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/IUAuthRequestClient.cs
@@ -28,6 +28,10 @@ public interface IUAuthRequestClient
Task SendFormAsync(string endpoint, IDictionary? form = null, CancellationToken ct = default);
+ /// Observes logout before following successful browser redirects. Existing adapters may delegate to their form transport.
+ Task SendFormAsync(string endpoint, IDictionary? form, bool navigateOnSuccess, CancellationToken ct = default)
+ => SendFormAsync(endpoint, form, ct);
+
///
/// Sends a JSON payload to the specified endpoint and returns the result of the operation.
///
diff --git a/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthRateLimit.cs b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthRateLimit.cs
new file mode 100644
index 00000000..32923b4e
--- /dev/null
+++ b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthRateLimit.cs
@@ -0,0 +1,34 @@
+using System.Globalization;
+using System.Net;
+using CodeBeam.UltimateAuth.Client.Contracts;
+using CodeBeam.UltimateAuth.Client.Errors;
+using CodeBeam.UltimateAuth.Core.Contracts;
+
+namespace CodeBeam.UltimateAuth.Client.Infrastructure;
+
+internal static class UAuthRateLimit
+{
+ internal static TimeSpan? ParseRetryAfter(string? header, DateTimeOffset now)
+ {
+ if (string.IsNullOrWhiteSpace(header)) return null;
+ header = header.Trim();
+ if (long.TryParse(header, NumberStyles.None, CultureInfo.InvariantCulture, out var seconds))
+ return seconds <= TimeSpan.MaxValue.Ticks / TimeSpan.TicksPerSecond ? TimeSpan.FromTicks(seconds * TimeSpan.TicksPerSecond) : null;
+ if (DateTimeOffset.TryParseExact(header, "r", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var date))
+ return date > now ? date - now : TimeSpan.Zero;
+ return null;
+ }
+
+ internal static DateTimeOffset NotBefore(DateTimeOffset now, TimeSpan? retryAfter)
+ {
+ var delay = retryAfter ?? TimeSpan.FromSeconds(30);
+ if (delay < TimeSpan.FromSeconds(1)) delay = TimeSpan.FromSeconds(1);
+ return delay.Ticks > DateTimeOffset.MaxValue.Ticks - now.UtcTicks ? DateTimeOffset.MaxValue : now.Add(delay);
+ }
+
+ internal static UAuthTransportException Exception(string? header) => new("Temporarily rate limited.", HttpStatusCode.TooManyRequests)
+ { RetryAfter = ParseRetryAfter(header, DateTimeOffset.UtcNow) };
+
+ internal static UAuthProblem Problem(string? header) => new()
+ { Status = 429, Title = "Too many requests", Detail = "rate_limited", RetryAfter = ParseRetryAfter(header, DateTimeOffset.UtcNow) };
+}
diff --git a/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthResultMapper.cs b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthResultMapper.cs
index 17bc8eef..0ca40000 100644
--- a/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthResultMapper.cs
+++ b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthResultMapper.cs
@@ -94,6 +94,7 @@ private static void EnsureTransport(UAuthTransportResult raw)
private static UAuthProblem? CreateProblem(UAuthTransportResult raw)
{
+ if (raw.Status == 429) return UAuthRateLimit.Problem(raw.RetryAfter);
var parsed = TryDeserializeProblem(raw);
if (parsed is null)
diff --git a/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthFlowClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthFlowClient.cs
index 41ede98f..d61535df 100644
--- a/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthFlowClient.cs
+++ b/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthFlowClient.cs
@@ -27,6 +27,8 @@ internal class UAuthFlowClient : IFlowClient
private readonly IReturnUrlProvider _returnUrlProvider;
private readonly UAuthClientOptions _options;
private readonly UAuthClientDiagnostics _diagnostics;
+ private DateTimeOffset _refreshNotBefore;
+ private RefreshResult? _refreshThrottle;
public UAuthFlowClient(IUAuthRequestClient post, IUAuthClientEvents events, IClientDeviceProvider clientDeviceProvider, IReturnUrlProvider returnUrlProvider, IOptions options, UAuthClientDiagnostics diagnostics)
{
@@ -65,6 +67,7 @@ public async Task TryLoginAsync(LoginRequest request, UAuthSubmi
{
var raw = await _post.SendJsonAsync(tryUrl, payload);
+ if (raw.Status == 429) return new TryLoginResult { Status = 429, ErrorCode = "rate_limited", RetryAfter = UAuthRateLimit.ParseRetryAfter(raw.RetryAfter, DateTimeOffset.UtcNow) };
EnsureValidHttpStatus(raw);
if (raw.Status is >= 400 and < 500)
@@ -102,7 +105,10 @@ public async Task TryLoginAsync(LoginRequest request, UAuthSubmi
case UAuthSubmitMode.TryAndCommit:
default:
{
- var result = await _post.TryAndCommitAsync(tryUrl, commitUrl, payload);
+ TryLoginResult result;
+ try { result = await _post.TryAndCommitAsync(tryUrl, commitUrl, payload); }
+ catch (UAuthTransportException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests)
+ { return new TryLoginResult { Status = 429, ErrorCode = "rate_limited", RetryAfter = ex.RetryAfter }; }
if (result is null)
throw new UAuthProtocolException("Invalid try-login result.");
@@ -115,7 +121,11 @@ public async Task TryLoginAsync(LoginRequest request, UAuthSubmi
public async Task LogoutAsync()
{
var url = Url(_options.Endpoints.Logout);
- await _post.NavigateAsync(url);
+ var raw = await _post.SendFormAsync(url, null, navigateOnSuccess: true);
+ EnsureValidHttpStatus(raw);
+ if (raw.Status is < 200 or >= 300)
+ throw new UAuthTransportException("Server logout did not succeed.", (HttpStatusCode)raw.Status);
+ await _events.PublishAsync(new UAuthStateEventArgsEmpty(UAuthStateEvent.LogoutVariant, _options.StateEvents.HandlingMode));
}
@@ -126,7 +136,24 @@ public async Task RefreshAsync(bool isAuto = false)
_diagnostics.MarkManualRefresh();
}
+ var now = DateTimeOffset.UtcNow;
+ if (_refreshThrottle is not null && now < _refreshNotBefore)
+ return _refreshThrottle with { RetryAfter = _refreshNotBefore - now };
var raw = await _post.SendFormAsync(Url(_options.Endpoints.Refresh));
+ if (raw.Status == 429)
+ {
+ _refreshThrottle = new RefreshResult
+ {
+ Status = 429,
+ IsSuccess = false,
+ Outcome = RefreshOutcome.None,
+ ErrorCode = "rate_limited",
+ RetryAfter = UAuthRateLimit.ParseRetryAfter(raw.RetryAfter, DateTimeOffset.UtcNow)
+ };
+ _refreshNotBefore = UAuthRateLimit.NotBefore(DateTimeOffset.UtcNow, _refreshThrottle.RetryAfter);
+ return _refreshThrottle;
+ }
+ _refreshThrottle = null;
// 401 is a normal authentication outcome for refresh.
if (raw.Status == 401)
@@ -197,6 +224,7 @@ public async Task ValidateAsync()
var url = Url(_options.Endpoints.Validate);
var raw = await _post.SendFormAsync(url);
+ if (raw.Status == 429) throw UAuthRateLimit.Exception(raw.RetryAfter);
if (raw.Status == 0)
throw new UAuthTransportException("Network error during validation.");
@@ -335,6 +363,7 @@ public async Task TryCompletePkceLoginAsync(PkceCompleteRequ
case UAuthSubmitMode.TryOnly:
{
var raw = await _post.SendJsonAsync(tryUrl, request);
+ if (raw.Status == 429) return new TryPkceLoginResult { Status = 429, ErrorCode = "rate_limited", RetryAfter = UAuthRateLimit.ParseRetryAfter(raw.RetryAfter, DateTimeOffset.UtcNow) };
EnsureValidHttpStatus(raw);
@@ -367,7 +396,10 @@ public async Task TryCompletePkceLoginAsync(PkceCompleteRequ
case UAuthSubmitMode.TryAndCommit:
default:
{
- var result = await _post.TryAndCommitAsync(tryUrl, commitUrl, payload);
+ TryPkceLoginResult result;
+ try { result = await _post.TryAndCommitAsync(tryUrl, commitUrl, payload); }
+ catch (UAuthTransportException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests)
+ { return new TryPkceLoginResult { Status = 429, ErrorCode = "rate_limited", RetryAfter = ex.RetryAfter }; }
if (result is null)
throw new UAuthProtocolException("Invalid PKCE try result.");
@@ -530,6 +562,7 @@ private static void EnsureValidHttpStatus(UAuthTransportResult raw)
if (raw is null)
throw new UAuthProtocolException("Transport client returned a null response.");
+ if (raw.Status == 429) throw UAuthRateLimit.Exception(raw.RetryAfter);
if (raw.Status == 0)
throw new UAuthTransportException("Network error.");
diff --git a/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthResourceClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthResourceClient.cs
index 5e9eb4c1..a9998c63 100644
--- a/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthResourceClient.cs
+++ b/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthResourceClient.cs
@@ -34,9 +34,9 @@ public async Task DeleteAsync(string endpoint, CancellationToken ct
return new UAuthResult
{
- IsSuccess = response.Ok,
+ IsSuccess = (response.Ok && response.Status is >= 200 and < 300),
Status = response.Status,
- Problem = response.Ok ? null : ReadProblem(response)
+ Problem = (response.Ok && response.Status is >= 200 and < 300) ? null : ReadProblem(response)
};
}
@@ -46,10 +46,10 @@ private async Task> SendAsync(string method, string endpoint,
return new UAuthResult
{
- IsSuccess = response.Ok,
+ IsSuccess = (response.Ok && response.Status is >= 200 and < 300),
Status = response.Status,
- Value = response.Ok ? ReadValue(response) : default,
- Problem = response.Ok ? null : ReadProblem(response)
+ Value = (response.Ok && response.Status is >= 200 and < 300) ? ReadValue(response) : default,
+ Problem = (response.Ok && response.Status is >= 200 and < 300) ? null : ReadProblem(response)
};
}
@@ -63,6 +63,7 @@ private async Task> SendAsync(string method, string endpoint,
private static UAuthProblem? ReadProblem(UAuthResourceTransportResult response)
{
+ if (response.Status == 429) return UAuthRateLimit.Problem(response.RetryAfter);
if (string.IsNullOrWhiteSpace(response.Body))
return null;
diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/ClientRateLimitingIntegrationTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/ClientRateLimitingIntegrationTests.cs
new file mode 100644
index 00000000..3baccf5c
--- /dev/null
+++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/ClientRateLimitingIntegrationTests.cs
@@ -0,0 +1,139 @@
+using System.Net;
+using System.Net.Http.Json;
+using System.Text.Json;
+using CodeBeam.UltimateAuth.Client;
+using CodeBeam.UltimateAuth.Client.Abstractions;
+using CodeBeam.UltimateAuth.Client.Contracts;
+using CodeBeam.UltimateAuth.Client.Diagnostics;
+using CodeBeam.UltimateAuth.Client.Extensions;
+using CodeBeam.UltimateAuth.Client.Infrastructure;
+using CodeBeam.UltimateAuth.Core.Abstractions;
+using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Core.MultiTenancy;
+using CodeBeam.UltimateAuth.Core.Options;
+using CodeBeam.UltimateAuth.Server.RateLimiting;
+using CodeBeam.UltimateAuth.Tests.Integration.Infrastructure;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace CodeBeam.UltimateAuth.Tests.Integration;
+
+public sealed class ClientRateLimitingIntegrationTests
+{
+ [Fact]
+ public async Task NativeValidation429PreservesClientState_AndLaterRevocationStillClearsIt()
+ {
+ using var factory = AuthServerFactory.Create(o =>
+ { o.RateLimiting.Enabled = true; o.RateLimiting.Endpoints[UAuthEndpoint.Validate] = new() { PermitLimit = 1 }; });
+ var user = await factory.CreateLoginUserAsync();
+ using var http = await Login(factory, user);
+ var transport = new HttpTransport(http);
+ using var services = ClientServices(factory, transport);
+ using var scope = services.CreateScope();
+ var manager = scope.ServiceProvider.GetRequiredService();
+ await manager.EnsureAsync();
+ Assert.True(manager.State.IsAuthenticated);
+ var validatedAt = manager.State.LastValidatedAt;
+ await manager.EnsureAsync(force: true);
+ await manager.EnsureAsync(force: true);
+ Assert.True(manager.State.IsAuthenticated);
+ Assert.Equal(validatedAt, manager.State.LastValidatedAt);
+ Assert.Equal(2, transport.Count);
+ using (var serverScope = factory.Services.CreateScope())
+ {
+ var store = serverScope.ServiceProvider.GetRequiredService().Create(TenantKey.Single);
+ await store.ExecuteAsync(ct => store.RevokeRootCascadeAsync(user.UserKey, factory.Clock.UtcNow, ct));
+ }
+ factory.Clock.Advance(TimeSpan.FromMinutes(2));
+ await manager.EnsureAsync(force: true);
+ Assert.False(manager.State.IsAuthenticated);
+ }
+
+ [Fact]
+ public async Task NativeRefresh429PreservesCredentials_AndSuppressesImmediateRefreshLoop()
+ {
+ using var factory = AuthServerFactory.Create(o =>
+ { o.RateLimiting.Enabled = true; o.RateLimiting.Endpoints[UAuthEndpoint.Refresh] = new() { PermitLimit = 1 }; });
+ var user = await factory.CreateLoginUserAsync();
+ using var http = await Login(factory, user);
+ var credential = http.DefaultRequestHeaders.GetValues("Cookie").Single();
+ var transport = new HttpTransport(http);
+ using var services = ClientServices(factory, transport);
+ using var scope = services.CreateScope();
+ var flow = scope.ServiceProvider.GetRequiredService().Flows;
+ Assert.True((await flow.RefreshAsync()).IsSuccess);
+ var throttle = await flow.RefreshAsync();
+ Assert.Equal(429, throttle.Status);
+ Assert.Equal("rate_limited", throttle.ErrorCode);
+ Assert.Equal(RefreshOutcome.None, throttle.Outcome);
+ Assert.Equal(credential, http.DefaultRequestHeaders.GetValues("Cookie").Single());
+ await flow.RefreshAsync(isAuto: true);
+ Assert.Equal(2, transport.Count);
+ var session = await http.PostAsync("/auth/validate", null);
+ Assert.Equal(HttpStatusCode.OK, session.StatusCode);
+ Assert.True((await session.Content.ReadFromJsonAsync())!.IsValid);
+ }
+
+ private static async Task Login(AuthServerFactory factory, IntegrationTestUser user)
+ {
+ var http = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false, HandleCookies = false });
+ http.DefaultRequestHeaders.Add("Origin", "https://localhost:6130");
+ http.DefaultRequestHeaders.Add("X-UDID", "client-rate-device-123456789");
+ http.DefaultRequestHeaders.Add("X-UAuth-ClientProfile", "BlazorServer");
+ var login = await http.PostAsJsonAsync("/auth/login", new { identifier = user.Identifier, secret = user.Secret });
+ Assert.True(login.Headers.TryGetValues("Set-Cookie", out var cookies));
+ http.DefaultRequestHeaders.Add("Cookie", string.Join("; ", cookies!.Select(c => c.Split(';')[0])));
+ return http;
+ }
+
+ private static ServiceProvider ClientServices(AuthServerFactory factory, HttpTransport transport)
+ {
+ var services = new ServiceCollection();
+ services.AddLogging();
+ services.AddSingleton(new ConfigurationBuilder().Build());
+ services.AddUltimateAuthClient(o => { o.ClientProfile = UAuthClientProfile.BlazorServer; o.AutoDetectClientProfile = false; });
+ services.AddSingleton();
+ services.AddSingleton(factory.Clock);
+ services.AddSingleton(transport);
+ services.AddSingleton(transport);
+ services.AddSingleton(transport);
+ services.AddSingleton(transport);
+ return services.BuildServiceProvider();
+ }
+
+ // Thin HTTP test adapter: public SDK contracts against the real auth host and native middleware.
+ private sealed class HttpTransport(HttpClient http) : IUAuthRequestClient, IUAuthResourceRequestClient, IClientDeviceProvider, IReturnUrlProvider
+ {
+ internal int Count { get; private set; }
+ public Task NavigateAsync(string endpoint, IDictionary? form = null, CancellationToken ct = default) => throw new NotSupportedException();
+ public Task TryAndCommitAsync(string tryEndpoint, string commitEndpoint, object request, CancellationToken ct = default) => throw new NotSupportedException();
+ public Task GetAsync() => Task.FromResult(DeviceContext.Anonymous());
+ public string GetCurrentUrl() => "/";
+ public async Task SendFormAsync(string endpoint, IDictionary? form = null, CancellationToken ct = default)
+ {
+ Count++;
+ using var response = await http.PostAsync(endpoint, form is null ? null : new FormUrlEncodedContent(form), ct);
+ return await Read(response, ct);
+ }
+ public async Task SendJsonAsync(string endpoint, object? payload = null, CancellationToken ct = default)
+ {
+ Count++;
+ using var response = await http.PostAsJsonAsync(endpoint, payload, ct);
+ return await Read(response, ct);
+ }
+ private static async Task Read(HttpResponseMessage response, CancellationToken ct)
+ {
+ var body = await response.Content.ReadAsStringAsync(ct);
+ return new()
+ {
+ Status = (int)response.StatusCode,
+ Ok = response.IsSuccessStatusCode,
+ RetryAfter = response.Headers.TryGetValues("Retry-After", out var retry) ? retry.First() : null,
+ RefreshOutcome = response.Headers.TryGetValues("X-UAuth-Refresh", out var outcome) ? outcome.First() : null,
+ Body = string.IsNullOrWhiteSpace(body) ? null : JsonSerializer.Deserialize(body)
+ };
+ }
+ public Task SendAsync(string method, string endpoint, object? payload = null, CancellationToken ct = default) => throw new NotSupportedException();
+ }
+}
diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/Infrastructure/AuthServerFactory.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/Infrastructure/AuthServerFactory.cs
index 025b4dec..a07c17cd 100644
--- a/tests/CodeBeam.UltimateAuth.Tests.Integration/Infrastructure/AuthServerFactory.cs
+++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/Infrastructure/AuthServerFactory.cs
@@ -61,6 +61,9 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
services.RemoveAll();
services.AddSingleton(Clock);
+ // Business-flow tests share a host; limiter contracts use isolated hosts.
+ services.PostConfigure(options => options.RateLimiting.Enabled = false);
+
if (_configureServer is not null)
{
services.PostConfigure(options => _configureServer(options));
diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingFlowTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingFlowTests.cs
new file mode 100644
index 00000000..d5883975
--- /dev/null
+++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingFlowTests.cs
@@ -0,0 +1,63 @@
+using System.Net;
+using System.Net.Http.Json;
+using CodeBeam.UltimateAuth.Core.Abstractions;
+using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Core.MultiTenancy;
+using CodeBeam.UltimateAuth.Server.RateLimiting;
+using CodeBeam.UltimateAuth.Tests.Integration.Infrastructure;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace CodeBeam.UltimateAuth.Tests.Integration;
+
+public class RateLimitingFlowTests
+{
+ [Fact]
+ public async Task RejectedLoginDoesNotLockAccount_AndRejectedLogoutDoesNotRevokeSession()
+ {
+ using var factory = AuthServerFactory.Create(o =>
+ {
+ o.RateLimiting.Enabled = true;
+ o.RateLimiting.Login.PermitLimit = 2;
+ o.RateLimiting.Endpoints[UAuthEndpoint.Logout] = new() { PermitLimit = 1 };
+ o.Login.MaxFailedAttempts = 1;
+ });
+ var user = await factory.CreateLoginUserAsync();
+ HttpClient Client(string device)
+ {
+ var client = factory.CreateClient(new WebApplicationFactoryClientOptions
+ { AllowAutoRedirect = false, HandleCookies = false });
+ client.DefaultRequestHeaders.Add("Origin", "https://localhost:6130");
+ client.DefaultRequestHeaders.Add("X-UDID", device);
+ return client;
+ }
+ using var first = Client("rate-limit-flow-first-123456");
+ using var second = Client("rate-limit-flow-second-123456");
+ foreach (var client in new[] { first, second })
+ {
+ var login = await client.PostAsJsonAsync("/auth/login", new { identifier = user.Identifier, secret = user.Secret });
+ Assert.True(login.Headers.TryGetValues("Set-Cookie", out var cookies));
+ client.DefaultRequestHeaders.Add("Cookie", cookies!.First());
+ }
+ var rejected = await first.PostAsJsonAsync("/auth/login", new { identifier = user.Identifier, secret = "wrong" });
+ Assert.Equal(HttpStatusCode.TooManyRequests, rejected.StatusCode);
+ Assert.False(rejected.Headers.Contains("Set-Cookie"));
+ using (var scope = factory.Services.CreateScope())
+ {
+ var security = scope.ServiceProvider.GetRequiredService();
+ var factor = await security.GetOrCreateFactorAsync(TenantKey.Single, user.UserKey, CredentialType.Password);
+ Assert.Equal(0, factor.FailedAttempts);
+ }
+ Assert.NotEqual(HttpStatusCode.TooManyRequests, (await first.PostAsync("/auth/logout", null)).StatusCode);
+ // Two validated sessions of the same actor share logout capacity.
+ var logout = await second.PostAsync("/auth/logout", null);
+ Assert.Equal(HttpStatusCode.TooManyRequests, logout.StatusCode);
+ Assert.False(logout.Headers.Contains("Set-Cookie"));
+ // The rejected logout did not revoke the second session.
+ var validation = await second.PostAsync("/auth/validate", null);
+ Assert.Equal(HttpStatusCode.OK, validation.StatusCode);
+ var result = await validation.Content.ReadFromJsonAsync();
+ Assert.True(result?.IsValid);
+ Assert.NotNull(result!.Snapshot);
+ }
+}
diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingProofFlowTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingProofFlowTests.cs
new file mode 100644
index 00000000..2b9910fd
--- /dev/null
+++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingProofFlowTests.cs
@@ -0,0 +1,164 @@
+using System.Net;
+using System.Net.Http.Json;
+using System.Security.Cryptography;
+using System.Text;
+using CodeBeam.UltimateAuth.Core.Abstractions;
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Core.MultiTenancy;
+using CodeBeam.UltimateAuth.Credentials;
+using CodeBeam.UltimateAuth.Credentials.Contracts;
+using CodeBeam.UltimateAuth.Server.Options;
+using CodeBeam.UltimateAuth.Server.RateLimiting;
+using CodeBeam.UltimateAuth.Tests.Integration.Infrastructure;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace CodeBeam.UltimateAuth.Tests.Integration;
+
+public sealed class RateLimitingProofFlowTests
+{
+ private static readonly TimeSpan Window = TimeSpan.FromSeconds(2);
+
+ [Fact]
+ public async Task PkceCompletion_RejectedRequestPreservesAuthorizationProof()
+ {
+ using var factory = AuthServerFactory.Create(o =>
+ { o.RateLimiting.Enabled = true; o.RateLimiting.Login = new() { PermitLimit = 1, Window = Window }; });
+ var user = await factory.CreateLoginUserAsync();
+ using var client = Client(factory);
+ static string Encode(byte[] value) => Convert.ToBase64String(value).TrimEnd('=').Replace('+', '-').Replace('/', '_');
+ var verifier = Encode(RandomNumberGenerator.GetBytes(32));
+ var authorize = await client.PostAsync("/auth/pkce/authorize", new FormUrlEncodedContent(new Dictionary
+ { ["code_challenge"] = Encode(SHA256.HashData(Encoding.ASCII.GetBytes(verifier))), ["challenge_method"] = "S256", ["redirect_uri"] = "/callback" }));
+ Assert.Equal(HttpStatusCode.OK, authorize.StatusCode);
+ var proof = await authorize.Content.ReadFromJsonAsync();
+ Assert.NotNull(proof);
+ // Malformed payload consumes request quota without supplying the valid authorization proof.
+ Assert.Equal(HttpStatusCode.BadRequest, (await client.PostAsync("/auth/pkce/complete", new FormUrlEncodedContent(new Dictionary()))).StatusCode);
+ Task Complete() => client.PostAsync("/auth/pkce/complete", new FormUrlEncodedContent(new Dictionary
+ {
+ ["authorization_code"] = proof.AuthorizationCode,
+ ["code_verifier"] = verifier,
+ ["Identifier"] = user.Identifier,
+ ["Secret"] = user.Secret,
+ ["return_url"] = "/callback"
+ }));
+ var rejected = await Complete();
+ Assert.Equal(HttpStatusCode.TooManyRequests, rejected.StatusCode);
+ Assert.False(rejected.Headers.Contains("Set-Cookie"));
+ var completed = await AfterReplenishment(Complete);
+ Assert.NotEqual(HttpStatusCode.TooManyRequests, completed.StatusCode);
+ Assert.True(completed.Headers.Contains("Set-Cookie"));
+ }
+
+ [Fact]
+ public async Task Refresh_RejectedRequestPreservesRefreshToken_ThenSameTokenRotates()
+ {
+ using var factory = AuthServerFactory.Create(o =>
+ { o.RateLimiting.Enabled = true; o.RateLimiting.Endpoints[UAuthEndpoint.Refresh] = new() { PermitLimit = 1, Window = Window }; });
+ var user = await factory.CreateLoginUserAsync();
+ using var client = Client(factory);
+ client.DefaultRequestHeaders.Add("X-UAuth-ClientProfile", "BlazorWasm");
+ var login = await client.PostAsJsonAsync("/auth/login", new { identifier = user.Identifier, secret = user.Secret });
+ var cookies = Cookies(login);
+ var session = cookies.Split("; ").Single(c => c.StartsWith("uas="));
+ var refresh = cookies.Split("; ").Single(c => c.StartsWith("uar="))[4..];
+ using var scope = factory.Services.CreateScope();
+ var store = scope.ServiceProvider.GetRequiredService().Create(TenantKey.Single);
+ var hash = scope.ServiceProvider.GetRequiredService().Hash(refresh);
+ var before = await store.FindByHashAsync(hash);
+ Assert.NotNull(before);
+ client.DefaultRequestHeaders.Add("Cookie", session);
+ Assert.Equal(HttpStatusCode.Unauthorized, (await client.PostAsync("/auth/refresh", null)).StatusCode);
+ client.DefaultRequestHeaders.Remove("Cookie");
+ client.DefaultRequestHeaders.Add("Cookie", cookies);
+ var rejected = await client.PostAsync("/auth/refresh", null);
+ Assert.Equal(HttpStatusCode.TooManyRequests, rejected.StatusCode);
+ Assert.False(rejected.Headers.Contains("Set-Cookie"));
+ Assert.Equal(before, await store.FindByHashAsync(hash));
+ var renewed = await AfterReplenishment(() => client.PostAsync("/auth/refresh", null));
+ Assert.Equal(HttpStatusCode.NoContent, renewed.StatusCode);
+ Assert.True(renewed.Headers.Contains("Set-Cookie"));
+ Assert.False((await store.FindByHashAsync(hash))!.IsActive(factory.Clock.UtcNow));
+ }
+
+ [Fact]
+ public async Task Recovery_RejectedRequestPreservesResetProof_ThenSameProofChangesPassword()
+ {
+ var notifier = new RecordingResetNotifier();
+ using var factory = AuthServerFactory.Create(o =>
+ { o.RateLimiting.Enabled = true; o.RateLimiting.Endpoints[UAuthEndpoint.CredentialResetCompleteAnonymous] = new() { PermitLimit = 1, Window = Window }; }, services =>
+ { services.RemoveAll(); services.AddSingleton(notifier); });
+ var user = await factory.CreateLoginUserAsync();
+ using var client = Client(factory);
+ var begin = await client.PostAsJsonAsync("/auth/me/credentials/reset/begin", new BeginResetCredentialRequest
+ { Identifier = user.Identifier, CredentialType = CredentialType.Password, ResetCodeType = ResetCodeType.Token });
+ Assert.Equal(HttpStatusCode.OK, begin.StatusCode);
+ Assert.NotNull(notifier.Last);
+ // Consume quota using an unknown account, never presenting the issued proof.
+ await client.PostAsJsonAsync("/auth/me/credentials/reset/complete", new CompleteResetCredentialRequest
+ { Identifier = "unknown-account", CredentialType = CredentialType.Password, ResetToken = "invalid", NewSecret = "Changed-password-123!" });
+ var request = new CompleteResetCredentialRequest
+ { Identifier = user.Identifier, CredentialType = CredentialType.Password, ResetToken = notifier.Last.Token, NewSecret = "Changed-password-123!" };
+ Assert.Equal(HttpStatusCode.TooManyRequests, (await client.PostAsJsonAsync("/auth/me/credentials/reset/complete", request)).StatusCode);
+ var completed = await AfterReplenishment(() => client.PostAsJsonAsync("/auth/me/credentials/reset/complete", request));
+ Assert.Equal(HttpStatusCode.OK, completed.StatusCode);
+ var login = await client.PostAsJsonAsync("/auth/login", new { identifier = user.Identifier, secret = request.NewSecret });
+ Assert.True(login.Headers.Contains("Set-Cookie"));
+ }
+
+ [Fact]
+ public async Task AuthenticatedUsersOnSameConnectionIpHaveIndependentValidationBudgets()
+ {
+ using var factory = AuthServerFactory.Create(o =>
+ { o.RateLimiting.Enabled = true; o.RateLimiting.Endpoints[UAuthEndpoint.Validate] = new() { PermitLimit = 1 }; });
+ using var first = Client(factory);
+ using var second = Client(factory);
+ foreach (var client in new[] { first, second })
+ {
+ var user = await factory.CreateLoginUserAsync();
+ var login = await client.PostAsJsonAsync("/auth/login", new { identifier = user.Identifier, secret = user.Secret });
+ client.DefaultRequestHeaders.Add("Cookie", Cookies(login));
+ var validation = await client.PostAsync("/auth/validate", null);
+ Assert.Equal(HttpStatusCode.OK, validation.StatusCode);
+ Assert.True((await validation.Content.ReadFromJsonAsync())!.IsValid);
+ Assert.Equal(HttpStatusCode.TooManyRequests, (await client.PostAsync("/auth/validate", null)).StatusCode);
+ }
+ }
+
+ private static async Task AfterReplenishment(Func> send)
+ {
+ // Native limiter time is independent of the authentication test clock. Its heartbeat
+ // can replenish segments later than the nominal window, so wait on observable behavior.
+ var elapsed = System.Diagnostics.Stopwatch.StartNew();
+ while (true)
+ {
+ await Task.Delay(100);
+ var response = await send();
+ if (response.StatusCode != HttpStatusCode.TooManyRequests || elapsed.Elapsed > TimeSpan.FromSeconds(10))
+ return response;
+ response.Dispose();
+ }
+ }
+
+ private static HttpClient Client(AuthServerFactory factory)
+ {
+ var client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false, HandleCookies = false });
+ client.DefaultRequestHeaders.Add("Origin", "https://localhost:6130");
+ client.DefaultRequestHeaders.Add("X-UDID", "rate-proof-device-1234567890");
+ return client;
+ }
+ private static string Cookies(HttpResponseMessage response)
+ {
+ Assert.True(response.Headers.TryGetValues("Set-Cookie", out var cookies));
+ return string.Join("; ", cookies!.Select(c => c.Split(';')[0]));
+ }
+ private sealed class RecordingResetNotifier : ICredentialResetNotifier
+ {
+ internal CredentialResetNotification? Last { get; private set; }
+ public Task NotifyAsync(CredentialResetNotification notification, CancellationToken ct = default)
+ { Last = notification; return Task.CompletedTask; }
+ }
+}
diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingTests.cs
new file mode 100644
index 00000000..d1a92bee
--- /dev/null
+++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingTests.cs
@@ -0,0 +1,389 @@
+using System.Net;
+using System.Threading.RateLimiting;
+using CodeBeam.UltimateAuth.InMemory;
+using CodeBeam.UltimateAuth.Server.Extensions;
+using CodeBeam.UltimateAuth.Server.Options;
+using CodeBeam.UltimateAuth.Server.RateLimiting;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.RateLimiting;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace CodeBeam.UltimateAuth.Tests.Integration;
+
+public class RateLimitingTests
+{
+ [Fact]
+ public async Task LoginAliasesShareBudget_AndMaintenanceIsIndependent()
+ {
+ await using var host = await Host.Create(o => o.RateLimiting.Login.PermitLimit = 1);
+ Assert.Equal(HttpStatusCode.NoContent, (await host.Post("/auth/login")).StatusCode);
+ foreach (var route in new[] { "/auth/try-login", "/auth/pkce/complete", "/auth/pkce/try-complete" })
+ {
+ var response = await host.Post(route);
+ Assert.Equal(HttpStatusCode.TooManyRequests, response.StatusCode);
+ if (response.Headers.RetryAfter is not null)
+ Assert.True(response.Headers.RetryAfter.Delta > TimeSpan.Zero);
+ Assert.True(response.Headers.CacheControl?.NoStore);
+ Assert.Contains("rate_limited", await response.Content.ReadAsStringAsync());
+ }
+ foreach (var route in new[] { "/auth/validate", "/auth/refresh", "/auth/logout" })
+ Assert.Equal(HttpStatusCode.NoContent, (await host.Post(route)).StatusCode);
+ }
+
+ [Theory]
+ [InlineData(false, false, false)]
+ [InlineData(false, true, true)]
+ [InlineData(true, false, false)]
+ [InlineData(true, true, true)]
+ public async Task HelperOptInAndOptOut(bool aspNet, bool enabled, bool throttled)
+ {
+ await using var host = await Host.Create(o => o.RateLimiting.Login.PermitLimit = 1, aspNet, enabled);
+ await host.Post("/auth/login");
+ Assert.Equal(throttled ? HttpStatusCode.TooManyRequests : HttpStatusCode.NoContent,
+ (await host.Post("/auth/login")).StatusCode);
+ }
+
+ [Fact]
+ public async Task DefaultHelperAndRepeatedHelper_DoNotConsumeTwice()
+ {
+ await using var host = await Host.Create(o => o.RateLimiting.Login.PermitLimit = 2, repeat: true);
+ Assert.Equal(HttpStatusCode.NoContent, (await host.Post("/auth/login")).StatusCode);
+ Assert.Equal(HttpStatusCode.NoContent, (await host.Post("/auth/login")).StatusCode);
+ Assert.Equal(HttpStatusCode.TooManyRequests, (await host.Post("/auth/login")).StatusCode);
+ }
+
+ [Fact]
+ public async Task DisableAndEndpointOverride_DoNotDisableApplicationGlobalLimiter()
+ {
+ await using var host = await Host.Create(o => o.RateLimiting.Enabled = false, native: o =>
+ {
+ o.GlobalLimiter = PartitionedRateLimiter.Create(_ =>
+ RateLimitPartition.GetSlidingWindowLimiter("global", _ => new()
+ { PermitLimit = 1, Window = TimeSpan.FromMinutes(1), SegmentsPerWindow = 6 }));
+ o.RejectionStatusCode = 418;
+ });
+ await host.Post("/auth/login");
+ Assert.Equal(418, (int)(await host.Post("/auth/login")).StatusCode);
+ }
+
+ [Fact]
+ public async Task ApplicationGroupPolicyWins_AndGenericNamesDoNotCollide()
+ {
+ await using var host = await Host.Create(o => o.RateLimiting.Login.PermitLimit = 1,
+ native: o => o.AddFixedWindowLimiter("Sensitive", x =>
+ { x.PermitLimit = 3; x.Window = TimeSpan.FromMinutes(1); }), applicationPolicy: true);
+ for (var i = 0; i < 3; i++)
+ Assert.Equal(HttpStatusCode.NoContent, (await host.Post("/auth/login")).StatusCode);
+ Assert.Equal(HttpStatusCode.ServiceUnavailable, (await host.Post("/auth/login")).StatusCode);
+ }
+
+ [Fact]
+ public async Task EndpointOverrideAndAnonymousIpIsolation()
+ {
+ await using var host = await Host.Create(o => o.RateLimiting.Endpoints[UAuthEndpoint.Login] = new() { PermitLimit = 1 });
+ await host.Post("/auth/login");
+ Assert.Equal(HttpStatusCode.TooManyRequests, (await host.Post("/auth/login", forwarded: "203.0.113.9")).StatusCode);
+ Assert.Equal(HttpStatusCode.NoContent, (await host.Post("/auth/login", ip: "192.0.2.2")).StatusCode);
+ Assert.Equal(HttpStatusCode.NoContent, (await host.Post("/auth/try-login")).StatusCode);
+ }
+
+ [Fact]
+ public async Task EveryMappedEndpointHasOneClassification()
+ {
+ await using var host = await Host.Create();
+ var routes = ((IEndpointRouteBuilder)host.App).DataSources.SelectMany(s => s.Endpoints).ToArray();
+ // Reviewed route/identity/group contract, independent of the runtime classification lookup.
+ const string expected = """
+/auth/admin/authorization/roles/create|CreateRole|Standard
+/auth/admin/authorization/roles/query|QueryRoles|Standard
+/auth/admin/authorization/roles/{roleId}/delete|DeleteRole|Standard
+/auth/admin/authorization/roles/{roleId}/permissions|SetRolePermissions|Standard
+/auth/admin/authorization/roles/{roleId}/rename|RenameRole|Standard
+/auth/admin/authorization/users/{userKey}/roles/assign|AssignRole|Standard
+/auth/admin/authorization/users/{userKey}/roles/get|GetUserRoles|Standard
+/auth/admin/authorization/users/{userKey}/roles/remove|RemoveRole|Standard
+/auth/admin/users/create|UserCreateAdmin|Standard
+/auth/admin/users/get-by-keys|GetUsersByKeys|Standard
+/auth/admin/users/query|QueryUsers|Standard
+/auth/admin/users/{userKey}/credentials/add|CredentialAddAdmin|Standard
+/auth/admin/users/{userKey}/credentials/change|CredentialChangeAdmin|Standard
+/auth/admin/users/{userKey}/credentials/delete|CredentialDeleteAdmin|Standard
+/auth/admin/users/{userKey}/credentials/reset/begin|CredentialResetBeginAdmin|Sensitive
+/auth/admin/users/{userKey}/credentials/reset/complete|CredentialResetCompleteAdmin|Sensitive
+/auth/admin/users/{userKey}/credentials/revoke|CredentialRevokeAdmin|Standard
+/auth/admin/users/{userKey}/delete|UserDeleteAdmin|Standard
+/auth/admin/users/{userKey}/identifiers/add|AddUserIdentifierAdmin|Standard
+/auth/admin/users/{userKey}/identifiers/delete|DeleteUserIdentifierAdmin|Standard
+/auth/admin/users/{userKey}/identifiers/get|GetUserIdentifiers|Standard
+/auth/admin/users/{userKey}/identifiers/set-primary|SetPrimaryUserIdentifierAdmin|Standard
+/auth/admin/users/{userKey}/identifiers/unset-primary|UnsetPrimaryUserIdentifierAdmin|Standard
+/auth/admin/users/{userKey}/identifiers/update|UpdateUserIdentifierAdmin|Standard
+/auth/admin/users/{userKey}/identifiers/verify|VerifyUserIdentifierAdmin|Standard
+/auth/admin/users/{userKey}/logout-all|LogoutAllAdmin|Standard
+/auth/admin/users/{userKey}/logout-device|LogoutDeviceAdmin|Standard
+/auth/admin/users/{userKey}/logout-others|LogoutOthersAdmin|Standard
+/auth/admin/users/{userKey}/profile/create|CreateProfileAdmin|Standard
+/auth/admin/users/{userKey}/profile/delete|DeleteProfileAdmin|Standard
+/auth/admin/users/{userKey}/profile/get|ProfileGetAdmin|Standard
+/auth/admin/users/{userKey}/profile/update|ProfileUpdateAdmin|Standard
+/auth/admin/users/{userKey}/sessions/chains/{chainId}/revoke|RevokeUserChain|Standard
+/auth/admin/users/{userKey}/sessions/chains/{chainId}|GetUserChainDetail|Standard
+/auth/admin/users/{userKey}/sessions/chains|GetUserChains|Standard
+/auth/admin/users/{userKey}/sessions/revoke-all|RevokeAllChains|Standard
+/auth/admin/users/{userKey}/sessions/revoke-root|RevokeRoot|Standard
+/auth/admin/users/{userKey}/sessions/{sessionId}/revoke|RevokeUserSession|Standard
+/auth/admin/users/{userKey}/status|ChangeStatusAdmin|Standard
+/auth/login|Login|Sensitive
+/auth/logout|Logout|Frequent
+/auth/me/authorization/check|AuthorizationCheck|Frequent
+/auth/me/authorization/permissions/get|GetMyPermissions|Standard
+/auth/me/authorization/roles/get|GetMyRoles|Standard
+/auth/me/credentials/add|CredentialAddSelf|Sensitive
+/auth/me/credentials/change|CredentialChangeSelf|Sensitive
+/auth/me/credentials/reset/begin|CredentialResetBeginAnonymous|Sensitive
+/auth/me/credentials/reset/complete|CredentialResetCompleteAnonymous|Sensitive
+/auth/me/credentials/revoke|CredentialRevokeSelf|Standard
+/auth/me/delete|UserDeleteSelf|Standard
+/auth/me/identifiers/add|AddUserIdentifierSelf|Sensitive
+/auth/me/identifiers/check-availability|CheckIdentifierAvailability|Sensitive
+/auth/me/identifiers/delete|DeleteUserIdentifierSelf|Sensitive
+/auth/me/identifiers/get|GetMyIdentifiers|Standard
+/auth/me/identifiers/set-primary|SetPrimaryUserIdentifierSelf|Sensitive
+/auth/me/identifiers/unset-primary|UnsetPrimaryUserIdentifierSelf|Sensitive
+/auth/me/identifiers/update|UpdateUserIdentifierSelf|Sensitive
+/auth/me/identifiers/verify|VerifyUserIdentifierSelf|Sensitive
+/auth/me/logout-all|LogoutAllSelf|Standard
+/auth/me/logout-device|LogoutDeviceSelf|Standard
+/auth/me/logout-others|LogoutOthersSelf|Standard
+/auth/me/profile/create|CreateProfileSelf|Standard
+/auth/me/profile/delete|DeleteProfileSelf|Standard
+/auth/me/profile/get|ProfileGetSelf|Standard
+/auth/me/profile/update|ProfileUpdateSelf|Standard
+/auth/me/sessions/chains/{chainId}/revoke|RevokeMyChain|Standard
+/auth/me/sessions/chains/{chainId}|GetMyChainDetail|Standard
+/auth/me/sessions/chains|GetMyChains|Standard
+/auth/me/sessions/revoke-all|RevokeAllMyChains|Standard
+/auth/me/sessions/revoke-others|RevokeOtherChains|Standard
+/auth/me/status|ChangeStatusSelf|Standard
+/auth/pkce/authorize|PkceAuthorize|Sensitive
+/auth/pkce/complete|PkceComplete|Sensitive
+/auth/pkce/try-complete|PkceTryComplete|Sensitive
+/auth/refresh|Refresh|Frequent
+/auth/try-login|TryLogin|Sensitive
+/auth/uauthhub/entry|HubEntry|Sensitive
+/auth/users/create|UserCreateAnonymous|Sensitive
+/auth/validate|Validate|Frequent
+""";
+ var actual = routes.Cast().Select(endpoint =>
+ {
+ var metadata = Assert.Single(endpoint.Metadata, m => m.GetType().Name == "UAuthRateLimitMetadata");
+ var type = metadata.GetType();
+ Assert.Equal(new[] { "POST" }, endpoint.Metadata.GetMetadata()!.HttpMethods);
+ return $"{endpoint.RoutePattern.RawText}|{type.GetProperty("Endpoint")!.GetValue(metadata)}|{type.GetProperty("Group")!.GetValue(metadata)}";
+ }).OrderBy(x => x, StringComparer.Ordinal).ToArray();
+ Assert.Equal(expected.Split('\n').Select(x => x.Trim()).OrderBy(x => x, StringComparer.Ordinal), actual);
+ }
+
+ [Fact]
+ public async Task TrustedForwardingSeparatesClients_UnknownProxiesCannotRotateHeaders()
+ {
+ await using var host = await Host.Create(o => o.RateLimiting.Login.PermitLimit = 1, trustedProxy: true);
+ Assert.Equal(HttpStatusCode.NoContent, (await host.Post("/auth/login", forwarded: "203.0.113.1")).StatusCode);
+ Assert.Equal(HttpStatusCode.NoContent, (await host.Post("/auth/login", forwarded: "203.0.113.2")).StatusCode);
+ await host.Post("/auth/login", ip: "192.0.2.2", forwarded: "203.0.113.3");
+ Assert.Equal(HttpStatusCode.TooManyRequests,
+ (await host.Post("/auth/login", ip: "192.0.2.2", forwarded: "203.0.113.4")).StatusCode);
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task NamedApplicationPolicyUsesEffectiveMiddlewareRejection(bool explicitOptions)
+ {
+ var options = new RateLimiterOptions();
+ options.AddFixedWindowLimiter("application-login", r => { r.PermitLimit = 1; r.Window = TimeSpan.FromMinutes(1); });
+ options.OnRejected = (rejected, ct) => { rejected.HttpContext.Response.StatusCode = 418; return ValueTask.CompletedTask; };
+ await using var host = await Host.Create(o => o.RateLimiting.Login.PolicyName = "application-login",
+ native: explicitOptions ? null : o =>
+ {
+ o.AddFixedWindowLimiter("application-login", r => { r.PermitLimit = 1; r.Window = TimeSpan.FromMinutes(1); });
+ o.OnRejected = options.OnRejected;
+ }, middlewareOptions: explicitOptions ? options : null);
+ await host.Post("/auth/login");
+ Assert.Equal(418, (int)(await host.Post("/auth/login")).StatusCode);
+ }
+
+ [Fact]
+ public async Task BuiltInPolicyOwnsRejectionEvenWithExplicitMiddlewareOptions()
+ {
+ var options = new RateLimiterOptions { OnRejected = (_, _) => throw new InvalidOperationException("Global callback must not replace endpoint policy rejection.") };
+ await using var host = await Host.Create(o => o.RateLimiting.Login.PermitLimit = 1, middlewareOptions: options);
+ await host.Post("/auth/login");
+ Assert.Equal(HttpStatusCode.TooManyRequests, (await host.Post("/auth/login")).StatusCode);
+ }
+
+ [Fact]
+ public async Task ChildPipelineDoesNotInheritInstallation_AndDeduplicatesItsOwnCalls()
+ {
+ await using var host = await Host.Create(o => o.RateLimiting.Login.PermitLimit = 1);
+ var endpoint = ((IEndpointRouteBuilder)host.App).DataSources.SelectMany(s => s.Endpoints)
+ .OfType().Single(e => e.RoutePattern.RawText == "/auth/login");
+ var child = ((IApplicationBuilder)host.App).New();
+ child.UseUltimateAuth(useRateLimiting: true);
+ child.UseUltimateAuth(useRateLimiting: true);
+ child.Run(context => { context.Response.StatusCode = 204; return Task.CompletedTask; });
+ var pipeline = child.Build();
+ async Task Send()
+ {
+ using var scope = host.App.Services.CreateScope();
+ var context = new DefaultHttpContext { RequestServices = scope.ServiceProvider };
+ context.Connection.RemoteIpAddress = IPAddress.Parse("192.0.2.1");
+ context.Response.Body = new MemoryStream();
+ context.SetEndpoint(endpoint);
+ await pipeline(context);
+ return context.Response.StatusCode;
+ }
+ Assert.Equal(204, await Send());
+ Assert.Equal(429, await Send());
+ }
+
+ [Fact]
+ public void MiddlewareHelperSignaturesAndCallFormsRemainAvailable()
+ {
+ var type = typeof(UltimateAuthApplicationBuilderExtensions);
+
+ Assert.NotNull(type.GetMethod(
+ "UseUltimateAuth",
+ new[] { typeof(IApplicationBuilder) }));
+
+ Assert.NotNull(type.GetMethod(
+ "UseUltimateAuth",
+ new[] { typeof(IApplicationBuilder), typeof(bool) }));
+
+ var method = type.GetMethod(
+ "UseUltimateAuthWithAspNetCore",
+ new[]
+ {
+ typeof(IApplicationBuilder),
+ typeof(bool?),
+ typeof(bool)
+ });
+
+ Assert.NotNull(method);
+
+ var parameters = method.GetParameters();
+
+ Assert.True(parameters[1].IsOptional);
+ Assert.Null(parameters[1].DefaultValue);
+
+ Assert.True(parameters[2].IsOptional);
+ Assert.Equal(true, parameters[2].DefaultValue);
+
+ // Compile-time coverage for the supported public API.
+ static void Calls(IApplicationBuilder app)
+ {
+ app.UseUltimateAuth();
+ app.UseUltimateAuth(true);
+ app.UseUltimateAuth(useRateLimiting: false);
+
+ app.UseUltimateAuthWithAspNetCore();
+ app.UseUltimateAuthWithAspNetCore(false);
+ app.UseUltimateAuthWithAspNetCore(null);
+ app.UseUltimateAuthWithAspNetCore(enableCors: true);
+ app.UseUltimateAuthWithAspNetCore(null, false);
+ app.UseUltimateAuthWithAspNetCore(
+ enableCors: null,
+ useRateLimiting: false);
+ }
+
+ Action compiled = Calls;
+ Assert.NotNull(compiled);
+ }
+
+ [Fact]
+ public async Task NativeDisableSkipsBothGlobalAndEndpointLimits()
+ {
+ await using var host = await Host.Create(o => o.RateLimiting.Login.PermitLimit = 1,
+ native: o => o.GlobalLimiter = PartitionedRateLimiter.Create(_ =>
+ RateLimitPartition.GetFixedWindowLimiter("global", _ => new()
+ { PermitLimit = 1, Window = TimeSpan.FromMinutes(1) })), disableNative: true);
+ Assert.Equal(HttpStatusCode.NoContent, (await host.Post("/auth/login")).StatusCode);
+ Assert.Equal(HttpStatusCode.NoContent, (await host.Post("/auth/login")).StatusCode);
+ }
+
+ [Fact]
+ public async Task GlobalAndBuiltInEndpointLimitsBothApply()
+ {
+ await using var host = await Host.Create(o => o.RateLimiting.Login.PermitLimit = 1, native: o =>
+ {
+ o.RejectionStatusCode = 418;
+ o.GlobalLimiter = PartitionedRateLimiter.Create(_ =>
+ RateLimitPartition.GetFixedWindowLimiter("global", _ => new() { PermitLimit = 4, Window = TimeSpan.FromMinutes(1) }));
+ });
+ Assert.Equal(HttpStatusCode.NoContent, (await host.Post("/auth/login")).StatusCode);
+ Assert.Equal(HttpStatusCode.TooManyRequests, (await host.Post("/auth/login")).StatusCode);
+ Assert.Equal(HttpStatusCode.NoContent, (await host.Post("/auth/validate")).StatusCode);
+ // Native middleware may acquire global permits again after endpoint rejection.
+ Assert.Equal(418, (int)(await host.Post("/auth/refresh")).StatusCode);
+ }
+
+ private sealed class Host(WebApplication app) : IAsyncDisposable
+ {
+ internal WebApplication App => app;
+ private readonly HttpClient _client = app.GetTestClient();
+ internal async Task Post(string path, string ip = "192.0.2.1", string? forwarded = null)
+ {
+ using var request = new HttpRequestMessage(HttpMethod.Post, path);
+ request.Headers.Add("Test-Connection-IP", ip);
+ if (forwarded is not null) request.Headers.Add("X-Forwarded-For", forwarded);
+ return await _client.SendAsync(request);
+ }
+ internal static async Task Create(Action? configure = null,
+ bool aspNet = true, bool? enabled = null, bool repeat = false,
+ Action? native = null, bool applicationPolicy = false, bool trustedProxy = false, bool disableNative = false, RateLimiterOptions? middlewareOptions = null)
+ {
+ var builder = WebApplication.CreateBuilder(new WebApplicationOptions { EnvironmentName = "Production" });
+ builder.WebHost.UseTestServer();
+ builder.Services.AddUltimateAuthServer(configure).AddUltimateAuthInMemory();
+ if (native is not null) builder.Services.AddRateLimiter(native);
+ var app = builder.Build();
+ app.Use((context, next) =>
+ {
+ // Test transport only; production never reads this header.
+ context.Connection.RemoteIpAddress = IPAddress.Parse(context.Request.Headers["Test-Connection-IP"].ToString());
+ return next(context);
+ });
+ if (trustedProxy)
+ {
+ var forwarding = new Microsoft.AspNetCore.Builder.ForwardedHeadersOptions
+ { ForwardedHeaders = Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedFor };
+ forwarding.KnownProxies.Add(IPAddress.Parse("192.0.2.1"));
+ app.UseForwardedHeaders(forwarding);
+ }
+ app.UseRouting();
+ if (middlewareOptions is not null) app.UseRateLimiter(middlewareOptions);
+ if (aspNet)
+ {
+ if (middlewareOptions is not null) app.UseUltimateAuthWithAspNetCore(enableCors: null, useRateLimiting: false);
+ else if (enabled is null) app.UseUltimateAuthWithAspNetCore();
+ else app.UseUltimateAuthWithAspNetCore(enableCors: null, useRateLimiting: enabled.Value);
+ if (repeat) app.UseUltimateAuthWithAspNetCore();
+ }
+ else app.UseUltimateAuth(useRateLimiting: enabled ?? false);
+ var group = app.MapGroup("");
+ if (applicationPolicy) group.RequireRateLimiting("Sensitive");
+ if (disableNative) group.DisableRateLimiting();
+ group.MapUltimateAuthEndpoints();
+ group.MapUAuthHub();
+ // Exercise the real endpoint inventory and middleware without invoking business handlers.
+ ((IEndpointConventionBuilder)group).Finally(b => b.RequestDelegate = context => { context.Response.StatusCode = 204; return Task.CompletedTask; });
+ await app.StartAsync();
+ return new Host(app);
+ }
+ public async ValueTask DisposeAsync() { _client.Dispose(); await app.DisposeAsync(); }
+ }
+}
diff --git a/tests/CodeBeam.UltimateAuth.Tests.RateLimiting.Compatibility/CodeBeam.UltimateAuth.Tests.RateLimiting.Compatibility.csproj b/tests/CodeBeam.UltimateAuth.Tests.RateLimiting.Compatibility/CodeBeam.UltimateAuth.Tests.RateLimiting.Compatibility.csproj
new file mode 100644
index 00000000..039804bc
--- /dev/null
+++ b/tests/CodeBeam.UltimateAuth.Tests.RateLimiting.Compatibility/CodeBeam.UltimateAuth.Tests.RateLimiting.Compatibility.csproj
@@ -0,0 +1,25 @@
+
+
+ net8.0;net9.0;net10.0
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/Blazor/UAuthRequestClientTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/Blazor/UAuthRequestClientTests.cs
index b2aeea08..d2257ae3 100644
--- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/Blazor/UAuthRequestClientTests.cs
+++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/Blazor/UAuthRequestClientTests.cs
@@ -144,6 +144,40 @@ public async Task SendFormAsync_WhenBootstrapperFails_DoesNotInvokeJavascript()
JSInterop.Invocations.Should().BeEmpty();
}
+ [Fact]
+ public async Task TryAndCommitThrottleKeepsStatusAndRetryGuidance()
+ {
+ JSInterop.Setup("uauth.tryAndCommit", _ => true)
+ .SetResult(System.Text.Json.JsonSerializer.SerializeToElement(new { status = 429, retryAfter = "45", isSuccess = false }));
+ var error = await Assert.ThrowsAsync(() =>
+ CreateSut().TryAndCommitAsync("/try", "/commit", new { }));
+ Assert.Equal(System.Net.HttpStatusCode.TooManyRequests, error.StatusCode);
+ Assert.Equal("rate_limited", error.Code);
+ Assert.Equal(TimeSpan.FromSeconds(45), error.RetryAfter);
+ }
+
+ [Fact]
+ public async Task TryAndCommitStillDeserializesSuccessfulPreview()
+ {
+ JSInterop.Setup("uauth.tryAndCommit", _ => true)
+ .SetResult(System.Text.Json.JsonSerializer.SerializeToElement(new { isSuccess = true, previewReceipt = "receipt" }));
+ var result = await CreateSut().TryAndCommitAsync("/try", "/commit", new { });
+ Assert.True(result.IsSuccess);
+ Assert.Equal("receipt", result.PreviewReceipt);
+ }
+
+ [Fact]
+ public async Task ObservedLogoutReturnsHttpRejectionRatherThanSubmittingAnUnobservedForm()
+ {
+ JSInterop.Setup("uauth.post", _ => true)
+ .SetResult(new UAuthTransportResult { Status = 429, RetryAfter = "15" });
+ var result = await CreateSut().SendFormAsync("/auth/logout", null, navigateOnSuccess: true);
+ Assert.Equal(429, result.Status);
+ Assert.Equal("15", result.RetryAfter);
+ var invocation = JSInterop.Invocations.Single(x => x.Identifier == "uauth.post");
+ Assert.Equal("logout", GetProperty(invocation.Arguments.Single()!, "mode"));
+ }
+
private sealed class TestTryResult
{
}
diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/ClientRateLimitingTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/ClientRateLimitingTests.cs
new file mode 100644
index 00000000..e3ee0872
--- /dev/null
+++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/ClientRateLimitingTests.cs
@@ -0,0 +1,181 @@
+using System.Net;
+using System.Text.Json;
+using CodeBeam.UltimateAuth.Client;
+using CodeBeam.UltimateAuth.Client.Authentication;
+using CodeBeam.UltimateAuth.Client.Blazor;
+using CodeBeam.UltimateAuth.Client.Blazor.Infrastructure;
+using CodeBeam.UltimateAuth.Client.Contracts;
+using CodeBeam.UltimateAuth.Client.Diagnostics;
+using CodeBeam.UltimateAuth.Client.Errors;
+using CodeBeam.UltimateAuth.Client.Events;
+using CodeBeam.UltimateAuth.Client.Infrastructure;
+using CodeBeam.UltimateAuth.Client.Options;
+using CodeBeam.UltimateAuth.Client.Services;
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Core.MultiTenancy;
+using CodeBeam.UltimateAuth.Tests.Unit.Helpers;
+using Microsoft.AspNetCore.Components;
+using Microsoft.Extensions.Options;
+using Moq;
+
+namespace CodeBeam.UltimateAuth.Tests.Unit;
+
+public sealed class ClientRateLimitingTests : UAuthClientTestBase
+{
+ [Theory]
+ [InlineData(null, null)]
+ [InlineData("", null)]
+ [InlineData("invalid", null)]
+ [InlineData("-1", null)]
+ [InlineData("1.5", null)]
+ [InlineData("60", 60)]
+ [InlineData("0", 0)]
+ [InlineData(" 120 ", 120)]
+ [InlineData("9999999999999999999999999", null)]
+ public void RetryAfterHandlesMissingInvalidAndDeltaSeconds(string? header, int? expected)
+ {
+ Assert.Equal(expected is null ? null : TimeSpan.FromSeconds(expected.Value), UAuthRateLimit.ParseRetryAfter(header, DateTimeOffset.UtcNow));
+ }
+
+ [Fact]
+ public void RetryAfterParsesHttpDateAndPastDate()
+ {
+ var now = new DateTimeOffset(2026, 11, 1, 12, 0, 0, TimeSpan.Zero);
+ Assert.Equal(TimeSpan.FromMinutes(2), UAuthRateLimit.ParseRetryAfter(now.AddMinutes(2).ToString("r"), now));
+ Assert.Equal(TimeSpan.Zero, UAuthRateLimit.ParseRetryAfter(now.AddMinutes(-1).ToString("r"), now));
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("90")]
+ public async Task ValidationThrottlePreservesStateAndBlazorPrincipal_ThenRevocationStillClears(string? retry)
+ {
+ var clock = new TestClock();
+ var snapshot = new AuthStateSnapshot
+ {
+ Identity = new AuthIdentitySnapshot
+ { UserKey = UserKey.FromString("rate-user"), Tenant = TenantKey.Single },
+ Claims = ClaimsSnapshot.Empty
+ };
+ Request.SetupSequence(x => x.SendFormAsync("/auth/validate", null, It.IsAny()))
+ .ReturnsAsync(new UAuthTransportResult { Status = 200, Ok = true, Body = JsonSerializer.SerializeToElement(new AuthValidationResult { State = SessionState.Active, Snapshot = snapshot }) })
+ .ReturnsAsync(new UAuthTransportResult { Status = 429, RetryAfter = retry })
+ .ReturnsAsync(new UAuthTransportResult { Status = 200, Ok = true, Body = JsonSerializer.SerializeToElement(new AuthValidationResult { State = SessionState.Revoked }) });
+ using var manager = new UAuthStateManager(CreateClient(flows: CreateFlowClient()), Events.Object, clock);
+ var provider = new UAuthAuthenticationStateProvider(manager);
+ await manager.EnsureAsync();
+ var lastValidated = manager.State.LastValidatedAt;
+ await manager.EnsureAsync(force: true);
+ await manager.EnsureAsync(force: true);
+ Assert.True(manager.State.IsAuthenticated);
+ Assert.True((await provider.GetAuthenticationStateAsync()).User.Identity!.IsAuthenticated);
+ Assert.Equal(lastValidated, manager.State.LastValidatedAt);
+ Request.Verify(x => x.SendFormAsync("/auth/validate", null, It.IsAny()), Times.Exactly(2));
+ clock.Advance(TimeSpan.FromMinutes(2));
+ await manager.EnsureAsync(force: true);
+ Assert.False(manager.State.IsAuthenticated);
+ Assert.False((await provider.GetAuthenticationStateAsync()).User.Identity!.IsAuthenticated);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("60")]
+ public async Task RefreshIsThrottledWithoutReauthOrImmediateRepeatedRequest(string? retry)
+ {
+ Request.Setup(x => x.SendFormAsync("/auth/refresh", null, It.IsAny()))
+ .ReturnsAsync(new UAuthTransportResult { Status = 429, RetryAfter = retry, RefreshOutcome = "reauth-required" });
+ var flow = CreateFlowClient();
+ var result = await flow.RefreshAsync();
+ Assert.False(result.IsSuccess);
+ Assert.Equal(429, result.Status);
+ Assert.Equal("rate_limited", result.ErrorCode);
+ Assert.Equal(RefreshOutcome.None, result.Outcome);
+ Assert.Equal(retry is null ? null : TimeSpan.FromSeconds(60), result.RetryAfter);
+ await flow.RefreshAsync(isAuto: true);
+ Request.Verify(x => x.SendFormAsync("/auth/refresh", null, It.IsAny()), Times.Once);
+ Events.Verify(x => x.PublishAsync(It.IsAny()), Times.Never);
+ }
+
+ [Fact]
+ public async Task GenuineRefresh401StillRequiresReauth()
+ {
+ Request.Setup(x => x.SendFormAsync("/auth/refresh", null, It.IsAny()))
+ .ReturnsAsync(new UAuthTransportResult { Status = 401 });
+ var result = await CreateFlowClient().RefreshAsync();
+ Assert.Equal(401, result.Status);
+ Assert.Equal(RefreshOutcome.ReauthRequired, result.Outcome);
+ }
+
+ [Theory]
+ [InlineData(UAuthSubmitMode.TryOnly)]
+ [InlineData(UAuthSubmitMode.TryAndCommit)]
+ public async Task LoginThrottleHasDistinctResultAndNeverCommits(UAuthSubmitMode mode)
+ {
+ Request.Setup(x => x.SendJsonAsync(It.IsAny(), It.IsAny