Implementing authorization

✅ Similar to Blazor WebAssembly

Implementing authorization in Blazor is an essential aspect of building secure web applications. Blazor provides various authorization features such as route and component-level authorization, role-based authorization, and policy-based authorization. This tutorial covers the following topics to guide you through the process of implementing authorization in your Blazor application:

  • Authorization approaches overview.
  • Implementing route-level authorization.
  • Implementing component-level authorization.
  • Access control mechanisms overview.
  • Implementing role-based authorization.
  • Implementing policy-based authorization.
  • Common mistakes.
You can download the example code used in this topic on GitHub.

Authorization approaches overview

Blazor provides two approaches to implementing authorization:

  • Route-level authorization
  • Component-level authorization

Route-level authorization enables you to control access to an entire page or set of pages based on user roles or policies. Component-level authorization allows you to control access to individual components within a page based on the same criteria.

By using these two approaches in combination, you can implement fine-grained access control over your Blazor application, ensuring that users only have access to the resources they are authorized to view or modify.

Route-level authorization

Route-level authorization in Blazor assumes that every route in your web application requires the user to be authenticated. However, you can also specify additional requirements for certain routes, such as requiring the user to have an admin role or be of a certain age. You can either explicitly specify these requirements or remove them entirely for each route.

Since a route is bound to an independent component, any requirements specified for a single route will be applied to all other routes within that component.

With route-level authorization, you can authorize your components as a whole. You can also combine this approach with component-level authorization to control access to specific parts of the UI.

The following image illustrates how the route-level authorization approach works:

route-level-authorization.png

Component-level authorization

Component-level authorization allows you to control access to individual components within a page, which means that you can display different parts of a component to different users based on their roles or policies. The following image illustrates how the authorization on an individual component approach works:

component-level-authorization.png

How to choose between route-level and component-level authorization?

The choice between route-level authorization and component-level authorization depends on your specific requirements and the level of control you need over access to your application resources.

Route-level authorization is suitable if you need to control access to entire pages or sets of pages based on user roles or policies. This approach is useful for applications where all users have the same level of access to a page or set of pages.

On the other hand, component-level authorization is ideal if you need fine-grained control over access to individual components within a page. This approach allows you to display different parts of a component to different users based on their roles or policies. It is useful for applications where users have different levels of access to different parts of a page.

In some cases, you may need to use both route-level and component-level authorization to achieve the level of access control required for your application.

Ultimately, the choice between route-level and component-level authorization will depend on your specific application requirements and the level of control you need over access to your application resources.


Implementing route-level authorization

To implement route-level authorization in your Blazor application, it is recommended that you have a good understanding of the previous tutorials. Follow these steps to set up route authorization:

  1. Replace the existing RouteView component with the AuthorizeRouteView component in your App.razor file. Here is an 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>
  1. Define the NotAuthorized and Authorizing projected content inside the AuthorizeRouteView component. This will be displayed when the user is not authorized to access a resource or when the application is authorizing the user's access.
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)">
    <NotAuthorized>
        <div>You are not authorized.</div>
    </NotAuthorized>
    <Authorizing>
        <div>Authorizing...</div>
    </Authorizing>
</AuthorizeRouteView>
  1. Authorize the entire website in the _Imports.razor file, like this:
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]

When you create a new independent component, you need to decide whether anyone can access it or if it's only available to authenticated users. By default, any new independent component is only accessible to authenticated users. If you want unauthenticated users to access the component, you can add the @attribute [AllowAnonymous] directive to the component's section.


Implementing component-level authorization

To implement component-level authorization in your Blazor application, follow these steps:

  1. Wrap the content that needs to be authorized in the AuthorizeView component, like this:
<AuthorizeView>
    <Authorized><div>Authenticated</div></Authorized>
    <Authorizing><div>Loading...</div></Authorizing>
    <NotAuthorized><div>Not authenticated</div></NotAuthorized>
</AuthorizeView>
  1. Within the AuthorizeView component, define the content that should be displayed for each scenario:
  • The Authorized content will be displayed if the user is authorized to access the resource.
  • The Authorizing content will be displayed while the application is determining the user's authorization status.
  • The NotAuthorized content will be displayed if the user is not authorized to access the resource.

By wrapping your content in the AuthorizeView component and defining the appropriate content for each scenario, you can ensure that only authorized users are able to view or interact with the content within your component.


Access control mechanisms overview

In a Blazor application, a resource such as a route, component, or button can be protected by an authorization rule. When a user meets all of the requirements of an authorization rule, they are granted access to the resource. There are two main types of authorization rules that can be used to control access in Blazor:

  1. Role-based rules: These rules grant access based on the user's role within the system. For example, an administrator may be granted access to certain parts of the application that are not available to regular users.
  2. Policy-based rules: These rules are more flexible and allow access to be granted based on a wide range of factors, such as the user's role, specific permissions, or custom criteria defined by the application.

Implementing role-based authorization

Role-based authorization is a common way to control access to resources in a Blazor application. With this approach, the resource is protected by a list of roles, and when a user belongs to one of the roles in the list, they are granted access to the resource.

What is a role?

A role is simply a string that can be assigned to a user to define their access level within the application. However, to include the role in a ClaimsIdentity, it must be assigned under the type ClaimTypes.Role.

Implementation

To implement role-based authentication in your Blazor application, you can follow these steps:

  1. Add a property for roles in the User model, as a user may have multiple roles.
public class User
{
    ...
    public List<string> Roles { get; set; } = new();
}
  1. Update the ToClaimsPrincipal method to convert the list of roles to the ClaimsPrincipal.
