From 86242a2dfd518f46d9b497c0de4267794b4bd9b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Sat, 24 Oct 2026 20:56:41 +0300 Subject: [PATCH] fix(authorization): return structured results from CheckAsync --- docs/content/client-sdk/authorization.md | 78 +++++++++++++++++++ docs/content/how-to-guides/protect-a-page.md | 2 + .../docs/client-sdk/authorization.json | 24 +++++- .../docs/how-to-guides/protect-a-page.json | 4 +- .../Endpoints/AuthorizationEndpointHandler.cs | 8 +- .../Abstractions/IAuthorizationClient.cs | 15 +++- .../AuthorizationManagementTests.cs | 11 ++- .../AuthorizationEndpointHandlerTests.cs | 28 +++++-- .../Client/UAuthClientAuthorizationTests.cs | 44 +++++++++++ 9 files changed, 193 insertions(+), 21 deletions(-) diff --git a/docs/content/client-sdk/authorization.md b/docs/content/client-sdk/authorization.md index 58844d42..53fe8852 100644 --- a/docs/content/client-sdk/authorization.md +++ b/docs/content/client-sdk/authorization.md @@ -164,6 +164,44 @@ A wildcard is not a substitute for satisfying independent security policies, aut ## 4. Check Authorization +`CheckAsync` evaluates whether the current authenticated user is authorized to perform a specific action against a resource. + +It provides an **on-demand, server-side authorization decision** based on the current authentication context, permissions, roles, and applicable authorization policies. + +Unlike operations that perform protected actions, `CheckAsync` only evaluates access. It does not execute the requested operation, modify permissions, grant access, or reserve authorization for a future request. + +### When to Use CheckAsync + +Use `CheckAsync` when your application needs to: + +- Determine whether an authenticated user can perform an action before presenting it in the UI. +- Enable, disable, or hide controls based on the current authorization decision. +- Explain why an operation is unavailable, using the returned denial reason where appropriate. +- Determine whether an operation requires reauthentication. +- Evaluate resource-specific access when the applicable policies support it. + +For example, an application may check whether the current user can delete a particular order before displaying the Delete button. + +### What CheckAsync Does Not Guarantee + +**A successful authorization check is not a guarantee that a subsequent operation will succeed.** + +Authorization is evaluated against the security context available at the time of the check. That context may change before the actual operation is performed. + +For example: + +- An administrator may revoke the user's permissions. +- The user's session may expire or become invalid. +- The target resource may be deleted or modified. +- The resource's ownership or tenant context may change. +- Additional business rules may prevent the operation. + +The protected operation must independently enforce authorization using its current context. + +**Never use a previous `CheckAsync` result as proof of authorization for a later request.** + +`CheckAsync` is primarily a developer-experience and user-experience API. It helps applications understand and present authorization decisions without replacing the security enforcement performed by protected endpoints and application services. + Use `CheckAsync` to ask the server whether the current user is authorized to perform an action. ```csharp @@ -230,6 +268,46 @@ Do not assume that sending `ResourceId` automatically enables ownership checking | `RequiresReauthentication` | Whether the decision indicates reauthentication is required | | `DenyReason` | Optional denial reason code | +### Authorization Decisions vs. HTTP Results + +`CheckAsync` separates the outcome of the API request from the authorization decision itself. + +The method returns `UAuthResult`: + +- `UAuthResult.IsSuccess` indicates whether the authorization check request completed successfully. +- `AuthorizationResult.IsAllowed` indicates whether the requested action is currently authorized. +- `AuthorizationResult.RequiresReauthentication` indicates whether additional authentication is required. +- `AuthorizationResult.DenyReason` provides an optional reason for denial. + +A denied authorization decision is a valid result of a successful authorization check. + +| Scenario | HTTP Status | Authorization Result | +|---|---|---| +| Action allowed | `200 OK` | `IsAllowed = true` | +| Action denied | `200 OK` | `IsAllowed = false` | +| Reauthentication required | `200 OK` | `RequiresReauthentication = true` | +| Caller not authenticated | `401 Unauthorized` | No authorization decision | +| Invalid request | `400 Bad Request` | No authorization decision | +| Server-side evaluation failure | Appropriate error status | No authorization decision | + +**HTTP 200 means the authorization evaluation completed successfully. It does not mean access was granted.** + +Only authenticated callers can use the current `CheckAsync` endpoint. However, callers do not need permission to perform the action they are checking. + +This distinction is intentional: the purpose of `CheckAsync` is to determine whether the action is permitted. + +### Handling Denied Decisions + +Applications should distinguish between authorization denial, reauthentication requirements, and request failures. + +A denied decision may be used to disable an action or display an appropriate message. + +A reauthentication requirement may be used to initiate the application's supported reauthentication flow. + +A failed request must not be interpreted as permission to proceed. + +**Fail closed:** If the authorization decision is unavailable, treat the operation as unavailable in the UI and rely on server-side authorization when the actual operation is requested. + ### Handle All Decision Outcomes ```csharp diff --git a/docs/content/how-to-guides/protect-a-page.md b/docs/content/how-to-guides/protect-a-page.md index 3dedd021..627889dc 100644 --- a/docs/content/how-to-guides/protect-a-page.md +++ b/docs/content/how-to-guides/protect-a-page.md @@ -739,6 +739,8 @@ That endpoint must independently validate the caller and enforce the required au For Blazor Server, the same principle applies to sensitive server-side operations, even when they are invoked without an HTTP call from the component. +> Use [UAuthAuthorize] to declare authorization requirements for routed pages and UAuthStateView for conditional UI rendering. Use UAuthClient.Authorization.CheckAsync() when you need an explicit, on-demand server-side authorization decision for a specific action or resource. These mechanisms serve different purposes and do not replace authorization enforcement on protected server operations. + ### UAuthAuthorize is currently a Blazor attribute The current `[UAuthAuthorize]` attribute is interpreted by UltimateAuth's Blazor authorization pipeline. diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/client-sdk/authorization.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/client-sdk/authorization.json index aec3e496..8df073ca 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/client-sdk/authorization.json +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/client-sdk/authorization.json @@ -1,7 +1,7 @@ { "Slug": "client-sdk/authorization", "Title": "Authorization", - "Html": "\n\u003Cp\u003EUltimateAuth provides a unified client API for managing roles, assigning permissions, controlling user-role relationships, and requesting authorization decisions from the server.\u003C/p\u003E\n\u003Cp\u003EThrough \u003Ccode\u003EUAuthClient.Authorization\u003C/code\u003E, applications can build administrative authorization interfaces and check whether the current user is allowed to perform a particular action.\u003C/p\u003E\n\u003Cp\u003EAuthorization is enforced on the server. Client-side permission checks are useful for improving the user experience, but they do not replace authorization at the endpoint or application-service level.\u003C/p\u003E\n\u003Cp\u003EThis guide covers the current \u003Ccode\u003EIAuthorizationClient\u003C/code\u003E API, practical role management workflows, permission patterns, and secure client integration.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022understanding-authorization\u0022\u003E1. Understanding Authorization\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth authorization combines several concepts:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EConcept\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\u003ERoles\u003C/td\u003E\n\u003Ctd\u003ENamed collections of permissions\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EPermissions\u003C/td\u003E\n\u003Ctd\u003EFine-grained capabilities represented by normalized values\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EUser-role assignments\u003C/td\u003E\n\u003Ctd\u003EAssociations between users and roles\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EPolicies\u003C/td\u003E\n\u003Ctd\u003EServer-side rules that participate in access decisions\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAuthorization context\u003C/td\u003E\n\u003Ctd\u003EInformation about the actor, resource, action, and current security state\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EA typical authorization relationship looks like this:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EUser\n |\n \u002B-- Manager\n | |\n | \u002B-- products.read\n | \u002B-- products.create\n | \u002B-- products.update\n |\n \u002B-- Auditor\n |\n \u002B-- reports.read\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EA user can have multiple roles, and each role can contain multiple permissions.\u003C/p\u003E\n\u003Cp\u003EHowever, \u003Cstrong\u003Ehaving a permission does not automatically guarantee access\u003C/strong\u003E. The server\u0027s authorization pipeline may apply additional requirements, such as resource ownership, session validity, tenant isolation, or application policies.\u003C/p\u003E\n\u003Cp\u003EFor the internal decision model, see \u003Ca href=\u0022/docs/security/authorization-pipeline\u0022\u003EAuthorization Pipeline Internals\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022accessing-the-authorization-client\u0022\u003E2. Accessing the Authorization Client\u003C/h2\u003E\n\u003Cp\u003EInject \u003Ccode\u003EIUAuthClient\u003C/code\u003E into a Blazor component:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@using CodeBeam.UltimateAuth.Client\n@inject IUAuthClient UAuthClient\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAuthorization operations are available through:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthClient.Authorization\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis property exposes \u003Ccode\u003EIAuthorizationClient\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe interface provides three main groups of operations:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Cstrong\u003EAuthorization checks:\u003C/strong\u003E Evaluate whether the current user may perform an action.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003ERole management:\u003C/strong\u003E Create, query, rename, configure, and delete roles.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EUser-role management:\u003C/strong\u003E Retrieve, assign, and remove user roles.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThe SDK sends requests to the corresponding UltimateAuth endpoints and returns structured \u003Ccode\u003EUAuthResult\u003C/code\u003E values.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022understanding-permissions\u0022\u003E3. Understanding Permissions\u003C/h2\u003E\n\u003Cp\u003EA permission is represented by the \u003Ccode\u003EPermission\u003C/code\u003E value 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\u003Evar permission = Permission.From(\u0026quot;products.create\u0026quot;);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe current implementation normalizes permission values by trimming surrounding whitespace and converting the value to lowercase.\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 permission = Permission.From(\u0026quot; PRODUCTS.CREATE \u0026quot;);\n\nConsole.WriteLine(permission.Value);\n// products.create\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe constructor rejects null, empty, and whitespace-only values.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022permission-patterns\u0022\u003EPermission Patterns\u003C/h3\u003E\n\u003Cp\u003EUltimateAuth recognizes three important forms:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EPattern\u003C/th\u003E\n\u003Cth\u003EExample\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\u003EExact\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Eproducts.create\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EA specific permission\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EPrefix wildcard\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Eproducts.*\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPermissions within the corresponding prefix\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EGlobal wildcard\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E*\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUnrestricted permission matching within the applicable authorization rules\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003ECreate them using:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar exact = Permission.From(\u0026quot;products.create\u0026quot;);\n\nvar prefix = Permission.From(\u0026quot;products.*\u0026quot;);\n\nvar global = Permission.Wildcard;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe type also 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\u003Epermission.IsWildcard;\npermission.IsPrefix;\npermission.IsValid;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022permission-normalization\u0022\u003EPermission Normalization\u003C/h3\u003E\n\u003Cp\u003EThe following values normalize to the same representation:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EPermission.From(\u0026quot;Products.Create\u0026quot;);\nPermission.From(\u0026quot;products.create\u0026quot;);\nPermission.From(\u0026quot; PRODUCTS.CREATE \u0026quot;);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAll produce:\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\u003Eproducts.create\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUse consistent permission names across application services, policies, and role configuration.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022wildcards-are-powerful\u0022\u003EWildcards Are Powerful\u003C/h3\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\u003EPermission.From(\u0026quot;users.*\u0026quot;)\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Emay grant broad access to operations within the \u003Ccode\u003Eusers\u003C/code\u003E permission prefix.\u003C/p\u003E\n\u003Cp\u003EThe global wildcard:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EPermission.Wildcard\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Eis especially sensitive.\u003C/p\u003E\n\u003Cp\u003EAvoid assigning broad wildcard permissions merely to simplify development. Use least-privilege permissions for ordinary application roles.\u003C/p\u003E\n\u003Cp\u003EA wildcard is not a substitute for satisfying independent security policies, authentication requirements, or tenant restrictions.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022check-authorization\u0022\u003E4. Check Authorization\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003ECheckAsync\u003C/code\u003E to ask the server whether the current user is authorized to perform an action.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.CheckAsync(\n new AuthorizationCheckRequest\n {\n Action = \u0026quot;read\u0026quot;,\n Resource = \u0026quot;products\u0026quot;\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { IsAllowed: true })\n{\n // The server authorized this action.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthResult\u0026lt;AuthorizationResult\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022authorizationcheckrequest\u0022\u003EAuthorizationCheckRequest\u003C/h3\u003E\n\u003Cp\u003EThe request contains:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EType\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAction\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Estring\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERequested operation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EResource\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Estring\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EResource being accessed\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EResourceId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Estring?\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional identifier for a specific resource instance\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EBoth \u003Ccode\u003EAction\u003C/code\u003E and \u003Ccode\u003EResource\u003C/code\u003E are required.\u003C/p\u003E\n\u003Cp\u003EThe action and resource values must follow the conventions understood by your server-side authorization configuration.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022check-a-specific-resource\u0022\u003ECheck a Specific Resource\u003C/h3\u003E\n\u003Cp\u003EFor example, check access to a particular product:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 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.Authorization.CheckAsync(\n new AuthorizationCheckRequest\n {\n Action = \u0026quot;update\u0026quot;,\n Resource = \u0026quot;products\u0026quot;,\n ResourceId = productId.ToString()\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EProviding \u003Ccode\u003EResourceId\u003C/code\u003E allows the server to evaluate resource-specific rules where supported by the configured authorization pipeline.\u003C/p\u003E\n\u003Cp\u003EFor example, the application may distinguish between updating any product and updating a product owned by the current user.\u003C/p\u003E\n\u003Cp\u003EDo not assume that sending \u003Ccode\u003EResourceId\u003C/code\u003E automatically enables ownership checking. The relevant server-side policy must implement the required rule.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022understanding-authorization-results\u0022\u003E5. Understanding Authorization Results\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003ECheckAsync\u003C/code\u003E returns an \u003Ccode\u003EAuthorizationResult\u003C/code\u003E with the following properties:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIsAllowed\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EWhether the request was authorized\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERequiresReauthentication\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EWhether the decision indicates reauthentication is required\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDenyReason\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional denial reason code\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=\u0022handle-all-decision-outcomes\u0022\u003EHandle All Decision Outcomes\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.CheckAsync(\n new AuthorizationCheckRequest\n {\n Action = \u0026quot;delete\u0026quot;,\n Resource = \u0026quot;products\u0026quot;,\n ResourceId = productId.ToString()\n });\n\nif (!result.IsSuccess)\n{\n // The authorization request itself failed.\n}\nelse if (result.Value is { RequiresReauthentication: true })\n{\n // Ask the user to authenticate again.\n}\nelse if (result.Value is { IsAllowed: true })\n{\n // The operation is allowed.\n}\nelse\n{\n // The operation was denied.\n var reason = result.Value?.DenyReason;\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAn unsuccessful API request and an explicit authorization denial are different outcomes.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003Eresult.IsSuccess == false\u003C/code\u003E means the authorization request did not complete successfully at the API level.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003Eresult.Value.IsAllowed == false\u003C/code\u003E means the server returned a denial decision.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003Eresult.Value.RequiresReauthentication == true\u003C/code\u003E indicates that the caller needs to reestablish authentication.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003ETreat failed or unavailable authorization checks as \u003Cstrong\u003Enot authorized\u003C/strong\u003E in the UI.\u003C/p\u003E\n\u003Cp\u003ENever grant access because an authorization request failed.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022use-authorization-checks-in-blazor\u0022\u003E6. Use Authorization Checks in Blazor\u003C/h2\u003E\n\u003Cp\u003EA common use case is controlling whether an action button should be displayed.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@using CodeBeam.UltimateAuth.Client\n@using CodeBeam.UltimateAuth.Authorization.Contracts\n@inject IUAuthClient UAuthClient\n\n@if (_canCreate)\n{\n \u0026lt;button @onclick=\u0026quot;CreateProductAsync\u0026quot;\u0026gt;Create Product\u0026lt;/button\u0026gt;\n}\n\n@code {\n private bool _canCreate;\n\n protected override async Task OnInitializedAsync()\n {\n var result = await UAuthClient.Authorization.CheckAsync(\n new AuthorizationCheckRequest\n {\n Action = \u0026quot;create\u0026quot;,\n Resource = \u0026quot;products\u0026quot;\n });\n\n _canCreate = result.IsSuccess \u0026amp;\u0026amp; result.Value is { IsAllowed: true };\n }\n\n private Task CreateProductAsync()\n {\n // Invoke the protected application operation.\n return Task.CompletedTask;\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis improves the interface by hiding unavailable actions.\u003C/p\u003E\n\u003Cp\u003EHowever, the endpoint handling product creation must still enforce authorization independently.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022why-client-checks-are-not-security-boundaries\u0022\u003EWhy Client Checks Are Not Security Boundaries\u003C/h3\u003E\n\u003Cp\u003EA user can bypass a hidden button and send a request directly to an endpoint.\u003C/p\u003E\n\u003Cp\u003ETherefore:\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\u003EClient authorization check\n |\n v\nImprove the user experience\n |\n v\nUser submits an operation\n |\n v\nServer independently authorizes the operation\n |\n v\nExecute or deny\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EClient authorization checks should never be the only protection for sensitive operations.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022get-my-roles\u0022\u003E7. Get My Roles\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EGetMyRolesAsync\u003C/code\u003E to retrieve roles assigned to the current authenticated user.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.GetMyRolesAsync();\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } data)\n{\n foreach (var role in data.Roles.Items)\n {\n Console.WriteLine(role.Name);\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthResult\u0026lt;UserRolesGetResult\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022userrolesgetresult\u0022\u003EUserRolesGetResult\u003C/h3\u003E\n\u003Cp\u003EThe response contains:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserKey\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUser whose roles were requested\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERoles\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPaginated collection of role assignments\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EEach entry is a \u003Ccode\u003EUserRoleInfo\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022userroleinfo\u0022\u003EUserRoleInfo\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETenant\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETenant associated with the assignment\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserKey\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAssigned user\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERoleId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAssigned role\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERole name\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAssignedAt\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAssignment timestamp\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=\u0022pagination\u0022\u003EPagination\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.GetMyRolesAsync(\n new PageRequest\n {\n PageNumber = 1,\n PageSize = 20\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe returned role collection is paginated.\u003C/p\u003E\n\u003Cp\u003EDo not assume that the first page contains all assigned roles.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022get-another-users-roles\u0022\u003E8. Get Another User\u0027s Roles\u003C/h2\u003E\n\u003Cp\u003EAdministrative interfaces can retrieve the roles assigned to a specific user.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.GetUserRolesAsync(userKey,\n new PageRequest\n {\n PageNumber = 1,\n PageSize = 20\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } data)\n{\n foreach (var role in data.Roles.Items)\n {\n Console.WriteLine($\u0026quot;{role.Name} \u2014 {role.AssignedAt}\u0026quot;);\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis operation requires appropriate administrative authorization.\u003C/p\u003E\n\u003Cp\u003EThe \u003Ccode\u003EUserKey\u003C/code\u003E identifies the target user, not the current actor.\u003C/p\u003E\n\u003Cp\u003EThe server determines whether the current actor may inspect that user\u0027s role assignments.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022query-roles\u0022\u003E9. Query Roles\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EQueryRolesAsync\u003C/code\u003E to retrieve roles with pagination and optional filtering.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 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.Authorization.QueryRolesAsync(\n new RoleQuery\n {\n PageNumber = 1,\n PageSize = 20\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } page)\n{\n foreach (var role in page.Items)\n {\n Console.WriteLine(role.Name);\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthResult\u0026lt;PagedResult\u0026lt;RoleInfo\u0026gt;\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022rolequery\u0022\u003ERoleQuery\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003ERoleQuery\u003C/code\u003E inherits from \u003Ccode\u003EPageRequest\u003C/code\u003E and adds:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESearch\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional role-name search\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIncludeDeleted\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EInclude deleted roles when supported and authorized\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPageNumber\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERequested page\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPageSize\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERequested page size\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022search-roles\u0022\u003ESearch Roles\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.QueryRolesAsync(\n new RoleQuery\n {\n Search = \u0026quot;manager\u0026quot;,\n PageNumber = 1,\n PageSize = 10\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022include-deleted-roles\u0022\u003EInclude Deleted Roles\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.QueryRolesAsync(\n new RoleQuery\n {\n IncludeDeleted = true,\n PageNumber = 1,\n PageSize = 20\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUse \u003Ccode\u003EIncludeDeleted\u003C/code\u003E only where the application needs administrative access to deleted role records.\u003C/p\u003E\n\u003Cp\u003EThe server remains responsible for enforcing the query\u0027s permissions and lifecycle rules.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022understanding-roleinfo\u0022\u003E10. Understanding RoleInfo\u003C/h2\u003E\n\u003Cp\u003EEach role returned by the SDK is represented by \u003Ccode\u003ERoleInfo\u003C/code\u003E.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EType\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003ERoleId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERole identifier\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Estring\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERole name\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPermissions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EIReadOnlyCollection\u0026lt;Permission\u0026gt;\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAssigned permissions\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECreatedAt\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EDateTimeOffset\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECreation timestamp\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUpdatedAt\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EDateTimeOffset?\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELast update timestamp\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.QueryRolesAsync(\n new RoleQuery\n {\n PageNumber = 1,\n PageSize = 20\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } page)\n{\n foreach (var role in page.Items)\n {\n Console.WriteLine($\u0026quot;Role: {role.Name}\u0026quot;);\n\n foreach (var permission in role.Permissions)\n {\n Console.WriteLine($\u0026quot; {permission.Value}\u0026quot;);\n }\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003ERoleId\u003C/code\u003E is a strongly typed identifier rather than a plain \u003Ccode\u003EGuid\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\u003ERoleId roleId = RoleId.From(existingGuid);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUse \u003Ccode\u003ERoleId\u003C/code\u003E for role management requests.\u003C/p\u003E\n\u003Cp\u003EDo not confuse it with \u003Ccode\u003EUserKey\u003C/code\u003E or other identifier types.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022create-a-role\u0022\u003E11. Create a Role\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003ECreateRoleAsync\u003C/code\u003E to create a role.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 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.Authorization.CreateRoleAsync(\n new CreateRoleRequest\n {\n Name = \u0026quot;Manager\u0026quot;\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } role)\n{\n Console.WriteLine($\u0026quot;Created: {role.Name}\u0026quot;);\n Console.WriteLine($\u0026quot;Role ID: {role.Id}\u0026quot;);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthResult\u0026lt;RoleInfo\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ERole names must be unique within the relevant tenant according to the server\u0027s role-name rules.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022create-a-role-with-permissions\u0022\u003ECreate a Role with Permissions\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003ECreateRoleRequest\u003C/code\u003E supports an optional initial permission collection.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 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.Authorization.CreateRoleAsync(\n new CreateRoleRequest\n {\n Name = \u0026quot;ProductManager\u0026quot;,\n Permissions =\n [\n Permission.From(\u0026quot;products.read\u0026quot;),\n Permission.From(\u0026quot;products.create\u0026quot;),\n Permission.From(\u0026quot;products.update\u0026quot;)\n ]\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis allows applications to create and configure a role in one operation.\u003C/p\u003E\n\u003Cp\u003EThe server validates the requested permissions and whether the caller is authorized to create the role.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022create-a-read-only-role\u0022\u003ECreate a Read-Only Role\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.CreateRoleAsync(\n new CreateRoleRequest\n {\n Name = \u0026quot;Auditor\u0026quot;,\n Permissions =\n [\n Permission.From(\u0026quot;reports.read\u0026quot;),\n Permission.From(\u0026quot;products.read\u0026quot;)\n ]\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUse role names that describe the responsibility or capability represented by the role.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022rename-a-role\u0022\u003E12. Rename a Role\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003ERenameRoleAsync\u003C/code\u003E to change a role\u0027s name.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 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.Authorization.RenameRoleAsync(\n new RenameRoleRequest\n {\n Id = roleId,\n Name = \u0026quot;SeniorManager\u0026quot;\n });\n\nif (result.IsSuccess)\n{\n // Role renamed.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003ERenameRoleRequest\u003C/code\u003E requires:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003EId\u003C/code\u003E: The role\u0027s \u003Ccode\u003ERoleId\u003C/code\u003E.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EName\u003C/code\u003E: The new role name.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThe operation may fail if another role already uses the requested name.\u003C/p\u003E\n\u003Cp\u003ERole renaming does not mean that permissions are automatically changed.\u003C/p\u003E\n\u003Cp\u003EAfter a successful rename, the SDK publishes an \u003Ccode\u003EAuthorizationChanged\u003C/code\u003E state event.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022set-role-permissions\u0022\u003E13. Set Role Permissions\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003ESetRolePermissionsAsync\u003C/code\u003E to configure a role\u0027s permissions.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 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.Authorization.SetRolePermissionsAsync(\n new SetRolePermissionsRequest\n {\n RoleId = roleId,\n Permissions =\n [\n Permission.From(\u0026quot;products.read\u0026quot;),\n Permission.From(\u0026quot;products.create\u0026quot;),\n Permission.From(\u0026quot;products.update\u0026quot;)\n ]\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe request contains:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERoleId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERole to modify\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPermissions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENew permission collection\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=\u0022important-this-is-a-replacement-operation\u0022\u003EImportant: This Is a Replacement Operation\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003ESetRolePermissionsAsync\u003C/code\u003E replaces the role\u0027s existing permission collection.\u003C/p\u003E\n\u003Cp\u003EIt does not append permissions to the current collection.\u003C/p\u003E\n\u003Cp\u003EFor example, suppose a role currently contains:\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\u003Eproducts.read\nproducts.create\nproducts.update\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ESubmitting:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EPermissions =\n[\n Permission.From(\u0026quot;products.read\u0026quot;)\n]\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Emeans the intended new permission collection contains only:\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\u003Eproducts.read\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe other permissions are removed from the role.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022safely-add-a-permission-to-an-existing-role\u0022\u003ESafely Add a Permission to an Existing Role\u003C/h3\u003E\n\u003Cp\u003ETo preserve existing permissions, first retrieve the role and construct the complete desired collection.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 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 query = await UAuthClient.Authorization.QueryRolesAsync(\n new RoleQuery\n {\n Search = \u0026quot;ProductManager\u0026quot;,\n PageNumber = 1,\n PageSize = 50\n });\n\nif (query.IsSuccess \u0026amp;\u0026amp; query.Value is { } page)\n{\n var role = page.Items.FirstOrDefault(x =\u0026gt; x.Name == \u0026quot;ProductManager\u0026quot;);\n\n if (role is not null)\n {\n var permissions = role.Permissions.Append(Permission.From(\u0026quot;products.delete\u0026quot;)).Distinct().ToArray();\n\n var result =\n await UAuthClient.Authorization.SetRolePermissionsAsync(\n new SetRolePermissionsRequest\n {\n RoleId = role.Id,\n Permissions = permissions\n });\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis is a read-modify-write pattern, not an atomic append operation.\u003C/p\u003E\n\u003Cp\u003EIf multiple administrators edit the same role concurrently, the last successful replacement may overwrite another administrator\u0027s changes.\u003C/p\u003E\n\u003Cp\u003EApplications requiring concurrency-safe editing should implement suitable coordination or version checking at the application level.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022remove-a-permission\u0022\u003ERemove a Permission\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 updatedPermissions = role.Permissions.Where(x =\u0026gt; x.Value != \u0026quot;products.delete\u0026quot;).ToArray();\n\nvar result = await UAuthClient.Authorization.SetRolePermissionsAsync(\n new SetRolePermissionsRequest\n {\n RoleId = role.Id,\n Permissions = updatedPermissions\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAgain, submit the complete desired permission set.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022clear-all-permissions\u0022\u003EClear All Permissions\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.SetRolePermissionsAsync(\n new SetRolePermissionsRequest\n {\n RoleId = roleId,\n Permissions = []\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAn empty collection requests that the role have no permissions.\u003C/p\u003E\n\u003Cp\u003EThe server determines whether the operation is permitted.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022assign-a-role-to-a-user\u0022\u003E14. Assign a Role to a User\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EAssignRoleToUserAsync\u003C/code\u003E to assign an existing role to a user.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.AssignRoleToUserAsync(\n new AssignRoleRequest\n {\n UserKey = userKey,\n RoleName = \u0026quot;Manager\u0026quot;\n });\n\nif (result.IsSuccess)\n{\n // Role assigned.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe request contains:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserKey\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETarget user\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERoleName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EExisting role name\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe role must exist, and the current actor must be authorized to assign it.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022assign-multiple-roles\u0022\u003EAssign Multiple Roles\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\u003Eforeach (var roleName in new[] { \u0026quot;Manager\u0026quot;, \u0026quot;Auditor\u0026quot; })\n{\n var result = await UAuthClient.Authorization.AssignRoleToUserAsync(\n new AssignRoleRequest\n {\n UserKey = userKey,\n RoleName = roleName\n });\n\n if (!result.IsSuccess)\n {\n // Handle the failed assignment.\n break;\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EEach call is a separate operation.\u003C/p\u003E\n\u003Cp\u003EThis example does not provide transactional, all-or-nothing behavior across the entire collection.\u003C/p\u003E\n\u003Cp\u003EFor complex provisioning workflows, consider whether the application needs a server-side orchestration operation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022remove-a-role-from-a-user\u0022\u003E15. Remove a Role from a User\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003ERemoveRoleFromUserAsync\u003C/code\u003E:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.RemoveRoleFromUserAsync(\n new RemoveRoleRequest\n {\n UserKey = userKey,\n RoleName = \u0026quot;Manager\u0026quot;\n });\n\nif (result.IsSuccess)\n{\n // Role removed.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ERemoving a role changes the user\u0027s role assignments.\u003C/p\u003E\n\u003Cp\u003EIt does not delete the role itself.\u003C/p\u003E\n\u003Cp\u003EOther users assigned to that role are unaffected by this particular operation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022important-effective-permissions-may-remain\u0022\u003EImportant: Effective Permissions May Remain\u003C/h3\u003E\n\u003Cp\u003EA user may receive the same permission through multiple roles.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EUser\n |\n \u002B-- Manager\n | \u002B-- products.read\n |\n \u002B-- Auditor\n \u002B-- products.read\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ERemoving \u003Ccode\u003EManager\u003C/code\u003E does not necessarily remove the user\u0027s ability to read products because \u003Ccode\u003EAuditor\u003C/code\u003E still provides \u003Ccode\u003Eproducts.read\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003ETo determine whether the user may perform an action, rely on the server\u0027s authorization decision rather than inspecting one role assignment in isolation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022delete-a-role\u0022\u003E16. Delete a Role\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EDeleteRoleAsync\u003C/code\u003E to delete a role.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 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.Authorization.DeleteRoleAsync(\n new DeleteRoleRequest\n {\n Id = roleId,\n Mode = DeleteMode.Soft\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe request supports a deletion mode.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022hard-delete\u0022\u003EHard Delete\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.DeleteRoleAsync(\n new DeleteRoleRequest\n {\n Id = roleId,\n Mode = DeleteMode.Hard\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe availability and effect of each deletion mode are determined by server-side lifecycle rules.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022roledeleteresult\u0022\u003ERoleDeleteResult\u003C/h3\u003E\n\u003Cp\u003EThe method returns:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthResult\u0026lt;RoleDeleteResult\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe response contains:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERoleId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EDeleted role\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERemovedAssignments\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENumber of user-role assignments removed\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMode\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EApplied deletion mode\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDeletedAt\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EDeletion timestamp\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022inspect-the-result\u0022\u003EInspect the Result\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.DeleteRoleAsync(\n new DeleteRoleRequest\n {\n Id = roleId,\n Mode = DeleteMode.Soft\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } deleted)\n{\n Console.WriteLine($\u0026quot;Removed assignments: {deleted.RemovedAssignments}\u0026quot;);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EDeleting a role removes its assignments from users.\u003C/p\u003E\n\u003Cp\u003EThis can affect multiple users at once.\u003C/p\u003E\n\u003Cp\u003EFor that reason, role deletion should be presented as a sensitive administrative operation with appropriate confirmation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022build-a-role-management-page\u0022\u003E17. Build a Role Management Page\u003C/h2\u003E\n\u003Cp\u003EThe following Blazor example demonstrates querying roles, creating a role, and deleting a selected role.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@using CodeBeam.UltimateAuth.Client\n@using CodeBeam.UltimateAuth.Authorization.Contracts\n@using CodeBeam.UltimateAuth.Core.Contracts\n@inject IUAuthClient UAuthClient\n\n\u0026lt;h3\u0026gt;Role Management\u0026lt;/h3\u0026gt;\n\n\u0026lt;input @bind=\u0026quot;_search\u0026quot; placeholder=\u0026quot;Search roles\u0026quot; /\u0026gt;\n\n\u0026lt;button @onclick=\u0026quot;LoadRolesAsync\u0026quot;\u0026gt;Search\u0026lt;/button\u0026gt;\n\n@if (_loading)\n{\n \u0026lt;p\u0026gt;Loading roles...\u0026lt;/p\u0026gt;\n}\nelse\n{\n @foreach (var role in _roles)\n {\n \u0026lt;div @key=\u0026quot;role.Id\u0026quot;\u0026gt;\n \u0026lt;strong\u0026gt;@role.Name\u0026lt;/strong\u0026gt;\n \u0026lt;span\u0026gt;@role.Permissions.Count permissions\u0026lt;/span\u0026gt;\n \u0026lt;button @onclick=\u0026quot;() =\u0026gt; DeleteRoleAsync(role.Id)\u0026quot;\u0026gt;Delete\u0026lt;/button\u0026gt;\n \u0026lt;/div\u0026gt;\n }\n}\n\n\u0026lt;h4\u0026gt;Create Role\u0026lt;/h4\u0026gt;\n\u0026lt;input @bind=\u0026quot;_newRoleName\u0026quot; placeholder=\u0026quot;Role name\u0026quot; /\u0026gt;\n\u0026lt;button @onclick=\u0026quot;CreateRoleAsync\u0026quot; disabled=\u0026quot;@_busy\u0026quot;\u0026gt;Create\u0026lt;/button\u0026gt;\n\n@if (_message is not null)\n{\n \u0026lt;p role=\u0026quot;status\u0026quot;\u0026gt;@_message\u0026lt;/p\u0026gt;\n}\n\n@code {\n private IReadOnlyList\u0026lt;RoleInfo\u0026gt; _roles = [];\n\n private string _search = \u0026quot;\u0026quot;;\n private string _newRoleName = \u0026quot;\u0026quot;;\n private string? _message;\n\n private bool _loading = true;\n private bool _busy;\n\n protected override async Task OnInitializedAsync()\n {\n await LoadRolesAsync();\n }\n\n private async Task LoadRolesAsync()\n {\n _loading = true;\n\n try\n {\n var result = await UAuthClient.Authorization.QueryRolesAsync(\n new RoleQuery\n {\n Search = _search,\n PageNumber = 1,\n PageSize = 50\n });\n\n if (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } page)\n {\n _roles = page.Items;\n }\n else\n {\n _message = \u0026quot;Unable to load roles.\u0026quot;;\n }\n }\n catch (Exception)\n {\n _message = \u0026quot;The authorization service is unavailable.\u0026quot;;\n }\n finally\n {\n _loading = false;\n }\n }\n\n private async Task CreateRoleAsync()\n {\n if (_busy || string.IsNullOrWhiteSpace(_newRoleName))\n return;\n\n _busy = true;\n _message = null;\n\n try\n {\n var result = await UAuthClient.Authorization.CreateRoleAsync(\n new CreateRoleRequest\n {\n Name = _newRoleName\n });\n\n if (result.IsSuccess)\n {\n _newRoleName = \u0026quot;\u0026quot;;\n await LoadRolesAsync();\n }\n else\n {\n _message = \u0026quot;Unable to create role.\u0026quot;;\n }\n }\n finally\n {\n _busy = false;\n }\n }\n\n private async Task DeleteRoleAsync(RoleId roleId)\n {\n // Add a confirmation dialog before deletion in production.\n\n var result = await UAuthClient.Authorization.DeleteRoleAsync(\n new DeleteRoleRequest\n {\n Id = roleId,\n Mode = DeleteMode.Soft\n });\n\n if (result.IsSuccess)\n {\n await LoadRolesAsync();\n }\n else\n {\n _message = \u0026quot;Unable to delete role.\u0026quot;;\n }\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis example demonstrates the SDK integration rather than a complete production administration interface.\u003C/p\u003E\n\u003Cp\u003EA production implementation should include pagination, role assignment controls, permission editing, deletion confirmation, and appropriate loading and error handling for every operation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022build-a-user-role-assignment-interface\u0022\u003E18. Build a User Role Assignment Interface\u003C/h2\u003E\n\u003Cp\u003EAn administrative user details page can display assigned roles and allow administrators to add or remove them.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022load-user-roles\u0022\u003ELoad User Roles\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.GetUserRolesAsync(userKey,\n new PageRequest\n {\n PageNumber = 1,\n PageSize = 50\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } data)\n{\n var assignedRoles = data.Roles.Items.ToList();\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022assign-a-selected-role\u0022\u003EAssign a Selected Role\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.AssignRoleToUserAsync(\n new AssignRoleRequest\n {\n UserKey = userKey,\n RoleName = selectedRoleName\n });\n\nif (result.IsSuccess)\n{\n // Reload the user\u0027s assigned roles.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022remove-a-selected-role\u0022\u003ERemove a Selected Role\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.RemoveRoleFromUserAsync(\n new RemoveRoleRequest\n {\n UserKey = userKey,\n RoleName = selectedRoleName\n });\n\nif (result.IsSuccess)\n{\n // Reload the user\u0027s assigned roles.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis pattern works well alongside \u003Ccode\u003EUAuthClient.Users\u003C/code\u003E for building a unified user administration interface.\u003C/p\u003E\n\u003Cp\u003ESee \u003Ca href=\u0022/docs/client/user-management\u0022\u003EUser Management\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022authorization-state-events\u0022\u003E19. Authorization State Events\u003C/h2\u003E\n\u003Cp\u003EThe current \u003Ccode\u003EUAuthAuthorizationClient\u003C/code\u003E publishes:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthStateEvent.AuthorizationChanged\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Eafter successful authorization mutations.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EOperation\u003C/th\u003E\n\u003Cth\u003EPublishes \u003Ccode\u003EAuthorizationChanged\u003C/code\u003E\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAssignRoleToUserAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERemoveRoleFromUserAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERenameRoleAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESetRolePermissionsAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDeleteRoleAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECreateRoleAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENo\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EQueryRolesAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENo\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EGetMyRolesAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENo\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EGetUserRolesAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENo\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECheckAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENo\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe event follows the configured client state event handling mode.\u003C/p\u003E\n\u003Cp\u003EIt can be used to refresh navigation menus, role assignment views, or permission-sensitive UI.\u003C/p\u003E\n\u003Cp\u003EHowever, it is a \u003Cstrong\u003Elocal client event\u003C/strong\u003E, not a guarantee that all active clients or distributed caches have immediately observed the change.\u003C/p\u003E\n\u003Cp\u003EFor example, an administrator assigning a role to another user does not automatically mean that user\u0027s open browser receives the administrator\u0027s local event.\u003C/p\u003E\n\u003Cp\u003EApplications requiring cross-client updates should provide an appropriate synchronization mechanism.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022authorization-changes-and-active-sessions\u0022\u003E20. Authorization Changes and Active Sessions\u003C/h2\u003E\n\u003Cp\u003ERole and permission changes may affect active users.\u003C/p\u003E\n\u003Cp\u003EHowever, the timing of those changes depends on how authorization data is resolved and cached by the server.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Col\u003E\n\u003Cli\u003EAn administrator removes a role.\u003C/li\u003E\n\u003Cli\u003EThe server persists the new role assignment state.\u003C/li\u003E\n\u003Cli\u003EA subsequent authorization evaluation uses the effective authorization state available to the server.\u003C/li\u003E\n\u003C/ol\u003E\n\u003Cp\u003EIf the application caches authorization information, changes may not become visible everywhere immediately.\u003C/p\u003E\n\u003Cp\u003EDo not assume that a client event invalidates every active session or refreshes every distributed authorization cache.\u003C/p\u003E\n\u003Cp\u003EFor sensitive operations, authorization should be evaluated against sufficiently current server-side state.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022roles-are-not-sessions\u0022\u003ERoles Are Not Sessions\u003C/h3\u003E\n\u003Cp\u003ERemoving a role and revoking a session are different operations.\u003C/p\u003E\n\u003Cp\u003ERemoving a role changes authorization data.\u003C/p\u003E\n\u003Cp\u003ERevoking a session changes whether an authentication session remains valid.\u003C/p\u003E\n\u003Cp\u003EApplications may combine these operations when security policy requires immediate termination of access, but the concepts should remain separate.\u003C/p\u003E\n\u003Cp\u003ESee \u003Ca href=\u0022/docs/client/session-management\u0022\u003ESession Management\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022multi-tenant-authorization\u0022\u003E21. Multi-Tenant Authorization\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth authorization operations are scoped to the current tenant context.\u003C/p\u003E\n\u003Cp\u003ERole names are unique within the relevant tenant, and role assignments belong to the applicable tenant.\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\u003ETenant A\n |\n \u002B-- Manager\n | \u002B-- products.read\n |\n \u002B-- User A\n\nTenant B\n |\n \u002B-- Manager\n | \u002B-- reports.read\n |\n \u002B-- User B\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe same role name may represent different permission sets in different tenants.\u003C/p\u003E\n\u003Cp\u003EThe client does not gain cross-tenant access merely by providing another user\u0027s \u003Ccode\u003EUserKey\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe server must enforce tenant isolation and administrative authorization.\u003C/p\u003E\n\u003Cp\u003EWhen implementing multi-tenant administration, do not assume role names or permission configurations are globally shared.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022handling-authorization-errors\u0022\u003E22. Handling Authorization Errors\u003C/h2\u003E\n\u003Cp\u003EAuthorization methods return \u003Ccode\u003EUAuthResult\u003C/code\u003E or \u003Ccode\u003EUAuthResult\u0026lt;T\u0026gt;\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022handle-role-creation-errors\u0022\u003EHandle Role Creation Errors\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.CreateRoleAsync(\n new CreateRoleRequest\n {\n Name = \u0026quot;Manager\u0026quot;\n });\n\nif (result.IsSuccess)\n{\n // Role created.\n}\nelse if (result.Http.IsConflict)\n{\n // A conflicting role may already exist.\n}\nelse if (result.Http.IsForbidden)\n{\n // Caller lacks permission.\n}\nelse if (result.Http.IsBadRequest)\n{\n // Invalid request.\n}\nelse\n{\n // Handle another failure.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022handle-authorization-check-failures\u0022\u003EHandle Authorization Check Failures\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.CheckAsync(\n new AuthorizationCheckRequest\n {\n Action = \u0026quot;update\u0026quot;,\n Resource = \u0026quot;products\u0026quot;\n });\n\nvar allowed = result.IsSuccess \u0026amp;\u0026amp; result.Value is { IsAllowed: true };\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis fail-closed pattern is appropriate when determining whether the UI should expose a protected operation.\u003C/p\u003E\n\u003Cp\u003EUse the server\u0027s structured error information for diagnostics where appropriate.\u003C/p\u003E\n\u003Cp\u003EDo not expose sensitive policy internals unnecessarily.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-considerations\u0022\u003E23. Security Considerations\u003C/h2\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022server-authorization-is-authoritative\u0022\u003EServer Authorization Is Authoritative\u003C/h3\u003E\n\u003Cp\u003EClient-side checks improve usability but cannot protect an endpoint by themselves.\u003C/p\u003E\n\u003Cp\u003EEvery protected operation must enforce authorization on the server.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022follow-least-privilege\u0022\u003EFollow Least Privilege\u003C/h3\u003E\n\u003Cp\u003EAssign only the permissions a role actually needs.\u003C/p\u003E\n\u003Cp\u003EAvoid using \u003Ccode\u003E*\u003C/code\u003E or broad prefix wildcards for ordinary users.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022protect-administrative-operations\u0022\u003EProtect Administrative Operations\u003C/h3\u003E\n\u003Cp\u003ERole creation, permission replacement, role assignment, and role deletion are privileged operations.\u003C/p\u003E\n\u003Cp\u003EThe server must authorize each mutation independently.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022treat-permission-replacement-carefully\u0022\u003ETreat Permission Replacement Carefully\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003ESetRolePermissionsAsync\u003C/code\u003E replaces the entire permission collection.\u003C/p\u003E\n\u003Cp\u003EAlways inspect the existing role configuration before submitting changes.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022handle-reauthentication-requirements\u0022\u003EHandle Reauthentication Requirements\u003C/h3\u003E\n\u003Cp\u003EAn authorization decision can indicate \u003Ccode\u003ERequiresReauthentication\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EApplications should handle this state explicitly rather than treating it as an ordinary permission denial.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022respect-tenant-boundaries\u0022\u003ERespect Tenant Boundaries\u003C/h3\u003E\n\u003Cp\u003ERoles and assignments must remain within their authorized tenant context.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022do-not-infer-effective-access-from-one-role\u0022\u003EDo Not Infer Effective Access from One Role\u003C/h3\u003E\n\u003Cp\u003EA user can have multiple roles, overlapping permissions, and additional policy restrictions.\u003C/p\u003E\n\u003Cp\u003EUse the server\u0027s authorization decision when determining whether an action is permitted.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022account-for-cached-authorization-state\u0022\u003EAccount for Cached Authorization State\u003C/h3\u003E\n\u003Cp\u003EPermission changes may not propagate instantly to every active client or cached authorization context.\u003C/p\u003E\n\u003Cp\u003EDesign sensitive operations around server-side enforcement and appropriate cache invalidation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022putting-everything-together\u0022\u003E24. Putting Everything Together\u003C/h2\u003E\n\u003Cp\u003EA typical administrative authorization workflow may look 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\u003ECreate role\n |\n v\nConfigure permissions\n |\n v\nAssign role to user\n |\n v\nUser requests protected action\n |\n v\nServer evaluates authorization\n |\n \u002B-- Allow --\u0026gt; Execute operation\n |\n \u002B-- Deny --\u0026gt; Reject operation\n |\n \u002B-- Reauthentication required\n |\n v\n Authenticate again\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUltimateAuth separates role management from authorization enforcement.\u003C/p\u003E\n\u003Cp\u003EThis allows applications to provide flexible administrative interfaces while keeping access decisions under server control.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022next-steps\u0022\u003ENext Steps\u003C/h2\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/plugin-domains/policies-access-control\u0022\u003EPolicies \u0026amp; Access Control\u003C/a\u003E \u2014 Understand application policies and access rules.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/security/authorization-pipeline-internals\u0022\u003EAuthorization Pipeline Internals\u003C/a\u003E \u2014 Explore how authorization decisions are evaluated.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/client/user-management\u0022\u003EUser Management\u003C/a\u003E \u2014 Manage user accounts and lifecycle.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/client/session-management\u0022\u003ESession Management\u003C/a\u003E \u2014 Manage authenticated sessions and devices.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/security/session-security\u0022\u003ESession Security Model\u003C/a\u003E \u2014 Understand session validity and security boundaries.\u003C/li\u003E\n\u003C/ul\u003E\n", + "Html": "\n\u003Cp\u003EUltimateAuth provides a unified client API for managing roles, assigning permissions, controlling user-role relationships, and requesting authorization decisions from the server.\u003C/p\u003E\n\u003Cp\u003EThrough \u003Ccode\u003EUAuthClient.Authorization\u003C/code\u003E, applications can build administrative authorization interfaces and check whether the current user is allowed to perform a particular action.\u003C/p\u003E\n\u003Cp\u003EAuthorization is enforced on the server. Client-side permission checks are useful for improving the user experience, but they do not replace authorization at the endpoint or application-service level.\u003C/p\u003E\n\u003Cp\u003EThis guide covers the current \u003Ccode\u003EIAuthorizationClient\u003C/code\u003E API, practical role management workflows, permission patterns, and secure client integration.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022understanding-authorization\u0022\u003E1. Understanding Authorization\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth authorization combines several concepts:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EConcept\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\u003ERoles\u003C/td\u003E\n\u003Ctd\u003ENamed collections of permissions\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EPermissions\u003C/td\u003E\n\u003Ctd\u003EFine-grained capabilities represented by normalized values\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EUser-role assignments\u003C/td\u003E\n\u003Ctd\u003EAssociations between users and roles\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EPolicies\u003C/td\u003E\n\u003Ctd\u003EServer-side rules that participate in access decisions\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAuthorization context\u003C/td\u003E\n\u003Ctd\u003EInformation about the actor, resource, action, and current security state\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EA typical authorization relationship looks like this:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EUser\n |\n \u002B-- Manager\n | |\n | \u002B-- products.read\n | \u002B-- products.create\n | \u002B-- products.update\n |\n \u002B-- Auditor\n |\n \u002B-- reports.read\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EA user can have multiple roles, and each role can contain multiple permissions.\u003C/p\u003E\n\u003Cp\u003EHowever, \u003Cstrong\u003Ehaving a permission does not automatically guarantee access\u003C/strong\u003E. The server\u0027s authorization pipeline may apply additional requirements, such as resource ownership, session validity, tenant isolation, or application policies.\u003C/p\u003E\n\u003Cp\u003EFor the internal decision model, see \u003Ca href=\u0022/docs/security/authorization-pipeline\u0022\u003EAuthorization Pipeline Internals\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022accessing-the-authorization-client\u0022\u003E2. Accessing the Authorization Client\u003C/h2\u003E\n\u003Cp\u003EInject \u003Ccode\u003EIUAuthClient\u003C/code\u003E into a Blazor component:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@using CodeBeam.UltimateAuth.Client\n@inject IUAuthClient UAuthClient\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAuthorization operations are available through:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthClient.Authorization\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis property exposes \u003Ccode\u003EIAuthorizationClient\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe interface provides three main groups of operations:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Cstrong\u003EAuthorization checks:\u003C/strong\u003E Evaluate whether the current user may perform an action.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003ERole management:\u003C/strong\u003E Create, query, rename, configure, and delete roles.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EUser-role management:\u003C/strong\u003E Retrieve, assign, and remove user roles.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThe SDK sends requests to the corresponding UltimateAuth endpoints and returns structured \u003Ccode\u003EUAuthResult\u003C/code\u003E values.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022understanding-permissions\u0022\u003E3. Understanding Permissions\u003C/h2\u003E\n\u003Cp\u003EA permission is represented by the \u003Ccode\u003EPermission\u003C/code\u003E value 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\u003Evar permission = Permission.From(\u0026quot;products.create\u0026quot;);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe current implementation normalizes permission values by trimming surrounding whitespace and converting the value to lowercase.\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 permission = Permission.From(\u0026quot; PRODUCTS.CREATE \u0026quot;);\n\nConsole.WriteLine(permission.Value);\n// products.create\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe constructor rejects null, empty, and whitespace-only values.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022permission-patterns\u0022\u003EPermission Patterns\u003C/h3\u003E\n\u003Cp\u003EUltimateAuth recognizes three important forms:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EPattern\u003C/th\u003E\n\u003Cth\u003EExample\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\u003EExact\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Eproducts.create\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EA specific permission\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EPrefix wildcard\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Eproducts.*\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPermissions within the corresponding prefix\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EGlobal wildcard\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E*\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUnrestricted permission matching within the applicable authorization rules\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003ECreate them using:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar exact = Permission.From(\u0026quot;products.create\u0026quot;);\n\nvar prefix = Permission.From(\u0026quot;products.*\u0026quot;);\n\nvar global = Permission.Wildcard;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe type also 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\u003Epermission.IsWildcard;\npermission.IsPrefix;\npermission.IsValid;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022permission-normalization\u0022\u003EPermission Normalization\u003C/h3\u003E\n\u003Cp\u003EThe following values normalize to the same representation:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EPermission.From(\u0026quot;Products.Create\u0026quot;);\nPermission.From(\u0026quot;products.create\u0026quot;);\nPermission.From(\u0026quot; PRODUCTS.CREATE \u0026quot;);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAll produce:\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\u003Eproducts.create\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUse consistent permission names across application services, policies, and role configuration.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022wildcards-are-powerful\u0022\u003EWildcards Are Powerful\u003C/h3\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\u003EPermission.From(\u0026quot;users.*\u0026quot;)\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Emay grant broad access to operations within the \u003Ccode\u003Eusers\u003C/code\u003E permission prefix.\u003C/p\u003E\n\u003Cp\u003EThe global wildcard:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EPermission.Wildcard\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Eis especially sensitive.\u003C/p\u003E\n\u003Cp\u003EAvoid assigning broad wildcard permissions merely to simplify development. Use least-privilege permissions for ordinary application roles.\u003C/p\u003E\n\u003Cp\u003EA wildcard is not a substitute for satisfying independent security policies, authentication requirements, or tenant restrictions.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022check-authorization\u0022\u003E4. Check Authorization\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003ECheckAsync\u003C/code\u003E evaluates whether the current authenticated user is authorized to perform a specific action against a resource.\u003C/p\u003E\n\u003Cp\u003EIt provides an \u003Cstrong\u003Eon-demand, server-side authorization decision\u003C/strong\u003E based on the current authentication context, permissions, roles, and applicable authorization policies.\u003C/p\u003E\n\u003Cp\u003EUnlike operations that perform protected actions, \u003Ccode\u003ECheckAsync\u003C/code\u003E only evaluates access. It does not execute the requested operation, modify permissions, grant access, or reserve authorization for a future request.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022when-to-use-checkasync\u0022\u003EWhen to Use CheckAsync\u003C/h3\u003E\n\u003Cp\u003EUse \u003Ccode\u003ECheckAsync\u003C/code\u003E when your application needs to:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EDetermine whether an authenticated user can perform an action before presenting it in the UI.\u003C/li\u003E\n\u003Cli\u003EEnable, disable, or hide controls based on the current authorization decision.\u003C/li\u003E\n\u003Cli\u003EExplain why an operation is unavailable, using the returned denial reason where appropriate.\u003C/li\u003E\n\u003Cli\u003EDetermine whether an operation requires reauthentication.\u003C/li\u003E\n\u003Cli\u003EEvaluate resource-specific access when the applicable policies support it.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EFor example, an application may check whether the current user can delete a particular order before displaying the Delete button.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022what-checkasync-does-not-guarantee\u0022\u003EWhat CheckAsync Does Not Guarantee\u003C/h3\u003E\n\u003Cp\u003E\u003Cstrong\u003EA successful authorization check is not a guarantee that a subsequent operation will succeed.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EAuthorization is evaluated against the security context available at the time of the check. That context may change before the actual operation is performed.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EAn administrator may revoke the user\u0027s permissions.\u003C/li\u003E\n\u003Cli\u003EThe user\u0027s session may expire or become invalid.\u003C/li\u003E\n\u003Cli\u003EThe target resource may be deleted or modified.\u003C/li\u003E\n\u003Cli\u003EThe resource\u0027s ownership or tenant context may change.\u003C/li\u003E\n\u003Cli\u003EAdditional business rules may prevent the operation.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThe protected operation must independently enforce authorization using its current context.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003ENever use a previous \u003Ccode\u003ECheckAsync\u003C/code\u003E result as proof of authorization for a later request.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003ECheckAsync\u003C/code\u003E is primarily a developer-experience and user-experience API. It helps applications understand and present authorization decisions without replacing the security enforcement performed by protected endpoints and application services.\u003C/p\u003E\n\u003Cp\u003EUse \u003Ccode\u003ECheckAsync\u003C/code\u003E to ask the server whether the current user is authorized to perform an action.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.CheckAsync(\n new AuthorizationCheckRequest\n {\n Action = \u0026quot;read\u0026quot;,\n Resource = \u0026quot;products\u0026quot;\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { IsAllowed: true })\n{\n // The server authorized this action.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthResult\u0026lt;AuthorizationResult\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022authorizationcheckrequest\u0022\u003EAuthorizationCheckRequest\u003C/h3\u003E\n\u003Cp\u003EThe request contains:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EType\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAction\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Estring\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERequested operation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EResource\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Estring\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EResource being accessed\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EResourceId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Estring?\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional identifier for a specific resource instance\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EBoth \u003Ccode\u003EAction\u003C/code\u003E and \u003Ccode\u003EResource\u003C/code\u003E are required.\u003C/p\u003E\n\u003Cp\u003EThe action and resource values must follow the conventions understood by your server-side authorization configuration.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022check-a-specific-resource\u0022\u003ECheck a Specific Resource\u003C/h3\u003E\n\u003Cp\u003EFor example, check access to a particular product:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 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.Authorization.CheckAsync(\n new AuthorizationCheckRequest\n {\n Action = \u0026quot;update\u0026quot;,\n Resource = \u0026quot;products\u0026quot;,\n ResourceId = productId.ToString()\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EProviding \u003Ccode\u003EResourceId\u003C/code\u003E allows the server to evaluate resource-specific rules where supported by the configured authorization pipeline.\u003C/p\u003E\n\u003Cp\u003EFor example, the application may distinguish between updating any product and updating a product owned by the current user.\u003C/p\u003E\n\u003Cp\u003EDo not assume that sending \u003Ccode\u003EResourceId\u003C/code\u003E automatically enables ownership checking. The relevant server-side policy must implement the required rule.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022understanding-authorization-results\u0022\u003E5. Understanding Authorization Results\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003ECheckAsync\u003C/code\u003E returns an \u003Ccode\u003EAuthorizationResult\u003C/code\u003E with the following properties:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIsAllowed\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EWhether the request was authorized\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERequiresReauthentication\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EWhether the decision indicates reauthentication is required\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDenyReason\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional denial reason code\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022authorization-decisions-vs.http-results\u0022\u003EAuthorization Decisions vs. HTTP Results\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003ECheckAsync\u003C/code\u003E separates the outcome of the API request from the authorization decision itself.\u003C/p\u003E\n\u003Cp\u003EThe method returns \u003Ccode\u003EUAuthResult\u0026lt;AuthorizationResult\u0026gt;\u003C/code\u003E:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003EUAuthResult.IsSuccess\u003C/code\u003E indicates whether the authorization check request completed successfully.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EAuthorizationResult.IsAllowed\u003C/code\u003E indicates whether the requested action is currently authorized.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EAuthorizationResult.RequiresReauthentication\u003C/code\u003E indicates whether additional authentication is required.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EAuthorizationResult.DenyReason\u003C/code\u003E provides an optional reason for denial.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EA denied authorization decision is a valid result of a successful authorization check.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EScenario\u003C/th\u003E\n\u003Cth\u003EHTTP Status\u003C/th\u003E\n\u003Cth\u003EAuthorization Result\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAction allowed\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E200 OK\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EIsAllowed = true\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAction denied\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E200 OK\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EIsAllowed = false\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EReauthentication required\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E200 OK\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003ERequiresReauthentication = true\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ECaller not authenticated\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E401 Unauthorized\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENo authorization decision\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EInvalid request\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E400 Bad Request\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENo authorization decision\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EServer-side evaluation failure\u003C/td\u003E\n\u003Ctd\u003EAppropriate error status\u003C/td\u003E\n\u003Ctd\u003ENo authorization decision\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Cstrong\u003EHTTP 200 means the authorization evaluation completed successfully. It does not mean access was granted.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EOnly authenticated callers can use the current \u003Ccode\u003ECheckAsync\u003C/code\u003E endpoint. However, callers do not need permission to perform the action they are checking.\u003C/p\u003E\n\u003Cp\u003EThis distinction is intentional: the purpose of \u003Ccode\u003ECheckAsync\u003C/code\u003E is to determine whether the action is permitted.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022handling-denied-decisions\u0022\u003EHandling Denied Decisions\u003C/h3\u003E\n\u003Cp\u003EApplications should distinguish between authorization denial, reauthentication requirements, and request failures.\u003C/p\u003E\n\u003Cp\u003EA denied decision may be used to disable an action or display an appropriate message.\u003C/p\u003E\n\u003Cp\u003EA reauthentication requirement may be used to initiate the application\u0027s supported reauthentication flow.\u003C/p\u003E\n\u003Cp\u003EA failed request must not be interpreted as permission to proceed.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EFail closed:\u003C/strong\u003E If the authorization decision is unavailable, treat the operation as unavailable in the UI and rely on server-side authorization when the actual operation is requested.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022handle-all-decision-outcomes\u0022\u003EHandle All Decision Outcomes\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.CheckAsync(\n new AuthorizationCheckRequest\n {\n Action = \u0026quot;delete\u0026quot;,\n Resource = \u0026quot;products\u0026quot;,\n ResourceId = productId.ToString()\n });\n\nif (!result.IsSuccess)\n{\n // The authorization request itself failed.\n}\nelse if (result.Value is { RequiresReauthentication: true })\n{\n // Ask the user to authenticate again.\n}\nelse if (result.Value is { IsAllowed: true })\n{\n // The operation is allowed.\n}\nelse\n{\n // The operation was denied.\n var reason = result.Value?.DenyReason;\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAn unsuccessful API request and an explicit authorization denial are different outcomes.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003Eresult.IsSuccess == false\u003C/code\u003E means the authorization request did not complete successfully at the API level.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003Eresult.Value.IsAllowed == false\u003C/code\u003E means the server returned a denial decision.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003Eresult.Value.RequiresReauthentication == true\u003C/code\u003E indicates that the caller needs to reestablish authentication.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003ETreat failed or unavailable authorization checks as \u003Cstrong\u003Enot authorized\u003C/strong\u003E in the UI.\u003C/p\u003E\n\u003Cp\u003ENever grant access because an authorization request failed.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022use-authorization-checks-in-blazor\u0022\u003E6. Use Authorization Checks in Blazor\u003C/h2\u003E\n\u003Cp\u003EA common use case is controlling whether an action button should be displayed.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@using CodeBeam.UltimateAuth.Client\n@using CodeBeam.UltimateAuth.Authorization.Contracts\n@inject IUAuthClient UAuthClient\n\n@if (_canCreate)\n{\n \u0026lt;button @onclick=\u0026quot;CreateProductAsync\u0026quot;\u0026gt;Create Product\u0026lt;/button\u0026gt;\n}\n\n@code {\n private bool _canCreate;\n\n protected override async Task OnInitializedAsync()\n {\n var result = await UAuthClient.Authorization.CheckAsync(\n new AuthorizationCheckRequest\n {\n Action = \u0026quot;create\u0026quot;,\n Resource = \u0026quot;products\u0026quot;\n });\n\n _canCreate = result.IsSuccess \u0026amp;\u0026amp; result.Value is { IsAllowed: true };\n }\n\n private Task CreateProductAsync()\n {\n // Invoke the protected application operation.\n return Task.CompletedTask;\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis improves the interface by hiding unavailable actions.\u003C/p\u003E\n\u003Cp\u003EHowever, the endpoint handling product creation must still enforce authorization independently.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022why-client-checks-are-not-security-boundaries\u0022\u003EWhy Client Checks Are Not Security Boundaries\u003C/h3\u003E\n\u003Cp\u003EA user can bypass a hidden button and send a request directly to an endpoint.\u003C/p\u003E\n\u003Cp\u003ETherefore:\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\u003EClient authorization check\n |\n v\nImprove the user experience\n |\n v\nUser submits an operation\n |\n v\nServer independently authorizes the operation\n |\n v\nExecute or deny\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EClient authorization checks should never be the only protection for sensitive operations.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022get-my-roles\u0022\u003E7. Get My Roles\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EGetMyRolesAsync\u003C/code\u003E to retrieve roles assigned to the current authenticated user.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.GetMyRolesAsync();\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } data)\n{\n foreach (var role in data.Roles.Items)\n {\n Console.WriteLine(role.Name);\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthResult\u0026lt;UserRolesGetResult\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022userrolesgetresult\u0022\u003EUserRolesGetResult\u003C/h3\u003E\n\u003Cp\u003EThe response contains:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserKey\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUser whose roles were requested\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERoles\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPaginated collection of role assignments\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EEach entry is a \u003Ccode\u003EUserRoleInfo\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022userroleinfo\u0022\u003EUserRoleInfo\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETenant\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETenant associated with the assignment\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserKey\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAssigned user\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERoleId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAssigned role\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERole name\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAssignedAt\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAssignment timestamp\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=\u0022pagination\u0022\u003EPagination\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.GetMyRolesAsync(\n new PageRequest\n {\n PageNumber = 1,\n PageSize = 20\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe returned role collection is paginated.\u003C/p\u003E\n\u003Cp\u003EDo not assume that the first page contains all assigned roles.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022get-another-users-roles\u0022\u003E8. Get Another User\u0027s Roles\u003C/h2\u003E\n\u003Cp\u003EAdministrative interfaces can retrieve the roles assigned to a specific user.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.GetUserRolesAsync(userKey,\n new PageRequest\n {\n PageNumber = 1,\n PageSize = 20\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } data)\n{\n foreach (var role in data.Roles.Items)\n {\n Console.WriteLine($\u0026quot;{role.Name} \u2014 {role.AssignedAt}\u0026quot;);\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis operation requires appropriate administrative authorization.\u003C/p\u003E\n\u003Cp\u003EThe \u003Ccode\u003EUserKey\u003C/code\u003E identifies the target user, not the current actor.\u003C/p\u003E\n\u003Cp\u003EThe server determines whether the current actor may inspect that user\u0027s role assignments.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022query-roles\u0022\u003E9. Query Roles\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EQueryRolesAsync\u003C/code\u003E to retrieve roles with pagination and optional filtering.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 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.Authorization.QueryRolesAsync(\n new RoleQuery\n {\n PageNumber = 1,\n PageSize = 20\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } page)\n{\n foreach (var role in page.Items)\n {\n Console.WriteLine(role.Name);\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthResult\u0026lt;PagedResult\u0026lt;RoleInfo\u0026gt;\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022rolequery\u0022\u003ERoleQuery\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003ERoleQuery\u003C/code\u003E inherits from \u003Ccode\u003EPageRequest\u003C/code\u003E and adds:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESearch\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional role-name search\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIncludeDeleted\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EInclude deleted roles when supported and authorized\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPageNumber\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERequested page\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPageSize\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERequested page size\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022search-roles\u0022\u003ESearch Roles\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.QueryRolesAsync(\n new RoleQuery\n {\n Search = \u0026quot;manager\u0026quot;,\n PageNumber = 1,\n PageSize = 10\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022include-deleted-roles\u0022\u003EInclude Deleted Roles\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.QueryRolesAsync(\n new RoleQuery\n {\n IncludeDeleted = true,\n PageNumber = 1,\n PageSize = 20\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUse \u003Ccode\u003EIncludeDeleted\u003C/code\u003E only where the application needs administrative access to deleted role records.\u003C/p\u003E\n\u003Cp\u003EThe server remains responsible for enforcing the query\u0027s permissions and lifecycle rules.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022understanding-roleinfo\u0022\u003E10. Understanding RoleInfo\u003C/h2\u003E\n\u003Cp\u003EEach role returned by the SDK is represented by \u003Ccode\u003ERoleInfo\u003C/code\u003E.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EType\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003ERoleId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERole identifier\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Estring\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERole name\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPermissions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EIReadOnlyCollection\u0026lt;Permission\u0026gt;\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAssigned permissions\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECreatedAt\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EDateTimeOffset\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECreation timestamp\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUpdatedAt\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EDateTimeOffset?\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELast update timestamp\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.QueryRolesAsync(\n new RoleQuery\n {\n PageNumber = 1,\n PageSize = 20\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } page)\n{\n foreach (var role in page.Items)\n {\n Console.WriteLine($\u0026quot;Role: {role.Name}\u0026quot;);\n\n foreach (var permission in role.Permissions)\n {\n Console.WriteLine($\u0026quot; {permission.Value}\u0026quot;);\n }\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003ERoleId\u003C/code\u003E is a strongly typed identifier rather than a plain \u003Ccode\u003EGuid\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\u003ERoleId roleId = RoleId.From(existingGuid);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUse \u003Ccode\u003ERoleId\u003C/code\u003E for role management requests.\u003C/p\u003E\n\u003Cp\u003EDo not confuse it with \u003Ccode\u003EUserKey\u003C/code\u003E or other identifier types.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022create-a-role\u0022\u003E11. Create a Role\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003ECreateRoleAsync\u003C/code\u003E to create a role.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 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.Authorization.CreateRoleAsync(\n new CreateRoleRequest\n {\n Name = \u0026quot;Manager\u0026quot;\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } role)\n{\n Console.WriteLine($\u0026quot;Created: {role.Name}\u0026quot;);\n Console.WriteLine($\u0026quot;Role ID: {role.Id}\u0026quot;);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthResult\u0026lt;RoleInfo\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ERole names must be unique within the relevant tenant according to the server\u0027s role-name rules.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022create-a-role-with-permissions\u0022\u003ECreate a Role with Permissions\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003ECreateRoleRequest\u003C/code\u003E supports an optional initial permission collection.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 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.Authorization.CreateRoleAsync(\n new CreateRoleRequest\n {\n Name = \u0026quot;ProductManager\u0026quot;,\n Permissions =\n [\n Permission.From(\u0026quot;products.read\u0026quot;),\n Permission.From(\u0026quot;products.create\u0026quot;),\n Permission.From(\u0026quot;products.update\u0026quot;)\n ]\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis allows applications to create and configure a role in one operation.\u003C/p\u003E\n\u003Cp\u003EThe server validates the requested permissions and whether the caller is authorized to create the role.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022create-a-read-only-role\u0022\u003ECreate a Read-Only Role\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.CreateRoleAsync(\n new CreateRoleRequest\n {\n Name = \u0026quot;Auditor\u0026quot;,\n Permissions =\n [\n Permission.From(\u0026quot;reports.read\u0026quot;),\n Permission.From(\u0026quot;products.read\u0026quot;)\n ]\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUse role names that describe the responsibility or capability represented by the role.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022rename-a-role\u0022\u003E12. Rename a Role\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003ERenameRoleAsync\u003C/code\u003E to change a role\u0027s name.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 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.Authorization.RenameRoleAsync(\n new RenameRoleRequest\n {\n Id = roleId,\n Name = \u0026quot;SeniorManager\u0026quot;\n });\n\nif (result.IsSuccess)\n{\n // Role renamed.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003ERenameRoleRequest\u003C/code\u003E requires:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003EId\u003C/code\u003E: The role\u0027s \u003Ccode\u003ERoleId\u003C/code\u003E.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EName\u003C/code\u003E: The new role name.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThe operation may fail if another role already uses the requested name.\u003C/p\u003E\n\u003Cp\u003ERole renaming does not mean that permissions are automatically changed.\u003C/p\u003E\n\u003Cp\u003EAfter a successful rename, the SDK publishes an \u003Ccode\u003EAuthorizationChanged\u003C/code\u003E state event.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022set-role-permissions\u0022\u003E13. Set Role Permissions\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003ESetRolePermissionsAsync\u003C/code\u003E to configure a role\u0027s permissions.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 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.Authorization.SetRolePermissionsAsync(\n new SetRolePermissionsRequest\n {\n RoleId = roleId,\n Permissions =\n [\n Permission.From(\u0026quot;products.read\u0026quot;),\n Permission.From(\u0026quot;products.create\u0026quot;),\n Permission.From(\u0026quot;products.update\u0026quot;)\n ]\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe request contains:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERoleId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERole to modify\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPermissions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENew permission collection\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=\u0022important-this-is-a-replacement-operation\u0022\u003EImportant: This Is a Replacement Operation\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003ESetRolePermissionsAsync\u003C/code\u003E replaces the role\u0027s existing permission collection.\u003C/p\u003E\n\u003Cp\u003EIt does not append permissions to the current collection.\u003C/p\u003E\n\u003Cp\u003EFor example, suppose a role currently contains:\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\u003Eproducts.read\nproducts.create\nproducts.update\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ESubmitting:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EPermissions =\n[\n Permission.From(\u0026quot;products.read\u0026quot;)\n]\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Emeans the intended new permission collection contains only:\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\u003Eproducts.read\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe other permissions are removed from the role.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022safely-add-a-permission-to-an-existing-role\u0022\u003ESafely Add a Permission to an Existing Role\u003C/h3\u003E\n\u003Cp\u003ETo preserve existing permissions, first retrieve the role and construct the complete desired collection.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 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 query = await UAuthClient.Authorization.QueryRolesAsync(\n new RoleQuery\n {\n Search = \u0026quot;ProductManager\u0026quot;,\n PageNumber = 1,\n PageSize = 50\n });\n\nif (query.IsSuccess \u0026amp;\u0026amp; query.Value is { } page)\n{\n var role = page.Items.FirstOrDefault(x =\u0026gt; x.Name == \u0026quot;ProductManager\u0026quot;);\n\n if (role is not null)\n {\n var permissions = role.Permissions.Append(Permission.From(\u0026quot;products.delete\u0026quot;)).Distinct().ToArray();\n\n var result =\n await UAuthClient.Authorization.SetRolePermissionsAsync(\n new SetRolePermissionsRequest\n {\n RoleId = role.Id,\n Permissions = permissions\n });\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis is a read-modify-write pattern, not an atomic append operation.\u003C/p\u003E\n\u003Cp\u003EIf multiple administrators edit the same role concurrently, the last successful replacement may overwrite another administrator\u0027s changes.\u003C/p\u003E\n\u003Cp\u003EApplications requiring concurrency-safe editing should implement suitable coordination or version checking at the application level.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022remove-a-permission\u0022\u003ERemove a Permission\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 updatedPermissions = role.Permissions.Where(x =\u0026gt; x.Value != \u0026quot;products.delete\u0026quot;).ToArray();\n\nvar result = await UAuthClient.Authorization.SetRolePermissionsAsync(\n new SetRolePermissionsRequest\n {\n RoleId = role.Id,\n Permissions = updatedPermissions\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAgain, submit the complete desired permission set.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022clear-all-permissions\u0022\u003EClear All Permissions\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.SetRolePermissionsAsync(\n new SetRolePermissionsRequest\n {\n RoleId = roleId,\n Permissions = []\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAn empty collection requests that the role have no permissions.\u003C/p\u003E\n\u003Cp\u003EThe server determines whether the operation is permitted.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022assign-a-role-to-a-user\u0022\u003E14. Assign a Role to a User\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EAssignRoleToUserAsync\u003C/code\u003E to assign an existing role to a user.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.AssignRoleToUserAsync(\n new AssignRoleRequest\n {\n UserKey = userKey,\n RoleName = \u0026quot;Manager\u0026quot;\n });\n\nif (result.IsSuccess)\n{\n // Role assigned.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe request contains:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserKey\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETarget user\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERoleName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EExisting role name\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe role must exist, and the current actor must be authorized to assign it.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022assign-multiple-roles\u0022\u003EAssign Multiple Roles\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\u003Eforeach (var roleName in new[] { \u0026quot;Manager\u0026quot;, \u0026quot;Auditor\u0026quot; })\n{\n var result = await UAuthClient.Authorization.AssignRoleToUserAsync(\n new AssignRoleRequest\n {\n UserKey = userKey,\n RoleName = roleName\n });\n\n if (!result.IsSuccess)\n {\n // Handle the failed assignment.\n break;\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EEach call is a separate operation.\u003C/p\u003E\n\u003Cp\u003EThis example does not provide transactional, all-or-nothing behavior across the entire collection.\u003C/p\u003E\n\u003Cp\u003EFor complex provisioning workflows, consider whether the application needs a server-side orchestration operation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022remove-a-role-from-a-user\u0022\u003E15. Remove a Role from a User\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003ERemoveRoleFromUserAsync\u003C/code\u003E:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.RemoveRoleFromUserAsync(\n new RemoveRoleRequest\n {\n UserKey = userKey,\n RoleName = \u0026quot;Manager\u0026quot;\n });\n\nif (result.IsSuccess)\n{\n // Role removed.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ERemoving a role changes the user\u0027s role assignments.\u003C/p\u003E\n\u003Cp\u003EIt does not delete the role itself.\u003C/p\u003E\n\u003Cp\u003EOther users assigned to that role are unaffected by this particular operation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022important-effective-permissions-may-remain\u0022\u003EImportant: Effective Permissions May Remain\u003C/h3\u003E\n\u003Cp\u003EA user may receive the same permission through multiple roles.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EUser\n |\n \u002B-- Manager\n | \u002B-- products.read\n |\n \u002B-- Auditor\n \u002B-- products.read\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ERemoving \u003Ccode\u003EManager\u003C/code\u003E does not necessarily remove the user\u0027s ability to read products because \u003Ccode\u003EAuditor\u003C/code\u003E still provides \u003Ccode\u003Eproducts.read\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003ETo determine whether the user may perform an action, rely on the server\u0027s authorization decision rather than inspecting one role assignment in isolation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022delete-a-role\u0022\u003E16. Delete a Role\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EDeleteRoleAsync\u003C/code\u003E to delete a role.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 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.Authorization.DeleteRoleAsync(\n new DeleteRoleRequest\n {\n Id = roleId,\n Mode = DeleteMode.Soft\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe request supports a deletion mode.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022hard-delete\u0022\u003EHard Delete\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.DeleteRoleAsync(\n new DeleteRoleRequest\n {\n Id = roleId,\n Mode = DeleteMode.Hard\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe availability and effect of each deletion mode are determined by server-side lifecycle rules.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022roledeleteresult\u0022\u003ERoleDeleteResult\u003C/h3\u003E\n\u003Cp\u003EThe method returns:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthResult\u0026lt;RoleDeleteResult\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe response contains:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERoleId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EDeleted role\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERemovedAssignments\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENumber of user-role assignments removed\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMode\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EApplied deletion mode\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDeletedAt\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EDeletion timestamp\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022inspect-the-result\u0022\u003EInspect the Result\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.DeleteRoleAsync(\n new DeleteRoleRequest\n {\n Id = roleId,\n Mode = DeleteMode.Soft\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } deleted)\n{\n Console.WriteLine($\u0026quot;Removed assignments: {deleted.RemovedAssignments}\u0026quot;);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EDeleting a role removes its assignments from users.\u003C/p\u003E\n\u003Cp\u003EThis can affect multiple users at once.\u003C/p\u003E\n\u003Cp\u003EFor that reason, role deletion should be presented as a sensitive administrative operation with appropriate confirmation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022build-a-role-management-page\u0022\u003E17. Build a Role Management Page\u003C/h2\u003E\n\u003Cp\u003EThe following Blazor example demonstrates querying roles, creating a role, and deleting a selected role.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@using CodeBeam.UltimateAuth.Client\n@using CodeBeam.UltimateAuth.Authorization.Contracts\n@using CodeBeam.UltimateAuth.Core.Contracts\n@inject IUAuthClient UAuthClient\n\n\u0026lt;h3\u0026gt;Role Management\u0026lt;/h3\u0026gt;\n\n\u0026lt;input @bind=\u0026quot;_search\u0026quot; placeholder=\u0026quot;Search roles\u0026quot; /\u0026gt;\n\n\u0026lt;button @onclick=\u0026quot;LoadRolesAsync\u0026quot;\u0026gt;Search\u0026lt;/button\u0026gt;\n\n@if (_loading)\n{\n \u0026lt;p\u0026gt;Loading roles...\u0026lt;/p\u0026gt;\n}\nelse\n{\n @foreach (var role in _roles)\n {\n \u0026lt;div @key=\u0026quot;role.Id\u0026quot;\u0026gt;\n \u0026lt;strong\u0026gt;@role.Name\u0026lt;/strong\u0026gt;\n \u0026lt;span\u0026gt;@role.Permissions.Count permissions\u0026lt;/span\u0026gt;\n \u0026lt;button @onclick=\u0026quot;() =\u0026gt; DeleteRoleAsync(role.Id)\u0026quot;\u0026gt;Delete\u0026lt;/button\u0026gt;\n \u0026lt;/div\u0026gt;\n }\n}\n\n\u0026lt;h4\u0026gt;Create Role\u0026lt;/h4\u0026gt;\n\u0026lt;input @bind=\u0026quot;_newRoleName\u0026quot; placeholder=\u0026quot;Role name\u0026quot; /\u0026gt;\n\u0026lt;button @onclick=\u0026quot;CreateRoleAsync\u0026quot; disabled=\u0026quot;@_busy\u0026quot;\u0026gt;Create\u0026lt;/button\u0026gt;\n\n@if (_message is not null)\n{\n \u0026lt;p role=\u0026quot;status\u0026quot;\u0026gt;@_message\u0026lt;/p\u0026gt;\n}\n\n@code {\n private IReadOnlyList\u0026lt;RoleInfo\u0026gt; _roles = [];\n\n private string _search = \u0026quot;\u0026quot;;\n private string _newRoleName = \u0026quot;\u0026quot;;\n private string? _message;\n\n private bool _loading = true;\n private bool _busy;\n\n protected override async Task OnInitializedAsync()\n {\n await LoadRolesAsync();\n }\n\n private async Task LoadRolesAsync()\n {\n _loading = true;\n\n try\n {\n var result = await UAuthClient.Authorization.QueryRolesAsync(\n new RoleQuery\n {\n Search = _search,\n PageNumber = 1,\n PageSize = 50\n });\n\n if (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } page)\n {\n _roles = page.Items;\n }\n else\n {\n _message = \u0026quot;Unable to load roles.\u0026quot;;\n }\n }\n catch (Exception)\n {\n _message = \u0026quot;The authorization service is unavailable.\u0026quot;;\n }\n finally\n {\n _loading = false;\n }\n }\n\n private async Task CreateRoleAsync()\n {\n if (_busy || string.IsNullOrWhiteSpace(_newRoleName))\n return;\n\n _busy = true;\n _message = null;\n\n try\n {\n var result = await UAuthClient.Authorization.CreateRoleAsync(\n new CreateRoleRequest\n {\n Name = _newRoleName\n });\n\n if (result.IsSuccess)\n {\n _newRoleName = \u0026quot;\u0026quot;;\n await LoadRolesAsync();\n }\n else\n {\n _message = \u0026quot;Unable to create role.\u0026quot;;\n }\n }\n finally\n {\n _busy = false;\n }\n }\n\n private async Task DeleteRoleAsync(RoleId roleId)\n {\n // Add a confirmation dialog before deletion in production.\n\n var result = await UAuthClient.Authorization.DeleteRoleAsync(\n new DeleteRoleRequest\n {\n Id = roleId,\n Mode = DeleteMode.Soft\n });\n\n if (result.IsSuccess)\n {\n await LoadRolesAsync();\n }\n else\n {\n _message = \u0026quot;Unable to delete role.\u0026quot;;\n }\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis example demonstrates the SDK integration rather than a complete production administration interface.\u003C/p\u003E\n\u003Cp\u003EA production implementation should include pagination, role assignment controls, permission editing, deletion confirmation, and appropriate loading and error handling for every operation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022build-a-user-role-assignment-interface\u0022\u003E18. Build a User Role Assignment Interface\u003C/h2\u003E\n\u003Cp\u003EAn administrative user details page can display assigned roles and allow administrators to add or remove them.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022load-user-roles\u0022\u003ELoad User Roles\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.GetUserRolesAsync(userKey,\n new PageRequest\n {\n PageNumber = 1,\n PageSize = 50\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } data)\n{\n var assignedRoles = data.Roles.Items.ToList();\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022assign-a-selected-role\u0022\u003EAssign a Selected Role\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.AssignRoleToUserAsync(\n new AssignRoleRequest\n {\n UserKey = userKey,\n RoleName = selectedRoleName\n });\n\nif (result.IsSuccess)\n{\n // Reload the user\u0027s assigned roles.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022remove-a-selected-role\u0022\u003ERemove a Selected Role\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.RemoveRoleFromUserAsync(\n new RemoveRoleRequest\n {\n UserKey = userKey,\n RoleName = selectedRoleName\n });\n\nif (result.IsSuccess)\n{\n // Reload the user\u0027s assigned roles.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis pattern works well alongside \u003Ccode\u003EUAuthClient.Users\u003C/code\u003E for building a unified user administration interface.\u003C/p\u003E\n\u003Cp\u003ESee \u003Ca href=\u0022/docs/client/user-management\u0022\u003EUser Management\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022authorization-state-events\u0022\u003E19. Authorization State Events\u003C/h2\u003E\n\u003Cp\u003EThe current \u003Ccode\u003EUAuthAuthorizationClient\u003C/code\u003E publishes:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthStateEvent.AuthorizationChanged\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Eafter successful authorization mutations.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EOperation\u003C/th\u003E\n\u003Cth\u003EPublishes \u003Ccode\u003EAuthorizationChanged\u003C/code\u003E\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAssignRoleToUserAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERemoveRoleFromUserAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERenameRoleAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESetRolePermissionsAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDeleteRoleAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECreateRoleAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENo\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EQueryRolesAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENo\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EGetMyRolesAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENo\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EGetUserRolesAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENo\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECheckAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENo\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe event follows the configured client state event handling mode.\u003C/p\u003E\n\u003Cp\u003EIt can be used to refresh navigation menus, role assignment views, or permission-sensitive UI.\u003C/p\u003E\n\u003Cp\u003EHowever, it is a \u003Cstrong\u003Elocal client event\u003C/strong\u003E, not a guarantee that all active clients or distributed caches have immediately observed the change.\u003C/p\u003E\n\u003Cp\u003EFor example, an administrator assigning a role to another user does not automatically mean that user\u0027s open browser receives the administrator\u0027s local event.\u003C/p\u003E\n\u003Cp\u003EApplications requiring cross-client updates should provide an appropriate synchronization mechanism.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022authorization-changes-and-active-sessions\u0022\u003E20. Authorization Changes and Active Sessions\u003C/h2\u003E\n\u003Cp\u003ERole and permission changes may affect active users.\u003C/p\u003E\n\u003Cp\u003EHowever, the timing of those changes depends on how authorization data is resolved and cached by the server.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Col\u003E\n\u003Cli\u003EAn administrator removes a role.\u003C/li\u003E\n\u003Cli\u003EThe server persists the new role assignment state.\u003C/li\u003E\n\u003Cli\u003EA subsequent authorization evaluation uses the effective authorization state available to the server.\u003C/li\u003E\n\u003C/ol\u003E\n\u003Cp\u003EIf the application caches authorization information, changes may not become visible everywhere immediately.\u003C/p\u003E\n\u003Cp\u003EDo not assume that a client event invalidates every active session or refreshes every distributed authorization cache.\u003C/p\u003E\n\u003Cp\u003EFor sensitive operations, authorization should be evaluated against sufficiently current server-side state.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022roles-are-not-sessions\u0022\u003ERoles Are Not Sessions\u003C/h3\u003E\n\u003Cp\u003ERemoving a role and revoking a session are different operations.\u003C/p\u003E\n\u003Cp\u003ERemoving a role changes authorization data.\u003C/p\u003E\n\u003Cp\u003ERevoking a session changes whether an authentication session remains valid.\u003C/p\u003E\n\u003Cp\u003EApplications may combine these operations when security policy requires immediate termination of access, but the concepts should remain separate.\u003C/p\u003E\n\u003Cp\u003ESee \u003Ca href=\u0022/docs/client/session-management\u0022\u003ESession Management\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022multi-tenant-authorization\u0022\u003E21. Multi-Tenant Authorization\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth authorization operations are scoped to the current tenant context.\u003C/p\u003E\n\u003Cp\u003ERole names are unique within the relevant tenant, and role assignments belong to the applicable tenant.\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\u003ETenant A\n |\n \u002B-- Manager\n | \u002B-- products.read\n |\n \u002B-- User A\n\nTenant B\n |\n \u002B-- Manager\n | \u002B-- reports.read\n |\n \u002B-- User B\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe same role name may represent different permission sets in different tenants.\u003C/p\u003E\n\u003Cp\u003EThe client does not gain cross-tenant access merely by providing another user\u0027s \u003Ccode\u003EUserKey\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe server must enforce tenant isolation and administrative authorization.\u003C/p\u003E\n\u003Cp\u003EWhen implementing multi-tenant administration, do not assume role names or permission configurations are globally shared.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022handling-authorization-errors\u0022\u003E22. Handling Authorization Errors\u003C/h2\u003E\n\u003Cp\u003EAuthorization methods return \u003Ccode\u003EUAuthResult\u003C/code\u003E or \u003Ccode\u003EUAuthResult\u0026lt;T\u0026gt;\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022handle-role-creation-errors\u0022\u003EHandle Role Creation Errors\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.CreateRoleAsync(\n new CreateRoleRequest\n {\n Name = \u0026quot;Manager\u0026quot;\n });\n\nif (result.IsSuccess)\n{\n // Role created.\n}\nelse if (result.Http.IsConflict)\n{\n // A conflicting role may already exist.\n}\nelse if (result.Http.IsForbidden)\n{\n // Caller lacks permission.\n}\nelse if (result.Http.IsBadRequest)\n{\n // Invalid request.\n}\nelse\n{\n // Handle another failure.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022handle-authorization-check-failures\u0022\u003EHandle Authorization Check Failures\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.CheckAsync(\n new AuthorizationCheckRequest\n {\n Action = \u0026quot;update\u0026quot;,\n Resource = \u0026quot;products\u0026quot;\n });\n\nvar allowed = result.IsSuccess \u0026amp;\u0026amp; result.Value is { IsAllowed: true };\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis fail-closed pattern is appropriate when determining whether the UI should expose a protected operation.\u003C/p\u003E\n\u003Cp\u003EUse the server\u0027s structured error information for diagnostics where appropriate.\u003C/p\u003E\n\u003Cp\u003EDo not expose sensitive policy internals unnecessarily.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-considerations\u0022\u003E23. Security Considerations\u003C/h2\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022server-authorization-is-authoritative\u0022\u003EServer Authorization Is Authoritative\u003C/h3\u003E\n\u003Cp\u003EClient-side checks improve usability but cannot protect an endpoint by themselves.\u003C/p\u003E\n\u003Cp\u003EEvery protected operation must enforce authorization on the server.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022follow-least-privilege\u0022\u003EFollow Least Privilege\u003C/h3\u003E\n\u003Cp\u003EAssign only the permissions a role actually needs.\u003C/p\u003E\n\u003Cp\u003EAvoid using \u003Ccode\u003E*\u003C/code\u003E or broad prefix wildcards for ordinary users.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022protect-administrative-operations\u0022\u003EProtect Administrative Operations\u003C/h3\u003E\n\u003Cp\u003ERole creation, permission replacement, role assignment, and role deletion are privileged operations.\u003C/p\u003E\n\u003Cp\u003EThe server must authorize each mutation independently.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022treat-permission-replacement-carefully\u0022\u003ETreat Permission Replacement Carefully\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003ESetRolePermissionsAsync\u003C/code\u003E replaces the entire permission collection.\u003C/p\u003E\n\u003Cp\u003EAlways inspect the existing role configuration before submitting changes.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022handle-reauthentication-requirements\u0022\u003EHandle Reauthentication Requirements\u003C/h3\u003E\n\u003Cp\u003EAn authorization decision can indicate \u003Ccode\u003ERequiresReauthentication\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EApplications should handle this state explicitly rather than treating it as an ordinary permission denial.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022respect-tenant-boundaries\u0022\u003ERespect Tenant Boundaries\u003C/h3\u003E\n\u003Cp\u003ERoles and assignments must remain within their authorized tenant context.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022do-not-infer-effective-access-from-one-role\u0022\u003EDo Not Infer Effective Access from One Role\u003C/h3\u003E\n\u003Cp\u003EA user can have multiple roles, overlapping permissions, and additional policy restrictions.\u003C/p\u003E\n\u003Cp\u003EUse the server\u0027s authorization decision when determining whether an action is permitted.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022account-for-cached-authorization-state\u0022\u003EAccount for Cached Authorization State\u003C/h3\u003E\n\u003Cp\u003EPermission changes may not propagate instantly to every active client or cached authorization context.\u003C/p\u003E\n\u003Cp\u003EDesign sensitive operations around server-side enforcement and appropriate cache invalidation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022putting-everything-together\u0022\u003E24. Putting Everything Together\u003C/h2\u003E\n\u003Cp\u003EA typical administrative authorization workflow may look 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\u003ECreate role\n |\n v\nConfigure permissions\n |\n v\nAssign role to user\n |\n v\nUser requests protected action\n |\n v\nServer evaluates authorization\n |\n \u002B-- Allow --\u0026gt; Execute operation\n |\n \u002B-- Deny --\u0026gt; Reject operation\n |\n \u002B-- Reauthentication required\n |\n v\n Authenticate again\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUltimateAuth separates role management from authorization enforcement.\u003C/p\u003E\n\u003Cp\u003EThis allows applications to provide flexible administrative interfaces while keeping access decisions under server control.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022next-steps\u0022\u003ENext Steps\u003C/h2\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/plugin-domains/policies-access-control\u0022\u003EPolicies \u0026amp; Access Control\u003C/a\u003E \u2014 Understand application policies and access rules.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/security/authorization-pipeline-internals\u0022\u003EAuthorization Pipeline Internals\u003C/a\u003E \u2014 Explore how authorization decisions are evaluated.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/client/user-management\u0022\u003EUser Management\u003C/a\u003E \u2014 Manage user accounts and lifecycle.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/client/session-management\u0022\u003ESession Management\u003C/a\u003E \u2014 Manage authenticated sessions and devices.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/security/session-security\u0022\u003ESession Security Model\u003C/a\u003E \u2014 Understand session validity and security boundaries.\u003C/li\u003E\n\u003C/ul\u003E\n", "Headings": [ { "Id": "understanding-authorization", @@ -38,6 +38,16 @@ "Text": "4. Check Authorization", "Level": 0 }, + { + "Id": "when-to-use-checkasync", + "Text": "When to Use CheckAsync", + "Level": 1 + }, + { + "Id": "what-checkasync-does-not-guarantee", + "Text": "What CheckAsync Does Not Guarantee", + "Level": 1 + }, { "Id": "authorizationcheckrequest", "Text": "AuthorizationCheckRequest", @@ -53,6 +63,16 @@ "Text": "5. Understanding Authorization Results", "Level": 0 }, + { + "Id": "authorization-decisions-vs.http-results", + "Text": "Authorization Decisions vs. HTTP Results", + "Level": 1 + }, + { + "Id": "handling-denied-decisions", + "Text": "Handling Denied Decisions", + "Level": 1 + }, { "Id": "handle-all-decision-outcomes", "Text": "Handle All Decision Outcomes", @@ -319,6 +339,6 @@ "Level": 0 } ], - "SourceHash": "D6F142925A628E00731B2C2A8DF9DF60EAEB3CCD27C9A3D5C9D458376FF87651", + "SourceHash": "D8570DD540FA723868466A82B7E1871265626B449FC5C1E0DD6BD02246282C53", "BuilderVersion": "1" } \ No newline at end of file diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/how-to-guides/protect-a-page.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/how-to-guides/protect-a-page.json index 2079660f..ec9014c2 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/how-to-guides/protect-a-page.json +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/how-to-guides/protect-a-page.json @@ -1,7 +1,7 @@ { "Slug": "how-to-guides/protect-a-page", "Title": "Protect a Page", - "Html": "\n\u003Cp\u003ELearn how to protect Blazor pages and UI sections using UltimateAuth and ASP.NET Core authorization.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth provides declarative authorization for routed Blazor pages through \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E, conditional rendering through \u003Ccode\u003EUAuthStateView\u003C/code\u003E, and interoperability with ASP.NET Core\u0027s \u003Ccode\u003E[Authorize]\u003C/code\u003E attribute.\u003C/p\u003E\n\u003Cp\u003EIn this guide, you\u0027ll learn how to:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003ERequire authentication before a page can be accessed.\u003C/li\u003E\n\u003Cli\u003ERestrict access using roles and permissions.\u003C/li\u003E\n\u003Cli\u003ECombine authorization requirements with different match modes.\u003C/li\u003E\n\u003Cli\u003ERequire an active session.\u003C/li\u003E\n\u003Cli\u003EProtect individual UI sections.\u003C/li\u003E\n\u003Cli\u003EUse ASP.NET Core authorization alongside UltimateAuth.\u003C/li\u003E\n\u003Cli\u003EConfigure authorization with built-in and custom routers.\u003C/li\u003E\n\u003Cli\u003EUnderstand the role of \u003Ccode\u003EUAuthApp\u003C/code\u003E and \u003Ccode\u003EUAuthComponentBase\u003C/code\u003E.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cblockquote\u003E\n\u003Cp\u003E\u003Cstrong\u003ESecurity:\u003C/strong\u003E Blazor authorization controls which UI components are rendered. Protected API endpoints, data, and server operations must enforce authorization independently on the server. Client-side authorization must never be the only security boundary.\u003C/p\u003E\n\u003C/blockquote\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022prerequisites\u0022\u003EPrerequisites\u003C/h2\u003E\n\u003Cp\u003EYou should have a working UltimateAuth Blazor application with authentication configured.\u003C/p\u003E\n\u003Cp\u003EIf you haven\u0027t created one yet, start with the \u003Ca href=\u0022/docs/getting-started/quickstart\u0022\u003EQuickStart\u003C/a\u003E.\u003C/p\u003E\n\u003Cp\u003EThe examples in this guide 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\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@using CodeBeam.UltimateAuth.Client\n@using CodeBeam.UltimateAuth.Client.Blazor\n@using CodeBeam.UltimateAuth.Core.Contracts\n@using Microsoft.AspNetCore.Authorization\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EYou can add these namespaces to your application\u0027s \u003Ccode\u003E_Imports.razor\u003C/code\u003E file to make them available across components.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022protect-a-page-with-uauthauthorize\u0022\u003E1. Protect a Page with UAuthAuthorize\u003C/h2\u003E\n\u003Cp\u003EThe simplest way to protect a routed Blazor page is to add the \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E attribute.\u003C/p\u003E\n\u003Cp\u003ECreate \u003Ccode\u003EPages/Dashboard.razor\u003C/code\u003E:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/dashboard\u0026quot;\n@attribute [UAuthAuthorize]\n\n\u0026lt;h1\u0026gt;Dashboard\u0026lt;/h1\u0026gt;\n\n\u0026lt;p\u0026gt;You are authenticated.\u0026lt;/p\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThat\u0027s it.\u003C/p\u003E\n\u003Cp\u003EWhen a user navigates to \u003Ccode\u003E/dashboard\u003C/code\u003E, UltimateAuth evaluates the page\u0027s authorization requirements before rendering the page component.\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Cstrong\u003EAuthenticated user with an acceptable session state:\u003C/strong\u003E The page can be rendered.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EAnonymous user:\u003C/strong\u003E Authorization is denied.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EInactive session:\u003C/strong\u003E Authorization is denied when an inactive session state is available and \u003Ccode\u003ERequireActive\u003C/code\u003E is enabled.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EBy default, \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E requires an authenticated user and an active session where session-state information is present.\u003C/p\u003E\n\u003Cp\u003ENo explicit role or permission is required.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022does-the-page-need-to-inherit-from-uauthcomponentbase\u0022\u003EDoes the page need to inherit from UAuthComponentBase?\u003C/h3\u003E\n\u003Cp\u003E\u003Cstrong\u003ENo.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EThe page above works without inheriting from any UltimateAuth base class.\u003C/p\u003E\n\u003Cp\u003EAuthorization is enforced by \u003Ccode\u003EUAuthAuthorizeRouteView\u003C/code\u003E, not by the page\u0027s base class.\u003C/p\u003E\n\u003Cp\u003EThis also means an unauthorized routed page is not instantiated by UltimateAuth\u0027s route view.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022require-a-role\u0022\u003E2. Require a Role\u003C/h2\u003E\n\u003Cp\u003EUse the \u003Ccode\u003ERoles\u003C/code\u003E property to restrict access to users with a specific role.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/admin\u0026quot;\n@attribute [UAuthAuthorize(Roles = \u0026quot;Admin\u0026quot;)]\n\n\u0026lt;h1\u0026gt;Administration\u0026lt;/h1\u0026gt;\n\n\u0026lt;p\u0026gt;Only administrators can access this page.\u0026lt;/p\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe user must be authenticated and have the \u003Ccode\u003EAdmin\u003C/code\u003E role.\u003C/p\u003E\n\u003Cp\u003EYou can also declare multiple roles:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/management\u0026quot;\n@attribute [UAuthAuthorize(Roles = \u0026quot;Admin,Manager\u0026quot;)]\n\n\u0026lt;h1\u0026gt;Management\u0026lt;/h1\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWith the default \u003Ccode\u003ECategory\u003C/code\u003E match mode, having \u003Cstrong\u003Eeither \u003Ccode\u003EAdmin\u003C/code\u003E or \u003Ccode\u003EManager\u003C/code\u003E\u003C/strong\u003E is sufficient, provided the other configured authorization categories also succeed.\u003C/p\u003E\n\u003Cp\u003ERole names must correspond to the roles recognized by UltimateAuth\u0027s authentication state.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022require-a-permission\u0022\u003E3. Require a Permission\u003C/h2\u003E\n\u003Cp\u003EFor more granular authorization, use \u003Ccode\u003EPermissions\u003C/code\u003E.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/users\u0026quot;\n@attribute [UAuthAuthorize(Permissions = \u0026quot;users.read.self\u0026quot;)]\n\n\u0026lt;h1\u0026gt;Users\u0026lt;/h1\u0026gt;\n\n\u0026lt;p\u0026gt;You have permission to view users.\u0026lt;/p\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUltimateAuth checks the requested permission against the current \u003Ccode\u003EUAuthState\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EYou can declare multiple permissions:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/reports\u0026quot;\n@attribute [UAuthAuthorize(Permissions = \u0026quot;reports.read.self,reports.manage.admin\u0026quot;)]\n \n\u0026lt;h1\u0026gt;Reports\u0026lt;/h1\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EBy default, the user needs at least one of these permissions.\u003C/p\u003E\n\u003Cp\u003EPermission strings must match the permissions represented in the authenticated user\u0027s UltimateAuth state.\u003C/p\u003E\n\u003Cblockquote\u003E\n\u003Cp\u003E\u003Cstrong\u003ETip:\u003C/strong\u003E Prefer permissions for operations such as reading reports, managing users, or editing records. Roles are useful for broader organizational responsibilities, while permissions can express specific capabilities.\u003C/p\u003E\n\u003C/blockquote\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022combine-roles-and-permissions\u0022\u003E4. Combine Roles and Permissions\u003C/h2\u003E\n\u003Cp\u003EYou can specify both \u003Ccode\u003ERoles\u003C/code\u003E and \u003Ccode\u003EPermissions\u003C/code\u003E.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/administration/users\u0026quot;\n@attribute [UAuthAuthorize(Roles = \u0026quot;Admin\u0026quot;, Permissions = \u0026quot;users.manage.admin\u0026quot;)]\n\n\u0026lt;h1\u0026gt;User Management\u0026lt;/h1\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWith the default \u003Ccode\u003EMatchMode = Category\u003C/code\u003E, the user must satisfy:\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EAdmin role AND users.manage permission\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EThis is because \u003Ccode\u003ECategory\u003C/code\u003E requires at least one match from each configured category.\u003C/p\u003E\n\u003Cp\u003EIf multiple roles and permissions are provided:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/management/users\u0026quot;\n@attribute [UAuthAuthorize(Roles = \u0026quot;Admin,Manager\u0026quot;, Permissions = \u0026quot;users.read.self,users.manage.admin\u0026quot;)]\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe requirement becomes:\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003E(Admin OR Manager) AND (users.read.self OR users.manage.admin)\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EThis is the default authorization behavior.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022understand-match-modes\u0022\u003E5. Understand Match Modes\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth supports three authorization matching modes:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EMatch Mode\u003C/th\u003E\n\u003Cth\u003EBehavior\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECategory\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAt least one matching value in each configured category\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAny\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAt least one configured role or permission must match\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAll\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEvery configured role and permission must match\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe default is \u003Ccode\u003EAuthorizationMatchMode.Category\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022category-the-default\u0022\u003ECategory \u2014 the default\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/category-example\u0026quot;\n@attribute [UAuthAuthorize(Roles = \u0026quot;Admin,Manager\u0026quot;, Permissions = \u0026quot;reports.read,reports.write\u0026quot;, MatchMode = AuthorizationMatchMode.Category)]\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe user must have:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003EAdmin\u003C/code\u003E \u003Cstrong\u003Eor\u003C/strong\u003E \u003Ccode\u003EManager\u003C/code\u003E.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003Ereports.read\u003C/code\u003E \u003Cstrong\u003Eor\u003C/strong\u003E \u003Ccode\u003Ereports.write\u003C/code\u003E.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EBoth configured categories must succeed.\u003C/p\u003E\n\u003Cp\u003EUse \u003Ccode\u003ECategory\u003C/code\u003E when roles and permissions represent independent requirements but alternatives within each category are acceptable.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022any-one-match-is-enough\u0022\u003EAny \u2014 one match is enough\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/any-example\u0026quot;\n@attribute [UAuthAuthorize(Roles = \u0026quot;Admin,Auditor\u0026quot;, Permissions = \u0026quot;reports.read.admin\u0026quot;, MatchMode = AuthorizationMatchMode.Any)]\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe user needs at least one of:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003EAdmin\u003C/code\u003E role.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EAuditor\u003C/code\u003E role.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003Ereports.read.admin\u003C/code\u003E permission.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThis is useful for pages with several alternative authorization paths.\u003C/p\u003E\n\u003Cp\u003EBe careful when combining high-privilege roles and lower-privilege permissions with \u003Ccode\u003EAny\u003C/code\u003E: a single matching permission grants access.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022all-every-value-is-required\u0022\u003EAll \u2014 every value is required\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/all-example\u0026quot;\n@attribute [UAuthAuthorize(Roles = \u0026quot;Admin,SecurityOfficer\u0026quot;, Permissions = \u0026quot;audit.read.self,audit.export.admin\u0026quot;, MatchMode = AuthorizationMatchMode.All)]\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe user must have:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003EAdmin\u003C/code\u003E role.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003ESecurityOfficer\u003C/code\u003E role.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003Eaudit.read.self\u003C/code\u003E permission.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003Eaudit.export.admin\u003C/code\u003E permission.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EUse \u003Ccode\u003EAll\u003C/code\u003E when every declared requirement is mandatory.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022choosing-a-match-mode\u0022\u003EChoosing a match mode\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003ERequirement\u003C/th\u003E\n\u003Cth\u003ERecommended mode\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAny authenticated user\u003C/td\u003E\n\u003Ctd\u003EParameterless attribute\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EOne required role\u003C/td\u003E\n\u003Ctd\u003EDefault \u003Ccode\u003ECategory\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAny of several roles\u003C/td\u003E\n\u003Ctd\u003EDefault \u003Ccode\u003ECategory\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EOne role AND one permission\u003C/td\u003E\n\u003Ctd\u003EDefault \u003Ccode\u003ECategory\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ERole OR permission\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EAny\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAll listed roles AND permissions\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EAll\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EIf roles and permissions are omitted entirely, an authenticated user satisfies the authorization requirement regardless of match mode, subject to the session-state requirement.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022require-an-active-session\u0022\u003E6. Require an Active Session\u003C/h2\u003E\n\u003Cp\u003EBy default, \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E uses:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003ERequireActive = true\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe default configuration can be written explicitly:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/account/settings\u0026quot;\n@attribute [UAuthAuthorize(RequireActive = true)]\n\n\u0026lt;h1\u0026gt;Account Settings\u0026lt;/h1\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWhen UltimateAuth has session-state information and the session is not active, authorization returns an inactive result.\u003C/p\u003E\n\u003Cp\u003EIf your application needs to allow an authenticated user regardless of a reported non-active session state, you can disable this requirement:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/session-information\u0026quot;\n@attribute [UAuthAuthorize(RequireActive = false)]\n\n\u0026lt;h1\u0026gt;Session Information\u0026lt;/h1\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis changes only the session-state condition. Authentication is still required.\u003C/p\u003E\n\u003Cblockquote\u003E\n\u003Cp\u003E\u003Cstrong\u003EImportant:\u003C/strong\u003E \u003Ccode\u003ERequireActive\u003C/code\u003E evaluates the session state currently available to the Blazor client. It does not independently validate a session against the server for every render. Server-side session validation and revocation enforcement remain necessary.\u003C/p\u003E\n\u003C/blockquote\u003E\n\u003Cp\u003EWhen no session-state value is available, the default evaluator does not reject the user solely because that value is absent.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022protect-part-of-a-page-with-uauthstateview\u0022\u003E7. Protect Part of a Page with UAuthStateView\u003C/h2\u003E\n\u003Cp\u003ESometimes you want the page to remain publicly accessible while protecting only a specific section.\u003C/p\u003E\n\u003Cp\u003EUse \u003Ccode\u003EUAuthStateView\u003C/code\u003E for conditional rendering.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/products\u0026quot;\n\n\u0026lt;h1\u0026gt;Products\u0026lt;/h1\u0026gt;\n\n\u0026lt;p\u0026gt;Everyone can view this page.\u0026lt;/p\u0026gt;\n\n\u0026lt;UAuthStateView Roles=\u0026quot;Admin\u0026quot;\u0026gt;\n \u0026lt;Authorized\u0026gt;\n \u0026lt;button\u0026gt;Edit Products\u0026lt;/button\u0026gt;\n \u0026lt;/Authorized\u0026gt;\n \u0026lt;NotAuthorized\u0026gt;\n \u0026lt;p\u0026gt;Administrative tools are unavailable.\u0026lt;/p\u0026gt;\n \u0026lt;/NotAuthorized\u0026gt;\n\u0026lt;/UAuthStateView\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe page itself remains accessible to everyone.\u003C/p\u003E\n\u003Cp\u003EHowever, the administrative section is only rendered for an authorized user.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthStateView\u003C/code\u003E uses the same \u003Ccode\u003EIUAuthAuthorizationEvaluator\u003C/code\u003E as \u003Ccode\u003EUAuthAuthorizeRouteView\u003C/code\u003E, so matching roles, permissions, and match modes have consistent semantics.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022basic-authentication-only-section\u0022\u003EBasic authentication-only section\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E\u0026lt;UAuthStateView\u0026gt;\n \u0026lt;Authorized\u0026gt;\n \u0026lt;p\u0026gt;Welcome back!\u0026lt;/p\u0026gt;\n \u0026lt;/Authorized\u0026gt;\n \u0026lt;NotAuthorized\u0026gt;\n \u0026lt;p\u0026gt;Sign in to access your account features.\u0026lt;/p\u0026gt;\n \u0026lt;/NotAuthorized\u0026gt;\n\u0026lt;/UAuthStateView\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAs with parameterless \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E, authentication is required, along with an acceptable session state when \u003Ccode\u003ERequireActive\u003C/code\u003E is true.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022protect-a-section-with-permissions\u0022\u003EProtect a section with permissions\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E\u0026lt;UAuthStateView Permissions=\u0026quot;orders.refund.self\u0026quot;\u0026gt;\n \u0026lt;Authorized\u0026gt;\n \u0026lt;button\u0026gt;Refund Order\u0026lt;/button\u0026gt;\n \u0026lt;/Authorized\u0026gt;\n \u0026lt;NotAuthorized\u0026gt;\n \u0026lt;p\u0026gt;You cannot refund orders.\u0026lt;/p\u0026gt;\n \u0026lt;/NotAuthorized\u0026gt;\n\u0026lt;/UAuthStateView\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022combine-requirements-in-a-section\u0022\u003ECombine requirements in a section\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E\u0026lt;UAuthStateView Roles=\u0026quot;Admin,Manager\u0026quot; Permissions=\u0026quot;orders.read.self,orders.manage.admin\u0026quot; MatchMode=\u0026quot;AuthorizationMatchMode.Category\u0026quot;\u0026gt;\n \u0026lt;Authorized\u0026gt;\n \u0026lt;p\u0026gt;Order management tools are available.\u0026lt;/p\u0026gt;\n \u0026lt;/Authorized\u0026gt;\n \u0026lt;NotAuthorized\u0026gt;\n \u0026lt;p\u0026gt;Access denied.\u0026lt;/p\u0026gt;\n \u0026lt;/NotAuthorized\u0026gt;\n\u0026lt;/UAuthStateView\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis uses the same \u003Ccode\u003ECategory\u003C/code\u003E semantics as the routed page attribute.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022access-the-current-authentication-state\u0022\u003EAccess the current authentication state\u003C/h3\u003E\n\u003Cp\u003EThe \u003Ccode\u003EAuthorized\u003C/code\u003E fragment receives the current \u003Ccode\u003EUAuthState\u003C/code\u003E as its context.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E\u0026lt;UAuthStateView\u0026gt;\n \u0026lt;Authorized Context=\u0026quot;state\u0026quot;\u0026gt;\n \u0026lt;p\u0026gt;Hello, @state.Identity?.DisplayName!\u0026lt;/p\u0026gt;\n \u0026lt;/Authorized\u0026gt;\n \u0026lt;NotAuthorized\u0026gt;\n \u0026lt;p\u0026gt;Please sign in.\u0026lt;/p\u0026gt;\n \u0026lt;/NotAuthorized\u0026gt;\n\u0026lt;/UAuthStateView\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis allows authorized content to access identity information without requiring a custom base class.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022handle-unauthorized-inactive-and-authorizing-states\u0022\u003E8. Handle Unauthorized, Inactive, and Authorizing States\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthStateView\u003C/code\u003E supports multiple rendering states.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E\u0026lt;UAuthStateView Roles=\u0026quot;Admin\u0026quot;\n RequireActive=\u0026quot;true\u0026quot;\u0026gt;\n \u0026lt;Authorized\u0026gt;\n \u0026lt;p\u0026gt;Administrator access granted.\u0026lt;/p\u0026gt;\n \u0026lt;/Authorized\u0026gt;\n\n \u0026lt;NotAuthorized\u0026gt;\n \u0026lt;p\u0026gt;You do not have permission to access this section.\u0026lt;/p\u0026gt;\n \u0026lt;/NotAuthorized\u0026gt;\n\n \u0026lt;Inactive Context=\u0026quot;state\u0026quot;\u0026gt;\n \u0026lt;p\u0026gt;Your session is not active.\u0026lt;/p\u0026gt;\n \u0026lt;/Inactive\u0026gt;\n\n \u0026lt;Authorizing\u0026gt;\n \u0026lt;p\u0026gt;Checking your permissions...\u0026lt;/p\u0026gt;\n \u0026lt;/Authorizing\u0026gt;\n\u0026lt;/UAuthStateView\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EFragment\u003C/th\u003E\n\u003Cth\u003EWhen displayed\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAuthorized\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthorization succeeds\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ENotAuthorized\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthorization is denied\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EInactive\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EA non-active session is detected and \u003Ccode\u003ERequireActive\u003C/code\u003E is true\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAuthorizing\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthorization evaluation is in progress\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EIf \u003Ccode\u003EInactive\u003C/code\u003E is not supplied, the component falls back to the \u003Ccode\u003ENotAuthorized\u003C/code\u003E fragment.\u003C/p\u003E\n\u003Cp\u003EIf \u003Ccode\u003EAuthorizing\u003C/code\u003E is not supplied, unauthorized content is used while a new authorization result is pending.\u003C/p\u003E\n\u003Cp\u003EDuring reevaluation, previously authorized section content is removed before an asynchronous policy evaluation completes. This prevents stale authorized content from remaining visible during that pending evaluation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022use-an-asp.net-core-authorization-policy\u0022\u003E9. Use an ASP.NET Core Authorization Policy\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthStateView\u003C/code\u003E supports the \u003Ccode\u003EPolicy\u003C/code\u003E parameter.\u003C/p\u003E\n\u003Cp\u003EFirst, register a policy using ASP.NET Core authorization:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddAuthorizationCore(options =\u0026gt;\n{\n options.AddPolicy(\u0026quot;CanViewReports\u0026quot;, policy =\u0026gt; policy.RequireClaim(\u0026quot;reports_access\u0026quot;, \u0026quot;granted\u0026quot;));\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThen use that policy in a UI 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\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E\u0026lt;UAuthStateView Policy=\u0026quot;CanViewReports\u0026quot;\u0026gt;\n \u0026lt;Authorized\u0026gt;\n \u0026lt;p\u0026gt;Reports are available.\u0026lt;/p\u0026gt;\n \u0026lt;/Authorized\u0026gt;\n \u0026lt;NotAuthorized\u0026gt;\n \u0026lt;p\u0026gt;Reports access is restricted.\u0026lt;/p\u0026gt;\n \u0026lt;/NotAuthorized\u0026gt;\n\u0026lt;/UAuthStateView\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe policy is evaluated through ASP.NET Core\u0027s \u003Ccode\u003EIAuthorizationService\u003C/code\u003E, using the claims principal produced from the current UltimateAuth state.\u003C/p\u003E\n\u003Cp\u003EYou can also combine a policy with roles and permissions:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E\u0026lt;UAuthStateView Roles=\u0026quot;Admin\u0026quot; Permissions=\u0026quot;reports.read\u0026quot; Policy=\u0026quot;CanViewReports\u0026quot; MatchMode=\u0026quot;AuthorizationMatchMode.All\u0026quot;\u0026gt;\n \u0026lt;Authorized\u0026gt;\n \u0026lt;p\u0026gt;All requirements passed.\u0026lt;/p\u0026gt;\n \u0026lt;/Authorized\u0026gt;\n\u0026lt;/UAuthStateView\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EFor \u003Ccode\u003EAll\u003C/code\u003E, every configured requirement must succeed.\u003C/p\u003E\n\u003Cp\u003EFor \u003Ccode\u003EAny\u003C/code\u003E, the policy is another possible successful condition.\u003C/p\u003E\n\u003Cp\u003EFor \u003Ccode\u003ECategory\u003C/code\u003E, the policy must succeed alongside the configured role and permission categories.\u003C/p\u003E\n\u003Cblockquote\u003E\n\u003Cp\u003E\u003Cstrong\u003ECurrent API distinction:\u003C/strong\u003E \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E currently exposes \u003Ccode\u003ERoles\u003C/code\u003E, \u003Ccode\u003EPermissions\u003C/code\u003E, \u003Ccode\u003EMatchMode\u003C/code\u003E, and \u003Ccode\u003ERequireActive\u003C/code\u003E, but not \u003Ccode\u003EPolicy\u003C/code\u003E. Use \u003Ccode\u003EUAuthStateView\u003C/code\u003E for policy-based section rendering, or ASP.NET Core \u003Ccode\u003E[Authorize(Policy = \u0026quot;...\u0026quot;)]\u003C/code\u003E for policy-protected routed pages.\u003C/p\u003E\n\u003C/blockquote\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022protect-a-page-with-asp.net-core-authorize\u0022\u003E10. Protect a Page with ASP.NET Core Authorize\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth integrates with ASP.NET Core authorization.\u003C/p\u003E\n\u003Cp\u003EYou can use the standard \u003Ccode\u003E[Authorize]\u003C/code\u003E attribute on routed Blazor pages.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/account\u0026quot;\n@attribute [Authorize]\n\n\u0026lt;h1\u0026gt;Account\u0026lt;/h1\u0026gt;\n\n\u0026lt;p\u0026gt;This page requires ASP.NET Core authorization.\u0026lt;/p\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EOr require a role:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/administration\u0026quot;\n@attribute [Authorize(Roles = \u0026quot;Admin\u0026quot;)]\n\n\u0026lt;h1\u0026gt;Administration\u0026lt;/h1\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EOr a registered policy:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/reports\u0026quot;\n@attribute [Authorize(Policy = \u0026quot;CanViewReports\u0026quot;)]\n\n\u0026lt;h1\u0026gt;Reports\u0026lt;/h1\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThese checks are performed through ASP.NET Core\u0027s \u003Ccode\u003EAuthorizeRouteView\u003C/code\u003E and its authentication and authorization infrastructure.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth provides a Blazor \u003Ccode\u003EAuthenticationStateProvider\u003C/code\u003E that integrates UltimateAuth authentication state with ASP.NET Core.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022which-attribute-should-i-use\u0022\u003EWhich attribute should I use?\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EFeature\u003C/th\u003E\n\u003Cth\u003E\u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E\u003C/th\u003E\n\u003Cth\u003E\u003Ccode\u003E[Authorize]\u003C/code\u003E\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ERequires authentication\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ERoles\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EUltimateAuth permissions\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003Ctd\u003EVia separately configured ASP.NET Core policies or handlers\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAuthorizationMatchMode\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003Ctd\u003ENo direct equivalent property\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERequireActive\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003Ctd\u003ENo direct equivalent property\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ENamed ASP.NET Core policy\u003C/td\u003E\n\u003Ctd\u003ENot currently\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ERouted Blazor page\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EUse \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E when working directly with UltimateAuth roles, permissions, and session-aware requirements.\u003C/p\u003E\n\u003Cp\u003EUse \u003Ccode\u003E[Authorize]\u003C/code\u003E when relying on ASP.NET Core authorization policies, requirements, or existing authorization infrastructure.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022combine-both-attributes\u0022\u003ECombine both attributes\u003C/h3\u003E\n\u003Cp\u003EYou can declare both on the same routed page:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/secure-reports\u0026quot;\n\n@attribute [UAuthAuthorize(Permissions = \u0026quot;reports.read\u0026quot;)]\n@attribute [Authorize(Roles = \u0026quot;Admin\u0026quot;)]\n\n\u0026lt;h1\u0026gt;Secure Reports\u0026lt;/h1\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe page is rendered only when \u003Cstrong\u003Eboth authorization systems allow access\u003C/strong\u003E.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth evaluates \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E, and the underlying ASP.NET Core \u003Ccode\u003EAuthorizeRouteView\u003C/code\u003E evaluates \u003Ccode\u003E[Authorize]\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe requirements are effectively combined using AND semantics across the two mechanisms.\u003C/p\u003E\n\u003Cblockquote\u003E\n\u003Cp\u003E\u003Cstrong\u003ENote:\u003C/strong\u003E ASP.NET Core \u003Ccode\u003E[Authorize]\u003C/code\u003E uses the configured ASP.NET Core authentication state. \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E uses \u003Ccode\u003EUAuthState\u003C/code\u003E. UltimateAuth integrates these models, but custom authentication providers must keep their states consistent.\u003C/p\u003E\n\u003C/blockquote\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022how-uauthapp-protects-routes\u0022\u003E11. How UAuthApp Protects Routes\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthApp\u003C/code\u003E is the root Blazor integration component for UltimateAuth.\u003C/p\u003E\n\u003Cp\u003EIt provides:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EClient runtime initialization.\u003C/li\u003E\n\u003Cli\u003EThe current \u003Ccode\u003EUAuthState\u003C/code\u003E as a cascading parameter.\u003C/li\u003E\n\u003Cli\u003EASP.NET Core cascading authentication state.\u003C/li\u003E\n\u003Cli\u003ESession lifecycle coordination.\u003C/li\u003E\n\u003Cli\u003ERuntime initialization readiness.\u003C/li\u003E\n\u003Cli\u003EOptional built-in routing.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022built-in-router\u0022\u003EBuilt-in router\u003C/h3\u003E\n\u003Cp\u003EIf you 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\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E\u0026lt;UAuthApp UseBuiltInRouter=\u0026quot;true\u0026quot; AppAssembly=\u0026quot;typeof(Program).Assembly\u0026quot; DefaultLayout=\u0026quot;typeof(Layout.MainLayout)\u0026quot; /\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUltimateAuth automatically configures its router to use \u003Ccode\u003EUAuthAuthorizeRouteView\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EYou do not need to add a separate \u003Ccode\u003EAuthorizeRouteView\u003C/code\u003E or \u003Ccode\u003EUAuthAuthorizeRouteView\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe built-in router supports both \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E and ASP.NET Core \u003Ccode\u003E[Authorize]\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EIt also supports an application-level \u003Ccode\u003ENotAuthorized\u003C/code\u003E fragment:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E\u0026lt;UAuthApp UseBuiltInRouter=\u0026quot;true\u0026quot;\n AppAssembly=\u0026quot;typeof(Program).Assembly\u0026quot;\n DefaultLayout=\u0026quot;typeof(Layout.MainLayout)\u0026quot;\u0026gt;\n \u0026lt;NotAuthorized\u0026gt;\n \u0026lt;h1\u0026gt;Access Denied\u0026lt;/h1\u0026gt;\n \u0026lt;p\u0026gt;You are not authorized to access this page.\u0026lt;/p\u0026gt;\n \u0026lt;/NotAuthorized\u0026gt;\n\u0026lt;/UAuthApp\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis fragment is displayed when route authorization fails.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022runtime-initialization\u0022\u003ERuntime initialization\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthApp\u003C/code\u003E initializes the client authentication state before allowing UltimateAuth-protected routes to be rendered.\u003C/p\u003E\n\u003Cp\u003EThis is important because authentication state may not be immediately available when a Blazor application first starts, particularly when restoring a session.\u003C/p\u003E\n\u003Cp\u003EDuring initialization or asynchronous authorization evaluation, \u003Ccode\u003EUAuthAuthorizeRouteView\u003C/code\u003E can display an \u003Ccode\u003EAuthorizing\u003C/code\u003E fragment.\u003C/p\u003E\n\u003Cp\u003EIf one is not supplied, the protected route content remains unrendered while authorization is pending.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022manual-and-reactive-render-modes\u0022\u003EManual and Reactive render modes\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthApp\u003C/code\u003E supports \u003Ccode\u003EUAuthRenderMode.Manual\u003C/code\u003E and \u003Ccode\u003EUAuthRenderMode.Reactive\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThese control how UltimateAuth state changes affect general component rendering.\u003C/p\u003E\n\u003Cp\u003ERoute authorization itself subscribes to relevant authentication state changes, and \u003Ccode\u003EUAuthStateView\u003C/code\u003E also reacts to such changes.\u003C/p\u003E\n\u003Cp\u003EYou do not need to enable reactive rendering merely to make \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E work.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022protect-pages-with-a-custom-router\u0022\u003E12. Protect Pages with a Custom Router\u003C/h2\u003E\n\u003Cp\u003EIf your application uses its own Blazor \u003Ccode\u003ERouter\u003C/code\u003E, set \u003Ccode\u003EUseBuiltInRouter=\u0026quot;false\u0026quot;\u003C/code\u003E on \u003Ccode\u003EUAuthApp\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThen use \u003Ccode\u003EUAuthAuthorizeRouteView\u003C/code\u003E inside the router\u0027s \u003Ccode\u003EFound\u003C/code\u003E fragment.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E\u0026lt;UAuthApp UseBuiltInRouter=\u0026quot;false\u0026quot;\u0026gt;\n \u0026lt;Router AppAssembly=\u0026quot;typeof(Program).Assembly\u0026quot;\u0026gt;\n \u0026lt;Found Context=\u0026quot;routeData\u0026quot;\u0026gt;\n \u0026lt;UAuthAuthorizeRouteView RouteData=\u0026quot;routeData\u0026quot; DefaultLayout=\u0026quot;typeof(Layout.MainLayout)\u0026quot;\u0026gt;\n\n \u0026lt;NotAuthorized\u0026gt;\n \u0026lt;h1\u0026gt;Access Denied\u0026lt;/h1\u0026gt;\n \u0026lt;p\u0026gt;You cannot access this page.\u0026lt;/p\u0026gt;\n \u0026lt;/NotAuthorized\u0026gt;\n\n \u0026lt;Authorizing\u0026gt;\n \u0026lt;p\u0026gt;Checking access...\u0026lt;/p\u0026gt;\n \u0026lt;/Authorizing\u0026gt;\n\n \u0026lt;/UAuthAuthorizeRouteView\u0026gt;\n\n \u0026lt;FocusOnNavigate RouteData=\u0026quot;routeData\u0026quot; Selector=\u0026quot;h1\u0026quot; /\u0026gt;\n \u0026lt;/Found\u0026gt;\n \u0026lt;/Router\u0026gt;\n\u0026lt;/UAuthApp\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthAuthorizeRouteView\u003C/code\u003E wraps ASP.NET Core\u0027s \u003Ccode\u003EAuthorizeRouteView\u003C/code\u003E internally.\u003C/p\u003E\n\u003Cp\u003EThis means it supports both authorization systems, while checking UltimateAuth-specific requirements before the protected page component is instantiated.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EDo not replace it with a plain \u003Ccode\u003ERouteView\u003C/code\u003E or \u003Ccode\u003EAuthorizeRouteView\u003C/code\u003E when your routed pages depend on \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EA plain ASP.NET Core \u003Ccode\u003EAuthorizeRouteView\u003C/code\u003E does not evaluate the UltimateAuth-specific attribute by itself.\u003C/p\u003E\n\u003Cp\u003EIf your custom router uses additional route assemblies, pass the appropriate \u003Ccode\u003EAdditionalAssemblies\u003C/code\u003E to the \u003Ccode\u003ERouter\u003C/code\u003E as needed.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022do-i-need-uauthcomponentbase\u0022\u003E13. Do I Need UAuthComponentBase?\u003C/h2\u003E\n\u003Cp\u003E\u003Cstrong\u003ENot for authorization.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EConsider this page:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/settings\u0026quot;\n@attribute [UAuthAuthorize]\n\n\u0026lt;h1\u0026gt;Settings\u0026lt;/h1\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EIt is protected when routed through \u003Ccode\u003EUAuthAuthorizeRouteView\u003C/code\u003E, even though it doesn\u0027t inherit from \u003Ccode\u003EUAuthComponentBase\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EYou can still use \u003Ccode\u003EUAuthComponentBase\u003C/code\u003E when you need access to the UltimateAuth state lifecycle.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/profile\u0026quot;\n@attribute [UAuthAuthorize]\n@inherits UAuthComponentBase\n\n\u0026lt;h1\u0026gt;Profile\u0026lt;/h1\u0026gt;\n\n\u0026lt;p\u0026gt;User: @AuthState.Identity?.DisplayName\u0026lt;/p\u0026gt;\n\n@code {\n protected override Task HandleAuthStateChangedAsync(\n UAuthStateChangeReason reason)\n {\n // React to authentication state changes.\n return Task.CompletedTask;\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthComponentBase\u003C/code\u003E provides:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EAccess to cascading \u003Ccode\u003EUAuthState\u003C/code\u003E.\u003C/li\u003E\n\u003Cli\u003EAuthentication state change notifications.\u003C/li\u003E\n\u003Cli\u003EAutomatic rerendering when the state changes, by default.\u003C/li\u003E\n\u003Cli\u003EAn asynchronous lifecycle hook.\u003C/li\u003E\n\u003Cli\u003EEvent subscription cleanup on disposal.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EHowever, it no longer performs page authorization itself.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022can-i-use-uauthauthorize-on-a-regular-child-component\u0022\u003ECan I use UAuthAuthorize on a regular child component?\u003C/h3\u003E\n\u003Cp\u003EThe attribute only declares metadata. It does not enforce authorization on its own.\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\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@attribute [UAuthAuthorize(Roles = \u0026quot;Admin\u0026quot;)]\n\n\u0026lt;div\u0026gt;Administrative tools\u0026lt;/div\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EIf this is an ordinary child component rendered directly inside another page, \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E is not automatically evaluated by \u003Ccode\u003EUAuthAuthorizeRouteView\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003ERoute authorization is performed for routed page components.\u003C/p\u003E\n\u003Cp\u003EFor conditional content inside a page, use \u003Ccode\u003EUAuthStateView\u003C/code\u003E instead.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022advanced-customize-authorization-evaluation\u0022\u003E14. Advanced: Customize Authorization Evaluation\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth uses the \u003Ccode\u003EIUAuthAuthorizationEvaluator\u003C/code\u003E service for Blazor authorization.\u003C/p\u003E\n\u003Cp\u003EThe default evaluator is registered by:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EIt evaluates:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EAuthentication status.\u003C/li\u003E\n\u003Cli\u003ESession state when required.\u003C/li\u003E\n\u003Cli\u003ERoles.\u003C/li\u003E\n\u003Cli\u003EPermissions.\u003C/li\u003E\n\u003Cli\u003EASP.NET Core policies supplied through authorization requirements.\u003C/li\u003E\n\u003Cli\u003EThe configured match mode.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThe evaluator is registered with \u003Ccode\u003ETryAddScoped\u003C/code\u003E, allowing applications to provide their own implementation through dependency injection.\u003C/p\u003E\n\u003Cp\u003EIf you need custom application-specific authorization behavior, you can implement \u003Ccode\u003EIUAuthAuthorizationEvaluator\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EA replacement evaluator receives the current \u003Ccode\u003EUAuthState\u003C/code\u003E and a \u003Ccode\u003EUAuthAuthorizationRequirement\u003C/code\u003E and returns a \u003Ccode\u003EUAuthAuthorizationResult\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EA custom implementation should preserve secure defaults, especially for anonymous users, inactive sessions, and failed authorization checks.\u003C/p\u003E\n\u003Cp\u003EKeep authorization evaluation separate from component rendering and navigation logic.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022important-security-considerations\u0022\u003E15. Important Security Considerations\u003C/h2\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022blazor-ui-authorization-is-not-api-authorization\u0022\u003EBlazor UI authorization is not API authorization\u003C/h3\u003E\n\u003Cp\u003EAn unauthorized user should not see or instantiate a protected routed page. However, hiding UI does not secure the HTTP endpoint or underlying data source.\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\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/users\u0026quot;\n@attribute [UAuthAuthorize(Roles = \u0026quot;Admin\u0026quot;)]\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis protects the Blazor page through UltimateAuth\u0027s router.\u003C/p\u003E\n\u003Cp\u003EIt does \u003Cstrong\u003Enot\u003C/strong\u003E automatically protect an unrelated API endpoint 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\u003EGET /api/users\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThat endpoint must independently validate the caller and enforce the required authorization.\u003C/p\u003E\n\u003Cp\u003EFor Blazor Server, the same principle applies to sensitive server-side operations, even when they are invoked without an HTTP call from the component.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022uauthauthorize-is-currently-a-blazor-attribute\u0022\u003EUAuthAuthorize is currently a Blazor attribute\u003C/h3\u003E\n\u003Cp\u003EThe current \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E attribute is interpreted by UltimateAuth\u0027s Blazor authorization pipeline.\u003C/p\u003E\n\u003Cp\u003EIt does not, by itself, secure ASP.NET Core controllers or Minimal API endpoints.\u003C/p\u003E\n\u003Cp\u003EUse the server-side authorization mechanisms appropriate for those endpoints.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022revocation-and-role-changes\u0022\u003ERevocation and role changes\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthAuthorizeRouteView\u003C/code\u003E and \u003Ccode\u003EUAuthStateView\u003C/code\u003E respond to authentication state changes.\u003C/p\u003E\n\u003Cp\u003EWhen the available \u003Ccode\u003EUAuthState\u003C/code\u003E changes, authorization can be reevaluated.\u003C/p\u003E\n\u003Cp\u003EHowever, a remote role change or server-side session revocation is not necessarily observed immediately by the Blazor client.\u003C/p\u003E\n\u003Cp\u003EServer-side authorization must remain authoritative, even while a client retains stale state.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022authorization-during-navigation\u0022\u003EAuthorization during navigation\u003C/h3\u003E\n\u003Cp\u003EA protected routed page is not instantiated until UltimateAuth authorization succeeds.\u003C/p\u003E\n\u003Cp\u003EWhen authorization becomes invalid, the route view stops rendering the protected page.\u003C/p\u003E\n\u003Cp\u003EThis is different from implementing a redirect inside the page\u0027s \u003Ccode\u003EOnInitialized\u003C/code\u003E or \u003Ccode\u003EOnAfterRender\u003C/code\u003E method, where the component may already have been instantiated.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022do-not-rely-on-hidden-controls-for-security\u0022\u003EDo not rely on hidden controls for security\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthStateView\u003C/code\u003E is useful for showing or hiding buttons, navigation entries, and sections.\u003C/p\u003E\n\u003Cp\u003EAlways authorize the underlying operation independently.\u003C/p\u003E\n\u003Cp\u003EFor example, hiding a \u003Ccode\u003EDelete User\u003C/code\u003E button does not remove the need to authorize the actual delete request.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022troubleshooting\u0022\u003E16. Troubleshooting\u003C/h2\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022uauthauthorize-does-not-prevent-access\u0022\u003EUAuthAuthorize does not prevent access\u003C/h3\u003E\n\u003Cp\u003EFirst, make sure you\u0027re using \u003Cstrong\u003EUltimateAuth v0.1.1 or later\u003C/strong\u003E.\u003C/p\u003E\n\u003Cp\u003EEarlier versions relied on \u003Ccode\u003EUAuthComponentBase\u003C/code\u003E for authorization checks. Starting with v0.1.1, \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E is enforced at the routing level through \u003Ccode\u003EUAuthAuthorizeRouteView\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EIf authorization is not working as expected:\u003C/p\u003E\n\u003Col\u003E\n\u003Cli\u003EVerify that \u003Ccode\u003ECodeBeam.UltimateAuth.Client.Blazor\u003C/code\u003E is version \u003Ccode\u003E0.1.1\u003C/code\u003E or later.\u003C/li\u003E\n\u003Cli\u003EIf you\u0027re using the built-in router, make sure \u003Ccode\u003EUseBuiltInRouter=\u0026quot;true\u0026quot;\u003C/code\u003E is configured on \u003Ccode\u003EUAuthApp\u003C/code\u003E.\u003C/li\u003E\n\u003Cli\u003EIf you\u0027re using a custom router, replace \u003Ccode\u003ERouteView\u003C/code\u003E or \u003Ccode\u003EAuthorizeRouteView\u003C/code\u003E with \u003Ccode\u003EUAuthAuthorizeRouteView\u003C/code\u003E.\u003C/li\u003E\n\u003Cli\u003EMake sure the page is routed within the \u003Ccode\u003EUAuthApp\u003C/code\u003E context.\u003C/li\u003E\n\u003Cli\u003EConfirm that the user meets the authentication, role, permission, and session-state requirements.\u003C/li\u003E\n\u003C/ol\u003E\n\u003Cp\u003E\u003Cstrong\u003EUpgrading from v0.1.0?\u003C/strong\u003E See \u003Ca href=\u0022/docs/migration/v0.1.1\u0022\u003EMigrating to v0.1.1\u003C/a\u003E for the required changes.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022the-protected-page-appears-blank-during-startup\u0022\u003EThe protected page appears blank during startup\u003C/h3\u003E\n\u003Cp\u003EUltimateAuth may still be initializing or evaluating authorization.\u003C/p\u003E\n\u003Cp\u003EWith a custom router, provide an \u003Ccode\u003EAuthorizing\u003C/code\u003E fragment for a loading indicator.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022uauthstateview-reports-a-missing-cascading-uauthstate\u0022\u003EUAuthStateView reports a missing cascading UAuthState\u003C/h3\u003E\n\u003Cp\u003EMake sure the component is rendered inside \u003Ccode\u003EUAuthApp\u003C/code\u003E, or another host that correctly provides the required cascading UltimateAuth state.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022asp.net-core-authorization-reports-a-missing-authenticationstate\u0022\u003EASP.NET Core authorization reports a missing AuthenticationState\u003C/h3\u003E\n\u003Cp\u003EMake sure ASP.NET Core\u0027s authentication state infrastructure is available.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthApp\u003C/code\u003E provides a \u003Ccode\u003ECascadingAuthenticationState\u003C/code\u003E within its child content.\u003C/p\u003E\n\u003Cp\u003EIf you create a custom host outside \u003Ccode\u003EUAuthApp\u003C/code\u003E, you must provide the appropriate ASP.NET Core cascading authentication state yourself.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022roles-or-permissions-are-unexpectedly-denied\u0022\u003ERoles or permissions are unexpectedly denied\u003C/h3\u003E\n\u003Cp\u003EConfirm that:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EThe user is authenticated.\u003C/li\u003E\n\u003Cli\u003EThe state contains the expected role or permission.\u003C/li\u003E\n\u003Cli\u003EThe correct match mode is selected.\u003C/li\u003E\n\u003Cli\u003EThe user\u0027s session state satisfies \u003Ccode\u003ERequireActive\u003C/code\u003E.\u003C/li\u003E\n\u003Cli\u003EThe client state has been updated after any server-side changes.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022i-need-a-policy-directly-on-a-routed-page\u0022\u003EI need a policy directly on a routed page\u003C/h3\u003E\n\u003Cp\u003EUse ASP.NET Core\u0027s \u003Ccode\u003E[Authorize(Policy = \u0026quot;...\u0026quot;)]\u003C/code\u003E attribute.\u003C/p\u003E\n\u003Cp\u003EThe current \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E attribute does not expose a \u003Ccode\u003EPolicy\u003C/code\u003E property.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022summary\u0022\u003ESummary\u003C/h2\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EScenario\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\u003EProtect an entire Blazor page\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ERequire an UltimateAuth role\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E[UAuthAuthorize(Roles = \u0026quot;...\u0026quot;)]\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ERequire an UltimateAuth permission\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E[UAuthAuthorize(Permissions = \u0026quot;...\u0026quot;)]\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ECombine roles and permissions\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EMatchMode\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ERequire ASP.NET Core policy authorization\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E[Authorize(Policy = \u0026quot;...\u0026quot;)]\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ECombine ASP.NET Core and UltimateAuth\u003C/td\u003E\n\u003Ctd\u003EUse both attributes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EProtect an individual UI section\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthStateView\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EUse built-in routing\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthApp UseBuiltInRouter=\u0026quot;true\u0026quot;\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EUse custom routing\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthAuthorizeRouteView\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EReact to authentication lifecycle changes\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthComponentBase\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EProtect an API or server operation\u003C/td\u003E\n\u003Ctd\u003EEnforce authorization on the server\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EUltimateAuth\u0027s Blazor authorization model allows you to declare requirements at the page level, conditionally render protected sections, and integrate with ASP.NET Core authorization without coupling page protection to a specific base class.\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 Create a working UltimateAuth application.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/fundamentals/auth-model\u0022\u003EAuthentication Model\u003C/a\u003E \u2014 Understand UltimateAuth authentication concepts.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/migration/v0.1.1\u0022\u003EMigration to v0.1.1\u003C/a\u003E \u2014 Update custom routers and existing Blazor authorization code.\u003C/li\u003E\n\u003C/ul\u003E\n", + "Html": "\n\u003Cp\u003ELearn how to protect Blazor pages and UI sections using UltimateAuth and ASP.NET Core authorization.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth provides declarative authorization for routed Blazor pages through \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E, conditional rendering through \u003Ccode\u003EUAuthStateView\u003C/code\u003E, and interoperability with ASP.NET Core\u0027s \u003Ccode\u003E[Authorize]\u003C/code\u003E attribute.\u003C/p\u003E\n\u003Cp\u003EIn this guide, you\u0027ll learn how to:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003ERequire authentication before a page can be accessed.\u003C/li\u003E\n\u003Cli\u003ERestrict access using roles and permissions.\u003C/li\u003E\n\u003Cli\u003ECombine authorization requirements with different match modes.\u003C/li\u003E\n\u003Cli\u003ERequire an active session.\u003C/li\u003E\n\u003Cli\u003EProtect individual UI sections.\u003C/li\u003E\n\u003Cli\u003EUse ASP.NET Core authorization alongside UltimateAuth.\u003C/li\u003E\n\u003Cli\u003EConfigure authorization with built-in and custom routers.\u003C/li\u003E\n\u003Cli\u003EUnderstand the role of \u003Ccode\u003EUAuthApp\u003C/code\u003E and \u003Ccode\u003EUAuthComponentBase\u003C/code\u003E.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cblockquote\u003E\n\u003Cp\u003E\u003Cstrong\u003ESecurity:\u003C/strong\u003E Blazor authorization controls which UI components are rendered. Protected API endpoints, data, and server operations must enforce authorization independently on the server. Client-side authorization must never be the only security boundary.\u003C/p\u003E\n\u003C/blockquote\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022prerequisites\u0022\u003EPrerequisites\u003C/h2\u003E\n\u003Cp\u003EYou should have a working UltimateAuth Blazor application with authentication configured.\u003C/p\u003E\n\u003Cp\u003EIf you haven\u0027t created one yet, start with the \u003Ca href=\u0022/docs/getting-started/quickstart\u0022\u003EQuickStart\u003C/a\u003E.\u003C/p\u003E\n\u003Cp\u003EThe examples in this guide 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\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@using CodeBeam.UltimateAuth.Client\n@using CodeBeam.UltimateAuth.Client.Blazor\n@using CodeBeam.UltimateAuth.Core.Contracts\n@using Microsoft.AspNetCore.Authorization\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EYou can add these namespaces to your application\u0027s \u003Ccode\u003E_Imports.razor\u003C/code\u003E file to make them available across components.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022protect-a-page-with-uauthauthorize\u0022\u003E1. Protect a Page with UAuthAuthorize\u003C/h2\u003E\n\u003Cp\u003EThe simplest way to protect a routed Blazor page is to add the \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E attribute.\u003C/p\u003E\n\u003Cp\u003ECreate \u003Ccode\u003EPages/Dashboard.razor\u003C/code\u003E:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/dashboard\u0026quot;\n@attribute [UAuthAuthorize]\n\n\u0026lt;h1\u0026gt;Dashboard\u0026lt;/h1\u0026gt;\n\n\u0026lt;p\u0026gt;You are authenticated.\u0026lt;/p\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThat\u0027s it.\u003C/p\u003E\n\u003Cp\u003EWhen a user navigates to \u003Ccode\u003E/dashboard\u003C/code\u003E, UltimateAuth evaluates the page\u0027s authorization requirements before rendering the page component.\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Cstrong\u003EAuthenticated user with an acceptable session state:\u003C/strong\u003E The page can be rendered.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EAnonymous user:\u003C/strong\u003E Authorization is denied.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EInactive session:\u003C/strong\u003E Authorization is denied when an inactive session state is available and \u003Ccode\u003ERequireActive\u003C/code\u003E is enabled.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EBy default, \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E requires an authenticated user and an active session where session-state information is present.\u003C/p\u003E\n\u003Cp\u003ENo explicit role or permission is required.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022does-the-page-need-to-inherit-from-uauthcomponentbase\u0022\u003EDoes the page need to inherit from UAuthComponentBase?\u003C/h3\u003E\n\u003Cp\u003E\u003Cstrong\u003ENo.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EThe page above works without inheriting from any UltimateAuth base class.\u003C/p\u003E\n\u003Cp\u003EAuthorization is enforced by \u003Ccode\u003EUAuthAuthorizeRouteView\u003C/code\u003E, not by the page\u0027s base class.\u003C/p\u003E\n\u003Cp\u003EThis also means an unauthorized routed page is not instantiated by UltimateAuth\u0027s route view.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022require-a-role\u0022\u003E2. Require a Role\u003C/h2\u003E\n\u003Cp\u003EUse the \u003Ccode\u003ERoles\u003C/code\u003E property to restrict access to users with a specific role.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/admin\u0026quot;\n@attribute [UAuthAuthorize(Roles = \u0026quot;Admin\u0026quot;)]\n\n\u0026lt;h1\u0026gt;Administration\u0026lt;/h1\u0026gt;\n\n\u0026lt;p\u0026gt;Only administrators can access this page.\u0026lt;/p\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe user must be authenticated and have the \u003Ccode\u003EAdmin\u003C/code\u003E role.\u003C/p\u003E\n\u003Cp\u003EYou can also declare multiple roles:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/management\u0026quot;\n@attribute [UAuthAuthorize(Roles = \u0026quot;Admin,Manager\u0026quot;)]\n\n\u0026lt;h1\u0026gt;Management\u0026lt;/h1\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWith the default \u003Ccode\u003ECategory\u003C/code\u003E match mode, having \u003Cstrong\u003Eeither \u003Ccode\u003EAdmin\u003C/code\u003E or \u003Ccode\u003EManager\u003C/code\u003E\u003C/strong\u003E is sufficient, provided the other configured authorization categories also succeed.\u003C/p\u003E\n\u003Cp\u003ERole names must correspond to the roles recognized by UltimateAuth\u0027s authentication state.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022require-a-permission\u0022\u003E3. Require a Permission\u003C/h2\u003E\n\u003Cp\u003EFor more granular authorization, use \u003Ccode\u003EPermissions\u003C/code\u003E.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/users\u0026quot;\n@attribute [UAuthAuthorize(Permissions = \u0026quot;users.read.self\u0026quot;)]\n\n\u0026lt;h1\u0026gt;Users\u0026lt;/h1\u0026gt;\n\n\u0026lt;p\u0026gt;You have permission to view users.\u0026lt;/p\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUltimateAuth checks the requested permission against the current \u003Ccode\u003EUAuthState\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EYou can declare multiple permissions:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/reports\u0026quot;\n@attribute [UAuthAuthorize(Permissions = \u0026quot;reports.read.self,reports.manage.admin\u0026quot;)]\n \n\u0026lt;h1\u0026gt;Reports\u0026lt;/h1\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EBy default, the user needs at least one of these permissions.\u003C/p\u003E\n\u003Cp\u003EPermission strings must match the permissions represented in the authenticated user\u0027s UltimateAuth state.\u003C/p\u003E\n\u003Cblockquote\u003E\n\u003Cp\u003E\u003Cstrong\u003ETip:\u003C/strong\u003E Prefer permissions for operations such as reading reports, managing users, or editing records. Roles are useful for broader organizational responsibilities, while permissions can express specific capabilities.\u003C/p\u003E\n\u003C/blockquote\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022combine-roles-and-permissions\u0022\u003E4. Combine Roles and Permissions\u003C/h2\u003E\n\u003Cp\u003EYou can specify both \u003Ccode\u003ERoles\u003C/code\u003E and \u003Ccode\u003EPermissions\u003C/code\u003E.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/administration/users\u0026quot;\n@attribute [UAuthAuthorize(Roles = \u0026quot;Admin\u0026quot;, Permissions = \u0026quot;users.manage.admin\u0026quot;)]\n\n\u0026lt;h1\u0026gt;User Management\u0026lt;/h1\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWith the default \u003Ccode\u003EMatchMode = Category\u003C/code\u003E, the user must satisfy:\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EAdmin role AND users.manage permission\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EThis is because \u003Ccode\u003ECategory\u003C/code\u003E requires at least one match from each configured category.\u003C/p\u003E\n\u003Cp\u003EIf multiple roles and permissions are provided:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/management/users\u0026quot;\n@attribute [UAuthAuthorize(Roles = \u0026quot;Admin,Manager\u0026quot;, Permissions = \u0026quot;users.read.self,users.manage.admin\u0026quot;)]\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe requirement becomes:\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003E(Admin OR Manager) AND (users.read.self OR users.manage.admin)\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EThis is the default authorization behavior.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022understand-match-modes\u0022\u003E5. Understand Match Modes\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth supports three authorization matching modes:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EMatch Mode\u003C/th\u003E\n\u003Cth\u003EBehavior\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECategory\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAt least one matching value in each configured category\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAny\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAt least one configured role or permission must match\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAll\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EEvery configured role and permission must match\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe default is \u003Ccode\u003EAuthorizationMatchMode.Category\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022category-the-default\u0022\u003ECategory \u2014 the default\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/category-example\u0026quot;\n@attribute [UAuthAuthorize(Roles = \u0026quot;Admin,Manager\u0026quot;, Permissions = \u0026quot;reports.read,reports.write\u0026quot;, MatchMode = AuthorizationMatchMode.Category)]\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe user must have:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003EAdmin\u003C/code\u003E \u003Cstrong\u003Eor\u003C/strong\u003E \u003Ccode\u003EManager\u003C/code\u003E.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003Ereports.read\u003C/code\u003E \u003Cstrong\u003Eor\u003C/strong\u003E \u003Ccode\u003Ereports.write\u003C/code\u003E.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EBoth configured categories must succeed.\u003C/p\u003E\n\u003Cp\u003EUse \u003Ccode\u003ECategory\u003C/code\u003E when roles and permissions represent independent requirements but alternatives within each category are acceptable.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022any-one-match-is-enough\u0022\u003EAny \u2014 one match is enough\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/any-example\u0026quot;\n@attribute [UAuthAuthorize(Roles = \u0026quot;Admin,Auditor\u0026quot;, Permissions = \u0026quot;reports.read.admin\u0026quot;, MatchMode = AuthorizationMatchMode.Any)]\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe user needs at least one of:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003EAdmin\u003C/code\u003E role.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EAuditor\u003C/code\u003E role.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003Ereports.read.admin\u003C/code\u003E permission.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThis is useful for pages with several alternative authorization paths.\u003C/p\u003E\n\u003Cp\u003EBe careful when combining high-privilege roles and lower-privilege permissions with \u003Ccode\u003EAny\u003C/code\u003E: a single matching permission grants access.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022all-every-value-is-required\u0022\u003EAll \u2014 every value is required\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/all-example\u0026quot;\n@attribute [UAuthAuthorize(Roles = \u0026quot;Admin,SecurityOfficer\u0026quot;, Permissions = \u0026quot;audit.read.self,audit.export.admin\u0026quot;, MatchMode = AuthorizationMatchMode.All)]\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe user must have:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003EAdmin\u003C/code\u003E role.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003ESecurityOfficer\u003C/code\u003E role.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003Eaudit.read.self\u003C/code\u003E permission.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003Eaudit.export.admin\u003C/code\u003E permission.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EUse \u003Ccode\u003EAll\u003C/code\u003E when every declared requirement is mandatory.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022choosing-a-match-mode\u0022\u003EChoosing a match mode\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003ERequirement\u003C/th\u003E\n\u003Cth\u003ERecommended mode\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAny authenticated user\u003C/td\u003E\n\u003Ctd\u003EParameterless attribute\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EOne required role\u003C/td\u003E\n\u003Ctd\u003EDefault \u003Ccode\u003ECategory\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAny of several roles\u003C/td\u003E\n\u003Ctd\u003EDefault \u003Ccode\u003ECategory\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EOne role AND one permission\u003C/td\u003E\n\u003Ctd\u003EDefault \u003Ccode\u003ECategory\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ERole OR permission\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EAny\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAll listed roles AND permissions\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EAll\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EIf roles and permissions are omitted entirely, an authenticated user satisfies the authorization requirement regardless of match mode, subject to the session-state requirement.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022require-an-active-session\u0022\u003E6. Require an Active Session\u003C/h2\u003E\n\u003Cp\u003EBy default, \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E uses:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003ERequireActive = true\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe default configuration can be written explicitly:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/account/settings\u0026quot;\n@attribute [UAuthAuthorize(RequireActive = true)]\n\n\u0026lt;h1\u0026gt;Account Settings\u0026lt;/h1\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EWhen UltimateAuth has session-state information and the session is not active, authorization returns an inactive result.\u003C/p\u003E\n\u003Cp\u003EIf your application needs to allow an authenticated user regardless of a reported non-active session state, you can disable this requirement:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/session-information\u0026quot;\n@attribute [UAuthAuthorize(RequireActive = false)]\n\n\u0026lt;h1\u0026gt;Session Information\u0026lt;/h1\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis changes only the session-state condition. Authentication is still required.\u003C/p\u003E\n\u003Cblockquote\u003E\n\u003Cp\u003E\u003Cstrong\u003EImportant:\u003C/strong\u003E \u003Ccode\u003ERequireActive\u003C/code\u003E evaluates the session state currently available to the Blazor client. It does not independently validate a session against the server for every render. Server-side session validation and revocation enforcement remain necessary.\u003C/p\u003E\n\u003C/blockquote\u003E\n\u003Cp\u003EWhen no session-state value is available, the default evaluator does not reject the user solely because that value is absent.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022protect-part-of-a-page-with-uauthstateview\u0022\u003E7. Protect Part of a Page with UAuthStateView\u003C/h2\u003E\n\u003Cp\u003ESometimes you want the page to remain publicly accessible while protecting only a specific section.\u003C/p\u003E\n\u003Cp\u003EUse \u003Ccode\u003EUAuthStateView\u003C/code\u003E for conditional rendering.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/products\u0026quot;\n\n\u0026lt;h1\u0026gt;Products\u0026lt;/h1\u0026gt;\n\n\u0026lt;p\u0026gt;Everyone can view this page.\u0026lt;/p\u0026gt;\n\n\u0026lt;UAuthStateView Roles=\u0026quot;Admin\u0026quot;\u0026gt;\n \u0026lt;Authorized\u0026gt;\n \u0026lt;button\u0026gt;Edit Products\u0026lt;/button\u0026gt;\n \u0026lt;/Authorized\u0026gt;\n \u0026lt;NotAuthorized\u0026gt;\n \u0026lt;p\u0026gt;Administrative tools are unavailable.\u0026lt;/p\u0026gt;\n \u0026lt;/NotAuthorized\u0026gt;\n\u0026lt;/UAuthStateView\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe page itself remains accessible to everyone.\u003C/p\u003E\n\u003Cp\u003EHowever, the administrative section is only rendered for an authorized user.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthStateView\u003C/code\u003E uses the same \u003Ccode\u003EIUAuthAuthorizationEvaluator\u003C/code\u003E as \u003Ccode\u003EUAuthAuthorizeRouteView\u003C/code\u003E, so matching roles, permissions, and match modes have consistent semantics.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022basic-authentication-only-section\u0022\u003EBasic authentication-only section\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E\u0026lt;UAuthStateView\u0026gt;\n \u0026lt;Authorized\u0026gt;\n \u0026lt;p\u0026gt;Welcome back!\u0026lt;/p\u0026gt;\n \u0026lt;/Authorized\u0026gt;\n \u0026lt;NotAuthorized\u0026gt;\n \u0026lt;p\u0026gt;Sign in to access your account features.\u0026lt;/p\u0026gt;\n \u0026lt;/NotAuthorized\u0026gt;\n\u0026lt;/UAuthStateView\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAs with parameterless \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E, authentication is required, along with an acceptable session state when \u003Ccode\u003ERequireActive\u003C/code\u003E is true.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022protect-a-section-with-permissions\u0022\u003EProtect a section with permissions\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E\u0026lt;UAuthStateView Permissions=\u0026quot;orders.refund.self\u0026quot;\u0026gt;\n \u0026lt;Authorized\u0026gt;\n \u0026lt;button\u0026gt;Refund Order\u0026lt;/button\u0026gt;\n \u0026lt;/Authorized\u0026gt;\n \u0026lt;NotAuthorized\u0026gt;\n \u0026lt;p\u0026gt;You cannot refund orders.\u0026lt;/p\u0026gt;\n \u0026lt;/NotAuthorized\u0026gt;\n\u0026lt;/UAuthStateView\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022combine-requirements-in-a-section\u0022\u003ECombine requirements in a section\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E\u0026lt;UAuthStateView Roles=\u0026quot;Admin,Manager\u0026quot; Permissions=\u0026quot;orders.read.self,orders.manage.admin\u0026quot; MatchMode=\u0026quot;AuthorizationMatchMode.Category\u0026quot;\u0026gt;\n \u0026lt;Authorized\u0026gt;\n \u0026lt;p\u0026gt;Order management tools are available.\u0026lt;/p\u0026gt;\n \u0026lt;/Authorized\u0026gt;\n \u0026lt;NotAuthorized\u0026gt;\n \u0026lt;p\u0026gt;Access denied.\u0026lt;/p\u0026gt;\n \u0026lt;/NotAuthorized\u0026gt;\n\u0026lt;/UAuthStateView\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis uses the same \u003Ccode\u003ECategory\u003C/code\u003E semantics as the routed page attribute.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022access-the-current-authentication-state\u0022\u003EAccess the current authentication state\u003C/h3\u003E\n\u003Cp\u003EThe \u003Ccode\u003EAuthorized\u003C/code\u003E fragment receives the current \u003Ccode\u003EUAuthState\u003C/code\u003E as its context.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E\u0026lt;UAuthStateView\u0026gt;\n \u0026lt;Authorized Context=\u0026quot;state\u0026quot;\u0026gt;\n \u0026lt;p\u0026gt;Hello, @state.Identity?.DisplayName!\u0026lt;/p\u0026gt;\n \u0026lt;/Authorized\u0026gt;\n \u0026lt;NotAuthorized\u0026gt;\n \u0026lt;p\u0026gt;Please sign in.\u0026lt;/p\u0026gt;\n \u0026lt;/NotAuthorized\u0026gt;\n\u0026lt;/UAuthStateView\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis allows authorized content to access identity information without requiring a custom base class.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022handle-unauthorized-inactive-and-authorizing-states\u0022\u003E8. Handle Unauthorized, Inactive, and Authorizing States\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthStateView\u003C/code\u003E supports multiple rendering states.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E\u0026lt;UAuthStateView Roles=\u0026quot;Admin\u0026quot;\n RequireActive=\u0026quot;true\u0026quot;\u0026gt;\n \u0026lt;Authorized\u0026gt;\n \u0026lt;p\u0026gt;Administrator access granted.\u0026lt;/p\u0026gt;\n \u0026lt;/Authorized\u0026gt;\n\n \u0026lt;NotAuthorized\u0026gt;\n \u0026lt;p\u0026gt;You do not have permission to access this section.\u0026lt;/p\u0026gt;\n \u0026lt;/NotAuthorized\u0026gt;\n\n \u0026lt;Inactive Context=\u0026quot;state\u0026quot;\u0026gt;\n \u0026lt;p\u0026gt;Your session is not active.\u0026lt;/p\u0026gt;\n \u0026lt;/Inactive\u0026gt;\n\n \u0026lt;Authorizing\u0026gt;\n \u0026lt;p\u0026gt;Checking your permissions...\u0026lt;/p\u0026gt;\n \u0026lt;/Authorizing\u0026gt;\n\u0026lt;/UAuthStateView\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EFragment\u003C/th\u003E\n\u003Cth\u003EWhen displayed\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAuthorized\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthorization succeeds\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ENotAuthorized\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthorization is denied\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EInactive\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EA non-active session is detected and \u003Ccode\u003ERequireActive\u003C/code\u003E is true\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAuthorizing\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAuthorization evaluation is in progress\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EIf \u003Ccode\u003EInactive\u003C/code\u003E is not supplied, the component falls back to the \u003Ccode\u003ENotAuthorized\u003C/code\u003E fragment.\u003C/p\u003E\n\u003Cp\u003EIf \u003Ccode\u003EAuthorizing\u003C/code\u003E is not supplied, unauthorized content is used while a new authorization result is pending.\u003C/p\u003E\n\u003Cp\u003EDuring reevaluation, previously authorized section content is removed before an asynchronous policy evaluation completes. This prevents stale authorized content from remaining visible during that pending evaluation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022use-an-asp.net-core-authorization-policy\u0022\u003E9. Use an ASP.NET Core Authorization Policy\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthStateView\u003C/code\u003E supports the \u003Ccode\u003EPolicy\u003C/code\u003E parameter.\u003C/p\u003E\n\u003Cp\u003EFirst, register a policy using ASP.NET Core authorization:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddAuthorizationCore(options =\u0026gt;\n{\n options.AddPolicy(\u0026quot;CanViewReports\u0026quot;, policy =\u0026gt; policy.RequireClaim(\u0026quot;reports_access\u0026quot;, \u0026quot;granted\u0026quot;));\n});\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThen use that policy in a UI 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\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E\u0026lt;UAuthStateView Policy=\u0026quot;CanViewReports\u0026quot;\u0026gt;\n \u0026lt;Authorized\u0026gt;\n \u0026lt;p\u0026gt;Reports are available.\u0026lt;/p\u0026gt;\n \u0026lt;/Authorized\u0026gt;\n \u0026lt;NotAuthorized\u0026gt;\n \u0026lt;p\u0026gt;Reports access is restricted.\u0026lt;/p\u0026gt;\n \u0026lt;/NotAuthorized\u0026gt;\n\u0026lt;/UAuthStateView\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe policy is evaluated through ASP.NET Core\u0027s \u003Ccode\u003EIAuthorizationService\u003C/code\u003E, using the claims principal produced from the current UltimateAuth state.\u003C/p\u003E\n\u003Cp\u003EYou can also combine a policy with roles and permissions:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E\u0026lt;UAuthStateView Roles=\u0026quot;Admin\u0026quot; Permissions=\u0026quot;reports.read\u0026quot; Policy=\u0026quot;CanViewReports\u0026quot; MatchMode=\u0026quot;AuthorizationMatchMode.All\u0026quot;\u0026gt;\n \u0026lt;Authorized\u0026gt;\n \u0026lt;p\u0026gt;All requirements passed.\u0026lt;/p\u0026gt;\n \u0026lt;/Authorized\u0026gt;\n\u0026lt;/UAuthStateView\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EFor \u003Ccode\u003EAll\u003C/code\u003E, every configured requirement must succeed.\u003C/p\u003E\n\u003Cp\u003EFor \u003Ccode\u003EAny\u003C/code\u003E, the policy is another possible successful condition.\u003C/p\u003E\n\u003Cp\u003EFor \u003Ccode\u003ECategory\u003C/code\u003E, the policy must succeed alongside the configured role and permission categories.\u003C/p\u003E\n\u003Cblockquote\u003E\n\u003Cp\u003E\u003Cstrong\u003ECurrent API distinction:\u003C/strong\u003E \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E currently exposes \u003Ccode\u003ERoles\u003C/code\u003E, \u003Ccode\u003EPermissions\u003C/code\u003E, \u003Ccode\u003EMatchMode\u003C/code\u003E, and \u003Ccode\u003ERequireActive\u003C/code\u003E, but not \u003Ccode\u003EPolicy\u003C/code\u003E. Use \u003Ccode\u003EUAuthStateView\u003C/code\u003E for policy-based section rendering, or ASP.NET Core \u003Ccode\u003E[Authorize(Policy = \u0026quot;...\u0026quot;)]\u003C/code\u003E for policy-protected routed pages.\u003C/p\u003E\n\u003C/blockquote\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022protect-a-page-with-asp.net-core-authorize\u0022\u003E10. Protect a Page with ASP.NET Core Authorize\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth integrates with ASP.NET Core authorization.\u003C/p\u003E\n\u003Cp\u003EYou can use the standard \u003Ccode\u003E[Authorize]\u003C/code\u003E attribute on routed Blazor pages.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/account\u0026quot;\n@attribute [Authorize]\n\n\u0026lt;h1\u0026gt;Account\u0026lt;/h1\u0026gt;\n\n\u0026lt;p\u0026gt;This page requires ASP.NET Core authorization.\u0026lt;/p\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EOr require a role:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/administration\u0026quot;\n@attribute [Authorize(Roles = \u0026quot;Admin\u0026quot;)]\n\n\u0026lt;h1\u0026gt;Administration\u0026lt;/h1\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EOr a registered policy:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/reports\u0026quot;\n@attribute [Authorize(Policy = \u0026quot;CanViewReports\u0026quot;)]\n\n\u0026lt;h1\u0026gt;Reports\u0026lt;/h1\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThese checks are performed through ASP.NET Core\u0027s \u003Ccode\u003EAuthorizeRouteView\u003C/code\u003E and its authentication and authorization infrastructure.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth provides a Blazor \u003Ccode\u003EAuthenticationStateProvider\u003C/code\u003E that integrates UltimateAuth authentication state with ASP.NET Core.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022which-attribute-should-i-use\u0022\u003EWhich attribute should I use?\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EFeature\u003C/th\u003E\n\u003Cth\u003E\u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E\u003C/th\u003E\n\u003Cth\u003E\u003Ccode\u003E[Authorize]\u003C/code\u003E\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ERequires authentication\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ERoles\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EUltimateAuth permissions\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003Ctd\u003EVia separately configured ASP.NET Core policies or handlers\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAuthorizationMatchMode\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003Ctd\u003ENo direct equivalent property\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERequireActive\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003Ctd\u003ENo direct equivalent property\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ENamed ASP.NET Core policy\u003C/td\u003E\n\u003Ctd\u003ENot currently\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ERouted Blazor page\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EUse \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E when working directly with UltimateAuth roles, permissions, and session-aware requirements.\u003C/p\u003E\n\u003Cp\u003EUse \u003Ccode\u003E[Authorize]\u003C/code\u003E when relying on ASP.NET Core authorization policies, requirements, or existing authorization infrastructure.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022combine-both-attributes\u0022\u003ECombine both attributes\u003C/h3\u003E\n\u003Cp\u003EYou can declare both on the same routed page:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/secure-reports\u0026quot;\n\n@attribute [UAuthAuthorize(Permissions = \u0026quot;reports.read\u0026quot;)]\n@attribute [Authorize(Roles = \u0026quot;Admin\u0026quot;)]\n\n\u0026lt;h1\u0026gt;Secure Reports\u0026lt;/h1\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe page is rendered only when \u003Cstrong\u003Eboth authorization systems allow access\u003C/strong\u003E.\u003C/p\u003E\n\u003Cp\u003EUltimateAuth evaluates \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E, and the underlying ASP.NET Core \u003Ccode\u003EAuthorizeRouteView\u003C/code\u003E evaluates \u003Ccode\u003E[Authorize]\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe requirements are effectively combined using AND semantics across the two mechanisms.\u003C/p\u003E\n\u003Cblockquote\u003E\n\u003Cp\u003E\u003Cstrong\u003ENote:\u003C/strong\u003E ASP.NET Core \u003Ccode\u003E[Authorize]\u003C/code\u003E uses the configured ASP.NET Core authentication state. \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E uses \u003Ccode\u003EUAuthState\u003C/code\u003E. UltimateAuth integrates these models, but custom authentication providers must keep their states consistent.\u003C/p\u003E\n\u003C/blockquote\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022how-uauthapp-protects-routes\u0022\u003E11. How UAuthApp Protects Routes\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthApp\u003C/code\u003E is the root Blazor integration component for UltimateAuth.\u003C/p\u003E\n\u003Cp\u003EIt provides:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EClient runtime initialization.\u003C/li\u003E\n\u003Cli\u003EThe current \u003Ccode\u003EUAuthState\u003C/code\u003E as a cascading parameter.\u003C/li\u003E\n\u003Cli\u003EASP.NET Core cascading authentication state.\u003C/li\u003E\n\u003Cli\u003ESession lifecycle coordination.\u003C/li\u003E\n\u003Cli\u003ERuntime initialization readiness.\u003C/li\u003E\n\u003Cli\u003EOptional built-in routing.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022built-in-router\u0022\u003EBuilt-in router\u003C/h3\u003E\n\u003Cp\u003EIf you 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\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E\u0026lt;UAuthApp UseBuiltInRouter=\u0026quot;true\u0026quot; AppAssembly=\u0026quot;typeof(Program).Assembly\u0026quot; DefaultLayout=\u0026quot;typeof(Layout.MainLayout)\u0026quot; /\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUltimateAuth automatically configures its router to use \u003Ccode\u003EUAuthAuthorizeRouteView\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EYou do not need to add a separate \u003Ccode\u003EAuthorizeRouteView\u003C/code\u003E or \u003Ccode\u003EUAuthAuthorizeRouteView\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe built-in router supports both \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E and ASP.NET Core \u003Ccode\u003E[Authorize]\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EIt also supports an application-level \u003Ccode\u003ENotAuthorized\u003C/code\u003E fragment:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E\u0026lt;UAuthApp UseBuiltInRouter=\u0026quot;true\u0026quot;\n AppAssembly=\u0026quot;typeof(Program).Assembly\u0026quot;\n DefaultLayout=\u0026quot;typeof(Layout.MainLayout)\u0026quot;\u0026gt;\n \u0026lt;NotAuthorized\u0026gt;\n \u0026lt;h1\u0026gt;Access Denied\u0026lt;/h1\u0026gt;\n \u0026lt;p\u0026gt;You are not authorized to access this page.\u0026lt;/p\u0026gt;\n \u0026lt;/NotAuthorized\u0026gt;\n\u0026lt;/UAuthApp\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis fragment is displayed when route authorization fails.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022runtime-initialization\u0022\u003ERuntime initialization\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthApp\u003C/code\u003E initializes the client authentication state before allowing UltimateAuth-protected routes to be rendered.\u003C/p\u003E\n\u003Cp\u003EThis is important because authentication state may not be immediately available when a Blazor application first starts, particularly when restoring a session.\u003C/p\u003E\n\u003Cp\u003EDuring initialization or asynchronous authorization evaluation, \u003Ccode\u003EUAuthAuthorizeRouteView\u003C/code\u003E can display an \u003Ccode\u003EAuthorizing\u003C/code\u003E fragment.\u003C/p\u003E\n\u003Cp\u003EIf one is not supplied, the protected route content remains unrendered while authorization is pending.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022manual-and-reactive-render-modes\u0022\u003EManual and Reactive render modes\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthApp\u003C/code\u003E supports \u003Ccode\u003EUAuthRenderMode.Manual\u003C/code\u003E and \u003Ccode\u003EUAuthRenderMode.Reactive\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThese control how UltimateAuth state changes affect general component rendering.\u003C/p\u003E\n\u003Cp\u003ERoute authorization itself subscribes to relevant authentication state changes, and \u003Ccode\u003EUAuthStateView\u003C/code\u003E also reacts to such changes.\u003C/p\u003E\n\u003Cp\u003EYou do not need to enable reactive rendering merely to make \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E work.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022protect-pages-with-a-custom-router\u0022\u003E12. Protect Pages with a Custom Router\u003C/h2\u003E\n\u003Cp\u003EIf your application uses its own Blazor \u003Ccode\u003ERouter\u003C/code\u003E, set \u003Ccode\u003EUseBuiltInRouter=\u0026quot;false\u0026quot;\u003C/code\u003E on \u003Ccode\u003EUAuthApp\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThen use \u003Ccode\u003EUAuthAuthorizeRouteView\u003C/code\u003E inside the router\u0027s \u003Ccode\u003EFound\u003C/code\u003E fragment.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E\u0026lt;UAuthApp UseBuiltInRouter=\u0026quot;false\u0026quot;\u0026gt;\n \u0026lt;Router AppAssembly=\u0026quot;typeof(Program).Assembly\u0026quot;\u0026gt;\n \u0026lt;Found Context=\u0026quot;routeData\u0026quot;\u0026gt;\n \u0026lt;UAuthAuthorizeRouteView RouteData=\u0026quot;routeData\u0026quot; DefaultLayout=\u0026quot;typeof(Layout.MainLayout)\u0026quot;\u0026gt;\n\n \u0026lt;NotAuthorized\u0026gt;\n \u0026lt;h1\u0026gt;Access Denied\u0026lt;/h1\u0026gt;\n \u0026lt;p\u0026gt;You cannot access this page.\u0026lt;/p\u0026gt;\n \u0026lt;/NotAuthorized\u0026gt;\n\n \u0026lt;Authorizing\u0026gt;\n \u0026lt;p\u0026gt;Checking access...\u0026lt;/p\u0026gt;\n \u0026lt;/Authorizing\u0026gt;\n\n \u0026lt;/UAuthAuthorizeRouteView\u0026gt;\n\n \u0026lt;FocusOnNavigate RouteData=\u0026quot;routeData\u0026quot; Selector=\u0026quot;h1\u0026quot; /\u0026gt;\n \u0026lt;/Found\u0026gt;\n \u0026lt;/Router\u0026gt;\n\u0026lt;/UAuthApp\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthAuthorizeRouteView\u003C/code\u003E wraps ASP.NET Core\u0027s \u003Ccode\u003EAuthorizeRouteView\u003C/code\u003E internally.\u003C/p\u003E\n\u003Cp\u003EThis means it supports both authorization systems, while checking UltimateAuth-specific requirements before the protected page component is instantiated.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EDo not replace it with a plain \u003Ccode\u003ERouteView\u003C/code\u003E or \u003Ccode\u003EAuthorizeRouteView\u003C/code\u003E when your routed pages depend on \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EA plain ASP.NET Core \u003Ccode\u003EAuthorizeRouteView\u003C/code\u003E does not evaluate the UltimateAuth-specific attribute by itself.\u003C/p\u003E\n\u003Cp\u003EIf your custom router uses additional route assemblies, pass the appropriate \u003Ccode\u003EAdditionalAssemblies\u003C/code\u003E to the \u003Ccode\u003ERouter\u003C/code\u003E as needed.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022do-i-need-uauthcomponentbase\u0022\u003E13. Do I Need UAuthComponentBase?\u003C/h2\u003E\n\u003Cp\u003E\u003Cstrong\u003ENot for authorization.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EConsider this page:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/settings\u0026quot;\n@attribute [UAuthAuthorize]\n\n\u0026lt;h1\u0026gt;Settings\u0026lt;/h1\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EIt is protected when routed through \u003Ccode\u003EUAuthAuthorizeRouteView\u003C/code\u003E, even though it doesn\u0027t inherit from \u003Ccode\u003EUAuthComponentBase\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EYou can still use \u003Ccode\u003EUAuthComponentBase\u003C/code\u003E when you need access to the UltimateAuth state lifecycle.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/profile\u0026quot;\n@attribute [UAuthAuthorize]\n@inherits UAuthComponentBase\n\n\u0026lt;h1\u0026gt;Profile\u0026lt;/h1\u0026gt;\n\n\u0026lt;p\u0026gt;User: @AuthState.Identity?.DisplayName\u0026lt;/p\u0026gt;\n\n@code {\n protected override Task HandleAuthStateChangedAsync(\n UAuthStateChangeReason reason)\n {\n // React to authentication state changes.\n return Task.CompletedTask;\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthComponentBase\u003C/code\u003E provides:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EAccess to cascading \u003Ccode\u003EUAuthState\u003C/code\u003E.\u003C/li\u003E\n\u003Cli\u003EAuthentication state change notifications.\u003C/li\u003E\n\u003Cli\u003EAutomatic rerendering when the state changes, by default.\u003C/li\u003E\n\u003Cli\u003EAn asynchronous lifecycle hook.\u003C/li\u003E\n\u003Cli\u003EEvent subscription cleanup on disposal.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EHowever, it no longer performs page authorization itself.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022can-i-use-uauthauthorize-on-a-regular-child-component\u0022\u003ECan I use UAuthAuthorize on a regular child component?\u003C/h3\u003E\n\u003Cp\u003EThe attribute only declares metadata. It does not enforce authorization on its own.\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\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@attribute [UAuthAuthorize(Roles = \u0026quot;Admin\u0026quot;)]\n\n\u0026lt;div\u0026gt;Administrative tools\u0026lt;/div\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EIf this is an ordinary child component rendered directly inside another page, \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E is not automatically evaluated by \u003Ccode\u003EUAuthAuthorizeRouteView\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003ERoute authorization is performed for routed page components.\u003C/p\u003E\n\u003Cp\u003EFor conditional content inside a page, use \u003Ccode\u003EUAuthStateView\u003C/code\u003E instead.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022advanced-customize-authorization-evaluation\u0022\u003E14. Advanced: Customize Authorization Evaluation\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth uses the \u003Ccode\u003EIUAuthAuthorizationEvaluator\u003C/code\u003E service for Blazor authorization.\u003C/p\u003E\n\u003Cp\u003EThe default evaluator is registered by:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Ebuilder.Services.AddUltimateAuthClientBlazor();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EIt evaluates:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EAuthentication status.\u003C/li\u003E\n\u003Cli\u003ESession state when required.\u003C/li\u003E\n\u003Cli\u003ERoles.\u003C/li\u003E\n\u003Cli\u003EPermissions.\u003C/li\u003E\n\u003Cli\u003EASP.NET Core policies supplied through authorization requirements.\u003C/li\u003E\n\u003Cli\u003EThe configured match mode.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThe evaluator is registered with \u003Ccode\u003ETryAddScoped\u003C/code\u003E, allowing applications to provide their own implementation through dependency injection.\u003C/p\u003E\n\u003Cp\u003EIf you need custom application-specific authorization behavior, you can implement \u003Ccode\u003EIUAuthAuthorizationEvaluator\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EA replacement evaluator receives the current \u003Ccode\u003EUAuthState\u003C/code\u003E and a \u003Ccode\u003EUAuthAuthorizationRequirement\u003C/code\u003E and returns a \u003Ccode\u003EUAuthAuthorizationResult\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EA custom implementation should preserve secure defaults, especially for anonymous users, inactive sessions, and failed authorization checks.\u003C/p\u003E\n\u003Cp\u003EKeep authorization evaluation separate from component rendering and navigation logic.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022important-security-considerations\u0022\u003E15. Important Security Considerations\u003C/h2\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022blazor-ui-authorization-is-not-api-authorization\u0022\u003EBlazor UI authorization is not API authorization\u003C/h3\u003E\n\u003Cp\u003EAn unauthorized user should not see or instantiate a protected routed page. However, hiding UI does not secure the HTTP endpoint or underlying data source.\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\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@page \u0026quot;/users\u0026quot;\n@attribute [UAuthAuthorize(Roles = \u0026quot;Admin\u0026quot;)]\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis protects the Blazor page through UltimateAuth\u0027s router.\u003C/p\u003E\n\u003Cp\u003EIt does \u003Cstrong\u003Enot\u003C/strong\u003E automatically protect an unrelated API endpoint 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\u003EGET /api/users\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThat endpoint must independently validate the caller and enforce the required authorization.\u003C/p\u003E\n\u003Cp\u003EFor Blazor Server, the same principle applies to sensitive server-side operations, even when they are invoked without an HTTP call from the component.\u003C/p\u003E\n\u003Cblockquote\u003E\n\u003Cp\u003EUse [UAuthAuthorize] to declare authorization requirements for routed pages and UAuthStateView for conditional UI rendering. Use UAuthClient.Authorization.CheckAsync() when you need an explicit, on-demand server-side authorization decision for a specific action or resource. These mechanisms serve different purposes and do not replace authorization enforcement on protected server operations.\u003C/p\u003E\n\u003C/blockquote\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022uauthauthorize-is-currently-a-blazor-attribute\u0022\u003EUAuthAuthorize is currently a Blazor attribute\u003C/h3\u003E\n\u003Cp\u003EThe current \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E attribute is interpreted by UltimateAuth\u0027s Blazor authorization pipeline.\u003C/p\u003E\n\u003Cp\u003EIt does not, by itself, secure ASP.NET Core controllers or Minimal API endpoints.\u003C/p\u003E\n\u003Cp\u003EUse the server-side authorization mechanisms appropriate for those endpoints.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022revocation-and-role-changes\u0022\u003ERevocation and role changes\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthAuthorizeRouteView\u003C/code\u003E and \u003Ccode\u003EUAuthStateView\u003C/code\u003E respond to authentication state changes.\u003C/p\u003E\n\u003Cp\u003EWhen the available \u003Ccode\u003EUAuthState\u003C/code\u003E changes, authorization can be reevaluated.\u003C/p\u003E\n\u003Cp\u003EHowever, a remote role change or server-side session revocation is not necessarily observed immediately by the Blazor client.\u003C/p\u003E\n\u003Cp\u003EServer-side authorization must remain authoritative, even while a client retains stale state.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022authorization-during-navigation\u0022\u003EAuthorization during navigation\u003C/h3\u003E\n\u003Cp\u003EA protected routed page is not instantiated until UltimateAuth authorization succeeds.\u003C/p\u003E\n\u003Cp\u003EWhen authorization becomes invalid, the route view stops rendering the protected page.\u003C/p\u003E\n\u003Cp\u003EThis is different from implementing a redirect inside the page\u0027s \u003Ccode\u003EOnInitialized\u003C/code\u003E or \u003Ccode\u003EOnAfterRender\u003C/code\u003E method, where the component may already have been instantiated.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022do-not-rely-on-hidden-controls-for-security\u0022\u003EDo not rely on hidden controls for security\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthStateView\u003C/code\u003E is useful for showing or hiding buttons, navigation entries, and sections.\u003C/p\u003E\n\u003Cp\u003EAlways authorize the underlying operation independently.\u003C/p\u003E\n\u003Cp\u003EFor example, hiding a \u003Ccode\u003EDelete User\u003C/code\u003E button does not remove the need to authorize the actual delete request.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022troubleshooting\u0022\u003E16. Troubleshooting\u003C/h2\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022uauthauthorize-does-not-prevent-access\u0022\u003EUAuthAuthorize does not prevent access\u003C/h3\u003E\n\u003Cp\u003EFirst, make sure you\u0027re using \u003Cstrong\u003EUltimateAuth v0.1.1 or later\u003C/strong\u003E.\u003C/p\u003E\n\u003Cp\u003EEarlier versions relied on \u003Ccode\u003EUAuthComponentBase\u003C/code\u003E for authorization checks. Starting with v0.1.1, \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E is enforced at the routing level through \u003Ccode\u003EUAuthAuthorizeRouteView\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EIf authorization is not working as expected:\u003C/p\u003E\n\u003Col\u003E\n\u003Cli\u003EVerify that \u003Ccode\u003ECodeBeam.UltimateAuth.Client.Blazor\u003C/code\u003E is version \u003Ccode\u003E0.1.1\u003C/code\u003E or later.\u003C/li\u003E\n\u003Cli\u003EIf you\u0027re using the built-in router, make sure \u003Ccode\u003EUseBuiltInRouter=\u0026quot;true\u0026quot;\u003C/code\u003E is configured on \u003Ccode\u003EUAuthApp\u003C/code\u003E.\u003C/li\u003E\n\u003Cli\u003EIf you\u0027re using a custom router, replace \u003Ccode\u003ERouteView\u003C/code\u003E or \u003Ccode\u003EAuthorizeRouteView\u003C/code\u003E with \u003Ccode\u003EUAuthAuthorizeRouteView\u003C/code\u003E.\u003C/li\u003E\n\u003Cli\u003EMake sure the page is routed within the \u003Ccode\u003EUAuthApp\u003C/code\u003E context.\u003C/li\u003E\n\u003Cli\u003EConfirm that the user meets the authentication, role, permission, and session-state requirements.\u003C/li\u003E\n\u003C/ol\u003E\n\u003Cp\u003E\u003Cstrong\u003EUpgrading from v0.1.0?\u003C/strong\u003E See \u003Ca href=\u0022/docs/migration/v0.1.1\u0022\u003EMigrating to v0.1.1\u003C/a\u003E for the required changes.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022the-protected-page-appears-blank-during-startup\u0022\u003EThe protected page appears blank during startup\u003C/h3\u003E\n\u003Cp\u003EUltimateAuth may still be initializing or evaluating authorization.\u003C/p\u003E\n\u003Cp\u003EWith a custom router, provide an \u003Ccode\u003EAuthorizing\u003C/code\u003E fragment for a loading indicator.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022uauthstateview-reports-a-missing-cascading-uauthstate\u0022\u003EUAuthStateView reports a missing cascading UAuthState\u003C/h3\u003E\n\u003Cp\u003EMake sure the component is rendered inside \u003Ccode\u003EUAuthApp\u003C/code\u003E, or another host that correctly provides the required cascading UltimateAuth state.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022asp.net-core-authorization-reports-a-missing-authenticationstate\u0022\u003EASP.NET Core authorization reports a missing AuthenticationState\u003C/h3\u003E\n\u003Cp\u003EMake sure ASP.NET Core\u0027s authentication state infrastructure is available.\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003EUAuthApp\u003C/code\u003E provides a \u003Ccode\u003ECascadingAuthenticationState\u003C/code\u003E within its child content.\u003C/p\u003E\n\u003Cp\u003EIf you create a custom host outside \u003Ccode\u003EUAuthApp\u003C/code\u003E, you must provide the appropriate ASP.NET Core cascading authentication state yourself.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022roles-or-permissions-are-unexpectedly-denied\u0022\u003ERoles or permissions are unexpectedly denied\u003C/h3\u003E\n\u003Cp\u003EConfirm that:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EThe user is authenticated.\u003C/li\u003E\n\u003Cli\u003EThe state contains the expected role or permission.\u003C/li\u003E\n\u003Cli\u003EThe correct match mode is selected.\u003C/li\u003E\n\u003Cli\u003EThe user\u0027s session state satisfies \u003Ccode\u003ERequireActive\u003C/code\u003E.\u003C/li\u003E\n\u003Cli\u003EThe client state has been updated after any server-side changes.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022i-need-a-policy-directly-on-a-routed-page\u0022\u003EI need a policy directly on a routed page\u003C/h3\u003E\n\u003Cp\u003EUse ASP.NET Core\u0027s \u003Ccode\u003E[Authorize(Policy = \u0026quot;...\u0026quot;)]\u003C/code\u003E attribute.\u003C/p\u003E\n\u003Cp\u003EThe current \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E attribute does not expose a \u003Ccode\u003EPolicy\u003C/code\u003E property.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022summary\u0022\u003ESummary\u003C/h2\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EScenario\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\u003EProtect an entire Blazor page\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ERequire an UltimateAuth role\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E[UAuthAuthorize(Roles = \u0026quot;...\u0026quot;)]\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ERequire an UltimateAuth permission\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E[UAuthAuthorize(Permissions = \u0026quot;...\u0026quot;)]\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ECombine roles and permissions\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EMatchMode\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ERequire ASP.NET Core policy authorization\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E[Authorize(Policy = \u0026quot;...\u0026quot;)]\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ECombine ASP.NET Core and UltimateAuth\u003C/td\u003E\n\u003Ctd\u003EUse both attributes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EProtect an individual UI section\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthStateView\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EUse built-in routing\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthApp UseBuiltInRouter=\u0026quot;true\u0026quot;\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EUse custom routing\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthAuthorizeRouteView\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EReact to authentication lifecycle changes\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EUAuthComponentBase\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EProtect an API or server operation\u003C/td\u003E\n\u003Ctd\u003EEnforce authorization on the server\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EUltimateAuth\u0027s Blazor authorization model allows you to declare requirements at the page level, conditionally render protected sections, and integrate with ASP.NET Core authorization without coupling page protection to a specific base class.\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 Create a working UltimateAuth application.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/fundamentals/auth-model\u0022\u003EAuthentication Model\u003C/a\u003E \u2014 Understand UltimateAuth authentication concepts.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/migration/v0.1.1\u0022\u003EMigration to v0.1.1\u003C/a\u003E \u2014 Update custom routers and existing Blazor authorization code.\u003C/li\u003E\n\u003C/ul\u003E\n", "Headings": [ { "Id": "prerequisites", @@ -229,6 +229,6 @@ "Level": 0 } ], - "SourceHash": "DCAF757286008CAF750DF1425284DB7AAD17EAAF6B7FEA009089C1D6DEBEB91F", + "SourceHash": "C0ED4B78307D622392B5E6733AE7FEF61BEC91A2185CF2915B0013893683FB02", "BuilderVersion": "1" } \ No newline at end of file diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Endpoints/AuthorizationEndpointHandler.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Endpoints/AuthorizationEndpointHandler.cs index c221688d..f590f18b 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Endpoints/AuthorizationEndpointHandler.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Endpoints/AuthorizationEndpointHandler.cs @@ -35,6 +35,7 @@ public AuthorizationEndpointHandler( public async Task CheckAsync(HttpContext ctx) { var flow = _authFlow.Current; + if (!flow.IsAuthenticated) return Results.Unauthorized(); @@ -54,12 +55,7 @@ public async Task CheckAsync(HttpContext ctx) var result = await _authorization.AuthorizeAsync(accessContext, ctx.RequestAborted); - if (result.RequiresReauthentication) - return Results.StatusCode(StatusCodes.Status428PreconditionRequired); - - return result.IsAllowed - ? Results.Ok(result) - : Results.Forbid(); + return Results.Ok(result); } public async Task GetMyRolesAsync(HttpContext ctx) diff --git a/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IAuthorizationClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IAuthorizationClient.cs index 8f339558..909dc268 100644 --- a/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IAuthorizationClient.cs +++ b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IAuthorizationClient.cs @@ -35,11 +35,20 @@ namespace CodeBeam.UltimateAuth.Client.Services; public interface IAuthorizationClient { /// - /// Evaluates whether the current user is authorized to perform a specific action. + /// Evaluates whether the current authenticated user is authorized to perform a specific action. /// /// - /// This method performs a server-side authorization check based on the current session, - /// roles, and assigned permissions. + /// + /// Performs an on-demand, server-side authorization evaluation using the current authentication context and applicable policies. + /// + /// + /// A successful request returns an even when authorization is denied or reauthentication is required. + /// The authorization decision is separate from the API request result. + /// + /// + /// This method does not execute the requested operation or grant authorization for subsequent requests. + /// Protected operations must independently enforce authorization when executed. + /// /// Task> CheckAsync(AuthorizationCheckRequest request); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/AuthorizationManagementTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/AuthorizationManagementTests.cs index ed912363..a3eaa9e6 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/AuthorizationManagementTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/AuthorizationManagementTests.cs @@ -604,7 +604,7 @@ await GrantAsync( } [Fact] - public async Task AuthorizationCheck_WithoutPermission_ShouldDeny() + public async Task AuthorizationCheck_WithoutPermission_ShouldReturnDenial() { using var factory = NewFactory(); @@ -620,7 +620,14 @@ public async Task AuthorizationCheck_WithoutPermission_ShouldDeny() Resource = "authorization.roles" }); - response.StatusCode.Should().Be(HttpStatusCode.Forbidden); + response.StatusCode.Should().Be(HttpStatusCode.OK); + + var result = await response.Content + .ReadFromJsonAsync(); + + result.Should().NotBeNull(); + result!.IsAllowed.Should().BeFalse(); + result.RequiresReauthentication.Should().BeFalse(); } [Fact] diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/AuthorizationEndpointHandlerTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/AuthorizationEndpointHandlerTests.cs index e357de36..222e26e5 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/AuthorizationEndpointHandlerTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/AuthorizationEndpointHandlerTests.cs @@ -118,7 +118,7 @@ public async Task CheckAsync_WhenAllowed_CreatesExpectedAccessContextAndReturnsO } [Fact] - public async Task CheckAsync_WhenDenied_ReturnsForbidden() + public async Task CheckAsync_WhenDenied_ReturnsOkWithDenial() { var f = new Fixture(); var accessContext = f.AccessContext("orders.delete"); @@ -144,7 +144,14 @@ public async Task CheckAsync_WhenDenied_ReturnsForbidden() var result = await f.Sut.CheckAsync(ctx); - result.Should().BeOfType(); + var ok = result.Should() + .BeOfType>() + .Subject; + + ok.Value.Should().NotBeNull(); + ok.Value!.IsAllowed.Should().BeFalse(); + ok.Value.DenyReason.Should().Be("access_denied"); + ok.Value.RequiresReauthentication.Should().BeFalse(); f.Authorization.Verify(x => x.AuthorizeAsync( accessContext, @@ -153,7 +160,7 @@ public async Task CheckAsync_WhenDenied_ReturnsForbidden() } [Fact] - public async Task CheckAsync_WhenReauthenticationIsRequired_Returns428() + public async Task CheckAsync_WhenReauthenticationIsRequired_ReturnsOkWithReauthentication() { var f = new Fixture(); var accessContext = f.AccessContext("orders.delete"); @@ -179,9 +186,18 @@ public async Task CheckAsync_WhenReauthenticationIsRequired_Returns428() var result = await f.Sut.CheckAsync(ctx); - AssertStatusCode( - result, - StatusCodes.Status428PreconditionRequired); + var ok = result.Should() + .BeOfType>() + .Subject; + + ok.Value.Should().NotBeNull(); + ok.Value!.IsAllowed.Should().BeFalse(); + ok.Value.RequiresReauthentication.Should().BeTrue(); + + f.Authorization.Verify(x => x.AuthorizeAsync( + accessContext, + ctx.RequestAborted), + Times.Once); } // ========================================================= diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthClientAuthorizationTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthClientAuthorizationTests.cs index 7590a37e..6511d7f0 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthClientAuthorizationTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthClientAuthorizationTests.cs @@ -274,4 +274,48 @@ public async Task DeleteRole_Should_Publish_Event_On_Success() await client.Authorization.DeleteRoleAsync(request); Events.Verify(x => x.PublishAsync(It.Is(e => e.Type == UAuthStateEvent.AuthorizationChanged)), Times.Once); } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Check_Should_Return_Structured_Denial(bool requiresReauthentication) + { + var expected = requiresReauthentication + ? AuthorizationResult.ReauthRequired() + : AuthorizationResult.Deny("access_denied"); + + Request.Setup(x => x.SendJsonAsync( + "/auth/me/authorization/check", + It.IsAny())) + .ReturnsAsync(SuccessJson(expected)); + + var client = CreateClient(); + + var request = new AuthorizationCheckRequest + { + Action = "orders.delete", + Resource = "orders", + ResourceId = "order-123" + }; + + var result = await client.Authorization.CheckAsync(request); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().NotBeNull(); + + result.Value!.IsAllowed.Should().BeFalse(); + result.Value.RequiresReauthentication + .Should().Be(requiresReauthentication); + + result.Value.DenyReason + .Should().Be(expected.DenyReason); + + Request.Verify(x => x.SendJsonAsync( + "/auth/me/authorization/check", + It.Is(r => + r.Action == request.Action && + r.Resource == request.Resource && + r.ResourceId == request.ResourceId)), + Times.Once); + } }