← All Posts

SameSite, Sessions, and Single Sign-On

Posted by

  tech web

12 min read | 3533 words | 992 views | 0 comments

Web developers in the past year may have been caught off-guard by the effects of Google's new stance on SameSite cookies, or rather, cookies without an explicit SameSite attribute. Or, perhaps a website you use regularly has been acting erratically of late. For those unfamiliar, Google has a good introductory overview of what SameSite cookies are all about, but we'll rehash the basics here.

Cookies, of course, are key-value pairs stored by the client, which are sent as part of the request headers when requesting a webpage. Originally, cookies were used to allow shoppers in the 1990s to save items to their shopping cart. This is rarely done anymore - instead, the server keeps track of your cart for you, but cookies are more important than ever before - session cookies allow you to stay logged into websites. Because HTTP is stateless, unlike other protocols which maintain state, each request a client makes to a server is fresh, with the server having no knowledge of previous requests or who might be making this new request. A session cookie allows the server to send the client a random string which uniquely identifies it. The browser sends the server this cookie on each subsequent request, thereby allow the server to know who's logged in and serve the webpage accordingly.

While sessions are a great use of cookies, most people are no doubt familiar with the various other uses of cookies today - some of which are more or less legitimate than others. Thanks to GDPR, you've probably seen various websites inform you that they use cookies, and you've numbly clicked the "Accept" banner and moved on. Cookies are absolutely essential to the modern web - unless your website is entirely static, they're essentially required for websites to function. At some level, this aspect of GDPR is controversial - telling you that a website uses cookies is like telling you that you need Internet access to access the website - well, no duh, right? But there are still plenty of reasons to be concerned about cookies - many cookies are used for advertising and tracking purposes, and third-party cookies, which are not set by the website being accessed, are seen as increasingly problematic today.

This is where the SameSite attribute of cookies comes in. Compared to other attributes, like Secure (which instructs the browser to send the cookie only on HTTPS requests, not HTTP requests) or HttpOnly (which prevents the cookie from being accessed or manipulated by JavaScript), the SameSite attribute is relatively new. It allows the creator of the cookie to specify the contexts in which the browser should send that cookie to the server with a request. For all practical purposes, there are four possible values: None, Lax, Strict, or no value at all (which, confusingly enough, is not exactly the same as None).

Unlike the other attributes, the SameSite attribute is quite nuanced, as we discovered ourselves. Setting the Secure and HttpOnly on most (if not all cookies) is simply a no-brainer these days - there's very little reason not to do these easy, simple things to increase the security of your cookies. On the other hand, setting the SameSite attribute incorrectly can break your website - literally; in fact, that's why it's been such a big deal to developers this year - many applications and/or cookies have had to be updated in order to prepare for the changes that have been rolling out in browsers this year.