public class User
{
    ...
    public ClaimsPrincipal ToClaimsPrincipal() => new (new ClaimsIdentity(new Claim[]
    {
        ...
    }.Concat(Roles.Select(r => new Claim(ClaimTypes.Role, r)).ToArray()),
    "BlazorSchool"));
}
  1. Update the FromClaimsPrincipal method to convert the ClaimsPrincipal 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()
    };
}
  1. Set the required roles for UI elements using the Roles parameter in the AuthorizeView component. For example:
<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>

Alternatively, you can 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>

Implementing policy-based authorization

Policy-based rules provide more flexibility in authorization. You can use built-in requirements or define your custom requirements to meet the specific needs of your application.

What is a Policy?

A policy specifies the requirements that a user must meet to access a resource. If a user fails to comply with any of the conditions in the policy, they will be denied access to the resource. For example, suppose your website only sells special alcohol to premium adult users. In that case, the policy would require that the user be older than 18 (the first requirement) and a premium user (the second requirement). Your website will refuse to sell special alcohol if the user fails to meet either of those requirements.

Implementation

A policy has 2 building blocks:

  1. The requirement.
  2. The requirement handler.

The requirement provides the static data needed to authorize the user. A policy includes one or more requirements, which can be reused in multiple policies. For example, to consider a user an adult, they must be 18 or older. In this case, 18 is the static data used to authorize the user, and you can declare a property in the requirement to yield this value. 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 is responsible for checking if the user satisfies the rule or not. The requirement handler also provides dynamic data to authorize the user. For example, your website sells video games that have ESRB ratings, which require the user to be older than 5, 7, 8, 13, or 18 to play the game. The required age is a dynamic number based on the game, and it is passed to the requirement handler.

To implement policy-based authentication, follow these steps:

  1. Add a property to the User model to authorize the user. For example, if you want to authorize the user's age, add a new property Age to the User model:
public class User
{
    ...
    public int Age { get; set; }
}
  1. Update the ToClaimsPrincipal method to convert the list of roles to the ClaimsPrincipal:
public class User
{
    ...
    public ClaimsPrincipal ToClaimsPrincipal() => new (new ClaimsIdentity(new Claim[]
    {
        ....
        new (nameof(Age), Age.ToString())
    }, "BlazorSchool"));
}
  1. Update the FromClaimsPrincipal method to convert the ClaimsPrincipal back to the user model:
public class User
{
    ...
    public static User FromClaimsPrincipal(ClaimsPrincipal principal) => new()
    {
        ...
        Age = Convert.ToInt32(principal.FindFirstValue(nameof(Age)))
    };
}
  1. Create the requirement. The requirement class must implement the IAuthorizationRequirement interface. For example:
public class AdultRequirement : IAuthorizationRequirement
{
    public int MinimumAgeToConsiderAnAdult { get; set; } = 18;
}
  1. Create the requirement handler. The requirement handler class must extend the 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 the Fail() method.

  1. Register the handler in Program.cs. For example:
builder.Services.AddScoped<IAuthorizationHandler, AdultRequirementHandler>();
  1. Define the policy. For example:
builder.Services.AddAuthorizationCore(config =>
{
    config.AddPolicy("AdultOnly", policy => policy.AddRequirements(new AdultRequirement()));
}

You can define multiple policies inside the AddAuthorizationCore method. A policy can also have multiple requirements. For example:

builder.Services.AddAuthorizationCore(config =>
{
    ...
    config.AddPolicy("AdultAdminOnly", policy =>
    {
        policy.AddRequirements(new AdultRequirement());
        policy.RequireRole("admin");
    });
});
  1. Set the required policy for the UI. For example:
<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 in the directive section. For example:

@attribute [Authorize(Policy = "AdultOnly")]

<div>Content for users in adult policy.</div>

How to pass resource metadata to an authorization requirement handler

In some cases, authorization rules may need to be dynamically determined based on the resource being accessed. For example, in the context of a video game, the ESRB rating of a game could dictate the minimum age requirement for accessing certain content. In such cases, it is necessary to pass the resource metadata to the requirement handler. Here is how to do it:

  1. Create the requirement. For example:
public class EsrbRequirement : IAuthorizationRequirement
{
}
  1. Create the requirement handler and use the context.Resource property to receive the resource metadata. 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;
    }
}
  1. Register the requirement handler and define the policy in the Program.cs file. For example:
builder.Services.AddScoped<IAuthorizationHandler, EsrbRequirementHandler>();
builder.Services.AddAuthorizationCore(config =>
{
    config.AddPolicy("EsrbPolicy", policy => policy.AddRequirements(new EsrbRequirement()));
});
  1. Set the required policy for the UI and pass the resource data to the requirement handler. This is not possible when using the attribute-based approach. Instead, use the following code snippet as an example:
<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>

Common mistakes

This section highlights some common mistakes that members of the Blazor School Discord Community have encountered.

Mistake #1: Accidentally removing the FocusOnNavigate component in the App.razor

When implementing the route-level authorization approach, modifying your App.razor component is crucial. However, it's essential to retain the FocusOnNavigate component while implementing this approach to avoid issues.

Mistake #2: Authorizing the entire website, including the login page

When implementing the route-level authorization approach, it's vital to add the [AllowAnonymous] attribute to your login page. Failure to do so would make it impossible for users to access your website.

BLAZOR SCHOOL
Designed and built with care by our dedicated team, with contributions from a supportive community. We strive to provide the best learning experience for our users.
Docs licensed CC-BY-SA-4.0
Copyright © 2021-2024 Blazor School
An unhandled error has occurred. Reload 🗙