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: [
    { resolve: "@perseidesjs/auth-otp", options: {} }
  ],
  // ... other 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
})
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
})
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
  }
}
OTP Options
- digits: The number of digits in the OTP to generate.
- ttl: The time to live for the OTP in seconds before it expires.
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' }
  }
}
- 
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 theiremailfield.
- 
entityIdAccessor: Specifies which field from the actor object should be used to find the corresponding auth identity.
When processing an OTP request, the plugin:
- Uses the accessorfield to find the actor record
- Extracts the value from the entityIdAccessorfield from that actor record
- Uses this extracted value to query for matching auth identities
For example, if you’ve registered customers using the standardemailpass 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' }
  }
}
{
  accessorsPerActor: {
    customer: { accessor: ['phone', 'email'], entityIdAccessor: 'email' }
  }
}
{
  accessorsPerActor: {
    customer: { accessor: 'email', entityIdAccessor: 'email' },
    user: { accessor: 'email', entityIdAccessor: 'email' }
  }
}
HTTP Options
The http configuration provides options for handling HTTP requests:
- 
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.
- 
warnOnError: Logs a warning when an error occurs during OTP generation. This helps you catch and handle errors that might occur during OTP generation.