DEV Community

loading...

Role-based Authentication with hosted Blazor template

mecoupz profile image Yasin Originally published at yasink.at ・4 min read

In this small tutorial, I'm going to show you something that I've struggled a lot in the last couple of days: Role-based authentication with the hosted Blazor template from the .NET CLI/Visual Studio.

First of all, we're going to create a new project:
Alt Text

If you prefer the CLI:

dotnet new blazorwasm --auth Individual --hosted
Enter fullscreen mode Exit fullscreen mode

Alt Text

Next, we'll run dotnet ef database update in the console inside the Server projects folder so EntityFramework can run the database migrations.

dotnet ef database update
Enter fullscreen mode Exit fullscreen mode

Alt Text

Then we need to make some changes in the Startup.cs class in the Server project:

You'll find a line inside the ConfigureServices method which adds the default Identity to our server:

services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>();
Enter fullscreen mode Exit fullscreen mode

We need to add one line to add our roles to the Identity:

services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();
Enter fullscreen mode Exit fullscreen mode

Now we need to make some changes to the following code:

services.AddIdentityServer()
    .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
Enter fullscreen mode Exit fullscreen mode

We'll expand it, so our Server always adds the role to the JWT token when the user requests his/her token:

services.AddIdentityServer()
    .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options =>
        {
            options.IdentityResources["openid"].UserClaims.Add("role");
            options.ApiResources.Single().UserClaims.Add("role");
        }
    );
System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler
      .DefaultInboundClaimTypeMap.Remove("role");
Enter fullscreen mode Exit fullscreen mode

I've also created a small helper method to create an admin Account, create all Roles from an Enum and assigned the Administrator role to the admin Account.

Create a new enum inside the Shared project:

// Role.cs
using System.ComponentModel;

namespace Shared
{
    public enum Role
    {
        [Description("Administrator")]
        Administrator,
        [Description("Free")]
        Free,
        [Description("Paid")]
        Paid
    }
}
Enter fullscreen mode Exit fullscreen mode

Helper method inside Startup.cs of the Server project:

private void CreateRoles(IServiceProvider serviceProvider)
{
    var roleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
    var userManager = serviceProvider.GetRequiredService<UserManager<ApplicationUser>>();
    Task<IdentityResult> roleResult;
    string email = "admin@domain.com";
    string securePassword = "Pa$$w0rd!";

    foreach (var role in Enum.GetValues(typeof(Role)))
    {
        Task<bool> roleExists = roleManager.RoleExistsAsync(role.ToString());
        roleExists.Wait();

        if (!roleExists.Result)
        {
            roleResult = roleManager.CreateAsync(new IdentityRole(role.ToString()));
            roleResult.Wait();
        }
    }

    Task<ApplicationUser> adminUser = userManager.FindByEmailAsync(email);
    adminUser.Wait();

    if (adminUser.Result == null)
    {
        var admin = new ApplicationUser();
        admin.Email = email;
        admin.UserName = email;

        Task<IdentityResult> newUser = userManager.CreateAsync(admin, securePassword);
        newUser.Wait();
    }

    var createdAdminUser = userManager.FindByEmailAsync(email);
        createdAdminUser.Wait();
    createdAdminUser.Result.EmailConfirmed = true; // confirm email so we can login
    Task<IdentityResult> newUserRoleAssignment = userManager.AddToRoleAsync(createdAdminUser.Result, Role.Administrator.ToString());
    newUserRoleAssignment.Wait();
}
Enter fullscreen mode Exit fullscreen mode

Now we need to call this helper method from the Configure method inside the Startup.cs. But first, we need to change our method signature (we need IServiceProvider inside this class, so we'll expect it from .NET Core's built in Dependency Injection:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider)
Enter fullscreen mode Exit fullscreen mode

Now we can add the following line at the bottom of the Configure method:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider)
{
    ...
    CreateRoles(serviceProvider);
}
Enter fullscreen mode Exit fullscreen mode

This change will create our 3 defined Roles inside the database: Administrator, Free and Paid.

After running the app with dotnet run you can see, that all 3 roles and our admin user have been created:

(I'm using VSCode with the SQLite extension to view data from the SQLite Database file)

Roles:
Alt Text

Admin User:
Alt Text

And the "Administrator" role assignment:
Alt Text

Now, our server knows about Roles and will also send the roles back via the JWT token.

To read and show all Claims from our Blazor frontend, we can expand the Index.razor inside our Client project with the following (look for the tag):

@page "/"

<h1>Hello, world!</h1>

Welcome to your new app.

<SurveyPrompt Title="How is Blazor working for you?" />

<AuthorizeView>
    <ul>
        @foreach (var item in context.User.Claims)
        {
            <li>Type: @item.Type, Value: @item.Value</li>
        }
    </ul>
</AuthorizeView>
Enter fullscreen mode Exit fullscreen mode

This will show us all the claims (and roles) after we've signed in. Let's run the app with dotnet run or dotnet watch run from inside the Server folder.

We're able to login with our Admin User and as you can see from the image below, our Role is exposed through the token to our Blazor frontend.

Alt Text

To test this out, we'll make 2 changes to the following files:

NavMenu.razor: wrap the last menu point "Fetch data" with an AuthorizeView component (built into Blazor) and a defined role:

// NavMenu.razor
...
<AuthorizeView Roles="Administrator">
    <li class="nav-item px-3">
        <NavLink
            class="nav-link"
            href="fetchdata"
        >
            <span
                class="oi oi-list-rich"
                aria-hidden="true"
            ></span> Fetch data
        </NavLink>
    </li>
</AuthorizeView>
...
Enter fullscreen mode Exit fullscreen mode

That means, that any user who has not the Role "Administrator" defined and exposed via the token, won't be able to see this menu element:
Alt Text

After logging in with the admin user, we can see the menu element again:
Alt Text

namespace BlazorAuth.Server.Controllers
{
    [Authorize] // replace this line
    [Authorize(Roles = "Administrator")] // with this line
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
       ...
    }
}
Enter fullscreen mode Exit fullscreen mode

With this last change, we've secured our Fetch data page on the frontend only for users in the Administrator role and our Controller too. That means, when a user does not have this role, any call to this endpoint (Controller) or the page via the browser, won't get any data back.

I hope that this tutorial helped you. If you have any further questions or feedback to this, ping me on Twitter https://twitter.com/mecoupz .

Discussion (1)

pic
Editor guide
Collapse
tisquip profile image
Kudzanayi Takaendesa

Great post, after struggling for hours, I found your post - Life saver. Well I got my solution working, but its only working properly when the user is only in one role, if the user is in two roles, the attibute with a role is ignore (even Identity.IsInRole()), Its got something to do with this post github.com/dotnet/aspnetcore/issue... . So heads up to anyone who reads this post and implements it, if its not working in your case, maybe your user has two roles