The very first test that we usually write is a test for logging in to an application. This is a necessary step, a prerequisite, that is needed for writing tests for actual application features - if we cannot log in to an application then we won’t be able to test it.
Many applications use simple authentication systems where all we need to do in order to authenticate is to provide a login and a password, and if those are correct, a server will exchange those credentials for a token that is then stored in the Local Storage or as a cookie, and as a result we are able to log in to the application.
Some companies do not want to use such authentication systems, because they would have to create it, test it and maintain it and this takes time. Also, security is not that great in those systems either. These days it is very convenient to use Single Sign-On so instead of wasting time and trying to reinvent the wheel, some companies decide to use 3rd party authorization systems like Okta, Auth0 or ADFS which are much more secure.
This is where Cypress starts to struggle a little, and this is where some QA engineers hit a wall and decide to use other tools instead.
The same-origin limitation
In Cypress, each test is limited to only visiting the domains that are determined to be of the same-origin.
For example, if the application under the test is available at https://apptension.com but the login page is available at https://auth0.login.com/apptension then Cypress will not be happy about it and will give us an error:
It is possible to bypass that error by setting chromeWebSecurity flag to true:
However, this would work only in Chrome and in some cases it might not work at all so I would not suggest relying on this solution.
In 9.6.0, the cy.origin command was added which allows you to visit two different super domains in one test. At the moment the command is in the experimental stage and it requires adding the experimentalSessionAndOrigin flag in the cypress.json file.
This command, at least for now, deals with simple cases, such as going from google.com to facebook.com in a single test, however, it has problems when it comes to logging into some applications.
If the login page is in a different superdomain than the application itself, the transition to this page will work properly thanks to the cy.origin command. However, if we use the Google login option from this login page and we are redirected to the google.com domain, the cy.origin command will not be able to handle it.
Also, if using the Sign in with Google option opens the Google login form in a new browser window, then Cypress also will not be able to handle that.
Best practices for authenticating in automation tests
Usually, if the app under the test has simple authentication system, we have separate tests for logging in, just to be sure that it works properly, but in every other test, in the before hook, we just authenticate programmatically to receive the token, which is much faster as we do not have to interact with the UI on the login page in every test.
When it comes to testing 3rd party apps and libraries like Okta, Auth0 or ADFS, the best practice is not to test them at all, since they are out of our control.
Here also the best approach would be to authenticate programmatically to receive the token, to make the entire process faster, however it is not so easy as it sounds.
The real issue
As was mentioned above, 3rd party apps like Auth0 or Okta, are much more secure than simple, custom solutions, however, with security comes complexity.
If we take a look at what happens between providing credentials and receiving a token in a very basic system and compare it to Auth0, we can see that there are many requests and redirects and many of them are no longer in the same superdomain as the app under the test:
On top of that, SDK like auth0-spa-js can be configured in many ways: using cookies, local storage or memory and very often authenticating programmatically is not possible. As a result, logging in to an app like this in Cypress is not going to work.
Since obtaining a token is so complex we could make it simpler:
- ask a developer to create and configure a separate endpoint, for example /login-qa
- making a POST request to this endpoint with login and a password in the body would return the final token
- we could store the token in the browser and proceed with testing
- this endpoint should work only on test environments for security reasons
Solution #1 is not always possible simply because sometimes we do not have access to anybody that could make that happen or the authentication/authorization system relies on complex policies and permissions, for example ADFS, and creating such an endpoint could take a lot of effort.
Since Cypress does not allow for two superdomains in a test, to solve our problem, we could just not use Cypress ...for authentication.
Cypress has its limitations but it also has ways of overcoming those limitations. In automation testing, very often we need to do some things outside of a browser, like clear the database or seed it with some testing data. To do this, Cypress can spawn a Node.js process using a special task() command.
We can use this command to spawn a Chromium instance with the help of Puppeteer and then, since Puppeteer does not mind different superdomains, we can complete the entire authentication flow via UI of the application, and once we receive a token, we can pass it to Cypress, store it in the browser and start testing.
First, we need to install Puppeteer:
Next, we need to write a code for logging in with Puppeteer:
There is nothing extraordinary here: we navigate to the login page, pass email and password and click on a submit button - the difference is that we do that in Puppeteer and not in Cypress.
The only problem is that the entire process of logging in via UI can be time consuming, especially on secure 3rd party services, however, we can solve that, at least to some extent.
Depending on implementation, tokens can have long expiry dates, and we could use that to our advantage by saving tokens in the JSON file.
As you can see in the code above, before we log in, we check whether the file with cookies exists, and only if it does not exist we perform the login. Once we obtain cookies we save them in the file for later use.
Finally, before we read cookies from the file to pass them to Cypress, we check whether they expired - if they did then we use recursion to authenticate again and store new cookies into a file, and if they did not expire then we simply return them.
Since the code above runs outside of Cypress, we need to wrap it and define it as a task in index.js file that is located in the plugins directory, where we can handle all the plugin events:
In the code above we defined a task called puppeteer:saveCookiesToFile that is an async function in which we await the saveCookiesToFile function that we imported from the puppeteer-authentication.js file, and finally we return cookies obtained from that function.
Now all we have to do is to use that task in a test:
Here we invoke the task function that invokes the node code for authenticating in the app but if cookies exist in the JSON file and did not expire, we skip authenticating altogether, and instead we simply read cookies from the JSON and pass them to the setCookies function that in turn saves cookies in the browser and as a result we are authenticated.
The above solution makes it possible to bypass the single-origin limitation of Cypress. It can also, depending on implementation, save us a lot of time because reading cookies from JSON and storing them in the browser is much faster then authenticating via UI.