Matt Laux

How to set up Google Authentication with Cypress

Cypress is the most popular end-to-end testing tool for web applications. Other options such as Jest and React Testing Library were developed more for unit and funtional testing.

The major difference between Cypress and these other tools is that Cypress runs in a browser of your choosing. This is the most accurate method of reflecting how a user will actually navigate your application.

If you are an aspiring web developer or are developing your own application, testing is essential to your development flow. As your application grows, it will become very difficult and time-consuming to manually check all of the different features each time you make a change.

Testing allows you to make changes to your web application and have confidence that these changes will not break an existing feature.

In addition, the Test-driven development (TDD) methodology can increase quality and efficiency of code.

Overview

Throughout this guide, I will reveal how you can incorporate Google Sign-In into your Cypress flow. Handling third-party sign-in services (also known as OAuth) can be tricky with Cypress.

If you have attempted to test your Google Sign-In flow with Cypress you most likely have encountered the following errors:

  1. TimeoutError: waiting for selector "some-selector" failed: timeout 30000ms exceeded
  2. failed with the following error: > waiting for selector "undefined" failed
  3. "Couldn't sign you in. This browser or app may not be secure." error from Google sign-in

Error 1 and 2 are caused by Cypress' inability to communicate with a cross-origin iframe. While you can physically see the Google Sign-in page rendered on the browser, Cypress is unable to interact with it as it is cross-origin. This is the reason it is important to use the cypress-social-logins package to handle OAuth sign-ins.

Error 3 is caused by Google's increased security practices. Google claims it is difficult to differentiate between man-in-the-middle attacks and legitimate login automation tools. More can be read here.

I will discuss how to handle these errors using best practice as recommended by NextAuth.js and Cypress.

This guide assumes that you have already set up your Google OAuth flow. If not here are some resources to help get started with it:

I used the Google OAuth provider through NextAuth.js and highly recommend it. However, this guide should be applicable even if you are not using NextAuth.js. As third-party sign-in services become more prevalent, I hope that this guide can help save some time and energy for future developers.

Table of Contents

Set up cypress-social-logins

Before you can get started you need to install the cypress, cypress-social-logins, and @testing-library/cypress packages:

npm install --save-dev cypress cypress-social-logins @testing-library/cypress

Cypress will populate the necessary directories as well as some example files.

Next you need to create a cypress.config.ts file in your root directory.

NOTE: If you are not using Typescript the file type will be .js. Add the following code to the file:

import { defineConfig } from 'cypress';

import { plugins } from 'cypress-social-logins';

const googleSocialLogin = plugins.GoogleSocialLogin;

export default defineConfig({
  e2e: {
    setupNodeEvents(on, config) {
      // implement node event listeners here
      on('task', {
        GoogleSocialLogin: googleSocialLogin, // listens for GoogleSocialLogin task in tests
      });
    },
    baseUrl: 'http://localhost:3000',
    chromeWebSecurity: false, // allow cypress to access cross-domain URLS such as NextAuth.js login provider pages
  },
});

Add Google Sign-In env variables

After creating your cypress.config file, you need to add your Google Sign-In credentials to your environment variables. Rather than using the .env file you use for production, you will create a cypress.env.json file in your root directory with the following variables:

{
  "GOOGLE_USER": "testuser@gmail.com",
  "GOOGLE_PW": "testGmailPassword",
  "COOKIE_NAME": "next-auth.session-token",
  "SITE_NAME": "http://localhost:3000"
}

The GOOGLE_USER and GOOGLE_PW variables should be the credentials for your Google account.

Create package.json Cypress scripts

There is a script you need to add to your package.json file in order to run Cypress:

"cypress": "cypress open",

Now to open Cypress all you have to do is enter the following command into your terminal:

npm run cypress

Cypress login task / test

The next step is to create your actual Cypress test. The Cypress test should reflect the below code:

NOTE: The loginSelector and postLoginSelector fields use Document.querySelector() syntax which can make it a little tricky. The easiest method is to use the data-testid attribute and selector as shown below.

import { Cookie } from 'next-auth/core/lib/cookie';

