There are three ways to integrate the auth-otp plugin into your Medusa application, each offering different levels of customization.

Method 1: Simple Direct Integration

For the most straightforward setup, simply add the plugin to your medusa-config.ts file:

import { loadEnv, defineConfig } from '@medusajs/framework/utils'

loadEnv(process.env.NODE_ENV || 'development', process.cwd())

module.exports = defineConfig({
  plugins: [
    "@perseidesjs/auth-otp"
  ],
  // ... other configuration
})

With this approach, the plugin will use its default configuration.

Method 2: Custom Options

When you need more control over how OTP works in your application, you can provide custom options:

import { loadEnv, defineConfig } from '@medusajs/framework/utils'
import type { OtpOptions } from '@perseidesjs/auth-otp/types/index'

loadEnv(process.env.NODE_ENV || 'development', process.cwd())

module.exports = defineConfig({
  plugins: [
    {
      resolve: "@perseidesjs/auth-otp",
      options: {
        digits: 6, // Will generate 6 digits
        ttl: 60 * 5, // OTP lives for 5 minutes
      } satisfies Partial<OtpOptions>
    }
  ],
  // ... other configuration
})

This configuration gives you the ability to fine-tune the OTP experience.

Method 3: Advanced Integration with Auth Module

For advanced use cases, such as creating accounts with OTP-only authentication (no passwords), we can extend Medusa’s Auth Module:

import { loadEnv, defineConfig, Modules, ContainerRegistrationKeys } from '@medusajs/framework/utils'
import type { OtpOptions } from '@perseidesjs/auth-otp/types/index'

loadEnv(process.env.NODE_ENV || 'development', process.cwd())

module.exports = defineConfig({
  plugins: [
    {
      resolve: "@perseidesjs/auth-otp",
      options: {
        digits: 6,
        ttl: 60 * 5, // OTP lives for 5 minutes
      } satisfies Partial<OtpOptions>
    }
  ],
  modules: [
    {
      resolve: "@medusajs/medusa/auth",
      options: {
        providers: [
          // default provider
          {
            resolve: "@medusajs/medusa/auth-emailpass",
            dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER],
            id: "emailpass",
          },
          {
            resolve: "@perseidesjs/auth-otp/providers/otp",
            id: "otp",
            dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER],
          },
        ],
      },
    },
  ],
  // ... other configuration
})

This configuration creates a new authentication system where OTP becomes a first-class authentication method alongside traditional password authentication.

When using the otp provider, you’re planning to add a new Auth Provider to your Medusa apps, allowing you to create new accounts with OTP-only authentication, if your goal is just to have OTP as a secondary authentication method, do not use this method and stick to the plugin approach only.


Default options

When you don’t specify options, the plugin uses these sensible defaults:

{
  digits: 6, // 6 digits
  ttl: 60 * 5, // 5 minutes
  accessorsPerActor: {
    customer: { accessor: 'email', entityIdAccessor: 'email' },
    user: { accessor: 'email', entityIdAccessor: 'email' }
  },
  http: {
    alwaysReturnSuccess: true, // Always return success to prevent data leakage
    warnOnError: true // Warn when an error occurs during OTP generation
  }
}
  • digits: The number of digits in the OTP to generate.
  • ttl: The time to live for the OTP in seconds

Accessors

The accessorsPerActor configuration controls how the plugin identifies and retrieves actors (users, customers or any custom actor) when processing OTP requests. This configuration specifies two critical fields for each actor type:

{
  accessorsPerActor: {
    customer: { accessor: 'email', entityIdAccessor: 'email' }
  }
}

The two key properties are:

  1. accessor: Defines which field is used to initially locate the actor in the database when a request is made. In the example above, customers would be looked up by their email field.

  2. entityIdAccessor: Specifies which field from the actor object should be used to find the corresponding auth identity.

When processing an OTP request, the plugin:

  1. Uses the accessor field to find the actor record
  2. Extracts the value from the entityIdAccessor field from that actor record
  3. Uses this extracted value to query for matching auth identities

For example, if you’ve registered customers using the standard emailpass provider (which uses email as the entity ID), but want to allow OTP authentication via phone numbers, you would configure:

{
  accessorsPerActor: {
    customer: { accessor: 'phone', entityIdAccessor: 'email' }
  }
}

This tells the system: “Find the customer by their phone number, then look up their auth identity using their email field.”

You can also use an array of accessors to support multiple ways to identify the actor:

{
  accessorsPerActor: {
    customer: { accessor: ['phone', 'email'], entityIdAccessor: 'email' }
  }
}

In that case, the plugin will try to find the actor using the first accessor in the array, and if it doesn’t find it, it will try the next one.

The default configuration uses email for both finding actors and determining their identity:

{
  accessorsPerActor: {
    customer: { accessor: 'email', entityIdAccessor: 'email' },
    user: { accessor: 'email', entityIdAccessor: 'email' }
  }
}

HTTP Options

The http configuration provides options for handling HTTP requests:

  1. alwaysReturnSuccess: Determines whether the plugin should always return a success response, even if an error occurs. This helps prevent data leakage by ensuring that the response is not affected by errors.

  2. warnOnError: Logs a warning when an error occurs during OTP generation. This helps you catch and handle errors that might occur during OTP generation.