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:
If you prefer the CLI:
dotnet new blazorwasm --auth Individual --hosted
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
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>();
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>();
Now we need to make some changes to the following code:
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
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");
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
}
}
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();
}
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)
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);
}
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)
And the "Administrator" role assignment:
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>
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.
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>
...
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:
After logging in with the admin user, we can see the menu element again:
namespace BlazorAuth.Server.Controllers
{
[Authorize] // replace this line
[Authorize(Roles = "Administrator")] // with this line
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
...
}
}
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 .
Top comments (1)
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