SignInManager and Blazor

If you are interested in using Asp.Net Core's Identity to manage and authenticate users inside your Blazor app, then you will be unpleasantly surprised to find out (the hard way in our case) that certain operations on the SignInManager class are not supported. The normal way you use the SignInManager in a traditional Razor is after the user clicks the Login button, you verify their credentials using UserManager, after which you call SignInAsync on the SignInManager. Easy! Not so easy in Blazor. We will explore why that is and what you can do about it.

User Management Scaffolding

When you first set up a new Blazor project, Microsoft's wizard will ask you if you would like to use Identity for authentication. If you say yes, you are then given a way to scaffold the Razor pages for registration, login, reset password, confirm email and so on. That's all fine and well, but the problem is that you don't want Razor pages. You want all the user management forms to be Blazor pages. That's fine, you spend some time pilfering through the scaffolded pages and through the magic of copy-paste, you port over the various forms into your Blazor application. This is what we did in Start Blazoring, where we have a fully native user management experience.

SignInManager Does Not Work!

When you get to the login page and write the code to actually log in (copied from the scaffolded code), everything looks great, you compile and run it, only to discover that the most crucial call to SignInManager.SignInAsync() does not work. You get an exception that says something like this:

The response headers cannot be modified because the response has already started

Mystified you go to the internet only to find out that at this point in the Blazor application there is no concept of an http request. That has already completed when _Host.cshtml was served. The code where SignInAsync is running from in the context of the Blazor circuit/SignalR connection. It turns out that the way SignInAsync works is by setting a cookie on the HttpContext. However, because the http response has already been sent, you get an exception instead.

Leveraging Controllers and Navigation

What to do? We have to somehow call SignInAsync from somewhere that is in the realm of the http request. We do that by leveraging the NavigationManager. But first, let's see how our Blazor login code looks like:

@inject UserManager<ApplicationUser> UserManager
@inject NavigationManager NavigationManager
@inject IDataProtectionProvider DataProtectionProvider

//...

@code 
{
    // ...
    
    var identityUser = await UserManager.FindByEmailAsync(_model.UsernameOrEmail) ?? 
        await UserManager.FindByNameAsync(_model.UsernameOrEmail);
    
    if (await UserManager.CheckPasswordAsync(identityUser, _model.Password) == true) 
    {
        // Now what?
    }
}

Now we use the NavigationManager to navigate to a new controller, where we can call into the SignInManager. We will explain below why this works, but first what exactly are we navigating to and how does the controller handler method know whom to log in? Here is the code:

var token = await UserManager.GenerateUserTokenAsync(identityUser, TokenOptions.DefaultProvider, "Login");
var data = $"{identityUser.Id}|{token}|_model.RememberMe";
var protector = DataProtectionProvider.CreateProtector("Login");
var protectedData = protector.Protect(data);

NavigationManager.NavigateTo("/Account/LoginInternal?token=" + protectedData, true);

Let's break this down. The first call to generate a user specific token for the "Login" purpose. The purpose can be any string, but when you verify the token, you must pass the same purpose. Now that we have a token, we put it alongside the user's id and encrypt it using the data protection provider. Finally, we tell Blazor to tell the browser to navigate to /Account/LoginInternal and pass in our encrypted token.

So far so good. On the other side of the fence, we have the following:

public class AccountController : Controller
{
    private readonly IDataProtectionProvider _dataProtectionProvider;
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly SignInManager<ApplicationUser> _signInManager;

    public AccountController(
        IDataProtectionProvider dataProtectionProvider, 
        UserManager<ApplicationUser> userManager, 
        SignInManager<ApplicationUser> signInManager
    )
    {
        _dataProtectionProvider = dataProtectionProvider;
        _userManager = userManager;
        _signInManager = signInManager;
    }

    [HttpGet("/Account/LoginInternal")]
    [AllowAnonymous]
    public async Task<IActionResult> LoginInternal(string token)
    {
        var dataProtector = _dataProtectionProvider.CreateProtector("Login");
        var data = dataProtector.Unprotect(t);
        var parts = data.Split('|');
        var identityUser = await _userManager.FindByIdAsync(parts[0]);

        if (identityUser == null)
        {
            return Unauthorized();
        }

        var isTokenValid = await _userManager.VerifyUserTokenForLoginAsync(identityUser, TokenOptions.DefaultProvider, parts[1]);

        if (isTokenValid)
        {
            var isPersistent = bool.Parse(parts[2]);

            await _userManager.ResetAccessFailedCountAsync(identityUser);

            await _signInManager.SignInAsync(identityUser, isPersistent);

            return Redirect("/");
        }

        return Unauthorized();
    }
}

Here we basically do the reverse of what we did in the previous step. We use the data protector to decrypt the token so we can get the user id and the access token. After verifying the access token, we can now use the SignInManager to log the user in. The reason this works is because they way we got here is by way of navigation. That means that we are now inside the http request/response pipeline and the SignInManager now has access to the HttpContext so it can set the cookie. When we are done, we simply do a redirect to the home page.

The logout operation is analogous and in that case we need to call SignInManager.SignOutAsync().

Table of Contents

  • Refit and Controllers

    We talk about how to create apis alongside your Blazor app and how to access those apis using Refit.

  • Blazor Gotchas

    This article explores various Blazor pitfalls that you should be aware of.

  • Blazor and HttpContext

    Here we discuss how to correctly use HttpContext in a Blazor app.

  • SignInManager and Blazor

    Blazor and SignInManager don't play well together. Let's see how to get around that.

  • One Year with Blazor

    We've switched over to using Blazor exclusively a year ago. Here are our thoughts.



An error has occurred. This application may no longer respond until reloaded. Reload 🗙