Repeat prompting when using universal login and multifactor authentication

Hello, we are transitioning from password resource owner grant type/workflow to Authorization Code Flow with Proof Key for Code Exchange (PKCE).

Currently we controlling whether users are prompted for mfa using an Auth Pipeline rule (Customize Multi-Factor Authentication Pages).

We were able to switch our single page application over to using Authorization Code Flow with Proof Key for Code Exchange (PKCE) with universal login by following the documentation with one exception, users with MFA enabled are now repeatedly asked to enter their mfa code over and over. Each time successful and each time redirecting them back to the enter MFA code screen.

Our suspicion was that our custom MFA rule is causing the re-prompting. So to test I tried adding an condition:
if (context...response_mode === 'web_message') return callback(null, user, context)

The theory being that if a user is attempting to use getAccessTokenSilently() from the auth0-react SDK that we should not redirect them to the MFA prompt.

As far as I can tell this has made things work successfully.

My question is, is this ok from a security standpoint, and more to the point what is the correct way to handle this?

Hello @anthony.saldivar ,

Thank you for reaching out.

The idea of getAccessTokenSilently() is to receive a new access and or ID token from the Auth0 server, using refresh token requested while the user log in, without disrupting the user experience - so without prompting them to provide authentication data again.

At the same time, the Authorization Code Flow with Proof Key for Code Exchange (PKCE)
which optionally can use refresh tokens to silently renew the access and or ID token is the supported by Auth0 solution.

Hope this gave you the needed context, let us know. Thank you!

Hi thank you. So what I am doing is using Authorization Code Flow with Proof Key for Code Exchange but I’m also using the above custom rule to control the MFA flow conditionally (as suggested by Auth0 documentation). There is no documentation on specifically working with these two together so I’m hoping you can tell me whether what I am doing is still considered secure, or if there is another best practice that I am not seeing.

1 Like

Hi, thanks, let me please share some more context -

The getAccessTokenSilently() should be mostly the wrapper for getTokenSilently() (auth0-spa-js library’s method). But to the point - If the Auth0 client is initialized with the cacheLocation set to localstorage, something like this:

 const auth0 = await createAuth0Client({
  domain: '{yourDomain}',
  clientId: '{yourClientId}',
  cacheLocation: 'localstorage'
});

Then the security context is as follows:

Storing tokens in browser local storage provides persistence across page refreshes and browser tabs. However, if an attacker can achieve running JavaScript in the SPA using a cross-site scripting (XSS) attack, they can retrieve the tokens stored in local storage. A vulnerability leading to a successful XSS attack can be either in the SPA source code or in any third-party JavaScript code (such as bootstrap, jQuery, or Google Analytics) included in the SPA.

I understand that you have custom rules for MFA that currently breaks the flow, but do you need custom rules?

I reviewed the Adaptive MFA feature overview (without custom rules), which cover assessors: NewDevice, ImpossibleTravel, and UntrustedIP.

And in terms of the SPA source code / third party libraries potential vulnerability -
When Auth0 triggers MFA on low confidence (high-risk) logins, we disregard any existing MFA session such as when the user enables Remember this browser. Users have no way to go around the MFA challenges triggered by Adaptive MFA.

Unfortunately I didn’t have a chance yet to test the Custom (rules-based) Adaptive MFA with the SPA’s recommended login flow, but I hope this context will give you some insight to decide the further steps.

—----------------------------------------------------------------------------------------------------------------------------
If you found this post helpful or interesting, please give it a like :+1: . Your interaction makes a difference. Have a wonderful day! :sun_with_face:

Marcelina


:video_camera: Prefer how-to videos instead of written docs? We’ve got you covered! Check out our OktaDev YouTube channel for those helpful resources!
—----------------------------------------------------------------------------------------------------------------------------

We are specifically not using the localStorage option as it was flagged during a security audit.

This is our Auth Pipeline rule in full:

function multifactorAuthentication(user, context, callback) {
  // Check if context.request.query.response_mode === 'web_message'
  const isSilentAuth = (((context || {}).request || {}).query || {}).response_mode === 'web_message';
  if (isSilentAuth) {
    // This is a token request from getAccessTokenSilently() do not send to mfa flow');
    return callback(null, user, context);
  }
  const hasMultifactor = !!user.multifactor;
  const hasAppMetadataMFA = user.app_metadata && user.app_metadata.has_mfa;
  if (hasAppMetadataMFA) {
    console.log('User has MFA enabled. Force MFA authorization');
    context.multifactor = {
      provider: 'any',
      allowRememberBrowser: true
    };
  }
  callback(null, user, context);
}

The conditional statement to direct users to the MFA workflow is straight out of Auth0 documentation. But it causes requests from getAccessTokenSilently() to get redirected as well which is bad.

So the above code works I need to know if there is something wrong with it of if there is another prescribed way to accomplish this.

Adaptive MFA does not work for our usecase.

Thank you

Hi Anthony,

After further research, I didn’t find a recommendation that the PKCE flow requires this additional step to be considered secure.

Thanks!

I’l also leave below other relevant information -

https://auth0.com/docs/customize/actions/flows-and-triggers/login-flow/redirect-with-actions

—----------------------------------------------------------------------------------------------------------------------------
If you found this post helpful or interesting, please give it a like :+1: . Your interaction makes a difference. Have a wonderful day! :sun_with_face:

[Name]


:video_camera: Prefer how-to videos instead of written docs? We’ve got you covered! Check out our OktaDev YouTube channel for those helpful resources!
—----------------------------------------------------------------------------------------------------------------------------

The additional step is not to make it more secure, it is to prevent our custom rule from denying getAccessTokenSilently() requests (technically it doesn’t deny it just responds with an MFA redirect). Doing this step makes our workflows work but I’m hoping to confirm that you (Auth0) consider it secure.

We are planning to migrate this custom Rule to an Action as soon as this first migration step to universal login is complete.

If I’m being honest none of your responses seem to be very related to my question and you haven’t asked any clarifying questions so I am at a loss as how to get closer to a solution. It seems you are searching buzz words and linking related docs.

I am asking in the community forums so that the general public might be able to seek similar answers but I feel like I’m not getting very far, just generic replies that seem aimed at closing the question as quickly as possible rather than finding a solution.

Hi Anthony,

Thank you for these pointers, definitely something for our consideration.

Just one note from my side regarding my last replay (to clarify it more):

My answer was that, at the getAccessTokenSilently() step, redirecting users to MFA check (which you were able to mitigate by adjusting your rule), is not obligatory for the flow to to be considered secure.

Thank you,
Marcelina

Yes but it is obligatory for our users to successfully login. If they don’t then when the UI tries to call getAccessTokenSilently() the rule will respond with mfa required, which breaks our UI.
If there is something more I need to configure in order for this not to happen or a better way to write the rule then I would appreciate guidance. As is, it is working, but it feels a little hacky. I’m not a security expert. I understand enough to get this far but we are relying on your help to implement your product properly.

Thank you

I’m in almost the exact same boat as you, including recently migrating to UL and utilizing legacy rules. When testing MFA, I have the same login loop. After some time of Googling, I managed to track down a piece of documentation which specifically addresses this specific use-case.

In your case, you are returning early if you detect a silent auth call. Instead, you can check whether the user has previously authenticated using MFA.

We decided it was time to migrate to Actions, and it was relatively painless, but the concept applies to rules too.

Here’s our action:

exports.onExecutePostLogin = async (event, api) => {
  const userMetadata = event.user.user_metadata;
  const multifactor = api.multifactor;
  const authMethods = event.authentication?.methods || [];

  /**
   * If user has completed mfa, don't make them redo the MFA prompt every login.
   * Particularly useful for silent authentication where you may become stuck in a login loop
   * https://auth0.com/docs/authenticate/login/configure-silent-authentication#silent-authentication-with-multi-factor-authentication
   */
  const isMfaCompleted = authMethods.find(method => method.name === 'mfa');
  if (isMfaCompleted) {
    return;
  }

  if (userMetadata && userMetadata.REQUIRE_MFA) {
    multifactor.enable('any')
  };
};

@seanerice, This is exactly what I was looking for. Thank you!

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.