Skip to content
  • Testing
  • Cypress

Test passwordless flows

This guide shows you how to automate passwordless authentication testing with Cypress by intercepting the one-time code email. Learn more about testing passwordless flows with Kinde.

The examples use Mailosaur, but any inbox service with an API for reading emails programmatically (for example MailSlurp or Mailtrap) works the same way: give each test a unique inbox, wait for the code email, extract the code, and enter it in the form.

Kinde one-time codes are always 6 digits, are generated fresh for every request, and expire after 2 hours (120 minutes) by default (configurable per environment). There is no static “test code”: intercepting the real email is the supported way to automate these flows.

Keep OTP tests lean

Link to this section

Waiting on real email delivery is the slowest and most failure-prone part of any test suite. We recommend covering the passwordless UI with a small number of smoke tests, and authenticating the rest of your suite another way: reuse authenticated state with cy.session (see Test authenticated features), use an email + password test user, or use a machine-to-machine (M2M) application for API-level setup.

Setup Mailosaur

Link to this section
  1. Sign in to your Mailosaur account.

  2. Copy the Server ID from the dashboard.

  3. Go to API Keys > select Create standard key and copy the value.

  4. Install the Mailosaur client in your Cypress project:

    Terminal window
    npm i -D mailosaur
  5. Update your cypress.env.json file with your Mailosaur API key and Server ID:

    {
    // ...your other environment variables...
    "MAILOSAUR_API_KEY": "your-api-key",
    "MAILOSAUR_SERVER_ID": "your-server-id"
    }
  6. Create a helper utility file cypress/support/utils/mailosaur.js:

    Terminal window
    mkdir -p cypress/support/utils
    touch cypress/support/utils/mailosaur.js
  7. Add the following code to mailosaur.js:

    cypress/support/utils/mailosaur.js
    const MailosaurClient = require('mailosaur')
    async function getOTPFromEmail(email, apiKey, serverId, receivedAfter) {
    const mailosaur = new MailosaurClient(apiKey)
    // Pass receivedAfter (an ISO string) when a test reuses an inbox across
    // runs, so you never pick up a stale code from a previous run's email.
    const message = await mailosaur.messages.get(
    serverId,
    { sentTo: email },
    { receivedAfter: receivedAfter ? new Date(receivedAfter) : undefined }
    )
    // Kinde codes are always exactly 6 digits
    const otpMatch = message.text?.body?.match(/\b(\d{6})\b/)
    const otpCode = otpMatch?.[1]
    if (!otpCode) {
    throw new Error("Could not extract OTP code from email")
    }
    return {
    emailId: message.id,
    otpCode,
    }
    }
    async function deleteEmail(emailId, apiKey) {
    const mailosaur = new MailosaurClient(apiKey)
    await mailosaur.messages.del(emailId)
    }
    module.exports = {
    getOTPFromEmail,
    deleteEmail,
    }
  8. Add the following tasks to your cypress.config.js file:

    cypress.config.js
    const { defineConfig } = require('cypress')
    const { getOTPFromEmail, deleteEmail } = require('./cypress/support/utils/mailosaur')
    module.exports = defineConfig({
    e2e: {
    baseUrl: 'http://localhost:3000',
    setupNodeEvents(on, config) {
    on('task', {
    async getOTPFromEmail({ email, apiKey, serverId, receivedAfter }) {
    return await getOTPFromEmail(email, apiKey, serverId, receivedAfter)
    },
    async deleteEmail({ emailId, apiKey }) {
    await deleteEmail(emailId, apiKey)
    return null
    },
    })
    },
    },
    })

Write resilient tests

Link to this section

Two things to know before writing selectors against Kinde’s hosted pages:

  • The screens around the code step evolve. For example, enabling passkeys adds a passkey option to the sign-in screen, and passing login_hint can pre-fill (or skip) the email step. Prefer selectors tied to stable attributes over positional ones, and keep the authentication methods on your test environment limited to the ones you are actually testing.
  • The code entry step itself is stable. The code input is input[name="p_confirmation_code"] (labelled “Code”) and accepts exactly 6 digits, and the submit control is [data-kinde-button-variant="primary"].

