Skip to content

Mail OTP

Mail OTP (One-Time Password) provides a passwordless authentication method that sends a temporary code to the user’s email address. This approach eliminates the need for users to remember passwords while maintaining strong security.

Overview

sequenceDiagram
    participant User
    participant Authrim
    participant Email as Email Service (SMTP)

    User->>Authrim: Enter email address
    Authrim->>Authrim: Generate OTP code
    Authrim->>Email: Send OTP email
    Email->>User: Deliver OTP code
    User->>Authrim: Enter OTP code
    Authrim->>Authrim: Validate code
    Authrim->>User: Issue tokens

Features

  • Passwordless: No password to remember or manage
  • Phishing-resistant: OTP codes are time-limited and single-use
  • Universal: Works on any device with email access
  • No app required: Unlike TOTP, no authenticator app needed
  • Familiar UX: Users are accustomed to email-based verification

Configuration

Environment Variables

Terminal window
# Email service configuration
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_PASSWORD=your-smtp-password
SMTP_FROM_NAME=Authrim
# OTP settings
OTP_LENGTH=6 # Number of digits (default: 6)
OTP_EXPIRY_SECONDS=300 # Validity period (default: 5 minutes)
OTP_MAX_ATTEMPTS=3 # Max verification attempts
OTP_RATE_LIMIT_WINDOW=3600 # Rate limit window in seconds
OTP_RATE_LIMIT_MAX=5 # Max OTPs per window per email

Client Configuration

Enable Mail OTP for your OAuth client:

{
"client_id": "your-client-id",
"allowed_grant_types": ["authorization_code", "refresh_token"],
"allowed_auth_methods": ["mail_otp", "password", "passkey"],
"mail_otp_config": {
"enabled": true,
"template": "default",
"subject": "Your login code"
}
}

API Usage

Step 1: Request OTP

POST /api/auth/otp/request
Content-Type: application/json
{
"email": "[email protected]",
"client_id": "your-client-id"
}

Response:

{
"success": true,
"message": "OTP sent to email",
"expires_in": 300,
"otp_id": "otp_abc123..."
}

Step 2: Verify OTP

POST /api/auth/otp/verify
Content-Type: application/json
{
"otp_id": "otp_abc123...",
"code": "123456",
"client_id": "your-client-id"
}

Success Response:

{
"success": true,
"auth_code": "auth_xyz789...",
"redirect_uri": "https://app.example.com/callback"
}

Error Response:

{
"success": false,
"error": "invalid_otp",
"error_description": "The OTP code is invalid or expired",
"attempts_remaining": 2
}

OAuth 2.0 Integration

Mail OTP integrates seamlessly with the OAuth 2.0 authorization code flow:

// 1. Start authorization with mail_otp hint
const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('client_id', 'your-client-id');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('redirect_uri', 'https://app.example.com/callback');
authUrl.searchParams.set('scope', 'openid profile email');
authUrl.searchParams.set('acr_values', 'urn:authrim:acr:mail_otp'); // Request Mail OTP
window.location.href = authUrl.toString();
// 2. User receives email, enters code on Authrim's UI
// 3. Authrim redirects back with authorization code
// 4. Exchange code for tokens (standard OAuth flow)

Security Considerations

OTP Generation

Authrim generates cryptographically secure OTP codes:

// OTP generation (internal)
function generateOTP(length: number = 6): string {
const array = new Uint8Array(length);
crypto.getRandomValues(array);
return Array.from(array)
.map(b => b % 10)
.join('');
}

Rate Limiting

To prevent abuse, Mail OTP implements multiple rate limits:

LimitDefaultDescription
Per email5/hourMax OTP requests per email address
Per IP20/hourMax OTP requests per IP address
Global1000/hourMax OTP requests across all users
Verification attempts3Max attempts per OTP

Brute Force Protection

// Verification with attempt tracking
async function verifyOTP(otpId: string, code: string): Promise<VerifyResult> {
const otp = await getOTP(otpId);
if (!otp) {
throw new Error('OTP not found or expired');
}
if (otp.attempts >= MAX_ATTEMPTS) {
await deleteOTP(otpId);
throw new Error('Max attempts exceeded');
}
if (otp.code !== code) {
await incrementAttempts(otpId);
return {
success: false,
attempts_remaining: MAX_ATTEMPTS - otp.attempts - 1
};
}
// Success - delete OTP and create session
await deleteOTP(otpId);
return { success: true };
}

Storage

OTP records are stored with security in mind:

CREATE TABLE otp_codes (
id TEXT PRIMARY KEY,
email_blind_index TEXT NOT NULL, -- Hashed email for lookup
code_hash TEXT NOT NULL, -- Hashed OTP code
client_id TEXT NOT NULL,
attempts INTEGER DEFAULT 0,
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
used_at INTEGER,
INDEX idx_email_blind (email_blind_index),
INDEX idx_expires (expires_at)
);

Note: OTP codes are hashed before storage, so even database access won’t reveal the actual codes.

Email Templates

Default Template

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Your Login Code</title>
</head>
<body style="font-family: sans-serif; padding: 20px;">
<h1>Your Login Code</h1>
<p>Use this code to sign in:</p>
<div style="font-size: 32px; font-weight: bold;
letter-spacing: 8px; padding: 20px;
background: #f5f5f5; text-align: center;">
{{OTP_CODE}}
</div>
<p>This code expires in {{EXPIRY_MINUTES}} minutes.</p>
<p>If you didn't request this code, you can safely ignore this email.</p>
</body>
</html>

Custom Templates

Configure custom templates per client:

const emailTemplate = {
subject: "{{APP_NAME}} - Your verification code",
html: `
<div style="max-width: 600px; margin: 0 auto;">
<img src="{{LOGO_URL}}" alt="{{APP_NAME}}" width="150">
<h1>Sign in to {{APP_NAME}}</h1>
<p>Your verification code is:</p>
<code style="font-size: 24px; padding: 10px 20px;
background: #e0f7fa; border-radius: 4px;">
{{OTP_CODE}}
</code>
<p>Valid for {{EXPIRY_MINUTES}} minutes.</p>
</div>
`,
text: "Your {{APP_NAME}} verification code is: {{OTP_CODE}}"
};

Best Practices

  1. Use appropriate expiry times: 5 minutes is a good balance between security and usability
  2. Implement rate limiting: Prevent enumeration and brute force attacks
  3. Hash OTP codes: Never store plain-text OTP codes in the database
  4. Clear UI: Show remaining time and allow resend after expiry
  5. Audit logging: Log all OTP requests and verification attempts
  6. Email deliverability: Use reputable SMTP providers and configure SPF/DKIM/DMARC

Comparison with Other Methods

MethodSecurityUsabilityRequirements
Mail OTPHighHighEmail access
PasswordMediumMediumMemory
TOTPHighMediumAuthenticator app
PasskeyVery HighHighCompatible device
SMS OTPMediumHighPhone number

Troubleshooting

Common Issues

OTP not received:

  • Check spam/junk folder
  • Verify SMTP configuration
  • Check rate limits
  • Ensure email is valid

OTP expired:

  • Request a new code
  • Check system clock synchronization
  • Consider increasing expiry time

Max attempts exceeded:

  • Wait for rate limit window to reset
  • Request a new OTP code
  • Contact support if issue persists