'Cypress with Azure AD (MSAL)
I'm new to both Cypress and Azure AD, but I've been following the steps described here to create Cypress tests on an existing Angular app that uses Azure AD. It mentions that they are using ADAL, but our app uses MSAL, which it says should be similar. However, I'm struggling to get it to work. Here's my login function so far:
const tenant = 'https://login.microsoftonline.com/{my_tenant_id}/';
const tenantUrl = `${tenant}oauth2/token`;
const clientId = '{my_app_id}';
const clientSecret = '{my_secret}';
const azureResource = 'api://{my_app_id}';
const knownClientApplicationId = '{client_application_id_from_manifest}';
const userId = '{user_identifier}';
export function login() {
cy.request({
method: 'POST',
url: tenantUrl,
form: true,
body: {
grant_type: 'client_credentials',
client_id: clientId,
client_secret: clientSecret,
resource: azureResource
}
}).then(response => {
const Token = response.body.access_token;
const ExpiresOn = response.body.expires_on;
const key = `{"authority":"${tenant}","clientId":"${knownClientApplicationId}","scopes":${knownClientApplicationId},"userIdentifier":${userId}}`;
const authInfo = `{"accessToken":"${Token}","idToken":"${Token}","expiresIn":${ExpiresOn}}`;
window.localStorage.setItem(`msal.idtoken`, Token);
window.localStorage.setItem(key, authInfo);
}
Cypress.Commands.add('login', login);
When I run this, an access token is returned. When I examine the local storage after a normal browser request, it has many more fields, such as msal.client.info
(the authInfo
value in the code above should also contain this value), but I've no idea where to get this information from.
The end result is that the POST request seems to return successfully, but the Cypress tests still consider the user to be unauthenticated.
The existing app implements a CanActivate
service that passes if MsalService.getUser()
returns a valid user. How can I convince this service that my Cypress user is valid?
Update:
After some experimentation with the local storage values, it looks like only two values are required to get past the login:
msal.idtoken
msal.client.info
The first I already have; the second one I'm not sure about, but it appears to return the same value every time. For now, I'm hard coding that value into my tests, and it seems to work somewhat:
then(response => {
const Token = response.body.access_token;
window.localStorage.setItem(`msal.idtoken`, Token);
window.localStorage.setItem(`msal.client.info`, `{my_hard_coded_value}`);
});
The only minor issue now is that the MsalService.getUser()
method returns slightly different values than the app is expecting (e.g. displayableId
and name
are missing; idToken.azp
and idToken.azpacr
are new). I'll investigate further...
Solution 1:[1]
This solution got me a successful request but I still couldn't get past the login screen on my app, after some searching I found this great tutorial video that got me past the login screen! I am using the msal-react
& msal-browser
npm packages.
https://www.youtube.com/watch?v=OZh5RmCztrU
Repo for the code is here:
https://github.com/juunas11/AzureAdUiTestAutomation/tree/main/UiTestAutomation.Cypress/cypress
Solution 2:[2]
I want to thank you for all of the groundwork you've done to figure out which variables to set when working with MSAL! I think I can help with figuring out where clientInfo comes from. It looks like it is generated from the clientId, which explains why it is always the same value:
static createClientInfoFromIdToken(idToken:IdToken, authority: string): ClientInfo {
const clientInfo = {
uid: idToken.subject,
utid: ""
};
return new ClientInfo(CryptoUtils.base64Encode(JSON.stringify(clientInfo)), authority);
}
See source here: https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/70b87bcd27bfe67789c92b9bc046b45733f490ed/lib/msal-core/src/ClientInfo.ts
I was able to use your code and just added import * as MSAL from "@azure/msal-browser"
and window.localStorage.setItem(`msal.client.info`, MSAL.clientInfo);
Worked like a charm for me!
Solution 3:[3]
Just in case someone else is facing the issue of not knowing exactly what content is required in the keys/values of the local storage entries, this guy here explains it pretty well.
He does the whole walkthrough, explaining how to build the local storage entries by decoding some parts of the token, and also shows how to integrate this steps with Cypress so that you can programmatically login into MSAL, in a pretty tidy way.
Solution 4:[4]
Using the two code examples above from juunas and kauppfbi kind of worked for me. The issue I had was that rather than invoking the ROPC flow, the first request would end up at the standard login screen. This would usually happen when cypress first opened up and on the very first executed test. Subsequent test would run as expected.
Changing the login() function as follows corrected the problem for me:
export const login = (cachedTokenResponse) => {
let tokenResponse = null;
let chainable;
if (!cachedTokenResponse) {
chainable = cy.request({
url: authority + "/oauth2/v2.0/token",
method: "POST",
body: {
grant_type: "password",
client_id: clientId,
client_secret: clientSecret,
scope: ["openid profile"].concat(apiScopes).join(" "),
username: username,
password: password,
},
form: true,
})
.then((response) => {
injectTokens(response.body);
tokenResponse = response.body;
})
.visit("/")
.then(() => {
return tokenResponse;
});
} else {
chainable = cy.visit("/")
.then(() => {
return tokenResponse;
});
}
return chainable;
};
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Dharman |
Solution 2 | Kimberly Ly |
Solution 3 | aleclara95 |
Solution 4 | James Blake |