Kestrel Server登录重定向转到HTTP而不是HTTPS

时间:2017-07-14 17:15:21

标签: c# asp.net-core asp.net-core-mvc kestrel-http-server

我的MVC网站遇到了问题。我们使用Shibboleth和Kerberos根据用户的网络ID处理自动登录。我们配置Shibboleth以便它保护整个网站,减去一些匿名允许的页面以显示错误或访问被拒绝的消息。

如果您尝试导航到网站的根目录,系统会很有效。它转发给Shibboleth进行身份验证,然后使用包含用户ID的Headers转发回我的站点,然后我们根据用户和权限的数据库列表进行授权。

如果第一次导航到/ Home以外的任何页面,则会出现问题。重定向步骤如下。请注意,这是一个内部网站,我更改了域名,因此请勿尝试以下任何实际链接。他们不会工作。

更新:Kestrel Web服务器是将我们传递给HTTP的服务器,而前面的Apache负载均衡器猛扑进去并说“不允许http,请转到https”。

1)用户导航到https://ets.com/ets/Employees,并由于Shibboleth的站点范围保护而收到302。这指向我们的IDP服务器的SSOService.php?SAMLResponse = giantquerystringhere根据登录到计算机的用户ID执行自动登录。

2)Shibboleth验证然后转发到https://ets.com/Shibboleth.sso/SAML2/POST的POST事件,该事件用标题填充标题。

3)POST事件完成并按预期将用户转发到https://ets.com/ets/Employees

4)但我们需要在应用程序本身处理登录,因此Kestrel服务器调用302并将用户转发到http://ets.com/ets/Home/Index?ReturnUrl=%2Fets%2FEmployees INSTEAD的https

5)Apache LB接受了这一点,并且由于不允许HTTP,它调用301(永久移动)并转发到https://ets.com/ets/Home/Index?ReturnUrl=%252Fets%252FEmployees(注意双编码查询字符串)

6)该URL响应302,处理返回URL,并将用户转发到https://ets.com/ets/Home/%2Fets%2FEmployees的错误网址,而不是https://ets.com/ets/Employees

有没有办法阻止它首先转发到http?我已经尝试了一些我在搜索中看到的“强制HTTPS”方法,但是大多数这些方法都需要改变才能打破Shibboleth集成,或者只是不起作用。 (强制依赖ASP身份,URL重写等)

我的问题是,为什么Kestrel / MVC转发到HTTP呢?配置错误了吗?有没有选项可以防止这种情况发生?我无法更改我的登录机制,但我可以更改处理Shibboleth响应的方式并处理应用程序端的登录。

我会尝试包含相关代码。如果有任何其他信息可以提供帮助,请告诉我。

Startup.cs

public class Startup
{
    public static IConfigurationRoot Configuration { get; private set; }

    public Startup(IHostingEnvironment env) {
        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);

