ASP.NET Core Blazor Hybrid authentication and authorization

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 describes ASP.NET Core's support for the configuration and management of security and ASP.NET Core Identity in Blazor Hybrid apps.

Authentication in Blazor Hybrid apps is handled by native platform libraries, as they offer enhanced security guarantees that the browser sandbox can't offer. Authentication of native apps uses an OS-specific mechanism or via a federated protocol, such as OpenID Connect (OIDC). Follow the guidance for the identity provider that you've selected for the app and then further integrate identity with Blazor using the guidance in this article.

Integrating authentication must achieve the following goals for Razor components and services:

After authentication is added to a .NET MAUI, WPF, or Windows Forms app and users are able to log in and log out successfully, integrate authentication with Blazor to make the authenticated user available to Razor components and services. Perform the following steps:

.NET MAUI apps use Xamarin.Essentials: Web Authenticator: The WebAuthenticator class allows the app to initiate browser-based authentication flows that listen for a callback to a specific URL registered with the app.

For additional guidance, see the following resources:

Windows Forms apps use the Microsoft identity platform to integrate with Microsoft Entra (ME-ID) and AAD B2C. For more information, see Overview of the Microsoft Authentication Library (MSAL).

Create a custom AuthenticationStateProvider without user change updates

If the app authenticates the user immediately after the app launches and the authenticated user remains the same for the entirety of the app lifetime, user change notifications aren't required, and the app only provides information about the authenticated user. In this scenario, the user logs into the app when the app is opened, and the app displays the login screen again after the user logs out. The following ExternalAuthStateProvider is an example implementation of a custom AuthenticationStateProvider for this authentication scenario.

Note

The following custom AuthenticationStateProvider doesn't declare a namespace in order to make the code example applicable to any Blazor Hybrid app. However, a best practice is to provide your app's namespace when you implement the example in a production app.

ExternalAuthStateProvider.cs:

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

public class ExternalAuthStateProvider : AuthenticationStateProvider
{
    private readonly Task<AuthenticationState> authenticationState;

    public ExternalAuthStateProvider(AuthenticatedUser user) => 
        authenticationState = Task.FromResult(new AuthenticationState(user.Principal));

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

public class AuthenticatedUser
{
    public ClaimsPrincipal Principal { get; set; } = new();
}

The following steps describe how to:

  • Add required namespaces.
  • Add the authorization services and Blazor abstractions to the service collection.
  • Build the service collection.
  • Resolve the AuthenticatedUser service to set the authenticated user's claims principal. See your identity provider's documentation for details.
  • Return the built host.

In the MauiProgram.CreateMauiApp method of MauiProgram.cs, add namespaces for Microsoft.AspNetCore.Components.Authorization and System.Security.Claims:

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

Remove the following line of code that returns a built Microsoft.Maui.Hosting.MauiApp:

- return builder.Build();

Replace the preceding line of code with the following code. Add OpenID/MSAL code to authenticate the user. See your identity provider's documentation for details.

builder.Services.AddAuthorizationCore();
builder.Services.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
builder.Services.AddSingleton<AuthenticatedUser>();
var host = builder.Build();

var authenticatedUser = host.Services.GetRequiredService<AuthenticatedUser>();

/*
Provide OpenID/MSAL code to authenticate the user. See your identity provider's 
documentation for details.

The user is represented by a new ClaimsPrincipal based on a new ClaimsIdentity.
*/
var user = new ClaimsPrincipal(new ClaimsIdentity());

authenticatedUser.Principal = user;

return host;

The following steps describe how to:

  • Add required namespaces.
  • Add the authorization services and Blazor abstractions to the service collection.
  • Build the service collection and add the built service collection as a resource to the app's ResourceDictionary.
  • Resolve the AuthenticatedUser service to set the authenticated user's claims principal. See your identity provider's documentation for details.
  • Return the built host.

In the MainWindow's constructor (MainWindow.xaml.cs), add namespaces for Microsoft.AspNetCore.Components.Authorization and System.Security.Claims:

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

Remove the following line of code that adds the built service collection as a resource to the app's ResourceDictionary:

- Resources.Add("services", serviceCollection.BuildServiceProvider());

Replace the preceding line of code with the following code. Add OpenID/MSAL code to authenticate the user. See your identity provider's documentation for details.

serviceCollection.AddAuthorizationCore();
serviceCollection.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
serviceCollection.AddSingleton<AuthenticatedUser>();
var services = serviceCollection.BuildServiceProvider();
Resources.Add("services", services);

var authenticatedUser = services.GetRequiredService<AuthenticatedUser>();

/*
Provide OpenID/MSAL code to authenticate the user. See your identity provider's 
documentation for details.

The user is represented by a new ClaimsPrincipal based on a new ClaimsIdentity.
*/
var user = new ClaimsPrincipal(new ClaimsIdentity());

authenticatedUser.Principal = user;

The following steps describe how to:

