In this chapter, we will learn how to add authentication and authorization to our blog because we don’t want just anyone to be able to create or edit blog posts.
Covering authentication and authorization could take a whole book, so we will keep things simple here. This chapter aims to get the built-in authentication and authorization functionalities working, building on the already existing functionality that’s built into ASP.NET. That means that there is not a lot of Blazor magic involved here; many resources already exist that we can take advantage of.
Almost every system today has some way to log in, whether it is an admin interface (like ours) or a member login portal. There are many different login providers, such as Google, Twitter, and Microsoft. We can use all of these providers since we will just be building on existing architecture.
Some sites might already have a database for storing login credentials, but for our blog, we will use a service called Auth0 to manage our users. It is a very powerful way to add many different social providers (if we want to), and we don’t have to manage the users ourselves.
We can check the option to add authentication when creating our project. The authentication works differently when it comes to Blazor Server and Blazor WebAssembly, which we will look at in more detail in this chapter.
We will cover the following topics in this chapter:
Make sure you have followed the previous chapters or use the Chapter07
folder as a starting point.
You can find the source code for this chapter’s end result at https://github.com/PacktPublishing/Web-Development-with-Blazor-Second-Edition/tree/main/Chapter08.
There are a lot of built-in functionalities when it comes to authentication. The easiest way to add authentication is to select an authentication option when creating a project.
We need to implement authentication separately for the Blazor Server project and the Blazor WebAssembly project because they work differently.
But there are still things we can share between these two projects. First, we need to set up Auth0.
Auth0 is a service that can help us with handling our users. There are many different services like this, but Auth0 is the one that seems to be a very good service to use. We can connect one or many social connectors, which will allow our users to log in with Facebook, Twitter, Twitch, or whatever we add to our site.
Even though all of this can be achieved by writing code ourselves, integration like this is a great way to add authentication fast and also get a very powerful solution. Auth0 is free for up to 7,000 users (which our blog probably won’t reach, especially not the admin interface).
It also has great functionality to add data to our users that we have access to. We will do that later in the chapter when we add roles to our users. You’ll need to take the following steps:
MyBlog
, for example. Then it’s time to select what kind of application type we are using. Is it a native app? Is it a Single-Page Web Application, Regular Web Application, or Machine to Machine Application?
This depends on what version of Blazor we are going to run.
But it won’t limit the functionality, only what we need to configure when setting up our application.
We will start with Blazor server, which is a regular web application. But we want to be able to use the same authentication for both Blazor Server and Blazor WebAssembly, and we can do that by selecting Single Page Application.
And if we are only making a Blazor Server Application, we should use Regular Web Application, but since we are doing both, select Single Page Web Application since this will make it possible to run both.
Next, we will choose what technology we are using for our project. We have got Apache, .NET, Django, Go, and many other choices, but we don’t have a choice for Blazor specifically, at least not at the time of writing.
Just skip this and click the Setting tab.
If we scroll down, we can change the logo, but we will skip that.
localhost
for now), and the port number.
Starting with .NET 6, the port numbers are random, so make sure you add your application’s port number:
https://localhost:PORTNUMBER/callback
https://localhost:PORTNUMBER/
Allowed Callback URLs are the URLs Auth0 will make a call to after the user authentication and Allowed Logout URLs are where the user should be redirected after logout.
Now press Save Changes at the bottom of the page.
We are done with configuring Auth0. Next, we will configure our Blazor application.
There are many ways to store secrets in .NET (a file that is not checked in, Azure Key Vault, etc.). You can use the one that you are most familiar with.
We will keep it very simple and store secrets in our appsettings.json
. Make sure to remember to exclude the file when you check in. You don’t check the secrets in source control.
To configure our Blazor Server project, follow these steps:
appsettings.json
and add the following code:
{
"Auth0": {
"Authority": "Get this from the domain for your application at Auth0",
"ClientId": "Get this from Auth0 setting"
}
}
These are the values we made a note of in the previous section.
Blazor server is an ASP.NET site with some added Blazor functionality, which means we can use a NuGet package to get some of the functionality out of the box.
Program.cs
and add the following code just before WebApplication app = builder.Build();
:
builder.Services
.AddAuth0WebAppAuthentication(options =>
{
options.Domain = builder.Configuration["Auth0:Authority"]??"";;
options.ClientId = builder.Configuration["Auth0:ClientId"]??"";;
});
app.UseRouting();
. This code will allow us to secure our site:
app.UseAuthentication();
app.UseAuthorization();
using
at the top of the file:
using Auth0.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
Minimal APIs are a great way to do this by adding two get methods. This way, we don’t need to create a Razor page.
Program.cs
, add the following code just before app.Run()
:
app.MapGet("authentication/login", async (string redirectUri, HttpContext context) =>
{
var authenticationProperties = new LoginAuthenticationPropertiesBuilder()
.WithRedirectUri(redirectUri)
.Build();
await context.ChallengeAsync(Auth0Constants.AuthenticationScheme, authenticationProperties);
});
When our site redirects to "authentication/login"
, the minimal API endpoint will kick off the login functionality.
app.MapGet("authentication/logout", async (HttpContext context) =>
{
var authenticationProperties = new LogoutAuthenticationPropertiesBuilder()
.WithRedirectUri("/")
.Build();
await context.SignOutAsync(Auth0Constants.AuthenticationScheme, authenticationProperties);
await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
});
The configuration is all set. Now, we need something to secure.
Blazor uses App.razor
for routing. To enable securing Blazor, we need to add a couple of components in the app component.
We need to add a CascadingAuthenticationState
, which will send the authentication state to all the components that are listening for it. We also need to change the route view to an AuthorizeRouteView
, which can have different views depending on whether or not you are authenticated:
App.razor
component should look like this:
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(App).Assembly" AdditionalAssemblies="new[] { typeof(Components.Pages.Index).Assembly}">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<Authorizing>
<p>Determining session state, please wait...</p>
</Authorizing>
<NotAuthorized>
<h1>Sorry</h1>
<p>You're not authorized to reach this page. You need to log in.</p>
</NotAuthorized>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
Now only two things remain, a page that we can secure and a login link display.
_Imports.razor
and add the namespaces:
@using Microsoft.AspNetCore.Components.Authorization
@using Components.RazorComponents
RazorComponents
folder, add a new interface called ILoginStatus
and replace the content with:
namespace Components.RazorComponents;
public interface ILoginStatus
{
}
RazorComponents
folder, add a new razor component called LoginStatus.razor
.
Replace the content with:
@implements ILoginStatus
<AuthorizeView>
<Authorized>
<a href="authentication/logout">Log out</a>
</Authorized>
<NotAuthorized>
<a href="authentication/login?redirectUri=/">Log in</a>
</NotAuthorized>
</AuthorizeView>
LoginStatus
is a component that will show a login link if we are not authenticated and a logout link if we are authenticated.
The code above is the Blazor Server implementation of that control. For Blazor WebAssembly, we need to change the component just a bit, but since all the components, including the layout, are in a shared library, it’s not entirely easy to do.
Here is where the DynamicComponent
component can help us. It makes it possible to load a component using a type or a string. We will solve this by dependency-injecting the component type we want the MainLayout
to use.
Shared/MainLayout.razor
and add the following:
@inject ILoginStatus status
We are injecting a component of the type LoginStatus
; another way would be to create an interface and use it instead, but to keep it simple, let’s use the LoginStatus
component for now.
<DynamicComponent Type="@status.GetType()"/>
So, based on what type of component the dependency injection returns to us, it will render that component.
In the next section, we will also create one for Blazor WebAssembly.
authorize
attribute to the component we wish to secure. The choices are:
Pages/Admin/BlogPostEdit.razor
Pages/Admin/BlogPostList.razor
Pages/Admin/CategoryList.razor
Pages/Admin/TagList.razor
Add the following attribute to all of them:
@attribute [Authorize]
Program.cs
, add the following line:
builder.Services.AddTransient<ILoginStatus,LoginStatus>();
using Components.RazorComponents;
The dependency injection will return an instance of an ILoginStatus
and we will get the LoginStatus
class.
This is all it takes, some configuration, and then we are all set.
Now set the BlazorServer
-project as a startup project and see if you can access the /admin/blogposts
page (spoiler: you shouldn’t be able to); log in (create a user), and see if you can access the page now.
Our admin interface is secured.
In the next section, we will secure the Blazor WebAssembly version of our blog and the API.
The WebAssembly project has some of the same functionalities; it is a bit more complicated because it requires API authentication, but we will start with securing the client.
By default (if we choose to add authentication when we create the project), it will use IdentityServer
to authenticate both the client and the API.
We will use Auth0 for this instead, the same application we created earlier in this chapter:
wwwroot
folder, add a new JSON file called appsettings.json
.
The appsettings
file will automatically be picked up by Blazor.
{
"Auth0": {
"Authority": "Get this from the domain for your application from Auth0",
"ClientId": "Get this from Auth0 setting"
}
}
Replace the values with the same ones we did with the Blazor Server project – the values from the Setting up Authentication section.
Make sure to add https://
at the beginning of the Authority
(this is not needed in the Blazor Server project).
Microsoft.AspNetCore.Components.WebAssembly.Authentication
Microsoft.Extensions.Http
HttpClient
.
In Program.cs
, add the following lines:
builder.Services.AddHttpClient("Public",
client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress));
builder.Services.AddHttpClient("Authenticated", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
We will create one for getting requests (non-authenticated calls) called public
, and one for authenticated calls called authenticated
.
These are the names we used in Chapter 7, Creating an API.
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Data;
IBlogAPI
, we will get the BlogApiWebClient
that we created in Chapter 7, Creating and API.
In Program.cs
, add the following code:
builder.Services.AddTransient<IBlogApi, BlogApiWebClient>();
builder.Services.AddOidcAuthentication(options =>
{
builder.Configuration.Bind("Auth0", options.ProviderOptions);
options.ProviderOptions.ResponseType = "code";
});
We are getting the configuration from our appsettings.json
file. In this case, we are using built-in functionality in .NET instead of using a library that Auth0 has provided for us.
wwwroot/index.html
, we need to add a reference to JavaScript.</body>
tag, add this JavaScript:
<script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>
Great, our app is configured. Next, let’s secure it.
Now everything is prepared for us to secure our WebAssembly app.
This process is pretty much the same as for the Blazor server, but we need to implement it a bit differently.
We need to add a CascadingAuthenticationState
, which will send the authentication state to all the components that are listening for it. We also need to change the route view to an AuthorizeRouteView
, which can have different views depending on whether or not you are authenticated:
App.razor
component should look like this:
@using Microsoft.AspNetCore.Components.Authorization
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(App).Assembly" AdditionalAssemblies="new[] { typeof(Components.Pages.Index).Assembly}">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<Authorizing>
<p>Determining session state, please wait...</p>
</Authorizing>
<NotAuthorized>
<h1>Sorry</h1>
<p>You're not authorized to reach this page. You need to log in.</p>
</NotAuthorized>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
Now only two things remain, a page that we can secure and a login link display.
_Imports.razor
and add the namespace:
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Authorization
@using Components.RazorComponents;
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
LoginStatusWasm.razor
. This is the same component we created in our shared library, but this one is specific for WebAssembly.@implements ILoginStatus
@inject NavigationManager Navigation
<AuthorizeView>
<Authorized>
<a href="#" @onclick="BeginSignOut">Log out</a>
</Authorized>
<NotAuthorized>
<a href="authentication/login">Log in</a>
</NotAuthorized>
</AuthorizeView>
@code {
private async Task BeginSignOut(MouseEventArgs args)
{
Navigation.NavigateToLogout("authentication/logout");
}
}
NavigateToLogout
. LoginStatusWasm
is a component that will show a login link if we are not authenticated and a logout link if we are authenticated.
We also have a route called authentication that we will implement in just a bit.
The LoginStatusWasm
will be injected using dependency injection, just like we did with the Blazor Server implementation.
Program.cs
, add the following line:
builder.Services.AddTransient<ILoginStatus, LoginStatusWasm>();
When our MainLayout
is rendered, it will get an instance of LoginStatusWasm
and render that component.
Now it’s time to implement the authentication route. Create a new Razor component called Authentication.razor
and add the following code:
@page "/authentication/{action}"
@inject NavigationManager Navigation
@inject IConfiguration Configuration
<RemoteAuthenticatorView Action="@Action">
<LogOut>
@{
var authority = Configuration["Auth0:Authority"]??string.Empty;
var clientId = Configuration["Auth0:ClientId"]?? string.Empty;
Navigation.NavigateTo($"{authority}/v2/logout?client_id={clientId}");
}
</LogOut>
</RemoteAuthenticatorView>
@code{
[Parameter] public string Action { get; set; } = "";
}
It uses a built-in component called RemoteAuthenticatorView
. It makes the necessary calls and also makes sure to protect us from cross-site calls.
The call to await SignOutManager.SetSignOutState();
that we added in our LoginStatusWasm
component will set a state that will be checked in the RemoteAuthenticatorView
.
We now have secured our WebAssembly project. We also need to secure the pages we want to protect, but since they are in the Components project, they are already secured since we did that in the Securing Blazor Server section.
We also need to update the Auth0 Allowed Logout URLs and Allowed Callback URLs as follows:
https://localhost:PORTNUMBER/authentication/login-callback
Note: this port number is something else (not the same port we added earlier).
In my case it looks like this:
https://localhost:7174/callback
, https://localhost:7276/authentication/login-callback
https://localhost:PORTNUMBER
In my case, it looks like this: https://localhost:7276/
, https://localhost:7174/
Set the BlazorWebAssembly.Server project as our startup project and run.
We should now be able to click Login in the top right corner, log in, and you will end up with a logout link in the top left corner.
If we navigate to /admin/blogposts
, we will see a list of blog posts if we are authenticated; if we are not, we will see a message saying: Sorry, You’re not authorized to reach this page. You need to log in.
Fantastic! Our pages are secure, but our API is still wide open. We need to secure the API using the same login mechanism used to secure the client.
When working with Blazor WebAssembly, we need a central place that handles authentication since we need to authenticate both the client and use the same authentication for the API.
Auth0 has support for APIs as well.
To secure our API, we need to let Auth0 know about the API:
MyBlogAPI
, and add an identifier, https://MyBlogApi
. This is what we will later use as Audience; Auth0 will never call this URL.
Leave Signing Algorithm as is.
Our authentication for our API is all done; next, we will limit access inside the API.
Now we need to configure the API:
Microsoft.AspNetCore.Authentication.JwtBearer
Program.cs
and add the using
statement:
using Microsoft.AspNetCore.Authentication.JwtBearer;
var app=builder.Build;
:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, c =>
{
c.Authority = builder.Configuration["Auth0:Authority"];
c.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidAudience = builder.Configuration["Auth0:Audience"],
ValidIssuer = builder.Configuration["Auth0:Authority"]
};
});
builder.Services.AddAuthorization();
app.UseRouting();
, add:
app.UseAuthentication();
app.UseAuthorization();
appsettings.json
and add:
"Auth0": {
"Authority": "Get this value from the Domain in Auth0 application settings",
"Audience": "Get this value from the Identifier in Auth0 API settings"
}
Replace the placeholder with values from Auth0. Make sure to add https://
at the beginning of the Authority.
wwwroot/appsettings.json
in the Auth0 JSON object, and add:
"Audience": "Get this value from the Identifier in Auth0 API settings"
Program.cs
, inside the AddOidcAuthentication
method call, add:
options.ProviderOptions.AdditionalProviderParameters.Add("audience", builder.Configuration["Auth0:Audience"]);
That should be all it takes to configure the API.
Set BlazorWebAssembly.Server as the startup project and run the project. You should now be able to log in, add blog posts, and manage tags and categories.
But what if different users have different permissions?
That is where roles come in.
Blazor Server and Blazor WebAssembly handle roles a bit differently; it’s nothing major but we need to do different implementations.
Let’s start by adding roles in Auth0:
Administrator
and the description Can do anything
and press Create.We do that by adding an action.
Flows are a way to execute code in a particular flow.
We want Auth0 to add our roles when we log in.
Add Roles
, leave Trigger and Runtime as is, and press Create.
We will see a window where we can write our action.
/**
* @param {Event} event - Details about the user and the context in which they are logging in.
* @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
*/
exports.onExecutePostLogin = async (event, api) => {
const claimName = 'http://schemas.microsoft.com/ws/2008/06/identity/claims/role'
if (event.authorization) {
api.idToken.setCustomClaim(claimName, event.authorization.roles);
api.accessToken.setCustomClaim(claimName, event.authorization.roles);
}
}
Now we have an action that will add the roles to our login token.
Our user is now an administrator. It’s worth noting that roles are a paid feature in Auth0 and will only be free during the trial.
Now let’s set up Blazor Server to use this new role.
Since we are using the Auth0 library the setup is almost done for Blazor Server.
Let’s modify a component to show if the user is an administrator:
Shared/NavMenu.razor
:
At the top of the component, add:
<AuthorizeView Roles="Administrator">
<Authorized>
Hi admin!
</Authorized>
<NotAuthorized>
You are not an admin =(
</NotAuthorized>
</AuthorizeView>
Set BlazorServer as a startup project and run it.
If we log in, we should be able to see text to the left saying, Hi Admin! in black text on top of dark blue, so it might not be very visible. We will take care of this in Chapter 9, Sharing Code and Resources.
Next, we will add the same roles to the BlazorWebAssembly project.
Adding roles to Blazor WebAssembly is almost as easy. There is one challenge we need to fix first.
When we get the roles from Auth0, we get them as an array, but we need to split them up into separate objects, and to do that, we need to create a class that does that for us:
ArrayClaimsPrincipalFactory.cs
.using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
using System.Security.Claims;
using System.Text.Json;
namespace BlazorWebAssembly.Client;
public class ArrayClaimsPrincipalFactory<TAccount> : AccountClaimsPrincipalFactory<TAccount> where TAccount : RemoteUserAccount
{
public ArrayClaimsPrincipalFactory(IAccessTokenProviderAccessor accessor)
: base(accessor)
{ }
public async override ValueTask<ClaimsPrincipal> CreateUserAsync(TAccount account, RemoteAuthenticationUserOptions options)
{
var user = await base.CreateUserAsync(account, options);
var claimsIdentity = (ClaimsIdentity?)user.Identity;
if (account != null)
{
foreach (var kvp in account.AdditionalProperties)
{
var name = kvp.Key;
var value = kvp.Value;
if (value != null && (value is JsonElement element && element.ValueKind == JsonValueKind.Array))
{
claimsIdentity?.RemoveClaim(claimsIdentity.FindFirst(kvp.Key));
var claims = element.EnumerateArray()
.Select(x => new Claim(kvp.Key, x.ToString()));
claimsIdentity?.AddClaims(claims);
}
}
}
return user;
}
}
The class checks if the roles we got back are in an array, and if so, splits them up into multiple entries.
In the Git repo, there is a page in the components project showing the roles if you would like to dig deeper (Pages/AuthTest.razor
).
Program.cs
, add the following just after the call to AddOidcAuthentication
:
.AddAccountClaimsPrincipalFactory<ArrayClaimsPrincipalFactory <RemoteUserAccount>>();
In the end, it should look something like this:
builder.Services.AddOidcAuthentication(options =>
{
//Removed for brevity
}).AddAccountClaimsPrincipalFactory<ArrayClaimsPrincipalFactory <RemoteUserAccount>>();
Set BlazorWebAssembly.Server as a startup project and run it. If we log in, we should be able to see text to the left saying Hi Admin! in black text on top of dark blue, so it might not be very visible. We will take care of this in Chapter 9, Sharing Code and Resources.
Awesome! We have authentication and authorization working for both Blazor Server and Blazor WebAssembly and secured our API!
In this chapter, we learned how to add authentication to our existing site. It is easier to add authentication at the point of creating a project, but now we have a better understanding of what is going on under the hood and how to handle adding an external source for authentication.
Throughout the book, we have been sharing components between the two projects.
In the next chapter, we will look at sharing even more things like static files and CSS and try to make everything look nice.