2
votes

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
Have you tried to set the cookie using the JavaScript interop?WΩLLE - ˈvɔlə
Not yet. Storing elsewhere would require creating a custom AuthenticationStateProvider to get the state... and other code elsewhere to set the authentication state. There might be a lot of complex details involved. We might need to also create a custom SignInManager and override SignInAsync.Etienne Charland
For AuthenticationStateProvider, I would not want a round-trip for every check. It would keep the standard cookie look-up, but needs to additionally detect a login that happened since... where would that login be stored meanwhile? If noone has done it, I'll put this on the back-burner to try it later on.Etienne Charland
Why don't you just use an ASP.NET Core MVC AuthenticationController to handle the login / logout? Works like a charm for us in Blazor Server-Side. Maybe there will be nice implementation in the future for Blazor-Only authentication, who knows.WΩLLE - ˈvɔlə

2 Answers

1
votes

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.

0
votes

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.