cypress-io / cypress

Fast, easy and reliable testing for anything that runs in a browser.
https://cypress.io
MIT License
46.85k stars 3.17k forks source link

Problems with origin on login when immediately redirecting #25373

Open ghost opened 1 year ago

ghost commented 1 year ago

Current behavior

A single page web app is hosted on server A, and the login page is hosted on another server B using Authorization Code Grant with PKCE. The web app on server A does not have a public landing page with a login button, but instead immediately redirects to the login page on server B if the user has not been authenticated yet. After login using server B, the user is redirected to server A again. Authentication itself is handled via certificates not username/password. The certificate is specified in the clientCertificates section of the config file.

The baseUrl is set to domain A.

I tried to implement the login in Cypress using cy.session and cy.origin as suggested https://docs.cypress.io/guides/end-to-end-testing/auth0-authentication#Login-with-cy-origin:

Cypress.Commands.add("login", () => {
      const args = {};
      cy.session(
    args,
    () => {
      cy.visit("/");
      cy.origin("loginPageDomain.com", {}, () => {        
        cy.get("#certificateAuthenticationSelectionButton").click();
        cy.get("#login-button").click();
      });
    },
    {
      cacheAcrossSpecs: true,
    }
  );
});

However, when doing so I get a 401 error message for the first visit call:

The response we received from your web server was:

  > 401: Unauthorized

This was considered a failure because the status code was not `2xx`.

This http request was redirected '4' times to:
  - 302: redirect1
  - 302: redirect2
  - 302: redirect3
  - 302: redirect4

If you do not want status codes to cause failures pass the option: `failOnStatusCode: false`

If I add failOnStatusCode: false to the cy.visit method, i.e. cy.visit("/", {failOnStatusCode: false}); then the redirects are called but I get the error:

cy.origin() requires the first argument to be a different domain than top. You passed B to the origin command, while top is at B.

Either the intended page was not visited prior to running the cy.origin block or the cy.origin block may not be needed at all.

I also tried to leave out cy.origin but then when being redirected from server B to server A, cookies (sessionId and XSRF-Token) are provided for the session, however those are not set because those cookies are for domain A but they appears to be cross domain in the browser and since the sameSite property is not set, the cookies are not stored.

This is a problem in Chrome, Edge, Electron. When running in Firefox, the login works without using cy.origin but then the first cy.get in the first test fails with

Timed out retrying after 10000ms: The command was expected to run against origin https://B but the application is at origin https://A.

All further tests run without problems in Firefox. So, my workaround at the moment is to add a dummy test without any body which gets executed first. Then the other tests all run. However, I really need to test Chrome and Edge as well, so this is not a viable solution.

It appears to me that the base origin is set to the redirected domain B instead of the domain that I provided in the baseUrl.

Desired behavior

Overall I expect the login to work as in https://docs.cypress.io/guides/end-to-end-testing/auth0-authentication#Login-with-cy-origin

So, I would expect that 1) I don't get an error for the redirections which forces me to add the failOnStatusCode: false option 2) Even though the first call is immediately redirected, Cypress should set as base origin the web app domain A not the redirected domain B.

Test code to reproduce

see in issue. Cannot provide more details since this is a company internal web app.

Cypress Version

12.3.0

Node version

v16.18.1

Operating System

Windows

Debug Logs

see in description

Other

No response

chrisbreiding commented 1 year ago

Right now there doesn't seem to be enough information to reproduce the problem on our end. Unless we receive a reliable reproduction, we'll eventually have to close this issue until we can reproduce it. This does not mean that your issue is not happening - it just means that we do not have a path to move forward.

Please provide a reproducible example of the issue you're encountering. Here are some tips for providing a Short, Self Contained, Correct, Example and our own Troubleshooting Cypress guide.

chrisbreiding commented 1 year ago

Unfortunately we have to close this issue due to inactivity. Please comment if there is new information to provide concerning the original issue and we can re-open.

jondotblack commented 1 year ago

Same issue when migrating to v12.6 (Next v13.1.1 & Nx v15.7.2) from v10.2 .

My application only has authenticated pages. The only way I could work around the cy.origin() requires the first argument to be a different domain than top. error is to add a new public page with a link to log in on it.

function loginViaAuth0Ui(username, password) {
  // App landing page redirects to Auth0.
  // Throws cy.origin redirect error
  // cy.visit('/');

  // Hack because tests fail if starting with redirect to Auth0
  cy.visit('/test/login');
  cy.get('a').click();

  // Login on Auth0.
  cy.origin(
    Cypress.env('auth0_domain'),
    { args: { username, password } },
    ({ username, password }) => {
      cy.get('input[name="email"]').type(username);
      cy.get('input[name="password"]').type(password, { log: false });
      cy.contains('button[name="submit"]', 'Continue').click();
    }
  );

  // Ensure Auth0 has redirected us back to the app
  cy.url().should('equal', 'http://localhost:3000/');
}