Test passwordless sign-up flow

Link to this section

To test the passwordless sign-up flow, enable the Email + code option in authentication methods in your Kinde application.

  1. Create a new test file:

    Terminal window
    touch cypress/e2e/passwordless-signup.cy.js
  2. Add the following code:

    cypress/e2e/passwordless-signup.cy.js
    const serverId = Cypress.env('MAILOSAUR_SERVER_ID')
    describe('Passwordless Authentication Flows', () => {
    it('user can sign up with email and code', () => {
    // Generate unique email for this test run
    const timestamp = Date.now()
    const testEmail = `test-${timestamp}@${serverId}.mailosaur.net`
    cy.visit(Cypress.env('TEST_APP_URL'))
    cy.get('[data-testid="sign-up-button"]').click()
    // Wait for Kinde sign-up page
    cy.url().should('include', 'kinde.com')
    // Fill sign-up form
    cy.origin(Cypress.env('KINDE_ISSUER_URL'), { args: { testEmail } }, ({ testEmail }) => {
    cy.get('input[name="p_first_name"]').type('John')
    cy.get('input[name="p_last_name"]').type('Doe')
    cy.get('input[name="p_email"]').type(testEmail)
    cy.get('input[name="p_has_clickwrap_accepted"]').check() // Accept terms and conditions
    cy.get('button[type="submit"]').click()
    })
    // Wait for OTP email and extract code
    cy.task('getOTPFromEmail', {
    email: testEmail,
    apiKey: Cypress.env('MAILOSAUR_API_KEY'),
    serverId: Cypress.env('MAILOSAUR_SERVER_ID')
    }).then((result) => {
    cy.origin(Cypress.env('KINDE_ISSUER_URL'), { args: { otpCode: result.otpCode } }, ({ otpCode }) => {
    cy.get('input[name="p_confirmation_code"]').type(otpCode)
    cy.get('[data-kinde-button-variant="primary"]').click()
    })
    // Wait for successful auth
    cy.url().should('include', new URL(Cypress.env('TEST_APP_URL')).hostname)
    cy.get('[data-testid="user-profile"]').should('be.visible')
    // Clean up OTP email
    cy.task('deleteEmail', {
    emailId: result.emailId,
    apiKey: Cypress.env('MAILOSAUR_API_KEY')
    })
    })
    })
    })

Test passwordless sign-in flow

Link to this section

To test the passwordless sign-in flow:

  1. Enable the Email + code option in Authentication methods.

  2. Sign up for a new test user using the Mailosaur domain (e.g., test-123@your-server-id.mailosaur.net).

  3. Save the test user email to your cypress.env.json file as TEST_USER_EMAIL.

  4. Create a new test file:

    Terminal window
    touch cypress/e2e/passwordless-signin.cy.js
  5. Add the following code:

    cypress/e2e/passwordless-signin.cy.js
    describe('Passwordless Authentication Flows', () => {
    it('user can sign in with email and code', () => {
    // This test reuses one inbox on every run, so record the start time and
    // pass it to the task to avoid reading a code from a previous run.
    const receivedAfter = new Date().toISOString()
    cy.visit(Cypress.env('TEST_APP_URL'))
    cy.get('[data-testid="sign-in-button"]').click()
    // Wait for Kinde sign-in page
    cy.url().should('include', 'kinde.com')
    // Fill sign-in form
    cy.origin(Cypress.env('KINDE_ISSUER_URL'), () => {
    cy.get('input[name="p_email"]').type(Cypress.env('TEST_USER_EMAIL'))
    cy.get('[data-kinde-button-variant="primary"]').click()
    })
    // Wait for OTP email and extract code (only emails received after this test started)
    cy.task('getOTPFromEmail', {
    email: Cypress.env('TEST_USER_EMAIL'),
    apiKey: Cypress.env('MAILOSAUR_API_KEY'),
    serverId: Cypress.env('MAILOSAUR_SERVER_ID'),
    receivedAfter
    }).then((result) => {
    cy.origin(Cypress.env('KINDE_ISSUER_URL'), { args: { otpCode: result.otpCode } }, ({ otpCode }) => {
    cy.get('input[name="p_confirmation_code"]').type(otpCode)
    cy.get('[data-kinde-button-variant="primary"]').click()
    })
    // Wait for successful auth
    cy.url().should('include', new URL(Cypress.env('TEST_APP_URL')).hostname)
    cy.get('[data-testid="user-profile"]').should('be.visible')
    // Clean up OTP email
    cy.task('deleteEmail', {
    emailId: result.emailId,
    apiKey: Cypress.env('MAILOSAUR_API_KEY')
    })
    })
    })
    })

