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!
Context
-
Enabled factors:
- WebAuthn (FIDO device biometrics)
- One-time passwords (OTP)
- SMS
- 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:
- Never
- Adaptive
- Always
Adaptive MFA spec
We wanted to math Auth0 out of the box implementation as closely as possible, our adaptive flow is:
-
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).
-
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!