Cypress.Commands.add('loginToAuth0', (username, password) => {
  const log = Cypress.log({
    displayName: 'AUTH0 LOGIN',
    message: [`🔐 Authenticating | ${username}`],
    autoEnd: false,
  });

  log.snapshot('before');

  cy.session(
    `auth0-${username}`,
    () => {
      loginViaAuth0Ui(username, password);
    },
    {
      validate: () => {
        // Validate presence of access token in Cookie.
        // Docs: https://docs.cypress.io/api/commands/getcookie#Session-id
        cy.getCookie('appSession.0').should('exist');
        cy.getCookie('appSession.1').should('exist');
      },
      // Docs: https://docs.cypress.io/api/commands/session#Caching-session-data-across-specs
      cacheAcrossSpecs: true,
    }
  );

  log.snapshot('after');
  log.end();
});

Then for each test;

  ...
  before(() => {
    cy.loginToAuth0(
      Cypress.env('auth0_username'),
      Cypress.env('auth0_password')
    );

    cy.visit('/');
  });
  ...

The other gotcha I ran into is a Sessions cacheAcrossSpecs does not work as expected. If you are only using before(() => { ... }); you have to include { testIsolation: false } on either the test suite or global config. The other option is to use beforeEach(() => { ... }); to login before every spec.

mjhenkes commented 1 year ago

Let me restate the problem to clarify i'm understanding it.

Your login workflow is this: (url names have been changed to protect the innocent)

  1. Vist site www.site.com
  2. If the server detects that you aren't logged in it will server-side redirect you to www.auth.com (possibly a client side redirect?)
  3. browser loads www.auth.com
  4. user enters login info, clicks accept
  5. browser redirects to www.site.com

When you write the cypress test, when you visit www.site.com for it initial log in it sets base url to be www.auth.com because of the redirect and any subsequent tests then expect to be on www.auth.com or the cy.origin block rejects you because your on the 'same origin'.

Try moving the visit to be inside of the login call.

Cypress.Commands.add("login", () => {
      const args = {};
      cy.session(
    args,
    () => {
      cy.origin("loginPageDomain.com", {}, () => {
        cy.visit("/");        
        cy.get("#certificateAuthenticationSelectionButton").click();
        cy.get("#login-button").click();
      });
    },
    {
      cacheAcrossSpecs: true,
    }
  );
});

or

  cy.origin(
    Cypress.env('auth0_domain'),
    { args: { username, password } },
    ({ username, password }) => {
      cy.visit('/')
      cy.get('input[name="email"]').type(username);
      cy.get('input[name="password"]').type(password, { log: false });
      cy.contains('button[name="submit"]', 'Continue').click();
    }
  );

This seems counter-intuitive but since the initial visit redirects to www.auth.com placing the visit in the cy.origin block will prevent it from being set as the base url and allow you to interact with it before returning to your actual site.

mjhenkes commented 1 year ago

Right now there doesn't seem to be enough information to reproduce the problem on our end. We'll have to close this issue until we can reproduce it. This does not mean that your issue is not happening - it just means that we do not have a path to move forward.

Please open comment on this issue with a reproducible example, and we can look into it further. Here are some tips for providing a Short, Self Contained, Correct, Example and our own Troubleshooting Cypress guide.

adamsp70 commented 1 year ago

Hi there - i'm having the same issue and am trying the solution above.

But if the first thing i do is

cy.origin(
    'https://ABCD.auth0.com',
    { args: { username, password } },
    ({ username, password }) => {
      cy.visit('/')
      cy.get('input[name="email"]').type(username);
      cy.get('input[name="password"]').type(password, { log: false });
      cy.contains('button[name="submit"]', 'Continue').click();
    }
  );

Then i get the error cy.origin() requires the first argument to be a different domain than top. You passed https://ABCD.auth0.com to the origin command, while top is at https://ABCD.auth0.com

My baseUrl is set to the main site, which should be "top", no?

nvianhd commented 11 months ago

I have same issue, why does cypress care if page redirect to login page. Is cypress suppose to be test framework rather than testing browser security vulnerability?

Mostafa-Mohammadi commented 9 months ago

Same issue, any workaround?

xaviergmail commented 8 months ago

