Entity Framework with Blazor Login Page

Viewed 6276

When it comes to use Identity Framework with Blazor Server, Microsoft's official statement is that it's not supported and to use a Razor login page instead. The issue is that Blazor can't write cookies.

This has a few drawbacks:

  • Cannot re-use the HTML layout files, need to recreate duplicate layout files for Razor.
  • Cannot embed login buttons on Blazor page
  • Poor user experience if need to optionally login as part of a checkout experience

This guy figured out a way to make a Blazor login page work with Blazor WebAssembly... not sure how he worked around the issue nor if a similar approach can work for Blazor Server.

I was thinking of alternative solutions. The issue is to store the cookie. Local storage can be used in Blazor but local storage is not safe for security tokens. Cookies however could also be set via JavaScript interop in a similar way.

Would this approach work, setting the login cookie via JavaScript interop after login, and then the cookie gets sent on any further page load? Has anyone done it?

For single-page admin content, I found a simpler approach of creating a GatedContent component that shows a login form, and then shows ChildContent after login. Of course, this won't preserve the session on page refresh, but it works for some cases.

2 Answers

Here's a solution for single-page admin content: GatedContent

It will show a login form, and after successful login, show the gated content.

SpinnerButton is defined here.

@using Microsoft.AspNetCore.Identity
@using System.ComponentModel.DataAnnotations
@inject NavigationManager navManager
@inject SignInManager<ApplicationUser> signInManager
@inject UserManager<ApplicationUser> userManager;

@if (!LoggedIn)
{
<EditForm Context="formContext" class="form-signin" OnValidSubmit="@SubmitAsync" Model="this">
    <DataAnnotationsValidator />

    <div style="margin-bottom: 1rem; margin-top: 1rem;" class="row">
        <Field For="@(() => Username)" Width="4" Required="true">
            <RadzenTextBox @bind-Value="@Username" class="form-control" Style="margin-bottom: 0px;" />
        </Field>
    </div>
    <div style="margin-bottom: 1rem" class="row">
        <Field For="@(() => Password)" Width="4" Required="true">
            <RadzenPassword @bind-Value="@Password" class="form-control" />
        </Field>
    </div>

    <SpinnerButton @ref="ButtonRef" style="width:150px" Text="Login" ButtonType="@((Radzen.ButtonType)ButtonType.Submit)" ButtonStyle="@((Radzen.ButtonStyle)ButtonStyle.Primary)" OnSubmit="LogInAsync" />
    @if (Error.HasValue())
    {
        <div class="red">@Error</div>
    }
</EditForm>
}
else
{
    @ChildContent
}
@code {
    public SpinnerButton? ButtonRef { get; set; }
    public async Task SubmitAsync() => await ButtonRef!.FormSubmitAsync().ConfigureAwait(false);

    [Required]
    public string Username { get; set; } = string.Empty;

    [Required]
    public string Password { get; set; } = string.Empty;

    string? Error { get; set; }
    bool LoggedIn { get; set; }

    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    public async Task LogInAsync()
    {
        Error = null;
        try
        {
            var user = await userManager.FindByNameAsync(Username).ConfigureAwait(false);
            if (user != null)
            {
                var isAdmin = await userManager.IsInRoleAsync(user, ApplicationRole.Admin).ConfigureAwait(false);
                if (isAdmin)
                {
                    var singInResult = await signInManager.CheckPasswordSignInAsync(user, Password, true).ConfigureAwait(false);
                    if (singInResult.Succeeded)
                    {
                        LoggedIn = true;
                    }
                    else
                    {
                        Error = "Invalid password";
                    }
                }
                else
                {
                    Error = "User is not Admin";
                }
            }
            else
            {
                Error = "Username not found";
            }
        }
        catch (Exception ex)
        {
            Error = ex.Message;
        }
    }
}

Use like this

<GatedContent>
    This is an admin page.
</GatedContent>

Of course, this solution doesn't preserve the state after page reload, but it works for simple admin pages.

My workaround is like this: I've created a SignInController like this

    public class SignInController : ControllerBase
    {
        private readonly SignInManager<IdentityUser> _signInManager;

        public SignInController(SignInManager<IdentityUser> signInManager)
        {
            _signInManager = signInManager;
        }

        [HttpPost("/signin")]
        public IActionResult Index([FromForm]string username, [FromForm]string password, [FromForm]string rememberMe)
        {
            bool.TryParse(rememberMe, out bool res);
            var signInResult = _signInManager.PasswordSignInAsync(username, password, res, false);
            if (signInResult.Result.Succeeded)
            {
                return Redirect("/");
            }
            return Redirect("/login/"+ signInResult.Result.Succeeded);
        }

        [HttpPost("/signout")]
        public async Task<IActionResult> Logout()
        {
            if (_signInManager.IsSignedIn(User))
            {
                await _signInManager.SignOutAsync();
            }
            return Redirect("/");
        }
    }

And I've a login.razor like this:

@page "/login"
    <form action="/signin" method="post">
            <div class="form-group">
                <p> Username </p>
                <input id="username" Name="username" />
            </div>
            <div class="form-group">
                <p>password<p/>
                <input type="password" id="password" />
            </div>
            <div class="form-group">
                <p> remember me? <p/>
                <input type="checkbox" id="rememberMe" />
            </div>
            <button type="Submit" Text="login" />
    </form>

This issue is caused by the signInManager which is only working if you use HTTP/S requests. Otherwise, it will throw Exceptions. There are also some reports about that on the dotnet/aspcore.net Repo on GitHub.

At the moment this is the only way (I know) to use it with blazor server.

Related