[Server Side] Support Custom Login Component when using Identity

See original GitHub issue

Description

Since I’m not a huge fan of the scaffolder to create and customize Identity pages, it would be nice to have a Login Component. So we can use the SignInManager to login and not rely on the Scaffolder.

Currently, if you use a SignInManager in a custom made Login Component with the following code on a button click:

@inject SignInManager<IdentityUser> SignInManager
// Some textfields and button here
    public async Task DoLogin()
    {
        await SignInManager.PasswordSignInAsync(Model.Email, Model.Password, true, false);
    }

Due to SignalR, the following exception is thrown:

System.InvalidOperationException: Headers are read-only, response has already started.
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.ThrowHeadersReadOnlyException()
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.Microsoft.AspNetCore.Http.IHeaderDictionary.set_Item(String key, StringValues value)
   at Microsoft.AspNetCore.Http.ResponseCookies.Append(String key, String value, CookieOptions options)
   at Microsoft.AspNetCore.Authentication.Cookies.ChunkingCookieManager.AppendResponseCookie(HttpContext context, String key, String value, CookieOptions options)
   at Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler.HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)
   at Microsoft.AspNetCore.Authentication.AuthenticationService.SignInAsync(HttpContext context, String scheme, ClaimsPrincipal principal, AuthenticationProperties properties)
   at Microsoft.AspNetCore.Identity.SignInManager`1.SignInWithClaimsAsync(TUser user, AuthenticationProperties authenticationProperties, IEnumerable`1 additionalClaims)
   at Microsoft.AspNetCore.Identity.SignInManager`1.SignInOrTwoFactorAsync(TUser user, Boolean isPersistent, String loginProvider, Boolean bypassTwoFactor)
   at Microsoft.AspNetCore.Identity.SignInManager`1.PasswordSignInAsync(TUser user, String password, Boolean isPersistent, Boolean lockoutOnFailure)
   at Microsoft.AspNetCore.Identity.SignInManager`1.PasswordSignInAsync(String userName, String password, Boolean isPersistent, Boolean lockoutOnFailure)
   at Ecocolors.Server.Pages.Users.Users_Login.DoSomething() in C:\Users\Append\source\repos\vertonghenb\Ecocolors\source\Ecocolors.Server\Pages\Users\Users.Login.razor:line 74
   at Microsoft.AspNetCore.Components.ComponentBase.CallStateHasChangedOnAsyncCompletion(Task task)
   at Metronic.Components.KTButton.ClickCallback(UIMouseEventArgs args) in C:\Users\Append\source\repos\vertonghenb\Ecocolors\source\Metronic\Components\Buttons\KTButton.razor:line 50
   at Microsoft.AspNetCore.Components.ComponentBase.CallStateHasChangedOnAsyncCompletion(Task task)
   at Microsoft.AspNetCore.Components.Rendering.Renderer.GetErrorHandledTask(Task taskToHandle)

Solution

  • Create a build-in LoginComponent we can override.
  • Provide a workaround solution for the exception.

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Reactions:22
  • Comments:20 (3 by maintainers)

github_iconTop GitHub Comments

10reactions
MarcoTheFirstcommented, Aug 25, 2020

I found a solution to make Identity work with pure Blazor components in Core 3.1. I’ll write a blog post about it and share it later. Here’s the principle:

  1. Create a Login.razor component and inject SignInManager and NavigationManager. Use SignInManager to verify the password using the method CheckPasswordSignInAsync(). Do NOT call PasswordSignInAsync() as it will throw the exception mentioned earlier. Instead, pass the credentials to a credentials-cache in a custom middleware (see next paragraph). Then call NavigationManager.NagigateTo(/login?key=<someGuid>, true ) to execute a full postback, which is required for setting the cookie.

  2. Create a Middleware class (I called it BlazorCookieLoginMiddleware): In there you use a static dictionary to cache login info from the Blazor login component. Also, you intercept the request to “/login?key=<guid>” and then perform the actual sign in using the SignInManager. This works because the middleware is executed earlier in the pipeline, when cookies can still be set. The credentials can be retrieved from the static dictionary cache and should immediately be removed from the dict. If the authentication was successful, you simply redirect the user to the app root “/” or where ever you want.

I tested this, it works like a charm. I also added 2FA successfully, but that would be too much for this post.

Here’s some code (please note: Edge cases and errors are not handled correctly for the sake of simplicity; just a PoC):

Login.razor:

@page "/login"
@attribute [AllowAnonymous]
@inject SignInManager<ApplicationUser> SignInMgr
@inject UserManager<ApplicationUser> UserMgr
@inject NavigationManager NavMgr

<h3>Login</h3>

    <label for="email">Email:</label>
    <input type="email" @bind="Email" name="email" />
    <label for="password">Password:</label>
    <input type="password" @bind="password" name="password" />
    @if (!string.IsNullOrEmpty(error))
    {
        <div class="alert-danger">
            <p>@error</p>
        </div>
    }
    <button @onclick="LoginClicked">Login</button>

@code {
    public string Email { get; set; }

    private string password;
    private string error;

    private async Task LoginClicked()
    {
        error = null;
        var usr = await UserMgr.FindByEmailAsync(Email);
        if (usr == null)
        {
            error = "User not found";
            return;
        }


        if (await SignInMgr.CanSignInAsync(usr))
        {
            var result = await SignInMgr.CheckPasswordSignInAsync(usr, password, true);
            if (result == Microsoft.AspNetCore.Identity.SignInResult.Success)
            {
                Guid key = Guid.NewGuid();
                BlazorCookieLoginMiddleware.Logins[key] = new LoginInfo { Email = Email, Password = password };
                NavMgr.NavigateTo($"/login?key={key}", true);
            }
            else
            {
                error = "Login failed. Check your password.";
            }
        }
        else
        {
            error = "Your account is blocked";
        }
    }
}

BlazorCookieLoginMiddleware.cs:

    public class LoginInfo
    {
        public string Email { get; set; }

        public string Password { get; set; }
    }

    public class BlazorCookieLoginMiddleware
    {
        public static IDictionary<Guid, LoginInfo> Logins { get; private set; }
            = new ConcurrentDictionary<Guid, LoginInfo>();        


        private readonly RequestDelegate _next;

        public BlazorCookieLoginMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task Invoke(HttpContext context, SignInManager<ApplicationUser> signInMgr)
        {
            if (context.Request.Path == "/login" && context.Request.Query.ContainsKey("key"))
            {
                var key = Guid.Parse(context.Request.Query["key"]);
                var info = Logins[key];

                var result = await signInMgr.PasswordSignInAsync(info.Email, info.Password, false, lockoutOnFailure: true);
                info.Password = null;
                if (result.Succeeded)
                {
                    Logins.Remove(key);
                    context.Response.Redirect("/");
                    return;
                }
                else if (result.RequiresTwoFactor)
                {
                    //TODO: redirect to 2FA razor component
                    context.Response.Redirect("/loginwith2fa/" + key);
                    return;
                }
                else
                {
                    //TODO: Proper error handling
                    context.Response.Redirect("/loginfailed");
                    return;
                }    
            }     
            else
            {
                await _next.Invoke(context);
            }
        }
    }

and don’t forget to add new middleware to Startup.cs:

        public void Configure(IApplicationBuilder app)
        {
            //.....
            app.UseAuthentication();
            app.UseAuthorization();
            
            app.UseMiddleware<BlazorCookieLoginMiddleware>();
            //.....
        }
8reactions
vhafdalcommented, Sep 28, 2019

This has also been an issue for me. the need to direct to a Razor Page to handle the login as a work around.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Adding a custom login page to Blazor Server app.
If you want to customize the Identity pages, you need to add a bit of scaffolding. The easiest way to do this is...
Read more >
How To Add Custom Authentication In Blazor
This code creates a custom authentication handler that uses a user service to validate tokens and create a claims identity. The ...
Read more >
How to implement custom authentication in Blazor Server ...
Identity ) #Blazor #AspNetCore # Authentication Join this channel to get access to ... Creating an Auth Service ( use databases instead in...
Read more >
Custom blazor server-side authentication
I would like to add roles or claims now to be able to include them in the component, but find it difficult to...
Read more >
Scaffold Identity in ASP.NET Core projects
Support Custom Login Component when using Identity ... to actually implement custom login form for server-side blazor (dotnet/AspNetCore.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found