Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When using SignInManager.SignInAsync(.......) under Blazor on server side then it is throwing the exception

I am learning Server side blazor and trying to learn Authentication. Whenever I use SignInManager.SignInAsync(.......), it throws the following exception:

System.InvalidOperationException: The response headers cannot be modified because the response has already started. at Microsoft.AspNetCore.HttpSys.Internal.HeaderCollection.ThrowIfReadOnly() at Microsoft.AspNetCore.HttpSys.Internal.HeaderCollection.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.SignInManager1.SignInWithClaimsAsync(TUser user, AuthenticationProperties authenticationProperties, IEnumerable1 additionalClaims) at GroupMembersInfo.Pages.RegisterUser.Register() in C:\my_work\Blazor_learning\GroupMembersInfo\Pages\RegisterUser.razor:line 52 at Microsoft.AspNetCore.Components.ComponentBase.CallStateHasChangedOnAsyncCompletion(Task task) at Microsoft.AspNetCore.Components.Forms.EditForm.HandleSubmitAsync()
at Microsoft.AspNetCore.Components.ComponentBase.CallStateHasChangedOnAsyncCompletion(Task task) at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle)

I think the exception is being thrown from the method I have highlighted. So How do I mitigate this issue. Here my code

starup.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using GroupMembersInfo.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;

namespace GroupMembersInfo
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContextPool<AppDbContext>(
                options => options.UseSqlServer(Configuration.GetConnectionString("GMIDbConnection")));

            services.AddIdentity<IdentityUser, IdentityRole>(options => options.SignIn.RequireConfirmedAccount = true)
                .AddEntityFrameworkStores<AppDbContext>();

            services.AddRazorPages();
            services.AddServerSideBlazor();
            services.AddScoped<AuthenticationStateProvider, RevalidatingIdentityAuthenticationStateProvider<IdentityUser>>();
            services.AddSingleton<WeatherForecastService>();

            services.ConfigureApplicationCookie(options =>
            {
                options.SlidingExpiration = true;

                options.Events.OnRedirectToLogin = cxt =>
                {
                    cxt.Response.StatusCode = 401;
                    return Task.CompletedTask;
                };

                options.Events.OnRedirectToAccessDenied = cxt =>
                {
                    cxt.Response.StatusCode = 403;
                    return Task.CompletedTask;
                };

                options.Events.OnRedirectToLogout = cxt => Task.CompletedTask;
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapBlazorHub();
                endpoints.MapFallbackToPage("/_Host");
            });
        }
    }
}

Login Component Code

@page "/loginPage"

@using Microsoft.AspNetCore.Identity;
@using GroupMembersInfo.Data;

@inject UserManager<IdentityUser> UserManager
@inject SignInManager<IdentityUser> SignInManager
@inject NavigationManager NavigationManager

<h3>Log In</h3>

<div class="row">
    <div class="col-md-12">
        <EditForm Model="@RegisterUserModel" OnValidSubmit="@LogIn">
            <div class="form-group">
                <label>Iser Id: </label>
                <input @bind-value="RegisterUserModel.Email" class="form-control" />
            </div>
            <div class="form-group">
                <label>Password: </label>
                <input @bind-value="RegisterUserModel.Password" class="form-control" />
            </div>
            <div class="form-group">
                <button type="submit">Submit</button>
            </div>
        </EditForm>
    </div>
</div>

@code {
    public RegisterUserModel RegisterUserModel { get; set; } = new RegisterUserModel();

    public async Task LogIn()
    {
        var user = new IdentityUser
        {
            UserName = RegisterUserModel.Email
        };

        user.PasswordHash = SignInManager.UserManager.PasswordHasher.HashPassword(user, RegisterUserModel.Password);

        await SignInManager.SignInAsync(user, isPersistent: false);

        NavigationManager.NavigateTo("/");
    }
}
like image 745
Sanjay Jain Avatar asked Jan 24 '20 14:01

Sanjay Jain


3 Answers

I edited my code as below as per your suggestion and it worked.

        await SignInManager.SignInAsync(user, isPersistent: false).ContinueWith(prop =>
        {
            NavigationManager.NavigateTo("/");
        });
like image 70
Sanjay Jain Avatar answered Oct 11 '22 23:10

Sanjay Jain


The problem is in below two lines:

await SignInManager.SignInAsync(user, isPersistent: false);

NavigationManager.NavigateTo("/");

Before SignInAsync is completed, the page is navigating to the next page. That's why SignInAsync is not able to complete its work.

Solution:

If you add return statement before NavigationManager.NavigateTo, it should resolve the issue.

This is because the method cannot return until the awaited operation is complete.

return NavigationManager.NavigateTo("/");

Hope this helps.

like image 39
Manoj Choudhari Avatar answered Oct 11 '22 22:10

Manoj Choudhari


the given answer did not work for me, maybe things are now different in the current Blazor version. What helped me to solve the problem was the following solution given by MarcoTheFirst: https://github.com/dotnet/aspnetcore/issues/13601

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.

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.

Follow the link for code samples.

like image 1
Allie Avatar answered Oct 11 '22 22:10

Allie



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!