21

Hopefully I'm just missing something really simple/obvious - why, and more importantly, how do you maintain (or force) the protocol during the redirect to Login?

To illustrate:

request trace

  • the original protocol is https
  • one would think this should be the "default" for something like login, but as shown, the redirect (seems) doesn't maintain it.

Stuff I tried:

  • There is a RequireHttps attribute that one could use, but:

    1. seems "weird" that it would take 2 redirects to get "there"
    2. in situations where you have a load balancer and/or have SSL "offloaded" elsewhere (not in server), then this would then be a redirect loop (SSL is between client and front-end net/ssl lb, and http to your box(es)/application). This is actually my production case...
  • I have already set IIS URL re-write as well (aka canonical rule to https for entire site), and that seems "ignored" (too) (rule does not check for "https" otherwise it suffers same redirect loop).

  • tried and failed to set absolute URL in LoginPath (in CookieAuthenticationOptions)..because you can't do that...

Thanks for advice or pointers...


Update

As to the "why"?

  1. in situations where you have a load balancer and/or have SSL "offloaded" elsewhere (not in server), then this would then be a redirect loop (SSL is between client and front-end net/ssl lb, and http to your box(es)/application). This is actually my production case..

Further tinkering got me to the above, as shown in this (localhost - my local dev box, not server) request sequence (the above issue manifests in a production load balanced environment where SSL processing is "up the stack" - e.g. ARR):

localhost https

  • the protocol is in fact maintained
  • the issue seems exactly related to the situation where the application and the "infrastructure" don't "match". It seems similar to the situation where in code, you would do a Request.IsSecureConnection in a "load balanced"/"web farm" environment (say ARR where the cert is in your ARR, not in your host/s). That check will always return false in such a situation..

So the question really is on guidance on how to get around this?


Update 2

Many thanks to Richard for changing my "direction" in trying to resolve this. I originally was looking for a way to:

  • set/tell OWIN/Identity to use a secure URL (explicitly) and "override" the way it evaluates LoginPath. The Secure (only) option in handling cookies somehow led me that way (if I can explicitly say cookies in HTTPS only, then it sort of gave me an impression of being able to do so for LoginPath..one way or the other)

  • a "hacky" way in my mind was to just deal with it client side (Javascript).

In the end, Richard's answer took me to URL Rewriting (though still not on the LB side because that's beyond my control). I'm currently working off of (based on my environment):

<rule name="Redirect to HTTPS" stopProcessing="true">
    <match url=".*" />

    <conditions>
      <add input="{HTTP_CLUSTER_HTTPS}" pattern="^on$" negate="true" />
      <add input="{HTTP_CLUSTER_HTTPS}" pattern=".+" negate="true" />

    </conditions>
    <action type="Redirect" url="https://{HTTP_HOST}{SCRIPT_NAME}/{REQUEST_URI}" redirectType="SeeOther" />
</rule>

and see some light at the end of the tunnel.


Update 3

