Skip to content

Password Authentication (Email + Password) Setup Guide

Password Authentication is a traditional email + password login method where users create accounts with a password and verify their email via a secure link. After verification, users can log in with email and password, and change or reset their password as needed.

Password authentication provides two main workflows:

  1. Registration — New user creates account with email, password, and name; verifies email via link
  2. Login — Existing verified user authenticates with email and password (with optional 2FA)

Additional flows: Password Reset (forgot password via email), Password Change (authenticated password update), Email Verification (admin resend).


Endpoint: POST /auth/register-password

Request:

{
"email": "user@example.com",
"name": "Alice Johnson",
"password": "SecureP@ss123!",
"cf-turnstile-response": "<turnstile-token>"
}

Security Checks:

  • ✅ Turnstile bot protection (required)
  • ✅ IP-based registration rate limit (5 registrations/hour per IP)
  • ✅ Email velocity tracking (3 attempts/hour per email, 5+ IPs = harassment detected)
  • ✅ Password policy validation (min 8 chars, complexity requirements)
  • ✅ Password breach check (Have I Been Pwned via k-anonymity)

Step 2: Account Created with Pending Verification

Section titled “Step 2: Account Created with Pending Verification”

If all checks pass:

  • New user record created in backend with authentication_status: "Email Verification Pending"
  • authentication_type: ["Password"] set (user can add Magic Login later)
  • _2fa_status: "Disabled" (user can enable 2FA after email verification)
  • Password hashed with per-user salt (stored as password_hash and password_salt)
  • Account is locked until email verification completes

Response:

{
"success": true,
"recordid": "123456789",
"message": "Account created. Please check your email to verify your account."
}
  • Verification token (32-byte, base64-encoded) generated
  • Token stored in Cloudflare KV with 24-hour expiration
  • Email sent via emailVerificationWebhook (tenant-configured automation)
  • Email contains verification link: https://portal.example.com/email-verified/{recordId}/{token}

Webhook Payload:

{
"event_type": "verify_email",
"recordid": "123456789",
"email": "user@example.com",
"name": "Alice Johnson",
"verificationLink": "https://your.domain.com/email-verified/123456789/abc123..."
}

Endpoint: POST /auth/verify-email

Request:

{
"recordId": "123456789",
"token": "abc123..."
}

Validation:

  • Token must match Cloudflare KV stored token (one-time use)
  • Token must not be expired (24-hour window)

On Success:

  • authentication_status set to "Unlocked"
  • Token deleted from Cloudflare KV (can’t be reused)
  • User account is now active and can log in

Endpoint: POST /auth/login

Request:

{
"email": "user@example.com",
"password": "SecureP@ss123!",
"cf-turnstile-response": "<turnstile-token>"
}

Security Checks:

  • ✅ Turnstile bot protection
  • ✅ Login velocity tracking (detects distributed attacks: 10+ IPs in 15 min)
  • ✅ Progressive lockout (exponential penalties: 1 hour → 4 hours → 24 hours → 7 days)
  • ✅ Account status validation:
    • Account must not be Locked (manually locked by admin)
    • Email must be verified (authentication_status != "Email Verification Pending")
    • Password change not required (authentication_status != "Password Change Pending")

If Turnstile and velocity checks pass:

  • Password hashed with stored salt
  • Hash compared to stored password_hash
  • If mismatch: increment failed login count and apply progressive lockout

Failed Login Response (429):

{
"authorized": false,
"message": "Too many failed attempts. Please try again later."
}

Step 3a: Password Correct, No 2FA Required

Section titled “Step 3a: Password Correct, No 2FA Required”

If password matches and 2FA disabled:

  • Session created with user’s recordId
  • Session cookie set: session_id={sessionId}; HttpOnly; Secure; SameSite=Lax
  • Session TTL determined by sessionTier (15 mins to 30 days)
  • Geo-location tracked (for anomaly detection on next login)

Response (200 OK):

{
"authorized": true,
"recordid": "123456789",
"redirectUrl": "/dashboard"
}

Set-Cookie Header:

session_id=sess_abc123...; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=3600

If password matches but 2FA is enabled and no code provided:

Response (200 OK):

{
"authorized": false,
"two_factor_required": true,
"message": "Please enter your 2FA code."
}

Frontend prompts for 2FA code and re-calls endpoint with twoFACode:

{
"email": "user@example.com",
"password": "SecureP@ss123!",
"twoFACode": "123456",
"cf-turnstile-response": "<turnstile-token>"
}

2FA Validation:

  • ✅ TOTP code verified (time-based one-time password)
  • ✅ OR recovery code verified (one-time use recovery codes)
  • ✅ Max 3 failed 2FA attempts before temporary lockout (15 minutes)

2FA Success: Session created and returned as above.


Endpoint: POST /auth/forgot-password

Request:

{
"email": "user@example.com",
"cf-turnstile-response": "<turnstile-token>"
}

Security Checks:

  • ✅ Turnstile bot protection (required)
  • ✅ IP-based rate limit (10 requests/hour per IP)
  • ✅ Email velocity tracking (5+ IPs in 1 hour = harassment)

Step 2: Conditions for Sending Reset Email

Section titled “Step 2: Conditions for Sending Reset Email”
ConditionResult
Account not foundGeneric success (email enumeration protection)
Account lockedGeneric success (don’t reveal locked status)
All checks passPassword reset email sent

Why generic responses? Prevents attackers from discovering which emails are registered.

  • Reset token (32-byte, base64-encoded) generated
  • Token stored in Cloudflare KV with 24-hour expiration
  • Email sent via passwordResetWebhook (tenant-configured automation)
  • Email contains reset link: https://portal.example.com/reset-password/{recordId}/{token}
  • All existing sessions for this user are invalidated (forced re-login after password change)

Webhook Payload:

{
"event_type": "password_reset",
"recordid": "123456789",
"email": "user@example.com",
"name": "Alice Johnson",
"resetLink": "https://your.domain.com/reset-password/123456789/xyz789..."
}

Endpoint: POST /auth/reset-password

Request:

{
"recordId": "123456789",
"token": "xyz789...",
"newPassword": "NewSecureP@ss456!"
}

Validation:

  • ✅ Reset token must match Cloudflare KV (one-time use)
  • ✅ Token must not be expired (24-hour window)
  • ✅ New password must pass policy (min 8 chars, complexity, breach check)

On Success:

  • Token deleted from Cloudflare KV (can’t be reused)
  • Password hashed with new salt and stored
  • All sessions invalidated (user must log in again)
  • authentication_status reset to "Unlocked" (if was “Password Change Pending”, now clear)

Response (200 OK):

{
"success": true,
"message": "Password updated successfully. Please log in with your new password."
}

Endpoint: POST /auth/change-password

Request:

Headers:

Cookie: session_id=sess_abc123...
X-Tenant-Domain: portal.example.com

Body:

{
"currentPassword": "SecureP@ss123!",
"newPassword": "NewSecureP@ss456!"
}

Security Checks:

  • ✅ Session validation (user must be logged in)
  • ✅ Current password verification (prevent unauthorized changes)
  • ✅ Password policy validation (same as registration)
  • ✅ Password breach check (Have I Been Pwned)
  • Old password hashes invalidated
  • New password hashed with new salt
  • All other sessions for this user invalidated (forced re-login on other devices)
  • Current session remains active (no immediate logout)

Response (200 OK):

{
"success": true,
"message": "Password changed successfully."
}

Endpoint: POST /auth/admin/resend-verification-email

For cases where user’s verification email was lost or expired. See Resend Verification Email guide for details.


  • Tokens are deleted immediately after use (Cloudflare KV.delete)
  • Cannot be reused even within the expiration window
  • If user clicks link twice, second click fails with “Invalid or expired token”
  • Size: 32 random bytes (256 bits of entropy)
  • Encoding: Base64 URL-safe (+ replaced with -, / replaced with _, = removed)
  • Example: abc123-_XYZ789 (no +/= characters)
  • Cloudflare KV Key Format:
    • Email verification: email_verify:{hostname}:{recordId}
    • Password reset: password_reset:{hostname}:{recordId}
  • TTL:
    • Email verification: 24 hours
    • Password reset: 24 hours
  • No plaintext in logs — only token existence is logged, never the actual token

  • Per-user salt: 32 random bytes generated at registration
  • Hash function: PBKDF2 with SHA-256
  • Stored fields: password_hash and password_salt in user record

