Using Cypress Intercept to Fix a Cross-Domain Test

The full example for this scenario can be found at https://github.com/scolton99/cypress-sso-cookies

At work, we have been implementing Cypress testing so that we can automate what once was the very drawn-out process of manually testing our highly customized identity management software. It’s certainly not the typical use case for Cypress — we’re not testing a software that is entirely our own and we don’t have a way of running the software locally (on our own laptops), so we’re only using Cypress to test our deployed dev, QA, and production instances of the application.

Like most other applications, our identity management software is behind SSO, so we wanted to use Cypress to test a user’s ability to log in via SSO. This comes with extra added complexity: Cypress is on shaky historical ground with regard to tests that require visiting more than one domain.

Still, for a while, Cypress was working well to test our SSO login process. The test that we wrote was only a few lines of code and seemed to work with mostly out-of-the-box settings. Nothing particularly tricky was required on the Cypress end to make it work. That all changed when we added MFA to our login process.

Adding MFA added another redirect to our login process and this time, it required going out to an external site rather than a domain within our organization. As it turns out, this would break our test. One of the internal SSO domains set a cookie properly, then expected it to be there when the user returned from the third-party MFA service. Because Cypress tests run in an iframe, however, the rules that determine which cookies are sent are different than at the top-level of a browser window.

The cookies being set by our SSO provider were very basic — no SameSite directive, and no Secure directive. The rules determining which cookies send with a request can be found in RFC6265§5.6.3. Let’s go through them one-by-one and see why these cookies don’t send in a Cypress test context:

  1. Either the cookie has no domain set and the domain of the request exactly matches the domain that set the cookie OR the request domain matches or is a subdomain of the domain set on the cookie.
    We’re good here. The cookies are only expected to be visible to the same domain that set them.
  2. The request’s path matches or is a subpath of the cookie’s path.
    All set here as well. The cookies paths are set to “/”, so they match all paths.
  3. If the cookie is marked Secure , the request must use HTTPS.
    All requests in the login flow use HTTPS (of course).
  4. If the cookie is marked HttpOnly , the request must be an HTTP request (i.e., the cookie is not accessible within scripts).
    All of the requests are HTTP requests — we’re not trying to access these cookies from within a script. (The cookies aren’t HttpOnly anyway.)
  5. If the cookie’s SameSite flag is not “None” and this is a cross-site request (which includes redirections from other domains), ALL of the subsequent bullet points must be true.
    We are in the process of being redirected from an outside domain, so this counts as a cross-site request. SameSite was not set on these cookies, so per new web standards, it defaults to “Lax”.
  6. This must be an HTTP(S) request (not a JavaScript request for a cookie value).
    These are HTTP requests, not requests for the cookie within a script.
  7. The SameSite flag must be “Lax” or “Default”.
    As stated above, the SameSite value for these cookies was not specified and thus defaulted to “Lax”.
  8. The request must use HTTPS.
    We are still, of course, using HTTPS.
  9. The request must come from a top-level browsing context (i.e., not a frame).
    Here lies the source of our woes. We’re running this from within a frame, so these cookies will not be sent.

So we’ve identified why the cookies aren’t sending, so how do we go about solving the problem? Cypress has an ingenious method of doing so.

Cypress Interception

Let’s create a project to mock-up this issue. We’ll create two small NodeJS servers to represent our MFA host and our SSO host. Create a new project via npm init and then run the following two commands to get the dependencies:

npm install -s express cookie-parser pug
npm install --save-dev cypress
sso.js

It accepts a request and shows the SSO login page. Then, a form on that page submits and the server redirects to the MFA server and sets a cookie. When MFA redirects back to the SSO site, it will expect to see the cookie again and if it does, it will say “Welcome back!”. If not, it will say “Who are you?”.

There are two files needed for the (very rudimentary) SSO UI:

This will go in views/sso.pug :

views/sso.pug

And this will go in public/css/sso.css :

public/css/sso.css

All this does is create a rudimentary login form. We’re not going to check the values submitted for the username and password (which is good, because both are missing the name attribute and won’t be sent to the server anyway).

Now, we need the MFA server:

mfa.js

This server accepts a request and checks to be sure that there is a query parameter called return , then sets up a page to redirect the user back to that page after a few seconds.

Our MFA server also depends on a very tiny and rudimentary UI. The view should go in views/mfa.pug :

views/mfa.pug

And the script to be run on this page belongs in public/js/mfa.js and is very small:

public/js/mfa.js

We almost have everything we need. Here’s a small index.js to tie everything together:

You’ll need to generate yourself an SSL certificate with a private key. You can store the certificate at the root of the project as cert.crt and the key as cert.key . Then, open Cypress for the first time by running npx cypress open and then close it. In the generated cypress.json , add the line "chromeWebSecurity": false in order for our tests to work.

Since our servers also need to be on different domains in order to have this be a true simulation of the issue, you’ll want to edit your hosts file to include the following lines:

127.0.0.1      sso.local.domain
127.0.0.1 mfa.external.domain

Once that’s done, run node index.js and you should see the following output:

MFA listening...
SSO listening...

Now, open up a new web browser and go to https://sso.local.domain:8443/ . It should prompt you with this login page:

Our SSO UI.

Go ahead an type anything for the username and password, then click “Log in”. With any luck, you’ll land on a screen that says “Welcome back!” after several seconds of waiting.

SSO login process

Now let’s write a Cypress test for this. Create cypress/integration/sso.spec.js :

Failing Cypress test spec

All this test does is mimic exactly what we just did in the browser, but it will be using an iframe to host the site, so we’ll see the problem that we expect:

Cypress test failure

Let’s use Cypress Interception to fix this issue. Before the SSO server’s request to set the cookie can reach the browser, we will have Cypress intercept it and alter the cookie so that it matches the settings we need it to have (namely, SameSite="None" and Secure .

Add the following function at the top of the sso.spec.js file:

And add this test to the existing test suite:

Now, when Cypress sees a POST request to https://sso.local.domain:8443/ , it will intercept that request and pass information about the request being made to the cookieFixer function. That will include all of the request headers and query string parameters, among other things. We don’t care about the request — only the response — so we’ll allow the request to continue unaltered by calling req.continue . This function takes a callback which is passed information about the response, and it’s a modifiable object!

We can alter the headers on the response object to change them before they reach the browser. In this case, we want to take the first value of the Set-Cookie header (it’s a multi-valued header but in this case we know there will be exactly one value) and add ; SameSite=None; Secure to the end to set these flags on our cookie.

This guarantees that our cookie will be sent to the SSO server once again when we are redirected there:

The new passing test!

Voilà! We can now test a complete log-in flow using SSO using this same technique. All in all, not that much needed to be added to our test to make this possible.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store