OAuth

OAuth

دع الأشخاص يسجّلون الدخول إلى عميلك بحساب Atli الخاص بهم، وتصرّف نيابةً عنهم، باستخدام OAuth 2.1 القياسي: الاكتشاف، وPKCE، ورموز التحديث التي تتجدّد مع كل استخدام، والإلغاء. يمكن لأي عميل استخدامه: تطبيق، أو روبوت، أو موقع ويب، أو خادم، أو مساعد ذكاء اصطناعي.

متى تستخدمه

  • مساعدات الذكاء الاصطناعي من هذه العملاء. عندما تتصل بخادم MCP الخاص بـ Atli، تسجّل دخول الأشخاص بهذه الطريقة من تلقاء نفسها، فلا يلزمك بناء شيء لها. والتدفّق نفسه يخدم كل عميل.

  • أي عميل يتصرّف نيابةً عن شخص: تطبيق هاتف، أو موقع ويب، أو روبوت، أو خادم يعرض على شخص شرائح eSIM الخاصة به أو يشتري له واحدة، بحساب Atli الخاص به. سجّل عميلًا خاصًا بك، وستعمل رموزه على واجهة Atli البرمجية.

  • يمكن ربط الأشخاص الذين يسجّلون الدخول عبر عميل تملكه، أو على عنوان الشريك الخاص بك في خادم MCP، بك بوصفهم من إحالاتك. هذا معطّل افتراضيًا، ويمكن لـ Atli تفعيله لحسابك. وفي كل الأحوال، يستطيع عميلك بنفسه أن ينسب إليك الأشخاص الذين يجلبهم: راجع «احصل على رصيد الأشخاص الذين تجلبهم» أدناه.

  • لا يمكن ربط إلا حساب يستطيع صاحبه تسجيل الدخول إليه مجددًا في مكان آخر: حساب ببريد إلكتروني، بما في ذلك عبر Google أو Apple أو Facebook، أو بـ Telegram أو WhatsApp.

كيف يعمل

واجهة Atli البرمجية هي خادم التفويض. تسجيل الدخول وشاشة الموافقة على heyatli.com، في /connect، لذا لا يرى تطبيقك كلمة مرور أبدًا. اقرأ كل نقطة نهاية من البيانات الوصفية بدلًا من كتابتها يدويًا. العناوين هنا تتبع مفتاح التبديل بين بيئة الاختبار وبيئة الإنتاج.

سجّل عميلًا مرة واحدة. ثم لكل شخص: أرسله إلى نقطة نهاية التفويض مع تحدّي PKCE، وخذ الرمز الذي يعود به، واستبدله برموز وصول، واستدعِ الواجهة برمز الوصول. حدّثه عند انتهاء صلاحيته، وألغِه عند تسجيل خروجه.

curl https://api.heyatli.com/.well-known/oauth-authorization-server

{
  "issuer": "https://api.heyatli.com",
  "authorization_endpoint": "https://api.heyatli.com/oauth2/authorize",
  "token_endpoint": "https://api.heyatli.com/oauth2/token",
  "revocation_endpoint": "https://api.heyatli.com/oauth2/revoke",
  "registration_endpoint": "https://api.heyatli.com/oauth2/register",
  "jwks_uri": "https://api.heyatli.com/oauth2/jwks",
  "scopes_supported": ["api", "offline_access"],
  "code_challenge_methods_supported": ["S256"],
  "client_id_metadata_document_supported": true,
  …
}

لأي شيء يصلح الرمز

كل طلب يحدّد العنوان الذي رمزه مخصّص له، في resource (RFC 8707)، ولا يعمل الرمز إلا هناك. العنوان الذي ليس من هذه العناوين، أو رمز الإحالة غير الموجود، يُرفض بـ invalid_target، فلا يمكن تخمين أيٍّ منهما.

العنوانمن يمكنه طلبهماذا يفعل الرمز
واجهة Atli البرمجية
https://api.heyatli.com
عميل تملكه فقط

