How to login in Auth0 in an E2E test with Cypress?
Asked Answered
E

4

31

I have started testing a react webapp but I didn't go far because I had issues with the login. I am using cypress e2e testing tool.

A welcome page is shown with a button to login, which will redirect you to auth0 service. User is login with email and password , then is redirected back to the webapp with a token.

I tried many different approach each of them resulting in a different problem.

Note: I don't want to test Auth0, I just want to enter in my webapp.

Attempt 1. Clicking on login button

Tried: Cypress should do the same as what the user does, therefore the test will click login button and go to Auth0 and fill in credentials. Problem: Cypress doesn't allow you to navigate to another domain during the test.

Because Cypress changes its own host URL to match that of your applications, it requires that your application remain on the same superdomain for the entirety of a single test.

You are supposed to be able to disable that setting setting "chromeWebSecurity": false in cypress.json but it will not work yet because you can only visit a single domain with cy.visit()

Attempt 2. Login programmatically from the test

Tried: login from the cypress test with auth0-js library so it is not needed to click in login button and thus no domain change occurs.

describe('Waiting to fetch', () => {
  beforeEach(() => {
    this.fetchAuthDeferred = getDeferred()
    cy.visit('http://localhost:3000', {
      onBeforeLoad(win) {
        cy.stub(win, 'fetch')
          .withArgs($url)
          .as('fetchAuth')
          .returns(this.fetchAuthDeferred.promise)
      }
    })
  })

  it('login', () => {
    cy.visit('http://localhost:3000')

    const auth = new auth0.WebAuth(authOptions)
    auth.login(loginOptions)

    cy.get('@fetchAuth', { timeout: 10000 }).should('haveOwnProperty', 'token')

    cy.visit('http://localhost:3000')
    cy.get('[class*="hamburger"]').click()
  })
})

Problems: cy.route() doesn't wait for fetch request, a workaround is to use cy.stub(win, 'fetch'). It won't wait:

enter image description here

Attempt 3. Login programmatically from the webapp

Tried: I started to think that cypress only spy request made from the app and not from the test itself (as I tried in the point above).

I added a fake-login button in the welcome page which will call auth0-js (so no domain change) with hardcoded credentials and click it from the test

cy.get('#fake-login').click()

Problems: that strategy worked, but of course I don't want to add a button with credential in the welcome page. So I tried adding the button element to the webapp during the test:

it('Login adding element', () => {
  cy.visit('http://localhost:3000')
  const = document.createElement('div')
  fakeLogin.innerHTML = 'Fake login'
  fakeLogin.onclick = function() {
    const auth = new auth0.WebAuth(authOptions)
    auth.login(loginOptions)
  }
  fakeLogin.style.position = 'absolute'
  fakeLogin.style.zIndex = 1000
  fakeLogin.id = 'fake-login'

  cy.get('#root').invoke('prepend', fakeLogin)
  cy.get('#fake-login').click()
  cy.get('[class*="hamburger"]').click() // Visible when logged in
})

And for some reason this doesn't work, the element is added but yt will not wait until the request are made.

So I don't know what else to try. Maybe everything is a misunderstanding of how login should be done in E2E, should I work with mock data so login is not needed?

Ethaethan answered 6/7, 2018 at 11:3 Comment(2)
You have probably seen this already, but in case you haven't, OAuth interaction is known to be difficult to impossible: docs.cypress.io/guides/references/known-issues.html#OAuth - This is actively being worked on. You could try gitter as they suggest, though as of writing this the gitter chat has a lot more people asking questions than people giving answers. I do believe some of the devs read the questions there regularly so you may have some luck there.Xerosere
You might find this helpful - #59665221Jamesy
R
4

This is not currently supported in Cypress. I built a workaround that might help, though.

I set up a simple server that runs in parallel to cypress. The endpoint opens a headless instance of Puppeteer and completes the login flow, responding to the call with all the cookies:

