> ## Documentation Index
> Fetch the complete documentation index at: https://docs.didit.me/llms.txt
> Use this file to discover all available pages before exploring further.

# رموز المصادقة وJWKS: التحقق من JWT في Didit

> تحقق من رموز Didit Authentication: تبادل PKCE، ومطالبات ES256، واكتشاف JWKS لكل تطبيق، وتدوير المفاتيح، واكتشاف إعادة استخدام رموز التحديث.

> [!WARNING]
>
> المصادقة ميزة جديدة. كل استدعاء في هذه الصفحة يستخدم بيانات اعتماد **المستخدم النهائي** أو لا يستخدم أي بيانات اعتماد. لا ترسل أبدًا مفتاح API الخاص بالتحقق أو رمز وحدة التحكم إلى نقاط النهاية هذه.

كل تطبيق هو مُصدِر OpenID Connect مستقل بذاته. والمُصدِر هو أصل صفحة تسجيل الدخول الخاصة بتطبيقك:

* `https://<name>.auth.didit.me` لتطبيق في بيئة الإنتاج،
* `https://<name>.sandbox.auth.didit.me` للتطبيق التجريبي،
* أو نطاقك المخصص، بعد تفعيله.

بيئتا الإنتاج والبيئة التجريبية مُصدِران منفصلان بمفاتيح منفصلة. اضبط `DIDIT_AUTH_ISSUER` على المُصدِر الخاص بك، من دون شرطة مائلة في النهاية، واضبط `DIDIT_APPLICATION_ID` على معرّف تطبيقك، لأن الرموز تحمله في `aud`. يشفّر مفتاحك القابل للنشر المُصدِر: فبعد `pk_live_` أو `pk_test_` يكون المُصدِر بترميز base64url بدون حشو.

> [!NOTE]
>
> عند تفعيل نطاقك المخصص، يحل محل المضيف الافتراضي بوصفه المُصدِر. تحمل الرموز الجديدة اسمه في `iss`، ويشير اكتشاف الإعدادات إليه في `jwks_uri`، ويتغير مفتاحك القابل للنشر. ستفشل رموز الوصول الصادرة عن المضيف الافتراضي في `GET /session/` إلى أن تُجدَّد. حدّث `DIDIT_AUTH_ISSUER` عند تفعيل النطاق، أو اقبل المُصدِرَين معًا إلى أن تنتهي صلاحية الرموز الأقدم، خلال 5 دقائق.

