This tutorial will walk you through step by step on how to authorize a user, display the UI for each user differently based on their identity. In this tutorial, you will discover:
You can download the example code used in this topic on GitHub.
Display different UI for each user is a way to describe authorization. You need 3 steps:
There are 2 approaches to authorize the users.
This approach assumes every route in your website requires the user to be authenticated. Some routes can require more than authenticated user like require the user must have admin role, must be an adult, etc. You can also specify those requirements explicitly or remove the requirements entirely for each route eventually.
A route is bound to an independent component, that means if one of a route in the component requires some requirements, all the requirements will be applied to all other routes in that component.
With this approach, you will authorize your component as a whole. You can also combine authorize on route with authorize on individual component to authorize a part of UI.
The following image illustrates the authorize on route approach:
This approach can segregate a component to many parts and display them differently per user. The following image illustrates the authorize on individual component approach:
Authorize on route is easy and suitable when you want to bar an entire page for a group of user, whereas authorize on individual component allows you to display a page differently for each user. However, choosing authorize on route does not mean you can't have the flexibility of authorize on individual component.
RouteView
with AuthorizeRouteView
component in the App.razor
. For example:<CascadingAuthenticationState> <Router AppAssembly="@typeof(App).Assembly"> <Found Context="routeData"> <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"> </AuthorizeRouteView> <FocusOnNavigate RouteData="@routeData" Selector="h1" /> </Found> <NotFound> ... </NotFound> </Router> </CascadingAuthenticationState>
NotAuthorized
and Authorizing
projected content inside the AuthorizeRouteView
component. For example:<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"> <NotAuthorized> <div>You are not authorized.</div> </NotAuthorized> <Authorizing> <div>Authorizing...</div> </Authorizing> </AuthorizeRouteView>
_Imports.razor
. For example:@using Microsoft.AspNetCore.Authorization @attribute [Authorize]
From now on, when create a new independent component, you need to decide if:
By default, any new independent component is for authenticated users only. If you want unauthenticated users to access the component, you need to add @attribute [AllowAnonymous]
to the directive section.
Wrap the content that needs to be authorized in the AuthorizeView
component. For example:
<AuthorizeView> <Authorized><div>Authenticated</div></Authorized> <Authorizing><div>Loading...</div></Authorizing> <NotAuthorized><div>Not authenticated</div></NotAuthorized> </AuthorizeView>
A resource (route, component, button, etc) is protected by an authorization rule, when a user satisfied all the requirements in an authorization rule, the user will be able to access the resource. There are 2 types of authorization rule:
The resource will be protected by a list of roles. When the user belongs to one of the roles in the list, the rule is satisfied and the user can access the resource.
A role is just a string. It can be whatever you want it to be. However, when it comes to include the role into a ClaimsIdentity
, you must put the role under the type ClaimTypes.Role
.
The following steps will help you to implement the role based authentication:
User
model, add a property for roles (a user might have multiple roles). For example:public class User { ... public List<string> Roles { get; set; } = new(); }
ToClaimsPrincipal
method to convert the list of roles to the ClaimsPrinciple
.public class User { ... public ClaimsPrincipal ToClaimsPrincipal() => new (new ClaimsIdentity(new Claim[] { ... }.Concat(Roles.Select(r => new Claim(ClaimTypes.Role, r)).ToArray()), "BlazorSchool")); }
FromClaimsPrincipal
method to convert the ClaimsPrinciple
back to the user model.public class User { ... public static User FromClaimsPrincipal(ClaimsPrincipal principal) => new() { ... Roles = principal.FindAll(ClaimTypes.Role).Select(c => c.Value).ToList() }; }
<AuthorizeView Roles="normal_user,paid_user"> <Authorized> <div>Content for users with either normal or paid or both roles.</div> </Authorized> </AuthorizeView> <AuthorizeView Roles="paid_user"> <Authorized> <div>Content for paid users.</div> </Authorized> </AuthorizeView> <AuthorizeView Roles="admin"> <Authorized> <div>Content for admin users.</div> </Authorized> </AuthorizeView>
You can also set the required roles for the entire component in the directive section. For example:
@attribute [Authorize(Roles ="admin,paid_user")] <div>This page is for admin or paid users only.</div>
The policy based rule provide more flexible in authorization, you can use built-in requirements or define your custom requirements.
A policy specifies requirements that a user must meet to access a resource, fail to comply any condition in the policy will lead to be denied access to the resource. For example, your website only sells special alcohol to premium adult user (the policy). In the other word, the user must be older than 18 (first requirement) and the user must be a premium user (second requirement). Your website refuses to sell special alcohol if a user failing any of those requirements.
A policy has 2 building blocks:
The requirement will yield the static data to authorize the user. A policy includes one or more requirements. A requirement is reusable and can be used in multiple policies. For example, to consider a user is an adult, the user must be 18 or older. 18 in this case is the static data to authorize, and you can declare a property in the requirement to yield value 18. If you don't have any static data to authorize, you still need to create a requirement.
The requirement handler is bound to a requirement, and responsible to check if the user satisfies the rule or not. Furthermore, the requirement handler will yield dynamic data to authorize the user. For example, your website sells video games, some games have ESRB rating, it requires the user must be older than 5, 7, 8, 13 or 18 in order to play that game. The required age is a dynamic number based on the game. The required age will be passed to the requirement handler.
The following steps will help you to implement the policy based authentication:
User
model, add a property to authorize the user. For example, we intend to authorize the user's age, so we will a new property age to the User
model:public class User { ... public int Age { get; set; } }
ToClaimsPrincipal
method to convert the list of roles to the ClaimsPrinciple
.public class User { ... public ClaimsPrincipal ToClaimsPrincipal() => new (new ClaimsIdentity(new Claim[] { .... new (nameof(Age), Age.ToString()) }, "BlazorSchool")); }
FromClaimsPrincipal
method to convert the ClaimsPrinciple
back to the user model.public class User { ... public static User FromClaimsPrincipal(ClaimsPrincipal principal) => new() { ... Age = Convert.ToInt32(principal.FindFirstValue(nameof(Age))) }; }
IAuthorizationRequirement
interface. For example:public class AdultRequirement : IAuthorizationRequirement { public int MinimumAgeToConsiderAnAdult { get; set; } = 18; }
AuthorizationHandler<T>
class where T
is the requirement class. For example:public class AdultRequirementHandler : AuthorizationHandler<AdultRequirement> { protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AdultRequirement requirement) { var user = User.FromClaimsPrincipal(context.User); if (user.Age >= requirement.MinimumAgeToConsiderAnAdult) { context.Succeed(requirement); } else { context.Fail(); } return Task.CompletedTask; } }
Call the Succeed(requirement)
method to indicate that the user passes the requirement. Otherwise, call Fail()
method.
Program.cs
. For example:builder.Services.AddScoped<IAuthorizationHandler, AdultRequirementHandler>();
builder.Services.AddAuthorizationCore(config => { config.AddPolicy("AdultOnly", policy => policy.AddRequirements(new AdultRequirement())); }
You can define multiple policies inside the AddAuthorizationCore
method. A policy also can have multiple requirements as well. For example:
builder.Services.AddAuthorizationCore(config => { ... config.AddPolicy("AdultAdminOnly", policy => { policy.AddRequirements(new AdultRequirement()); policy.RequireRole("admin"); }); });
<AuthorizeView Policy="AdultOnly"> <Authorized> <div>Content for users in adult policy.</div> </Authorized> <NotAuthorized> <div>This content is for adult.</div> </NotAuthorized> </AuthorizeView>
You can also set the policy for the entire component as well in the directive section. For example:
@attribute [Authorize(Policy = "AdultOnly")] <div>Content for users in adult policy.</div>
Sometimes, the rule is dynamically decide by the resource (the video game ESRB rating example). That is when you need to pass the resource data to the requirement handler.
public class EsrbRequirement : IAuthorizationRequirement { }
context.Resource
to receive the resource data. For example:public class EsrbRequirementHandler : AuthorizationHandler<EsrbRequirement> { protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, EsrbRequirement requirement) { var user = User.FromClaimsPrincipal(context.User); int minimumAge = Convert.ToInt32(context.Resource); if (user.Age >= minimumAge) { context.Succeed(requirement); } else { context.Fail(); } return Task.CompletedTask; } }
Program.cs
. For example:builder.Services.AddScoped<IAuthorizationHandler, EsrbRequirementHandler>(); builder.Services.AddAuthorizationCore(config => { config.AddPolicy("EsrbPolicy", policy => policy.AddRequirements(new EsrbRequirement())); });
<AuthorizeView Policy="EsrbPolicy" Resource="13"> <Authorized> <div>Content for users in esrb policy with age 13+.</div> </Authorized> <NotAuthorized> <div>This content is for 13+ user only.</div> </NotAuthorized> </AuthorizeView>
You cannot pass resource data when using the attribute.
In this section, we have collected some common mistakes from the Blazor School Discord Community.
FocusOnNavigate
component in the App.razor
When you are implementing the authorize on route approach, you will need to change your App.razor
component. Remember to keep the FocusOnNavigate
component when you are implementing this approach if you are not intended to remove it.
When you are implementing the authorize on route approach, you need to add [AllowAnonymous]
attribute to your login page to let the user login to the website. Otherwise, the users have no way to access your website.