Blog

How to use Azure AD Single sign on with Cypress

30 Nov, 2018

The challenge

At my current assignment we recently introduced Azure active directory based single sign on(SSO). Since we are building a React app we were able to leverage the react-adal library and implementing SSO on the front-end side was a matter of hours instead of days.
This however did pose a challenge for our end-to-end tests. We aim to perform a cycle that is as complete as possible in our end-to-end tests and decided that a valid JWT token and its validation should also be part of that suite. Cypress is our end-to-end testing tool and this offers a recipe for testing applications that use single sign on. Unfortunately this recipe didn’t provide us with a working solution, mainly because the (react-)adal library utilizes cross origin iframes for (re-) authentication. Cypress also runs the application under test in an iframe so we cannot leverage the existing iframe detection offered by react-adal.

Tackling the challenge

Our solution involves 2 components: a slightly adjusted runWithAdal function and a Cypress utility that mimics the behaviour of the (react-)adal library.

The adjusted runWithAdal function

This is what the runWithAdal originally looks like:

The original runwithadal method checks whether it is running in an iframe and doesn’t start react when it is. We loosen this restriction to also run inside a frame that contains the Cypress global.
The second check is whether there is an authenticated user. Since we only request an access token and not an identity token we cannot mimic the presence of an authenticated user token. We loosen this check once again by also allowing the presence of a Cypress global.
This results in the following runWithAdal function:

Cypress utility for mimicking react-adal

Now in order to obtain the access token we use the client id and client secret. We make sure the client id and secret aren’t hard coded into our tests by reading it from environment variables using Cypress.env. This way we can have our CI environment provision the id and secret without exposing it to developers. Using cy.request we perform the call to the token API and we store the resulting token via the adal authentication context API so it can pick it up when the application starts.

The adalCofig file imported at the top of the utility is the same config as the one you are using in your web application project, so it should look something like:

Conclusion

With a few slight adjustments you can keep using your end to end tests and test the handling of authorization in your application while you run them. The login utility mimics the behaviour of the react adal library as close as possible so your web application runs the same as in production.
Happy testing!

guest
5 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Bryce Kolton
3 years ago

Just wanted to say thank you very much. I found this guide extremely helpful in setting up my own e2e environment. I ended up having to do some weirder configuration with the tokens in order to spoof some profile data. In the hopes this helps anyone, here is my code
“`javascript
import btoa from ‘btoa’;
import { AuthenticationContext } from ‘react-adal’;
import { azureTenantId, azureClientId } from ‘../../server/config/environment’;
/* eslint-disable no-underscore-dangle */
// Need to get data points from server’s environment, not src
const adalConfig = {
tenant: azureTenantId,
clientId: azureClientId,
cacheLocation: ‘localStorage’,
replyUrl: ‘/’,
endpoints: {
api: ”,
},
};
const authContext = new AuthenticationContext(adalConfig);
export default function doLogin() {
// getCachedToken also does an expiration check so we know for sure the tokens are usable
if (!authContext.getCachedToken(adalConfig.endpoints.api) || !authContext.getCachedToken(adalConfig.clientId)) {
cy.request({
method: ‘POST’,
url: ‘https://login.microsoftonline.com/[TENANT]/oauth2/token’,
form: true,
body: {
client_secret: Cypress.env(‘AZURE_CLIENT_SECRET’),
grant_type: ‘client_credentials’,
resource: adalConfig.endpoints.api,
client_id: Cypress.env(‘AZURE_CLIENT_ID’),
},
}).then((response) => {
// These steps are basically from the adal.js module’s _extractIdToken
const token = response.body.access_token;
console.log(‘raw token, ‘, token);
const decodedToken = authContext._decodeJwt(token);
const base64Decoded = authContext._base64DecodeStringUrlSafe(decodedToken.JWSPayload);
const idToken = JSON.parse(base64Decoded);
// put your custom profile data here
idToken.aud = authContext.config.clientId.toLowerCase();
const reverseIdToken = JSON.stringify(idToken);
const reverseBase64Encoded = btoa(unescape(encodeURIComponent(reverseIdToken)));
const reverseJwt = `${decodedToken.header}.${reverseBase64Encoded}.${decodedToken.JWSSig}`;
authContext._saveItem(
authContext.CONSTANTS.STORAGE.IDTOKEN,
reverseJwt,
);
// Store the token in the location where adal expects it
authContext._saveItem(
authContext.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY + adalConfig.endpoints.api,
reverseJwt,
);
authContext._saveItem(
authContext.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY + adalConfig.clientId,
reverseJwt,
);
authContext._saveItem(
authContext.CONSTANTS.STORAGE.EXPIRATION_KEY + adalConfig.endpoints.api,
response.body.expires_on,
);
authContext._saveItem(
authContext.CONSTANTS.STORAGE.EXPIRATION_KEY + adalConfig.clientId,
response.body.expires_on,
);
authContext._saveItem(
authContext.CONSTANTS.STORAGE.TOKEN_KEYS,
[adalConfig.clientId].join(authContext.CONSTANTS.RESOURCE_DELIMETER)
+ authContext.CONSTANTS.RESOURCE_DELIMETER,
);
});
}
}
describe(‘First Test’, () => {
it(‘is working’, () => {
expect(true).to.equal(false);
});
});
describe(‘second test’, () => {
it(‘works’, () => {
doLogin();
cy.visit(‘/’);
cy.visit(‘/build’);
});
});
“`