  • Add required namespaces.
  • Add the authorization services and Blazor abstractions to the service collection.
  • Build the service collection and add the built service collection to the app's service provider.
  • Resolve the AuthenticatedUser service to set the authenticated user's claims principal. See your identity provider's documentation for details.

In the Form1's constructor (Form1.cs), add namespaces for Microsoft.AspNetCore.Components.Authorization and System.Security.Claims:

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

Remove the following line of code that sets the built service collection to the app's service provider:

- blazorWebView1.Services = services.BuildServiceProvider();

Replace the preceding line of code with the following code. Add OpenID/MSAL code to authenticate the user. See your identity provider's documentation for details.

services.AddAuthorizationCore();
services.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
services.AddSingleton<AuthenticatedUser>();
var serviceCollection = services.BuildServiceProvider();
blazorWebView1.Services = serviceCollection;

var authenticatedUser = serviceCollection.GetRequiredService<AuthenticatedUser>();

/*
Provide OpenID/MSAL code to authenticate the user. See your identity provider's 
documentation for details.

The user is represented by a new ClaimsPrincipal based on a new ClaimsIdentity.
*/
var user = new ClaimsPrincipal(new ClaimsIdentity());

authenticatedUser.Principal = user;

Create a custom AuthenticationStateProvider with user change updates

To update the user while the Blazor app is running, call NotifyAuthenticationStateChanged within the AuthenticationStateProvider implementation using either of the following approaches:

Signal an authentication update from outside of the BlazorWebView (Option 1)

A custom AuthenticationStateProvider can use a global service to signal an authentication update. We recommend that the service offer an event that the AuthenticationStateProvider can subscribe to, where the event invokes NotifyAuthenticationStateChanged.

Note

The following custom AuthenticationStateProvider doesn't declare a namespace in order to make the code example applicable to any Blazor Hybrid app. However, a best practice is to provide your app's namespace when you implement the example in a production app.

ExternalAuthStateProvider.cs:

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

public class ExternalAuthStateProvider : AuthenticationStateProvider
{
    private AuthenticationState currentUser;

