Learn how to log into different environments with Cypress, protecting sensitive data, such as username and password
In another blog post of the Pinches of Cypress series, I wrote about how to change the baseUrl via the command line with Cypress.
But what about sensitive data, such as authentication credentials within the same application, when the application is deployed across different environments? That's precisely what I'll teach you in this content!
Let's imagine an application deployed in three different environments, each with its specific credentials:
- Local (your computer)
- Staging
- Production
The user's email and password are required to log into the application (in any of the environments).
Let's also assume that in the Cypress configuration file (cypress.config.js), the baseUrl points by default to the local development environment, as shown below:
// cypress.config.js
const { defineConfig } = require('cypress')
module.exports = defineConfig({
allowCypressEnv: false,
e2e: {
baseUrl: 'http://localhost:3000'
}
})
Note the allowCypressEnv: false setting. This disables the use of Cypress.env() to read environment variables in the browser, pushing you toward the more secure cy.env() command instead (more on this shortly).
However, the staging and production environments have the following URLs, respectively:
https://example.staging.comhttps://example.com
Let's also assume that in the package.json file, we have the following scripts for running tests in each specific environment:
"scripts": {
"test": "cypress run",
"test:staging": "cypress run --config baseUrl=https://example.staging.com --expose environment=staging",
"test:prod": "cypress run --config baseUrl=https://example.com --expose environment=prod"
}
The test script runs the tests in the local environment because the baseUrl is not overridden.
In the test:staging and test:prod scripts, the baseUrl is overridden via the command line. Additionally, we use the --expose flag to pass a variable named environment with a value that identifies each environment (staging and prod). Unlike --env, --expose is intended for non-sensitive configuration values that you want to access synchronously via Cypress.expose().
In the local environment, we can have an unversioned file (included in the .gitignore file) called cypress.env.json, which would have the following structure:
{
"LOCAL_USER": {
"email": "local@user.com",
"password": "the-password-of-the-above-user"
},
"STAGING_USER": {
"email": "some-user@example.staging.com",
"password": "the-password-of-the-above-user"
},
"PROD_USER": {
"email": "another-user@example.com",
"password": "the-password-of-the-above-user"
}
}
Note: I also recommend creating a versioned file called cypress.env.example.json with example values so other team members can use it as a template for creating their own unversioned cypress.env.json files.
Note 2: In a continuous integration environment, such values (STAGING_USER and PROD_USER) could be set as secrets, with the prefix CYPRESS_, i.e., CYPRESS_STAGING_USER and CYPRESS_PROD_USER, with their respective values.
The security distinction: Cypress.expose vs cy.env
Cypress provides two different APIs for accessing external values, and understanding the difference matters for security:
-
Cypress.expose(key)— synchronous, intended for non-sensitive configuration values like the current environment name. Values are passed via the--exposeCLI flag. -
cy.env([keys])— asynchronous, intended for sensitive data like credentials. It takes an array of key names and yields only the variables you request, keeping them out of the general browser state. Values come fromcypress.env.jsonorCYPRESS_*environment variables.
Now that we have the credentials and understand the APIs, let's implement a custom command for logging in based on the environment where the tests are executed.
// cypress/support/commands.js
Cypress.Commands.add('login', () => {
const environment = Cypress.expose('environment')
cy.log(`Logging into the ${environment ? environment : 'local'} environment`)
let userKey = 'LOCAL_USER'
if (environment === 'staging') userKey = 'STAGING_USER'
if (environment === 'prod') userKey = 'PROD_USER'
cy.visit('/login')
cy.env([userKey]).then((vars) => {
cy.get('[data-cy="emailField"]')
.should('be.visible')
.type(vars[userKey].email, { log: false })
cy.get('[data-cy="passwordField"]')
.should('be.visible')
.type(vars[userKey].password, { log: false })
cy.contains('button', 'Login')
.should('be.visible')
.click()
})
})
In the login custom command:
-
Cypress.expose('environment')synchronously reads the non-sensitive environment name passed via--expose. - Two
ifstatements set the correctuserKeydepending on the environment, defaulting toLOCAL_USER. -
cy.env([userKey])securely fetches only the credential object needed for that environment, and.then()gives access to the values within Cypress's command chain. - The email and password are typed with
{ log: false }to keep them out of the Cypress command log.
Additionally, we log the phrase "Logging into the [environment] environment" in the Cypress command log, depending on the environment where the tests are running. If the environment variable is passed, we use it; otherwise, the default is the local value.
So, if the value of the environment variable is, for example, staging, the following would be "printed" in the Cypress command log:
Logging into the staging environment.
And the login test would look something like this:
// cypress/e2e/login.cy.js
it('logs in', () => {
cy.login()
cy.get('[data-cy="avatar"]')
.should('be.visible')
})
As you can see, in the actual test, we only call the custom command cy.login, which authenticates the application with the correct credentials.
That's it! I hope you learned something new.
For more details on how Cypress works, I recommend reading the official documentation.
Did you like the content? Leave a comment.
This blog post was originally published in Portuguese at the Talking About Testing blog.
Want to go deeper?
I built the Cypress Simulator — a hands-on course designed to take you from your first test to a full end-to-end testing workflow with confidence.
You'll learn how to test real user interactions, catch accessibility issues early, and run your tests automatically in CI/CD pipelines — through 20 interactive lessons, coding challenges, and quizzes.
Top comments (2)
Thanks for article, really helpful, now I do have a doubt, hopefully you can help me understand
So you mention to use for instance
CYPRESS_STAGING_USERin let's say github actions, I know thatCYPRESS_will be removed once it is read by cypress but my two questions are:Should I get the secrets like this?
run: echo "CYPRESS_STAGING_USER=${{secrets.CYPRESS_STAGING_USER}}" >> $GITHUB_ENV?once I got the env in a continuos integration setup, and in this case where the cypress.env.json is not versioned there, where do these values are coming from if they are not in the repo?
.type(Cypress.env('user').email.type(Cypress.env('user').password
No, your secret on GitHub action will already be in a JSON format.