        builder.AddEnvironmentVariables();
        Configuration = builder.Build();
    }

   public void ConfigureServices(IServiceCollection services) {
        services.AddMvc(options => {
            // Only allow authenticated users.
            var defaultPolicy = new AuthorizationPolicyBuilder()
                .RequireAuthenticatedUser()
                .Build();

            options.Filters.Add(new AuthorizeFilter(defaultPolicy));
        });

        // Authorization policies.
        services.AddAuthorization(options => {
            options.AddPolicy("EditPolicy", policy => {
                policy.RequireClaim("readwrite", "true");
            });

            options.AddPolicy("AdminPolicy", policy => {
                policy.RequireClaim("admin", "true");
            });

            options.AddPolicy("SuperAdminPolicy", policy => {
                policy.RequireClaim("superadmin", "true");
            });
        });

        services.AddDbContext<LoanDbContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("ETS_Web")));
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, LoanDbContext context) {
        loggerFactory.AddConsole(Configuration.GetSection("Logging"));
        loggerFactory.AddDebug();

        app.UseStatusCodePagesWithReExecute("/Home/Error/{0}");
        if (env.IsDevelopment()) {
            app.UseDeveloperExceptionPage();
            app.UseBrowserLink();
        } else {
            app.UseExceptionHandler("/Home/Error");
        }
        app.UseExceptionLogger(loggerFactory);      // Custom middleware to log exceptions. Rethrows the exception for Exception Handler/Page

        // Forces the website to use en-US locale, no matter what the browser settings are.
        // This is done to enforce date format of mm/dd/yyyy (Canada uses dd/mm/yyyy)
        var supportedCultures = new[] {
            new CultureInfo("en-US")
        };

        app.UseRequestLocalization(new RequestLocalizationOptions {
            DefaultRequestCulture = new RequestCulture("en-US"),
            SupportedCultures = supportedCultures,      // Formatting numbers, dates, etc.
            SupportedUICultures = supportedCultures     // UI strings that may be localized.
        });

        app.UseStaticFiles();   // Allows serving pages that don't belong to a controller.

        app.UseCookieAuthentication(new CookieAuthenticationOptions() {
            CookieName = $"ETS.Web.{env.EnvironmentName.ToLower()}",
            AuthenticationScheme = "ETSCookieAuth",
            LoginPath = new PathString("/Home/Index"),
            AccessDeniedPath = new PathString("/Home/AccessDenied"),
            AutomaticAuthenticate = true,
            AutomaticChallenge = true,
            ExpireTimeSpan = TimeSpan.FromHours(2),
            SlidingExpiration = true
        });

        app.UseMvc(routes => {
            routes.MapRoute(name: "default", template: "{controller=Home}/{action=Index}/{id?}");
            routes.MapRoute(name: "loan", template: "{controller=LoanRequests}/{action=Index}/{id?}");
            routes.MapRoute(name: "budget", template: "{controller=Budgets}/{action=Index}/{id?}");
            routes.MapRoute(name: "company", template: "{controller=Companies}/{action=Index}/{id?}");
            routes.MapRoute(name: "employee", template: "{controller=Employees}/{action=Index}/{id?}");
            routes.MapRoute(name: "product", template: "{controller=Products}/{action=Index}/{productID?}");
            routes.MapRoute(name: "inventory", template: "{controller=Inventory}/{action=Index}/{productID?}");
            routes.MapRoute(name: "salesarea", template: "{controller=SalesAreas}/{action=Index}/{id?}");
            routes.MapRoute(name: "reports", template: "{controller=Reports}/{action=Index}/{id?}");
        });
    }

HomeController.cs

public class HomeController : Controller
{
    private const string SHIB_HEAD_IDP = "IDP_uid";
    private readonly LoanDbContext _context;

    public HomeController(LoanDbContext context) {
        _context = context;
    }

    [AllowAnonymous]
    public async Task<IActionResult> Index(string returnUrl = null) {
        string ldapID = "";

        if (HttpContext.Request.Headers.ContainsKey(SHIB_HEAD_IDP)) {  // In a hosted environment, the Shibboleth IDP will automatically log the user in.
            ldapID = HttpContext.Request.Headers[SHIB_HEAD_IDP].ToString();
        } else if (HttpContext.Request.Host.ToString().Contains("localhost")) {  // For a Development machine, there is no Shibboleth to read Headers from.
            ldapID = "FallbackIdNumber";
        }

        if (!string.IsNullOrWhiteSpace(ldapID)) {
            Employee loginUser = await _context.Employees.SingleOrDefaultAsync(m => m.LdapID == ldapID);
            if (loginUser?.AccessLevel == AccessType.Disabled)
                return RedirectToAction("AccessDenied");   // If the user isn't in the DB, or is Disabled, they are not authorized to access ETS.

            List<Claim> claims = new List<Claim>
            {
                new Claim("id", loginUser.LdapID),
                new Claim("name", loginUser.FullName),
                new Claim("email", loginUser.Email),
                new Claim("role", loginUser.AccessLevel.ToString()),
                new Claim("department", loginUser.Department),
                new Claim("readonly", (loginUser.AccessLevel >= AccessType.ReadOnly).ToString()),
                new Claim("readwrite", (loginUser.AccessLevel >= AccessType.ReadWrite).ToString()),
                new Claim("admin", (loginUser.AccessLevel >= AccessType.Admin).ToString()),
                new Claim("superadmin", (loginUser.AccessLevel >= AccessType.SuperAdmin).ToString())
            };

            // Update the Employee's Last Login time
            loginUser.LastLogin = DateTime.UtcNow;
            await _context.SaveChangesAsync();

            // Create a cookie to hold the user's login details
            var id = new ClaimsIdentity(claims, "local", "name", "role");
            await HttpContext.Authentication.SignInAsync("ETSCookieAuth", new ClaimsPrincipal(id));
            if (string.IsNullOrWhiteSpace(returnUrl))
                return RedirectToAction("Login");
            else {
                // May require multiple Decodes to change characters like "%252F" to "%2F" to "/" This code seems not to help.
                if (returnUrl.Contains("%")) {
                    returnUrl = System.Net.WebUtility.UrlDecode(returnUrl);
                    if (returnUrl.Contains("%"))
                        returnUrl = System.Net.WebUtility.UrlDecode(returnUrl);
                }
                return Redirect(returnUrl);
            }
        }

        return RedirectToAction("AccessDenied");
    }

