Review and feedback of our implementation of Adaptive MFA

Hi everyone,

I’d welcome your thoughts on my Auth0 MFA implementation. Below is some background, the details of our approach and the code for my Post-Login Action. Any feedback—on logic, policy choices, code style or edge cases—is much appreciated! :folded_hands:

Context

  • Enabled factors:

    • WebAuthn (FIDO device biometrics)
    • One-time passwords (OTP)
    • SMS
    • Email
    • Recovery codes
  • Enforcement scope: MFA is currently enforced on every password-based login. I’m open to opinions on whether this is appropriate, overly restrictive or leaves any gaps.

  • Per-organisation policies: We use Auth0 Organisations; each can select one of three MFA policies:

    1. Never
    2. Adaptive
    3. Always

Adaptive MFA spec

We wanted to math Auth0 out of the box implementation as closely as possible, our adaptive flow is:

  1. User not enrolled

    • High/Medium risk confidence: Prompt to enrol in any supported factor.
    • Low risk confidence: Only offer OTP enrolment (I tried adding email here but couldn’t get it working).
  2. User already enrolled

    • Only challenge again if risk confidence is low.

Post-Login Action

exports.onExecutePostLogin = async (event, api) => {
  const usedPassword = (event.authentication?.methods || [])
    .some(m => m.name === 'pwd');

  // Only enforce MFA when a password was used
  if (!usedPassword) return;

  const orgId      = event.organization?.id;
  const mfaPolicy  = await getMfaPolicy(event.user.email, orgId);

  switch (mfaPolicy) {
    case 'never':
      // No MFA
      return;

    case 'always':
      api.multifactor.enable('any', { allowRememberBrowser: false });
      return;

    case 'adaptive':
      await handleAdaptiveMfa(event, api);
      return;

    default:
      console.warn(`Unrecognised MFA policy: ${mfaPolicy}`);
  }
};

async function getMfaPolicy(email, orgId) {
  // Fetch the MFA policy for this organisation by making a call to our backend service
}

async function handleAdaptiveMfa(event, api) {
  const confidence   = event.authentication?.riskAssessment?.confidence;
  const userEnrolled = event.user.enrolledFactors.length > 0;

  switch (confidence) {
    case 'high':
    case 'medium':
      if (!userEnrolled) {
        api.multifactor.enable('any', { allowRememberBrowser: false });
      }
      break;

    case 'low':
      if (userEnrolled) {
        api.multifactor.enable('any', { allowRememberBrowser: false });
      } else {
        api.authentication.enrollWith({ type: 'otp' });
      }
      break;

    default:
      console.error('No risk confidence level; blocking access');
      api.access.deny('Missing risk assessment');
  }
}

Questions & feedback welcome on:

  • Policy design: Is enforcing on every password login sensible?
  • Adaptive logic: Any gaps or improvements to the risk-based flow?

Thanks in advance for your insights! :blush:

Hi @ammo

Your implementation for custom MFA policies looks great! I can see that instead of depending on the built in Adaptive MFA feature, you are handling everything depending on your use case/necessities.

I could not identify any problems at a first glance with your action code, I will be looking forwards to more info on the matter after some testing is performed to see if all scenarios behave as expected.

Otherwise, for certain users, enforcing the MFA on every password login might be a little too much depending on how long their sessions are being preserved for in the application. Perhaps enforcing the MFA on a set period of time (once every 30 days) might be a better user experience, however, depending on what you are trying to achieve, your approach might be more suitable.

Regarding your low confidence level code, if you are not happy with only enforcing MFA, you could check if the user’s email address/phone number is verified, if not, force them to verify those elements as well depending on the situation.

Since you are looking forward to other feedback on your approach, I would suggest to refrain from marking any reply as solution in order for the post to be kept open for other users!

If you have any other questions on the matter, let me know!

Kind Regards,
Nik