Test sign-up flow with email and password

Link to this section

We have included this example because OTP verification is required for all new users in Kinde regardless of the authentication method used.

This example uses the Kinde email + password authentication method to sign up a new test user.

  1. Create a new test file:

    Terminal window
    touch cypress/e2e/email-password-signup.cy.js
  2. Add the following code:

    cypress/e2e/email-password-signup.cy.js
    const serverId = Cypress.env('MAILOSAUR_SERVER_ID')
    describe('Authentication Flows', () => {
    it('user can sign up with email and password', () => {
    // Generate unique email for this test run
    const timestamp = Date.now()
    const testEmail = `test-${timestamp}@${serverId}.mailosaur.net`
    cy.visit(Cypress.env('TEST_APP_URL'))
    cy.get('[data-testid="sign-up-button"]').click()
    // Wait for Kinde sign-up page
    cy.url().should('include', 'kinde.com')
    // Fill sign-up form
    cy.origin(Cypress.env('KINDE_ISSUER_URL'), { args: { testEmail } }, ({ testEmail }) => {
    cy.get('input[name="p_first_name"]').type('John')
    cy.get('input[name="p_last_name"]').type('Doe')
    cy.get('input[name="p_email"]').type(testEmail)
    cy.get('input[name="p_has_clickwrap_accepted"]').check() // Accept terms and conditions
    cy.get('[data-kinde-button-variant="primary"]').click()
    })
    // Wait for OTP email
    cy.task('getOTPFromEmail', {
    email: testEmail,
    apiKey: Cypress.env('MAILOSAUR_API_KEY'),
    serverId: Cypress.env('MAILOSAUR_SERVER_ID')
    }).then((result) => {
    cy.origin(Cypress.env('KINDE_ISSUER_URL'), { args: { otpCode: result.otpCode } }, ({ otpCode }) => {
    cy.get('input[name="p_confirmation_code"]').type(otpCode)
    cy.get('[data-kinde-button-variant="primary"]').click()
    })
    // Enter password
    cy.origin(Cypress.env('KINDE_ISSUER_URL'), () => {
    cy.get('input[name="p_first_password"]').type(Cypress.env('TEST_USER_PASSWORD'))
    cy.get('input[name="p_second_password"]').type(Cypress.env('TEST_USER_PASSWORD'))
    cy.get('[data-kinde-button-variant="primary"]').click()
    })
    // Wait for successful auth
    cy.url().should('include', new URL(Cypress.env('TEST_APP_URL')).hostname)
    cy.get('[data-testid="user-profile"]').should('be.visible')
    // Clean up OTP email
    cy.task('deleteEmail', {
    emailId: result.emailId,
    apiKey: Cypress.env('MAILOSAUR_API_KEY')
    })
    })
    })
    })
  1. Use the following command:

    Terminal window
    npx cypress open
  2. Select the test spec to run.

    You should now see the Cypress tests passing in the browser.

    user can sign up with email and code

  3. Go to your Kinde dashboard > Users to find the test users you created.

    Test users in Kinde dashboard