Azure AD in Cypress

Login through Azure AD account in your Cypress tests

cypress azure

Cypress is an amazing end-to-end testing framework that can test anything that runs in a browser. If you haven't tried using it yet then I strongly recommend you to check it out. At Kablamo, we have been using Cypress since early 2020 and after working with many Selenium-based testing frameworks, I wouldn't want to use anything else unless of course there is a particular project need.

Cypress is built for Developers & QA alike, requires minimal setup, and makes it easy to write, run and debug tests. As of now, it supports a wide variety of major browsers and is constantly rolling out new features and functionalities. Our clients have also benefited because of said features, as we are able to run the automated test suite on-demand and in the CI/CD setup to prevent any bug leaks into production, with the biggest benefit of Cypress being its developer-friendly nature.

I first came across authenticating through Azure Active Directory in one of our recent projects, a quick google search revealed an open Cypress Github issue since early 2018. There were a few suggestions in there on how to overcome this, such as using Cypress Recipe for Logging in and this article from Auth0. However none of them worked for our setup and after trying and testing a few different approaches, it turned out to be a much simpler solution in the end.

Our Azure AD login setup is pretty basic: the user hits login from our application and is redirected to https://login.microsoftonline.com/ using OAuth2.0, here the user enters their Active Directory email & password combination to authenticate and gets redirected back in our application with a valid auth/access token set in localStorage.

I first thought of just creating the test purely via UI as an end-user would interact with the application. However, when visiting external sites Cypress strongly discourages testing via third-party UI, since your application is the system under test not the third-party app.

On top of that, if you still want to login via third-party UI you will need to compromise your browser security by adding "chromeWebSecurity": false in your configuration which is not ideal for security reasons, and even then you are going through a third-party system that is completely outside your control and can contribute to flakey tests down the road.

Ideally, we should programmatically test the integration between microsoftonline and our application with cy.request() command. It is not only quicker but also more reliable and still gives us 100% confidence of testing the functionality of our app. This Azure AD authentication article helped inspire me to create the solution implemented here, along with the help of my colleagues Nathan & Ian.

So without any further background here it goes:

Following best practices, I started out with writing a custom command to login via Azure AD in cypress/support/index.ts file.

js
1
// ------------------------------------------
2
// Log in and set localStorage with auth key
3
// ------------------------------------------
4
5
Cypress.Commands.add('login', () => {
6
cy.log('-- Login via Azure AD & Set Local Storage --');
7
const options = {
8
method: 'POST',
9
url: `${Cypress.config('AUTH_URL')}` +
10
`/${Cypress.config('AUTH_TENANT_ID')}/oauth2/v2.0/token`,
11
form: true,
12
body: {
13
client_id: Cypress.config('AUTH_CLIENT_ID'),
14
scope: Cypress.config('AUTH_SCOPE'),
15
username: Cypress.config('AUTH_USERNAME'),
16
password: Cypress.config('AUTH_PASSWORD'),
17
grant_type: Cypress.config('AUTH_GRANT_TYPE'),
18
},
19
};
20
21
cy.request(options).then((response) => {
22
// here I am appending an extra type/value
23
// to accomodate our app's localStorage requirements
24
response.body.type='azure';
25
const authValue = JSON.stringify(response.body);
26
window.localStorage.setItem('auth', authValue);
27
})
28
});
29

All Cypress.config() values reside in the <root>/cypress.config file, and all the Azure AD specific values are found on your Azure AD dashboard or talk to your devOps specialist for details.

As I am using TypeScript for my Cypress tests, I will also declare it in cypress/integration/index.ts file.

ts
1
/// <reference types="Cypress" />
2
declare namespace Cypress {
3
interface cy {
4
login(): void;
5
...
6
}
7
}
8

That's it, we are now ready to write our integration test as usual,

ts
1
/// <reference types="Cypress" />
2
describe('Dashboard test', () => {
3
beforeEach(() => {
4
cy.login(); // This will call our custom login function
5
cy.visit(/);
6
...
7
});
8
9
it.only('Login and search', () => {
10
...
11
}
12
}
13

I hope this helps inspire your own solution around it, of course there are a few articles on solving the same issue online, and as always depends on the setup of your application.

References: