Migrate ASP.NET Core Blazor Server to Blazor Web

This article shows how to migrate a Blazor server application to a Blazor Web application. The migration used the ASP.NET Core migration documentation, but this was not complete and a few extra steps were required. The starting point was a Blazor Server application secured using OpenID Connect for authentication. The target system is a Blazor Web application using the “InteractiveServer” rendermode.

History

2024-02-12 Updated to support CSP nonces

Codehttps://github.com/damienbod/BlazorServerOidc

Migration

The following Blazor Server application was used as a starting point:

https://github.com/damienbod/BlazorServerOidc/tree/main/BlazorServerOidc

This is a simple application using .NET 8 and OpenID Connect to implement the authentication flow. Security headers are applied and the user can login or logout using OpenIddict as the identity provider.

As in the migration guide, steps 1-3, the Routes.razor was created and the imports were extended. Migrating the contents of the Pages/_Host.cshtml to the App.razor was more complicated. I have a Layout in the original application and this needed migration into the App file as well.

This completed Blazor Web App.razor file looked like this:

@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="/" />
    <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
    <link href="css/site.css" rel="stylesheet" />
    <link href="BlazorWebFromBlazorServerOidc.styles.css" rel="stylesheet" />
    <HeadOutlet @rendermode="InteractiveServer" />
</head>
<body>
    <Routes @rendermode="InteractiveServer" />

    <script nonce="@BlazorNonceService.Nonce"  src="_framework/blazor.web.js"></script>
</body>
</html>

The App.razor uses the routes component. Inside the routes component, the CascadingAuthenticationState is used and a new component for the layout called the MainLayout.

@inject NavigationManager NavigationManager

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)">
                <NotAuthorized>
                    @{
                        var returnUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
                        NavigationManager.NavigateTo($"api/account/login?redirectUri={returnUrl}", forceLoad: true);
                    }
                </NotAuthorized>
                <Authorizing>
                    Wait...
                </Authorizing>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(Layout.MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

The MainLayout component uses two more new razor components, one for the nav menu and one for the login, logout component.

@inherits LayoutComponentBase

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <div class="top-row px-4">
            <LogInOrOut />
        </div>

        <article class="content px-4">
            @Body
        </article>
    </main>
</div>

<div id="blazor-error-ui">
    An unhandled error has occurred.
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

The login, logout component uses the original account controller and improved the logout.

@inject NavigationManager NavigationManager

<AuthorizeView>
    <Authorized>
        <div class="nav-item">
            <span>@context.User.Identity?.Name</span>
        </div>
        <div class="nav-item">
            <form action="api/account/logout" method="post">
                <AntiforgeryToken />
                <button type="submit" class="nav-link btn btn-link text-dark">
                    Logout
                </button>
            </form>
        </div>
    </Authorized>
    <NotAuthorized>
        <div class="nav-item">
            <a href="api/account/login?redirectUri=/">Log in</a>
        </div>          
    </NotAuthorized>
</AuthorizeView>

The program file was updated like in the migration docs. Blazor Web does not support reading the HTTP headers from inside a Blazor component and so the security headers were weakened which is a very bad idea. CSP nonces are not supported and so a super web security feature is lost if updating to Blazor Web. I believe moving forward, the application should be improved.

using BlazorWebFromBlazorServerOidc.Data;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;

namespace BlazorWebFromBlazorServerOidc;

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        builder.Services.TryAddEnumerable(ServiceDescriptor.Scoped<CircuitHandler, BlazorNonceService>        
            (sp => sp.GetRequiredService<BlazorNonceService>()));

        builder.Services.AddScoped<BlazorNonceService>();

        builder.Services.AddAuthentication(options =>
        {
            options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
        })
        .AddCookie()
        .AddOpenIdConnect(options =>
        {
            builder.Configuration.GetSection("OpenIDConnectSettings").Bind(options);

            options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            options.ResponseType = OpenIdConnectResponseType.Code;

            options.SaveTokens = true;
            options.GetClaimsFromUserInfoEndpoint = true;
            options.TokenValidationParameters = new TokenValidationParameters
            {
                NameClaimType = "name"
            };
        });

        builder.Services.AddRazorPages().AddMvcOptions(options =>
        {
            var policy = new AuthorizationPolicyBuilder()
                .RequireAuthenticatedUser()
                .Build();
            options.Filters.Add(new AuthorizeFilter(policy));
        });

        builder.Services.AddRazorComponents()
            .AddInteractiveServerComponents();

        builder.Services.AddSingleton<WeatherForecastService>();

        builder.Services.AddControllersWithViews(options =>
            options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()));

        var app = builder.Build();

        JsonWebTokenHandler.DefaultInboundClaimTypeMap.Clear();

        if (!app.Environment.IsDevelopment())
        {
            app.UseExceptionHandler("/Error");
            app.UseHsts();
        }

        // Using an unsecure CSP as CSP nonce is not supported in Blazor Web ...
        app.UseSecurityHeaders(
         SecurityHeadersDefinitions.GetHeaderPolicyCollection(app.Environment.IsDevelopment(),
        app.Configuration["OpenIDConnectSettings:Authority"]));

        app.UseMiddleware<NonceMiddleware>();

        app.UseHttpsRedirection();

        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthentication();
        app.UseAuthorization();

        app.UseAntiforgery();

        app.MapRazorPages();
        app.MapControllers();

        app.MapRazorComponents<App>()
            .AddInteractiveServerRenderMode().RequireAuthorization();

        app.Run();
    }
}

With the weakened security headers the application works and the authentication flow works.

Conclusion

Blazor Web in the InteractiveServer mode can use CSP nonces and it is possible to implement a secure web application.

Links

https://learn.microsoft.com/en-us/aspnet/core/migration/70-80

https://github.com/dotnet/aspnetcore/issues/53192

https://github.com/dotnet/aspnetcore/issues/51374

https://github.com/javiercn/BlazorWebNonceService

3 comments

  1. Paul van Bladel · · Reply

    Are there for the migration of a wasm app also specific concerns?

    cheers

    paul

    1. Will look at WASM hosted next

  2. […] Migrate ASP.NET Core Blazor Server to Blazor Web – Damien Bowden […]

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.