نقاط نهاية الخادم في هذه الصفحة (الاكتشاف والمفاتيح والرموز والجلسة) موجودة في [مرجع واجهة برمجة تطبيقات المصادقة](https://docs.didit.me/ar/authentication/api-reference/get-openid-configuration.md)، وهو مُولَّد من [openapi-authentication.json](https://docs.didit.me/openapi-authentication.json). أما صفحة تسجيل الدخول المستضافة فليست موجودة فيه.

## الاكتشاف والمفاتيح

| الطلب | النتيجة |
| - | - |
| `GET {issuer}/.well-known/openid-configuration` | `issuer` و`authorization_endpoint` و`token_endpoint` و`jwks_uri` وما يدعمه المُصدِر. |
| `GET {issuer}/.well-known/jwks.json` | `{"keys": [...]}`، وهي مفاتيح التوقيع العامة. |

يدعم المُصدِر:

* نوع الاستجابة `code` ونمط الاستجابة `query`.
* نوعَي المنح `authorization_code` و`refresh_token`.
* طريقة PKCE `S256` فقط. العملاء عامّون: لا يوجد سر للعميل، و`client_id` هو معرّف تطبيقك.
* خوارزمية التوقيع `ES256` (مفاتيح P-256).
* النطاقات `openid` و`email` و`phone` و`profile` و`offline_access`.

لا توجد نقاط نهاية لـ userinfo أو introspection أو end-session، و`prompt=none` غير مدعومة. استخدم [`GET /session/`](#التحقق-من-الجلسة) لقراءة الجلسة والتحقق منها.

### التخزين المؤقت والتدوير

يردّ المستندان بالترويسة `Cache-Control: public, max-age=300`، لذا يمكنك أنت والذاكرات المؤقتة المشتركة الاحتفاظ بهما 5 دقائق. ويسمحان بـ 1,200 طلب في الدقيقة و12,000 طلب في الساعة لكل عنوان، وهذا أعلى بكثير من نقاط نهاية تسجيل الدخول، لأن كثيرًا من مثيلات الخادم الخلفي تشترك غالبًا في عنوان واحد.

* لكل مفتاح `kid`، وتبدأ معرّفات المفاتيح الجديدة بـ `k_`. ويذكر كل رمز مفتاحه في ترويسة `kid`.
* تُدوَّر المفاتيح كل 30 يومًا. ينشر Didit المفتاح التالي قبل يوم من بدء توقيعه به، ويُبقي المفتاح السابق منشورًا ليوم بعد توقفه عن التوقيع. لذلك يستمر JWKS المخزّن مؤقتًا لمدة 5 دقائق في التحقق خلال التدوير.
* خزّن المفاتيح مؤقتًا بحسب `kid`. عندما يذكر رمز `kid` لا تملكه، فأعد جلب JWKS، مرة واحدة على الأكثر في الدقيقة، وارفض الرمز إذا بقي المفتاح مفقودًا.
* عندما يُبطل Didit مفتاحًا تعرّض للاختراق، يحذفه من JWKS فورًا، وترفض `GET /session/` كل رمز وقّعه المفتاح. ويبدأ المفتاح البديل بالتوقيع فورًا، دون يوم النشر المسبق. أما JWKS المخزّن مؤقتًا حتى 5 دقائق قبل ذلك فيظل يقبل المفتاح المُبطل ولا يعرف البديل بعد، لذلك قد تفشل الرموز الجديدة في التحقق دون اتصال إلى أن يتجدد التخزين المؤقت.

## بدء تسجيل الدخول

افتح الصفحة المستضافة، وهي `authorization_endpoint`:

```text theme={null}
{issuer}/sign-in?redirect_uri=...&state=...&code_challenge=...&code_challenge_method=S256
```

| المعامل | القاعدة |
| - | - |
| `redirect_uri` | يجب أن يطابق تمامًا أحد [روابط إعادة التوجيه المسجّلة](https://docs.didit.me/ar/authentication/overview.md#عناوين-إعادة-التوجيه) لديك. |
| `state` | حتى 512 حرفًا. تحقق منه في رد الاستدعاء. |
| `code_challenge` | ناتج SHA-256 بترميز base64url للمُحقق PKCE الخاص بك: 43 حرفًا بالضبط. |
| `code_challenge_method` | `S256`. |
| `client_id` | اختياري. يجب أن يساوي معرّف تطبيقك. |
| `nonce` | اختياري. يُنسخ إلى رمز الهوية، فيمكنك ربطه بتسجيل الدخول هذا. |
| `scope` و`login_hint` و`ui_locales` و`max_age` و`prompt` | اختيارية. يملأ `login_hint` حقل البريد الإلكتروني مسبقًا فقط. |

تُنشئ [أدلة البدء السريع](https://docs.didit.me/ar/authentication/quickstarts/javascript.md) هذا الطلب نيابةً عنك.

## استبدال الرمز

يستقبل رد الاستدعاء `code` و`state`. تحقق من `state`، ثم استبدل الرمز خلال 60 ثانية. كل رمز يعمل مرة واحدة فقط. ويمكن أن يكون الجسم بصيغة JSON أو بترميز النموذج.

```bash theme={null}
curl --fail-with-body "${DIDIT_AUTH_ISSUER}/token/" \
  -H 'Content-Type: application/json' \
  -d '{
    "grant_type": "authorization_code",
    "code": "CODE_FROM_CALLBACK",
    "code_verifier": "YOUR_PKCE_VERIFIER",
    "redirect_uri": "https://app.example.com/auth/callback"
  }'
```

```json theme={null}
{
  "access_token": "eyJhbGciOiJFUzI1NiIs...",
  "id_token": "eyJhbGciOiJFUzI1NiIs...",
  "refresh_token": "N4yZTl6y24YKYq77X_SO...",
  "token_type": "Bearer",
  "expires_in": 299,
  "session_id": "c9abf6cc-2d64-4bf2-b31e-a4d09a070ab1"
}
```

يحسب `expires_in` الثواني المتبقية من صلاحية رمز الوصول. وعند إيقاف الجلسات المتعددة، ينهي هذا الاستبدال الجلسات الأخرى للشخص.

| الاستجابة | المعنى |
| - | - |
| `400 {"error": "invalid_grant"}` | الرمز غير معروف أو مستخدم أو منتهٍ؛ أو المُحقق أو `redirect_uri` غير متطابق، أو أن `redirect_uri` لم يعد مسجّلًا؛ أو أن الشخص محظور أو أن بريده الإلكتروني لم يعد موثّقًا؛ أو أنك أوقفت طريقة تسجيل الدخول التي استخدمها. |
| `400 {"error": "invalid_client"}` | `client_id` ليس معرّف تطبيقك. |
| `400 {"error": "mfa_required"}` | لم يستوفِ تسجيل الدخول سياسة المصادقة متعددة العوامل (MFA) لديك. |
| `400 {"<field>": [...]}` | حقل مفقود أو غير صالح، مثل `{"non_field_errors": ["invalid_request"]}` عندما تفتقر المنحة إلى حقولها المطلوبة. |
| `400 {"<field>": "unknown_field"}` | يحتوي الجسم على حقل لا تقبله نقطة النهاية، مثل `client_secret`. العملاء العامّون لا يرسلون أيًا منه. |

## مطالبات رمز الوصول

رمز الوصول هو JWT رأسه `alg: ES256` و`typ: at+jwt`. تبلغ مدة صلاحيته 5 دقائق كحد أقصى، ولا تتجاوز أبدًا مدة جلسته.

| المطالبة | المعنى |
| - | - |
| `iss` | جهة الإصدار الخاصة بك. |
| `aud` | معرّف تطبيقك. |
| `client_id` | معرّف تطبيقك. |
| `sub` | معرّف هوية الشخص في هذا التطبيق. |
| `sid` | معرّف الجلسة. بالنسبة إلى رمز وكيل ذكاء اصطناعي، هو معرّف الوصول الذي منحه الشخص للوكيل. |
| `jti`، `iat`، `exp` | معرّف الرمز، ووقت الإصدار، ووقت الانتهاء. |
| `vendor_data` | المرجع الثابت للشخص، وتتم مشاركته مع التحقق ومراقبة المعاملات. |
| `amr` | طريقة تسجيل الدخول، مثلًا `["passkey"]`. |
| `org` | `{"id", "role"}` لأقدم عضوية للشخص في مؤسسة ضمن تطبيقك، أو `null` إذا لم تكن له عضوية أو كانت المؤسسات معطلة. تسرد `GET /session/` كل العضويات. |
| `verified` | مستوى التحقق والمطالبات التي اخترتها، أو `{}`. يصل التحقق المعتمد خلال الجلسة إلى الرمز عند التحديث التالي. |
| `act` | `null` للشخص. بالنسبة إلى وكيل ذكاء اصطناعي يتصرف نيابةً عن الشخص: `{"sub", "agent", "scopes"}`. انظر [الخطوة 5](#التحقق-من-رمز-على-الخادم-لديك). |

يحمل رمز الهوية الرأس `typ: JWT`. ويحمل المطالبات نفسها باستثناء `client_id`، إضافةً إلى `auth_time` و`email_verified`، وكذلك `email` و`name` و`phone_number` عندما تكون لها قيمة. لا يحمل `nonce` إلا رمز الهوية الناتج عن تبادل الرمز، وذلك إذا كان تسجيل الدخول يتضمن واحدًا، لذا تحقق منه هناك، لأن رموز الهوية الناتجة عن التحديث لا تحمله أبدًا. لا يُقبل رمز الهوية أبدًا بوصفه رمز وصول.

## التحقق من رمز على الخادم لديك

تحقق من كل ما يلي قبل أن تثق بأي مطالبة:

1. يجب أن تكون `alg` هي `ES256` وأن تكون `typ` هي `at+jwt`. ارفض أي خوارزمية أخرى، بما فيها `none`.
2. يجب أن يكون `kid` موجودًا في JWKS الخاص بتطبيقك، وأن يتم التحقق من التوقيع.
3. يجب أن تساوي `iss` جهة الإصدار المُعدّة لديك، وأن تساوي `aud` معرّف تطبيقك.
4. يجب أن تكون `exp` و`iat` صالحتين، مع السماح بفرق في ساعة النظام يصل إلى 60 ثانية، وأن تكون `sub` و`sid` و`jti` موجودة.
5. يجب أن تكون `act` تساوي `null`. يُوقَّع رمز وصول وكيل الذكاء الاصطناعي بالمفتاح نفسه، ويحمل `typ` و`iss` و`aud` نفسها، ولا تميّزه إلا `act`. ارفضه، ما لم تكن endpoint تقبل الوكلاء؛ ففي هذه الحالة اسمح فقط بما تمنحه `act.scopes`. ترفض `GET /session/` دائمًا رمز الوكيل.

خذ جهة الإصدار من إعداداتك، ولا تأخذها من الرمز أبدًا.

```javascript Node.js theme={null}
// npm install jose
import { createRemoteJWKSet, jwtVerify } from "jose";

const ISSUER = process.env.DIDIT_AUTH_ISSUER; // https://<name>.auth.didit.me
const AUDIENCE = process.env.DIDIT_APPLICATION_ID;
// Cache keys for 5 minutes; refetch an unknown kid at most once a minute.
const jwks = createRemoteJWKSet(new URL(`${ISSUER}/.well-known/jwks.json`), {
  cacheMaxAge: 5 * 60 * 1000,
  cooldownDuration: 60 * 1000,
});

export async function verifyAccessToken(token) {
  const { payload } = await jwtVerify(token, jwks, {
    issuer: ISSUER,
    audience: AUDIENCE,
    algorithms: ["ES256"],
    typ: "at+jwt",
    clockTolerance: 60,
    maxTokenAge: "5m", // also rejects an iat in the future
    requiredClaims: ["sub", "sid", "jti", "exp", "iat"],
  });
  if (payload.act !== null) throw new Error("agent tokens are not accepted");
  return payload; // payload.vendor_data is the person across Didit products
}
```

```python Python theme={null}
# pip install "pyjwt[crypto]"
import os
import threading
import time

import jwt

ISSUER = os.environ["DIDIT_AUTH_ISSUER"]  # https://<name>.auth.didit.me
AUDIENCE = os.environ["DIDIT_APPLICATION_ID"]
jwks = jwt.PyJWKClient(f"{ISSUER}/.well-known/jwks.json", lifespan=300)
refresh_lock = threading.Lock()
last_refresh = 0.0


def signing_key(kid: str | None) -> jwt.PyJWK:
    # PyJWKClient refetches for every unknown kid: limit it to once a minute.
    global last_refresh
    keys = {key.key_id: key for key in jwks.get_signing_keys()}
    if kid not in keys:
        with refresh_lock:
            if time.monotonic() - last_refresh >= 60:
                last_refresh = time.monotonic()
                fresh = jwks.get_signing_keys(refresh=True)
                keys = {key.key_id: key for key in fresh}
    if kid not in keys:
        raise jwt.InvalidTokenError("unknown signing key")
    return keys[kid]


def verify_access_token(token: str) -> dict:
    header = jwt.get_unverified_header(token)
    if header.get("typ") != "at+jwt":
        raise jwt.InvalidTokenError("not an access token")
    claims = jwt.decode(
        token,
        signing_key(header.get("kid")).key,
        algorithms=["ES256"],
        audience=AUDIENCE,
        issuer=ISSUER,
        leeway=60,
        options={"require": ["iss", "aud", "sub", "sid", "jti", "exp", "iat"]},
    )
    if "act" not in claims or claims["act"] is not None:
        raise jwt.InvalidTokenError("agent tokens are not accepted")
    return claims
```

لا يستطيع التحقق دون اتصال رؤية جلسة انتهت قبل لحظات. عندما يكون ذلك مهمًا، مثلًا قبل صرف المدفوعات، استدعِ `GET /session/`.

## التحقق من الجلسة

```bash theme={null}
curl --fail-with-body "${DIDIT_AUTH_ISSUER}/session/" \
  -H "Authorization: Bearer $DIDIT_ACCESS_TOKEN"
```

تحتوي الاستجابة على `sub` و`sid` و`application_id` و`vendor_data` و`amr` و`mfa_verified` و`email` و`email_verified` و`phone` و`verified_identity` و`verified` و`expires_at` و`post_logout_redirect_uri` و`organizations`. يحتوي كل إدخال لمؤسسة على `organization_id` و`organization__name` و`role` و`custom_role`.

هذا الفحص أشد صرامة من التحقق دون اتصال. ويفشل أيضًا عند إلغاء الجلسة، أو خمولها، أو تجاوزها مدة صلاحيتها، أو حظر الشخص، أو عدم تحقق بريده الإلكتروني بعد الآن، أو عدم استيفاء الجلسة لسياسة MFA لديك (مثلًا بعد أن يضيف الشخص تطبيق مصادقة، أو يصبح مسؤول مؤسسة)، أو استبدال الرمز بآخر عبر تحديث. يعيد الفشل `401 {"detail": "invalid_token"}` مع `WWW-Authenticate: Bearer`.

يسمح `GET /session/` بـ 30 طلبًا في الدقيقة و300 طلب في الساعة لكل عنوان IP، وهو حد يشاركه مع `DELETE /session/`. تحقق من الرموز دون اتصال في كل طلب، واحتفظ بهذا الاستدعاء للحظات التي تحتاجه. يُحسب كل استدعاء ناجح نشاطًا، فيعيد ضبط `idle_timeout` الخاص بالجلسة.

البريد الإلكتروني المُتحقَّق منه ليس تحققًا من الهوية. اقرأ `verified` لذلك: هنا تكون القيمة `null` عندما لا يوجد تحقق، بينما تحمل الرموز القيمة `{}`.

## تحديث الرموز

```bash theme={null}
curl --fail-with-body "${DIDIT_AUTH_ISSUER}/token/" \
  -H 'Content-Type: application/json' \
  -d '{"grant_type": "refresh_token", "refresh_token": "YOUR_REFRESH_TOKEN"}'
```

يعيد التحديث رمز وصول جديدًا ورمز هوية جديدًا ورمز تحديث جديدًا، ويتوقف رمز التحديث الذي أرسلته عن العمل.

* رمز التحديث سلسلة عشوائية، وصالح حتى تنتهي الجلسة.
* **إعادة الاستخدام تُنهي الجلسة.** عندما يصل رمز تحديث سبق استخدامه مرة أخرى، تعامله Didit بوصفه علامة على السرقة: فتُلغي الجلسة وتعيد `400 {"error": "invalid_grant"}`. تتوقف رموز الجلسة عن العمل أيضًا، بما فيها الرموز الناتجة عن التحديث السابق: يفشل رمز التحديث الخاص بها، ويفشل رمز الوصول في `GET /session/` فورًا، مع أنه يبقى قابلًا للتحقق دون اتصال حتى انتهاء صلاحيته، في غضون 5 دقائق.
* نفّذ تحديثًا واحدًا في كل مرة لكل جلسة، عبر علامات التبويب والأجهزة ومثيلات الخادم، واستبدل الرموز الثلاثة معًا.
* إذا فشل التحديث بالرمز `400` أو لم يتلقَّ ردًا، فابدأ تسجيل الدخول من جديد. لا تُعِد المحاولة بشكل أعمى: فإعادة المحاولة بعد تحديث نجح تُعد إعادة استخدام.
* يعني `503 {"error": "temporarily_unavailable"}` أن التحديث لم يحدث، وأن رمز التحديث ما زال يعمل. أعد المحاولة مع التراجع التدريجي.
* ترد Didit بـ `429` قبل تشغيل التحديث، لذا أعد المحاولة بعد `Retry-After`. تسمح endpoint الرموز بـ 30 طلبًا في الدقيقة و300 طلب في الساعة لكل عنوان IP. وإضافةً إلى ذلك، تتشارك كل طلبات `POST` و`DELETE` في تطبيقك حصص حماية من البوتات: 60 طلبًا في الدقيقة لكل عنوان IP عبر تلك الـendpoints، و30 طلبًا في الدقيقة لكل جهاز. وعلى مستوى التطبيق كله، للتحديثات حصة قدرها 6,000 في الدقيقة، ولطلبات `POST` و`DELETE` الأخرى (بما في ذلك المتصفحات وخطوات تسجيل الدخول) حصة واحدة قدرها 3,000 في الدقيقة، بحيث لا تستنفد التحديثات عمليات تسجيل الدخول أبدًا.
* مع رموز وصول مدتها 5 دقائق، تكفي 300 عملية تحديث في الساعة تقريبًا لـ25 شخصًا يبقون نشطين. وإذا كان خادمك يجري التحديث لأشخاص كثيرين من عنوان واحد، فحدّث فقط عندما يحتاج طلب إلى رمز جديد، وحدّث في المتصفح أو التطبيق حيثما أمكن.

## تسجيل الخروج

يؤدي طلب `DELETE /session/` باستخدام access token الحالي للشخص إلى إنهاء الجلسة، ويعيد `204`. يتوقف refresh token الخاص بالجلسة عن العمل، وتفشل access tokens الخاصة بها في `GET /session/` فوراً. أما الرموز التي تتحقق منها دون اتصال بالخادم فتبقى صالحة حتى تنتهي مدة صلاحيتها. بعد ذلك، وجّه الشخص إلى `post_logout_redirect_uri` إذا حددته.

يتطلب تسجيل الخروج access token يقبله `GET /session/`، أي رمز لم تنتهِ مدة صلاحيته ولم يُستبدل بعملية تجديد. تعني الاستجابة `401` أن الجلسة لم تنتهِ. جدّد الرمز أولاً، ثم سجّل الخروج باستخدام access token الجديد. إذا فشل التجديد بالاستجابة `400`، فلا يمكن استخدام الجلسة بعد الآن.

تنتهي الجلسة أيضاً في الحالات التالية:

* عند تجاوز `session_lifetime` (الافتراضي 30 يوماً) أو `idle_timeout` (الافتراضي يوم واحد)،
* إذا سجّل الشخص الدخول مرة أخرى وكانت الجلسات المتعددة معطلة،
* إذا أعاد الشخص تعيين كلمة المرور وكان `revoke_on_password_change` مفعّلاً،
* إذا ألغى فريقك الجلسة أو حظر الشخص من وحدة التحكم،
* إذا أُرسل refresh token سبق استخدامه مرة أخرى.

## مدد الصلاحية

| البند | مدة الصلاحية |
| - | - |
| معاملة تسجيل الدخول (`POST /authorize/`) | 10 دقائق |
| رمز التفويض | 60 ثانية، ويُستخدم مرة واحدة فقط |
| Access token | 5 دقائق كحد أقصى |
| ID token | نفس قيمة `exp` الخاصة بـ access token الذي صدر معه |
| Refresh token | حتى انتهاء الجلسة، ويُستخدم مرة واحدة فقط |
| الجلسة | `session_lifetime` و`idle_timeout` |
| التحقق الإضافي | 30 دقيقة |
| ذاكرة التخزين المؤقت للاكتشاف وJWKS | 5 دقائق |

## تخزين بيانات الاعتماد حسب الغرض

* احتفظ بمفاتيح API الخاصة بالتحقق ورموز وحدة التحكم على خادمك.
* احتفظ بـ access tokens الخاصة بالمتصفح في الذاكرة كلما أمكن ذلك. بالنسبة للتطبيقات التي تُعرض من جهة الخادم، استخدم ملفات تعريف الارتباط الآمنة HttpOnly، واحمِ الطلبات المصادَق عليها بملفات تعريف الارتباط من هجمات CSRF.
* خزّن refresh tokens الخاصة بالتطبيقات المحمولة في وحدة التخزين المحمية من قِبل النظام. امسح الحالة المحلية بعد تسجيل الخروج.
* لا تضع الرموز أبداً في عناوين URL أو في السجلات.

تابع مع [دليل البدء السريع لـ JavaScript](https://docs.didit.me/ar/authentication/quickstarts/javascript.md)، أو [دليل البدء السريع لـ Next.js](https://docs.didit.me/ar/authentication/quickstarts/nextjs.md)، أو [مرجع واجهة برمجة تطبيقات المصادقة](https://docs.didit.me/ar/authentication/api-reference/get-openid-configuration.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.