Secure ASP.NET Core server-side Blazor apps

Note

This isn't the latest version of this article. For the current release, see the .NET 8 version of this article.

Important

This information relates to a pre-release product that may be substantially modified before it's commercially released. Microsoft makes no warranties, express or implied, with respect to the information provided here.

For the current release, see the .NET 8 version of this article.

This article explains how to secure server-side Blazor apps as ASP.NET Core applications.

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.

The authentication context is only established when the app starts, which is when the app first connects to the WebSocket. The authentication context is maintained for the lifetime of the circuit. Apps periodically revalidate the user's authentication state, currently every 30 minutes by default.

If the app must capture users for custom services or react to updates to the user, see Server-side ASP.NET Core Blazor additional security scenarios.

Blazor differs from a traditional server-rendered web apps that make new HTTP requests with cookies on every page navigation. Authentication is checked during navigation events. However, cookies aren't involved. Cookies are only sent when making an HTTP request to a server, which isn't what happens when the user navigates in a Blazor app. During navigation, the user's authentication state is checked within the Blazor circuit, which you can update at any time on the server using the RevalidatingAuthenticationStateProvider abstraction.

Important

Implementing a custom NavigationManager to achieve authentication validation during navigation isn't recommended. If the app must execute custom authentication state logic during navigation, use a custom AuthenticationStateProvider.

Note

The code examples in this article adopt nullable reference types (NRTs) and .NET compiler null-state static analysis, which are supported in ASP.NET Core in .NET 6 or later. When targeting ASP.NET Core 5.0 or earlier, remove the null type designation (?) from the examples in this article.

Project template

Create a new server-side Blazor app by following the guidance in Tooling for ASP.NET Core Blazor.

After choosing the server-side app template and configuring the project, select the app's authentication under Authentication type:

  • None (default): No authentication.
  • Individual Accounts: User accounts are stored within the app using ASP.NET Core Identity.

Blazor Identity UI (Individual Accounts)

Blazor supports generating a full Blazor-based Identity UI when you choose the authentication option for Individual Accounts.

The Blazor Web App template scaffolds Identity code for a SQL Server database. The command line version uses SQLite by default and includes a SQLite database for Identity.

The template handles the following:

  • Adds Identity Razor components and related logic for routine authentication tasks, such as signing users in and out.
  • Adds the Identity-related packages and dependencies.
  • References the Identity packages in _Imports.razor.
  • Creates a custom user Identity class (ApplicationUser).
  • Creates and registers an EF Core database context (ApplicationDbContext).
  • Configures routing for the built-in Identity endpoints.
  • Includes Identity validation and business logic.

To inspect the Blazor framework's Identity components, access them in the Pages and Shared folders of the Account folder in the Blazor Web App project template (reference source).

When you choose the Interactive WebAssembly or Interactive Auto render modes, the server handles all authentication and authorization requests, and the Identity components render statically on the server in the Blazor Web App's main project. The project template includes a PersistentAuthenticationStateProvider class (reference source) in the .Client project to synchronize the user's authentication state between the server and the browser. The class is a custom implementation of AuthenticationStateProvider. The provider uses the PersistentComponentState class to prerender the authentication state and persist it to the page.

Blazor Identity depends on DbContext instances not created by a factory, which is intentional because DbContext is sufficient for the project template's Identity components to render statically without supporting interactivity.

In the main project of a Blazor Web App, the authentication state provider is named either IdentityRevalidatingAuthenticationStateProvider (reference source) (Server interactivity solutions only) or PersistingRevalidatingAuthenticationStateProvider (reference source) (WebAssembly or Auto interactivity solutions).

For a description on how global interactive render modes are applied to non-Identity components while at the same time enforcing static SSR for the Identity components, see ASP.NET Core Blazor render modes.

For more information on persisting prerendered state, see Prerender ASP.NET Core Razor components.

For more information on the Blazor Identity UI and guidance on integrating external logins through social websites, see What's new with identity in .NET 8.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the Switch branches or tags dropdown list. For more information, see How to select a version tag of ASP.NET Core source code (dotnet/AspNetCore.Docs #26205).

