پرش به محتویات

احراز هویت (Authentication)

مقدمه

تمامی درخواست‌ها به API (به جز اندپوینت‌های login و refresh) نیاز به احراز هویت دارند. این سیستم بر اساس JWT (JSON Web Token) پیاده‌سازی شده است.

انواع توکن‌ها

1. Access Token

  • مدت زمان: ۱۰ دقیقه

  • کاربرد: دسترسی به API

  • فرمت: JWT با امضای نامتقارن

  • نحوه استفاده: در هدر Authorization

2. Refresh Token

  • مدت زمان: ۷ روز

  • کاربرد: دریافت Access Token جدید

  • فرمت: رشته منحصر به فرد

  • نحوه استفاده: فقط برای refresh

گام 1: دریافت توکن اولیه

اندپوینت Login

curl -X POST "$BASE_URL/api/sso/v1/auth/login" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "your_client_id",
    "client_secret": "your_client_secret"
  }'

توضیحات: - client_id: شناسه کلاینت بانک که از مرکز مبادله دریافت کرده‌است - client_secret: رمز عبور مربوط به کلاینت

نمونه موفق

درخواست:

curl -X POST "$BASE_URL/api/sso/v1/auth/login" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "client_abc123",
    "client_secret": "secret_xyz789"
  }'

پاسخ (200 OK):

{
  "iss": "{BASE_URL}",
  "sub": "0197f464-46e0-7392-a54f-b08f07be6926",
  "iat": 1767120162,
  "exp": 1767120762,
  "jti": "a0b3af36-e498-4a62-9499-5aea9d7be5fe",
  ...
  "tokens": {
    "access": "<access_token>",
    "refresh": "<refresh_token>"
  }
}

نمونه ناموفق 1: کلاینت نامعتبر

درخواست:

curl -X POST "$BASE_URL/api/sso/v1/auth/login" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "wrong_client",
    "client_secret": "wrong_secret"
  }'

پاسخ (401 Unauthorized):

{
  "message": {
    "en": "Authentication failed"
  },
  "error": "invalid_credentials",
  "detail": "Authentication failed"
}

نمونه ناموفق 2: داده‌های ناقص

درخواست:

curl -X POST "$BASE_URL/api/sso/v1/auth/login" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "client_abc123"
    # client_secret حذف شده
  }'

پاسخ (422 Unprocessable Entity):

{
  "detail": [
    {
      "type": "missing",
      "loc": [
        "body",
        "client_secret"
      ],
      "msg": "Field required",
      "input": {
        "client_id": "client_abc123"
      }
    }
  ]
}

نمونه ناموفق 3: فرمت JSON نامعتبر

درخواست:

curl -X POST "$BASE_URL/api/sso/v1/auth/login" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "client_abc123",
    "client_secret": "secret_xyz789",
  }'  # فرمت صحیح است، اما فرض کنیم کاما اضافی دارد

پاسخ (422 Bad Request):

{
    "detail": [
        {
            "type": "json_invalid",
            "loc": [
                "body",
                66
            ],
            "msg": "JSON decode error",
            "input": {},
            "ctx": {
                "error": "Illegal trailing comma before end of object"
            }
        }
    ]
}

گام 2: استفاده از Access Token

فرمت هدر

در تمامی درخواست‌ها به API، هدر زیر الزامی است:

Authorization: Bearer <access_token>

نمونه استفاده

درخواست موفق:

# تعریف متغیرها
JWT_TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."

# درخواست به API
curl -X GET "$BASE_URL/api/exchange/v1/symbols" \
  -H "Authorization: Bearer $JWT_TOKEN"

پاسخ (200 OK):

