AzureAD / microsoft-authentication-library-for-js

Microsoft Authentication Library (MSAL) for JS
http://aka.ms/aadv2
MIT License
3.64k stars 2.65k forks source link

Is there a method to generate the localStorage items from a token? #5924

Closed grosch-intl closed 1 year ago

grosch-intl commented 1 year ago

Core Library

MSAL.js v2 (@azure/msal-browser)

Core Library Version

2.33.0

Wrapper Library

MSAL Angular (@azure/msal-angular)

Wrapper Library Version

2.5.3

Public or Confidential Client?

Public

Description

After getting a token, something writes entries to the localStorage so that msal knows where to grab it for future calls. When I'm testing with something like Cypress, seems like it would be easier if I could just generate those same entries, vs. having each call need to add the token to the headers.

Is there any type of method that can be called to do that once I get the token back from the POST to .../v2.0/token?

MSAL Configuration

No response

Relevant Code Snippets

Cypress.Commands.add('login', () => {
    cy.request({
        method: 'POST',
        url: 'https://login.microsoftonline.com/.../oauth2/v2.0/token',
        form: true,
        body: {
            grant_type: 'client_credentials',
            client_id: environment.azure.clientId,
            client_secret: Cypress.env('lampClientSecret'),
            scope: `${environment.azure.clientId}/.default`
        }
    }).then((response) => {
        // Want to make a call here to store response.token so that msal-browser can just use it.
    })
})

Identity Provider

Azure AD / MSA

Source

External (Customer)

sameerag commented 1 year ago

@grosch-intl msal-browser uses the web storage APIs to store tokens. We also send back the tokens acquired in the response in the format: AuthenticationResult.

If you want to store them, you can probably take the tokens from the response and try. We do not support custom storage yet unfortunately.

ghost commented 1 year ago

@grosch-intl This issue has been automatically marked as stale because it is marked as requiring author feedback but has not had any activity for 5 days. If your issue has been resolved please let us know by closing the issue. If your issue has not been resolved please leave a comment to keep this open. It will be closed automatically in 7 days if it remains stale.

ghdoergeloh commented 1 year ago

We took this approach: https://medium.com/version-1/using-cypress-to-test-azure-active-directory-protected-spas-47d04f5add9

However, since updating @azure/msal-browser from 2.34.0 to 2.36.0, there have been problems with it, so I've been looking for an alternative approach. Unfortunately, I haven't found one yet.

You could try the following in your then block:

      const pca = new PublicClientApplication(msalConfig);
      const { oid, tid } = JSON.parse(atob(response.body.access_token?.split('.')[1] ?? '{}')) as {
        oid: string;
        tid: string;
      };
      const clientInfo = btoa(oid) + '.' + btoa(tid); // mabe not neccessary
      return pca.getTokenCache().loadExternalTokens(
        {
          authority: msalConfig.auth.authority,
          scopes: loginRequest.scopes,
        },
        response.body,
        {
          clientInfo,
          expiresOn: response.body.expires_in,
          extendedExpiresOn: response.body.ext_expires_in,
        }
      );

It didn`t work for me because I used the Resource Owner Password Flow (see also https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth-ropc).

However, the application (React) then directly tried to refresh the token with the following error message:

Cross-origin token redemption is permitted only for the 'Single-Page Application' client-type.

I can't seem to use the refresh_token from the ROPC for the authorization code grant in an SPA.

The blob entry at the top says: "The OAuth Client Credentials flow does not give us the necessary claims that our API backend needs, specifically scope claims."

ghdoergeloh commented 1 year ago

Not sure if this finally will work but it avoids fetching the token:

          expiresOn: Date.now() + (response.body.expires_in ?? oneHoure),
          extendedExpiresOn: Date.now() + (response.body.ext_expires_in ?? oneHoure),

expiresOn is of course expires_in + the current timestamp.

sameerag commented 1 year ago

@ghdoergeloh Yeah, we do not recommend ROPC for Prod and it is definitely not supported for SPA flows (which are msal-browser based libraries intended for). We use msal-node in our tests with ROPC flow in our samples. Will modeling your tests (I assume that is the case here) on msal-node for ROPC help?

ghdoergeloh commented 1 year ago

Yes, I tried it with msal-node. Currently I stayed with the raw request. Do you have an example of your tests with msal-node? I am still struggling with the authentication. We are using the popup authentication in our main application, but I want to test another "sub-app", which has no authentication process but uses the authentication result from the sessionStorage of the main application. In short: I do not have the possibility to integrate the default SPA Authentication Flow in my cypress tests, so I want to mock it using the ROPC flow.

It already worked locally, but not in Github Actions. So maybe it is a problem I have with cypress. Before I try to reinvent the wheel or share my code, do you have examples on how to test a SPA without running through the whole login process?

ghdoergeloh commented 1 year ago

I finally managed it. It was a problem in the application, not in the test.

You can use the ROPC token for the tests. Only the refresh token won't work, but since the token is valid for at least an hour, you can use the access token for your tests.

Here is my code. It is based on the above mentioned medium blog by @franklores and replaces the injectTokens method by the msal internal loadExternalTokens method, which makes it a bit simpler and also independent from the msal internal implementation:

import { AuthenticationResult, ExternalTokenResponse, PublicClientApplication } from '@azure/msal-browser';
import { loginRequest, msalConfig } from './authConfig';
import Chainable = Cypress.Chainable;

interface LoginOptions {
  clientSecret: string;
  username: string;
  password: string;
}

interface MsalJwtPayload {
  iat: number;
  exp: number;
  oid: string;
  tid: string;
}

const oneHourInSeconds = 3600;

const pca = new PublicClientApplication(msalConfig);

export function loginCommand(loginOptions: LoginOptions): Chainable<AuthenticationResult> {
  // Get a new token via resource owner password credentials flow https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth-ropc
  if (!msalConfig.auth.authority) throw Error('No authority configured');
  return cy
    .request<ExternalTokenResponse & { ext_expires_in?: number }>({
      url: msalConfig.auth.authority + '/oauth2/v2.0/token',
      method: 'POST',
      body: {
        grant_type: 'password',
        client_id: msalConfig.auth.clientId,
        client_secret: loginOptions.clientSecret,
        scope: ['openid', 'profile', 'offline_access'].concat(loginRequest.scopes).join(' '),
        username: loginOptions.username,
        password: loginOptions.password,
      },
      form: true,
    })
    .then((response) => {
      const token = JSON.parse(atob(response.body.access_token?.split('.')[1] ?? '{}')) as MsalJwtPayload;
      const clientInfo = btoa(token.oid) + '.' + btoa(token.tid);
      const authResult = pca.getTokenCache().loadExternalTokens(
        {
          authority: msalConfig.auth.authority,
          scopes: loginRequest.scopes,
        },
        response.body,
        {
          clientInfo,
          expiresOn: token.iat + (response.body.expires_in ?? oneHourInSeconds),
          extendedExpiresOn: token.iat + (response.body.ext_expires_in ?? oneHourInSeconds),
        }
      );
      pca.setActiveAccount(authResult.account);
      return cy.reload().then(() => authResult);
    });
}
div-42 commented 5 months ago

Because of your @ghdoergeloh solution I also got it to work. I only had to adapt this part (we are using Azure B2C and according to the documentation the tid claim is not returned):

const clientInfoObject = {
  uid: token.oid,
  utid: tenantId,
};
const clientInfo = btoa(JSON.stringify(clientInfoObject));