Authenticate with OTP

Once installed, the plugin exposes API endpoints for generating and verifying OTPs.

Intercepting the event

Like any event on Medusa, you just need to create a “Subscriber” to intercept the event and send it to your users with your favorite notification provider.

import {
  SubscriberArgs,
  type SubscriberConfig,
} from "@medusajs/framework"
import { type OtpGeneratedEvent, Events } from "@perseidesjs/auth-otp/types"

export default async function otpGeneratedHandler({
  event: { data },
  container
}: SubscriberArgs<OtpGeneratedEvent>) {
  const { otp, identifier } = data
  console.log("An OTP was generated :", otp)

  // Use any notification provider to send the OTP to the user
}

export const config: SubscriberConfig = {
  event: Events.OTP_GENERATED, // ✅ This event is emitted when the OTP is generated
}

Generating an OTP

Let’s dive into the first step of the OTP flow. This API route will emit an event that you should handle to send the OTP to your users.

curl --request POST \
  --url http://localhost:9000/auth/{actor_type}/otp/generate \
  --header 'Content-Type: application/json' \
  --data '{
    "identifier": "<string>"
}'

The {actor_type} is the type of the actor you want to generate the OTP for. It can be customer, user or any custom actor type.

Verifying an OTP

Last step, you can verify the OTP using the following endpoint to get a fresh JWT token:

curl --request POST \
  --url http://localhost:9000/auth/{actor_type}/otp/verify \
  --header 'Content-Type: application/json' \
  --data '{
    "identifier": "<string>",
    "otp": "<string>"
}'

Response

If the OTP is valid, the endpoint returns a JWT token that can be used for subsequent requests:

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Create new actors without a password

If you want to allow your users to sign up without a password, you can use the OTP as an Auth Provider.

Adding the OTP provider

For that, you’ll need to update your medusa-config.ts file to add the OTP Auth Provider on top of your other auth providers(e.g. emailpass).

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

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

module.exports = defineConfig({
  plugins: [
    "@perseidesjs/auth-otp"
  ],
  // ✅ We need to extend the auth module to add the OTP provider
  modules: [
    {
      resolve: "@medusajs/medusa/auth",
      options: {
        providers: [
          // Medusa default provider
          {
            id: "emailpass",
            resolve: "@medusajs/medusa/auth-emailpass",
            dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER],
          },
          // 🆕 Our OTP provider
          {
            id: "otp",
            resolve: "@perseidesjs/auth-otp/providers/otp",
            dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER],
          },
        ],
      },
    },
  ],
  // ... rest
})

By doing this, you’re letting Medusa know that a new authentication provider is available and that new identities can now be created to create actors using the OTP. In this way, if you want to allow your users to create their accounts either using email and password, or using the OTP, this is possible.

You cannot create an account using the OTP provider for an identifier (e.g., email) that already exists with another auth provider (e.g., emailpass). This will throw a duplicate error from Medusa.

Pre-register process

Before we can create new accounts using the OTP provider, we need to go through the pre-registration process. This process essentially validates the identifier used before retrieving the registration token that will allow us to create an account.

curl --request POST \
  --url http://localhost:9000/auth/{actor_type}/otp/pre-register \
  --header 'Content-Type: application/json' \
  --data '{
    "identifier": "<string>"
  }'

When this route is triggered, and no actors with the provided identifier exist, an event will be emitted. We can subscribe to this event to send the generated OTP to the user.

import {
  SubscriberArgs,
  type SubscriberConfig,
} from "@medusajs/framework"
import { type OtpGeneratedEvent, Events } from "@perseidesjs/auth-otp/types"

export default async function otpGeneratedHandler({
  event: { data },
  container
}: SubscriberArgs<OtpGeneratedEvent>) {
  const { otp, identifier } = data
  console.log("An OTP was generated :", otp)

  // Use any notification provider to send the OTP to the user
}

export const config: SubscriberConfig = {
  event: Events.PRE_REGISTER_OTP_GENERATED, // ✅ This event is emitted when the OTP is generated for the pre-register process
}

Once the OTP is sent to the user, it will be verified on the native Medusa register route. This verification process will allow you to obtain the token necessary to create an actor (customer, user, etc.).

curl --request POST \
  --url http://localhost:9000/auth/{actor_type}/otp/register \
  --header 'Content-Type: application/json' \
  --data '{
    "identifier": "<string>",
    "otp": "<string>"
  }'

Next Steps

For more advanced configuration options and customizations, refer to the “Customization” guide.