Enforced at registration and reset:

  • Minimum 8 characters
  • At least one uppercase letter
  • At least one lowercase letter
  • At least one number or special character
  • Not in Have I Been Pwned database (checked via k-anonymity API)

LimitScopeWindowBlock Action
5 registrationsSingle IP1 hour429 Too Many Requests
3 attemptsSingle email1 hour429 Too Many Requests
5+ IPsSingle email1 hourLog warning (allowed but flagged)
ThresholdAction
5 IPs in 15 minLogged as suspicious
10 IPs in 15 min30-minute lockout
3 failed login attempts1-hour lockout (exponential: 1h → 4h → 24h → 7 days)
LimitScopeWindowBlock Action
10 requestsSingle IP1 hour429 Too Many Requests
5+ IPsSingle email1 hour429 Too Many Requests

After successful login, a session is created on Mindful Auth Cloudflare KV with:

Session Data (stored in KV):

{
"recordId": "123456789",
"hostname": "portal.example.com",
"iat": 1705779445,
"exp": 1705783045,
"is2FAEnabled": false,
"require2FASetup": false,
"passwordChangePending": false,
"loginContext": null,
"attemptedPath": null
}

Session Cookie (sent to client):

  • Name: session_id
  • Value: Cryptographically random sessionId
  • Flags:
    • HttpOnly — JavaScript cannot access (prevents XSS token theft)
    • Secure — Only sent over HTTPS (prevents man-in-the-middle)
    • SameSite=Lax — Prevents CSRF attacks on form submissions
    • Max-Age — Matches configured session TTL (15 mins to 30 days)
    • Path=/ — Valid for all paths on domain

FeaturePasswordMagic Link
Credentials requiredEmail + passwordEmail only
VerificationEmail link + passwordEmail link
Reset flowPassword reset via emailPassword reset via email (or add password)
Phishing riskMedium (password reuse)Low (token expires in 15 min)
UsabilityMedium (remember password)High (click link)
2FA capableYes (optional)Yes (optional)
Session TTLConfigurable (15 min - 30 days)Configurable (15 min - 30 days)
Hybrid supportYes (can add magic link)Yes (can add password)

Password authentication generates comprehensive audit logs. See Audit Logs Guide for details.


Adding Password Authentication to Your Tenant

Section titled “Adding Password Authentication to Your Tenant”

During tenant onboarding, provide:

{
"emailVerificationWebhook": "https://your-automation.com/verify-email",
"passwordResetWebhook": "https://your-automation.com/reset-password"
}

These are webhook URLs pointing to your email automation service (Tape automation, Make, Zapier, n8n, etc.).

Your automation should:

  • Accept recordid, email, name, verificationLink (registration flow)
  • Accept recordid, email, name, resetLink (password reset)
  • Send branded emails with secure links
  • Never log or display tokens in plaintext
  • Create registration form with email, name, password, and Turnstile widget
  • Create login form with email, password, and Turnstile widget
  • Create /email-verified/{recordId}/{token} page to verify email
  • Create /reset-password/{recordId}/{token} page to reset password
  • Create authenticated password change page for logged-in users
  • Handle two_factor_required: true response by prompting for 2FA code

IssueCauseSolution
User gets “Invalid or expired token”Token expired after 24h or already usedRequest new verification/reset email
”You’ve registered too many times from this IP”Rate limit triggeredWait 1 hour or use different IP
”Password doesn’t meet policy requirements”Weak password or breach check failedUse stronger password; check Have I Been Pwned
”Too many failed login attempts”Progressive lockout triggeredWait 1 hour (or longer if repeating)
“Your account has been locked”Admin locked accountContact support to unlock
Email never arrivesWebhook not configured or automation failedCheck webhook URL in tenant config
User can’t log in after password resetAll sessions invalidated, browser cache issueClear cookies and try again
”Email verification pending” error on loginUser hasn’t clicked verification link yetSend verification email and click link