Entra ID Cypress Authentication

post

Overview

I'm no wizard when it comes to acceptance/e2e tests but I know the purpose, benefit and key testing strategys well; one thing I was recently challenge with was authenticating tests with an MSAL (@azure/msal-browser v3.x) authenticated SPA.

Approach

Now lets face it, it would be easy enough to create a mechanism that emulates typing into the input boxes on the Microsoft login pages, it is infact what the Cypress docs themselves suggest (Azure Active Directory Authentication) but this feels a britle way of going about things.

I'm going to cover off an alternative using the @azure/msal-node package.

Project Setup

This is going to be a very basic example which uses a very basic test, its really just to demonstrate the authentication mechanism rather than a robust testing strategy.

Create a new project using:

npm init

And then install the following packages:

npm install @azure/msal-node@1.18.4 cypress typescript 

NB: This approach works with 1.x MSAL Node and I will update this guide once I have a working approach for MSAL Node 2.x.

Cypress Config

Create the following files for the Cypress test:

project
│   tsconfig.json    
│   cypress.config.ts
└───cypress
│   └───e2e
│       │   spec.cy.ts

tsconfig.json

{
  "compilerOptions": {
   "target": "es5",
   "lib": ["es5", "dom"],
   "types": ["cypress"]
 },
 "include": ["**/*.ts"]
}

cypress.config.ts (replacing values in the square brackets)

import { defineConfig } from 'cypress'

export default defineConfig({
    e2e: {
        baseUrl: '[URL TO YOUR APPLICATION]',
        chromeWebSecurity: false,
        experimentalModifyObstructiveThirdPartyCode: true,
    },
    env: {
        clientId: "api://[CLIENT ID OF YOUR APP REGISTRATION]",
        clientSecret: "[CLIENT SECRET OF YOUR APP REGISTRATION]",
        tenantId: '[TENANT ID]',
        username: "[NON MFA USERNAME]",
        password: "[PASSWORD]",
    }
})

Implementation

The following test uses the ConfidentialClientApplication from msal-node to authenticate against Entra ID and populate the session storage with the required information so it automatically considers the session authenticated:

import {ConfidentialClientApplication, UsernamePasswordRequest} from '@azure/msal-node';

let realm;
let homeAccountId;
let environment;

let msalKeys = {
  accessToken: [
  ],
  idToken: [
  ],
  refreshToken: [
  ]
}

const msalConfig = {
  auth: { 
    clientId: Cypress.env('clientId'),
    clientSecret: Cypress.env('clientSecret'),
    authority: `https://login.microsoftonline.com/${Cypress.env('tenantId')}` },
};

const defaultLogin: UsernamePasswordRequest = {
  scopes: [`${Cypress.env('clientId')}/user_impersonation`, 'user.read'],
  username: Cypress.env('username'),
  password: Cypress.env('password')
};

const processCacheValue = (cacheValue, key) => {
  if(cacheValue.realm && !realm) {
    realm = cacheValue.realm;
  }
  
  if(cacheValue.environment && !environment) {
    environment = cacheValue.environment;
  }

  if(cacheValue.homeAccountId && !homeAccountId) {
    homeAccountId = cacheValue.homeAccountId;
  }

  if(homeAccountId){
    if(key.startsWith(`${cacheValue.homeAccountId}-${cacheValue.environment}-accesstoken`)) {
      msalKeys.accessToken.push(key);
    } else if (key.startsWith(`${cacheValue.homeAccountId}-${cacheValue.environment}-idtoken`)) {
      msalKeys.idToken.push(key);
    } else if (key.startsWith(`${cacheValue.homeAccountId}-${cacheValue.environment}-refreshtoken`)) {
      msalKeys.refreshToken.push(key);
    }
  }
}

describe('empty spec', () => {
  before(() => {
    const msalClient = new ConfidentialClientApplication(msalConfig);

    cy.wrap(
      msalClient.acquireTokenByUsernamePassword(defaultLogin).then((auth) => {
        if (!auth) throw Error('Authentication failed.');
        msalClient.getTokenCache()
        const tokenCache = msalClient.getTokenCache().getKVStore();

        Object.keys(tokenCache).forEach((key) => {
          const cacheValue = tokenCache[key] as any;
          const value = JSON.stringify(cacheValue);
          window.sessionStorage.setItem(key, value);

          processCacheValue(cacheValue, key)
        });

        window.sessionStorage.setItem('msal.account.keys', JSON.stringify([`${homeAccountId}-${environment}-${realm}`]));
        window.sessionStorage.setItem(`msal.token.keys.${Cypress.env('clientId')}`, JSON.stringify(msalKeys));

        return auth.accessToken;
      }
    ),
    { timeout: 20000 });
  });

  it('passes', () => {  
    cy.visit('/');
  });
});

Once you have all of that in place, run cypress open and then navigate your way through the UI to the point of running the test and it should work nicely providing everything is setup correctly.

Summary

A short and sweet demonstration of a more robust authentication mechanism to use with your e2e tests, I hope you find it useful and of course, feel free to reach out if we can be of any assistance.

Tim Hills

Tim Hills

Tim has been working in solution delivery for over 15 years and has really exceled in the industry. He has been fortunate enough to work with some high-profile clients and challenging projects which has positioned him well for turning business requirements into reality.

Registered office

Address: Arceau Solutions Ltd, Dane John Works, Gordon Rd, Canterbury, CT1 3PP

Telephone: 0208 191 7030