Multi-Factor Authentication (MFA)
Multi-Factor Authentication (MFA) adds an extra layer of security to user accounts by requiring a second authentication factor beyond a password. Medplum supports two MFA methods:
- Authenticator app (TOTP) — a Time-based One-Time Password compatible with authenticator apps like Google Authenticator, Microsoft Authenticator, Authy, and others.
- Email — a single-use 6-digit code emailed to the user's address each time it is needed.
By default, only the authenticator app (TOTP) method is offered. Email-based MFA must be enabled per Project via the allowedMfaMethods setting. Users may enroll in more than one method and choose which to use at login.
Users enrolled in MFA will only be asked for an MFA code during a login with Username/Password
Configuring allowed MFA methods
The MFA methods that users in a Project can enroll in are controlled by the allowedMfaMethods setting on the Project resource. It is stored as a single Project.setting entry whose valueString is a comma-delimited list of method codes:
| Value | Methods offered |
|---|---|
| (unset) | Authenticator app (TOTP) only — the historical default |
totp | Authenticator app (TOTP) only |
email | Email codes only |
totp,email | Both — users choose which to enroll in and which to use at login |
When the setting is missing, empty, or contains no recognized value, Medplum falls back to totp only.
Enabling email-based MFA
To allow users in a Project to use email-based MFA, set the allowedMfaMethods setting to include email. Project settings can be edited by a Project Admin, or in the Medplum App by a Super Admin on the Project edit page.
- TypeScript
- CLI
const project = await medplum.readResource('Project', projectId);
await medplum.updateResource({
...project,
setting: [
// Preserve any other settings, replacing allowedMfaMethods if it exists
...(project.setting ?? []).filter((s) => s.name !== 'allowedMfaMethods'),
{ name: 'allowedMfaMethods', valueString: 'totp,email' },
],
});
# Add the setting to the Project resource (replace <projectId> and merge with
# any existing settings)
medplum patch Project/<projectId> \
'[{
"op": "add",
"path": "/setting/-",
"value": { "name": "allowedMfaMethods", "valueString": "totp,email" }
}]'
Use valueString: 'email' to offer email codes only, or valueString: 'totp' (or remove the setting) to restrict users to authenticator apps.
Changing allowedMfaMethods affects which methods users can newly enroll in. Users already enrolled in a method that is later disallowed keep that method until they remove it.
Self-Enrollment
Users can self-enroll in MFA through the Medplum App security settings. The methods offered depend on the Project's allowedMfaMethods setting.
Steps to Self-Enroll
- Navigate to the Security page at
https://app.medplum.com/security - You will see the "Multi Factor Auth" section showing your current enrollment status
- Choose a method to enroll in:
Authenticator app (TOTP)

- A QR code will be displayed that you can scan with your authenticator app
- Enter the 6-digit code from your authenticator app to complete enrollment
- Click "Add email-based MFA" — a 6-digit code is emailed to your account's address
- Enter the code to verify control of your email and complete enrollment
Once enrolled, you will be required to provide an MFA code during login. When both methods are allowed, a user can enroll in both and add or remove individual methods from the Security page. You can disable MFA at any time by clicking the "Disable MFA" button (you'll need to provide a current MFA code to disable it).
Requiring MFA for New Users
Administrators can require new users to set up MFA during the invitation process by setting the mfaRequired parameter to true in the invite request.
When a user is invited with mfaRequired: true:
- A MFA secret is automatically generated for the user
- During their first login, after entering their password, they will be prompted to enroll in MFA
- They must complete MFA enrollment before they can access the system
Example: Inviting a User with MFA Required
- TypeScript
- CLI
- cURL
await medplum.invite(123, {
resourceType: 'Practitioner',
firstName: 'Jane',
lastName: 'Doe',
email: 'jane.doe@example.com',
mfaRequired: true,
});
medplum post admin/projects/:projectId/invite \
'{
"resourceType": "Practitioner",
"firstName": "Jane",
"lastName": "Doe",
"email": "jane.doe@example.com",
"mfaRequired": true
}'
curl https://api.medplum.com/admin/projects/:projectId/invite \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"resourceType": "Practitioner",
"firstName": "Jane",
"lastName": "Doe",
"email": "jane.doe@example.com",
"mfaRequired": true
}'
For more details on the invite endpoint, see the Invite User Endpoint documentation.
Admin MFA Reset
Project admins can reset MFA enrollment for users who have lost access to their authenticator device via the POST /admin/projects/:projectId/members/:membershipId/mfa/reset endpoint.
When reset:
- The user's
mfaEnrolledflag is cleared - The TOTP secret is rotated (the old authenticator app entry cannot be reused)
- The user receives an email notification
- The user must re-enroll in MFA on their next login (if
mfaRequiredis set) or via the Security settings page
- TypeScript
- CLI
- cURL
await medplum.post(`admin/projects/${projectId}/members/${membershipId}/mfa/reset`, {});
medplum post admin/projects/:projectId/members/:membershipId/mfa/reset '{}'
curl -X POST https://api.medplum.com/admin/projects/:projectId/members/:membershipId/mfa/reset \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
How email-based MFA works
Unlike the authenticator app method, which derives codes from a shared secret stored on the user, email-based MFA issues a fresh single-use code each time one is needed.
When a code is required, Medplum:
- Generates a random 6-digit code.
- Stores a hash of the code (never the code itself) on the user's
Login, along with an expiration timestamp. - Emails the code to the user's address with the subject
Your Medplum verification code: <code>.
The code is valid for 20 minutes. Each code is single-use — it is cleared once verified — and submitting an expired or incorrect code is rejected, so the user must request a new one.
At login
When a user enrolled in email-based MFA signs in with their username and password:
- After the password is accepted, Medplum recognizes that the login still needs a second factor.
- If email is the user's only enrolled method, the code is sent automatically and the sign-in form goes straight to the code-entry step. If the user also has an authenticator app, they can choose to receive a code by email instead.
- The user enters the 6-digit code from their email to complete the login.
If a code expires or is lost, the user can request a new one from the sign-in form, which sends a fresh code and resets the 20-minute window.
At enrollment
Enrolling in email-based MFA requires the user to prove control of their email address. When the user starts enrollment, Medplum emails a code; the user enters it to verify and finish enrolling. The same emailed-code verification is required when an enrolled user adds, removes, or disables an email factor.
Using Medplum's SignInForm Component
We recommend using Medplum's SignInForm React component for handling authentication flows that include MFA. The SignInForm component automatically handles all MFA-related UI and flows, including:
- MFA Enrollment: Automatically detects when a user needs to enroll in MFA (e.g., when
mfaRequired: truewas set during invitation) and displays the enrollment screen with QR code - MFA Verification: Automatically prompts for MFA codes when users with enrolled MFA attempt to log in
Building Your Own MFA UI
Can't use SignInForm or the Medplum App Security page to manage MFA configuration and challenge completion? See MFA Under the Hood: Routes & User Flows for the underlying routes and the enrollment and challenge flows you can drive directly.