Designing authentication for borrowers who have never created a password
How we built stateless OTP login and anti-enumeration defenses for legal arbitration notices.
The Trigger & Underlying Problem
The Trigger: Over 5,000 legal notices were being dispatched daily to borrowers facing arbitration proceedings.
The Core Problem: Recipients were receiving notices containing a case number and a website link. Most borrowers were unfamiliar with password managers or complex account registration. Requiring standard sign-up flows resulted in massive abandonment and flooded customer phone lines.
Why Standard Solutions Were Insufficient: Standard authentication services (Auth0, Firebase) assume users have verified emails or want social login. Our users often had only a Loan Account Number (LAN) or Arbitration Case Number (ARB) on a physical letter or SMS.
What It Solves in Practice
Allows borrowers to immediately log in, see their claim status, review notices, and access settlement options without needing a pre-registered password.
Borrowers facing dispute proceedings and firm case managers answering status inquiries.
Borrowers had to call customer support hotlines, wait on hold, and manually verify identity over the phone while staff searched paper files.
Architecture & Key Engineering Decisions
A stateless OTP verification mechanism built into FastAPI. Borrowers input their Case Number or Loan Account Number. The system auto-detects the prefix, retrieves the masked phone/email on file, and sends an ephemeral OTP.
- Stateless HMAC-signed verification tokens with a 60-second time-to-live instead of storing OTP state in a database table.
- Dynamic OTP delivery routing: prioritizes email delivery over SMS to save messaging costs, falling back to SMS when email is absent.
- Anti-enumeration response normalization: valid and invalid lookups return identical response timing to defeat automated scraping of valid case IDs.
import hmac, hashlib, time
SECRET_KEY = b"firm-auth-signing-key"
def generate_otp_token(case_id: str, otp_code: str) -> str:
timestamp = int(time.time())
payload = f"{case_id}:{otp_code}:{timestamp}"
signature = hmac.new(SECRET_KEY, payload.encode(), hashlib.sha256).hexdigest()
return f"{payload}:{signature}"
def verify_otp_token(token: str, submitted_code: str, max_age_secs: int = 60) -> bool:
try:
case_id, expected_code, ts_str, sig = token.split(":")
if time.time() - int(ts_str) > max_age_secs:
return False
if not hmac.compare_digest(expected_code, submitted_code):
return False
expected_sig = hmac.new(SECRET_KEY, f"{case_id}:{expected_code}:{ts_str}".encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(sig, expected_sig)
except Exception:
return FalseWhat Failed in Practice & How It Was Addressed
- Offline borrowers: Some cases had neither an email nor phone number on file because the borrower had moved or changed SIM cards.
- Attackers could probe sequential case numbers to detect which dispute IDs actually existed in the system.
- SMS gateway delivery delays during peak evening hours caused OTPs to arrive after their 60-second expiration window.
- Cross-verification fallback: Offline borrowers with physical notices must match two pieces of data from the letter (LAN + ARB number). Once matched, they enter their current phone/email, triggering an operational review before unlocking.
- Constant-time responses and rate limits: invalid case lookups perform a synthetic hash computation and always return the same generic message: "If this record exists, an OTP has been sent".
- Added dynamic fallback resend timers and allowed a grace window of 120 seconds for SMS channels while keeping email at 60 seconds.
Takeaways & Future Iterations
- Security and user friction are not a zero-sum game. You can build passwordless authentication that is both highly secure and accessible to people with minimal technical literacy.
- Always protect public lookup endpoints against enumeration from day one.
I would add passkey/WebAuthn support as an optional one-tap upgrade after the first OTP verification for mobile users with biometrics.