I have the following code working and I’m able to login with username and password. I am working with Cypress to login to a webapp with MSAL.
In the e2e Testfile:
describe('Login with MSAL as xxUsername', () => { beforeEach(() => { cy.LoginWithMsal() })
Command.js:
import { login } from "./auth"; let cachedTokenExpiryTime = new Date().getTime(); let cachedTokenResponse = null; Cypress.Commands.add("LoginWithMsal", () => { if (cachedTokenExpiryTime <= new Date().getTime()) { cachedTokenResponse = null; } return login(cachedTokenResponse).then((tokenResponse) => { cachedTokenResponse = tokenResponse; cachedTokenExpiryTime = new Date().getTime() + 50 * 60 * 1000; }); });
Imported auth.js
/// <reference types="cypress" /> import { decode } from "jsonwebtoken"; import authSettings from "./authsettings.json"; const { authority, clientId, clientSecret, apiScopes, username, password, } = authSettings; const environment = "login.windows.net"; const buildAccountEntity = ( homeAccountId, realm, localAccountId, username, name ) => { return { authorityType: "MSSTS", // This could be filled in but it involves a bit of custom base64 encoding // and would make this sample more complicated. // This value does not seem to get used, so we can leave it out. clientInfo: "", homeAccountId, environment, realm, localAccountId, username, name, }; }; const buildIdTokenEntity = (homeAccountId, idToken, realm) => { return { credentialType: "IdToken", homeAccountId, environment, clientId, secret: idToken, realm, }; }; const buildAccessTokenEntity = ( homeAccountId, accessToken, expiresIn, extExpiresIn, realm, scopes ) => { const now = Math.floor(Date.now() / 1000); return { homeAccountId, credentialType: "AccessToken", secret: accessToken, cachedAt: now.toString(), expiresOn: (now + expiresIn).toString(), extendedExpiresOn: (now + extExpiresIn).toString(), environment, clientId, realm, target: scopes.map((s) => s.toLowerCase()).join(" "), // Scopes _must_ be lowercase or the token won't be found }; }; const injectTokens = (tokenResponse) => { const idToken = decode(tokenResponse.id_token); const localAccountId = idToken.oid || idToken.sid; const realm = idToken.tid; const homeAccountId = `${localAccountId}.${realm}`; const username = idToken.preferred_username; const name = idToken.name; const accountKey = `${homeAccountId}-${environment}-${realm}`; const accountEntity = buildAccountEntity( homeAccountId, realm, localAccountId, username, name ); const idTokenKey = `${homeAccountId}-${environment}-idtoken-${clientId}-${realm}-`; const idTokenEntity = buildIdTokenEntity( homeAccountId, tokenResponse.id_token, realm ); const accessTokenKey = `${homeAccountId}-${environment}-accesstoken-${clientId}-${realm}-${apiScopes.join( " " )}`; const accessTokenEntity = buildAccessTokenEntity( homeAccountId, tokenResponse.access_token, tokenResponse.expires_in, tokenResponse.ext_expires_in, realm, apiScopes ); localStorage.setItem(accountKey, JSON.stringify(accountEntity)); localStorage.setItem(idTokenKey, JSON.stringify(idTokenEntity)); localStorage.setItem(accessTokenKey, JSON.stringify(accessTokenEntity)); }; export const login = (cachedTokenResponse) => { let tokenResponse = null; let chainable = cy.visit("https://xxxxxxxxxxxxx.nl/"); if (!cachedTokenResponse) { chainable = chainable.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, }); } else { chainable = chainable.then(() => { return { body: cachedTokenResponse, }; }); } chainable .then((response) => { injectTokens(response.body); tokenResponse = response.body; }) .reload() .then(() => { return tokenResponse; }); return chainable; };
Got credentials in authSettings.json
{ "authority": "https://login.microsoftonline.com/x", "clientId": "x", "clientSecret": "x", "apiScopes": [ "x" ], "username": "xxUsername", "password": "xxPassword" }
As you can see I am able to login with the credentials which were saved as variable in the authSettings.json file. This is restricting me to use just 1 user to authenticate in my tests. What is the best practice to get logged in with any other usercredential?
Advertisement
Answer
Add users to the fixture keyed by an id
authsettings.json
{ "user1": { "username": "xxUsername", "password": "xxPassword" ... }, "user2": { "username": "xxUsername", "password": "xxPassword" ... }, ... }
In auth.js is gets a bit tricky since you have some closures on the initial import, for example
const buildIdTokenEntity = (homeAccountId, idToken, realm) => { return { credentialType: "IdToken", homeAccountId, environment, clientId, // closure from above (not a parameter) secret: idToken, realm, }; };
You could set the desired userid in an environment variable, so the top of auth.js becomes
import authSettings from "./authsettings.json"; const userId = Cypress.env('userId'); const { authority, clientId, clientSecret, apiScopes, username, password, } = authSettings[userId];
In the tests,
it('tests user1', () => { Cypress.env('userId', 'user1') ... })
Also use a default in Cypress configuration
// cypress.config.js const { defineConfig } = require('cypress') module.exports = defineConfig({ e2e: { baseUrl: 'http://localhost:1234' }, env: { userId: 'user3' } })
Timing
The above is your smallest change, but I suspect it won’t work since Command.js
is imported in cypress/support/e2e.js
and executes the auth.js
import before the test runs.
If that’s the case, you will need to pass userId
into the login
test
describe('Login with MSAL as xxUsername', () => { beforeEach(() => { cy.LoginWithMsal('user2') })
Commands.js
Cypress.Commands.add("LoginWithMsal", (userId) => { // receive here if (cachedTokenExpiryTime <= new Date().getTime()) { cachedTokenResponse = null; } return login(cachedTokenResponse, userId) // pass here .then((tokenResponse) => { cachedTokenResponse = tokenResponse; cachedTokenExpiryTime = new Date().getTime() + 50 * 60 * 1000; });
auth.js
import authSettings from "./authsettings.json"; let // const -> let to allow change authority, clientId, clientSecret, apiScopes, username, password; ... export const login = (cachedTokenResponse, userId) => { authority = authSettings[userId].authority; clientId = authSettings[userId].clientId; clientSecret = authSettings[userId].clientSecret; apiScopes = authSettings[userId].apiScopes; username = authSettings[userId].username; password = authSettings[userId].password; ...
You could reduce that down if some of the credentials are common to all users.