In this article, we are going to learn how to use Azure Active Directory to secure our Blazor WebAssembly hosted application. In the previous article, we’ve learned how to do the same thing with a Blazor WebAssembly standalone app, but this time, we have a server-side application too, which we need to register on Azure AD as well. So, we are going to split the Azure AD registration process into two steps. After that, we are going to create a new application and discover all the things we have to accomplish to secure Blazor WebAssembly hosted app with the Azure Active Directory.
Let’s get going.
Server App Registration in Azure Active Directory
Before we start, we will assume you have an Azure account, which you can use for Azure Active Directory registration.
After we login to our Azure portal, we are going to choose the Azure Active Directory service:
Under the Manage section, we are going to click the App Registrations
link, and then the New registration
button:
Then, we have to provide the required information for our server app:
So, as you can see, we have to provide a name for the server app and decide who can access this application. Also, since this is a server app, we don’t require a redirect URI, so we are going to leave it as-is.
After clicking the Register
button, we will see the Overview
page with our app’s information:
We have to remember the Application (client) ID and the Directory (tenant) ID values because we are going to use them soon.
Also, under the Manage section, we can click the Branding link and save the Publisher domain
value (it looks like: {CUSTOM_NAME}.onmicrosoft.com).
Since our server app doesn’t require sign-in or user profile access, we can navigate to the API permissions
menu, under the Manage section, and remove the User.Read
permission:
Excellent.
Now, we have to expose our API.
To do that, we are going to click the Expose an API link, under the Manage section, and then the Add a scope
button:
In the provided screen with the Application ID URI field, we can just click the Save and continue
button.
Then, we have to populate the required data and click the Add scope
button:
Also, the State
option must be Enabled
, as it should be by default.
After we create a scope, we are going to see it in the list:
We can copy and save this value since it contains both the App ID URI and the Scope name.
Client App Registration in Azure Active Directory
In our previous article, we’ve registered the client standalone application with Azure AD and we are going to follow the same steps here.
So, let’s navigate one more time to Azure Active Directory, click on the App registrations
link, and click the New registration
button:
With this registration process, we have to select a single-page application option and provide a redirect URI. We will force our app, once we create one, to run on port 5001.
In the Overview screen, we only have to record the Application (client) ID
value. The Directory (tenant) ID
value is the same and we already have it.
Now, to confirm the client app registration, we can navigate to the Authentication page:
Here, we can confirm our redirect URI is valid and also we can see that we are going to use Authorization Code Flow with PKCE, which is a recommended flow for a SPA. If you want to use the Implicit or the Hybrid flow, you can check any of the two checkboxes (Access tokens and ID tokens) provided a bit below on the Authentication page. Since we are using the Authorization Code flow, we are not going to check any of these options.
If you remember the last step of the server app registration, we have created an access scope for our API. Now, we can use it for the client app registration.
To do that, let’s navigate to the API permissions
page and click the Add a permission
button:
We can also see the User.Read
permission, which we are going to keep for the client app.
After clicking the Add a permission button, we can navigate to the My APIs menu and select the server app:
After clicking the application’s name, we can see our BlazorHostedAPI.Access
permission. All we have to do is to check it and click the Add permissions
button:
Now, we can see both permissions (API and Microsoft Graph) but our new API permission doesn’t have an admin consent granted. So, we have to grant it by clicking the Grant button:
That is it.
We can continue with the app creation.
Creating a New Blazor WebAssembly Hosted App
We can create our template app in two ways. By using the dotnet new
command or by using a Visual Studio template project. We will show you how to use the dotnet new
command but to show you the setup step by step process, we are going to use the Visual Studio template.
So, if we want to use the command, we have to provide our previously saved information:
dotnet new blazorwasm -au SingleOrg --api-client-id "{SERVER API APP CLIENT ID}" --app-id-uri "{SERVER API APP ID URI}" --client-id "{CLIENT APP CLIENT ID}" --default-scope "{DEFAULT SCOPE}" --domain "{TENANT DOMAIN}" -ho -o {APP NAME} --tenant-id "{TENANT ID}"
In our case:
- SERVER API APP CLIENT ID is the Application ID of the server app =>
862c81f0-91e8-476e-8646-b7c297967ec9
- SERVER API APP ID URI is the Application ID URI of the server app =>
862c81f0-91e8-476e-8646-b7c297967ec9
(you have to use it without theapi://
part) - CLIENT APP CLIENT ID is the Application ID of the client app =>
f3fea9a1-5ccd-4ff0-a446-2be905a9f67a
- DEFAULT SCOPE is the scope we’ve created in API permissions =>
BlazorHostedAPI.Access
- TENANT DOMAIN is the primary domain =>
MarinkoBlogCodeMaze.onmicrosoft.com
- APP NAME is the name of our application =>
BlazorWasmHostedAAD
- TENANT ID is the Directory (tenant) ID value =>
acd9a136-2bfa-485e-934b-4fb1c6fea421
dotnet new blazorwasm -au SingleOrg --api-client-id "862c81f0-91e8-476e-8646-b7c297967ec9" --app-id-uri "862c81f0-91e8-476e-8646-b7c297967ec9" --client-id "f3fea9a1-5ccd-4ff0-a446-2be905a9f67a" --default-scope "BlazorHostedAPI.Access" --domain "MarinkoBlogCodeMaze.onmicrosoft.com" -ho -o BlazorWasmHostedAAD --tenant-id "acd9a136-2bfa-485e-934b-4fb1c6fea421"
After we press the Enter
key, this will create a new application for us with all the required configurations.
You have to pay attention that even though we removed the api://
part from the APP ID URI value, it will be present inside the Program.cs
file of the client project:
options.ProviderOptions.DefaultAccessTokenScopes.Add("api://862c81f0-91e8-476e-8646-b7c297967ec9/BlazorHostedAPI.Access");
The command adds it by default.
If we want to use Visual Studio for the project creation, we have to create a new Blazor WebAssembly Hosted app with the Microsoft identity platform authentication type:
This is going to create a template for us with the placeholders for the required information.
Firstly, we are going to modify the server’s launchsettings.json
file to force the app to always start on port 5001:
{ "profiles": { "BlazorWebAssemblyHostedAAD.Server": { "commandName": "Project", "dotnetRunMessages": "true", "launchBrowser": true, "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } }
Then, let’s modify the server’s appsettings.json
file:
{ "AzureAd": { "Instance": "https://login.microsoftonline.com/", "Domain": "MarinkoBlogCodeMaze.onmicrosoft.com", "TenantId": "acd9a136-2bfa-485e-934b-4fb1c6fea421", "ClientId": "862c81f0-91e8-476e-8646-b7c297967ec9", "CallbackPath": "/signin-oidc" }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*" }
Here, we just provide the required information for the server application id, tenant id, and domain. The Instance
and the CallbackPath
properties are the same.
Additionally, we have to modify the client’s appsetting.json
file:
{ "AzureAd": { "Authority": "https://login.microsoftonline.com/acd9a136-2bfa-485e-934b-4fb1c6fea421", "ClientId": "f3fea9a1-5ccd-4ff0-a446-2be905a9f67a", "ValidateAuthority": true } }
Here, we add the combination of the instance and the tenant id values for the Authority property. Also, for the ClientId property, we use the client’s application id value.
Now, the last thing we have to do is to open the Program.cs
file of the client project, and modify the default access token scope:
builder.Services.AddMsalAuthentication(options => { builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication); options.ProviderOptions.DefaultAccessTokenScopes.Add("api://862c81f0-91e8-476e-8646-b7c297967ec9/BlazorHostedAPI.Access"); });
With the AddMsalAuthentication
method, we add the authentication to our Blazor app. It also accepts the callback where we provide our configuration from the appsettings.json
file and our default access token scopes.
Testing the Application
Let’s start our application, and as soon as it starts, we can see the Login button in the header of the app.
After we click on it, a new popup window will appear where we have to add our credentials. We can use our existing Azure credentials.
As soon as we log in successfully, we are going to see our username inside the “Hello” message:
But as soon as we try to visit the FetchData page, we are going to get a 403 (Forbidden) error.
To fix this, we have to navigate to the WeatherForecastController
class and modify the scopeRequiredByApi
variable:
static readonly string[] scopeRequiredByApi = new string[] { "BlazorHostedAPI.Access" };
The template places the default placeholder value here, but we have to replace that value with the name of our scope.
With this in place, we can log in again and navigate to the FetchData page, and we will be able to see the data.
Projects Overview
Let’s start with the server-side project.
We’ve already seen the appsettings.json
file but we have to inspect the Startup.cs
file to see where do we use all the information from the appsettings
file:
public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApi(Configuration.GetSection("AzureAd")); services.AddControllersWithViews(); services.AddRazorPages(); }
With the AddAuthentication
method, we register the authentication services in our app. Also, we use the AddMicrosoftIdentityWebApi
method, where we pass our configuration from the appsettings
file, to protect our API with Microsoft Identity Platform version 2.0.
Of course, to be able to use this code, we have to install the required libraries:
Also, if we inspect the Configure
method:
app.UseAuthentication(); app.UseAuthorization();
We can see two methods responsible for adding the authentication and authorization to the application’s pipeline.
The last important thing for the server part of our application is the WeatherForecastController.cs
file:
[HttpGet] public IEnumerable<WeatherForecast> Get() { HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi); var rng = new Random(); return Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateTime.Now.AddDays(index), TemperatureC = rng.Next(-20, 55), Summary = Summaries[rng.Next(Summaries.Length)] }) .ToArray(); }
In this action, we use the VerifyUserHasAnyAcceptedScope
method to verify that the right claim has been sent from the client app and that it has a value that contains the scope expected by the API.
The Client App Overview
So, we’ve seen the appsettings.json
file where we add the required configuration info and the Program.cs
file where we read that configuration using the AddMsalAuthentication
method. But to make this work, our template installs the required package for us:
If we inspect the index.html
file, we are going to see the reference to this package’s js file:
<!DOCTYPE html> <html> <head> ... </head> <body> <div id="app">Loading...</div> ... <script src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationService.js"></script> <script src="_framework/blazor.webassembly.js"></script> </body> </html>
This script is important for handling authentication actions that we perform in our app.
It is also important to notice the @using Microsoft.AspNetCore.Components.Authorization
using directive inside the Imports.razor
file. This way the authorization is enabled throughout the entire application.
Next, we can open the App.razor
file:
<CascadingAuthenticationState> <Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true"> <Found Context="routeData"> <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"> <NotAuthorized> @if (!context.User.Identity.IsAuthenticated) { <RedirectToLogin /> } else { <p>You are not authorized to access this resource.</p> } </NotAuthorized> </AuthorizeRouteView> </Found> <NotFound> <LayoutView Layout="@typeof(MainLayout)"> <p>Sorry, there's nothing at this address.</p> </LayoutView> </NotFound> </Router> </CascadingAuthenticationState>
There are different components here like CascadingAuthenticationState, AuthorizeRouteView, NotAuthorized. You can read our AuthenticationStateProvider article to learn more about how these components work and why do we wrap our code inside the CascadingAuthenticationState
component. Long story short, with the CascadingAuthenticationState
component, we expose the authentication state to the rest of the app. Additionally, we use the AuthorizeRouteView
component to determine whether the user is authorized or not. If they are not, we use the RedirectToLogin
component to redirect the user to the login page.
RedirectToLogin and Authentication Components
If we inspect the RedirectToLogin
component:
@inject NavigationManager Navigation @using Microsoft.AspNetCore.Components.WebAssembly.Authentication @code { protected override void OnInitialized() { Navigation.NavigateTo($"authentication/login?returnUrl={Uri.EscapeDataString(Navigation.Uri)}"); } }
We can see it uses the NavigationManager service to navigate the user to the Authentication
component with the login
action as a parameter and additional query string for returnUrl
.
So, if we open the Pages
folder, we are going to find the Authentication
component inside:
@page "/authentication/{action}" @using Microsoft.AspNetCore.Components.WebAssembly.Authentication <RemoteAuthenticatorView Action="@Action" /> @code{ [Parameter] public string Action { get; set; } }
It only calls the RemoteAuthenticatorView
component, which includes a default set of UI pieces for each authentication state. If we want, we can modify all these default UI components.
Log in and Log out Links
If we open the LoginDisplay.razor
file, we are going to see the code responsible for showing the Log in
and Log out
links:
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication @inject NavigationManager Navigation @inject SignOutSessionStateManager SignOutManager <AuthorizeView> <Authorized> Hello, @context.User.Identity.Name! <button class="nav-link btn btn-link" @onclick="BeginLogout">Log out</button> </Authorized> <NotAuthorized> <a href="authentication/login">Log in</a> </NotAuthorized> </AuthorizeView> @code{ private async Task BeginLogout(MouseEventArgs args) { await SignOutManager.SetSignOutState(); Navigation.NavigateTo("authentication/logout"); } }
If the user is authenticated, we are going to see the “Hello” message with the user’s username, and the Log out
button. Otherwise, we are going to see the Log in
button.
The FetchData Component
This component is secured against unauthorized users by using the Authorize
attribute:
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication @attribute [Authorize]
And that’s all it takes to protect this page from unauthenticated users.
Conclusion
So, that’s all the actions we need to secure our Blazor WebAssembly hosted app by using Azure Active Directory (AAD). With simple app registrations and using template projects, we can create well-secured applications. Additionally, we can use these steps to secure an existing Blazor WebAssembly hosted app without using the template project.
Until the next article,
All the best.