{
  "heads": {
    "id": {
      "en": "Id"
    },
    "revision_id": {
      "en": "Revision Id"
    },
    "uid": {
      "en": "Uid"
    },
    "created_at": {
      "en": "Created At"
    },
    "updated_at": {
      "en": "Updated At"
    },
    "is_deleted": {
      "en": "Is Deleted"
    },
    "meta_data": {
      "en": "Meta Data"
    },
    "user_id": {
      "en": "User Id"
    },
    "symbol": {
      "en": "Symbol"
    },
    "price_precision": {
      "en": "Price Precision"
    },
    "quantity_precision": {
      "en": "Quantity Precision"
    },
    "min_order_quantity": {
      "en": "Min Order Quantity"
    },
    "tick_size": {
      "en": "Tick Size"
    },
    "daily_fluctuation_price_limit_percent": {
      "en": "Daily Fluctuation Price Limit Percent"
    },
    "description": {
      "en": "Description"
    },
    "is_active": {
      "en": "Is Active"
    }
  },
  "items": [
    {
      "uid": "01995196-ef5e-7239-87b9-e93d61004317",
      "created_at": "2025-09-16T08:14:53.306000",
      "updated_at": "2025-09-16T08:14:53.306000",
      "is_deleted": false,
      "meta_data": null,
      "user_id": "admin",
      "symbol": "EUR-IRR",
      "price_precision": 0,
      "quantity_precision": -2,
      "min_order_quantity": "100",
      "tick_size": "100",
      "daily_fluctuation_price_limit_percent": "5",
      "description": "Euro to Iranian Rial",
      "is_active": true,
      "properties": {
        "EUR": {
          "name": {
            "fa": "یورو",
            "en": "Euro"
          },
          "symbol": "EUR",
          "precision": 2,
          "icon": "https://flagcdn.com/w40/eu.png",
          "is_crypto": false,
          "color": "#26a17b",
          "unit_value_irr": 1000000
        },
        "IRR": {
          "name": {
            "fa": "ریال",
            "en": "Iranian Rial"
          },
          "symbol": "IRR",
          "precision": 0,
          "icon": "https://flagcdn.com/w40/ir.png",
          "is_crypto": false,
          "color": "#1976d2",
          "unit_value_irr": 1
        }
      }
    },
    {
      "uid": "019855a6-0f44-7767-9bfb-65672b207fde",
      "created_at": "2025-07-29T10:06:43.012000",
      "updated_at": "2025-07-29T10:06:43.012000",
      "is_deleted": false,
      "meta_data": null,
      "user_id": "admin",
      "symbol": "USD-IRR",
      "price_precision": 0,
      "quantity_precision": -2,
      "min_order_quantity": "100",
      "tick_size": "100",
      "daily_fluctuation_price_limit_percent": "5",
      "description": "US Dollar to Iranian Rial",
      "is_active": true,
      "properties": {
        "USD": {
          "name": {
            "fa": "دلار",
            "en": "Dollar"
          },
          "symbol": "USD",
          "precision": 2,
          "icon": "https://flagcdn.com/w40/us.png",
          "is_crypto": false,
          "color": "#f7931a",
          "unit_value_irr": 900000
        },
        "IRR": {
          "name": {
            "fa": "ریال",
            "en": "Iranian Rial"
          },
          "symbol": "IRR",
          "precision": 0,
          "icon": "https://flagcdn.com/w40/ir.png",
          "is_crypto": false,
          "color": "#1976d2",
          "unit_value_irr": 1
        }
      }
    }
  ],
  "total": 2,
  "offset": 0,
  "limit": 10
}```

### نمونه ناموفق 1: بدون توکن

**درخواست:**
```bash
curl -X GET "$BASE_URL/api/exchange/v1/symbols"

پاسخ (401 Unauthorized):

{
    "message": "No token provided",
    "error": "Unauthorized",
    "detail": "No token provided"
}

نمونه ناموفق 2: توکن منقضی شده

درخواست:

curl -X GET "$BASE_URL/api/exchange/v1/symbols" \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.expired_token"

پاسخ (401 Unauthorized):

{
  "message": "JWT has expired",
  "error": "Unauthorized",
  "detail": "JWT has expired"
}

نمونه ناموفق 3: فرمت اشتباه - بدون Bearer

درخواست:

curl -X GET "$BASE_URL/api/exchange/v1/symbols" \
  -H "Authorization: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."

پاسخ (401 Unauthorized):

{
  "message": "JWT has an invalid format",
  "error": "Unauthorized",
  "detail": "JWT has an invalid format"
}

نمونه ناموفق 4: توکن دستکاری شده

درخواست:

curl -X GET "$BASE_URL/api/exchange/v1/symbols" \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.modified_part"

پاسخ (401 Unauthorized):

{
  "message": "JWT signature is invalid",
  "error": "Unauthorized",
  "detail": "JWT signature is invalid"
}

نمونه ناموفق 5: توکن بدون سطح دسترسی مناسب

درخواست:

# کلاینت A سعی می‌کند از توکن کلاینت B استفاده کند
curl -X GET "$BASE_URL/api/sso/v1/users/user_123" \
  -H "Authorization: Bearer <token_of_client_B>"

پاسخ (403 Forbidden):

{
  "message": "JWT signature is invalid",
  "error": "permission_denied",
  "detail": "..."
}

گام 3: رفرش توکن

اندپوینت Refresh

POST $BASE_URL/api/sso/v1/auth/refresh

پارامترهای ورودی

{
  "refresh_token": "string"
}