const micro = require("micro");
const puppeteer = require("puppeteer");
const url = require("url");

const login = async (email, password) => {
  const browser = await puppeteer.launch({ headless: true });
  const page = await browser.newPage();
  await page.goto("https://my-login-page.com");
  // do whatever you have to do to get to your auth0 lock screen, then:
  await page.waitFor(".auth0-lock-input-email");
  await page.waitFor("span.auth0-label-submit");
  await page.type(".auth0-lock-input-email input", email);
  await page.type(".auth0-lock-input-password input", password);
  await page.click("span.auth0-label-submit");
  await page.waitFor("some-selector-on-your-post-auth-page");
  return page.cookies();
 };

const server = micro(async (req, res) => {
  // expect request Url of form `http://localhost:3005?email=blahblah&password=blahblah
  const data = url.parse(req.url, true);
  const { email, password} = data.query;
  console.log(`Logging ${email} in.`);
  return login(email, password);
});

server.listen(3005);

Then I just extend Cypress to add the login command:

Cypress.Commands.add("login", (email, password) => {
  const reqUrl = `http://localhost:3005?email=${encodeURIComponent(
    email
  )}&password=${encodeURIComponent(password)}`;
  console.log("Beginning login.", reqUrl);
  cy.request(reqUrl).then(res => {
    const cookies = res.body;
    cookies.forEach((c) => {
      cy.setCookie(c.name, c.value, c);
    });
  });
});

Each call takes ~5-10s, which sucks, but better than not having any auth at all :/

Reflector answered 10/9, 2018 at 23:37 Comment(1)
This worked for me. But I edited the server to prompt for a code in the two-factor authentication.Quanta
A
4

You can follow this article though for me it didn't work. I managed it work with the help of this article:

yarn add auth0-js --dev

Let's create a custom command called loginAsAdmin:

import { WebAuth } from 'auth0.js';

Cypress.Commands.add('loginAsAdmin', (overrides = {}) => {
Cypress.log({
    name: 'loginAsAdminBySingleSignOn'
});

const webAuth = new WebAuth.WebAuth({
    domain: 'my-super-duper-domain.eu.auth0.com', // Get this from https://manage.auth0.com/#/applications and your application
    clientID: 'myclientid', // Get this from https://manage.auth0.com/#/applications and your application
    responseType: 'token id_token'
});

webAuth.client.login(
    {
        realm: 'Username-Password-Authentication',
        username: '[email protected]',
        password: 'SoVeryVeryVery$ecure',
        audience: 'myaudience', // Get this from https://manage.auth0.com/#/apis and your api, use the identifier property
        scope: 'openid email profile'
    },
    function(err, authResult) {
        // Auth tokens in the result or an error
        if (authResult && authResult.accessToken && authResult.idToken) {
            const token = {
                accessToken: authResult.accessToken,
                idToken: authResult.idToken,
                // Set the time that the access token will expire at
                expiresAt: authResult.expiresIn * 1000 + new Date().getTime()
            };

            window.sessionStorage.setItem('my-super-duper-app:storage_token', JSON.stringify(token));
        } else {
            console.error('Problem logging into Auth0', err);
throw err;
        }
    }
);
  });

To use it:

    describe('access secret admin functionality', () => {
    it('should be able to navigate to', () => {
        cy.visitHome()
            .loginAsAdmin()
            .get('[href="/secret-adminny-stuff"]') // This link should only be visible to admins
            .click()
            .url()
            .should('contain', 'secret-adminny-stuff/'); // non-admins should be redirected away from this url
    });
});

All credit goes to Johnny Reilly

Anatolian answered 23/11, 2018 at 14:12 Comment(2)
and how do you import it? Their docs don't really say.Hotze
strike that. I found it import createAuth0Client from '@auth0/auth0-spa-js'; auth0.com/docs/libraries/auth0-single-page-app-sdk/… We might want to update the answer since it's not documented how it should be.Hotze
F
2

