AzureAD / microsoft-authentication-library-for-js

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

Empty token Cache #6995

Open REALSTEVEIG opened 5 months ago

REALSTEVEIG commented 5 months ago

Core Library

MSAL.js (@azure/msal-browser)

Core Library Version

"@azure/msal-node": "^2.6.6",

Wrapper Library

MSAL Angular (@azure/msal-angular)

Wrapper Library Version

None

Public or Confidential Client?

Confidential

Description

So I am trying to update my tokens whenever they expire (Both access tokens and refresh tokens) However, It seems my token cache is always empty even this method: "acquireTokenByRefreshToken" returns a successfull response with new accessTokens. The token cache is always empty.

Error Message

tokenCache {"Account":{},"IdToken":{},"AccessToken":{},"RefreshToken":{},"AppMetadata":{}}

MSAL Logs

[Mon, 01 Apr 2024 22:21:16 GMT] : [] : @azure/msal-node@2.6.6 : Info - acquireTokenByRefreshToken called [Mon, 01 Apr 2024 22:21:16 GMT] : [] : @azure/msal-node@2.6.6 : Verbose - initializeRequestScopes called [Mon, 01 Apr 2024 22:21:16 GMT] : [0dbed1f3-ff5e-44e5-a9b3-2d9aab270d3d] : @azure/msal-node@2.6.6 : Verbose - buildOauthClientConfiguration called [Mon, 01 Apr 2024 22:21:16 GMT] : [0dbed1f3-ff5e-44e5-a9b3-2d9aab270d3d] : @azure/msal-node@2.6.6 : Verbose - createAuthority called
[Mon, 01 Apr 2024 22:21:16 GMT] : [] : @azure/msal-node@2.6.6 : Verbose - Attempting to get cloud discovery metadata from authority configuration [Mon, 01 Apr 2024 22:21:16 GMT] : [] : @azure/msal-node@2.6.6 : Verbose - Did not find cloud discovery metadata in the config... Attempting to get cloud discovery metadata from the hardcoded values. [Mon, 01 Apr 2024 22:21:16 GMT] : [] : @azure/msal-node@2.6.6 : Verbose - Found cloud discovery metadata from hardcoded values.
[Mon, 01 Apr 2024 22:21:16 GMT] : [] : @azure/msal-node@2.6.6 : Verbose - Attempting to get endpoint metadata from authority configuration [Mon, 01 Apr 2024 22:21:16 GMT] : [] : @azure/msal-node@2.6.6 : Verbose - Did not find endpoint metadata in the config... Attempting to get endpoint metadata from the hardcoded values. [Mon, 01 Apr 2024 22:21:16 GMT] : [0dbed1f3-ff5e-44e5-a9b3-2d9aab270d3d] : @azure/msal-node@2.6.6 : Info - Building oauth client configuration with the following authority: https://login.microsoftonline.com/common/oauth2/v2.0/token. [Mon, 01 Apr 2024 22:21:16 GMT] : [0dbed1f3-ff5e-44e5-a9b3-2d9aab270d3d] : @azure/msal-node@2.6.6 : Verbose - Refresh token client created [Mon, 01 Apr 2024 22:21:17 GMT] : [0dbed1f3-ff5e-44e5-a9b3-2d9aab270d3d] : @azure/msal-common@14.8.1 : Verbose - setCachedAccount called [Mon, 01 Apr 2024 22:21:17 GMT] : [0dbed1f3-ff5e-44e5-a9b3-2d9aab270d3d] : @azure/msal-common@14.8.1 : Warning - Account used to refresh tokens not in persistence, refreshed tokens will not be stored in the cache

Network Trace (Preferrably Fiddler)

MSAL Configuration

const msalConfig = {
  auth: {
    clientId: OAUTH.MICROSOFT_ENV.MICROSOFT_CLIENT_ID_V2 ?? '',
    authority: OAUTH.MICROSOFT_ENV.MICROSOFT_AUTHORITY_V2,
    clientSecret: OAUTH.MICROSOFT_ENV.MICROSOFT_CLIENT_SECRET_V2,
  },
  system: {
    loggerOptions: {
      loggerCallback(_loglevel: any, message: any, _containsPii: any) {
        console.log(message);
      },
      piiLoggingEnabled: false,
      logLevel: LogLevel.Verbose,
    },
  },
};

Relevant Code Snippets