Manage authentication state in Blazor Web Apps

This section applies to Blazor Web Apps that adopt:

  • Interactive server-side rendering (interactive SSR) and CSR.
  • Client-side rendering (CSR).

A client-side authentication state provider is only used within Blazor and isn't integrated with the ASP.NET Core authentication system. During prerendering, Blazor respects the metadata defined on the page and uses the ASP.NET Core authentication system to determine if the user is authenticated. When a user navigates from one page to another, a client-side authentication provider is used. When the user refreshes the page (full-page reload), the client-side authentication state provider isn't involved in the authentication decision on the server. Since the user's state isn't persisted by the server, any authentication state maintained client-side is lost.

To address this, the best approach is to perform authentication within the ASP.NET Core authentication system. The client-side authentication state provider only takes care of reflecting the user's authentication state. Examples for how to accomplish this with authentication state providers are demonstrated by the Blazor Web App project template:

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the Switch branches or tags dropdown list. For more information, see How to select a version tag of ASP.NET Core source code (dotnet/AspNetCore.Docs #26205).

Scaffold Identity

For more information on scaffolding Identity into a server-side Blazor app, see Scaffold Identity in ASP.NET Core projects.

Scaffold Identity into a server-side Blazor app:

Additional claims and tokens from external providers

To store additional claims from external providers, see Persist additional claims and tokens from external providers in ASP.NET Core.

Azure App Service on Linux with Identity Server

Specify the issuer explicitly when deploying to Azure App Service on Linux with Identity Server. For more information, see Use Identity to secure a Web API backend for SPAs.

Implement a custom AuthenticationStateProvider

If the app requires a custom provider, implement AuthenticationStateProvider and override GetAuthenticationStateAsync.

In the following example, all users are authenticated with the username mrfibuli.

CustomAuthStateProvider.cs:

using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;

public class CustomAuthStateProvider : AuthenticationStateProvider
{
    public override Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var identity = new ClaimsIdentity(new[]
        {
            new Claim(ClaimTypes.Name, "mrfibuli"),
        }, "Custom Authentication");

        var user = new ClaimsPrincipal(identity);

        return Task.FromResult(new AuthenticationState(user));
    }
}

The CustomAuthStateProvider service is registered in the Program file:

using Microsoft.AspNetCore.Components.Authorization;

...

builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();

The CustomAuthStateProvider service is registered in the Program file after the call to AddServerSideBlazor:

using Microsoft.AspNetCore.Components.Authorization;

...

builder.Services.AddServerSideBlazor();

...

builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();

The CustomAuthStateProvider service is registered in Startup.ConfigureServices of Startup.cs after the call to AddServerSideBlazor:

using Microsoft.AspNetCore.Components.Authorization;

...

services.AddServerSideBlazor();

...

services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();

Confirm or add an AuthorizeRouteView to the Router component.

In the Routes component (Components/Routes.razor):

<Router ...>
    <Found ...>
        <AuthorizeRouteView RouteData="routeData" 
            DefaultLayout="typeof(Layout.MainLayout)" />
        ...
    </Found>
</Router>

Add cascading authentication state services to the service collection in the Program file:

builder.Services.AddCascadingAuthenticationState();

Note

When you create a Blazor app from one of the Blazor project templates with authentication enabled, the app includes the AuthorizeRouteView and call to AddCascadingAuthenticationState. For more information, see ASP.NET Core Blazor authentication and authorization with additional information presented in the article's Customize unauthorized content with the Router component section.

Confirm or add an AuthorizeRouteView and CascadingAuthenticationState to the Router component:

<CascadingAuthenticationState>
    <Router ...>
        <Found ...>
            <AuthorizeRouteView RouteData="routeData" 
                DefaultLayout="typeof(MainLayout)" />
            ...
        </Found>
    </Router>
</CascadingAuthenticationState>

Note

When you create a Blazor app from one of the Blazor project templates with authentication enabled, the app includes the AuthorizeRouteView and CascadingAuthenticationState components shown in the preceding example. For more information, see ASP.NET Core Blazor authentication and authorization with additional information presented in the article's Customize unauthorized content with the Router component section.

