How to test single page application with Cypress and Auth0
Asked Answered
G

3

5

I am having a single page application hidden behind Auth0 lock, using @auth0/auth0-spa-js. I would like to test it using Cypress, so I have decided to follow the official Auth0 blog post, as well as Johnny Reilly blog post.

I am able to successfully retrieve valid JWT token from auth0 using suggested request. I have no idea what to do with it :(

The trouble I am facing is that both of the above approaches are relying on the app to store the JWT token locally (either in cookie or localstorage). The @auth0/auth0-spa-js is, however, using a different approach, and I assume all the relevant cookies/localstorage is stored on auth0 domains.

Do you have any idea, if there is a way to get around it?

There is a similar issue reported here raised in July 2018, not really providing any solution

Gosselin answered 9/1, 2020 at 13:9 Comment(5)
I managed the problem of testing an oauth2 authenticated spring boot/security application by running it using form based authentication - made cypress use very easy, reading credentials from a json fixture file. Are you able to use a different authtentication mechanism during tests?Philippians
I assume it meant, you had to enter the email and address in login form generated by Auth0. I assume I could do that, but then my tests would be actually testing the Auth0 form, which is outside of my control. I would prefer to generate a token and then just instruct auth0 to use it somehowGosselin
no, meant that I side-stepped the problem of integrating oauth with cypress by using vanilla username/password form based security when running my functional ui tests.Philippians
oh, so you are having 2 ways of authenticating. One with auth0 for all users, and then separate "hidden" one for e2e? That will probably not work for my use case, since the JWT is later used to authorize a separate REST call, belonging to different serviceGosselin
yes, but the form based way is only used in in testing. Could the later REST calls be stubbed in Cypress? Appreciate that this might be compromising the integration aspect of the testing. Anyway, I think you get my point, when faced with a problem, dodge around it ..Philippians
G
5

I found a resolved issue on @auth0/auth0-spa-js github. The approach suggested by cwmrowe seems to be working

The solution is to mock the response of oauth/token endpoint with token generated on e2e test side.

The approach seems to be working for us

I am copying over the sample code cwmrowe has provided

Cypress.Commands.add(
  'login',
  (username, password, appState = { target: '/' }) => {
    cy.log(`Logging in as ${username}`);
    const options = {
      method: 'POST',
      url: Cypress.env('Auth0TokenUrl'),
      body: {
        grant_type: 'password',
        username,
        password,
        audience: Cypress.env('Auth0Audience'),
        scope: 'openid profile email',
        client_id: Cypress.env('Auth0ClientId'),
        client_secret: Cypress.env('Auth0ClientSecret')
      }
    };
    cy.request(options).then(({ body }) => {
      const { access_token, expires_in, id_token } = body;

      cy.server();

      // intercept Auth0 request for token and return what we have
      cy.route({
        url: 'oauth/token',
        method: 'POST',
        response: {
          access_token,
          expires_in,
          id_token,
          token_type: 'Bearer'
        }
      });

      // Auth0 SPA SDK will check for value in cookie to get appState
      // and validate nonce (which has been removed for simplicity)
      const stateId = 'test';
      const encodedAppState = encodeURI(JSON.stringify(appState));
      cy.setCookie(
        `a0.spajs.txs.${stateId}`,
        `{%22appState%22:${encodedAppState}%2C%22scope%22:%22openid%20profile%20email%22%2C%22audience%22:%22default%22}`
      );

      const callbackUrl = `/auth/callback?code=test-code&state=${stateId}`;
      return cy.visit(callbackUrl);
    });
  }
);

declare namespace Cypress {
  interface Chainable<Subject> {
    login(
      username: string,
      password: string,
      appState?: any
    ): Chainable<Subject>;
  }
}
Gosselin answered 13/1, 2020 at 12:18 Comment(0)
B
0

Whilst it's not recommended to use the UI to login I do this myself once prior to all tests and then use the silent auth for the tests:- cy.visit("/") silent auths and allows access to the app.

integration/app.js

describe("App", () => {
  before(() => {
    Cypress.config("baseUrl", "http://localhost:3000");
    cy.login();
  });

  /** Uses silent auth for successive tests */
  beforeEach(() => {
    cy.restoreLocalStorage();
  });

  afterEach(() => {
    cy.saveLocalStorage();
  });

  /** tests */

support/commands.js

/**
 * Auth0 login
 * https://github.com/cypress-io/cypress/issues/461#issuecomment-392070888
 *
 * Allows silent auth login between tests
 */
let LOCAL_STORAGE_MEMORY = {};

Cypress.Commands.add("saveLocalStorage", () => {
  Object.keys(localStorage).forEach(key => {
    LOCAL_STORAGE_MEMORY[key] = localStorage[key];
  });
});

Cypress.Commands.add("restoreLocalStorage", () => {
  Object.keys(LOCAL_STORAGE_MEMORY).forEach(key => {
    localStorage.setItem(key, LOCAL_STORAGE_MEMORY[key]);
  });
});

Cypress.Commands.add("clearLocalStorage", () => {
  LOCAL_STORAGE_MEMORY = {};
});
Bettyebettzel answered 13/1, 2020 at 12:36 Comment(0)
J
0

For those who has issue with Google Sign in for Cypress look at the plugin: https://github.com/lirantal/cypress-social-logins/

  it('Login through Google', () => {
    const username = Cypress.env('googleSocialLoginUsername')
    const password = Cypress.env('googleSocialLoginPassword')
    const loginUrl = Cypress.env('loginUrl')
    const cookieName = Cypress.env('cookieName')
    const socialLoginOptions = {
      username,
      password,
      loginUrl,
      headless: false,
      isPopup: true,
      logs: false,
      loginSelector: 'a[href="/auth/auth0/google-oauth2"]',
      postLoginSelector: '.account-panel'
    }

    return cy.task('GoogleSocialLogin', socialLoginOptions).then(({cookies}) => {
      cy.clearCookies()

      const cookie = cookies.filter(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({
          whitelist: cookieName
        })
      }
    })
  });
Jessalin answered 17/6, 2020 at 19:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.