Search code examples
asp.net-corenginxasp.net-identity

.Net Core Identity External Login not working


I have a website that has external logins set up for google. I have been successful in getting this to work in development, but when I moved to production it failed to work. When I click the button, instead of sending me to Google's account.google.com page, it just sends me to /Identity/Account/ExternalLogins with an error code of HTTP ERROR 400. The button is the same between production and environment. Other than the email sending portion, everything is the same as it was generated during scaffolding. My production environment uses Ubuntu and Nginx. What could be causing this? I have been unable to recreate the issue outside of production.

btw, this is the default button:

<form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
    <button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" 
          title="Log in using your @provider.DisplayName account">@provider.DisplayName
    </button>
</form>

My button is the default google log-in button altered, but it works in development, so I don't think that it matters:

<form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
    <div class="g-signin2" style="border:inherit;" data-onsuccess="onSuccess" data-gapiscan="true" data-onload="true">
        <div style="height:36px;width:120px;" class="abcRioButton abcRioButtonLightBlue">
            <button class="abcRioButtonContentWrapper" style="border:inherit; background-color:white !important"
                    type="submit" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">
                <img class="position-absolute" src="https://assets.stickpng.com/images/5847f9cbcef1014c0b5e48c8.png" style="left: 12px;height: 18px;bottom: 10px;">
                <span style="font-size:13px;line-height:34px;" class="abcRioButtonContents">
                    <span class="ml-3">Sign in</span>
                </span>
            </button>
        </div>
    </div>
</form>

Here are the relevant parts of my Startup.cs with some stuff removed from ConfigureServices() for brevity:

public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseMySql(Configuration.GetConnectionString("UserDB"), MySqlOptions => MySqlOptions
                .ServerVersion(new Version(8, 0, 22), ServerType.MySql)));

            services.AddIdentity<ApplicationUser, ApplicationRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultUI()
                .AddDefaultTokenProviders();
            services.Configure<IdentityOptions>(options =>
            {
                options.SignIn.RequireConfirmedAccount = true;
                options.User.RequireUniqueEmail = true;
                options.User.AllowedUserNameCharacters =
                        "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._";
                options.Password.RequireNonAlphanumeric = true;
                options.Password.RequireDigit = true;
                options.Password.RequireLowercase = true;
                options.Password.RequireUppercase = true;
                options.Password.RequiredLength = 8;
            });


            services.AddAuthentication()
                .AddGoogle(options =>
            {
                IConfigurationSection googleAuthNSection =
                    Configuration.GetSection("Authentication:Google");

                options.ClientId = googleAuthNSection["ClientId"];
                options.ClientSecret = googleAuthNSection["ClientSecret"];
            });

            services.AddServerSideBlazor();
            services.AddScoped<AuthenticationStateProvider, RevalidatingIdentityAuthenticationStateProvider<ApplicationUser>>();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            
            ConnectionString = Configuration["ConnectionStrings:DataDB"];

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseDatabaseErrorPage();
                app.UseBrowserLink();
            }
            else
            {               
                app.UseExceptionHandler("/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }
            app.UseHeadElementServerPrerendering();

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

            app.UseRouting();

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

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

After some more investigation and setting logging to trace, I found the error of:

Connection id "0HM5SB235ONN8" bad request data: "Requests with 'Connection: Upgrade' cannot have content in the request body."

I think is is because of my sites-enabled/default file that I had to configure this way according to the official Blazor deployment docs:

        location / {
                # First attempt to serve request as file, then
                # as directory, then fall back to displaying a 404.
                proxy_pass http://localhost:5000;
                proxy_http_version 1.1;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection "upgrade";
                proxy_set_header Connection $http_connection;
                proxy_set_header Host $host;
                proxy_cache_bypass $http_upgrade;
        }

Solution

  • The issue was in my nginx file, I had incorrectly set it up for hosting. I changed it to the following and it worked:

    map $http_connection $connection_upgrade {
         "~*Upgrade" $http_connection;
        default keep-alive;
    }
    
    server {
        root /var/www/html;
    
        # Add index.php to the list if you are using PHP
        index index.html;
            server_name domain.us;
    
    
        location / {
            proxy_pass         http://localhost:5000;
            proxy_http_version 1.1;
            proxy_set_header   Upgrade $http_upgrade;
            proxy_set_header   Connection $connection_upgrade;
            proxy_set_header   Host $host;
            proxy_cache_bypass $http_upgrade;
            proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header   X-Forwarded-Proto $scheme;
        }
    }
    

    And then I changed my Startup.cs to include:

    if (string.Equals(
                Environment.GetEnvironmentVariable("ASPNETCORE_FORWARDEDHEADERS_ENABLED"),
                "true", StringComparison.OrdinalIgnoreCase))
                {
                    services.Configure<ForwardedHeadersOptions>(options =>
                    {
                        options.ForwardedHeaders = ForwardedHeaders.XForwardedFor |
                            ForwardedHeaders.XForwardedProto;
                        // Only loopback proxies are allowed by default.
                        // Clear that restriction because forwarders are enabled by explicit 
                        // configuration.
                        options.KnownNetworks.Clear();
                        options.KnownProxies.Clear();
                    });
                }
    //#################
     app.UseForwardedHeaders();
     app.UseCookiePolicy(new CookiePolicyOptions(){
        MinimumSameSitePolicy = SameSiteMode.Lax
     });
     app.UseCertificateForwarding();