رمز Bearer للواجهة البرمجية يتصرّف بصفة الشخص، مثل الرمز الذي يأتي من /login.

خادم MCP الخاص بـ Atli
https://mcp.heyatli.com
أي عميل

يتيح لمساعد أن يتصرّف نيابةً عن الشخص عبر أدوات MCP.

عنوان الشريك الخاص بك
https://mcp.heyatli.com/r/YOUR_CODE
أي عميل

الشيء نفسه، ويمكن ربط الشخص بك بوصفه من إحالاتك، بعد تفعيل ذلك لحسابك.

تسجيل عميل

هناك ثلاث طرق. لتطبيقك الخاص، سجّل عميلًا خاصًا بك: إنه النوع الوحيد الذي يمكنه طلب واجهة Atli البرمجية.

  1. عميل خاص بك

    وأنت مسجّل الدخول بحسابك، استدعِ POST /user/oauth-clients. يمكنك امتلاك 10 عملاء. اختر public لأي شيء يعمل على جهاز شخص آخر، مثل تطبيق هاتف أو صفحة في متصفحه، وconfidential لخادم يستطيع حفظ سرّ.

    قيمة client_secret للعميل السرّي موجودة في هذه الاستجابة ولا مكان آخر. إذا فقدتها، استبدلها بـ POST /user/oauth-clients/{id}/rotate-secret. يحتاج حسابك إلى بريد إلكتروني، لأن العميل يحتاج إلى رمز إحالة؛ ويُنشأ لك رمز إن لم يكن لديك واحد بعد.

    curl https://api.heyatli.com/user/oauth-clients \
      -H 'Authorization: Bearer YOUR_TOKEN' \
      -H 'Content-Type: application/json' \
      -d '{ "name": "Travel Bot",
            "redirect_uris": ["https://bot.example/oauth/callback"],
            "client_type": "confidential" }'
    
    {
      "client_id": "atli_client_7f3a9c2e…",
      "client_secret": "…",
      "client_type": "confidential",
      …
    }
  2. التسجيل الديناميكي

    POST /oauth2/register (RFC 7591)، دون تسجيل دخول. هكذا تسجّل معظم المساعدات. يسمح بـ 10 تسجيلات في الساعة من عنوان واحد، والعميل الذي لا يتصل به أحد أبدًا يُحذف بعد 30 يومًا.

    curl https://api.heyatli.com/oauth2/register \
      -H 'Content-Type: application/json' \
      -d '{ "client_name": "Travel Bot",
            "redirect_uris": ["https://bot.example/oauth/callback"] }'
    
    {
      "client_id": "atli_client_…",
      "token_endpoint_auth_method": "none",
      …
    }
  3. مستند البيانات الوصفية لمعرّف العميل

    استخدم عنوان https تستضيفه أنت قيمةً لـ client_id، يقدّم مستند JSON يصف العميل. تقرؤه Atli عندما يسجّل أحد الدخول، فلا يوجد ما يلزم تسجيله. وتفضّل مواصفة MCP هذه الطريقة على التسجيل الديناميكي.

    // Served at https://bot.example/oauth/client.json
    {
      "client_id": "https://bot.example/oauth/client.json",
      "client_name": "Travel Bot",
      "redirect_uris": ["https://bot.example/oauth/callback"],
      "token_endpoint_auth_method": "none"
    }
  • تُطابَق عناوين URI لإعادة التوجيه تطابقًا تامًّا. كلٌّ منها https، أو http على localhost أو 127.0.0.1 أو [::1] حيث يمكن أن يختلف المنفذ فقط، ودون جزء fragment. يمكن أن يكون للعميل حتى 10 منها.

تسجيل دخول شخص

