12
votes

Problem

I could not get the automatic redirection from HTTP to HTTPS to work correctly when the app is published to App Engine.

When i access the website through example.com the site got routed to http://www.example.com and show that the connection is unsecured. When i access the website through https://www.example.com the website is then correctly secured with google-managed SSL. However the automatic redirection from HTTP to HTTPS does not occurs.

Insecured connection

I also got an error in Log Viewer warning that Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware was throwing Failed to determine the https port for redirect.

enter image description here

I have followed the documentation from MSDN and only get it to work locally, but not when the app is published to App Engine. https://docs.microsoft.com/en-us/aspnet/core/security/enforcing-ssl?view=aspnetcore-2.1&tabs=visual-studio

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory logger)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseDatabaseErrorPage();
    }
    else
    {
        app.UseStatusCodePages();
        app.UseExceptionHandler("/Error");
        app.UseHsts(); // This was added by the template
    }

    app.UseHttpsRedirection(); // This was added by the template
    app.UseStaticFiles();
    app.UseCookiePolicy();
    app.UseAuthentication();
    app.UseMvc();
}

Here is the Program.cs. Basically default from the project template

public static IWebHostBuilder CreateWebHostBuilder(string[] args)
{
    return WebHost.CreateDefaultBuilder(args)
        .CaptureStartupErrors(true)
        .UseStartup<Startup>();
}

The app.yaml used for the deployment

runtime: aspnetcore
env: flexible
automatic_scaling:
  min_num_instances: 1
  max_num_instances: 20
  cpu_utilization:
    target_utilization: 0.8
readiness_check:
  path: "/readinesscheck"
  check_interval_sec: 5
  timeout_sec: 4
  failure_threshold: 2
  success_threshold: 2
  app_start_timeout_sec: 300
liveness_check:
  path: "/livenesscheck"
  check_interval_sec: 30
  timeout_sec: 4
  failure_threshold: 2
  success_threshold: 2
skip_files:
  - node_modules/
  - wwwroot/src/vendor/
  - ^(.*/)?.*\.pdb$
  - ^(.*/)?.*\.log$

What I've tried was the following (Only one at a time)

  1. AddHttpsRedirection Middleware to the ConfigureServices method.

Which ended up with app being inaccessible (502 Server Error).

services.AddHttpsRedirection(options =>
{
    options.RedirectStatusCode = StatusCodes.Status307TemporaryRedirect;
    options.HttpsPort = 443;
});
  1. Add EnvironmentVariable to app.yaml

Also ended up with app being inaccessible (502 Server Error).

env_variables:
   ASPNETCORE_HTTPS_PORT: "443"
  1. Manually configuring the HTTPS port in Program.cs

Also ended up with app being inaccessible (502 Server Error).

WebHost.CreateDefaultBuilder(args)
    .UseSetting("https_port", "8080") // also return 502 when port is 443
  1. Configure ForwardedHeaderOptions in ConfigureServices method and Use ForwardedHeaderOptions in Configure method. https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-2.1#other-proxy-server-and-load-balancer-scenarios

App is accessible but no automatic HTTP/HTTPS redirection.

services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders = 
        ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
});

app.UseForwardedHeaders();
  1. Manually exposing port 443 and 8080 in Dockerfile.

App is accessible but no automatic HTTP/HTTPS redirection. I understand that when runtime in app.yaml is set to aspnetcore. The publish process automatically generated it's own Dockerfile which is used to deploy the app to App Engine.

EXPOSE 443
EXPOSE 8080
3

3 Answers

16
votes

Somehow I got this to work after creating my own middleware that look for "X-Forwarded-Proto" header according to this hint on Microsoft and App Engine documentation.

Microsoft: Forwarded Headers Middleware must be enabled for an app to process forwarded headers with UseForwardedHeaders.

App Engine: SSL connections are terminated at the load balancer. Traffic from the load balancer is sent to the instance over an encrypted channel, and then forwarded to the application server over HTTP. The X-Forwarded-Proto header lets you understand if the origin request was HTTP or HTTPs.

Microsoft requires the Middleware to be activated first before the app start to process the forwarded headers

So configure the middleware options in ConfigureServices method

services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders = 
        ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
});

and use it in Configure method before anything else

app.UseForwardedHeaders();

Then writing the custom middleware that reads the forwarded headers and redirect to HTTPS including queries.

In Configure method

app.Use(async (context, next) =>
{
    if (context.Request.IsHttps || context.Request.Headers["X-Forwarded-Proto"] == Uri.UriSchemeHttps)
    {
        await next();
    }
    else
    {
        string queryString = context.Request.QueryString.HasValue ? context.Request.QueryString.Value : string.Empty;
        var https = "https://" + context.Request.Host + context.Request.Path + queryString;
        context.Response.Redirect(https);
    }
});

In the end the Configure method looks like this

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    app.UseForwardedHeaders();
    app.Use(async (context, next) =>
    {
        if (context.Request.IsHttps || context.Request.Headers["X-Forwarded-Proto"] == Uri.UriSchemeHttps)
        {
            await next();
        }
        else
        {
            string queryString = context.Request.QueryString.HasValue ? context.Request.QueryString.Value : string.Empty;
            var https = "https://" + context.Request.Host + context.Request.Path + queryString;
            context.Response.Redirect(https);
        }
    });

    if (env.IsDevelopment())
    {
        // code removed for clarity
    }
    else
    {
        // code removed for clarity
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    // code removed for clarity
    app.UseMvc();
}

Now navigating to example.com redirect me directly https://www.example.com

1
votes

This is what worked for me (the code is located in Startup.cs):

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<ForwardedHeadersOptions>(options =>
    {
        options.ForwardedHeaders = 
            ForwardedHeaders.XForwardedFor | 
            ForwardedHeaders.XForwardedProto;

        options.KnownNetworks.Clear();
        options.KnownProxies.Clear();
    });

    services.AddHttpsRedirection(opt => opt.HttpsPort = 443);

    // code removed for clarity
}


public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        // code removed for clarity
    }
    else
    {
        // code removed for clarity
        app.UseHsts();
    }

    app.UseForwardedHeaders();
    app.UseHttpsRedirection();
    
    // code removed for clarity
}
        
0
votes

The scheme can be manually set in Startup.Configure before using any type of middleware:

app.Use((context, next) =>
{
    context.Request.Scheme = "https";
    return next();
});