    public ExternalAuthStateProvider(ExternalAuthService service)
    {
        currentUser = new AuthenticationState(service.CurrentUser);

        service.UserChanged += (newUser) =>
        {
            currentUser = new AuthenticationState(newUser);
            NotifyAuthenticationStateChanged(Task.FromResult(currentUser));
        };
    }

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

public class ExternalAuthService
{
    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 MauiProgram.CreateMauiApp method of MauiProgram.cs, add a namespace for Microsoft.AspNetCore.Components.Authorization:

using Microsoft.AspNetCore.Components.Authorization;

Add the authorization services and Blazor abstractions to the service collection:

builder.Services.AddAuthorizationCore();
builder.Services.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
builder.Services.AddSingleton<ExternalAuthService>();

In the MainWindow's constructor (MainWindow.xaml.cs), add a namespace for Microsoft.AspNetCore.Components.Authorization:

using Microsoft.AspNetCore.Components.Authorization;

Add the authorization services and the Blazor abstractions to the service collection:

serviceCollection.AddAuthorizationCore();
serviceCollection.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
serviceCollection.AddSingleton<ExternalAuthService>();

In the Form1's constructor (Form1.cs), add a namespace for Microsoft.AspNetCore.Components.Authorization:

using Microsoft.AspNetCore.Components.Authorization;

Add the authorization services and Blazor abstractions to the service collection:

services.AddAuthorizationCore();
services.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
services.AddSingleton<ExternalAuthService>();

Wherever the app authenticates a user, resolve the ExternalAuthService service:

var authService = host.Services.GetRequiredService<ExternalAuthService>();

Execute your custom OpenID/MSAL code to authenticate the user. See your identity provider's documentation for details. The authenticated user (authenticatedUser in the following example) is a new ClaimsPrincipal based on a new ClaimsIdentity.

Set the current user to the authenticated user:

authService.CurrentUser = authenticatedUser;

An alternative to the preceding approach is to set the user's principal on System.Threading.Thread.CurrentPrincipal instead of setting it via a service, which avoids use of the dependency injection container:

public class CurrentThreadUserAuthenticationStateProvider : AuthenticationStateProvider
{
    public override Task<AuthenticationState> GetAuthenticationStateAsync() =>
        Task.FromResult(
            new AuthenticationState(Thread.CurrentPrincipal as ClaimsPrincipal ?? 
                new ClaimsPrincipal(new ClaimsIdentity())));
}

Using the alternative approach, only authorization services (AddAuthorizationCore) and CurrentThreadUserAuthenticationStateProvider (.TryAddScoped<AuthenticationStateProvider, CurrentThreadUserAuthenticationStateProvider>()) are added to the service collection.

Handle authentication within the BlazorWebView (Option 2)

A custom AuthenticationStateProvider can include additional methods to trigger log in and log out and update the user.

Note

The following custom AuthenticationStateProvider doesn't declare a namespace in order to make the code example applicable to any Blazor Hybrid app. However, a best practice is to provide your app's namespace when you implement the example in a production app.

ExternalAuthStateProvider.cs:

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

public class ExternalAuthStateProvider : AuthenticationStateProvider
{
    private ClaimsPrincipal currentUser = new ClaimsPrincipal(new ClaimsIdentity());

    public override Task<AuthenticationState> GetAuthenticationStateAsync() =>
        Task.FromResult(new AuthenticationState(currentUser));

    public Task LogInAsync()
    {
        var loginTask = LogInAsyncCore();
        NotifyAuthenticationStateChanged(loginTask);

        return loginTask;

        async Task<AuthenticationState> LogInAsyncCore()
        {
            var user = await LoginWithExternalProviderAsync();
            currentUser = user;

            return new AuthenticationState(currentUser);
        }
    }

    private Task<ClaimsPrincipal> LoginWithExternalProviderAsync()
    {
        /*
            Provide OpenID/MSAL code to authenticate the user. See your identity 
            provider's documentation for details.

            Return a new ClaimsPrincipal based on a new ClaimsIdentity.
        */
        var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity());

        return Task.FromResult(authenticatedUser);
    }

    public void Logout()
    {
        currentUser = new ClaimsPrincipal(new ClaimsIdentity());
        NotifyAuthenticationStateChanged(
            Task.FromResult(new AuthenticationState(currentUser)));
    }
}

In the preceding example:

  • The call to LogInAsyncCore triggers the login process.
  • The call to NotifyAuthenticationStateChanged notifies that an update is in progress, which allows the app to provide a temporary UI during the login or logout process.
  • Returning loginTask returns the task so that the component that triggered the login can await and react after the task is complete.
  • The LoginWithExternalProviderAsync method is implemented by the developer to log in the user with the identity provider's SDK. For more information, see your identity provider's documentation. The authenticated user (authenticatedUser) is a new ClaimsPrincipal based on a new ClaimsIdentity.

In the MauiProgram.CreateMauiApp method of MauiProgram.cs, add the authorization services and the Blazor abstraction to the service collection:

builder.Services.AddAuthorizationCore();
builder.Services.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();

In the MainWindow's constructor (MainWindow.xaml.cs), add the authorization services and the Blazor abstraction to the service collection:

serviceCollection.AddAuthorizationCore();
serviceCollection.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();

In the Form1's constructor (Form1.cs), add the authorization services and the Blazor abstraction to the service collection:

services.AddAuthorizationCore();
services.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();

The following LoginComponent component demonstrates how to log in a user. In a typical app, the LoginComponent component is only shown in a parent component if the user isn't logged into the app.

Shared/LoginComponent.razor:

@inject AuthenticationStateProvider AuthenticationStateProvider

<button @onclick="Login">Log in</button>

@code
{
    public async Task Login()
    {
        await ((ExternalAuthStateProvider)AuthenticationStateProvider)
            .LogInAsync();
    }
}

The following LogoutComponent component demonstrates how to log out a user. In a typical app, the LogoutComponent component is only shown in a parent component if the user is logged into the app.

Shared/LogoutComponent.razor:

@inject AuthenticationStateProvider AuthenticationStateProvider

<button @onclick="Logout">Log out</button>

@code
{
    public async Task Logout()
    {
        await ((ExternalAuthStateProvider)AuthenticationStateProvider)
            .Logout();
    }
}

Accessing other authentication information

Blazor doesn't define an abstraction to deal with other credentials, such as access tokens to use for HTTP requests to web APIs. We recommend following the identity provider's guidance to manage the user's credentials with the primitives that the identity provider's SDK provides.

It's common for identity provider SDKs to use a token store for user credentials stored in the device. If the SDK's token store primitive is added to the service container, consume the SDK's primitive within the app.

The Blazor framework isn't aware of a user's authentication credentials and doesn't interact with credentials in any way, so the app's code is free to follow whatever approach you deem most convenient. However, follow the general security guidance in the next section, Other authentication security considerations, when implementing authentication code in an app.

Other authentication security considerations

The authentication process is external to Blazor, and we recommend that developers access the identity provider's guidance for additional security guidance.

When implementing authentication:

