A secure, production-oriented authentication system built with the MERN stack, designed to support both React web applications and React Native mobile applications from the same backend.
The system provides authentication, email verification, password recovery, password management, email changes, refresh-token sessions, and device/session management.
- User registration
- Email OTP verification
- Login with email and password
- JWT-based authentication
- Short-lived access tokens
- Long-lived refresh tokens
- Automatic access-token refresh
- Logout
- Protected API routes
- Support for multiple authenticated devices
- OTP-based email verification
- Secure OTP hashing using HMAC
- OTP expiration
- Maximum OTP attempt protection
- OTP resend cooldown
- Email verification during registration
- Current-email verification before changing email
- New-email verification before completing an email change
- Secure password hashing with bcrypt
- Change password
- Forgot password
- Password-reset OTP
- Password-reset JWT
- Password confirmation
- Prevents changing to the same password
- Invalidates all refresh sessions after password change
- Invalidates all refresh sessions after password reset
The email-change flow uses a multi-step verification process:
Current Email
β
Verify Current Email OTP
β
Enter New Email
β
Verify New Email OTP
β
Email Changed
The email-change process uses a short-lived email-change token between verification steps.
Authenticated refresh-token sessions are stored individually in MongoDB.
Each session can contain:
- User
- Device ID
- Device type
- Device name
- IP address
- User agent
- Creation timestamp
- Last-used timestamp
- Expiration timestamp
Users can:
- View active sessions/devices
- Revoke individual sessions
- Log out from specific devices
Supported device types:
web
android
ios
- HTTP-only refresh-token cookies for web
- Secure cookies in production
SameSite=Nonefor cross-origin production web authenticationSameSite=Laxduring local development- Refresh-token rotation
- Refresh-token hashing before database storage
- HMAC-hashed OTPs
- Timing-safe OTP comparison
- OTP expiration
- OTP attempt limits
- OTP resend cooldown
- JWT purpose validation
- Helmet security headers
- CORS protection
- Request body size limits
- Centralized error handling
- Centralized API responses
- MongoDB TTL indexes for expiring records
The backend is designed to work with both web and mobile clients.
ββββββββββββββββββββ
β React Web β
ββββββββββ¬ββββββββββ
β
β
ββββββββββΌββββββββββ
β β
β Express API β
β β
ββββββββββ¬ββββββββββ
β
βββββββββββββββββββΌββββββββββββββββββ
β β β
βββββββββΌββββββββ βββββββββΌβββββββ ββββββββββΌββββββ
β MongoDB β β Email β β JWT β
β β β Service β β Tokens β
βββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β
ββββββββββΌββββββββββ
β React Native β
β Mobile Client β
ββββββββββββββββββββ
The system uses two primary authentication tokens.
The access token is short-lived and is used to access protected API endpoints.
Client
β
β Authorization: Bearer <access-token>
βΌ
Express API
β
βββ verifyAccessToken
Access tokens are intentionally short-lived to reduce the impact of token theft.
Refresh tokens are long-lived and are used to obtain new access tokens.
For the web application:
Browser
β
β HTTP-only cookie
βΌ
Express API
For React Native:
React Native
β
β Secure device storage
βΌ
Refresh Token
Refresh tokens are rotated and their hashes are stored in MongoDB.
The raw refresh token is never stored in the database.
The React frontend uses Axios interceptors.
When an authenticated request receives a 401 response:
API Request
β
βΌ
401
β
βΌ
Refresh Access Token
β
βββ Success
β β
β Retry request
β
βββ Failure
β
Logout
Concurrent requests are queued while a refresh operation is already running, preventing multiple simultaneous refresh requests.
Special-purpose authorization tokens such as registration, password-reset, and email-change tokens are preserved instead of being overwritten by the normal access-token interceptor.
OTP codes are not stored as plain text.
The system generates a six-digit OTP:
123456
The OTP is hashed using HMAC:
OTP
β
βΌ
HMAC-SHA256
β
βΌ
Stored hash
During verification, the submitted OTP is hashed again and compared using a timing-safe comparison.
OTP protection includes:
- 6-digit format validation
- Expiration
- Maximum failed attempts
- Resend cooldown
- Previous OTP cleanup
OTP types include:
EMAIL_VERIFICATION
PASSWORD_RESET
PASSWORD_CHANGE
EMAIL_CHANGE_OLD
EMAIL_CHANGE_NEW
A simplified structure of the project:
project/
β
βββ backend/
β βββ src/
β β βββ config/
β β β βββ env.js
β β β βββ email.js
β β β
β β βββ controllers/
β β β βββ auth/
β β β
β β βββ middleware/
β β β
β β βββ models/
β β β βββ user.model.js
β β β βββ otp.model.js
β β β βββ refreshToken.model.js
β β β
β β βββ routes/
β β β
β β βββ services/
β β β βββ email.service.js
β β β
β β βββ utils/
β β β βββ ApiError.js
β β β βββ ApiResponse.js
β β β βββ asyncHandler.js
β β β βββ token.utils.js
β β β
β β βββ app.js
β β
β βββ package.json
β
βββ web/
β βββ src/
β β βββ components/
β β β βββ change-email/
β β β
β β βββ context/
β β β
β β βββ pages/
β β β βββ LoginPage.jsx
β β β βββ ForgotPasswordPage.jsx
β β β βββ DashboardPage.jsx
β β β βββ Profile.jsx
β β β βββ ChangePasswordPage.jsx
β β β βββ ChangeEmailPage.jsx
β β β βββ DevicesPage.jsx
β β β
β β βββ services/
β β βββ api.js
β β βββ auth.service.js
β β
β βββ package.json
β
βββ README.md
The user model contains:
name
email
phone
dateOfBirth
password
isEmailVerified
createdAt
updatedAt
Passwords are automatically hashed before being saved.
Stores temporary OTP information:
email
type
codeHash
expiresAt
attempts
verifiedAt
createdAt
updatedAt
A MongoDB TTL index automatically removes expired OTP documents.
Stores authenticated sessions:
user
tokenHash
deviceId
deviceType
deviceName
ip
userAgent
expiresAt
lastUsedAt
createdAt
updatedAt
Refresh-token sessions also use a TTL index.
A unique combination of:
user + deviceId
prevents duplicate sessions for the same device.
Base URL:
/api
POST /auth/register/send-otp
POST /auth/register/verify-otp
POST /auth/register/completePOST /auth/login
POST /auth/refresh-token
POST /auth/logoutPOST /auth/forgot-password/send-otp
POST /auth/forgot-password/verify-otp
POST /auth/forgot-password/reset-password
POST /auth/change-passwordPOST /auth/change-email/send-old-otp
POST /auth/change-email/verify-old-otp
POST /auth/change-email/send-new-otp
POST /auth/change-email/verify-new-otpGET /users/devices
DELETE /users/devices/:sessionIdCreate a .env file in the backend.
Example:
MONGODB_URI=
OTP_SECRET=
CLIENT_URL=http://localhost:5173
PORT=8000
REGISTRATION_TOKEN_SECRET=
REGISTRATION_TOKEN_EXPIRATION=
ACCESS_TOKEN_SECRET=
ACCESS_TOKEN_EXPIRATION=
REFRESH_TOKEN_SECRET=
REFRESH_TOKEN_EXPIRATION=
PASSWORD_RESET_TOKEN_SECRET=
PASSWORD_RESET_TOKEN_EXPIRATION=
EMAIL_CHANGE_TOKEN_SECRET=
EMAIL_CHANGE_TOKEN_EXPIRATION=
EMAIL_HOST=
EMAIL_PORT=
EMAIL_USER=
EMAIL_PASS=
EMAIL_FROM=
NODE_ENV=developmentNever commit .env to Git.
Add it to .gitignore:
.env
.env.*Use strong, randomly generated secrets for all JWT and OTP secrets.
git clone https://github.com/jdcodebase/authentication-system
cd authentication-systemcd backend
npm installcd ../web
npm installCreate:
backend/.env
and add the required configuration.
cd backend
npm run devThe backend runs on:
http://localhost:8000
cd web
npm run devThe frontend will normally run on:
http://localhost:5173
The project currently uses Nodemailer with SMTP for local email delivery.
Typical local configuration:
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USER=your-email
EMAIL_PASS=your-app-password
EMAIL_FROM=your-emailFor Gmail SMTP, use an App Password rather than your normal Gmail password.
Email functionality can be tested locally without requiring a separate transactional email provider.
This project follows several important authentication security practices.
Passwords are:
- Never returned in normal user queries
- Hashed before database storage
- Compared using bcrypt
Refresh tokens are:
- Rotated
- Hashed before database storage
- Associated with a specific device/session
- Expirable
- Revocable
Production web refresh tokens use:
HttpOnly
Secure
SameSite=None
This prevents JavaScript from directly accessing the refresh token.
OTP codes are:
- Short-lived
- Hashed
- Attempt-limited
- Rate-limited
- Deleted after successful verification
Changing or resetting a password invalidates all existing refresh sessions.
This helps prevent previously issued sessions from remaining active after a password compromise.
The backend is designed to support React Native without requiring a separate authentication API.
The main difference is refresh-token storage.
Refresh Token
β
HTTP-only Cookie
Refresh Token
β
Secure Device Storage
The same authentication backend can therefore serve:
React Web
+
React Native
β
Same Express Authentication API
- Node.js
- Express 5
- MongoDB
- Mongoose 9
- JSON Web Tokens
- bcrypt
- Nodemailer
- Helmet
- CORS
- Cookie Parser
- React
- React Router
- Axios
- Tailwind CSS
- React Hot Toast
- React Icons
- React Native
This project is currently intended for personal/educational development.
Add your preferred license here before public distribution.