Using Cypress to test Azure Active Directory protected Single-Page Applications

Frank Lores-Penalver
Version 1
Published in
5 min readAug 17, 2022

Testing Single-Page Applications that use the OAuth2 Authorisation Code flow can be challenging because the user is redirected from the application's main page to the authorisation server. In this article, we will discuss how to overcome this challenge when testing Azure Active Directory-protected Single-Page Applications using Cypress.

These are the components for the test scenario:

Cypress is an all-in-one, end-to-end testing framework and assertion library which allows you to automate tests for the user interface of your web application.

Azure AD is a cloud-based identity and access management service which implements the OAuth2 specification.

MSAL.js is a javascript library, written and maintained by Microsoft that allows SPAs to authenticate users with Azure AD.

The authorisation flow recommended by OAuth2 for SPAs is the Authorisation Code flow which is summarised below:

MSAL.js acquires tokens using Authorisation Code with PKCE flow.
  1. The user navigates to the single-page app.
  2. If the user is not authenticated, they are redirected to Azure AD to authenticate.
  3. After successful authentication, Azure AD returns an authorisation code to the SPA.
  4. The SPA exchanges the authorisation code for an id token, an access token and a refresh token.
  5. The SPA securely stores the tokens in the local storage.
  6. The SPA passes the access token on request to the backend API, also protected by Azure AD.

When we attempt to write Cypress automated tests for an Azure AD-protected Single-Page Application (SPA), we need to replicate the process of acquiring tokens without a user present. This, however, presents a challenge…

Related reads: Why Choose Version 1 as your Microsoft Partner?

We first considered writing a test for the login page, however, Cypress discourages testing third-party external sites (Azure AD login page) for several reasons such as:

  1. We have no control over the design of the said page, and our test could break if the 3rd party changes the layout/elements of the page.
  2. The 3rd party site may be having issues outside of our control.

Secondly, we considered trying to replace the redirection with our code, login and get the access token before we run our tests; there are several articles on this subject on how to achieve this. Still, these articles describe using either the OAuth Implicit or the OAuth Client Credentials flow; both are unsuitable for our scenario for the reasons outlined below:

  • The OAuth Implicit flow presents security issues and is no longer recommended for use.
  • The OAuth Client Credentials flow does not give us the necessary claims that our API backend needs, specifically scope claims.

Finally, we settled on using the Resource Owner Password Credentials (ROPC) flow. This flow is not without its drawbacks, and Microsoft does not recommend its use unless there is a high degree of trust in the application. However, we considered automated Cypress tests to be secure, as there are no users present.

The approach we followed replaces MSAL.js for acquiring the token with our code and saves the tokens into local storage in the shape/format that MSAL.js expects, hence tricking the MSAL.js library into thinking that the user has already authenticated and not redirecting the user to the Microsoft login page.

The steps of the Resource Owner Password Credentials flow are summarised below:

Cypress acquiries tokens using ROPC Flow
  1. Cypress script sends authentication details to Azure AD’s token endpoint using the ROPC flow.
  2. Azure AD returns an id token, an access token and a refresh token.
  3. Cypress script stores the tokens in local storage in the format expected by the MSAL.js library.
  4. Cypress runs automated test scripts on the UI.
  5. UI, using MSAL.js, collects the tokens from local storage.
  6. UI passes the access token on the requests sent to the backend API.

To follow this approach you will need the following information:

  • An Azure AD account with MFA disabled, this account should only have the minimum access required to test the application.
  • Azure AD Application details for your UI: Directory Id/Tenant Id, Application Id/Client Id, Client Secret.
  • Azure AD Application details for your API: Directory Id/Tenant Id, Application Id/Client Id, App ID URI and Scope ID.

In your Cypress project add json configuration file to save the authentication settings:

Next, create a support module for authorisation, this module will handle the authentication and will save the authorisation details to local storage:

The format for the key-value pairs added to local storage are as follows:

Id Token key:
[homeAccountId]-[environment]-idtoken-[clientId]-[tenantId]-
Id Token value:
clientId: [your app id]
credentialType: "IdToken"
environment: "login.windows.net"
homeAccountId: [oid or sid].[tid] //these values come from the id-token
realm: [tid]
--------------------------------------------------------------------Access Token key:
[homeAccountId]-[environment]-accesstoken-[clientId]-[tenantId]-[api-scope (lowercase)]
Access Token value:
cachedAt: [timestamp]
clientId: [your app id]
credentialType: "AccessToken"
environment: "login.windows.net"
expiresOn: [timestamp + expiresIn] //expiresIn is the expire_in from the token response
extendedExpiresOn: [timestamp + extendedExpiresIn] //ext_expires_in from the token response
homeAccountId: [oid or sid].[tid], //these values come from the access_token
realm: [tid]
secret: [access_token] //from token response
target: [api_scope]
tokenType: "Bearer"
--------------------------------------------------------------------Refresh Token key:
[homeAccountId]-[environment]-refreshtoken-[clientId]--
Refresh Token value:
clientId: [your app id]
credentialType: "RefreshToken"
environment: "login.windows.net"
homeAccountId: [oid or sid].[tid], //these values come from the access token or id token
secret: [refresh_token] //from token response
--------------------------------------------------------------------Account key:
[homeAccountId]-[environment]-[tenantId]
Account value:
authorityType:
"MSSTS"
clientInfo: ""
environment: "login.windows.net"
homeAccountId: [oid or sid].[tid], //these values come from the access token or id token
idTokenClaims: map of id_token claims (aud, exp, iat, iss, name, nbf, nonce, oir, preferred_username, rh, sub, tid, uti, ver)
localAccountId: [oid or sid]
name: name claim from id_token
realm: [tid]
username: preferred_name claim from id_token

The information from which these values are derived are found in the id_token and access_token returned from the authentication process:

id_token
access_token

Next, we can create a cypress command to invoke our login function from cypress tests:

Finally, write your tests invoking the login command:

The main inspiration for this article is this excellent talk by Joonas Westlin.

All done!

References

About the Author:
Frank Lores-Penalver is a solution Architect here at Version 1.

--

--

Frank Lores-Penalver
Version 1

Software developer, software architect, and life-long learner.