Cypress with Azure AD (MSAL)
Asked Answered
T

4

8

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...

Toilet answered 26/9, 2020 at 7:3 Comment(1)
msal.client.info has base 64 encoded value of user id and user's tenant id. You can refer this code for sample Client_info value link. If you decode the value, you can get the uid and utid information. Regarding the missing claims, do you see those claims (e.g. displayableId and name) in Id token that you received from azure ad ? You can verify that it jwt.ms Url.Supat
Z
3

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

Zawde answered 8/7, 2021 at 9:27 Comment(1)
works perfectly!Retrospective
C
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!

Calcimine answered 16/10, 2020 at 1:27 Comment(4)
have you faced any issues with acquireTokenSilent? I did this and I was able to pass the login, but it fails when it tries to get a token by acquireTokenSilentJameljamerson
Token is received but on line MSAL.clientInfo, I am getting error : ClientAuthError: The client info could not be parsed/decoded correctly. Please review the trace to determine the root cause. Failed with error: Error: Invalid base64 string #67716605Clemens
This sure looks promising, but you are out of luck if you are using MSAL v2. None of those local storage keys is valid there. Now they have "{homeAccountId}-login.windows.net-{tokentype}-{clientid}-{tenentid}-{scope}" formatted multiple keys.Materfamilias
This guy here dev.to/kauppfbi_96/test-msal-based-spas-with-cypress-4goe saved my life. He exposes an example with MSAL v2, and shows how to build each local/session storage key, and their values. Also, he published the end result here: github.com/kauppfbi/kauppfbi-blogs/blob/main/apps/…Apopemptic
A
0

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.

Apopemptic answered 31/3, 2022 at 14:38 Comment(0)
S
0

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;
};
Sewell answered 18/4, 2022 at 17:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.