نمونه موفق

درخواست:

curl -X POST ${BASE_URL}/api/sso/v1/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{
    "refresh_token": "refresh_token_xyz789abc123"
  }'

پاسخ (200 OK):

{
  "status": "refreshed",
  "tokens": {
    "access": "<access_token>",
    "refresh": "<refresh_token>"
  },
  "expires_in": 600
}

نمونه ناموفق 1: Refresh Token نامعتبر

درخواست:

curl -X POST "${BASE_URL}/api/auth/v1/refresh" \
  -H "Content-Type: application/json" \
  -d '{
    "refresh_token": "invalid_refresh_token"
  }'

پاسخ (401 Unauthorized):

{
  "message": "JWT has an invalid format",
  "error": "Unauthorized",
  "detail": "JWT has an invalid format"
}

نمونه ناموفق 2: داده ناقص

درخواست:

curl -X POST "$BASE_URL/api/sso/v1/auth/refresh" \
  -H "Content-Type: application/json" \
  -d '{}'

پاسخ (422 Unprocessable Entity):

{
  "detail": [
    {
      "type": "missing",
      "loc": [
        "body",
        "refresh_token"
      ],
      "msg": "Field required",
      "input": {}
    }
  ]
}

ساختار JWT Token

Payload نمونه

{
  "iss": "$BASE_URL",
  "sub": "0197f464-46e0-7392-a54f-b08f07be6926",
  "iat": 1767121796,
  "exp": 1767122396,
  "jti": "322fa880-b4c6-4a05-9db9-f952edc1b5f0",
  "token_type": "access",
  "session_id": "019b70aa-bceb-7f9c-a820-ceeafea93310",
  "tenant_id": "0197f464-46c8-72a1-8b23-0819b8622a0c",
  "roles": [
    "admin",
    "user"
  ],
  "scopes": [
    "*:*"
  ],
  "amr": [
    "client_secret"
  ]
}

توضیحات فیلدها

  • iss: صادرکننده توکن (issuer)

  • sub: شناسه کلاینت (subject)

  • iat: زمان صدور (issued at time - Unix timestamp)

  • exp: زمان انقضا (expiration time - Unix timestamp)

  • jti: شناسه منحصر به فرد توکن (JWT ID)

  • token_type: نوع توکن (access/refresh)

  • session_id: شناسه جلسه

  • tenant_id: شناسه tenant

  • roles: نقش‌های کاربر (لیست رشته‌ها)

  • scopes: حوزه‌های دسترسی (لیست رشته‌ها)

  • amr: روش‌های احراز هویت (authentication methods)

چرخه عمر توکن

sequenceDiagram
    participant Client
    participant AuthAPI
    participant OtherAPI

    Client->>AuthAPI: POST /login (client_id, secret)
    AuthAPI-->>Client: access_token + refresh_token

    Client->>OtherAPI: GET /resource (با access_token)
    OtherAPI-->>Client: داده‌ها

    Note over Client,AuthAPI: access_token منقضی می‌شود

    Client->>AuthAPI: POST /refresh (refresh_token)
    AuthAPI-->>Client: access_token جدید

    Client->>OtherAPI: GET /resource (با access_token جدید)
    OtherAPI-->>Client: داده‌ها

    Note over Client,AuthAPI: refresh_token منقضی می‌شود

    Client->>AuthAPI: POST /login (جدد)
    AuthAPI-->>Client: access_token + refresh_token جدید

چک‌لیست امنیتی

درست: - [ ] توکن را در متغیر محیطی ذخیره کنید

  • [ ] از HTTPS استفاده کنید

  • [ ] توکن را در لاگ‌ها نمایش ندهید

  • [ ] توکن را در URL ندهید

  • [ ] هر ۲۴ ساعت رفرش کنید

  • [ ] refresh_token را امن نگه دارید

اشتباه: - [ ] توکن را در کد source control قرار دهید

  • [ ] از HTTP استفاده کنید

  • [ ] توکن را در لاگ‌ها بنویسید

  • [ ] توکن منقضی شده را دوباره استفاده کنید

  • [ ] refresh_token را به اشتراک بگذارید

خلاصه

  1. Login: دریافت access_token و refresh_token با client_id/secret
  2. استفاده: ارسال Authorization: Bearer <access_token> در هر درخواست
  3. Refresh: وقتی access_token منقضی شد، با refresh_token جدید بگیرید
  4. امنیت: همیشه از HTTPS استفاده کنید و توکن‌ها را محرمانه نگه دارید

بعدی: مطالعه قراردادها و کانوانشن‌ها