Entra ID Cypress Authentication
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.