Skip to content

[Entra ID] Add standalone Blazor WebAssembly authentication integration #3869

Description

@danroth27

Summary

Add Entra ID support for standalone Blazor WebAssembly applications.

The current Entra ID scaffolder accepts a standalone blazorwasm project and reports success, but it doesn't generate any authentication code or configuration. It also creates an inappropriate secret-bearing app registration.

Standalone Blazor WebAssembly is a public SPA client and requires a different implementation from a Blazor Web App using Interactive WebAssembly.

Related: #2890

Steps to reproduce

dotnet new blazorwasm -n EntraStandaloneWasm `
    --framework net11.0

dotnet scaffold aspnet entra-id `
    --project .\EntraStandaloneWasm\EntraStandaloneWasm.csproj `
    --username <tenant-user> `
    --tenantId <tenant> `
    --use-existing-application false

dotnet build .\EntraStandaloneWasm\EntraStandaloneWasm.csproj
dotnet run --project .\EntraStandaloneWasm\EntraStandaloneWasm.csproj

Current behavior

The scaffolder creates an Entra app registration and password credential, then attempts to create:

<project-file>.csproj/appsettings.json

The log reports:

Creating new appsettings.json file at
...\EntraStandaloneWasm.csproj\appsettings.json

Failed to write appsettings.json:
Could not find a part of the path ...

The command still returns exit code 0, as tracked separately by #3845.

Because the settings step fails, the remaining package and code-modification steps don't run. The project is left unchanged:

  • No Microsoft.Authentication.WebAssembly.Msal package.
  • No AddMsalAuthentication.
  • No AzureAd configuration under wwwroot.
  • No authentication callback component.
  • No AuthorizeRouteView.
  • No login/logout UI.

The scaffolded .NET 11 app still builds and runs, but it remains anonymous:

Home
Counter
Weather
About
Hello, world!
Welcome to your new app.

No requests are made to Entra.

The temporary registration created during testing had:

signInAudience: AzureADandPersonalMicrosoftAccount
Web redirect URIs: none
SPA redirect URIs: none
Implicit ID-token issuance: enabled
Password credentials: 1

A standalone WebAssembly app can't protect or use a client secret. The registration is unusable because it has no SPA callback URI.

Two separate scaffold runs created two orphaned registrations because the client ID couldn't be written back to either project.

Expected behavior

When the selected project is a standalone Blazor WebAssembly app, generate public SPA client authentication using MSAL.

Project detection

Detect the standalone client directly from the project SDK:

<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

and/or its evaluated SDK and Microsoft.AspNetCore.Components.WebAssembly package reference.

The current detector only examines projects referenced by a server project:

dotnet reference list --project <project>

A standalone project normally has no project references, so this path always records IsBlazorWasmProject = false.

Detection should distinguish:

  • Standalone Blazor WebAssembly: the selected project itself uses Microsoft.NET.Sdk.BlazorWebAssembly.
  • Blazor Web App Interactive WebAssembly/Auto: a server project references a .Client WebAssembly project.

These scenarios require different authentication architectures.

Packages

Add:

<PackageReference Include="Microsoft.Authentication.WebAssembly.Msal" />

Don't add:

<PackageReference Include="Microsoft.Identity.Web" />

A standalone browser application has no confidential server host.

Program.cs

Generate:

builder.Services.AddMsalAuthentication(options =>
{
    builder.Configuration.Bind(
        "AzureAd",
        options.ProviderOptions.Authentication);
});

If API scopes are selected, add them to DefaultAccessTokenScopes and configure the appropriate authorized HTTP client/message handler.

Development configuration

Write the public development registration values to:

wwwroot/appsettings.Development.json

For example:

{
  "AzureAd": {
    "Authority": "https://login.microsoftonline.com/<tenant-id>",
    "ClientId": "<development-client-id>",
    "ValidateAuthority": true
  }
}

No secret or other sensitive value may be placed in a standalone WebAssembly project because all browser-delivered files and assemblies are visible to users.

Authentication components

Generate the standard standalone WebAssembly authentication surface:

@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="@Action" />

@code {
    [Parameter]
    public string? Action { get; set; }
}

Use AuthorizeRouteView with unauthenticated redirection:

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(App).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData"
                                DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    @if (context.User.Identity?.IsAuthenticated != true)
                    {
                        <RedirectToLogin />
                    }
                    else
                    {
                        <p>You aren't authorized to access this resource.</p>
                    }
                </NotAuthorized>
            </AuthorizeRouteView>
            <FocusOnNavigate RouteData="@routeData" Selector="h1" />
        </Found>
    </Router>
</CascadingAuthenticationState>

Generate login/logout UI using the WebAssembly authentication APIs:

@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject NavigationManager Navigation

<AuthorizeView>
    <Authorized>
        Hello, @context.User.Identity?.Name!
        <button @onclick="BeginLogOut">Log out</button>
    </Authorized>
    <NotAuthorized>
        <a href="authentication/login">Log in</a>
    </NotAuthorized>
</AuthorizeView>

@code {
    private void BeginLogOut() =>
        Navigation.NavigateToLogout("authentication/logout");
}

Don't generate server-only MapLoginAndLogout() endpoints or form-post logout UI.

App registration

Provision a public SPA registration:

The registration should be clearly identified as development-only and preserve unrelated values when an existing SPA registration is selected.

Runtime behavior

After scaffolding:

  • The project builds.
  • The navigation displays Log in.
  • Login redirects to Entra and returns through /authentication/login-callback.
  • Authentication state is available in client components.
  • [Authorize], roles, and policies work through AuthorizeRouteView.
  • Logout uses the MSAL browser flow.
  • No secret is generated or shipped to the browser.
  • A failed scaffold run cleans up a newly created registration.

Template parity and guidance

The generated output should preserve the standalone Microsoft identity platform behavior currently provided by:

dotnet new blazorwasm `
    --auth SingleOrg `
    --client-id <client-id> `
    --tenant-id <tenant-id>

References:

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions