امروزه داشتن یک رمز عبور قوی بهتنهایی همیشه برای محافظت از حسابهای کاربری کافی نیست.
اگر رمز عبور یک کاربر به هر دلیلی افشا شود، مهاجم میتواند با داشتن همان رمز وارد حساب شود. یکی از روشهای مهم برای کاهش این ریسک، استفاده از Two-Factor Authentication یا 2FA است.
در احراز هویت دو مرحلهای، کاربر برای ورود به حساب خود علاوه بر رمز عبور، باید یک عامل دوم را نیز ارائه کند.
برای مثال:
Email + Password
+
OTP Code
در این مقاله با مفهوم 2FA، تفاوت آن با MFA، انواع روشهای پیادهسازی، OTP، Session، Rate Limiting و یک نمونه عملی با PHP و PDO آشنا میشویم.
# 2FA چیست؟
2FA مخفف:
Two-Factor Authentication
به معنی احراز هویت دو عاملی است.
در این روش برای احراز هویت کاربر از دو عامل متفاوت استفاده میشود.
مثلاً:
عامل اول:
Password
عامل دوم:
OTP
بنابراین اگر مهاجم رمز عبور را داشته باشد، هنوز برای ورود کامل به حساب به عامل دوم نیاز دارد.
# چرا Password بهتنهایی کافی نیست؟
فرض کنید کاربر:
Email:
user@example.com
Password:
************
دارد.
اگر Password در اثر یکی از موارد زیر افشا شود:
- فیشینگ
- بدافزار
- نشت اطلاعات
- استفاده از Password تکراری
- Credential Stuffing
- Password Spraying
مهاجم میتواند تلاش کند وارد حساب شود.
اما اگر 2FA فعال باشد:
Password
↓
درست است
↓
OTP
↓
درست است
↓
Login
در این حالت داشتن Password بهتنهایی برای تکمیل Login کافی نیست.
# تفاوت 2FA و MFA
این دو اصطلاح شبیه هستند اما دقیقاً یکی نیستند.
2FA
یعنی دقیقاً دو عامل احراز هویت داشته باشیم:
Factor 1
+
Factor 2
MFA
مخفف:
Multi-Factor Authentication
است و میتواند دو یا چند عامل داشته باشد.
بنابراین:
2FA ⊂ MFA
به بیان ساده، 2FA یکی از حالتهای MFA است.
# سه عامل اصلی احراز هویت
عوامل احراز هویت معمولاً در چند دسته قرار میگیرند.
1. چیزی که کاربر میداند
مثل:
Password
PIN
Security Answer
2. چیزی که کاربر در اختیار دارد
مثل:
Mobile Phone
Authenticator App
Hardware Security Key
3. چیزی که کاربر هست
مثل:
Fingerprint
Face Recognition
برای 2FA باید از دو عامل متفاوت استفاده شود.
# آیا Password + Password دو عاملی است؟
خیر.
برای مثال:
Password
+
PIN
هر دو چیزی هستند که کاربر میداند.
بنابراین از نظر مفهوم عامل متفاوتی محسوب نمیشوند.
اما:
Password
+
Authenticator App
دو عامل متفاوت هستند.
# OTP چیست؟
OTP مخفف:
One-Time Password
است.
یعنی رمز یا کدی که برای مدت محدود و معمولاً برای یک بار استفاده معتبر است.
مثلاً:
583214
کاربر این کد را در مرحله دوم Login وارد میکند.
# یک OTP ساده چگونه کار میکند؟
فرآیند:
User
↓
Email + Password
↓
Server
↓
Password صحیح است
↓
Generate OTP
↓
Send OTP
↓
User enters OTP
↓
Verify OTP
↓
Login
# آیا OTP شش رقمی کافی است؟
یک OTP شش رقمی:
000000
تا
999999
در مجموع یک میلیون حالت دارد.
اما امنیت آن فقط به تعداد حالتها بستگی ندارد.
موارد مهم دیگری نیز وجود دارند:
- Expiration
- Rate Limiting
- محدودیت تعداد تلاش
- یکبارمصرف بودن
- تولید تصادفی امن
- عدم ثبت OTP در Log
اگر این کنترلها وجود نداشته باشند، OTP شش رقمی میتواند در برابر Brute Force آسیبپذیر شود.
# تولید OTP با PHP
برای تولید کد عددی میتوانیم از منبع تصادفی امن استفاده کنیم.
مثلاً:
$otp = (string) random_int(100000, 999999);
تابع random_int() برای تولید اعداد تصادفی مناسب کاربردهای امنیتی طراحی شده است.
از روشهایی مانند:
rand()
برای OTP امنیتی استفاده نکنید.
# ذخیره OTP در دیتابیس
میتوانیم جدول زیر را داشته باشیم:
CREATE TABLE login_otp_codes (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
otp_hash CHAR(64) NOT NULL,
expires_at DATETIME NOT NULL,
attempts INT NOT NULL DEFAULT 0,
used_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX (user_id),
INDEX (otp_hash)
);
ستونهای مهم:
| ستون | کاربرد |
|---|---|
user_id | کاربر |
otp_hash | Hash کد |
expires_at | تاریخ انقضا |
attempts | تعداد تلاش |
used_at | زمان مصرف |
created_at | زمان ایجاد |
# آیا OTP خام را در دیتابیس ذخیره کنیم؟
بهتر است OTP خام را ذخیره نکنیم.
به جای:
583214
میتوانیم Hash آن را ذخیره کنیم:
$otpHash = hash(
'sha256',
$otp
);
و فقط $otpHash را در دیتابیس قرار دهیم.
این طراحی باعث میشود OTP خام مستقیماً در دیتابیس ذخیره نشود.
# Hash کردن OTP
مثلاً:
$otp = (string) random_int(
100000,
999999
);
$otpHash = hash(
'sha256',
$otp
);
سپس:
$stmt = $pdo->prepare(
'INSERT INTO login_otp_codes
(user_id, otp_hash, expires_at)
VALUES
(:user_id, :otp_hash, :expires_at)'
);
$stmt->execute([
'user_id' => $userId,
'otp_hash' => $otpHash,
'expires_at' => $expiresAt
]);
# زمان انقضای OTP
OTP نباید برای مدت طولانی معتبر باشد.
مثلاً میتوانیم آن را برای 5 دقیقه معتبر کنیم:
$expiresAt = date(
'Y-m-d H:i:s',
time() + 300
);
که:
300 ثانیه = 5 دقیقه
است.
در بعضی سیستمها ممکن است زمان کوتاهتری انتخاب شود.
# فرآیند کامل Login با 2FA
حالا فرآیند Login به این شکل میشود:
Login
↓
Email + Password
↓
بررسی Password
↓
2FA فعال است؟
/ \
خیر بله
↓ ↓
Login Generate OTP
↓
Send OTP
↓
OTP Verification
↓
Login
# چرا نباید قبل از OTP Session کامل بسازیم؟
فرض کنید کاربر Password صحیح دارد اما هنوز OTP را وارد نکرده است.
اگر همینجا:
$_SESSION['user_id'] = $userId;
قرار دهیم، ممکن است بخشهای محافظتشده برنامه او را Login شده در نظر بگیرند.
بهتر است وضعیت Login موقت باشد.
مثلاً:
$_SESSION['2fa_user_id'] = $userId;
$_SESSION['2fa_pending'] = true;
تا زمانی که OTP تأیید نشده است.
# Session موقت 2FA
بعد از درست بودن Password:
session_regenerate_id(true);
$_SESSION['2fa_user_id'] = $user['id'];
$_SESSION['2fa_pending'] = true;
کاربر هنوز Login کامل نشده است.
پس:
2FA Pending
است.
# بعد از تأیید OTP
وقتی OTP درست بود:
unset($_SESSION['2fa_user_id']);
unset($_SESSION['2fa_pending']);
$_SESSION['user_id'] = $user['id'];
session_regenerate_id(true);
حالا Login کامل شده است.
# چرا session_regenerate_id() مهم است؟
بعد از تغییر سطح احراز هویت بهتر است Session ID تغییر کند.
مثلاً:
session_regenerate_id(true);
این کار در برابر برخی سناریوهای Session Fixation اهمیت دارد.
جزئیات Session Hijacking و Session Fixation را میتوانید در مقاله مربوط به همین موضوع بررسی کنید.
# ساخت Login اولیه
برای مثال:
$email = trim($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
سپس User را پیدا میکنیم:
$stmt = $pdo->prepare(
'SELECT id, email, password, two_factor_enabled
FROM users
WHERE email = :email
LIMIT 1'
);
$stmt->execute([
'email' => $email
]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
# بررسی Password
if (
!$user ||
!password_verify(
$password,
$user['password']
)
) {
exit('اطلاعات ورود صحیح نیست.');
}
بهتر است پیام خطا برای Email و Password نادرست یکسان باشد.
# اگر 2FA فعال نباشد
if (!$user['two_factor_enabled']) {
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
// Login کامل
}
# اگر 2FA فعال باشد
if ($user['two_factor_enabled']) {
$otp = (string) random_int(
100000,
999999
);
$otpHash = hash(
'sha256',
$otp
);
$expiresAt = date(
'Y-m-d H:i:s',
time() + 300
);
// ذخیره OTP
$_SESSION['2fa_user_id'] = $user['id'];
$_SESSION['2fa_pending'] = true;
// ارسال OTP
header(
'Location: /verify-2fa'
);
exit;
}
# ارسال OTP به کاربر
روش ارسال OTP بستگی به سیستم دارد.
میتوان از مواردی مانند:
Email
SMS
Authenticator App
استفاده کرد.
اما این روشها از نظر امنیت یکسان نیستند.
برای مثال، در طراحیهای حساس معمولاً باید به جای اینکه صرفاً از SMS استفاده کنیم، گزینههای مقاومتر مانند TOTP یا Security Key را نیز در نظر بگیریم.
# ساخت فرم OTP
صفحه دوم Login میتواند چنین فرمی داشته باشد:
<form method="POST" action="/verify-2fa">
<label for="otp">
کد تأیید
</label>
<input
type="text"
id="otp"
name="otp"
inputmode="numeric"
autocomplete="one-time-code"
maxlength="6"
required
>
<button type="submit">
تأیید
</button>
</form>
# دریافت OTP
$otp = trim(
$_POST['otp'] ?? ''
);
سپس میتوانیم بررسی کنیم:
if (
!preg_match(
'/^\d{6}$/',
$otp
)
) {
exit('کد نامعتبر است.');
}
# Hash کردن OTP واردشده
$otpHash = hash(
'sha256',
$otp
);
حالا آن را با مقدار ذخیرهشده مقایسه میکنیم.
# پیدا کردن OTP
$stmt = $pdo->prepare(
'SELECT
id,
user_id,
otp_hash,
expires_at,
attempts,
used_at
FROM login_otp_codes
WHERE user_id = :user_id
AND used_at IS NULL
ORDER BY id DESC
LIMIT 1'
);
$stmt->execute([
'user_id' => $_SESSION['2fa_user_id']
]);
$otpRecord = $stmt->fetch(
PDO::FETCH_ASSOC
);
# بررسی وجود OTP
if (!$otpRecord) {
exit('کد تأیید معتبر نیست.');
}
# بررسی تاریخ انقضا
if (
strtotime($otpRecord['expires_at']) < time()
) {
exit('کد تأیید منقضی شده است.');
}
# محدود کردن تعداد تلاش
یکی از مهمترین قسمتهای 2FA همین بخش است.
فرض کنید مهاجم OTP شش رقمی را نمیداند.
بدون Rate Limit:
000000
000001
000002
...
999999
در بدترین حالت میتواند تمام ترکیبها را امتحان کند.
بنابراین باید تعداد تلاش محدود شود.
مثلاً:
if ($otpRecord['attempts'] >= 5) {
exit(
'تعداد تلاشهای مجاز به پایان رسیده است.'
);
}
عدد دقیق باید متناسب با سیستم انتخاب شود.
# افزایش تعداد تلاش
قبل از بررسی یا بعد از هر تلاش ناموفق میتوان تعداد تلاش را افزایش داد.
مثلاً:
$stmt = $pdo->prepare(
'UPDATE login_otp_codes
SET attempts = attempts + 1
WHERE id = :id'
);
$stmt->execute([
'id' => $otpRecord['id']
]);
# مقایسه OTP
if (!hash_equals(
$otpRecord['otp_hash'],
$otpHash
)) {
exit('کد تأیید صحیح نیست.');
}
اگر کد صحیح باشد، وارد مرحله Login کامل میشویم.
# مصرف OTP
بعد از موفقیت:
$stmt = $pdo->prepare(
'UPDATE login_otp_codes
SET used_at = NOW()
WHERE id = :id
AND used_at IS NULL'
);
$stmt->execute([
'id' => $otpRecord['id']
]);
حالا OTP دیگر قابل استفاده نیست.
# تکمیل Login
session_regenerate_id(true);
unset($_SESSION['2fa_user_id']);
unset($_SESSION['2fa_pending']);
$_SESSION['user_id'] =
$otpRecord['user_id'];
از اینجا به بعد کاربر Login شده است.
# چرا OTP باید یکبارمصرف باشد؟
فرض کنید کاربر امروز OTP زیر را دریافت کند:
583214
اگر این OTP برای همیشه معتبر باشد، شخصی که آن را در اختیار دارد میتواند دوباره از آن استفاده کند.
طراحی مناسب:
OTP Generated
↓
OTP Used
↓
used_at = NOW()
↓
Invalid
# ارسال مجدد OTP
کاربر ممکن است OTP را دریافت نکند.
بنابراین گزینه:
ارسال مجدد کد
لازم است.
اما این Endpoint باید Rate Limited باشد.
مثلاً:
ارسال OTP
↓
60 ثانیه انتظار
↓
Resend
و در کنار آن محدودیت کلی نیز داشته باشیم.
# چرا Resend خطرناک است؟
بدون Rate Limiting:
Resend
Resend
Resend
Resend
...
میتواند باعث:
- Spam
- مصرف منابع
- هزینه SMS
- هزینه Email
- سوءاستفاده از سرویس
شود.
# OTP در Email یا SMS؟
دو روش رایج:
Password
+
Email OTP
و:
Password
+
SMS OTP
وجود دارند.
هر کدام مزایا و محدودیتهای خود را دارند.
# آیا SMS بهترین روش 2FA است؟
SMS میتواند کاربردی باشد، اما نباید آن را معادل همه روشهای 2FA در نظر گرفت.
مواردی مانند:
- SIM Swap
- سرقت شماره
- حملات مخابراتی
- وابستگی به اپراتور
میتوانند ریسکهایی ایجاد کنند.
بنابراین برای سیستمهای حساس بهتر است روشهای قویتر نیز در نظر گرفته شوند.
# TOTP چیست؟
یکی از روشهای رایجتر برای 2FA، TOTP است.
TOTP مخفف:
Time-Based One-Time Password
است.
در این روش یک Secret مشترک بین سرور و Authenticator App وجود دارد و کد بر اساس زمان تولید میشود.
برای مثال کاربر میتواند از یک برنامه Authenticator استفاده کند.
فرآیند:
Server Secret
+
Current Time
↓
TOTP Code
و Authenticator App نیز با همان Secret و زمان مشابه کد را تولید میکند.
# تفاوت OTP تصادفی و TOTP
| ویژگی | OTP تصادفی | TOTP |
|---|---|---|
| تولید کد | سرور | سرور + Authenticator |
| ارسال لازم | معمولاً بله | خیر |
| Email/SMS | قابل استفاده | معمولاً لازم نیست |
| Secret مشترک | لزوماً ندارد | دارد |
| وابستگی به سرویس ارسال | دارد | ندارد |
| استفاده برای 2FA | بله | بله |
# TOTP از نظر معماری چگونه کار میکند؟
هنگام فعالسازی 2FA:
User
↓
Enable 2FA
↓
Generate Secret
↓
Display QR Code
↓
Authenticator App
↓
Scan QR
↓
Generate Code
↓
Verify Code
↓
2FA Activated
در Login:
Password
↓
Correct
↓
Ask TOTP
↓
Authenticator Code
↓
Verify
↓
Login
این روش نیاز به ارسال SMS یا Email برای هر Login ندارد.
# Secret مربوط به TOTP را چگونه نگه داریم؟
Secret مربوط به TOTP حساس است.
اگر مهاجم Secret را به دست آورد، میتواند کدهای TOTP را تولید کند.
بنابراین باید:
- در Log ثبت نشود.
- در محیط امن نگهداری شود.
- دسترسی دیتابیس محدود باشد.
- در صورت امکان Encryption at Rest در نظر گرفته شود.
- هنگام نمایش اولیه با دقت مدیریت شود.
# آیا TOTP را با rand() بسازیم؟
خود Secret باید با یک منبع تصادفی امن تولید شود.
نباید از:
rand()
برای Secret امنیتی استفاده کنیم.
برای چنین کاربردهایی میتوان از منابع تصادفی امن PHP استفاده کرد.
# Backup Codes چیست؟
اگر کاربر گوشی خود را گم کند یا به Authenticator دسترسی نداشته باشد، ممکن است کاملاً از حساب خارج شود.
برای همین بسیاری از سیستمها Recovery Codes یا Backup Codes دارند.
مثلاً:
A8K4-9F2L
P7Q1-X3ZM
...
کاربر آنها را در زمان فعالسازی 2FA دریافت میکند.
هر Backup Code نیز باید:
- یکبارمصرف باشد.
- به شکل امن ذخیره شود.
- قابل حدس نباشد.
- بعد از استفاده باطل شود.
# چرا Backup Code مهم است؟
بدون Recovery Mechanism:
2FA فعال
↓
Phone lost
↓
Authenticator unavailable
↓
Account locked
با Backup Code:
2FA فعال
↓
Phone lost
↓
Backup Code
↓
Recovery
البته فرآیند بازیابی حساب باید خودش با دقت امنیتی طراحی شود.
# آیا Backup Code را خام ذخیره کنیم؟
بهتر است مانند Password یا Token حساس، اطلاعات قابل استفاده مستقیم را در دیتابیس ذخیره نکنیم.
میتوان:
Backup Code
↓
Hash
↓
Database
داشت.
# 2FA و Session
بعد از اینکه مرحله دوم تأیید شد، Session باید با دقت مدیریت شود.
برای مثال:
session_regenerate_id(true);
و سپس:
$_SESSION['user_id'] = $userId;
قرار میگیرد.
همچنین Session Cookie باید تنظیمات مناسبی مانند:
Secure
HttpOnly
SameSite
داشته باشد.
# 2FA و Session Hijacking
اگر Session بعد از Login دزدیده شود، مهاجم ممکن است بدون وارد کردن دوباره Password و OTP از Session استفاده کند.
بنابراین 2FA جایگزین امنیت Session نیست.
همچنان باید:
- HTTPS
- Secure Cookie
- HttpOnly
- SameSite
- Session Regeneration
- Timeout
- Logout مناسب
را رعایت کنیم.
# 2FA و CSRF
فرمهای مربوط به:
Enable 2FA
Disable 2FA
Verify 2FA
Regenerate Backup Codes
باید با توجه به معماری برنامه در برابر CSRF محافظت شوند.
بهخصوص فعال یا غیرفعال کردن 2FA یک عملیات حساس است.
# غیرفعال کردن 2FA
کاربر نباید صرفاً با باز کردن یک URL بتواند 2FA را خاموش کند.
مثلاً این طراحی نامناسب است:
/disable-2fa
بهتر است کاربر برای غیرفعال کردن 2FA دوباره احراز هویت شود.
مثلاً:
Password
+
Current OTP
یا یک مکانیزم احراز هویت مجدد مناسب.
# Re-authentication چیست؟
برای عملیات حساس میتوان از کاربر خواست دوباره هویت خود را تأیید کند.
مثلاً:
Change Email
Disable 2FA
Change Password
Delete Account
میتوانند نیازمند Re-authentication باشند.
# Rate Limiting در 2FA
Rate Limiting باید در چند بخش وجود داشته باشد:
Login
Email + Password attempts
OTP Verification
OTP attempts
Resend
OTP resend
Recovery
Backup code attempts
هرکدام ممکن است سیاست متفاوتی داشته باشند.
# ثبت رویدادهای امنیتی
برای سیستمهای مهم بهتر است رویدادهایی مانند موارد زیر ثبت شوند:
2FA enabled
2FA disabled
OTP requested
OTP failed
OTP verified
Backup code used
Password reset
اما مراقب باشید اطلاعات حساس را Log نکنید.
برای مثال:
OTP = 583214
نباید در Log ذخیره شود.
# جلوگیری از Brute Force
فرض کنیم مهاجم OTP را نمیداند.
اگر محدودیتی نداشته باشیم:
000000
000001
000002
...
ممکن است تلاشهای زیادی انجام شود.
بنابراین باید ترکیبی از این کنترلها را داشته باشیم:
Attempt Limit
+
Rate Limit
+
Expiration
+
Single-use
# آیا بعد از چند تلاش حساب را قفل کنیم؟
میتوان از Account Lockout استفاده کرد، اما باید با دقت طراحی شود.
اگر بهسادگی بگوییم:
5 OTP اشتباه
↓
Account Locked
مهاجم میتواند عمداً حساب قربانی را قفل کند.
این مشکل را Denial of Service through Account Lockout میتوان در نظر گرفت.
بنابراین به جای قفل دائمی، سیاستهایی مانند:
- افزایش تأخیر
- محدودیت موقت
- Rate Limit
- نیاز به Recovery
میتوانند بررسی شوند.
# نمونه Login کامل با 2FA
یک نمونه ساده:
<?php
session_start();
$email = trim($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
$stmt = $pdo->prepare(
'SELECT
id,
email,
password,
two_factor_enabled
FROM users
WHERE email = :email
LIMIT 1'
);
$stmt->execute([
'email' => $email
]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if (
!$user ||
!password_verify(
$password,
$user['password']
)
) {
exit('اطلاعات ورود صحیح نیست.');
}
if (!$user['two_factor_enabled']) {
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
header('Location: /dashboard');
exit;
}
$otp = (string) random_int(
100000,
999999
);
$otpHash = hash(
'sha256',
$otp
);
$expiresAt = date(
'Y-m-d H:i:s',
time() + 300
);
$stmt = $pdo->prepare(
'INSERT INTO login_otp_codes
(user_id, otp_hash, expires_at)
VALUES
(:user_id, :otp_hash, :expires_at)'
);
$stmt->execute([
'user_id' => $user['id'],
'otp_hash' => $otpHash,
'expires_at' => $expiresAt
]);
$_SESSION['2fa_user_id'] = $user['id'];
$_SESSION['2fa_pending'] = true;
// ارسال OTP
header('Location: /verify-2fa');
exit;
این نمونه برای آموزش مفهوم Flow است و در پروژه واقعی باید Rate Limiting، CSRF، مدیریت OTPهای قبلی، Transaction و کنترل Session را نیز کامل کنید.
# نمونه تأیید OTP
<?php
session_start();
if (
empty($_SESSION['2fa_pending']) ||
empty($_SESSION['2fa_user_id'])
) {
exit('درخواست نامعتبر است.');
}
$otp = trim(
$_POST['otp'] ?? ''
);
if (!preg_match('/^\d{6}$/', $otp)) {
exit('کد تأیید نامعتبر است.');
}
$stmt = $pdo->prepare(
'SELECT
id,
user_id,
otp_hash,
expires_at,
attempts,
used_at
FROM login_otp_codes
WHERE user_id = :user_id
AND used_at IS NULL
ORDER BY id DESC
LIMIT 1'
);
$stmt->execute([
'user_id' => $_SESSION['2fa_user_id']
]);
$otpRecord = $stmt->fetch(
PDO::FETCH_ASSOC
);
if (!$otpRecord) {
exit('کد تأیید معتبر نیست.');
}
if (
$otpRecord['attempts'] >= 5
) {
exit('تعداد تلاشهای مجاز تمام شده است.');
}
if (
strtotime($otpRecord['expires_at']) < time()
) {
exit('کد تأیید منقضی شده است.');
}
$otpHash = hash(
'sha256',
$otp
);
if (!hash_equals(
$otpRecord['otp_hash'],
$otpHash
)) {
$stmt = $pdo->prepare(
'UPDATE login_otp_codes
SET attempts = attempts + 1
WHERE id = :id'
);
$stmt->execute([
'id' => $otpRecord['id']
]);
exit('کد تأیید صحیح نیست.');
}
$stmt = $pdo->prepare(
'UPDATE login_otp_codes
SET used_at = NOW()
WHERE id = :id
AND used_at IS NULL'
);
$stmt->execute([
'id' => $otpRecord['id']
]);
$userId = $_SESSION['2fa_user_id'];
unset(
$_SESSION['2fa_user_id'],
$_SESSION['2fa_pending']
);
session_regenerate_id(true);
$_SESSION['user_id'] = $userId;
header('Location: /dashboard');
exit;
# یک نکته مهم درباره نمونه بالا
در پروژه واقعی، بهتر است عملیات مصرف OTP به شکل اتمیک انجام شود.
یعنی نباید دو درخواست همزمان بتوانند یک OTP را مصرف کنند.
برای همین میتوان از Transaction و شرط:
WHERE used_at IS NULL
استفاده کرد و نتیجه UPDATE را نیز بررسی کرد.
این موضوع در سیستمهایی که چند درخواست همزمان دریافت میکنند اهمیت بیشتری دارد.
# 2FA فقط برای Login نیست
میتوان 2FA یا Step-up Authentication را برای عملیات حساس نیز استفاده کرد.
مثلاً:
Login
Change Password
Change Email
Withdraw Money
Delete Account
Disable 2FA
Generate API Key
در این حالت کاربر ممکن است قبلاً Login کرده باشد، اما برای انجام یک عملیات حساس دوباره احراز هویت شود.
# تفاوت Authentication و Authorization
این دو مفهوم نیز نباید با یکدیگر اشتباه شوند.
Authentication
یعنی:
تو چه کسی هستی؟
Authorization
یعنی:
چه کاری اجازه داری انجام دهی؟
2FA بخشی از Authentication است.
مثلاً:
Password
+
OTP
=
Authentication
اما اینکه کاربر اجازه حذف یک محصول را داشته باشد، موضوع Authorization است.
# اشتباهات رایج در پیادهسازی 2FA
1. استفاده از rand()
اشتباه:
$otp = rand(100000, 999999);
برای کاربردهای امنیتی از منبع تصادفی مناسب استفاده کنید.
2. OTP بدون Expiration
OTP نباید برای مدت نامحدود معتبر باشد.
3. عدم محدودیت تلاش
این کار Brute Force را سادهتر میکند.
4. OTP قابل استفاده چندباره
بعد از موفقیت باید مصرف شود.
5. ذخیره OTP خام
بهتر است Hash آن ذخیره شود.
6. ثبت OTP در Log
اطلاعات حساس را Log نکنید.
7. Resend نامحدود
ارسال مجدد باید Rate Limited باشد.
8. Login کامل قبل از OTP
قبل از تأیید عامل دوم، Session کامل Login ایجاد نکنید.
9. امکان Disable کردن 2FA بدون Re-authentication
خاموش کردن 2FA یک عملیات حساس است.
10. بیتوجهی به Session
فعال بودن 2FA به معنی بینیازی از امنیت Session نیست.
# چکلیست امنیت 2FA
قبل از انتشار سیستم 2FA موارد زیر را بررسی کنید:
- [ ] Password قبل از مرحله دوم بررسی میشود.
- [ ] OTP با منبع تصادفی امن تولید میشود.
- [ ] OTP مدت اعتبار محدود دارد.
- [ ] OTP یکبارمصرف است.
- [ ] OTP خام در دیتابیس ذخیره نمیشود.
- [ ] تعداد تلاشها محدود است.
- [ ] Resend دارای Rate Limit است.
- [ ] Login کامل قبل از OTP انجام نمیشود.
- [ ] Session بعد از تکمیل 2FA بازتولید میشود.
- [ ] Session Cookie امن است.
- [ ] Endpointهای حساس CSRF Protection دارند.
- [ ] OTP در Log ثبت نمیشود.
- [ ] Disable کردن 2FA نیازمند احراز هویت مجدد است.
- [ ] Recovery/Backup Code در نظر گرفته شده است.
- [ ] رویدادهای امنیتی مهم ثبت میشوند.
- [ ] اطلاعات حساس داخل Log قرار نمیگیرد.
- [ ] برای سیستمهای حساس گزینههایی مانند TOTP یا Security Key بررسی شدهاند.
# FAQ
آیا 2FA همان OTP است؟
خیر.
OTP یک روش یا مکانیزم برای ارائه یک کد یکبارمصرف است. 2FA یک مدل احراز هویت است که از دو عامل متفاوت استفاده میکند.
آیا Password + OTP از طریق Email همان 2FA است؟
در بسیاری از طراحیها میتواند به عنوان یک مدل دومرحلهای استفاده شود، اما باید دقیقاً بررسی شود که عوامل مورد استفاده واقعاً مستقل و مناسب سطح تهدید سیستم هستند.
OTP چند دقیقه اعتبار داشته باشد؟
به نیاز سیستم بستگی دارد. برای OTPهای Login معمولاً زمان کوتاه مانند چند دقیقه قابل استفاده است.
آیا OTP باید در دیتابیس Hash شود؟
بهتر است مقدار قابل استفاده مستقیم در دیتابیس نگهداری نشود و Hash آن ذخیره شود.
آیا SMS برای 2FA امن است؟
SMS میتواند برای برخی کاربردها قابل استفاده باشد، اما ریسکهایی مانند SIM Swap دارد. برای سیستمهای حساس بهتر است گزینههای قویتر نیز بررسی شوند.
TOTP چیست؟
TOTP یک کد یکبارمصرف مبتنی بر زمان است که بین Secret سرور و Authenticator App تولید میشود.
اگر کاربر گوشی خود را گم کند چه میشود؟
به همین دلیل بهتر است Recovery Code یا یک فرآیند بازیابی امن برای حساب طراحی شود.
آیا بعد از وارد کردن Password باید Session ایجاد کنیم؟
میتوان یک Session موقت برای وضعیت 2fa_pending داشت، اما نباید قبل از تکمیل عامل دوم، کاربر را به عنوان Login کامل در نظر گرفت.
# جمعبندی
احراز هویت دو مرحلهای یکی از مهمترین روشها برای افزایش امنیت حسابهای کاربری است.
ساختار ساده یک Login مجهز به 2FA:
Email
↓
Password
↓
Password Verification
↓
2FA Check
↓
OTP / TOTP
↓
Rate Limit
↓
Verify Second Factor
↓
Session Regeneration
↓
Authenticated Session
اما امنیت 2FA فقط به تولید یک کد شش رقمی محدود نمیشود.
یک پیادهسازی مناسب باید موارد زیر را نیز در نظر بگیرد:
Secure Randomness
+
Expiration
+
Single-use
+
Attempt Limit
+
Rate Limiting
+
Secure Session
+
CSRF Protection
+
Recovery Codes
+
Re-authentication
همچنین باید بین روشهای مختلف 2FA تفاوت قائل شویم. Email OTP، SMS OTP، TOTP و Security Key از نظر معماری و سطح حفاظت یکسان نیستند و انتخاب روش مناسب باید بر اساس حساسیت سیستم و مدل تهدید انجام شود.
در یک پروژه PHP ساده میتوان OTP را با random_int() تولید کرد، Hash آن را در دیتابیس نگه داشت و بعد از تأیید، Session کاربر را ایجاد کرد. در پروژههای حرفهایتر، استفاده از TOTP، Recovery Code، Rate Limiting دقیق، ثبت رویدادهای امنیتی و مدیریت کامل Session اهمیت بیشتری پیدا میکند.