    [HttpGet]
    public IActionResult Login(string returnUrl = null) {
        ViewData["ReturnUrl"] = System.Net.WebUtility.UrlDecode(returnUrl);
        return View();
    }

    [AllowAnonymous]
    public async Task<IActionResult> Logout() {
        await HttpContext.Authentication.SignOutAsync("ETSCookieAuth");
        return View();
    }

    [AllowAnonymous]
    public IActionResult Error(int id = 0) {
        string pageTitle = "An Unhandled Exception Occurred";
        string userMessage = "";
        string returnUrl = "/Home";
        string routeWhereExceptionOccurred;

        try {
            if (id > 0) {
                userMessage = GetErrorTextFromCode(id);
                pageTitle = $"An HTTP Error {id} has occurred; {userMessage}";
            }

            // Get the details of the exception that occurred
            var exceptionFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();

            if (exceptionFeature != null) {
                routeWhereExceptionOccurred = exceptionFeature.Path;
                Exception ex = exceptionFeature.Error;

                if (ex is ArgumentException argEx) {
                    userMessage = argEx.Message;
                } else if (ex is InvalidOperationException ioEx) {
                    userMessage = "An error occurred while trying to fetch a single item from the database. Either none, or more than one were found. "
                        + "This may require manual database changes to fix.";
                } else if (ex is System.Data.SqlClient.SqlException sqlEx) {
                    userMessage = $"A SQL database error occurred. Error Number {sqlEx.Number}";
                } else if (ex is NullReferenceException nullEx) {
                    userMessage = $"A Null Reference error occurred. Source: {nullEx.Source}.";
                } else if (ex is DbUpdateConcurrencyException dbEx) {
                    userMessage = "An error occurred while trying to update your item in the database. This is usually due to someone else modifying the item since you loaded it. Please try again.";
                } else {
                    userMessage = "An unhandled exception has occurred.";
                }
            }
        } catch (Exception) {
            pageTitle = "Your Error Cannot Be Displayed";
            userMessage = "An unknown error occurred while trying to process your error. The original error has been logged, but please report this one as it has not been logged.";
            // TODO: Log the exception
        } finally {
            // Set up the Error page by passing data into the ViewBag/ViewData
            ViewBag.Title = pageTitle;
            ViewBag.UserMessage = userMessage;
            ViewBag.ReturnURL = returnUrl;
        }
        return View();
    }

    [AllowAnonymous]
    public IActionResult AccessDenied() {
        return View();
    }

1 个答案:

答案 0 :(得分:0)

在aspnet核心中,我设法通过以下代码解决了这个问题。

app.Use(async (ctx, next) =>
        {
            ctx.Request.Scheme = "https";
            await next();
        });

        ForwardedHeadersOptions forwardOptions = new 
  ForwardedHeadersOptions
        {
            ForwardedHeaders = ForwardedHeaders.XForwardedFor | 
   ForwardedHeaders.XForwardedProto,
            RequireHeaderSymmetry = false
        };

        forwardOptions.KnownNetworks.Clear();
        forwardOptions.KnownProxies.Clear();

        app.UseForwardedHeaders(forwardOptions);

设置: 我的 Web 应用程序位于 traefik 反向代理后面,它使用 letencrypt 证书处理 https 请求。对我托管在 kestrel 中的网络应用程序的调用是 https。

当用户尝试调用带有 Authorize 属性修饰的 Action 时,kestrel 过去常常重定向到身份服务器以使用 http 方案登录,这导致了问题,因为 google/facebook 在生产中不再支持 http。

我将传入请求的方案更改为 https ,这应该是安全的,因为 traefik 已经处理了来自互联网的公共请求,现在只知道 http 的 kestrel 被迫从上面的代码转换为 https 方案。同样,我的身份服务器也位于处理身份验证请求的 traefik 反向代理后面。

注意:我的应用没有仅限 http 的端点。