The testing methodology has been updated. All previous tests were run with the IdP configured to allow the storage of sessions in client side HTML local storage (idp.storage.htmlLocalStorage=true). This has the effect of enabling SSO when using the HTTP-POST binding even when cookies are defaulted to SameSite=Lax, or even when using server side session storage. Without it, SSO will fail when initiated using the HTTP-POST binding from a cross-site SP, and the user will be always presented with the login page. 



Jira Issue

 

This work has been triggered by the Google IETF draft 'Incrementally Better Cookies' [6], which aims to default all cookies without a same-site flag to SameSite=Lax, and  requiring SameSite=None cookies to be sent over secure transport (HTTPS). This will become the default in Chrome 80, which will be released approximately February 2020.  

Introduction


The Same-site cookie atribute is a IETF draft written by Google Inc. [1][2]which, at its most strict, instructs the user-agent not to send (attach) a SameSite cookie during a cross site HTTP request. The principal aim of which is to help prevent forms of cross site request forgery that ‘ride’ ambient authority session cookies.

A cross site request is, abstractly (see appendix A for the draft algorithmic definition), one where the top level site (typically that shown in an address bar) changes during navigation.

There are three values for the same-site cookie attribute:

Note, there is an updated draft [3] which defines two new SameSite values ‘FirstPartyLax’ and ‘FirstPartyStrict’, which are used in relation to ‘First-Party sets’ (groupings of registered domains to a single controlled entity). These values are not used here, and will be discussed in follow up implementation work - although at first glance, this seems unlikely to be used in a dynamic SSO federation.

Chrome 76 and onwards contain a flag to enable the treatment of cookies without a SameSite attribute to be SameSite=Lax. This will become the default in Chrome 80 [4], which has an approximate release date of February 2020 - see the section below for an overview of browser support. The aim of this document is to test the affects of such change on the IdP. Our immediate concern is, if the session cookies used by the IdP (depending on configuration) e.g. JSESSIONID, or shib_idp_session, do not specify the SameSite attribute - and are therefore defaulted to SameSite=Lax - and are being used in a cross-site POST request from an SP to the IdP, will it break either the IdP and or single-sign-on as potentially the session cookies will not be sent.

Further work will investigate a Proof of Concept servlet filter for munging appropriate IdP session cookies in order to set their SameSite attribute to None - thus maintaining their current behaviour. This is only necessary because the Java Servlet Specification v3.0 or v4.0 does not cater for the SameSite attribute, and it can not be set through the Java Cookie API.

SameSite Browser Support


The table below shows same-site cookie attribute compatibility amongst desktop browsers (see [5] for a complete list including mobile variants). It also includes an attempt to determine current/future browser support for IETF draft 'Incrementally Better Cookies '[6]. That is, defaulting same-site unset cookies to SameSite=Lax, and requiring SameSite=None cookies to be sent over secure transport (HTTPS).

The table will be updated as and when information becomes available.

