Skip to content

Email OTP

EmailOTPFactor signs users in with short numeric codes sent by email. It suits users who don't have an authenticator app, or as a step-up factor for sensitive actions.

Setup

from neozen.core.factors.email_otp import EmailOTPConfig, EmailOTPFactor

factor = EmailOTPFactor(
    config=EmailOTPConfig(
        code_length=6,
        code_lifetime=timedelta(minutes=10),
        max_pending_per_subject=3,
        max_attempts=5,
        app_name="My App",
    ),
    code_repository=otp_repo,
    subject_email_lookup=email_lookup,
    email_sender=sender,  # any EmailSenderProtocol
    code_hash_key="secret",
)

Flow

# 1. User requests a code
result = await factor.begin_authentication(user_id)
# Always returns success-shaped response (anti-enumeration)

# 2. User enters the code from their email
ok = await factor.complete_authentication(user_id, response="123456")
if ok:
    # Authenticated -- proceed with login or step-up
    ...

Configuration

Setting Default Description
code_length 6 Number of digits
code_lifetime 10 minutes How long codes are valid
max_pending_per_subject 3 Rate limit on active codes
rate_limit_window 1 hour Window for rate limiting
max_attempts 5 Max verification attempts before invalidation
app_name "NeoZen" Name shown in email

Security

  • Codes are short (6 digits) and short-lived (10 minutes)
  • Hashes (HMAC-SHA256) are stored, not plain codes
  • Rate limited per subject
  • Single-use: marked as used after verification
  • Max attempts: codes are invalidated after too many wrong guesses
  • Constant-time comparison
  • Anti-enumeration: response never reveals whether the email exists