So, how does SameSite work? Traditionally, this attribute has not existed, and browsers have simply sent all cookies on all requests (except for Secure cookies on non-HTTPS connections and exceptions like that). The SameSite attribute further limits when cookies should be sent. The Lax setting prevents a cookie from being sent in certain types of requests: cross-site POST requests (e.g. when you POST from site A to site B, cookies for site B won't be sent along with that request), most importantly, but also image requests, AJAX requests, and in iframes. Why is this? Part of the goal of SameSite is helping developers mitigate cross-site request forgery (CSRF) attacks. CSRF occurs by malicious taking advantage of a user's cookies - a simple way to think about it would be a request like https://example.com/account?deleteaccount=true - when logged in, sending this request would delete a user's account. A malicious website could simply include an image with that URL as the source attribute, which means a user navigating to that malicious page will execute that request in the context of that website. Historically, cookies would have been sent with the request, the server would authenticate the user correctly, and the account would get deleted. Whoops.

That's a silly example, of course, and the idea of using GET (rather than POST) for such a request is ludicrous, but CSRF is an extremely real problem. The most common way of mitigating it is by using unique tokens embedded in the page to prevent the possibility of such an attack. An attacker could thus POST (or GET) the URL, but without the unique CSRF token, the request fails to achieve the intended effect, protecting the user. CSRF tokens alone may not necessarily be enough - most sites use HTTP compression to reduce bandwidth consumption, and are thus vulnerable to the BREACH attack. Although the BREACH attack is beyond the scope of this article, pages reflecting user input are vulnerable to having their CSRF tokens stolen by attackers - if this applies to your website, you should definitely be cognizant of it. But let's set the BREACH attack aside for a moment - many poorly written websites may not use CSRF tokens, and this is where SameSite comes into play - it adds an additional layer of protection to your application. There's generally little reason for cookies to be sent along with a request for an image, since everybody gets the same image when navigating to that URL. So, why not prevent cookies from being sent on image requests? That's part of what Lax accomplishes, among other things.

What about Strict. Well, as the name implies, Strict is even more, well, strict. Strict also prevents a cookie from being sent on a cross-site GET request, as well as on cross-site links. It's quite restrictive - and even Lax can be too restrictive for some websites - which is why getting this attribute right is so utterly important.

Here's a helpful table summarizing the differences:

What's all the fuss about? Until recently, the SameSite attribute has not seen wide adoption. The effect of this is that all cookies are generally sent on most requests. With the new browser changes, cookies without an explicit SameSite attribute are treated as if they had the Lax setting, rather than the None setting (which, in theory, allows the historically open "anything goes" behavior). This can be problematic for more complex websites. The primary problem that developers and users have faced is single sign-on, which is used by many websites, including some of our own.

Single sign-on, of course, is when a website uses another website as its identity provider to authenticate. At a high level, it works like this. You navigate to website B. Website B sees that you're not logged in, and either automatically or optionally redirects to a page on website A to request authentication. You're logged into website A, which is the identity provider. Website A knows who you are, so it creates a unique token associated with your user account and sends that back to website B. Website B reverse-verifies the token (to make sure it's legitimate), and optionally gets details about the user upon doing so. Then, website B sets it own session based on that information, and voila, you're logged in to website B, without having to re-authenticate. Convenient, eh?

What's not so convenient is when you're redirected to website A and then redirected back to website B, but then you're prompted for your credentials again. Huh, you think. You provide your credentials and you're logged into the site. And then it happens again - and again. This is exactly what happened when browsers rolled out their SameSite=Lax by default change earlier this year. Single sign-on on many websites, particularly corporate applications, outright broke. Now, why was that?

Let's pick apart the single sign-on process, this time technically, and it will be easy to spot why:

  1. Website B detects the user is not logged in, because no session is active for the user.
  2. Website B redirects to its login page.
  3. Website B attempts to automatically POST to website A to take advantage of single sign-on.
  4. Website A receives the POST request for authentication. Website A checks who's logged in and creates a unique token associated with that user ID.
  5. Website A sends the unique token back to website B in a POST request redirect.
  6. Website B sends another request to website A with the token to verify its authenticity and potentially obtain user details associated with the user ID associated with the token.
  7. After verifying the token, website B sets its own session and sends a cookie back to the client.
  8. The client sends the cookie to website B on all requests, allowing it to be "logged in" to website B.

(A brief technical aside - the POST requests mentioned above are typically redirects. This is why if JavaScript is disabled in your browser, you might see a message or two saying "click here to login". Clicking the button initiates the POST request. If JavaScript is enabled, JavaScript will automatically click the button for you and initiate the POST request, resulting in a seamless single sign-on experience with no user intervention required.)

Do you spot the problem in the steps above? The problem, in fact, occurs relatively early on in the process. The issue with the process described above is that website B POSTs to website A. Recall that cross-site POST requests are not allowed by SameSite=Lax (or by SameSite=Strict, of course). Ordinarily, when website B POSTs to website A, the client sends it cookies for website A to website A - that's how website A knows who's logged in. The problem occurs when browsers begin treating all cookies without a SameSite attribute as SameSite=Lax automatically. Now, the browser won't send its cookies for website A along with that POST request. Now, website A doesn't know who's logged in, because it didn't get any cookies for the request. As a result, it throws up its hands and redirects back to website B, saying "Sorry, Joe, I don't know who this is." Website B throws up its hands as well and displays a login prompt, forcing the user to authenticate again. At best, this is annoying. At worst, your website isn't even set up to allow manual login, and now nobody can access your website at all, because requests to the identity provider don't work and all come back without authorization. So when I say this can literally break your website, this is what I mean. It's a serious problem.

The conventional wisdom espoused and encouraged in developer resources is setting SameSite=None to prevent this. In fact, it's the most common thing you'll hear, even from major companies like Google or Microsoft:

There are a number of problems with this approach, some of which are discussed in the more technical discussions regarding mitigations. A relatively trivial one is that most browsers required the Secure attribute in order for SameSite=None to be used - otherwise, they'll just ignore the cookie. In our opinion, this is relatively trivial, because developers should be using Secure (and HttpOnly) for all of their cookies anyways, unless they have a really, really, really good reason not to do so. The more legitimate problem here is that not all browsers treat SameSite=None equally. Thing is, when the SameSite spec was initially released, it specified that if Lax or Strict was not the attribute value, the cookie should be ignored - ignored, not treated as "normal"! That's a huge problem, because although the spec has since been updated, there's a whole host of browsers out there that will simply discard cookies with SameSite=None. This puts developers in a precarious situation: set SameSite=None, and your cookie will not even be set in some browsers in the first place. Do nothing, and most newer browsers will interpret it as SameSite=Lax by de facto, and your single sign-on will break anyways.

There are two mitigations for dealing with this, and they both suck. The first is using user-agent sniffing. The good news is that it's relatively well-known what clients are incompatible with SameSite=None, and you can inspect the user agent when a client makes a request to the server. Based on the user agent, you can serve the cookie appropriately, omitting SameSite=None if the client is incompatible and including it otherwise. Since these are older user agents anyways, they're not going to default the lack of a SameSite attribute to Lax, so you don't need to worry about that.

The problem with this should be obvious. User agents can be spoofed, of course, though one could reasonably say that if an incompatible client is spoofing its user agent intentionally and gets the wrong cookie, that's its own fault, anyways. The more practical issue is that sniffing user agents is hard to do right. Google has an entire page dedicated to the topic, including some sample code that could be used to detect the incompatible clients. One issue is that parsing user agents is hard, and there's no guarantee that you have the entire list. Secondly, some incompatible clients don't display their true agent, and to make matters worse, they'll sometimes use the same user agent as a user agent that actually is not incompatible. In other words, there's no easy way for you to actually tell who's incompatible and who's not. It's a complete mess.

The second mitigation that could be used doesn't really suck much less - it involves duplicating all of your cookies, sending one cookie with SameSite=None and sending one without. Browsers will only use the cookie that applies to them, and then you don't need to worry about parsing user agents correctly. The obvious problem here is this is annoying at best - why send two cookies for every one when no browser is going to use both of them? It's inefficient and it wastes bandwidth. And even if bandwidth isn't limited, the size of HTTP response headers are - Apache, for instance, has a maximum of 8KB total for all HTTP headers by default. These limits can be adjusted somewhat, but at some point, you hit the ceiling of the size of your header response, and the reality is that many websites today set so many cookies that they're already pushing the limits of how big their response is compared to how big it could possibly be. For these websites, they can't duplicate all their cookies, even if they wanted to. Thus, at worst, this is completely impractical.


Fortunately, you don't necessarily need to do either of these things. There's a much better way, instead. What is it? Use SameSite=Lax, explicitly.


Wait, what? Interpreting the cookie as such is what created this issue in the first place, right? Yes and no. The catch is that, with a simple tweak, the single sign-on process we described above will become compatible with SameSite=Lax. Recall that SameSite=Lax prohibits cross-site POST requests, which is why single sign-on failed in our example scenario. However, only SameSite=Strict prohibits cross-site GET requests - SameSite=Lax does not. Let's consider a slightly revised single sign-on process:

  1. Website B detects the user is not logged in, because no session is active for the user.
  2. Website B redirects to its login page.
  3. Website B attempts to automatically GET to website A to take advantage of single sign-on.
  4. Website A receives the GET request for authentication. Website A checks who's logged in and creates a unique token associated with that user ID.
  5. Website A sends the unique token back to website B in a POST request redirect.
  6. Website B sends another request to website A with the token to verify its authenticity and potentially obtain user details associated with the user ID associated with the token.
  7. After verifying the token, website B sets its own session and sends a cookie back to the client.
  8. The client sends the cookie to website B on all requests, allowing it to be "logged in" to website B.

Only two words have changed: in steps 3 and 4, we are now using GET requests instead of POST requests - and this makes all the difference. Single sign-on works perfectly as before now. Why is this?

In the scenario above, website B isn't sending website A any sensitive data in that initial request; it's simply requesting authentication. The URL endpoint on website A might be as simple as https://example.com/auth/sso/WebsiteB - website B doesn't have anything sensitive to send, so who cares, honestly, if we use a GET request instead of a POST request? We're not sending a ton of information, and a GET request works perfectly fine.

You might say, wait a minute, what about the POST request sent from website A to website B? Good question. It's true that this will result in no cookies being sent on the request back to website B. Guess what, it doesn't matter! The whole point was that there was no session cookie set on website B to begin with - that's why we requested authentication from website A to begin with. All website B needs from website A is the secret token, and nothing about SameSite=Lax prevents website B from receiving it. From there, the process continues on its way, the session cookie gets set locally on website B, and life is good.

Effectively, this means you can have your cake and eat it, too! No need to settle for the more insecure SameSite=None, no need to worry about breaking any browsers with the change, and no need to set duplicate cookies. It's the best of all worlds, everything considered.

We'll add the caveat here that this may not work for all websites. If the initial POST request contains a whole host of information, which can't feasibly be displayed in the URL (since it will be a GET request instead of a POST request), you may be out of luck, and you will have to use SameSite=None. Additionally, SameSite=Strict will never work for single sign-on - it is literally impossible. However, SameSite=Strict cookies can be great candidates for session cookies that only need to be accessed on a single website that doesn't do any kind of external federation or authentication.

Although this doesn't sound like a particularly profound breakthrough, this really does bear mentioning. The general consensus today is that single sign-on requires SameSite=None. This simply is not true! Single sign-on can coexist with SameSite=Lax. Now, it would be naïve to say that this is always possible - that, of course, depends entirely on your website, your cookies, your single sign-on process, and any related factors. However, SameSite=None is not inevitable. There is another way. The proof is in this website. We grappled with this issue for a few weeks ourselves and spent an extensive amount of time testing. We could've decided to use SameSite=None, but we choice a better, more compatible, and more secure way - a higher way. We made a few slight changes to the way our single sign-on process works, and we're happy to say that we have no issues SameSite=Lax. In fact, all the cookies we set are now Lax (and in some cases, Strict) cookies, helping to bring more secure first-party cookies to all of our users.

Don't buy into the poor and incomplete advice that you need to use insecure cookies to not break your single sign-on process. If you or your developers have the capability, consider making slight changes to your single sign-on process to make it compatible with the more secure cookie settings. Not only does it signal to your users that you've made a commitment to keeping their cookies secure by designating them as restricted access, but you'll avoid the endless issues associated with trying to get your site working properly with wide open SameSite=None cookies. Some websites may not have the luxury of doing so, but web developers should take advantage of the current situation by securing their cookies, not just trying to find a band-aid fix for the problem at hand. Today, the world is asking for more secure cookies - in many cases, demanding them - and web developers have a responsibility to oblige, and by transitioning your cookies to SameSite=Lax, you'll save yourself - and your users - major headaches to come.

← All Posts


Comments

Log in to leave a comment!