A beginner-friendly authentication project that feels like a real app, but is still small enough to understand end-to-end.
This repo shows the complete auth story:
- Sign up with email verification (4-digit OTP)
- Sign in with optional 2FA:
- Email OTP (4 digits)
- Authenticator app (TOTP, 6 digits)
- Backup codes (10 one-time codes)
- Forgot password (email OTP + reset)
- Account settings (update profile + change password)
It also includes small UX + “production-ish” details beginners usually miss:
- Auto Unique UserName suggestion (slug from display name) on Sign up + Account settings
- “Dirty guard”: if you manually edit Unique UserName, autosuggest stops overwriting it
- OTP resend cooldown + simple rate limiting (API returns
retryAfter) - “Instructor mode”: if email can’t be sent, the UI shows
debug_codeso you can keep learning
- Node.js (recommended: 18+)
- npm
npm install- Create a
.envfile with:
# Copy to .env (DO NOT commit .env)
# Gmail App Password recommended.
BREVO_API_KEY=
BREVO_FROM=""
PORT=3000
# SMTP settings (defaults are Gmail)
SMTP_USER=
SMTP_PASS=
SMTP_HOST=
SMTP_PORT=
SMTP_SECURE=
# Optional mail settings
# MAIL_SUBJECT=
# SMTP (Brevo relay) (alternative to HTTPS APIs)
SMTP_HOST=
SMTP_PORT=
SMTP_SECURE=
SMTP_USER=
SMTP_PASS=
# Sender address (should be a verified sender in Brevo)
MAIL_FROM=
- Optional: change
PORT(default is3000)
npm startOpen the URL printed in your terminal, e.g. http://localhost:3000.
Important: don’t open index.html directly. The UI calls /api/* endpoints and needs the server.
$env:PORT=3009; npm startWhen you click Send code, the server generates a 4-digit OTP and tries to deliver it.
Delivery options (in order):
- Brevo HTTPS API (recommended for hosting)
- SMTP (works locally, but can be blocked by some hosts)
- Instructor / Debug mode (no email sent; the UI displays the OTP)
This is the exact fallback logic used by the server when sending OTP emails:
- If
BREVO_API_KEYis set → try Brevo first - If Brevo fails (or
BREVO_API_KEYis missing) → try SMTP
- SMTP is only attempted if both
SMTP_USERandSMTP_PASSare set
- If SMTP fails (or is not configured) → Instructor/Debug mode
- Response includes
delivered: falseand adebug_code
If you’re asking “what happens if Brevo fails?” → the app will then try SMTP.
- Force Brevo-only: set
BREVO_API_KEY, and remove/emptySMTP_USER+SMTP_PASS - Force SMTP-only: remove/empty
BREVO_API_KEY, and setSMTP_USER+SMTP_PASS
If you want the opposite order (SMTP → Brevo → Debug), that requires a small change in server.js.
Brevo works on platforms that block outbound SMTP.
Required env var:
BREVO_API_KEY
Recommended sender env var:
MAIL_FROM(example:Email Verification Demo <your@gmail.com>)- The sender email must be verified in your Brevo account.
Optional:
BREVO_FROM(if set, Brevo will prefer this sender)MAIL_SUBJECT(custom subject)
In .env:
SMTP_USER= your Gmail addressSMTP_PASS= a Gmail App Password (not your normal password)
OR, visit: https://myaccount.google.com/apppasswords
Optional:
MAIL_FROM(example:Email Verification Demo <your@gmail.com>)MAIL_SUBJECT
If email isn’t configured (or delivery fails), OTP endpoints return:
delivered: falsedebug_code: "1234"
The frontend shows this code so you can keep learning without setting up email.
Render does not want you to upload a .env file.
Instead, copy env vars into Render:
- Render Dashboard → your service → Environment
- Add your env vars (do not wrap values in quotes)
- Deploy
Template file you can use as a checklist:
.envForRender.example
Important:
- Don’t set
PORTon Render (Render injects it automatically) - Never commit secrets (API keys, SMTP passwords)
- Open Sign up
- Enter UserName
- The app auto-suggests a Unique UserName from your name (example:
sagar-biswas) - Enter email → Send code → enter the 4-digit OTP
- Create account
Notes:
- OTPs expire in ~5 minutes
- OTP resend is rate-limited (API returns
retryAfter)
- Enter email + password
- If you enabled a 2FA method, complete one of:
- Email OTP (4 digits)
- Authenticator TOTP (6 digits)
- Backup code (one-time)
After sign-in you can manage:
- 2FA (Email OTP)
- Authenticator (TOTP)
- Backup codes
- Update profile fields (including Unique UserName)
- Unique UserName can be auto-suggested here too
- If you type your own Unique UserName manually, suggestions won’t overwrite it
- Change password requires your current password
- From Sign in, click Forgot password?
- Enter your registered email
- Send code → enter 4-digit OTP
- Set a new password
- Any active sessions for that email are cleared (forces sign-in again)
index.html— UI layout (Sign in / Sign up / Forgot password / Dashboard)src/app.js— frontend logic (fetch to/api/*, view switching, autosuggest, forms)src/styles.css— stylingserver.js— Express API, sessions, OTP/TOTP, backup codes, persistencedata/users.json— local JSON “database” (auto-created)
High-level architecture:
Browser (index.html + src/app.js)
|
| fetch('/api/...')
v
Node/Express (server.js)
|
| reads/writes
v
data/users.json
- OTP is 4 digits, TTL ~5 minutes
- Server stores only a hash of the OTP in memory (
otpStore) - OTP entries are scoped by purpose (signup/reset/signin)
- Attempts are limited (
MAX_ATTEMPTS) - Send has cooldown + simple per-IP limits
- The server issues an HttpOnly cookie named
sid - Session data is stored in memory (
sessionsMap)
- RFC 6238 style (SHA1, 30s step, 6 digits)
- Server creates a Base32 secret and an
otpauth://URI - QR generation is supported (uses
qrcode)
- Generates 10 one-time codes
- Stores hashes only
- Using a backup code consumes it
GET /api/health
GET /api/username/suggest?name=Your%20Name
POST /api/signup/send-code— send signup OTPPOST /api/signup— create account (requires OTP)
POST /api/password/forgot/send-code— send reset OTPPOST /api/password/forgot/reset— verify OTP + set new password
POST /api/signin/startPOST /api/signin/send-codePOST /api/signin/complete
GET /api/me(requires auth)POST /api/signout
POST /api/account/update— update profile fieldsPOST /api/account/password— change password
POST /api/2fa/setPOST /api/2fa/method
POST /api/totp/beginPOST /api/totp/reset-beginGET /api/totp/qrPOST /api/totp/cancelPOST /api/totp/confirm
POST /api/backup-codes/enablePOST /api/backup-codes/disableGET /api/backup-codes/status
GET /api/users/by-unique/:unique
Legacy / generic OTP helpers (used for learning / reuse):
POST /api/send— send OTP for an arbitrarypurposePOST /api/verify— verify OTP for an arbitrarypurpose
Usually means:
- you opened
index.htmldirectly (don’t), or - you started a different server from another folder/port
Fix:
- Start the server from the correct project folder
- Open the exact URL printed by the server
If the port is busy, set a different one:
$env:PORT=3009; npm start- If you see
debug_code, delivery failed and the app switched to Instructor mode — this is OK for learning. - For real delivery on hosting, use Brevo HTTPS (
BREVO_API_KEY+ a verified sender). - For real delivery locally, use a Gmail App Password and restart after editing
.env.
- Ensure
qrcodeis installed (npm i qrcode), or - Use manual setup with the Base32 secret shown in the UI
This is still a learning project, but it includes a few “real world” touches:
- Disables
X-Powered-By - Adds basic security/correctness headers
- Avoids caching
/api/*responses (OTP endpoints areno-store) - Stores OTPs as hashes (not plaintext)
High-value checklist:
- Password hashing: replace SHA-256 with bcrypt / scrypt / Argon2
- Persistence: replace
data/users.jsonwith a real DB (Postgres/MySQL/MongoDB) - Sessions: store sessions in Redis/DB; use HTTPS; set cookie
Secure+SameSite - CSRF protection (because cookies are used)
- Logging + monitoring (and never leak secrets)
- Email enumeration: consider making forgot-password always return a generic response
- Protect TOTP secrets properly (encrypt-at-rest in a real system)
- Show Sign up with email OTP (OTP TTL + attempts)
- Show Unique UserName auto-suggestion (slugifying + uniqueness)
- Sign in and explain sessions (HttpOnly cookie
sid) - Enable Email 2FA → sign out → sign in again → complete OTP
- Enable TOTP and explain
otpauth://+ 30s window - Enable backup codes and explain “one-time” behavior
- Show Account settings update + password change
- Show Forgot password flow + forced sign-out of active sessions
- Add
GET /api/versionreturning app name + version. - Improve forgot-password to avoid email enumeration (always respond
ok: true). - Add
SameSite=Laxcookie attribute in the session cookie helper. - Replace password hashing with bcrypt.
- Add tests for OTP expiry + attempt limits + backup code consumption.










