Quantcast
Channel: damienbod – Software Engineering
Viewing all articles
Browse latest Browse all 359

Migrate ASP.NET Core Blazor Server to Blazor Web

$
0
0

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.

Note

The result of this project is not ready to use in production as it was not possible to migrate the security headers fully and I am not sure if the state management works correctly. I would hold off migrating Blazor Server to Blazor Web until this is solved. Updating an existing application should not result in weaker security.

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 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.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.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 is starting to look good, but it still cannot be implemented using the recommended security standards required for the majority of my production applications or professional solutions. The authentication has problems with the different render modes and persisting the authentication state. I am confident this will be solved and documented. It is also not possible to implement strong session protection like Blazor Server. I would not recommend migrating to Blazor Web from Blazor server until Blazor security supports CSP nonces. For now, I use the following Blazor type of applications

  • Blazor Server .NET 8
  • Blazor WASM hosted in ASP.NET Core (.NET 8)

In the next blog I will look at migrating the Blazor WASM hosted in ASP.NET Core (.NET 8) to Blazor Web.

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


Viewing all articles
Browse latest Browse all 359

Trending Articles