Does Msal work with Blazor V8 release? And where should the Msal components be located?

923 Views Asked by At

I'm trying to put together a Blazor app that uses Entra Id with a popup form for authentication. I have had some success with an old version of Blazor but I would like to do this with the release version of Blazor (8). The "split" project file has me a bit confused. I'm wondering if Msal works with Blazor V8 release as server side or client side or both? Anyone have experience or success incorporating Entra ID authentication with a Blazor server or webasm app?

2

There are 2 best solutions below

4
On

If we are creating a new Blazor web app in .net 8 and want to integrate Azure AD, we need to change 7 files.

In appsettings.json, adding AAD related configurations:

"AzureAd": {
  "Instance": "https://login.microsoftonline.com/",
  "Domain": "tenant_id",
  "TenantId": "tenant_id",
  "ClientId": "aad_client_id",
  "CallbackPath": "/signin-oidc",
  "ClientSecret": "client_secret"
}

In csproj file, adding nuget packages:

<ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" NoWarn="NU1605" />
    <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.0" NoWarn="NU1605" />
    <PackageReference Include="Microsoft.Identity.Web" Version="2.16.0" />
    <PackageReference Include="Microsoft.Identity.Web.UI" Version="2.16.0" />
</ItemGroup>

In Program.cs, adding related services.

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));
builder.Services.AddControllersWithViews()
    .AddMicrosoftIdentityUI();
builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = options.DefaultPolicy;
});
// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();
builder.Services.AddCascadingAuthenticationState();

In _Imports.cshtml, adding @using Microsoft.AspNetCore.Components.Authorization.

In Routes.razor, adding AuthorizeRouteView

<Router AppAssembly="@typeof(Program).Assembly">
    @* <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found> *@

    <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)" />
            <FocusOnNavigate RouteData="@routeData" Selector="h1" />
        </Found>
        <NotFound>
            <PageTitle>Not found</PageTitle>
        <LayoutView Layout="@typeof(Layout.MainLayout)">
                <p role="alert">Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
</Router>

Creating LoginDisplay.razor in Pages folder.

<AuthorizeView>
    <Authorized>
        Hello, @context.User.Identity?.Name!
        @authMessage
        <a href="MicrosoftIdentity/Account/SignOut">Log out</a>
    </Authorized>
    <NotAuthorized>
        <a href="MicrosoftIdentity/Account/SignIn">Log in</a>
    </NotAuthorized>
</AuthorizeView>

@code {
    private string authMessage = "The user is NOT authenticated.";

    [CascadingParameter]
    private Task<AuthenticationState>? authenticationState { get; set; }

    protected override async Task OnInitializedAsync()
    {
        if (authenticationState is not null)
        {
            var authState = await authenticationState;
            var user = authState?.User;

            if (user?.Identity is not null && user.Identity.IsAuthenticated)
            {
                authMessage = $"{user.Identity.Name} is authenticated.";
            }
        }
    }
}

Adding LoginDisplay component into MainLayout.razor

<div class="top-row px-4">
    <LoginDisplay />
    <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div>

================== Update above ===================

We can create a blazor app integrating Azure AD by the default template. Here's the screenshot for stand-alone blazor wsam(client side). Or we could refer to this document to learn more about how to create a standalone Blazor WebAssembly app that uses Microsoft Accounts with Microsoft Entra (ME-ID) for authentication.

enter image description here

But we don't have this option when choosing blazor web app.

enter image description here

Blazor web app is a replacement for Blazor server and Blazor wsam asp.net core hosted.

We removed the Blazor Server template, and the ASP.NET Core Hosted option has been removed from the Blazor WebAssembly template. Both of these scenarios are represented by options when using the Blazor Web App template.

For Blazor server in .net 8(blazor web app), we have document which mentioned:

Server-side Blazor apps are configured for security in the same manner as ASP.NET Core apps. For more information, see the articles under ASP.NET Core security topics.

0
On

I had the following test today which is based on a .net 7 blazor server and migrate to .net 8 blazor web app.