An AuthorizeView demonstrates the authenticated user's name in any component:

<AuthorizeView>
    <Authorized>
        <p>Hello, @context.User.Identity?.Name!</p>
    </Authorized>
    <NotAuthorized>
        <p>You're not authorized.</p>
    </NotAuthorized>
</AuthorizeView>

For guidance on the use of AuthorizeView, see ASP.NET Core Blazor authentication and authorization.

Notification about authentication state changes

A custom AuthenticationStateProvider can invoke NotifyAuthenticationStateChanged on the AuthenticationStateProvider base class to notify consumers of the authentication state change to rerender.

The following example is based on implementing a custom AuthenticationStateProvider by following the guidance in the Implement a custom AuthenticationStateProvider section.

The following CustomAuthStateProvider implementation exposes a custom method, AuthenticateUser, to sign in a user and notify consumers of the authentication state change.

CustomAuthStateProvider.cs:

using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;

public class CustomAuthStateProvider : AuthenticationStateProvider
{
    public override Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var identity = new ClaimsIdentity();
        var user = new ClaimsPrincipal(identity);

        return Task.FromResult(new AuthenticationState(user));
    }

    public void AuthenticateUser(string userIdentifier)
    {
        var identity = new ClaimsIdentity(new[]
        {
            new Claim(ClaimTypes.Name, userIdentifier),
        }, "Custom Authentication");

        var user = new ClaimsPrincipal(identity);

        NotifyAuthenticationStateChanged(
            Task.FromResult(new AuthenticationState(user)));
    }
}

In a component:

@inject AuthenticationStateProvider AuthenticationStateProvider

<input @bind="userIdentifier" />
<button @onclick="SignIn">Sign in</button>

<AuthorizeView>
    <Authorized>
        <p>Hello, @context.User.Identity?.Name!</p>
    </Authorized>
    <NotAuthorized>
        <p>You're not authorized.</p>
    </NotAuthorized>
</AuthorizeView>

@code {
    public string userIdentifier = string.Empty;

    private void SignIn()
    {
        ((CustomAuthStateProvider)AuthenticationStateProvider)
            .AuthenticateUser(userIdentifier);
    }
}
@inject AuthenticationStateProvider AuthenticationStateProvider

<input @bind="userIdentifier" />
<button @onclick="SignIn">Sign in</button>

<AuthorizeView>
    <Authorized>
        <p>Hello, @context.User.Identity?.Name!</p>
    </Authorized>
    <NotAuthorized>
        <p>You're not authorized.</p>
    </NotAuthorized>
</AuthorizeView>

@code {
    public string userIdentifier = string.Empty;

    private void SignIn()
    {
        ((CustomAuthStateProvider)AuthenticationStateProvider)
            .AuthenticateUser(userIdentifier);
    }
}

The preceding approach can be enhanced to trigger notifications of authentication state changes via a custom service. The following AuthenticationService maintains the current user's claims principal in a backing field (currentUser) with an event (UserChanged) that the AuthenticationStateProvider can subscribe to, where the event invokes NotifyAuthenticationStateChanged. With the additional configuration later in this section, the AuthenticationService can be injected into a component with logic that sets the CurrentUser to trigger the UserChanged event.

using System.Security.Claims;

public class AuthenticationService
{
    public event Action<ClaimsPrincipal>? UserChanged;
    private ClaimsPrincipal? currentUser;

    public ClaimsPrincipal CurrentUser
    {
        get { return currentUser ?? new(); }
        set
        {
            currentUser = value;

            if (UserChanged is not null)
            {
                UserChanged(currentUser);
            }
        }
    }
}

In the Program file, register the AuthenticationService in the dependency injection container:

builder.Services.AddScoped<AuthenticationService>();

In Startup.ConfigureServices of Startup.cs, register the AuthenticationService in the dependency injection container:

services.AddScoped<AuthenticationService>();

