Back to Medusa

{metadata.title}

www/apps/resources/app/commerce-modules/auth/mfa/page.mdx

2.16.011.9 KB
Original Source

import { Prerequisites } from "docs-ui"

export const metadata = { title: Multi-Factor Authentication, }

{metadata.title}

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" } ]} />

What is Multi-Factor Authentication?

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:

  • MFA factor: A second factor enrolled for an auth identity, such as a TOTP authenticator. A factor is enabled only after the user verifies it once during setup.
  • MFA challenge: A short-lived challenge issued during authentication. The user must verify the challenge with a registered MFA method to obtain a session token.

Configure MFA Encryption Key

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:

bash
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:

ts
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.


MFA Providers

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

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.

Step 1: Authenticate with the Primary Factor

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:

bash
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.

Step 2: Start Enrollment

The client sends a POST request to /auth/mfa/factors to start enrollment for a specific provider:

bash
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:

json
{
  "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.

Step 3: Verify the Factor

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:

bash
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.


Authenticating with MFA

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.

Step 1: Authenticate with the Primary Factor

The user initiates authentication with their primary factor (for example, by logging in with their email and password):

bash
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:

json
{
  "mfa_challenge": {
    "id": "authmfachal_...",
    "methods": ["totp", "recovery_code"]
  }
}

The methods array lists the verification methods the user can complete to satisfy the challenge.

Step 2: Verify 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:

bash
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:

json
{
  "token": "..."
}

Each challenge is single-use and expires after the configured TTL (default five minutes).


Recovery Codes

Recovery codes give the user a way to verify an MFA challenge if they lose access to their primary factor.

Generating Recovery Codes

Once the user has at least one enabled MFA factor, they can generate recovery codes. First, they must be authenticated:

bash
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:

bash
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:

json
{
  "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.

Using a Recovery Code

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:

bash
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.


Disabling an MFA Factor

To disable an enabled MFA factor, the client must be authenticated and send a DELETE request to /auth/mfa/factors/{id}:

bash
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.


Listing Enrolled Factors

The client can list the MFA factors enrolled for the authenticated user by sending a GET request to /auth/mfa/factors:

bash
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.


Events

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.