BrowserSameSite SupportCan enable SameSite Lax By DefaultCan enable SameSite None requires Secure by defaultSameSite Lax enabled in browser by defaultSameSite None requires secure enabled in browser by default
Firefox desktopv60 onwardsin advanced setting/preferences (about:config) from v69in advanced setting/preferences (about:config) from v69unknown but likely to happenunknown but likely to happe
Chrome desktopv51 onwardin experimental features (chrome://flags) from v76in experimental features (chrome://flags) from v76Stable Chrome 80 (aprox. Feb 4th 2020,https://chromiumdash.appspot.com/schedule). Although likely to be the default in Chrome Beta 79.Stable Chrome 80 (aprox. Feb 4th 2020,https://chromiumdash.appspot.com/schedule).Although likely to be the default in Chrome Beta 79.
Safari desktopv12 onward**unknownunknownunknownunknown
Opera desktopv39 onwardin experiments (opera://flags/) from 63 onwardin experiments (opera://flags/) from 63 onwardlikely to follow chrome as chromium/blink based.likely to follow chrome as chromium/blink based.
Edgev16 onwardpossible in Edge builds based on chromium 76 onward (2019). Not sure about older EdgeHTML engines. In experimental features (edge://flags)possible in Edge builds based on chromium 76 onward (2019). Not sure about older EdgeHTML engines - looks like that will NOT happen in IE or Edge <v18. In experimental features (edge://flags)likely to follow chrome as in 2019 Edge became chromium/blink based.likely to follow chrome as in 2019 Edge became chromium/blink based.

Webkit based browsers on Mac (safari) and iOS (safair, chome, firefox etc)  are currently affected by a bug that treats SameSite=None or SameSite=nonesense cookies as SameSite=Strict (https://bugs.webkit.org/show_bug.cgi?id=198181). We believe the fix for this will only take effect from MacOS 10.15 and iOS 13. Consequently, any attempt to maintain the current functional behaviour of cookies by setting SameSite=None on unpatched versions of Webkit will break SSO.

This bug also affects older Chrome and Android browsers.

To compound matters, Firefox has a bug filed (https://bugzilla.mozilla.org/show_bug.cgi?id=1465402) that suggests they may consider the ability to set even Lax cookies in the middle of SP→ IdP → IdP redirect chains to be a bug. If they decide to "fix" this non-bug, it will cause the current default behavior to stop functioning correctly at all with Firefox because the JSESSIONID cookie set upon initial contact to the IdP will be ignored, and the follow-on request will trigger a flow exception error. This would make "doing nothing" a non-option and create a hopeless situation without ugly workarounds to accomodate three different non-overlapping modes of behavior.

Testing

To test the affects of various SameSite settings, the following setup will be used:


Using Client Side Session Storage, allowing HTML local storage.

The IdP is configured here to use client side session storage with, importantly, idp.storage.htmlLocalStorage set too true. The table below shows the results of various tests using different SAML2.0 bindings or initiators. The shib_idp_session_ss is less relevant, and could use local storage if supported by the browser with similar same effect.

Of note, this does not test the affect of any SP cookies sent cross-site from the IdP.


NoTestSameSite ValueResultsBrowserIdP WorkingSSO WorkingExpected Result
1GET - Unsolicited SSO Cross SiteJSESSIONID=Strict, shib_idp_session=Lax, shib_idp_session_ss=LaxJSESSIONID Cookie is not sent cross site. The flow can not be resumed during the GET-REDIRECT-GET for the client-storage-read view-state. That is, the JSESSIONID is SET on response from the inital GET request, but is not sent cross-site on the REDIRECT GET request, hence webflow can not resume the conversationChrome 78.0.3888.0 (Official Build) canary (64-bit)NoNoYes
2

GET - Unsolicited SSO Cross Site


JSESSIONID=Lax, shib_idp_session=Lax, shib_idp_session_ss=LaxAll cookies are sent cross site.Chrome 78.0.3888.0 (Official Build) canary (64-bit)YesYesYes
3GET - Unsolicited SSO Cross SiteNot Set (defaults to Lax)All cookies are sent cross site. Chrome is treating all cookies not set with SameSite as Lax, as enabled by the #same-site-by-default-cookies chrome flagChrome 78.0.3888.0 (Official Build) canary (64-bit)YesYesYes
4POST - SAML 2.0 Post BindingJSESSIONID=Strict, shib_idp_session=Lax,shib_idp_session_ss=LaxThe IdP goes in search of the session from HTML local storage (because it is enabled), hence webflow POST-REDIRECT-GET cycle on the initial client-storage-read view, JSESSIONID is SET on REDIRECT, but not sent on GET, hence the flow can not be resumed from the client-storage-read viewChrome 78.0.3888.0 (Official Build) canary (64-bit)NoNoYes
5POST - SAML 2.0 Post BindingJSESSIONID=Lax, shib_idp_session=Lax, shib_idp_session_ss=LaxThe IdP goes in search of the session from HTML local storage (because it is enabled), hence webflow performs a POST-REDIRECT-GET cycle on the initial client-storage-read view, JSESSIONID is SET on REDIRECT, and is sent along with the shib_idp_session and shib_idp_session_ss on GET, hence the flow can be resumed, the shib_idp_session_ss is reloaded, matched to the shib_idp_session and SSO performedChrome 78.0.3888.0 (Official Build) canary (64-bit)YesYesNo, expecting to fail because of the initial POST request. See the detailed description in section [SameSite Lax POST flow] below
6


POST - SAML 2.0 Post Binding

Not Set (defaults to Lax)Chrome is treating all cookies not set with SameSite as Lax, as enabled by the #same-site-by-default-cookies chrome flag. The IdP goes in search of the session from HTML local storage (because it is enabled), hence webflow POST-REDIRECT-GET cycle on the initial client-storage-read view, JSESSIONID is set on REDIRECT, and is sent along with the shib_idp_session and shib_idp_session_ss on GET, hence the flow can be resumed, the shib_idp_session_ss is reloaded, matched to the shib_idp_session and SSO performed. Although this is not consistent.Chrome 78.0.3888.0 (Official Build) canary (64-bit)YesYes/NoNo, expecting to fail because of the initial POST request. See the note below, behaviour is not consistent
7POST - SAML 2.0 Post BindingJSESSIONID=Lax, shib_idp_session=None, shib_idp_session_ss=LaxA new JSESSIONID is set during the initial Webflow POST-REDIRECT-GET cycle on the initial client-storage-read view, the shib_idp_session is sent with every request, the shib_idp_session_ss is sent on the final GET request. SSO is performedChrome 78.0.3888.0 (Official Build) canary (64-bit)YesYesYes, all requests carry the shib_idp_session required for SSO, the initial POST-REDIRECT-GET cycle creates and then sends the JSESSIONID to progress the webflow conversation
8GET - RedirectJSESSIONID=Lax, shib_idp_session=Lax,shib_idp_session_ss=LaxAll cookies are sent cross-siteChrome 78.0.3888.0 (Official Build) canary (64-bit)YesYesYes
9GET - RedirectNot Set (defaults to Lax)All cookies are sent cross-siteChrome 78.0.3888.0 (Official Build) canary (64-bit)YesYes

Yes


** This is only the case if the Client Session Storage Service beans are removed from the shibboleth.ClientStorageServices list. Otherwise, the PopulateClientStorageLoadContext will load session information from the client using the client-storage-read view-state, which will then send the shib_idp_session cookie same-origin after that view-state has posted back to the IdP.


IMPORTANT:
It appears that current versions of Chrome are treating defaulted (SameSite not set) cookies as SameSite Lax + POST for a period of 2 minutes from when they are set, then SameSite Lax thereafter Issue 990439 - chromium - An open-source project to help move the web forward. - Monorail. Within this window, the the IdP will operate normally, as if there is no SameSite restriction. Otherwise, there are two general possibities:

  1. All IdP session cookies which where set more than 2 minutes ago are treated as Lax. This then gives the same result as test 5 above, and the description in the section [SameSite Lax POST flow].
  2. Different combinations of JSESSIONID, shib_idp_session or shib_idp_session_SS are inside or outside of the 2 minute window. This leads to different types of behaviour; most commonly, as JSESSIONID is nearly (always) set first, the following would be true
    1. JSESSIONID=Lax+POST, shib_idp_session=Lax, shib_idp_session_ss=Lax. The JSESSIONID is sent cross-site with the post request but the shib_idp_session and shib_idp_session_ss are not. The storage services can be pulled out of the given session referenced by the JESSIONID, and hence client-storage-read is not required to reload it. The flow continues without the POST-REDIRECT-GET cycle of a view-state, hence the shib_idp_session cookie is not sent and the previous authentication result can not be retrieved from the IdPSession. As a result, the username and password login view is presented - thus breaking SSO.

SameSite Lax, POST flow, with client side session storage and HTML local storage option enabled

The following outlines the network interactions with the IdP for a POST SAML SSO request. The user has previously authenticated using the standard Password flow, and in so doing obtained shib_idp_session and JSESSIONID cookies. The IdP is using client side session storage, either as a cookie or in HTML local storage.

SO = Same-origin request.
CS = Cross-site request.
sp/SAML2/InitSSO-CS/POST = is a new controller endpoint that overrides destination address with ‘localhost’ to mimic a cross-site request.

  1. user-agent top-level site is https://shibtest.com:8443.
  2. (SO)(GET) user-agent navigates to https://shibtest.com:8443/sp/SAML2/InitSSO-CS/POST. Which is a same-origin request to initate a SAML 2 AuthN Request.
  3. BEGIN SAML AUTHENTICATION REQUEST ------
  4. (CS)(POST) The shibtest.com sp/SAML2/InitSSO-CS/POST controller performs a cross-site POST SAML authentication request to the IdP at https://localhost:8443/idp/profile/SAML2/POST/SSO.
    1. SameSite is LAX, hence cookies for the localhost domain are NOT sent cross-site e.g. any existing JSESSIONID, shib_idp_session or if present shib_idp_session_ss cookies are not sent.
    2. The IdP processes the SAML request, is unable to load the client storage context as the JESSIONID was not sent, and the new one the container issues does not link to a session that already exists. Hence it goes in search of the session from HTML local storage (because it is enabled), and proceeds to the LocalStorageRead view-state, commits a redirect response (https://localhost:8443/idp/profile/SAML2/POST/SSO?execution=e1s1) to the user-agent, and issues a new JSESSIONID.
    3. Note, the user-agent still has a top-level site of https://shibtest.com:8443.
  1. (CS)(GET) The user-agent responds to the redirect with a cross-site GET request to https://localhost:8443/idp/profile/SAML2/POST/SSO?execution=e1s1. Which then displays the client-storage-read.vm page.
    1. Webflow is using a POST-REDIRECT-GET strategy*. This cycle is happening cross-site, but the final GET request allows Lax SameSite cookies to be sent. Hence, the new JSESSIONID obtained in the inital POST-REDIRECT response is sent with the final GET request and the flow can resume. Note, the shib_idp_session cookie is sent with the final GET cross-site request but that is not strictly that important at this stage.
    2. From here any subsequent requests occur same-origin so cookies are always sent.
  1. (SO)(POST) The user-agent then POSTS form data from the client-storage-read view back to the IdP. This is a same-origin request, and contains all required cookies and session information.
    1. The flow continues and the shib_idp_sesison cookie is used to find a previous IdPSession from the loaded (from cookie or local storage) shib_idp_session_ss storage record during the PopulateSessionContext action using the StorageBackedSessionManager.
    2. The ExtractActiveAuthenticationResults action then extracts the AuthenticationResult from the IdpSession and, providing certain criteria are satisfied, enables SSO.

[*][By default when a flow enters a view state, it executes a client-side redirect before rendering the view]

Using Client Side Session Storage and Disabling HTML Local Storage.

Here we test again, this time disabling the IdPs ability to use HTML local storage to store session information, instead only using cookies e.g. setting idp.storage.htmlLocalStorage to false.


NoTestSameSite ValueResultsBrowserIdP workingSSO WorkingExpected Result
10POST - SAML 2.0 Post BindingJSESSIONID=Strict, shib_idp_session=Lax,shib_idp_session_ss=LaxThe IdP does not receive the JSESSIONID cross-site, it can not resume an IdPSession, hence it tries to load the DisplayUsernamePasswordPage (login) page. Webflow performs a POST-REDIRECT-GET cycle to render the login page, but because it can not resume the flow on the final GET request (because the JSESSIONID is still not sent cross-site), Webflow is unable to resume the flow and throws an error.Firefox 72.0.1 with sameSite default enabledNoNoYes
11POST - SAML 2.0 Post BindingJSESSIONID=Lax, shib_idp_session=Lax, shib_idp_session_ss=Lax

No cookies are sent cross-site, however the IdP no longer goes in search of the session from HTML local storage on the browser, and hence does not render the client-storage-read view. As a result, there are no same-site requests to the IdP where the the shib_idp_session_ss cookie could be sent before the IdP tries to load the SSO session. Therefore, left without any SSO session information, the IdP renders the login page. 

Firefox 72.0.1 with sameSite default enabledYesNoYes, was expected a similar result to test 5. 
12

POST - SAML 2.0 Post Binding

Not Set (defaults to Lax). Same results as 11. Firefox 72.0.1 with sameSite default enabledYesNoNo, expecting to fail because of the initial POST request. See the note below, behaviour is not consistent


Using Server Side Session Storage and Enabling/Disabling HTML Local Storage. 


Four different POST requests where again tested, this time using server side session storage. The hypothesis being, under the following conditions, SSO will break:

  1. The IdP session cookies are treated as SameSite=Lax.
  2. The entire SAML2 SSO flow on the IdP is completed from the initial cross-site POST request from the SP - because the client-storage-read view is not required as client side storage is not used.
NoTestSameSite ValueResultsBrowser

IdP

working

SSO WorkingExpected Result
13POST - SAML 2.0 Post Binding (Server Side Session Storage). Client storage services list  not empty. HTML local storage set too true.JSESSIONID=Lax, shib_idp_session=LaxNo cookies sent cross-site, but the client-storage-read view is rendered because the idp.storage.htmlLocalStorage option is set to true, and the shib_idp_session cookie is sent on subsequent same-origin requests from that view. Hence, the shib_idp_session can be used to retrieve a server side IdPSession and SSO occurs. Chrome 78.0.3888.0 (Official Build) canary (64-bit)YesYesNo
14POST - SAML 2.0 Post Binding (Server Side Session Storage).  ClientStorageServices list empty. HTML local storage set too true.JSESSIONID=Lax, shib_idp_session=LaxNo cookies sent cross-site, the IdP does not attempt to load session from client HTML storage because the bean references in shibboleth.ClientStorageServices are commented out (see below), hence the client-storage-read view-state is not required, therefore the shib_idp_session cookie is not available when the session is loaded by the sessionResolver during the PopulateSessionContext action. This breaks SSO, instead showing the user the login page. Chrome 78.0.3888.0 (Official Build) canary (64-bit)YesNoYes
15POST - SAML 2.0 Post Binding (Server Side Session Storage). ClientStorageServices list empty. HTML local storage set too true.JSESSIONID=Lax, shib_idp_session=NoneThe entire flow is executed from the initial SP post request up until the SAML Request is posted back to the SP (via a view-state). A new JSESSIONID is created, the shib_idp_session is sent cross-site allowing the IdPSession to be retrieved, and SSO occurs.Chrome 78.0.3888.0 (Official Build) canary (64-bit)YesYesYes
16POST - SAML 2.0 Post Binding (Server Side Session Storage). .ClientStorageServices list  not empty. HTML local storage set too false.JSESSIONID=Lax, shib_idp_session=LaxNo cookies sent cross-site, the IdP does not attempt to load session from client HTML storage because idp.storage.htmlLocalStorage is set to false. Hence the client-storage-read view-state is not required, therefore the shib_idp_session cookie is not available when the session is loaded by the sessionResolver during the PopulateSessionContext action. This breaks SSO, instead showing the user the login page.Chrome 78.0.3888.0 (Official Build) canary (64-bit)YesNoYes


Contradictory to the hypothesis, test 13 shows SSO working even though the initial cross-site POST request does not send any of the IdP session cookies. This occurs because of the configuration of the IdP, that is; even if server side session storage is configured in idp.properties (idp.session.StorageService), the client-storage-read view is still rendered provided the idp.storage.htmlLocalStorage property is set to true, and the list of shibboleth.ClientStorageServices still contains reference to the various client storage services e.g.

idp.properties:
	idp.storage.htmlLocalStorage = true

session-manager.xml:
	<util:list id="shibboleth.ClientStorageServices">
        	<ref bean="shibboleth.ClientSessionStorageService" />
       	 <ref bean="shibboleth.ClientPersistentStorageService" />
	</util:list>

Of note, the IdP is still storing session information on the server even if those properties/beans are enabled. 

As per XML documentation surrounding this list, If server side storage is used these bean references can be safely removed. However, If they are removed, or the idp.storage.htmlLocalStorage is set to false,  the results in test 14 are observed and SSO breaks. That is to say, if server side session storage is used AND IdP session cookies are treated as SameSite=Lax (defaulting in chrome 80), and EITHER those bean references are removed OR idp.storage.htmlLocalStorage is disabled, SSO will break. 

Test 15 shows what happens if the shib_idp_session cookie is set to SameSite=None. This enables SSO, even if the JSESSIONID is set to SameSite=Lax and is not sent in the initial cross-site request.

In conclusion, IdP deployers using server side session storage should not experience issues with SSO for defaulted SameSite=Lax IdP session cookies using the HTTP-POST SAML2 binding, providing they have not removed those bean references and the idp.storage.htmlLocalStorage is enabled. 

Conclusion

In conclusion, the IdP should continue to function when its cookies are being defaulted to SameSite=Lax by browsers (currently tested on Chrome 78-81 and Firefox 72 with the same-site default flags set). Typically, we have only seen the IdP itself break when the JSESSIONID is set to SameSite 'Strict', which should not happen apart from when explicitly trying to set SameSite=None with older versions of Safari on MacOS <=10.14 and all WebKit browsers on iOS <=12  (https://bugs.webkit.org/show_bug.cgi?id=198181). However with regards to achieving single-sign-on you may see degraded operation, and the following possibilities occur:


Therefore to try and guarantee SSO on existing installations of the IdP v3.X you could enabled the HTML Local Storage plugin whether you use client-side storage or server-side storage by setting the idp.storage.htmlLocalStorage property to true in idp.properties, see StorageConfiguration for more information and any implications.

SameSite and Single Logout


SP Front-channel SLO propagation from the IdP is facilitated using iframes, javascript, and in some cases HTML5 storage [LogoutConfiguration]. iframes that send SLO requests to SPs do not trigger top level navigation changes to the main browser document/window. As a result, same-site cookies will not be sent cross-site within the iframe - even when using 'safe' HTTP GET requests - unless the cookie is SameSite=None. Consequently, in cases where a principal's session state exists solely in a user agent in the form of a cookie, the SP must set the same-site attribute on the cookie to None - this is the responsibility of the SP.

For an SLO request to a Shibboleth SP the SP session cookie is optional, requiring only the NameID in the request, as a result SLO is unaffected for either HTTP-Redirect or HTTP-POST bindings.

The SOAP SLO binding uses backchannel communication and hence cookies are not relevant in this context.


Appendix A

the following algorithm returns “same-site” or “cross-site” (see [1]):

  1. If “request”'s client is “null”, return “same-site”.
  2. Let “site” be “request”'s client’s “site for cookies” (as defined in the following sections).
  3. Let “target” be the registrable domain of “request”'s current url.
  4. If “site” is an exact match for “target”, return “same-site”.
  5. Return “cross-site”.

References

(1) https://tools.ietf.org/html/draft-west-first-party-cookies

(2) https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#page-42

(3) https://tools.ietf.org/html/draft-west-cookie-samesite-firstparty-00

(4) https://www.chromestatus.com/feature/5088147346030592

[5] Can I use… Support tables for HTML5, CSS3, etc

(6) https://datatracker.ietf.org/doc/draft-west-cookie-incrementalism/