Firstly, I created a new blazor server .net 7 and choose the "Microsoft identity platform" as the authentication type when using default Blazor template in VS. Then I updated the nuget packages (Microsoft.Identity.Web and .UI) to the latest version (now it's 2.16.0) and modify the configuration file so that we are able to sign into the application.

Now we already integrated Azure AD into this blazor server app. The next is following the migration tutorial to convert the app to use .net 8.

Firstly, in csproj file, changing to use <TargetFramework>net8.0</TargetFramework> instead of net7.0, then upgrade the left nuget packages Microsoft.AspNetCore.Authentication.JwtBearer and .OpenIdConnect to Version="8.0.0" instead of 7.0.0.

Creating Routes.razor then and move the codes from App.razor into this file . Comment the <CascadingAuthenticationState> because we use CascadingParameter instead.

@* <CascadingAuthenticationState> *@
    <Router AppAssembly="@typeof(App).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
            <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> *@

Then in the _Imports.razor, adding @using static Microsoft.AspNetCore.Components.Web.RenderMode.

Delete the _Host.cshtml but moving the codes from _Host.cshtml to App.razor in advance.

@* @page "/"
@using Microsoft.AspNetCore.Components.Web
@namespace BlazorServer7To8.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers *@

@inject IHostEnvironment Env

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    @* <base href="~/" /> *@
    <base href="/" />
    <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
    <link href="css/site.css" rel="stylesheet" />
    <link href="BlazorServer7To8.styles.css" rel="stylesheet" />
    <link rel="icon" type="image/png" href="favicon.png"/>
    @* <component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" /> *@
    <HeadOutlet @rendermode="InteractiveServer" />
</head>
<body>
    @* <component type="typeof(App)" render-mode="ServerPrerendered" /> *@
    <Routes @rendermode="InteractiveServer" />

    <div id="blazor-error-ui">
        @* <environment include="Staging,Production">
            An error has occurred. This application may no longer respond until reloaded.
        </environment>
        <environment include="Development">
            An unhandled exception has occurred. See browser dev tools for details.
        </environment> *@
        @if (Env.IsDevelopment())
        {
            <text>
                An unhandled exception has occurred. See browser dev tools for details.
            </text>
        }
        else
        {
            <text>
                An error has occurred. This app may no longer respond until reloaded.
            </text>
        }
        <a href="" class="reload">Reload</a>
        <a class="dismiss"></a>
    </div>

    @* <script src="_framework/blazor.server.js"></script> *@
    <script src="_framework/blazor.web.js"></script>
</body>
</html>

Then modifying the Program.cs. Replacing builder.Services.AddServerSideBlazor(); with builder.Services.AddRazorComponents().AddInteractiveServerComponents();, replacing app.MapBlazorHub(); with app.MapRazorComponents<App>().AddInteractiveServerRenderMode();, comment app.MapFallbackToPage("/_Host");, put app.UseAntiforgery(); below app.UseRouting, adding builder.Services.AddCascadingAuthenticationState(); for the authentication.

using BlazorServer7To8.Data;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.UI;
using BlazorServer7To8;

var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddDistributedTokenCaches();
builder.Services.AddControllersWithViews()
    .AddMicrosoftIdentityUI();
builder.Services.AddAuthorization(options =>
{
    // By default, all incoming requests will be authorized according to the default policy
    options.FallbackPolicy = options.DefaultPolicy;
});
builder.Services.AddRazorPages();
//builder.Services.AddServerSideBlazor()
//    .AddMicrosoftIdentityConsentHandler();
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents()
    .AddMicrosoftIdentityConsentHandler();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddSingleton<WeatherForecastService>();

var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAntiforgery();
app.MapControllers();
//app.MapBlazorHub();
//app.MapFallbackToPage("/_Host");
app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();
app.Run();

Finally I changed the default LoginDisplay.razor component like below.

<AuthorizeView>
    <Authorized>
        Hello, @context.User.Identity?.Name!
        @authMessage
        <a href="MicrosoftIdentity/Account/SignOut">Log out</a>
    </Authorized>
    <NotAuthorized>
        <a href="MicrosoftIdentity/Account/SignIn">Log in</a>
    </NotAuthorized>
</AuthorizeView>

@code {
    private string authMessage = "The user is NOT authenticated.";

    [CascadingParameter]
    private Task<AuthenticationState>? authenticationState { get; set; }

    protected override async Task OnInitializedAsync()
    {
        if (authenticationState is not null)
        {
            var authState = await authenticationState;
            var user = authState?.User;

            if (user?.Identity is not null && user.Identity.IsAuthenticated)
            {
                authMessage = $"{user.Identity.Name} is authenticated.";
            }
        }
    }
}

enter image description here