Tips on customizing the MSAL login commands for successful integration with numerous users

Successfully implemented the code to login using a username and password combination with Cypress for a webapp integrated with MSAL.

In the end-to-end 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",
        // Custom base64 encoding omitted for simplicity
        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(" "),
    };
};

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;
};

Fetched credentials from authSettings.json

{
  "authority": "https://login.microsoftonline.com/x",
  "clientId": "x",
  "clientSecret": "x",
  "apiScopes": [ "x" ],
  "username": "xxUsername",
  "password": "xxPassword"
}

While successfully logging in with the stored user credentials as variables in the authSettings.json file, this limits the ability to authenticate using only one user in tests. How can I implement flexibility to log in with any other user's credentials?

Answer №1

Assign users to the fixture using individual IDs

authsettings.json

{
  "user1": {
    "username": "xxUsername",
    "password": "xxPassword"
    ...
  },
  "user2": {
    "username": "xxUsername",
    "password": "xxPassword"
    ...
  },
  ...
}

In auth.js, things get complex with closures on 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 can specify the desired user ID in an environment variable, leading to this setup at the top of auth.js:

import authSettings from "./authsettings.json";

const userId = Cypress.env('userId');
const {
    authority,
    clientId,
    clientSecret,
    apiScopes,
    username,
    password,
} = authSettings[userId];

For testing purposes,

it('tests user1', () => {
  Cypress.env('userId', 'user1')
  ...
})

Add a default setting in Cypress configuration as well

// cypress.config.js

const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:1234'
  },
  env: {
    userId: 'user3'
  }
})

Timing

The previous change might not suffice 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'll need to pass the userId into the login functionality

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;

  ...

This process could be streamlined if certain credentials are shared among all users.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

The DOM is failing to refresh in Vue.js even after the array has been updated

After receiving a list of items using AJAX, I store them in a data Array: loadSparepartFiles: function() { var vm = this; vm.activeSparepart.attachments = []; ajaxApi.loadJson('spareparts/sparepart/getFiles/'+vm.activeSparepartId, fu ...

Animating the Bookmark Star with CSS: A Step-by-Step Guide

I found this interesting piece of code: let animation = document.getElementById('fave'); animation.addEventListener('click', function() { $(animation).toggleClass('animate'); }); .fave { width: 70px; height: 50px; p ...

In JavaScript, a prompt is used to request the user to input a CSS property. If the input is incorrect,

Implement a while loop that continuously prompts the user to enter a color. If the color entered matches a CSS property such as blue, red, or #000000: The background will change accordingly, but if the user enters an incorrect color, a message will be dis ...

Enhancing class names in production mode with Material UI, Webpack, and React to optimize and minimize code size

webpack - v4.5+ material ui - v4.9.7 react - v16.12.1 Ordinarily, all classes should follow the pattern of the last one in the first example. However, for some unknown reason, many classes remain unchanged in production mode. Any thoughts on this issue? ...

What is the process for establishing a connection between two sets of data?

I am working with two mongoose models: user.js and note.js, which correspond to the collections notes and users. I want users to be able to save their notes, but I'm unsure how to create relationships between these collections. Ideally, I would like t ...

How can I make the expired date display in red color in a JavaScript date property?

In my HTML, I have 4 different dates. The expired date "END MAR 20" should be displayed in red color instead of green. The next date, "END APR 29," should be in green color. All preceding dates should also be displayed in red. I have successfully filtered ...

When trying to make a POST request, the browser displayed an error message stating "net::ERR_CONNECTION

Currently, my project involves coding with React.js on the client side and Express.js on the server side. I have encountered an issue when attempting to use the POST method to transmit data from the client to the server for storage in a JSON file. The erro ...

Can we accurately pinpoint individual LineSegments in three.js by hovering over them, especially those with unique geometries?

Creating two examples of drawing lines in three.js was quite a fun challenge. One example showcased different geometry and material, while the other kept it simple with just one geometry and material. The demonstration links can be found here: Example 1 an ...

Array not transmitted via jQuery ajax

I am attempting to use the jQuery ajax function to send an array, but for some reason it is not functioning as expected. Below is the code I have been working with: if (section_name == "first_details_div") { var fields_arr = ["f_name", "l_name", "i ...

Android Chrome users experiencing sidebar disappearance after first toggle

Over the past few days, I've dedicated my time to developing a new project. I've been focusing on creating the HTML, CSS, and Java layout to ensure everything works seamlessly. After completing most of the work, the design looks great on desktop ...

How can I stop and hover over time in AngularJs Interval?

Within my UI, I have a time element that is continuously updated using AngularJS Interval. Even the milliseconds are constantly running. Is it possible to implement a feature where the time pauses when hovering over it? Any assistance would be greatly appr ...

Combining two JSON objects using Angular's ng-repeat

My goal is to extract data from two JSON files and present it in a table: The first file 'names.json' contains: [ { "name": "AAAAAA", "down": "False" }, { "name": "BBBBBB", ...

Using Javascript to retrieve a variable and dynamically updating the source of an HTML iframe

I have two JavaScript variables, 'long' and 'lat', in the code below. My challenge is to append these values to the end of the iframe URL. I would appreciate assistance on modifying the code below to achieve this. The iframe code bel ...

Tips for achieving an eye-catching text and image layout on a single page of your Wordpress blog

I've been exploring ways to achieve a design concept for my blog layout. I envision having the text and thumbnail displayed side by side, each taking up 50% of the width until the image reaches its end. Once the image ends, I want the text to span the ...

Installing material-ui using npm does not always result in getting the most up-to-date version

I'm currently facing a dilemma with an abandoned project that serves as the admin tool for my current project. The Material-UI version used in this project is 0.19.4. However, when I remove the dependency from my package.json file and execute npm inst ...

The computed variable in Vuex does not get updated when using the mapState function

I searched through several posts to find out what I am doing incorrectly. It seems like everything is set up correctly. MOTIVE Based on the value of COMPONENT A, I want to change hide/display content using v-show in DEPENDENT COMPONENT. ISSUE In the T ...

Assign an event listener to a collection of elements

Suppose I have an Array containing elements and another Array consisting of objects in the exact same index order. My goal is to add a click event for each element that will display a specific property of each object. For instance: myDivArray = [ div0, d ...

Retrieve data from the api

Can someone provide the JavaScript code to loop through an API, extract the coordinates/address, and map it? Here is a simple demonstration of fetching the API data: const fetch = require("node-fetch"); fetch('url').then(function (resp ...

What are some ways to direct users from one page to another without relying on server-side programming?

Is there a way to create a redirect page using jQuery or JavaScript? What is the process of writing client-side scripting code to redirect the browser from one page (page1) to another page (page n)? ...

What is the best way to capitalize the first letter of a string in JavaScript?

Is there a way to capitalize only the first letter of a string if it's a letter, without changing the case of any other letters? For example: "this is a test" → "This is a test" "the Eiffel Tower" → "The Eiffel ...