QA How-To
How to Use Cypress cy.origin cross domain (2026)
Master cypress cy.origin cross domain testing for SSO, exact origins, serializable args, session caching, callback limits, security, and debugging in 2026.
20 min read | 2,602 words
TL;DR
cypress cy.origin cross domain testing supports full-page navigation to another origin in the same test. Match the exact origin, interact inside its callback, transfer serializable args, and use cy.session to cache approved SSO flows.
Key Takeaways
- An origin is scheme plus hostname plus port, so subdomain, protocol, or port changes require an explicit boundary.
- Put secondary-page commands inside the matching cy.origin callback and verify the return before primary-page interaction.
- Pass outside values through serializable args because the callback is evaluated in a separate Cypress instance.
- Yield only strings or other serializable data, and keep DOM subjects inside their origin callback.
- Wrap origin-based SSO setup in cy.session for reuse, with an authenticated validation request.
- Do not use cy.origin for cross-origin iframes, popups, new tabs, insecure redirects, or uncontrolled public pages.
cypress cy.origin cross domain testing lets one Cypress test interact with pages on more than one origin while respecting the browser's same-origin boundaries. Wrap commands for the secondary origin in cy.origin(), match the scheme, hostname, subdomain, and port precisely, and pass outside values through the serializable args option.
This is the right approach for controlled multi-origin journeys such as an application redirecting to its identity provider and returning after login. It is not a universal bypass for third-party iframes, tabs, popups, insecure navigation, or pages you do not control. This guide explains the 2026 behavior, architecture, setup, security limits, SSO pattern, session caching, and debugging workflow.
TL;DR
it('signs in through a secondary identity origin', () => {
const username = Cypress.env('E2E_USERNAME')
const password = Cypress.env('E2E_PASSWORD')
cy.visit('/login')
cy.contains('button', 'Sign in').click()
cy.origin(
'https://login.example.test',
{ args: { username, password } },
({ username, password }) => {
cy.get('input[name="username"]').type(username)
cy.get('input[name="password"]').type(password, { log: false })
cy.contains('button', 'Continue').click()
},
)
cy.location('pathname').should('eq', '/dashboard')
cy.contains('h1', 'Dashboard').should('be.visible')
})
| Requirement | Supported approach |
|---|---|
| Full-page navigation to a controlled second origin | cy.origin() |
| Pass strings, arrays, or plain data into callback | options.args |
| Return text or another serializable value | Yield it from the callback |
| Reuse SSO state across tests | cy.session() around the flow |
| Interact with a cross-origin iframe | Not supported by cy.origin() |
| Control another browser tab or window | Not supported |
1. Understand cypress cy.origin cross domain Terminology
Developers often say cross domain, but browsers enforce the concept of an origin. An origin is the combination of scheme, hostname, and port. Paths, query strings, and fragments do not create a new origin.
URL comparison with https://app.example.test |
Same origin? | Reason |
|---|---|---|
https://app.example.test/settings |
Yes | Only path changed |
https://app.example.test?tab=billing |
Yes | Only query changed |
https://login.example.test |
No | Subdomain changed |
http://app.example.test |
No | Scheme changed |
https://app.example.test:8443 |
No | Port changed |
https://example.test |
No | Hostname changed |
Cypress runs in the browser and needs direct communication with the application page. When a test navigates to a different origin, commands still associated with the first execution context cannot safely reach into the new page. cy.origin() establishes a Cypress instance in the specified secondary origin and coordinates commands across that boundary.
The method is for end-to-end testing. You can visit different origins in separate tests without it. You need it when one test navigates across origins and then wants to execute Cypress commands against the new page.
Use the term origin in test names and helper APIs. A name such as loginThroughOrigin communicates the actual boundary more accurately than disableCorsLogin.
2. Know What Changed in Cypress 14 and Later
From Cypress 14, Cypress no longer injects document.domain by default. As a result, navigating between any two different origins in the same test requires cy.origin(), even when the hostnames share the same superdomain. For example, a move from app.example.test to login.example.test crosses an origin boundary.
Older suites may rely on injectDocumentDomain: true as a transition option. That behavior is deprecated and planned for removal, and it can conflict with sites using origin-keyed agent clusters. Do not design new tests around it. Migrate the relevant commands into explicit origin blocks.
The earlier experimentalSessionAndOrigin flag is also obsolete. The functionality has been enabled by default since Cypress 12. A current configuration should not copy old tutorial flags without confirming they still exist.
import { defineConfig } from 'cypress'
export default defineConfig({
e2e: {
baseUrl: 'https://app.example.test',
},
})
No special origin flag is required for the normal cy.origin() API. Keep the base URL on the primary application and specify the identity or partner origin at the call site.
When migrating, search for tests that click external links, submit forms to another host, or follow redirects between subdomains. A failure after navigation often means the next command is still executing in the primary context.
3. Write the First cypress cy.origin cross domain Test
There are two valid navigation styles. You can visit the secondary page inside cy.origin(), where that origin temporarily becomes the callback's baseUrl:
it('reads a controlled account help page', () => {
cy.visit('/settings')
cy.contains('h1', 'Settings').should('be.visible')
cy.origin('https://help.example.test', () => {
cy.visit('/account')
cy.contains('h1', 'Account help').should('be.visible')
})
cy.visit('/settings')
cy.contains('h1', 'Settings').should('be.visible')
})
You can also navigate through the UI, then enter the secondary execution context:
cy.visit('/settings')
cy.contains('a', 'Account help').click()
cy.origin('https://help.example.test', () => {
cy.contains('h1', 'Account help').should('be.visible')
})
Navigation itself can happen before or inside the callback. Interaction with the secondary page must happen inside it. After leaving the block, commands execute in the primary Cypress context, so navigate back or ensure the product redirect has returned before selecting primary-page elements.
Prefer origins you own or control in a dedicated test environment. If a link goes to an uncontrolled public site, assert the link's href instead of navigating. That produces a more deterministic test and avoids depending on third-party markup, rate limits, consent pages, and anti-automation controls.
4. Match Scheme, Hostname, Subdomain, and Port Exactly
The first argument identifies the secondary origin. It should include at least the hostname. If the scheme is omitted, Cypress defaults it to HTTPS. Query parameters are not supported in the origin argument, and a path is unnecessary even though it can be included.
Use the complete origin for clarity:
cy.origin('https://login.example.test', () => {
cy.visit('/authorize')
cy.location('hostname').should('eq', 'login.example.test')
})
https://login.example.test and https://www.login.example.test are different origins. So are HTTPS and HTTP, or default HTTPS port 443 and an explicit application on 8443. Match what the browser actually reaches after redirects.
If environment hosts differ, pass the origin through Cypress configuration and then through args only if the callback needs the string. The first argument can be an ordinary value outside the callback:
const identityOrigin = Cypress.env('IDENTITY_ORIGIN')
cy.origin(identityOrigin, () => {
cy.visit('/login')
})
Avoid building an origin from untrusted page text. Keep allowed test origins in controlled configuration. This reduces accidental navigation and makes environment review straightforward.
When Cypress reports that the expected origin does not match the application, inspect cy.url() before the failing interaction, including redirects and ports. Do not remove the scheme to hide a configuration mismatch.
5. Pass Dynamic Values with the args Option
The callback is stringified and evaluated in a separate Cypress instance. It is not a closure and cannot see variables declared or imported in the surrounding spec. Pass required data through options.args.
const tenant = 'quality-labs'
const username = Cypress.env('E2E_USERNAME')
cy.origin(
'https://login.example.test',
{ args: { tenant, username } },
({ tenant, username }) => {
cy.visit(`/authorize?tenant=${encodeURIComponent(tenant)}`)
cy.get('input[name="username"]').type(username)
},
)
Cypress transfers args through the structured clone algorithm. Strings, numbers, booleans, arrays, plain objects, and other supported cloneable data are typical choices. Functions, DOM nodes, Cypress chains, class behavior, and closure references are not a suitable interface.
Pass the minimum data required. A large application state object makes serialization slower and couples the identity interaction to unrelated details. For credentials, pass strings, mask password typing with { log: false }, and keep secrets in runtime configuration rather than committed fixtures.
Selectors can be passed as a plain object if a team needs centralized values, but do not hide every action behind an opaque structure. Commands inside the callback should remain readable enough to diagnose a provider UI change.
6. Yield Only Serializable Values to the Primary Origin
cy.origin() yields the last Cypress command's subject, or the callback return value when no Cypress commands exist. Whatever crosses back must be serializable. A DOM element is not serializable, so yielding cy.get('h1') and chaining an assertion outside the block fails.
Yield text instead:
cy.origin('https://help.example.test', () => {
cy.visit('/account')
cy.get('h1').invoke('text')
}).should('eq', 'Account help')
You can yield a plain object built from page values:
cy.origin('https://login.example.test', () => {
cy.get('[data-cy="tenant-name"]').invoke('text').then((tenantName) => ({
tenantName: tenantName.trim(),
origin: window.location.origin,
}))
}).then((details) => {
expect(details).to.deep.eq({
tenantName: 'Quality Labs',
origin: 'https://login.example.test',
})
})
Most assertions should stay in the origin where the elements exist. Yield data only when the primary side genuinely needs it. Moving every text value across the boundary adds serialization and makes tests harder to follow.
7. Implement an SSO Login Flow Safely
SSO is the main cypress cy.origin cross domain use case. The application redirects to an identity provider, Cypress fills the controlled test account, the provider redirects back, and the primary origin verifies authenticated state.
Cypress.Commands.add('loginWithSso', (username: string, password: string) => {
cy.visit('/login')
cy.contains('button', 'Sign in with Company SSO').click()
cy.origin(
'https://login.example.test',
{ args: { username, password } },
({ username, password }) => {
cy.get('input[name="username"]').type(username)
cy.get('input[name="password"]').type(password, { log: false })
cy.contains('button', 'Continue').click()
},
)
cy.location('origin').should('eq', 'https://app.example.test')
cy.location('pathname').should('eq', '/dashboard')
})
Use a provider tenant and accounts reserved for automation. Do not automate personal MFA, bypass security challenges, or weaken production authentication for tests. Coordinate an approved test path with the identity team, including cleanup, rate limits, and lockout behavior.
If login itself is not the feature under test, prefer programmatic authentication when the architecture exposes a supported test API. Keep at least one controlled browser SSO journey to verify redirects and cookies, then use session caching for the rest.
The Cypress authentication testing guide can help separate identity-provider coverage from application authorization checks.
8. Cache Cross-Origin Authentication with cy.session
cy.session() can cache and restore cookies, local storage, and session storage for a login flow. Put cy.origin() inside the session setup callback, not the reverse, because cy.session() is not allowed inside an origin callback.
Cypress.Commands.add('loginWithSso', (username: string, password: string) => {
const args = { username, password }
cy.session(
['sso', username],
() => {
cy.visit('/login')
cy.contains('button', 'Sign in with Company SSO').click()
cy.origin(
'https://login.example.test',
{ args },
({ username, password }) => {
cy.get('input[name="username"]').type(username)
cy.get('input[name="password"]').type(password, { log: false })
cy.contains('button', 'Continue').click()
},
)
cy.location('pathname').should('eq', '/dashboard')
},
{
validate() {
cy.request('/api/me').its('status').should('eq', 200)
},
},
)
})
The session key must distinguish accounts and authentication modes. Validation confirms that restored state is still accepted. After cy.session() completes, visit the page needed by the test because the session command does not guarantee the current page is your application.
Do not put raw passwords in a session key because keys can appear in logs or artifacts. The username or a safe account label is enough when combined with the authentication mode.
9. Understand Callback Restrictions and Dependencies
The origin callback cannot call cy.origin(), cy.intercept(), or cy.session(). Register intercepts at the top level before entering the callback, and place multiple origin blocks as siblings rather than nesting them.
cy.intercept('POST', '/api/auth/callback').as('authCallback')
cy.origin('https://login.example.test', () => {
// Interact with the identity page here.
cy.contains('button', 'Continue').click()
})
cy.wait('@authCallback')
The callback also cannot use ordinary ES module import() or CommonJS require() from its outer scope. Cypress.require() can load packages or support files inside the callback only when experimentalOriginDependencies is enabled. Treat that as an opt-in architecture choice, not a required setting for basic origin tests.
import { defineConfig } from 'cypress'
export default defineConfig({
experimentalOriginDependencies: true,
e2e: {
baseUrl: 'https://app.example.test',
},
})
For simple login flows, direct commands plus serializable selectors in args are often clearer than importing page objects. A class instance passed through args loses methods because structured cloning transfers data, not prototype behavior.
Custom commands loaded in the primary Cypress instance are not automatically available in a separate origin instance. If shared command code is essential, load it deliberately for that origin using the supported dependency mechanism.
10. Recognize What cy.origin Does Not Support
cy.origin() handles full-page navigation across origins. It does not enable commands inside a cross-origin iframe. Embedded payment fields, video players, comments, and hosted login widgets can use iframes whose documents remain outside direct Cypress control.
| Scenario | cy.origin() solution? |
Better strategy |
|---|---|---|
| Full browser navigation to controlled IdP | Yes | Enter IdP origin callback |
| Link to uncontrolled public documentation | Usually unnecessary | Assert href |
| Stripe field in cross-origin iframe | No | Test owned integration boundary and provider sandbox outcome |
| OAuth popup or new tab | No | Keep same-tab test flow or validate redirect URL |
| HTTPS page navigating to HTTP | Do not bypass casually | Fix insecure navigation |
| Separate origins in separate tests | Not required | Visit one origin per test |
The command also does not control another browser tab or window. Cypress is designed around one active browser context. Configure test OAuth flows for same-tab redirect when possible.
Do not set chromeWebSecurity: false as the first response to an origin failure. It weakens browser protections for the test and can hide a product security problem. Identify whether the scenario is full-page navigation, an iframe, an insecure redirect, or an uncontrolled external site, then choose the supported boundary.
The web security testing basics provide additional context for separating automation limitations from genuine security defects.
11. Debug Cross-Origin Timeouts and Mismatches
First capture the actual URL after navigation. The expected origin must match scheme, hostname, subdomain, and port. Identity providers often add tenant subdomains or redirect through an intermediate host, so an environment assumption can be wrong.
Check whether the failing command sits inside the block for the page currently displayed. A common symptom is a timeout on cy.get() immediately after a redirect because the command still belongs to the primary context.
If a variable is undefined in the callback, pass it through args. If a returned subject fails serialization, perform the assertion inside the callback or convert it to text, a number, an array, or a plain object.
Third-party pages can include framebusting code that obstructs automation. For controlled test origins, Cypress can modify obstructive third-party code when experimentalModifyObstructiveThirdPartyCode is enabled. Use the option only after confirming that framebusting is the cause and reviewing its effect on response rewriting.
export default defineConfig({
experimentalModifyObstructiveThirdPartyCode: true,
e2e: { baseUrl: 'https://app.example.test' },
})
Trace the product redirect outside Cypress too. Incorrect callback URLs, cookie SameSite attributes, CSP, provider consent, and expired accounts are application or environment issues, not selector issues. Avoid adding long fixed waits. Wait on URLs, visible controls, and authenticated API responses.
12. Design Maintainable cypress cy.origin cross domain Coverage
Keep origin transitions rare and high-value. A browser SSO flow is slower and more dependent on external infrastructure than a programmatic session setup. Use it to prove the redirect contract and a few essential account states, not as the setup for every UI assertion.
Create one custom command around the approved identity journey, but keep the destination origin, account type, and validation visible. Avoid a generic login() helper that silently switches among UI, API, and cached modes based on hidden environment conditions.
Treat the identity-provider page as an external contract. Prefer stable attributes available in a test tenant, assert only the controls necessary to complete login, and verify application authorization after return. Do not test the provider's full UI on behalf of its vendor.
Run cross-origin tests in an environment with predictable DNS, TLS, redirect URIs, and cookie policy. Parallel workers should use isolated accounts or idempotent sessions so lockouts and consent prompts do not create collisions.
Document unsupported boundaries in the test strategy. If embedded card fields cannot be directly automated, cover client integration callbacks, server webhooks, provider sandbox results, and a focused manual or vendor-approved test. Honest coverage mapping is stronger than a brittle workaround.
Interview Questions and Answers
Q: What problem does cy.origin solve?
It allows Cypress commands to execute against a second full-page origin within the same test. Cypress creates a coordinated instance in that origin so browser same-origin protections are respected rather than bypassed through ordinary DOM access.
Q: What makes two URLs different origins?
Any difference in scheme, hostname, or port creates a different origin. A path, query string, or fragment change alone does not.
Q: Why are outside variables undefined inside the callback?
The callback is stringified and evaluated in another Cypress instance, so it is not a JavaScript closure over the spec. Pass required serializable values through options.args.
Q: Can cy.origin automate a cross-origin iframe?
No. It is designed for full-page origin navigation, not commands inside cross-origin iframes. Test the owned integration boundary and the provider's supported sandbox outcome instead.
Q: Can cy.intercept be called inside cy.origin?
No. cy.intercept(), cy.session(), and nested cy.origin() calls are restricted inside the callback. Register intercepts or session wrappers at the top test level.
Q: How should SSO login be optimized across tests?
Wrap the origin-based login setup in cy.session() and add an authenticated validation request. Keep one or a few real browser login tests, then reuse validated session state for application-focused scenarios.
Q: What can cy.origin yield?
It can yield data from the last command or callback return, but the value must be serializable. Assert DOM elements inside the callback or convert them to strings or plain data before crossing the boundary.
Common Mistakes
- Treating domain and origin as interchangeable while ignoring scheme, subdomain, or port.
- Running secondary-page selectors outside the matching
cy.origin()callback. - Referencing closure variables instead of passing serializable
args. - Yielding a DOM element from the callback to the primary origin.
- Nesting
cy.origin()calls or puttingcy.session()inside the callback. - Expecting
cy.origin()to automate a cross-origin iframe, popup, or new tab. - Testing an uncontrolled public site instead of asserting the external link target.
- Re-enabling deprecated
document.domaininjection as a permanent strategy. - Logging passwords through normal
.type()command output. - Repeating the complete SSO UI flow before every application test.
Conclusion
cypress cy.origin cross domain testing is an explicit bridge between full-page browser origins. Match the exact secondary origin, keep its interactions inside the callback, move data through args, and return only serializable values.
Use the command for controlled, high-value journeys such as SSO, then cache validated sessions and test most application behavior on the primary origin. Respect the limits around iframes, tabs, insecure content, and third-party sites instead of weakening browser security to force unsupported automation.
Interview Questions and Answers
How does Cypress define an origin?
An origin is the URL scheme, hostname, and port. Paths and query strings do not change origin, but a subdomain, protocol, or port change does. I use that exact tuple when deciding whether an origin block is needed.
How does cy.origin work conceptually?
Cypress injects and coordinates an instance in the secondary origin, then serializes the callback for execution there. Data crosses through structured-clone-compatible arguments and yielded values. DOM objects remain within the origin that created them.
What changed for origin testing in Cypress 14?
Cypress stopped injecting `document.domain` by default. Different origins in one test now require `cy.origin()` even when hostnames share a superdomain. The transitional `injectDocumentDomain` option is deprecated, so new coverage should use explicit origin blocks.
How would you implement maintainable SSO coverage?
I keep a small approved browser SSO path on a controlled identity tenant, pass secrets through runtime configuration, and assert the application return. I wrap repeated setup in `cy.session()` with an authenticated validation request and test most authorization behavior on the primary app.
What commands are restricted inside the origin callback?
Nested `cy.origin()`, `cy.intercept()`, and `cy.session()` are restricted. I use sibling origin calls, register intercepts outside the callback, and wrap the origin flow inside the session setup when caching login.
How would you debug an origin mismatch?
I inspect the actual URL after every redirect and compare scheme, hostname, subdomain, and port with the expected argument. I also check whether the failing command is inside the correct callback and whether an intermediate identity host was introduced.
Why should uncontrolled external sites usually not be visited?
Their content, consent flows, rate limits, and anti-automation controls are outside the product contract. I assert the owned link target unless the team controls a stable test instance and the destination journey is truly in scope.
Frequently Asked Questions
What is cy.origin in Cypress?
It runs Cypress commands in a secondary full-page origin during one test. Cypress coordinates a separate instance for that origin so commands can interact while respecting browser same-origin boundaries.
When is cy.origin required?
It is required when a single test navigates between different origins and then interacts with the new page. Since Cypress 14, this includes different subdomains under the same superdomain by default.
What values can be passed to cy.origin args?
Pass values supported by structured cloning, such as strings, numbers, arrays, and plain objects. Do not pass DOM elements, Cypress chains, functions, or class behavior.
Can cy.origin be used for SSO?
Yes, a controlled full-page identity-provider redirect is a primary use case. Use an automation tenant, mask credentials, assert the return to the application, and cache repeated setup with `cy.session()`.
Does cy.origin work with cross-origin iframes?
No. The command handles full-page origin navigation and does not provide direct control inside cross-origin iframes. Test the integration boundary and provider sandbox outcome instead.
Can cy.intercept be called inside cy.origin?
No. `cy.intercept()`, `cy.session()`, and nested `cy.origin()` calls are restricted inside the callback. Register or wrap them at the test level around the origin call.
Why is a variable undefined inside cy.origin?
The callback is stringified and evaluated separately, so it is not a closure over the spec. Add the value to `options.args` and destructure it in the callback.