Multi‐Factor Authentication (MFA) - sonuprajapati15/Authentication-Authorization GitHub Wiki
Multi-Factor Authentication (MFA) enhances security by requiring users to verify their identity using two or more authentication factors before accessing a system.
🔹 Common MFA factors:
- Something You Know – Password, PIN, security questions.
- Something You Have – OTP (One-Time Password via SMS, Email, Authenticator App).
- Something You Are – Biometrics (Fingerprint, Face Recognition).
🔹 Example: Logging into a banking app with a password and then confirming the login with a one-time code sent to your phone.
✅ Users can enable or disable MFA.
✅ Supports multiple authentication factors (TOTP, SMS, Email, Push Notification, Biometrics).
✅ After entering a password, the user must complete a second verification step.
✅ Users can reset MFA settings if they lose access to their second factor.
✅ Remember device option for a seamless login experience.
⚡ Security – Protect against phishing, replay attacks, and brute-force attacks.
⚡ Scalability – Efficiently handle large numbers of MFA requests.
⚡ Performance – Generate and validate OTPs quickly without delay.
⚡ Availability – Ensure fallback methods (backup codes) in case primary MFA is unavailable.
⚡ Compliance – Must follow GDPR, PCI-DSS, HIPAA for sensitive authentication data.
- User (Client) – Initiates login and completes MFA verification.
- Authentication Server – Handles login, generates and verifies MFA codes.
- MFA Provider – Sends OTPs via SMS, Email, or Authenticator App (e.g., Google Authenticator).
- MFA Storage – Stores user MFA settings (enabled methods, backup codes).
- User logs in with username & password → If correct, proceed to MFA.
- MFA challenge → The system sends an OTP via SMS, Email, or an Authenticator App.
- User enters OTP → The authentication server verifies it.
- Success → User gains access, and a session is created.
- Failure → User is denied access and may retry or use a backup method.
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(255) UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
phone VARCHAR(20),
mfa_enabled BOOLEAN DEFAULT FALSE,
mfa_method VARCHAR(20) CHECK (mfa_method IN ('totp', 'sms', 'email'))
);
CREATE TABLE mfa_codes (
id SERIAL PRIMARY KEY,
user_id INT REFERENCES users(id) ON DELETE CASCADE,
otp_code VARCHAR(10) NOT NULL,
expires_at TIMESTAMP NOT NULL,
used BOOLEAN DEFAULT FALSE
);
Method | Endpoint | Description |
---|---|---|
POST | /login | Authenticate user with username/password |
POST | /mfa/verify | Verify OTP from SMS, Email, or Authenticator App |
POST | /mfa/setup | Enable/disable MFA for a user |
GET | /mfa/backup | Retrieve backup codes for MFA recovery |
POST | /logout | Terminate session |
Here is the MFA Authentication Flowchart:
View or edit this diagram in Whimsical.
npm install express bcrypt jsonwebtoken speakeasy twilio dotenv
require("dotenv").config();
const express = require("express");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const speakeasy = require("speakeasy");
const twilio = require("twilio");
const app = express();
app.use(express.json());
// Mock user database
const users = [
{ id: 1, username: "user1", passwordHash: bcrypt.hashSync("password", 10), mfaSecret: null, phone: "+1234567890", mfaEnabled: false }
];
// Twilio Client
const twilioClient = new twilio(process.env.TWILIO_SID, process.env.TWILIO_AUTH_TOKEN);
// Login Route
app.post("/login", async (req, res) => {
const { username, password } = req.body;
const user = users.find((u) => u.username === username);
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
return res.status(401).json({ message: "Invalid credentials" });
}
if (user.mfaEnabled) {
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { expiresIn: "5m" });
return res.json({ mfaRequired: true, token });
}
res.json({ message: "Login successful", token: jwt.sign({ userId: user.id }, process.env.JWT_SECRET) });
});
// MFA Verification Route
app.post("/mfa/verify", (req, res) => {
const { token, otp } = req.body;
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = users.find((u) => u.id === decoded.userId);
if (!speakeasy.totp.verify({ secret: user.mfaSecret, encoding: "base32", token: otp })) {
return res.status(400).json({ message: "Invalid OTP" });
}
res.json({ message: "MFA Successful", token: jwt.sign({ userId: user.id }, process.env.JWT_SECRET) });
});
app.listen(3000, () => console.log("MFA Server running on port 3000"));
✅ TOTP (Time-based One-Time Passwords) – Google Authenticator, Authy.
✅ SMS/Email OTP – Sends a one-time code to registered phone/email.
✅ Push Notifications – Approve login from a trusted device.
✅ Biometric Authentication – Face ID, fingerprint scan (for mobile apps).
Factor | Trade-off |
---|---|
Security | TOTP is more secure than SMS, as SMS is vulnerable to SIM-swapping. |
User Experience | Too many MFA prompts can frustrate users; "Remember this device" helps. |
Performance | OTPs should expire quickly and be rate-limited to prevent brute-force attacks. |
Scalability | MFA providers (Twilio, Google Authenticator) must handle high request volumes. |
Now, let's enhance our MFA system by adding the following authentication methods:
✅ Push Notifications (Firebase Cloud Messaging - FCM) – Users receive an approval request on their phone.
✅ Backup Codes – Pre-generated one-time use codes for emergency access.
✅ Biometric Authentication – Face ID or Fingerprint for mobile apps.
- Push Notification Service (FCM/APNs) – Sends login approval requests to mobile devices.
- Backup Codes Database – Stores one-time codes for emergency login.
- Biometric Authentication – Integrates Face ID or Fingerprint for mobile users.
- User logs in – Password verification is successful.
-
MFA Challenge – User selects one of the MFA methods:
- OTP (SMS/Email) – Enter the code sent via SMS or email.
- Authenticator App (TOTP) – Enter the time-based OTP.
- Push Notification (Firebase/APNs) – Approve login request from a mobile app.
- Biometric Authentication – Authenticate using Face ID/Fingerprint (if enabled).
- Backup Code – Use a previously generated one-time backup code.
- Authentication Server Validates MFA Response.
- Successful Authentication → User gains access.
CREATE TABLE mfa_methods (
id SERIAL PRIMARY KEY,
user_id INT REFERENCES users(id) ON DELETE CASCADE,
method VARCHAR(20) CHECK (method IN ('totp', 'sms', 'email', 'push', 'biometric', 'backup_code')),
secret TEXT,
enabled BOOLEAN DEFAULT TRUE
);
CREATE TABLE backup_codes (
id SERIAL PRIMARY KEY,
user_id INT REFERENCES users(id) ON DELETE CASCADE,
code VARCHAR(10) NOT NULL,
used BOOLEAN DEFAULT FALSE
);
Method | Endpoint | Description |
---|---|---|
POST | /mfa/verify | Verify OTP, push notification, or biometric auth |
GET | /mfa/push/send | Send push notification request |
POST | /mfa/push/approve | Approve or reject push notification request |
GET | /mfa/backup | Retrieve backup codes |
POST | /mfa/backup/use | Use a backup code for login |
Here is the Enhanced MFA Authentication Flowchart:
View or edit this diagram in Whimsical.
npm install express bcrypt jsonwebtoken speakeasy twilio firebase-admin dotenv
require("dotenv").config();
const express = require("express");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const speakeasy = require("speakeasy");
const twilio = require("twilio");
const admin = require("firebase-admin");
const app = express();
app.use(express.json());
// Firebase Push Notification Setup
admin.initializeApp({
credential: admin.credential.cert(require("./firebase-key.json"))
});
// Mock user database
const users = [
{ id: 1, username: "user1", passwordHash: bcrypt.hashSync("password", 10), mfaSecret: speakeasy.generateSecret().base32, phone: "+1234567890", mfaEnabled: true, pushToken: "user_fcm_token" }
];
// Twilio Client
const twilioClient = new twilio(process.env.TWILIO_SID, process.env.TWILIO_AUTH_TOKEN);
// Login Route
app.post("/login", async (req, res) => {
const { username, password } = req.body;
const user = users.find((u) => u.username === username);
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
return res.status(401).json({ message: "Invalid credentials" });
}
if (user.mfaEnabled) {
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { expiresIn: "5m" });
return res.json({ mfaRequired: true, token });
}
res.json({ message: "Login successful", token: jwt.sign({ userId: user.id }, process.env.JWT_SECRET) });
});
// Push Notification MFA
app.get("/mfa/push/send", (req, res) => {
const { userId } = jwt.verify(req.headers.authorization, process.env.JWT_SECRET);
const user = users.find((u) => u.id === userId);
admin.messaging().send({
token: user.pushToken,
notification: { title: "Login Request", body: "Approve login attempt?" }
});
res.json({ message: "Push notification sent" });
});
// MFA Verification
app.post("/mfa/verify", (req, res) => {
const { token, otp } = req.body;
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = users.find((u) => u.id === decoded.userId);
if (!speakeasy.totp.verify({ secret: user.mfaSecret, encoding: "base32", token: otp })) {
return res.status(400).json({ message: "Invalid OTP" });
}
res.json({ message: "MFA Successful", token: jwt.sign({ userId: user.id }, process.env.JWT_SECRET) });
});
app.listen(3000, () => console.log("MFA Server running on port 3000"));
MFA Method | Pros | Cons |
---|---|---|
TOTP (Authenticator App) | Secure, no internet required | User needs an app like Google Authenticator |
SMS/Email OTP | Easy to use | Susceptible to SIM swapping & phishing |
Push Notifications | Seamless UX, secure | Requires internet & mobile app |
Biometric Authentication | Fast & secure | Device-dependent |
Backup Codes | Emergency access | Must be stored securely by the user |
Now, our MFA system supports SMS, TOTP, Push Notifications, Backup Codes, and Biometrics! This makes it more secure and user-friendly.
learn WebAuthn (FIDO2) for passwordless authentication 🚀