Nest Authbeta

Passwordless OTP

One-time codes over email or SMS — no password required.

The user enters their email or phone, gets a 6-digit code, and types it back. No password.

Server config

NestAuthModule.forRoot({
  passwordless: {
    enabled: true,
    allowSignUp: true,         // create a new user if the identifier doesn't exist
  },
  otp: {
    length: 6,
    format: 'numeric',         // or 'alphanumeric'
    codeExpiresIn: '10m',
  },
});

allowSignUp: false disables the auto-signup behavior — passwordless then only works for existing users.

Endpoints

MethodPathPurpose
POST/auth/passwordless/send{ identifier, channel: 'email' | 'sms' } — emits PasswordlessCodeRequestedEvent
POST/auth/login{ providerName: 'passwordless', credentials: { identifier, code, channels } }

The send endpoint generates the code and emits an event. Your listener delivers it via Resend / Twilio / etc.

Client call

// Step 1: ask the server to send a code
await client.passwordlessSend({ identifier: 'alice@example.com', channel: 'email' });
 
// Step 2: user types the code; submit it
await client.login({
  providerName: 'passwordless',
  credentials: {
    identifier: 'alice@example.com',
    code: '123456',
    channels: ['email'],
  },
});

Listener (server)

@OnEvent(NestAuthEvents.PASSWORDLESS_CODE_REQUESTED)
async send(event: PasswordlessCodeRequestedEvent) {
  if (event.channel === 'email') {
    await this.email.send(event.identifier, 'passwordless-code', { code: event.code });
  } else {
    await this.sms.send(event.identifier, `Your code is ${event.code}`);
  }
}

See Sending Emails and Sending SMS for production examples.

Custom code generation

otp: {
  async generate(length, format) {
    return crypto.randomInt(100000, 999999).toString();
  },
  length: 6,
  format: 'numeric',
}
  • Magic Link — clickable URL instead of a typed code.
  • MFA — passwordless OTPs are reused for MFA challenges, but the flows are independent.

On this page