Aspnetcore: [Server Side] Support Custom Login Component when using Identity

Created on 1 Sep 2019  路  15Comments  路  Source: dotnet/aspnetcore

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.
affected-few area-blazor enhancement severity-major

Most helpful comment

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

All 15 comments

We've moved this issue to the Backlog milestone. This means that it is not going to happen for the coming release. We will reassess the backlog following the current release and consider this item at that time. However, keep in mind that there are many other high priority features with which it will be competing for resources.

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

Humble 'same here'.
Would be awesome to have this as it would bring us simple way how do single page app login without necessity of page refresh <3

+1

+1
Personally speaking it feels like a glaring oversight.
I want to be able to work in Blazor (server side) without the need for Identity whatsoever or having to redirect to a razor page to manage user login.

I would also appreciate. Sad thing is even login via httpclient/webapi is not working. You have to use razorpages

I would like to see this addressed. I was really surprised that the template project didn't have a razor component for sign in. I tried to make my own, exactly like this issue describes, and found it didn't work.

Same here, really disappointing to not have this in Blazor, and even more so that it seems to be only coming with 5.0. It's just so much more effort to style an application consistently when you have to use two different frameworks...

Unfortunately the workaround for this from now is the use of a Razor view to work with the Login process...

All the rest of roles integration is supported at Blazor (.razor) pages as described in the docs.

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=, 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=" 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>();
            //.....
        }

For anyone interested, here's a working sample. Demo-user: [email protected] / pw: Test.123

The customized parts are:

  • appsettings.json: Change connection string to your env!
  • App.razor: Added RedirectToLogin component
  • _Imports.razor: Added global [Authorize] attribute to protect pages by default
  • Folder IdentityUtils : Contains the required middleware and a small model to handle login procedure
  • Startup.cs: Registering of middleware and some code to create demo user with password
  • Folder Pages/Identity: Contains the Blazor components (views) for login and 2FA check
  • _Host.cshtml: Changed render-mode to "Server"

Additional identity features are quite easy to implement, because most stuff is really just about building a UI around SignInManager and UserManager. Personally I just copy the concepts from the Identity scaffolder into Blazor views. Quite simple once the login procedure works.

That's it, have fun!
Download: BlazorIdentity.zip

For anyone interested, here's a working sample. Demo-user: [email protected] / pw: Test.123

The customized parts are:

  • appsettings.json: Change connection string to your env!
  • App.razor: Added RedirectToLogin component
  • _Imports.razor: Added global [Authorize] attribute to protect pages by default
  • Folder IdentityUtils : Contains the required middleware and a small model to handle login procedure
  • Startup.cs: Registering of middleware and some code to create demo user with password
  • Folder Pages/Identity: Contains the Blazor components (views) for login and 2FA check
  • _Host.cshtml: Changed render-mode to "Server"

Additional identity features are quite easy to implement, because most stuff is really just about building a UI around SignInManager and UserManager. Personally I just copy the concepts from the Identity scaffolder into Blazor views. Quite simple once the login procedure works.

That's it, have fun!
Download: BlazorIdentity.zip

Maybe this could be added in the official docs somewhere?

For anyone interested, here's a working sample. Demo-user: [email protected] / pw: Test.123

The customized parts are:

  • appsettings.json: Change connection string to your env!
  • App.razor: Added RedirectToLogin component
  • _Imports.razor: Added global [Authorize] attribute to protect pages by default
  • Folder IdentityUtils : Contains the required middleware and a small model to handle login procedure
  • Startup.cs: Registering of middleware and some code to create demo user with password
  • Folder Pages/Identity: Contains the Blazor components (views) for login and 2FA check
  • _Host.cshtml: Changed render-mode to "Server"

Additional identity features are quite easy to implement, because most stuff is really just about building a UI around SignInManager and UserManager. Personally I just copy the concepts from the Identity scaffolder into Blazor views. Quite simple once the login procedure works.

That's it, have fun!
Download: BlazorIdentity.zip

Hi, can you tell me your blog URL? I want to read your article

Hi, can you tell me your blog URL? I want to read your article

Unfortunately I still haven't had time to write the blog article. Sorry. Can I answer any questions you may have on here?

Hi, can you tell me your blog URL? I want to read your article

Unfortunately I still haven't had time to write the blog article. Sorry. Can I answer any questions you may have on here?

Okay, I just saw your previous reply saying "I'll write a blog post about it and share it later.". Since there is no blog, there are no more questions. Thank you for your reply

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Rast1234 picture Rast1234  路  104Comments

radenkozec picture radenkozec  路  114Comments

glennc picture glennc  路  117Comments

KerolosMalak picture KerolosMalak  路  269Comments

barrytang picture barrytang  路  89Comments