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
# Email service configurationSMTP_HOST=smtp.example.comSMTP_PORT=587SMTP_PASSWORD=your-smtp-passwordSMTP_FROM_NAME=Authrim
# OTP settingsOTP_LENGTH=6 # Number of digits (default: 6)OTP_EXPIRY_SECONDS=300 # Validity period (default: 5 minutes)OTP_MAX_ATTEMPTS=3 # Max verification attemptsOTP_RATE_LIMIT_WINDOW=3600 # Rate limit window in secondsOTP_RATE_LIMIT_MAX=5 # Max OTPs per window per emailClient 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/requestContent-Type: application/json
{ "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/verifyContent-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 hintconst 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:
| Limit | Default | Description |
|---|---|---|
| Per email | 5/hour | Max OTP requests per email address |
| Per IP | 20/hour | Max OTP requests per IP address |
| Global | 1000/hour | Max OTP requests across all users |
| Verification attempts | 3 | Max attempts per OTP |
Brute Force Protection
// Verification with attempt trackingasync 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
- Use appropriate expiry times: 5 minutes is a good balance between security and usability
- Implement rate limiting: Prevent enumeration and brute force attacks
- Hash OTP codes: Never store plain-text OTP codes in the database
- Clear UI: Show remaining time and allow resend after expiry
- Audit logging: Log all OTP requests and verification attempts
- Email deliverability: Use reputable SMTP providers and configure SPF/DKIM/DMARC
Comparison with Other Methods
| Method | Security | Usability | Requirements |
|---|---|---|---|
| Mail OTP | High | High | Email access |
| Password | Medium | Medium | Memory |
| TOTP | High | Medium | Authenticator app |
| Passkey | Very High | High | Compatible device |
| SMS OTP | Medium | High | Phone 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