Bryce Kolton
3 years ago

Thank you very much, your code was extremely helpful! I just wanted to add some changes I had to make in order to get Cypress to login as a test user with Microsoft APIs. We needed an access token from Microsoft. We ended up solving how to POST on behalf of a user to Microsoft to get an access token, as an application, with the help of hands-on assistance from MS. We then store the access_token where the react-adal expects an id_token to be, which worked in our case.
“`javascript
/* eslint-disable no-underscore-dangle */
import { AuthenticationContext } from ‘react-adal’;
import { azTenantId, azClientId } from ‘../../server/config/environment’;
// Need to get data points from server’s environment, not src
const adalConfig = {
tenant: azTenantId,
clientId: azClientId,
cacheLocation: ‘localStorage’,
replyUrl: ‘/’,
endpoints: {
api: ”,
},
};
const authContext = new AuthenticationContext(adalConfig);
export default async function doLogin() {
// getCachedToken also does an expiration check so we know for sure the tokens are usable
if (
!authContext.getCachedToken(adalConfig.endpoints.api)
|| !authContext.getCachedToken(adalConfig.clientId)
) {
const response = await cy.request({
method: ‘POST’,
url:
‘https://login.microsoftonline.com/mercedesme.onmicrosoft.com/oauth2/token’,
// qs: { ‘api-version’: ‘1.0’ }, // uncomment if your consuming resource expects the ‘aud’ to have a prefix of ‘sn:’
headers: {
‘cache-control’: ‘no-cache’,
‘content-type’:
‘multipart/form-data; boundary=—-WebKitFormBoundary7MA4YWxkTrZu0gW’,
},
form: true,
body: {
grant_type: ‘password’,
response_type: ‘code’,
client_secret: ‘[[yourappsclientsecret]]’,
client_id: ‘[[yourappsclientid]]’,
username: ‘[[yourtestuzseremail]]’,
password: ‘[[yourtestuserpassword]]!’,
scope: ‘openid’,
resource: ‘[[some-resource-id]]’,
},
});
// Store the token and data in the location where adal expects it
authContext._saveItem(authContext.CONSTANTS.STORAGE.IDTOKEN, response.body.access_token);
authContext._saveItem(
authContext.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY + adalConfig.endpoints.api,
response.body.access_token,
);
authContext._saveItem(
authContext.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY + adalConfig.clientId,
response.body.access_token,
);
authContext._saveItem(
authContext.CONSTANTS.STORAGE.EXPIRATION_KEY + adalConfig.endpoints.api,
response.body.expires_on,
);
authContext._saveItem(
authContext.CONSTANTS.STORAGE.EXPIRATION_KEY + adalConfig.clientId,
response.body.expires_on,
);
authContext._saveItem(
authContext.CONSTANTS.STORAGE.TOKEN_KEYS,
[adalConfig.clientId].join(authContext.CONSTANTS.RESOURCE_DELIMETER)
+ authContext.CONSTANTS.RESOURCE_DELIMETER,
);
}
}
“`

Fabio Eloy
Fabio Eloy
2 years ago

Hi Mike,
How I get the “Resource id of the api” ? I have an Oracle APEX application that uses Azure to login. I am really new into cypress and I am completed lost.
Thank you

Sam
Sam
1 year ago

Any one tried this with msal? I am facing issues using msal and setting up the tokens in local storage

Ozgur
Ozgur
1 year ago

hi,
somehow i couldnt get it running.
do you have more documentation maybe ?
thanks in advance

Explore related posts