  • Avoid authentication in the context of the Web View. For example, avoid using a JavaScript OAuth library to perform the authentication flow. In a single-page app, authentication tokens aren't hidden in JavaScript and can be easily discovered by malicious users and used for nefarious purposes. Native apps don't suffer this risk because native apps are only able to obtain tokens outside of the browser context, which means that rogue third-party scripts can't steal the tokens and compromise the app.
  • Avoid implementing the authentication workflow yourself. In most cases, platform libraries securely handle the authentication workflow, using the system's browser instead of using a custom Web View that can be hijacked.
  • Avoid using the platform's Web View control to perform authentication. Instead, rely on the system's browser when possible.
  • Avoid passing the tokens to the document context (JavaScript). In some situations, a JavaScript library within the document is required to perform an authorized call to an external service. Instead of making the token available to JavaScript via JS interop:
    • Provide a generated temporary token to the library and within the Web View.
    • Intercept the outgoing network request in code.
    • Replace the temporary token with the real token and confirm that the destination of the request is valid.

Additional resources

Authentication in Blazor Hybrid apps is handled by native platform libraries, as they offer enhanced security guarantees that the browser sandbox can't offer. Authentication of native apps uses an OS-specific mechanism or via a federated protocol, such as OpenID Connect (OIDC). Follow the guidance for the identity provider that you've selected for the app and then further integrate identity with Blazor using the guidance in this article.

Integrating authentication must achieve the following goals for Razor components and services:

After authentication is added to a .NET MAUI, WPF, or Windows Forms app and users are able to log in and log out successfully, integrate authentication with Blazor to make the authenticated user available to Razor components and services. Perform the following steps:

.NET MAUI apps use Xamarin.Essentials: Web Authenticator: The WebAuthenticator class allows the app to initiate browser-based authentication flows that listen for a callback to a specific URL registered with the app.

For additional guidance, see the following resources:

Windows Forms apps use the Microsoft identity platform to integrate with Microsoft Entra (ME-ID) and AAD B2C. For more information, see Overview of the Microsoft Authentication Library (MSAL).

Create a custom AuthenticationStateProvider without user change updates

If the app authenticates the user immediately after the app launches and the authenticated user remains the same for the entirety of the app lifetime, user change notifications aren't required, and the app only provides information about the authenticated user. In this scenario, the user logs into the app when the app is opened, and the app displays the login screen again after the user logs out. The following ExternalAuthStateProvider is an example implementation of a custom AuthenticationStateProvider for this authentication scenario.

Note

The following custom AuthenticationStateProvider doesn't declare a namespace in order to make the code example applicable to any Blazor Hybrid app. However, a best practice is to provide your app's namespace when you implement the example in a production app.

ExternalAuthStateProvider.cs:

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

public class ExternalAuthStateProvider : AuthenticationStateProvider
{
    private readonly Task<AuthenticationState> authenticationState;

