This is the topic that generates the most "Cypress cannot do this" conversations in real companies, and most of them are half right. Here is the full picture.
Why it is hard. Your app is at app.company.com. Sign-in redirects the browser to login.okta.com. That is a different origin, and Cypress's test code is page code bound by the browser's same-origin policy. When the browser leaves your origin, your test cannot reach the page it lands on.
This is the architecture lecture's fact three, arriving in your sprint.
Option one: cy.origin. Cypress's supported answer. You wrap the cross-origin part in a block that runs in that origin's context:
cy.visit('/');
cy.get('[data-testid="sso-login"]').click();
cy.origin('https://login.okta.com', { args: { user } }, ({ user }) => {
cy.get('#okta-signin-username').type(user.email);
cy.get('#okta-signin-password').type(user.password);
cy.get('#okta-signin-submit').click();
});
cy.url().should('include', '/dashboard');
Note the args object. Code inside cy.origin runs in a separate context, so it cannot close over variables from your test. Anything it needs must be passed in explicitly, and it must be serialisable. This trips up everyone the first time.
This works. It is also slower, harder to debug, and it couples your suite to the identity provider's HTML, which you do not control and which changes without telling you.
Option two: get a token programmatically. Usually the better answer.
Cypress.Commands.add('loginByApi', (role) => {
const user = USERS[role];
cy.session(['api', role], () => {
cy.request({
method: 'POST',
url: `${Cypress.env('authUrl')}/oauth/token`,
body: {
grant_type: 'password',
username: user.email,
password: user.password,
client_id: Cypress.env('clientId'),
},
}).then(({ body }) => {
window.localStorage.setItem('access_token', body.access_token);
});
});
});
cy.request is not bound by same-origin, because it is an HTTP call rather than a page navigation. This is the key insight, and it is why this approach exists at all.
You skip the identity provider's UI entirely. Faster, far more stable, and it does not break when Okta redesigns their login page.
What it costs: you are no longer testing the real login flow. You need your provider to support a suitable grant type, and some enterprise setups deliberately do not. And you must know where your app stores the token, which couples the test to an implementation detail.
The decision, and this is the senior part.
Test the real SSO flow once. One test, using cy.origin, proving the actual redirect dance works. It is slow and occasionally brittle, and that is acceptable for one test. Run it in a nightly job rather than on every commit.
Authenticate every other test programmatically. Those four hundred tests are not testing login. They need a logged-in user, which is arrangement, and arrangement should be fast and reliable.
Splitting it this way gives you real coverage of the auth flow and a fast suite. Teams that refuse to split usually end up with neither.
Multi-factor authentication. MFA is designed to be impossible to automate; that is its purpose. The realistic options are a test-only account with MFA disabled, a provider test hook that bypasses it, or a deterministic TOTP secret your test can compute a code from. All three are legitimate. Trying to automate a real SMS code is not.
What to say when asked. If someone asks whether Cypress can test your SSO, the senior answer is: "Yes, through cy.origin, and we should do that once. For the rest of the suite we authenticate by API, because those tests are not about login." That answer shows you understand both the tool and the trade.
Remember this: cy.origin for the one real flow test, API tokens for everything else, and never let four hundred tests depend on a third party's HTML.
Reference: cy.origin.