I've had this same problem with a React application once before, and now have been facing it once again. Last time I was forced to migrate from the high-level auth0-spa-js library to the more generic auth0.js library, in order to get a working solution for both the "Cypress-way" (Password grant) and the "normal way" (Authorization Code grant).

Back then my problem with the auth0-spa-js library was that it was not possible to configure it to use localStorage as the token cache. Now, however, this has changed; that same library has gotten such support, and that same support is made available to us in the even-more-high-level auth0-react library (which is using the auth0-spa-js library under the hood), which is the library I've used this time.

The solution for me have been to configure the auth0-react library to use localstorage cache when in either test or development mode, while still using the recommended memory cache in production:

const {
  REACT_APP_AUTH0_DOMAIN,
  REACT_APP_AUTH0_CLIENT_ID,
  REACT_APP_AUDIENCE,
  NODE_ENV
} = process.env;

ReactDOM.render(
  <Auth0Provider
    domain={REACT_APP_AUTH0_DOMAIN as string}
    clientId={REACT_APP_AUTH0_CLIENT_ID as string}
    audience={REACT_APP_AUDIENCE as string}
    redirectUri={window.location.origin}
    cacheLocation={
      ["development", "test"].includes(NODE_ENV) ? "localstorage" : "memory"
    }
  >
    <App />
  </Auth0Provider>,
  document.getElementById("root")
);

If you are instead using the auth0-spa-js library directly you can configure the Auth0Client to use localStorage as the cache location.

This then enables us to effectively imitate the behavior of the auth0-spa-js library of storing a JSON object of login info in localStorage upon successful authentication. In the "login" Cypress command we dispatch the authentication request to Auth0 and use the tokens from the response to generate a "fake" authentication object which is put in localStorage:

import * as jwt from "jsonwebtoken";

const env = Cypress.env;

Cypress.Commands.add("login", () => {
  const username = env("auth-username");

  cy.log(`Login (${username})`);

  const audience = env("auth-audience");
  const client_id = env("auth-client-id");
  const scope = "openid profile email";

  cy.request({
    method: "POST",
    url: env("auth-url"),
    body: {
      grant_type: "password",
      username,
      password: env("auth-password"),
      audience,
      scope,
      client_id,
      client_secret: env("auth-client-secret")
    }
  }).then(({ body }) => {
    const itemName = `@@auth0spajs@@::${client_id}::${audience}::${scope}`;

    const claims = jwt.decode(body.id_token);
    const {
      nickname,
      name,
      picture,
      updated_at,
      email,
      email_verified,
      sub,
      exp
    } = claims;

    const item = {
      body: {
        ...body,
        decodedToken: {
          claims,
          user: {
            nickname,
            name,
            picture,
            updated_at,
            email,
            email_verified,
            sub
          },
          audience,
          client_id
        }
      },
      expiresAt: exp
    };

    window.localStorage.setItem(itemName, JSON.stringify(item));
  });
});
Flattish answered 18/8, 2020 at 19:43 Comment(1)
Great contribution, the only change on my implementation was that I had to force audience: 'default' as it was the way it was being set by the library itself, other than that, it works wonders.Tussle
H
0

recently I ran into the similar challenge, here is how I solved it:

  1. on the console identify the network request that carries username and password in a payload
  2. then I create a custom command in support/commands the command will look something like this :
Cypress.Commands.add('login', () => { 

    cy.request({

      method: 'POST',

      url: 'https://yourapp.com/auth?ReturnUrl=%2fa',

      form: true,

      body: {

        Username: 'usernamevalue',
        Password: 'passwordvalue',
       }

    });

  });`
  1. then use your command in your test before visiting your test destination:
it('Should Login', () => {
   
    cy.login();

    cy.visit("https://yourapp.com/a#/Contact");
})
Housebreaking answered 30/6, 2021 at 20:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.