The following CustomAuthStateProvider subscribes to the AuthenticationService.UserChanged event. GetAuthenticationStateAsync returns the user's authentication state. Initially, the authentication state is based on the value of the AuthenticationService.CurrentUser. When there's a change in user, a new authentication state is created with the new user (new AuthenticationState(newUser)) for calls to GetAuthenticationStateAsync:

using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;

public class CustomAuthStateProvider : AuthenticationStateProvider
{
    private AuthenticationState authenticationState;

    public CustomAuthStateProvider(AuthenticationService service)
    {
        authenticationState = new AuthenticationState(service.CurrentUser);

        service.UserChanged += (newUser) =>
        {
            authenticationState = new AuthenticationState(newUser);

            NotifyAuthenticationStateChanged(
                Task.FromResult(new AuthenticationState(newUser)));
        };
    }

    public override Task<AuthenticationState> GetAuthenticationStateAsync() =>
        Task.FromResult(authenticationState);
}

The following component's SignIn method creates a claims principal for the user's identifier to set on AuthenticationService.CurrentUser:

@inject AuthenticationService AuthenticationService

<input @bind="userIdentifier" />
<button @onclick="SignIn">Sign in</button>

<AuthorizeView>
    <Authorized>
        <p>Hello, @context.User.Identity?.Name!</p>
    </Authorized>
    <NotAuthorized>
        <p>You're not authorized.</p>
    </NotAuthorized>
</AuthorizeView>

@code {
    public string userIdentifier = string.Empty;

    private void SignIn()
    {
        var currentUser = AuthenticationService.CurrentUser;

        var identity = new ClaimsIdentity(
            new[]
            {
                new Claim(ClaimTypes.Name, userIdentifier),
            },
            "Custom Authentication");

        var newUser = new ClaimsPrincipal(identity);

        AuthenticationService.CurrentUser = newUser;
    }
}
@inject AuthenticationService AuthenticationService

<input @bind="userIdentifier" />
<button @onclick="SignIn">Sign in</button>

<AuthorizeView>
    <Authorized>
        <p>Hello, @context.User.Identity?.Name!</p>
    </Authorized>
    <NotAuthorized>
        <p>You're not authorized.</p>
    </NotAuthorized>
</AuthorizeView>

@code {
    public string userIdentifier = string.Empty;

    private void SignIn()
    {
        var currentUser = AuthenticationService.CurrentUser;

        var identity = new ClaimsIdentity(
            new[]
            {
                new Claim(ClaimTypes.Name, userIdentifier),
            },
            "Custom Authentication");

        var newUser = new ClaimsPrincipal(identity);

        AuthenticationService.CurrentUser = newUser;
    }
}

Inject AuthenticationStateProvider for services scoped to a component

Don't attempt to resolve AuthenticationStateProvider within a custom scope because it results in the creation of a new instance of the AuthenticationStateProvider that isn't correctly initialized.

To access the AuthenticationStateProvider within a service scoped to a component, inject the AuthenticationStateProvider with the @inject directive or the [Inject] attribute and pass it to the service as a parameter. This approach ensures that the correct, initialized instance of the AuthenticationStateProvider is used for each user app instance.

ExampleService.cs:

public class ExampleService
{
    public async Task<string> ExampleMethod(AuthenticationStateProvider authStateProvider)
    {
        var authState = await authStateProvider.GetAuthenticationStateAsync();
        var user = authState.User;

        if (user.Identity is not null && user.Identity.IsAuthenticated)
        {
            return $"{user.Identity.Name} is authenticated.";
        }
        else
        {
            return "The user is NOT authenticated.";
        }
    }
}

Register the service as scoped. In a server-side Blazor app, scoped services have a lifetime equal to the duration of the client connection circuit.

In the Program file:

builder.Services.AddScoped<ExampleService>();

In Startup.ConfigureServices of Startup.cs:

services.AddScoped<ExampleService>();

In the following InjectAuthStateProvider component:

InjectAuthStateProvider.razor:

@page "/inject-auth-state-provider"
@inherits OwningComponentBase
@inject AuthenticationStateProvider AuthenticationStateProvider