    public ExternalAuthStateProvider(AuthenticatedUser user) => 
        authenticationState = Task.FromResult(new AuthenticationState(user.Principal));

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

public class AuthenticatedUser
{
    public ClaimsPrincipal Principal { get; set; } = new();
}

The following steps describe how to:

  • Add required namespaces.
  • Add the authorization services and Blazor abstractions to the service collection.
  • Build the service collection.
  • Resolve the AuthenticatedUser service to set the authenticated user's claims principal. See your identity provider's documentation for details.
  • Return the built host.

In the MauiProgram.CreateMauiApp method of MauiProgram.cs, add namespaces for Microsoft.AspNetCore.Components.Authorization and System.Security.Claims:

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

Remove the following line of code that returns a built Microsoft.Maui.Hosting.MauiApp:

- return builder.Build();

Replace the preceding line of code with the following code. Add OpenID/MSAL code to authenticate the user. See your identity provider's documentation for details.

builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
builder.Services.AddSingleton<AuthenticatedUser>();
var host = builder.Build();

var authenticatedUser = host.Services.GetRequiredService<AuthenticatedUser>();

/*
Provide OpenID/MSAL code to authenticate the user. See your identity provider's 
documentation for details.

The user is represented by a new ClaimsPrincipal based on a new ClaimsIdentity.
*/
var user = new ClaimsPrincipal(new ClaimsIdentity());

authenticatedUser.Principal = user;

return host;

The following steps describe how to:

  • Add required namespaces.
  • Add the authorization services and Blazor abstractions to the service collection.
  • Build the service collection and add the built service collection as a resource to the app's ResourceDictionary.
  • Resolve the AuthenticatedUser service to set the authenticated user's claims principal. See your identity provider's documentation for details.
  • Return the built host.

In the MainWindow's constructor (MainWindow.xaml.cs), add namespaces for Microsoft.AspNetCore.Components.Authorization and System.Security.Claims:

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

Remove the following line of code that adds the built service collection as a resource to the app's ResourceDictionary:

- Resources.Add("services", serviceCollection.BuildServiceProvider());

Replace the preceding line of code with the following code. Add OpenID/MSAL code to authenticate the user. See your identity provider's documentation for details.

serviceCollection.AddAuthorizationCore();
serviceCollection.AddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
serviceCollection.AddSingleton<AuthenticatedUser>();
var services = serviceCollection.BuildServiceProvider();
Resources.Add("services", services);

var authenticatedUser = services.GetRequiredService<AuthenticatedUser>();

/*
Provide OpenID/MSAL code to authenticate the user. See your identity provider's 
documentation for details.

The user is represented by a new ClaimsPrincipal based on a new ClaimsIdentity.
*/
var user = new ClaimsPrincipal(new ClaimsIdentity());

authenticatedUser.Principal = user;

The following steps describe how to:

  • Add required namespaces.
  • Add the authorization services and Blazor abstractions to the service collection.
  • Build the service collection and add the built service collection to the app's service provider.
  • Resolve the AuthenticatedUser service to set the authenticated user's claims principal. See your identity provider's documentation for details.

In the Form1's constructor (Form1.cs), add namespaces for Microsoft.AspNetCore.Components.Authorization and System.Security.Claims:

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

Remove the following line of code that sets the built service collection to the app's service provider:

- blazorWebView1.Services = services.BuildServiceProvider();

Replace the preceding line of code with the following code. Add OpenID/MSAL code to authenticate the user. See your identity provider's documentation for details.

services.AddAuthorizationCore();
services.AddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
services.AddSingleton<AuthenticatedUser>();
var serviceCollection = services.BuildServiceProvider();
blazorWebView1.Services = serviceCollection;

var authenticatedUser = serviceCollection.GetRequiredService<AuthenticatedUser>();

/*
Provide OpenID/MSAL code to authenticate the user. See your identity provider's 
documentation for details.

The user is represented by a new ClaimsPrincipal based on a new ClaimsIdentity.
*/
var user = new ClaimsPrincipal(new ClaimsIdentity());

authenticatedUser.Principal = user;

Create a custom AuthenticationStateProvider with user change updates

To update the user while the Blazor app is running, call NotifyAuthenticationStateChanged within the AuthenticationStateProvider implementation using either of the following approaches:

Signal an authentication update from outside of the BlazorWebView (Option 1)

A custom AuthenticationStateProvider can use a global service to signal an authentication update. We recommend that the service offer an event that the AuthenticationStateProvider can subscribe to, where the event invokes NotifyAuthenticationStateChanged.

Note

The following custom AuthenticationStateProvider doesn't declare a namespace in order to make the code example applicable to any Blazor Hybrid app. However, a best practice is to provide your app's namespace when you implement the example in a production app.

ExternalAuthStateProvider.cs:

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

public class ExternalAuthStateProvider : AuthenticationStateProvider
{
    private AuthenticationState currentUser;