describe('Login page', () => {
  before(() => {
    cy.log('Visiting http://localhost:3000');
    cy.visit('/auth/signin'); // navigate to signin page that has the OAuth providers
  });
  it('Login with Google', () => {
    const username = Cypress.env('GOOGLE_USER');
    const password = Cypress.env('GOOGLE_PW');
    const loginUrl = `${Cypress.env('SITE_NAME')}/auth/signin`;
    const cookieName = Cypress.env('COOKIE_NAME');
    const socialLoginOptions = {
      username,
      password,
      loginUrl,
      headless: false,
      logs: false,
      isPopup: true,
      loginSelector: '[data-testid=Google]', // selector from the signin page
      postLoginSelector: '[data-testid=dashboard]', // selector from the page the user should be redirected to after login
    };

    return cy
      .task<{
        cookies: any;
        lsd: any;
        ssd: any;
      }>('GoogleSocialLogin', socialLoginOptions)
      .then(({ cookies }) => {
        cy.clearCookies();

        const cookie = cookies
          .filter((cookie: Cookie) => cookie.name === cookieName)
          .pop();
        if (cookie) {
          cy.setCookie(cookie.name, cookie.value, {
            domain: cookie.domain,
            expiry: cookie.expires,
            httpOnly: cookie.httpOnly,
            path: cookie.path,
            secure: cookie.secure,
          });

          Cypress.Cookies.defaults({
            preserve: cookieName,
          });

          // remove the two lines below if you need to stay logged in
          // for your remaining tests
          cy.visit('/api/auth/signout'); // signout flow line #1
          cy.get('form').submit(); // signout flow line #2
        }
      });
  });
});

The above Cypress test was pulled from NextAuth.js' Cypress documentation.

NOTE: I have adjusted the headless option of socialLoginOptions to false. This will allow you to physically observe the login flow. This makes debugging much more simple. Once you have the login test working, I recommend changing headless to true.

At this point, you should be able to test your login flow. Comment out the bottom two lines of the test labeled signout flow. Run the test and then open the application window of developer tools in your Cypress controlled Chrome tab. If you see an active session cookie it is working.

You may receive the following pop-up:

Allow incoming connections pop-up

It is fine to click Allow.

If you run into any issues read the Using a new Google account section below.

Using a new Google account

Even if people successfully implement the Cypress login test, many still experience issues. One of the most common issues is the Google account that they are using to login.

If you are attempting to use a pre-existing Google account it is likely you will encounter issues. This is due to your Google account having various security settings that prevent Cypress from logging in such as multi-factor authentication.

The solution is to create a brand new Google account with minimal security measures in place. A new Google account solves many of the issues with the cypress-social-login flow.

Integrating Cypress login test with other tests

The purpose behind this login test is not necessarily to test the login flow. Rather it is used to retrieve a session cookie so that login-protected pages can be tested.

Remember that without a session token Cypress will be unable to access your login-protected pages and API routes.

As a result, my recommendation is to place the above login test in the Cypress before() hook and then have your remaining tests follow.

This will only require Cypress to conduct the login flow which retrieves the session cookie once. Then that session cookie will be passed to your other tests allowing you to test protected pages.

The last two lines of the test initiate the NextAuth.js signout flow, which will erase the session cookie retrieved from your login task. More than likely you will want to place the two lines marked signout flow in the Cypress after() hook. This will run the signout flow once after all of your tests complete.

Your final Cypress flow should reflect the following:

  1. before() hook retrieves session cookie
  2. Login-protected page tests are run
  3. after() hook erases session cookie

Here is an example of your final Cypress file:

import { Cookie } from 'next-auth/core/lib/cookie';

describe('Login to dashboard', () => {
  before(() => {
    cy.log('Visiting http://localhost:3000');
    cy.visit('/auth/signin');
    const username = Cypress.env('GOOGLE_USER');
    const password = Cypress.env('GOOGLE_PW');
    const loginUrl = `${Cypress.env('SITE_NAME')}/auth/signin`;
    const cookieName = Cypress.env('COOKIE_NAME');
    const socialLoginOptions = {
      username,
      password,
      loginUrl,
      headless: true,
      logs: false,
      isPopup: true,
      loginSelector: '[data-testid=Google]',
      postLoginSelector: '[data-testid=dashboard]',
    };

    return cy
      .task<{
        cookies: any;
        lsd: any;
        ssd: any;
      }>('GoogleSocialLogin', socialLoginOptions)
      .then(({ cookies }) => {
        cy.clearCookies();

        const cookie = cookies
          .filter((cookie: Cookie) => cookie.name === cookieName)
          .pop();
        if (cookie) {
          cy.setCookie(cookie.name, cookie.value, {
            domain: cookie.domain,
            expiry: cookie.expires,
            httpOnly: cookie.httpOnly,
            path: cookie.path,
            secure: cookie.secure,
          });

          Cypress.Cookies.defaults({
            preserve: cookieName,
          });
        }
      });
  });

  after(() => {
    cy.visit('/api/auth/signout');
    cy.get('form').submit();
  });

  it('dashboard', () => {
    cy.visit('/app/dashboard'); // login-protected page that will redirect to homepage if no session cookie
    cy.get('h1').should('contain', 'Course Catalog'); // check to ensure you could actually navigate to the dashboard page
  });
});

Now you can add as many tests involving login-protected pages and API routes as you want.

References

Hopefully this article was helpful in setting up your Cypress login test and flow. I am not a Cypress expert so if any readers have any improvements please reach out.

The following documentation and articles were all very helpful in my creation of this guide.

Documentation

Troubleshooting

;