<h1>Inject <code>AuthenticationStateProvider</code> Example</h1>

<p>@message</p>

@code {
    private string? message;
    private ExampleService? ExampleService { get; set; }

    protected override async Task OnInitializedAsync()
    {
        ExampleService = ScopedServices.GetRequiredService<ExampleService>();

        message = await ExampleService.ExampleMethod(AuthenticationStateProvider);
    }
}
@page "/inject-auth-state-provider"
@inject AuthenticationStateProvider AuthenticationStateProvider
@inherits OwningComponentBase

<h1>Inject <code>AuthenticationStateProvider</code> Example</h1>

<p>@message</p>

@code {
    private string? message;
    private ExampleService? ExampleService { get; set; }

    protected override async Task OnInitializedAsync()
    {
        ExampleService = ScopedServices.GetRequiredService<ExampleService>();

        message = await ExampleService.ExampleMethod(AuthenticationStateProvider);
    }
}

For more information, see the guidance on OwningComponentBase in ASP.NET Core Blazor dependency injection.

Unauthorized content display while prerendering with a custom AuthenticationStateProvider

To avoid showing unauthorized content, for example content in an AuthorizeView component, while prerendering with a custom AuthenticationStateProvider, adopt one of the following approaches:

  • Disable prerendering: Indicate the render mode with the prerender parameter set to false at the highest-level component in the app's component hierarchy that isn't a root component.

    Note

    Making a root component interactive, such as the App component, isn't supported. Therefore, prerendering can't be disabled directly by the App component.

    For apps based on the Blazor Web App project template, prerendering is typically disabled where the Routes component is used in the App component (Components/App.razor) :

    <Routes @rendermode="new InteractiveServerRenderMode(prerender: false)" />
    

    Also, disable prerendering for the HeadOutlet component:

    <HeadOutlet @rendermode="new InteractiveServerRenderMode(prerender: false)" />
    

    You can also selectively disable prerendering with fine control of the render mode applied to the Routes component instance. For more information, see ASP.NET Core Blazor render modes.

  • Disable prerendering: Open the _Host.cshtml file and change the render-mode attribute of the Component Tag Helper to Server:

    <component type="typeof(App)" render-mode="Server" />
    
  • Authenticate the user on the server before the app starts: To adopt this approach, the app must respond to a user's initial request with the Identity-based sign-in page or view and prevent any requests to Blazor endpoints until they're authenticated. For more information, see Create an ASP.NET Core app with user data protected by authorization. After authentication, unauthorized content in prerendered Razor components is only shown when the user is truly unauthorized to view the content.

User state management

In spite of the word "state" in the name, AuthenticationStateProvider isn't for storing general user state. AuthenticationStateProvider only indicates the user's authentication state to the app, whether they are signed into the app and who they are signed in as.

Authentication uses the same ASP.NET Core Identity authentication as Razor Pages and MVC apps. The user state stored for ASP.NET Core Identity flows to Blazor without adding additional code to the app. Follow the guidance in the ASP.NET Core Identity articles and tutorials for the Identity features to take effect in the Blazor parts of the app.

For guidance on general state management outside of ASP.NET Core Identity, see ASP.NET Core Blazor state management.

Additional security abstractions

Two additional abstractions participate in managing authentication state:

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the Switch branches or tags dropdown list. For more information, see How to select a version tag of ASP.NET Core source code (dotnet/AspNetCore.Docs #26205).

Temporary redirection URL validity duration

This section applies to Blazor Web Apps.

Use the RazorComponentsServiceOptions.TemporaryRedirectionUrlValidityDuration option to get or set the lifetime of data protection validity for temporary redirection URLs emitted by Blazor server-side rendering. These are only used transiently, so the lifetime only needs to be long enough for a client to receive the URL and begin navigation to it. However, it should also be long enough to allow for clock skew across servers. The default value is five minutes.

In the following example the value is extended to seven minutes:

builder.Services.AddRazorComponents(options => 
    options.TemporaryRedirectionUrlValidityDuration = 
        TimeSpan.FromMinutes(7));

Additional resources