    public ExternalAuthStateProvider(ExternalAuthService service)
    {
        currentUser = new AuthenticationState(service.CurrentUser);

        service.UserChanged += (newUser) =>
        {
            currentUser = new AuthenticationState(newUser);
            NotifyAuthenticationStateChanged(Task.FromResult(currentUser));
        };
    }

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

public class ExternalAuthService
{
    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 MauiProgram.CreateMauiApp method of MauiProgram.cs, add a namespace for Microsoft.AspNetCore.Components.Authorization:

using Microsoft.AspNetCore.Components.Authorization;

Add the authorization services and Blazor abstractions to the service collection:

builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
builder.Services.AddSingleton<ExternalAuthService>();

In the MainWindow's constructor (MainWindow.xaml.cs), add a namespace for Microsoft.AspNetCore.Components.Authorization:

using Microsoft.AspNetCore.Components.Authorization;

Add the authorization services and the Blazor abstractions to the service collection:

serviceCollection.AddAuthorizationCore();
serviceCollection.AddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
serviceCollection.AddSingleton<ExternalAuthService>();

In the Form1's constructor (Form1.cs), add a namespace for Microsoft.AspNetCore.Components.Authorization:

using Microsoft.AspNetCore.Components.Authorization;

Add the authorization services and Blazor abstractions to the service collection:

services.AddAuthorizationCore();
services.AddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
services.AddSingleton<ExternalAuthService>();

Wherever the app authenticates a user, resolve the ExternalAuthService service:

var authService = host.Services.GetRequiredService<ExternalAuthService>();

Execute your custom OpenID/MSAL code to authenticate the user. See your identity provider's documentation for details. The authenticated user (authenticatedUser in the following example) is a new ClaimsPrincipal based on a new ClaimsIdentity.

Set the current user to the authenticated user:

authService.CurrentUser = authenticatedUser;

An alternative to the preceding approach is to set the user's principal on System.Threading.Thread.CurrentPrincipal instead of setting it via a service, which avoids use of the dependency injection container:

public class CurrentThreadUserAuthenticationStateProvider : AuthenticationStateProvider
{
    public override Task<AuthenticationState> GetAuthenticationStateAsync() =>
        Task.FromResult(
            new AuthenticationState(Thread.CurrentPrincipal as ClaimsPrincipal ?? 
                new ClaimsPrincipal(new ClaimsIdentity())));
}

Using the alternative approach, only authorization services (AddAuthorizationCore) and CurrentThreadUserAuthenticationStateProvider (.AddScoped<AuthenticationStateProvider, CurrentThreadUserAuthenticationStateProvider>()) are added to the service collection.

Handle authentication within the BlazorWebView (Option 2)

A custom AuthenticationStateProvider can include additional methods to trigger log in and log out and update the user.

Note

The following custom AuthenticationStateProvider doesn't declare a namespace in order to make the code example applicable to any Blazor Hybrid app. However, a best practice is to provide your app's namespace when you implement the example in a production app.

ExternalAuthStateProvider.cs:

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

public class ExternalAuthStateProvider : AuthenticationStateProvider
{
    private ClaimsPrincipal currentUser = new ClaimsPrincipal(new ClaimsIdentity());

    public override Task<AuthenticationState> GetAuthenticationStateAsync() =>
        Task.FromResult(new AuthenticationState(currentUser));

    public Task LogInAsync()
    {
        var loginTask = LogInAsyncCore();
        NotifyAuthenticationStateChanged(loginTask);

        return loginTask;

        async Task<AuthenticationState> LogInAsyncCore()
        {
            var user = await LoginWithExternalProviderAsync();
            currentUser = user;

            return new AuthenticationState(currentUser);
        }
    }

