www/apps/resources/app/commerce-modules/auth/mfa/page.mdx
import { Prerequisites } from "docs-ui"
export const metadata = {
title: Multi-Factor Authentication,
}
In this guide, you'll learn how multi-factor authentication (MFA) works in the Auth Module, the providers it ships with, and how the authentication flow changes when an auth identity has MFA enabled.
<Prerequisites items={[ { text: "Medusa v2.15.5+", link: "!docs!/learn/update" } ]} />
Multi-factor authentication adds a second verification step to the standard authentication flow. After a user authenticates with their credentials (such as email and password), they must pass another verification step using a second factor, such as a code from an authenticator app or a recovery code.
The Auth Module implements MFA around two concepts:
By default, Medusa sets the MFA encryption key to the AUTH_MFA_ENCRYPTION_KEY environment variable. If you've created your application after v2.15.5 of Medusa, this key is generated for you and stored in the .env file.
If you created your application before v2.15.5, generate a random 64-character string and set it as the value of the AUTH_MFA_ENCRYPTION_KEY environment variable in your .env file:
AUTH_MFA_ENCRYPTION_KEY=your_random_64_character_string
Also, if you've added the Auth Module to your medusa-config.ts file to set any of its options, make sure to set the mfa.encryption_key option to the same environment variable:
import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils"
// ...
module.exports = defineConfig({
// ...
modules: [
{
resolve: "@medusajs/medusa/auth",
dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER],
options: {
mfa: {
encryption_key: process.env.AUTH_MFA_ENCRYPTION_KEY,
},
providers: [
{
resolve: "@medusajs/medusa/auth-emailpass",
id: "emailpass",
options: {
// provider options...
},
},
],
},
},
],
})
If you don't set the mfa.encryption_key option, you'll get a "MFA encryption key is required to use MFA methods" error whenever trying to enroll or verify an MFA factor.
The Auth Module registers two MFA providers out of the box:
totp: Time-based one-time password provider. Users enroll by scanning a QR code in an authenticator app (such as Google Authenticator or 1Password) and verify the factor with a six-digit code.recovery_code: Provides single-use backup codes that users can use to verify an MFA challenge when they don't have access to their primary factor.The recovery_code provider is not enrolled as a factor on its own. It becomes available as a verification method once the user has at least one enabled MFA factor and generates recovery codes.
{/* ### Custom MFA Providers
You can register additional MFA providers in the mfa.providers array of the Auth Module's options. A custom provider must implement the AuthMfaProvider interface, exposing start, verifySetup, canVerifyForAuthIdentity, and verify methods.
Refer to the Module Options guide for the full list of MFA configuration options. */}
Enrolling an MFA factor means registering a second factor for an auth identity (such as a TOTP authenticator). Once the factor is enrolled and enabled, the user must verify with that factor during authentication.
The user enrolls an MFA factor by starting the enrollment, then verifying it.
Before enrolling an MFA factor, the user must be authenticated with their primary factor (for example, by logging in with their email and password). This ensures that only authenticated users can enroll MFA factors for their account.
To authenticate, send a POST request to the /auth/{actor_type}/{auth_provider} API route with the user's credentials.
For example, to authenticate an admin user with their email and password, send a request to the login route:
curl -X POST http://localhost:9000/auth/user/emailpass \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "supersecret"
}'
Use the returned JWT token in the Authorization header of subsequent requests to enroll and manage MFA factors.
The client sends a POST request to /auth/mfa/factors to start enrollment for a specific provider:
curl -X POST http://localhost:9000/auth/mfa/factors \
-H "Content-Type: application/json" \
-H "Authorization: Bearer {jwt_token}" \
-d '{
"provider": "totp",
"label": "Authenticator App",
"issuer": "My Store"
}'
In the request body, you pass the following parameters:
provider: The MFA provider to enroll (for example, totp).label: A label to identify the factor in the user's authenticator app.issuer: The name of your application or store, shown in the authenticator app.The response contains the new factor in pending status, along with the data the user needs to register the factor in their authenticator app:
{
"mfa_factor": { "id": "authmfa_...", "status": "pending", ... },
"secret": "JBSWY3DPEHPK3PXP",
"otpauth_url": "otpauth://totp/My%20Store:[email protected]?secret=..."
}
For totp providers, the client typically renders otpauth_url as a QR code that the user scans with their authenticator app.
Once the user has registered the factor in their authenticator app, the client sends the generated code to /auth/mfa/factors/{id}/verify to verify and activate the factor:
curl -X POST http://localhost:9000/auth/mfa/factors/{id}/verify \
-H "Content-Type: application/json" \
-H "Authorization: Bearer {jwt_token}" \
-d '{
"code": "123456"
}'
In the request body, you pass the code generated by the user's authenticator app.
On success, the factor's status becomes enabled, and the auth.mfa_enabled event is emitted. From now on, any authentication for this auth identity will require an MFA challenge.
When an auth identity has at least one enabled MFA factor, the standard authentication flow returns an MFA challenge instead of an immediate session token.
Once the user successfully verifies the MFA challenge, they receive the session token and can access protected resources as usual.
The user initiates authentication with their primary factor (for example, by logging in with their email and password):
curl -X POST http://localhost:9000/auth/user/emailpass \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "supersecret"
}'
The response indicates that an MFA challenge is required:
{
"mfa_challenge": {
"id": "authmfachal_...",
"methods": ["totp", "recovery_code"]
}
}
The methods array lists the verification methods the user can complete to satisfy the challenge.
Based on the available methods, the client prompts the user to verify the challenge. For example, if totp is available, the user can enter the six-digit code from their authenticator app.
To verify the challenge, the client sends a POST request to /auth/mfa/challenges/{id}/verify with the chosen verification method and the corresponding code:
curl -X POST http://localhost:9000/auth/mfa/challenges/{id}/verify \
-H "Content-Type: application/json" \
-d '{
"method": "totp",
"code": "123456"
}'
On success, the response contains the JWT token the user can use in the Authorization header for subsequent requests:
{
"token": "..."
}
Each challenge is single-use and expires after the configured TTL (default five minutes).
Recovery codes give the user a way to verify an MFA challenge if they lose access to their primary factor.
Once the user has at least one enabled MFA factor, they can generate recovery codes. First, they must be authenticated:
curl -X POST http://localhost:9000/auth/user/emailpass \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "supersecret"
}'
Then, they can generate recovery codes using the /auth/mfa/recovery-codes route:
curl -X POST http://localhost:9000/auth/mfa/recovery-codes \
-H "Content-Type: application/json" \
-H "Authorization: Bearer {jwt_token}" \
-d '{
"count": 10
}'
In the request body, you can specify the number of recovery codes to generate. The default is either the mfa.recovery_code_count option from the Auth Module's configuration or 10.
The response returns the generated codes in plain text:
{
"recovery_codes": ["abcd-efgh-ijkl", "..."]
}
The user should store these codes in a safe place. The Auth Module stores only the hashed codes, so they cannot be retrieved again.
Generating a new set of recovery codes replaces the previous ones and emits the auth.mfa_recovery_codes_generated event.
To verify an MFA challenge with a recovery code, send a POST request to /auth/mfa/challenges/{id}/verify with method set to recovery_code and the code set to one of the valid recovery codes:
curl -X POST http://localhost:9000/auth/mfa/challenges/{id}/verify \
-H "Content-Type: application/json" \
-d '{
"method": "recovery_code",
"code": "abcd-efgh-ijkl"
}'
Each recovery code can be used only once. Once consumed, it cannot be reused for future challenges.
To disable an enabled MFA factor, the client must be authenticated and send a DELETE request to /auth/mfa/factors/{id}:
curl -X DELETE http://localhost:9000/auth/mfa/factors/{id} \
-H "Content-Type: application/json" \
-H "Authorization: Bearer {jwt_token}" \
-d '{
"method": "totp",
"code": "123456"
}'
Whether the method and code fields are required depends on the value of the Auth Module's disable_policy option:
session (default): A valid session is enough to disable the factor. The method and code fields are optional.challenge: The user must additionally verify with an MFA method (TOTP code or recovery code) before the factor can be disabled.Use the challenge policy to protect users from a stolen session being used to remove their MFA factor.
On success, the factor's status becomes disabled, and the auth.mfa_disabled event is emitted.
The client can list the MFA factors enrolled for the authenticated user by sending a GET request to /auth/mfa/factors:
curl -X GET http://localhost:9000/auth/mfa/factors \
-H "Content-Type: application/json" \
-H "Authorization: Bearer {jwt_token}"
The response includes each factor's id, provider, status, and metadata. Use this route to render the user's MFA settings page in your application.
The Auth Module emits the following events around MFA:
auth.mfa_enabled: Emitted when a factor transitions from pending to enabled.auth.mfa_disabled: Emitted when an enabled factor is disabled.auth.mfa_recovery_codes_generated: Emitted when a user generates a new set of recovery codes.Subscribe to these events to send confirmation emails, audit log entries, or admin notifications.
Refer to the Events Reference for the full list of Auth Module events.