FeaturesAuthenticationOTP & Magic Link

OTP & Magic Link

Passwordless sign-in via one-time codes sent to email or SMS. The flow is two steps: send a code, then verify it.

How it works

  1. User enters their email (or phone)
  2. Aerostack sends a 6-digit code
  3. User enters the code — they’re signed in

No password required. No link to click. The code expires in 10 minutes.

Send OTP

import { useAuth } from '@aerostack/react'
 
function OtpStep1() {
  const { sendOTP } = useAuth()
 
  const handleSend = async () => {
    await sendOTP('[email protected]', 'email')
    // Show code input form
  }
}

Verify OTP

function OtpStep2() {
  const { verifyOTP, user } = useAuth()
 
  const handleVerify = async () => {
    await verifyOTP('[email protected]', '123456', 'email')
    // user is now signed in
  }
}

Complete OTP login component

import { useState } from 'react'
import { useAuth } from '@aerostack/react'
 
export function OtpLogin() {
  const { sendOTP, verifyOTP, user, loading, error } = useAuth()
  const [step, setStep] = useState<'email' | 'code'>('email')
  const [email, setEmail] = useState('')
  const [code, setCode] = useState('')
 
  const handleSend = async (e) => {
    e.preventDefault()
    await sendOTP(email)
    setStep('code')
  }
 
  const handleVerify = async (e) => {
    e.preventDefault()
    await verifyOTP(email, code)
  }
 
  if (user) return <p>Signed in as {user.email}</p>
 
  return (
    <>
      {step === 'email' && (
        <form onSubmit={handleSend}>
          <input
            type="email"
            value={email}
            onChange={e => setEmail(e.target.value)}
            placeholder="[email protected]"
          />
          <button disabled={loading}>Send code</button>
        </form>
      )}
 
      {step === 'code' && (
        <form onSubmit={handleVerify}>
          <p>Enter the 6-digit code sent to {email}</p>
          <input
            value={code}
            onChange={e => setCode(e.target.value)}
            maxLength={6}
            placeholder="123456"
          />
          {error && <p className="text-red-500">{error}</p>}
          <button disabled={loading}>Verify</button>
          <button type="button" onClick={() => setStep('email')}>Back</button>
        </form>
      )}
    </>
  )
}

OTP verification is rate-limited to 3 attempts per code. After 3 failures, the code is invalidated and a new one must be requested.