I have the same issue. You can easily reproduce this by creating a private github pages endpoint and visiting the .pages.github.io domain, which 302s to github.com/login. The origin then becomes github.com, allowing me to sign in, but when it returns to our app, we have sign-in logic that detects if a user is not signed in and redirects (client-side via window.location.href = sign-in-domain. The behavior all seems different between electron, chrome and firefox.

thatDaxton commented 6 months ago

I am facing this same exact issue. Current behavior is resetting the top domain to the redirected URL, not the URL that I am connecting to on my initial visit command. The suggested work around above does not solve this issue. Our application automatically redirects to an Okta login page if your session is not cached, as of right now I can not find a work around that is viable to retain this UI login approach...

jennifer-shehane commented 3 months ago

@xaviergmail Do you have an example github pages url that is private where this could be reproduced?

jennifer-shehane commented 3 months ago

There were workarounds outlined in this comment: https://github.com/cypress-io/cypress/issues/25373#issuecomment-1450755381

Do these workarounds not work in this case?

TomDeSmet commented 3 months ago

Hi, I'm having the exact same issue and the workarounds are not working.

jennifer-shehane commented 3 months ago

@TomDeSmet Can you provide an example we could run to demonstrate the issue?

TomDeSmet commented 3 months ago

I can’t make an example unfortunately as it’s not a public application I’m working on. But I got something working today but with some hacks. I’m planning on submitting a new issue on Monday so we can hopefully discuss.

MarkiyanPyts commented 3 months ago

Same issue when migrating to v12.6 (Next v13.1.1 & Nx v15.7.2) from v10.2 .

My application only has authenticated pages. The only way I could work around the cy.origin() requires the first argument to be a different domain than top. error is to add a new public page with a link to log in on it.

function loginViaAuth0Ui(username, password) {
  // App landing page redirects to Auth0.
  // Throws cy.origin redirect error
  // cy.visit('/');

  // Hack because tests fail if starting with redirect to Auth0
  cy.visit('/test/login');
  cy.get('a').click();

  // Login on Auth0.
  cy.origin(
    Cypress.env('auth0_domain'),
    { args: { username, password } },
    ({ username, password }) => {
      cy.get('input[name="email"]').type(username);
      cy.get('input[name="password"]').type(password, { log: false });
      cy.contains('button[name="submit"]', 'Continue').click();
    }
  );

  // Ensure Auth0 has redirected us back to the app
  cy.url().should('equal', 'http://localhost:3000/');
}

Cypress.Commands.add('loginToAuth0', (username, password) => {
  const log = Cypress.log({
    displayName: 'AUTH0 LOGIN',
    message: [`🔐 Authenticating | ${username}`],
    autoEnd: false,
  });

  log.snapshot('before');

  cy.session(
    `auth0-${username}`,
    () => {
      loginViaAuth0Ui(username, password);
    },
    {
      validate: () => {
        // Validate presence of access token in Cookie.
        // Docs: https://docs.cypress.io/api/commands/getcookie#Session-id
        cy.getCookie('appSession.0').should('exist');
        cy.getCookie('appSession.1').should('exist');
      },
      // Docs: https://docs.cypress.io/api/commands/session#Caching-session-data-across-specs
      cacheAcrossSpecs: true,
    }
  );

  log.snapshot('after');
  log.end();
});

Then for each test;

  ...
  before(() => {
    cy.loginToAuth0(
      Cypress.env('auth0_username'),
      Cypress.env('auth0_password')
    );

    cy.visit('/');
  });
  ...

The other gotcha I ran into is a Sessions cacheAcrossSpecs does not work as expected. If you are only using before(() => { ... }); you have to include { testIsolation: false } on either the test suite or global config. The other option is to use beforeEach(() => { ... }); to login before every spec.

Thanks for the tip, this one worked for me:

function loginViaAuth0Ui(username: string, password: string) {
  cy.origin(
    Cypress.env('auth0_domain'),
    { args: { username, password } },
    ({ username, password }) => {
      cy.visit(Cypress.config().baseUrl || '')

      cy.get('input#username').type(username)
      cy.contains('button[type=submit]', 'Continue').click()
      cy.get('input#password').type(password, { log: false })
      cy.contains('button[value=default]', 'Continue').click()
    },
  )

  cy.url().should('equal', Cypress.config().baseUrl + '/')
}

Cypress.Commands.add('loginToAuth0', (username: string, password: string) => {
  const log = Cypress.log({
    displayName: 'AUTH0 LOGIN',
    message: [`🔐 Authenticating | ${username}`],
    autoEnd: false,
  })
  log.snapshot('before')

  loginViaAuth0Ui(username, password)

  log.snapshot('after')
  log.end()
})

I use with https://github.com/auth0/nextjs-auth0 and / route protected with ./middleware.ts:

import { withMiddlewareAuthRequired } from '@auth0/nextjs-auth0/edge'

export default withMiddlewareAuthRequired()