Awesome thanks again to Richard for the sleuthing! Latest answer got me sleuthing too and it turns out there's quite a few posts here on SO related to CookieApplyRedirectContext...so now this what I have in place (which is specific to my case), and is what I was originally going after:

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
   AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
   LoginPath = new PathString("/Account/Login"),

   //This is why. If I could explicitly set this, then I (thought) I should
   //be able to explicitly enforce https (too..as a setting)
   //for the LoginPath...
   CookieSecure = CookieSecureOption.Always,

   Provider = new CookieAuthenticationProvider 
   {
      OnValidateIdentity = .....
      ,
      OnApplyRedirect = context =>
      {
         Uri absoluteUri;
          if (Uri.TryCreate(context.RedirectUri, UriKind.Absolute, out absoluteUri))
          {
             var path = PathString.FromUriComponent(absoluteUri);
             if (path == context.OwinContext.Request.PathBase + context.Options.LoginPath)
             {
                context.RedirectUri = context.RedirectUri.Replace("http:", "https:");
             }
           }
          context.Response.Redirect(context.RedirectUri);
        }
     }
});
EdSF
  • 11,753
  • 6
  • 42
  • 83
  • I don't understand what your problem is. Do you mean that when the user clicks on a link, like "Login" it should go directly to an https page and not an http page that is then redirected to https? – Erik Funkenbusch Jun 03 '15 at 09:05
  • I'm also confused by your #2, if you're not using SSL on the host, and only on the front end, how are SSL requests getting through to the host in the first place? – Erik Funkenbusch Jun 03 '15 at 09:09
  • What's your login controller looks like? – trailmax Jun 03 '15 at 13:09
  • @ErikFunkenbusch when a user requests a resource that requires authentication, and the user isn't authenticated, there will be a `redirect` to your `'login` process. I have updated the question after more tinkering. – EdSF Jun 03 '15 at 14:34
  • @ErikFunkenbusch in a load balanced environment, you have the option to "offload" SSL processing "up in the stack" (e.g. ARR). So ARR and the client (end user browser) is under SSL/HTTPS while ARR to your "web farm" (of X hosts) will be HTTP. So the _application_ (correctly) thinks its an `http` connection. You then also don't have to install certs for _all_ your hosts (which could expand/contract)...Hope this makes sense...Thnx! – EdSF Jun 03 '15 at 14:38
  • @trailmax I wouldn't think its related - I've updated the question with more tinkering/detail....thanks! – EdSF Jun 03 '15 at 14:42
  • @EdSF - Yes, i'm aware of how this works. Which is my point. If your app is getting only http requests, how exactly are you seeing https requests in your debugger? – Erik Funkenbusch Jun 03 '15 at 16:35
  • @EdSF - if your application only sees http, then you cannot have the application enforce https. That's the job of your load balancer. However, you might have bigger problem in that your load balance will have to convert all object requests within the page to https as well (such as images... if it doesn't do that, then you have to do this yourself. – Erik Funkenbusch Jun 03 '15 at 16:39
  • 1
    Awesome post and code snippet! – Spence Jun 28 '15 at 22:04

2 Answers2

14

This problem is occurring because your application is issuing a redirect to an absolute URL. You can fix this in one of two ways, in the load balancer or in the application itself.

Load Balancer

Configure your load balancer to rewrite redirect responses from http to https. If you were using ARR, the following rule (taken from here) should work:

<rule name="forum-redirect" preCondition="IsRedirection" enabled="true">
  <match serverVariable="RESPONSE_LOCATION" pattern="^http://[^/]+/(.*)" />
  <conditions>
    <add input="{ORIGINAL_HOST}" pattern=".+" />
  </conditions>
  <action type="Rewrite" value="http://{ORIGINAL_HOST}/{R:1}" />
</rule>

Other load balancers will require similar configuration.

Application

We can replace the URL that OWIN redirects to in the authorization process with a relative URL, which means the protocol will stay as whatever the browser was previously using.

It took a bit of digging in the Owin source to find how to do this, but the following change to your Application startup should solve your problems. First, extract the CookieAuthenticationProvider initialisation from your startup config.

Change:

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
    LoginPath = new PathString("/Account/Login"),
    Provider = new CookieAuthenticationProvider 
    {
        // Move these options in the step below...
    }
});

To:

var cookieProvider = new CookieAuthenticationProvider
{ 
    // ... Options from your existing application
};
// Modify redirect behaviour to convert login URL to relative
var applyRedirect = cookieProvider.OnApplyRedirect;
cookieProvider.OnApplyRedirect = context =>
{
    if (context.RedirectUri.StartsWith("http://" + context.Request.Host))
    {
        context.RedirectUri = context.RedirectUri.Substring(
            context.RedirectUri.IndexOf('/', "http://".Length));
    }
    applyRedirect(context);
};

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
    LoginPath = new PathString("/Account/Login"),
    Provider = cookieProvider
});

While we can't get at where the redirection rule is set easily, OWIN uses a delegate to perform the actual redirect. What I've done here is stored that delegate, modified the URL it is about to be given, and then called it again.

With this option, ensure that any other redirects and links within your site are relative.

Richard
  • 29,854
  • 11
  • 77
  • 120
  • Agreed. Though I don't think it's anything's "fault" - the app is doing what it does and the LB does too - it manages the SSL between it and public clients. The issue crops up between LB and host/s because the hosts don't have certs (one of the "pros" of offloading SSL to a dedicated "device"). Yes too on IIS, e.g. a `{HTTPS}` rule trying to redirect http -> https == redirect loop. – EdSF Jun 03 '15 at 17:58
  • 2
    It *is* the LB's fault - if it's encapsulating your http traffic into https, then it should be recognising a redirect to a http version of a URL it is authoritative and swap that for a https link. If it's ARR you're using, then you need [Outbound rewrite rules](http://www.iis.net/learn/extensions/url-rewrite-module/creating-outbound-rules-for-url-rewrite-module). I've added a rule which might do the job. – Richard Jun 03 '15 at 18:03
  • Ahh, got it! I will have to talk to my provider (ARR is just my example, I don't know what they actually have) - THANK YOU! – EdSF Jun 03 '15 at 18:29
  • I was _originally_ on the _"how do I tell OWIN/Identity to take a setting / `LoginPath` that says `Secure`"_ (or similar) track..an "explicit" setting...That or even the "hacky" way (and if my provider can't help) just deal with it client-side (javascript).... – EdSF Jun 03 '15 at 18:49
  • Your comment got me thinking, and I dug around and found you an OWIN approach which won't require any changes to your load balancer. – Richard Jun 04 '15 at 09:39
  • Man, if there was a way to mark your answer **twice** I would. Based on what you have, updated my post with what I ended up with. THANK YOU! – EdSF Jun 05 '15 at 04:39
  • I was initially going to go with the solution you ended up with. However, the logic in the default redirect is [more complex than just issuing a redirect](http://katanaproject.codeplex.com/SourceControl/latest#src/Microsoft.Owin.Security.Cookies/Provider/DefaultBehavior.cs) - there is logic to handle ajax requests too. Removing the protocol from the URL and allowing the original logic to fire seemed easier. – Richard Jun 05 '15 at 06:59
1

This is a slight modification of the 'Application' option in Richard's answer. Some of the string manipulation there is delegated to the Uri class.

var cookieProvider = new CookieAuthenticationProvider
{ 
    // ... Options from your existing application
};
// Modify redirect behaviour to convert login URL to relative
var applyRedirect = cookieProvider.OnApplyRedirect;
cookieProvider.OnApplyRedirect = context =>
{
    var redirectUri = new Uri(context.RedirectUri, UriKind.Absolute);

    if (redirectUri.Scheme == "http" && redirectUri.Host == context.Request.Uri.Host)
    {
        context.RedirectUri = redirectUri.PathAndQuery;
    }

    applyRedirect(context);
};

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
    LoginPath = new PathString("/Account/Login"),
    Provider = cookieProvider
});
Community
  • 1
  • 1
Scott Munro
  • 13,369
  • 3
  • 74
  • 80