تدفّق رمز التفويض مع PKCE (S256)، ويستخدمه كل عميل، عامًّا كان أو سرّيًّا.

  1. أنشئ زوج PKCE

    احتفظ بالـ verifier لتبادل الرمز، وأرسل تحدّيه SHA-256 مع الطلب. أنشئ زوجًا جديدًا، وstate جديدًا، لكل تسجيل دخول.

    const base64url = (bytes) =>
      btoa(String.fromCharCode(...bytes))
        .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
    
    const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
    const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
    const challenge = base64url(new Uint8Array(digest));
    const state = base64url(crypto.getRandomValues(new Uint8Array(16)));
  2. أرسله لتسجيل الدخول

    افتح نقطة نهاية التفويض في متصفحه. قيمة scope هي api، وoffline_access يضيف رمز تحديث. يسجّل الشخص الدخول ويوافق على heyatli.com/connect، الذي يعرض اسم عميلك، وإلى أين يعيده، وما إذا كان سيصبح من إحالات أحد. تدوم صلاحية الطلب 10 دقائق.

    const url = new URL('https://api.heyatli.com/oauth2/authorize');
    url.search = new URLSearchParams({
      response_type: 'code',
      client_id: CLIENT_ID,
      redirect_uri: 'https://bot.example/oauth/callback',
      scope: 'api offline_access',
      state,
      code_challenge: challenge,
      code_challenge_method: 'S256',
      // Atli's API, for a client you own; or the MCP server, https://mcp.heyatli.com
      resource: 'https://api.heyatli.com',
    }).toString();
    window.location.assign(url);
  3. خذ الرمز الذي يعود به

    يعود إلى عنوان URI لإعادة التوجيه لديك ومعه code وstate الخاص بك وiss. تحقّق من أن state لك وأن iss هو الجهة المُصدِرة. إذا رفض، فسيحمل error=access_denied بدلًا من ذلك.

    https://bot.example/oauth/callback?code=4bd81f…&state=…&iss=https%3A%2F%2Fapi.heyatli.com
    
    https://bot.example/oauth/callback?error=access_denied&state=…&iss=https%3A%2F%2Fapi.heyatli.com
  4. استبدل الرمز برموز وصول

    أرسل الرمز مع الـ verifier، ونفس redirect_uri وresource، وبيانات اعتماد عميلك إن كانت لديه. يدوم الرمز 60 ثانية ويعمل مرة واحدة: إرساله مرتين، أو مع verifier خاطئ، ينهي وصول الشخص، لأن شخصًا آخر قد يكون حصل عليه.

    يدوم رمز الوصول expires_in ثانية، أي 15 دقيقة. ويأتي معه رمز تحديث دائمًا.

    curl https://api.heyatli.com/oauth2/token \
      -u 'CLIENT_ID:CLIENT_SECRET' \
      -d grant_type=authorization_code \
      -d code=THE_CODE \
      -d code_verifier=THE_VERIFIER \
      -d redirect_uri=https://bot.example/oauth/callback \
      -d client_id=CLIENT_ID \
      -d resource=https://api.heyatli.com
    
    {
      "access_token": "…",
      "token_type": "Bearer",
      "expires_in": 900,
      "refresh_token": "…",
      "scope": "api offline_access"
    }

أبقِه مسجّل الدخول

عندما تنتهي صلاحية رمز الوصول، اطلب رمزًا جديدًا برمز التحديث. يمكنك طلب scope أضيق، ولا يمكنك طلب نطاق أوسع أبدًا.

يتغيّر رمز التحديث مع كل استخدام، لذا احفظ الرمز الجديد في كل مرة. إذا لم تصلك استجابة، فإن إعادة إرسال الرمز القديم خلال فترة قصيرة تعيد الاستجابة نفسها. بعد ذلك، يعني الرمز القديم أنه نُسخ، وينتهي وصول الشخص.

curl https://api.heyatli.com/oauth2/token \
  -u 'CLIENT_ID:CLIENT_SECRET' \
  -d grant_type=refresh_token \
  -d refresh_token=THE_REFRESH_TOKEN \
  -d client_id=CLIENT_ID \
  -d resource=https://api.heyatli.com

استخدم الرمز