    private Task<ClaimsPrincipal> LoginWithExternalProviderAsync()
    {
        /*
            Provide OpenID/MSAL code to authenticate the user. See your identity 
            provider's documentation for details.

            Return a new ClaimsPrincipal based on a new ClaimsIdentity.
        */
        var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity());

        return Task.FromResult(authenticatedUser);
    }

    public void Logout()
    {
        currentUser = new ClaimsPrincipal(new ClaimsIdentity());
        NotifyAuthenticationStateChanged(
            Task.FromResult(new AuthenticationState(currentUser)));
    }
}

In the preceding example:

  • The call to LogInAsyncCore triggers the login process.
  • The call to NotifyAuthenticationStateChanged notifies that an update is in progress, which allows the app to provide a temporary UI during the login or logout process.
  • Returning loginTask returns the task so that the component that triggered the login can await and react after the task is complete.
  • The LoginWithExternalProviderAsync method is implemented by the developer to log in the user with the identity provider's SDK. For more information, see your identity provider's documentation. The authenticated user (authenticatedUser) is a new ClaimsPrincipal based on a new ClaimsIdentity.

In the MauiProgram.CreateMauiApp method of MauiProgram.cs, add the authorization services and the Blazor abstraction to the service collection:

builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();

In the MainWindow's constructor (MainWindow.xaml.cs), add the authorization services and the Blazor abstraction to the service collection:

serviceCollection.AddAuthorizationCore();
serviceCollection.AddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();

In the Form1's constructor (Form1.cs), add the authorization services and the Blazor abstraction to the service collection:

services.AddAuthorizationCore();
services.AddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();

The following LoginComponent component demonstrates how to log in a user. In a typical app, the LoginComponent component is only shown in a parent component if the user isn't logged into the app.

Shared/LoginComponent.razor:

@inject AuthenticationStateProvider AuthenticationStateProvider

<button @onclick="Login">Log in</button>

@code
{
    public async Task Login()
    {
        await ((ExternalAuthStateProvider)AuthenticationStateProvider)
            .LogInAsync();
    }
}

The following LogoutComponent component demonstrates how to log out a user. In a typical app, the LogoutComponent component is only shown in a parent component if the user is logged into the app.

Shared/LogoutComponent.razor:

@inject AuthenticationStateProvider AuthenticationStateProvider

<button @onclick="Logout">Log out</button>

@code
{
    public async Task Logout()
    {
        await ((ExternalAuthStateProvider)AuthenticationStateProvider)
            .Logout();
    }
}

Accessing other authentication information

Blazor doesn't define an abstraction to deal with other credentials, such as access tokens to use for HTTP requests to web APIs. We recommend following the identity provider's guidance to manage the user's credentials with the primitives that the identity provider's SDK provides.

It's common for identity provider SDKs to use a token store for user credentials stored in the device. If the SDK's token store primitive is added to the service container, consume the SDK's primitive within the app.

The Blazor framework isn't aware of a user's authentication credentials and doesn't interact with credentials in any way, so the app's code is free to follow whatever approach you deem most convenient. However, follow the general security guidance in the next section, Other authentication security considerations, when implementing authentication code in an app.

Other authentication security considerations

The authentication process is external to Blazor, and we recommend that developers access the identity provider's guidance for additional security guidance.

When implementing authentication:

  • Avoid authentication in the context of the Web View. For example, avoid using a JavaScript OAuth library to perform the authentication flow. In a single-page app, authentication tokens aren't hidden in JavaScript and can be easily discovered by malicious users and used for nefarious purposes. Native apps don't suffer this risk because native apps are only able to obtain tokens outside of the browser context, which means that rogue third-party scripts can't steal the tokens and compromise the app.
  • Avoid implementing the authentication workflow yourself. In most cases, platform libraries securely handle the authentication workflow, using the system's browser instead of using a custom Web View that can be hijacked.
  • Avoid using the platform's Web View control to perform authentication. Instead, rely on the system's browser when possible.
  • Avoid passing the tokens to the document context (JavaScript). In some situations, a JavaScript library within the document is required to perform an authorized call to an external service. Instead of making the token available to JavaScript via JS interop:
    • Provide a generated temporary token to the library and within the Web View.
    • Intercept the outgoing network request in code.
    • Replace the temporary token with the real token and confirm that the destination of the request is valid.

Additional resources