Skip to main content

احراز هویت

احراز هویت روی OTP پیامکی + JWT دو‌توکنی استوار است، و برای این‌که کاربر بتواند هم‌زمان از چند دستگاه وارد شود، به‌صورت مبتنی بر دستگاه (device-aware) طراحی شده.

جریان ورود (OTP)

ورود دو مرحله دارد:

  1. POST /api/auth/send-otp — با گرفتنِ phone، یک کد یک‌بار‌مصرف ۶ رقمی از طریق کاوه‌نگار پیامک می‌شود.
  2. POST /api/auth/verify-otp — با phone + otp، کد اعتبارسنجی می‌شود. اگر شماره جدید باشد، کاربر و یک تقویم پیش‌فرض به‌صورت خودکار برایش ساخته می‌شوند. پاسخ شاملِ یک accessToken و یک refreshToken است، و اگر device هم فرستاده باشید، یک DeviceDto هم برمی‌گردد.

مسیر POST /api/auth/login هم برای ورود با ایمیل/رمز موجود است و همان شکلِ پاسخ را می‌دهد.

روی خط فرمان:

# ۱) ارسال OTP
curl -X POST http://localhost:5000/api/auth/send-otp \
-H "Content-Type: application/json" \
-d '{"phone": "09123456789", "name": "Test User"}'

# ۲) اعتبارسنجی OTP (کد را از پاسخ یا SMS بگیرید)
curl -X POST http://localhost:5000/api/auth/verify-otp \
-H "Content-Type: application/json" \
-d '{"phone": "09123456789", "otp": "123456"}'

و پاسخِ verify-otp این شکلی است:

{
"success": true,
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"isNewUser": false,
"user": {
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"phone": "09123456789",
"role": "USER",
"isPhoneVerified": true,
"notificationSettings": {
"syncNotifications": true,
"eventReminders": true,
"conflictAlerts": true
}
}
}
}

توکن‌های JWT

توکنطول عمرکاربرد
Access tokenکوتاه (پیش‌فرض ۱۵ دقیقه)در هدر Authorization: Bearer <accessToken> برای تمام درخواست‌های محافظت‌شده
Refresh tokenطولانی (پیش‌فرض ۹۰ روز)فقط برای گرفتن access token جدید از POST /api/auth/refresh
  • POST /api/auth/refresh — با refreshToken، یک access token جدید می‌دهد و همزمان refresh token را چرخش (rotate) می‌کند؛ یعنی هر بار یک refresh تازه صادر و ذخیره می‌شود.
  • POST /api/auth/logout — refresh token را باطل می‌کند.
  • GET /api/auth/me — اطلاعات کاربر فعلی را برمی‌گرداند.

نمونهٔ کامل روی خط فرمان:

# استفاده از access token
curl -X GET http://localhost:5000/api/auth/me \
-H "Authorization: Bearer <access-token>"

# چرخش refresh token
curl -X POST http://localhost:5000/api/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refreshToken": "<refresh-token>"}'

# خروج (ابطال refresh token)
curl -X POST http://localhost:5000/api/auth/logout \
-H "Authorization: Bearer <access-token>"

حالت مبتنی بر دستگاه (device-aware)

برای این‌که چند دستگاه هم‌زمان کار کنند، هر دستگاه با یک uid (که کلاینت می‌سازد) ثبت می‌شود:

  • در verify-otp یا login می‌توانید یک شیء device: { uid, platform, name, fcmToken } بفرستید؛ آن‌گاه یک refresh token جداگانه مخصوص همان دستگاه صادر می‌شود.
  • POST /api/auth/refresh اگر deviceUid بگیرد، فقط refresh مخصوص همان دستگاه را چرخش می‌دهد و access token صادرشده هم حاویِ deviceUid خواهد بود.
  • تشخیص replay: اگر یک refresh tokenِ چرخش‌یافته (یعنی قبلاً جایگزین شده) دوباره استفاده شود، سرور این را نشانهٔ سوءاستفاده می‌داند و کل خانوادهٔ refresh آن دستگاه را باطل می‌کند — کاربر مجبور به ورود دوباره می‌شود.
  • برای فهرست‌کردن یا ابطالِ دستگاه‌ها به صفحهٔ دستگاه‌ها مراجعه کنید.

مسیرهای قدیمی (بدون device) هم همچنان کار می‌کنند و از توکنِ ذخیره‌شده روی User استفاده می‌کنند.

برای شکلِ دقیقِ request/response به مرجع API مراجعه کنید.

پیاده‌سازی سمت کلاینت (الگوی refresh خودکار)

الگوی پیشنهادی وقتی access token منقضی شد: یک‌بار POST /api/auth/refresh بزنید، token جدید را ذخیره کنید، و درخواستِ اصلی را با token جدید دوباره ارسال کنید. اگر refresh هم شکست خورد، کاربر را به صفحهٔ ورود هدایت کنید.

این retry را فقط یک‌بار اجرا کنید، وگرنه حلقهٔ بی‌نهایت می‌سازد. در مدل مبتنی بر دستگاه، هنگام refresh مقدار deviceUid را هم بفرستید تا refresh مخصوص همان دستگاه چرخش کند.

class AuthManager {
// access/refresh token را در EncryptedSharedPreferences نگه دارید

suspend fun makeAuthenticatedRequest(request: suspend () -> Response): Response {
var response = request()
if (response.code == 401) {
refreshAccessToken() // POST /api/auth/refresh با deviceUid
response = request() // یک‌بار retry با token جدید
}
return response
}
}

خطاهایی که در این مسیر معمولاً می‌بینید:

وضعیت HTTPمعنیاقدام
401 (access منقضی)access token تمام شدهrefresh بزنید و درخواست را دوباره تلاش کنید
401 (refresh نامعتبر)refresh token باطل یا قبلاً استفاده شدهکاربر را مجبور به ورود دوباره کنید
403نوع توکن اشتباه یا دسترسی ممنوعکاربر را به صفحهٔ ورود هدایت کنید

بهترین رویه‌های امنیتی

  • Access token را در حافظه یا storage کوتاه‌مدت نگه دارید؛ عمرش کوتاه است (پیش‌فرض ۱۵ دقیقه) پس ریسکِ نگه‌داری‌اش هم کم است.
  • Refresh token را فقط در storage رمزنگاری‌شده بگذارید — مثل EncryptedSharedPreferences در اندروید، یا localStorage با رمزگذاری در وب. هرگز در متنِ ساده.
  • همیشه انقضای token را اعتبارسنجی کنید و شکستِ refresh را به‌خوبی هندل کنید.

متغیرهای محیطی مربوطه

JWT_SECRET=... # کلید امضای access token
JWT_REFRESH_SECRET=... # کلید امضای refresh token (جدا)
JWT_ACCESS_EXPIRES_IN=15m # عمر access (پیش‌فرض ۱۵ دقیقه)
JWT_REFRESH_EXPIRES_IN=90d # عمر refresh (پیش‌فرض ۹۰ روز)