أرسله بصيغة Authorization: Bearer ACCESS_TOKEN، إلى العنوان الذي أُصدر له ولا مكان آخر.

رمز واجهة Atli البرمجية يتصرّف بصفة الشخص على كل مسارات الواجهة، كما يفعل رمز /login. تعامل معه على أنه معتم: ما بداخله قد يتغيّر.

رمز خادم MCP هو JWT موقّع بـ EdDSA. يتحقّق الخادم منه بالمفاتيح الموجودة في /oauth2/jwks، حسب kid الخاص به، ويتحقّق من أن aud هو عنوانه هو. لا يحتوي على بريد إلكتروني أو أي تفاصيل أخرى: فقط sub (معرّف الشخص) وscope وgrant_id والأوقات.

curl https://api.heyatli.com/esims \
  -H 'Authorization: Bearer ACCESS_TOKEN'

احصل على رصيد الأشخاص الذين تجلبهم

ربط الأشخاص الذين يسجّلون الدخول عبر عميلك، أو على عنوان الشريك الخاص بك، بك معطّل افتراضيًا، ويمكن لـ Atli تفعيله لحسابك. وسواء كان مفعّلًا أم لا، يستطيع عميلك بنفسه ربط شخص مسجّل الدخول برمز الإحالة الخاص بك، باستخدام رمز الوصول الخاص به: POST /user/referral/redeem مع code الخاص بك.

يوضّح result ما حدث: ok تعني أنه رُبط، وalready أن لديه مُحيلًا بالفعل، وnotnew أنه سبق له الشراء، وown أنه رمزك أنت، وnotfound أن الرمز غير موجود. لا يمكن ربط إلا شخص ليس لديه مُحيل ولم يشترِ بعد، ولمرة واحدة فقط.

curl https://api.heyatli.com/user/referral/redeem \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{ "code": "YOUR_CODE" }'

{
  "success": true,
  "result": "ok",
  "message": "…",
  "discount_percent": 10
}

تسجيل الخروج وسحب الوصول

عندما يسجّل أحد الخروج من تطبيقك، ألغِ رمز التحديث الخاص به. ينهي ذلك وصوله في عميلك، مع كل الرموز التابعة له. تكون الاستجابة 200 حتى لرمز غير معروف، فلا يمكن استخدامها لمعرفة الرموز الموجودة.

يمكن للأشخاص أيضًا رؤية التطبيقات التي ربطوها وسحب الوصول منها بأنفسهم (GET /user/oauth-grants، DELETE /user/oauth-grants/{id}). ومن حينها لا يستطيع عميلك الحصول على رمز جديد، لذا تعامل مع رفض التحديث على أنه تسجيل خروج.

curl https://api.heyatli.com/oauth2/revoke \
  -u 'CLIENT_ID:CLIENT_SECRET' \
  -d token=THE_REFRESH_TOKEN \
  -d client_id=CLIENT_ID

الأخطاء

تستجيب نقاط نهاية الرمز والإلغاء والتسجيل بصيغة الخطأ الخاصة بـ OAuth، {"error": "invalid_grant", "error_description": "…"}، بدلًا من الصيغة المعتادة للواجهة البرمجية. ابنِ منطقك على error، ولا تعتمد على الوصف أبدًا. كل استجابة من نقطة نهاية الرمز تحمل Cache-Control: no-store.

  • invalid_grant: الرمز استُخدم أو انتهت صلاحيته، أو الـ verifier خاطئ، أو أُعيد استخدام رمز تحديث بعد انتهاء فترة السماح. سجّل دخول الشخص من جديد.

  • invalid_target: قيمة resource ليست عنوانًا يمكن أن يكون الرمز له، أو تشير إلى رمز إحالة غير موجود.

  • invalid_client: بيانات اعتماد العميل خاطئة أو مفقودة. كل الأنواع تفشل بالطريقة نفسها.

  • invalid_scope: طلب التحديث أكثر مما سمح به الشخص.