// Function to refresh access token using refresh token
const refreshAccessToken = async (
  refreshToken: string,
  req: Request,
): Promise<Record<string, unknown>> => {
  app.locals.msalClient = new msal.ConfidentialClientApplication(msalConfig);
  const refreshTokenRequest = {
    scopes: [
      'user.read',
      'calendars.readwrite',
      'mailboxsettings.read',
      'offline_access',
    ],
    accessType: 'offline',
    refreshToken,
  };

  try {
    const response = await req.app.locals.msalClient.acquireTokenByRefreshToken(
      refreshTokenRequest,
    );

    console.log('response', response);

    const refreshToken2 = async (): Promise<any> => {
      const tokenCache = await req.app.locals.msalClient
        .getTokenCache()
        .serialize();

      console.log('tokenCache', tokenCache);

      const refreshTokenObject = JSON.parse(tokenCache).RefreshToken;

      const myRefreshToken =
        refreshTokenObject[Object.keys(refreshTokenObject)[0]].secret;

      return myRefreshToken;
    };

    const updatedCredentials = {
      newAccesssToken: response?.accessToken,
      newRefreshToken: await refreshToken2(),
      newExpiresOn: response?.expiresOn,
    };

    return updatedCredentials;
  } catch (error) {
    // Handle token refresh failure
    throw new Error(error as string | undefined);
  }
};

//Get all calendar service
const getAllCalendarServices = async (
  req: Request,
  user: string,
): Promise<any> => {
  const account = await AccessTokenModel.findOne({
    user,
    provider: 'microsoft',
  })
    .sort({ createdAt: -1 })
    .limit(1);

  if (!account) {
    return {
      errorCode: 'missing-access-token',
      statusCode: 403,
      message: 'Kindly authorize the microsoft App to continue',
    };
  }

  if (account.expiryTime.getTime() < Date.now()) {
    const newTokens = await refreshAccessToken(account.refreshToken, req);

    await AccessTokenModel.findOneAndUpdate(
      { user, provider: 'microsoft' },
      {
        accessToken: newTokens.newAccessToken,
        expiryTime: newTokens.newExpiresOn,
      },
      { new: true, runValidators: true, sort: { createdAt: -1 } },
    );

    const newAccount = await AccessTokenModel.findOne({
      user,
      provider: 'microsoft',
    })
      .sort({ createdAt: -1 })
      .limit(1);

    // Use the new access token to make the request
    const calendarsResponse = await axios.get(
      'https://graph.microsoft.com/v1.0/me/calendars',
      {
        headers: {
          // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
          Authorization: `Bearer ${newAccount?.accessToken}`,
        },
      },
    );
    return calendarsResponse.data;
  }

  const calendarsResponse = await axios.get(
    'https://graph.microsoft.com/v1.0/me/calendars',
    {
      headers: {
        Authorization: `Bearer ${account.accessToken}`,
      },
    },
  );

  return calendarsResponse.data;
};

//Get all calendars controllers
const getAllCalendarsController = async (
  req: Request,
  res: Response,
): Promise<any> => {
  try {
    const user = req.session.user?.email;
    const allCalendars = await getAllCalendarServices(req, user!);
    return res.status(200).json(allCalendars);
  } catch (error: any) {
    // eslint-disable-next-line no-console
    // console.log('error', error);
    if (error.response.status !== 200) {
      return res.status(error.response.status).json(error.response.data.error);
    }
    return res.status(500).json(error.response.data);
  }
};

Reproduction Steps

  1. Set up DB for storing access tokens and refresh tokens. (AccessTokenModel).
  2. Import the getAllCalendar service function into your conroller file.
  3. Make a call to the getAllCalendarController endpoint.
  4. If accesTokenModel.expiry time === true/ access token has expired, endpoint will call the refreshAccessToken to obtain a new accesstoken and refreshtoken.

Here is a link to the sample code on github: SAMPLE OF CODE ON GITHUB

How to run the code.

  1. Git clone: Sample code
  2. npm install
  3. npm run dev
  4. Auth endpoint: http://localhost:8000/install
  5. callback endpoint : http://localhost:8000/callback
  6. Get all calendars endpoin: http://localhost:8000/calendars

Expected Behavior

I expect that the refreshAccessToken function should not only generate new accessTokens but should also store this access token in the cache along with the refresh token in the token cache.

Identity Provider

Entra ID (formerly Azure AD) / MSA

Browsers Affected (Select all that apply)

Chrome

Regression

No response

Source

External (Customer)

BertVanHeckeCertifisc commented 3 months ago

+1

bgavrilMS commented 2 months ago

MSAL doesn't expose the concept of refresh token. It handles it internally. This API is only for exotic migration scenario where you might have a refresh token.

Otherwise, in a website scenario, you are supposed to use AcquireTokenByDeviceCode + AcquireTokenSilent. In a web api scenario, AcquireTokenOnBehalf of (RT is not supported there yet).