SignIn for Blazor Server-Side app not working

Basically, it happens because the SigninManger::SignInAsync() will actually try to send a cookie over HTTP to indicate this user has already signed in. But when dealing with Blazor Server Side at this moment, there's no available HTTP Response at all , there's only a WebSocket connection (SignalR).

How to Fix

In a nutshell, Signin is to persist user credentials/cookies/... so that the WebApp knows who the client is. Since you're using a Blazor Server Side, your client is talking to the server within a WebSocket connection. There's no need to send cookies over HTTP. Because your WebApp has already knows who the current user is.

To fix this issue, register an IHostEnvironmentAuthenticationStateProvider service firstly:

services.AddScoped<AuthenticationStateProvider, RevalidatingIdentityAuthenticationStateProvider<IdentityUser>>();
services.AddScoped<IHostEnvironmentAuthenticationStateProvider>(sp => {
    // this is safe because 
    //     the `RevalidatingIdentityAuthenticationStateProvider` extends the `ServerAuthenticationStateProvider`
    var provider = (ServerAuthenticationStateProvider) sp.GetRequiredService<AuthenticationStateProvider>();
    return provider;
});

And then create a principal and replace the old one .

@inject AuthenticationStateProvider AuthenticationStateProvider
@inject IHostEnvironmentAuthenticationStateProvider HostAuthentication
...

var user = await userManager.FindByNameAsync(UserName);
var valid= await signInManager.UserManager.CheckPasswordAsync(user, Password);

if (valid)
{
    var principal = await signInManager.CreateUserPrincipalAsync(user);

    var identity = new ClaimsIdentity(
        principal.Claims,
        Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme
    );
    principal = new System.Security.Claims.ClaimsPrincipal(identity);
    signInManager.Context.User = principal;
    HostAuthentication.SetAuthenticationState(Task.FromResult(new AuthenticationState(principal)));

    // now the authState is updated
    var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();

    successMessage = $"{UserName}, signed in.";
    errorMessage = "";

}
else
{
    successMessage = "";
    errorMessage = "Username or password is incorrect.";
}

Demo

enter image description here

And check the authState:

enter image description here


One of the issues with the previous answer by itminus and discussed in the comments was keeping the state of the user after a manual refresh, session end, or a link that caused a refresh. This would lose the user's state because the cookie value wasn't being set to the client's browser, which meant the next HTTP request didn't include the cookie. One solution is to use static login/out pages which would allow the cookies to be sent to the client's browser.

This method instead uses JS to write the cookies to the client's browser, allowing Blazor to handle everything. I ran into some issues with the cookie settings not properly setting, because of my misunderstanding of how AddCookie() in the Startup adds the options to the DI container. It uses IOptionsMonitor to use named options, using the Scheme as the key.

I've modified the sign in code to invoke JS that will save the cookie. You can run this after registering a new user or signing in an existing user.

Ensure you DI the IOptionsMonitor<CookieAuthenticationOptions>, allowing you to resolve the named options, using the Scheme as the key. Ensure you use .Get(schemeName) instead of .CurrentValue, else you're TicketDataFormat (and other settings) will be incorrect, as it'll use the default values. It took me hours to realize this.

Note: IOptionsMonitor<CookieAuthenticationOptions> comes from calling services.AddAuthentication().AddCookie(). An example is provided below this.

    _cookieAuthenticationOptions = cookieAuthenticationOptionsMonitor.Get("MyScheme");
    ...
    private async Task SignInAsync(AppUser user, String password)
    {
        //original code from above answer
        var principal = await _signInManager.CreateUserPrincipalAsync(user);

        var identity = new ClaimsIdentity(
            principal.Claims,
            "MyScheme"
        );
        principal = new ClaimsPrincipal(identity);
        _signInManager.Context.User = principal;
        _hostAuthentication.SetAuthenticationState(Task.FromResult(new AuthenticationState(principal)));

        // this is where we create a ticket, encrypt it, and invoke a JS method to save the cookie
        var ticket = new AuthenticationTicket(principal, null, "MyScheme");
        var value = _cookieAuthenticationOptions.TicketDataFormat.Protect(ticket);
        await _jsRuntime.InvokeVoidAsync("blazorExtensions.WriteCookie", "CookieName", value, _cookieAuthenticationOptions.ExpireTimeSpan.TotalDays);
    }

We then write a JS cookie:

    window.blazorExtensions = {

        WriteCookie: function (name, value, days) {

            var expires;
            if (days) {
                var date = new Date();
                date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
                expires = "; expires=" + date.toGMTString();
            }
            else {
                expires = "";
            }
            document.cookie = name + "=" + value + expires + "; path=/";
        }
    }

This will successfully write the cookie to the client's browser. If you are having issues, make sure that your Startup is using the same scheme name. If you don't, then the normal cookie authentication system will not properly parse back the principal that was encoded:

        services.AddIdentityCore<AppUser>()
            .AddRoles<IdentityRole>()
            .AddEntityFrameworkStores<AppDbContext>()
            .AddSignInManager();

        services.AddAuthentication(options =>
        {
            options.DefaultScheme = "MyScheme";
        }).AddCookie("MyScheme", options =>
        {
            options.Cookie.Name = "CookieName";
        });

For completionist, you can also implement the log off the same way:

    private async Task SignOutAsync()
    {
        var principal = _signInManager.Context.User = new ClaimsPrincipal(new ClaimsIdentity());
        _hostAuthentication.SetAuthenticationState(Task.FromResult(new AuthenticationState(principal)));

        await _jsRuntime.InvokeVoidAsync("blazorExtensions.DeleteCookie", _appInfo.CookieName);

        await Task.CompletedTask;
    }

And the JS:

    window.blazorExtensions = {
        DeleteCookie: function (name) {
            document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:01 GMT";
        }
    }