Integrating Authدمج Auth

Auth is a passkey-only OpenID Connect provider. If your framework can talk to any OIDC provider, it can talk to this one — there is no SDK, and there is nothing to install.‏Auth مزوِّد OpenID Connect يعتمد مفاتيح المرور حصراً. إن كان إطار عملك يتحدّث إلى أي مزوِّد OIDC، فهو يتحدّث إلى هذا — لا حزمة تطوير، ولا شيء تُثبّته.

Private beta — enrolment is by invitation.نسخة تجريبية خاصة — التسجيل بالدعوة.

Overviewنظرة عامة

What Auth does, and what it deliberately does notما يفعله Auth، وما يمتنع عنه عمداً

Auth lets any service onboard a device, and then guarantees — on every sign-in, and on every action that device approves afterwards — that it is the same device that was onboarded. That guarantee is the whole product.يتيح Auth لأي خدمة أن تُلحِق جهازاً، ثم يضمن — في كل تسجيل دخول، وفي كل إجراء يوافق عليه ذلك الجهاز بعدها — أنه الجهاز نفسه الذي أُلحِق. وهذا الضمان هو المنتج كله.

The mechanism underneath is a passkey: onboarding enrols one, and every request after is a fresh proof that the same key is still in hand. So the question it answers reduces to is this the same key-holder as last time? — and it issues a stable identifier for whoever keeps proving that it is.والآلية تحته مفتاح مرور: يُسجّل الإلحاقُ واحداً، وكل طلب بعده إثباتٌ جديد أن المفتاح نفسه ما زال في اليد. فالسؤال الذي يُجيب عنه يؤول إلى أهذا حاملُ المفتاح نفسه كما في المرّة السابقة؟ — ويُصدر مُعرِّفاً ثابتاً لمن يواصل إثبات أنه هو.

It stores nothing about your users — no email, no phone, no name, no username. Not as a limitation, but because an identity provider that holds no personal data cannot leak any, and cannot be compelled to hand any over. Whatever you know about your users stays yours.لا يخزّن شيئاً عن مستخدميك — لا بريداً، ولا هاتفاً، ولا اسماً، ولا اسم مستخدم. ليس هذا قيداً، بل لأن مزوِّد هوية لا يحمل بيانات شخصية لا يمكن أن يُسرّبها، ولا أن يُجبَر على تسليمها. فما تعرفه عن مستخدميك يبقى لك وحدك.

There are no passwords, no SMS one-time codes and no email magic links. A WebAuthn signature verified against a public key we stored is the only credential this service accepts.لا كلمات مرور، ولا رموز لمرّة واحدة عبر الرسائل النصية، ولا روابط سحرية بالبريد. التوقيع عبر WebAuthn، مُتحقَّقاً منه مقابل مفتاح عام خزّنّاه، هو بيان الاعتماد الوحيد الذي تقبله هذه الخدمة.

ProtocolالبروتوكولOpenID Connect — authorization code with PKCE‏OpenID Connect — رمز التفويض مع PKCE
SigningالتوقيعES256, published at jwks_uri‏ES256، منشور في jwks_uri
Scopesالنطاقاتopenid, offline_access
Claimsالادعاءاتsub, aud, exp, iat, iss, amr, auth_time, nonce
Client authمصادقة العميلNone — every client is public, PKCE is requiredلا شيء — كل عميل عام، وPKCE إلزامي
We do not offer profile, address, phone — or email. There is no such data to fill them, and advertising a scope we cannot honour would only invite you to depend on it. If you ever need a claim about a user, it is yours to attach, not ours to store.لا نوفّر profile أو address أو phone — ولا email. لا بيانات كهذه لملئها، والإعلان عن نطاق لا نستطيع الوفاء به ليس إلا دعوةً للاعتماد عليه. وإن احتجت يوماً ادعاءً عن مستخدم، فهو لك تُرفقه، لا لنا نخزّنه.
Architectureالبنية

How it behaves, in one screenكيف يتصرّف، في شاشة واحدة

You do not need any of this to integrate — discovery tells your library everything it needs. It is here because knowing the shape of the thing you are trusting is reasonable, and because the whole design follows from one idea: hold no personal data, and depend on standards rather than on us.لا تحتاج شيئاً من هذا للتكامل — فوثيقة الاكتشاف تُخبر مكتبتك بكل ما يلزمها. هو هنا لأن معرفة شكل ما تأتمنه أمر معقول، ولأن التصميم كله ينبع من فكرة واحدة: ألّا نحمل بيانات شخصية، وأن نعتمد على المعايير لا علينا.

ScalingالتوسّعScales to zero, and a cold start is fast enough not to be felt on a sign-in path.يتوسّع إلى الصفر، والبدء على البارد أسرع من أن يُلحَظ على مسار تسجيل الدخول.
StateالحالةDurable records are shared, so any instance can finish a flow another one started. The short-lived records of a login — the request, the code, the challenge — live only in memory.السجلّات الدائمة مشتركة، فتستطيع أي نسخة إتمام تدفّق بدأته أخرى. أما سجلّات تسجيل الدخول قصيرة العمر — الطلب والرمز والتحدّي — فتعيش في الذاكرة وحدها.
SurfacesالواجهاتOne process, two surfaces: the standard OIDC endpoints, and the passkey ceremony behind them.عملية واحدة، وواجهتان: نقاط OIDC القياسية، ومراسم مفتاح المرور خلفها.
SigningالتوقيعA single ES256 key, published at jwks_uri. Access tokens are opaque handles, not JWTs.مفتاح ES256 واحد، منشور في jwks_uri. ورموز الوصول مقابض مُعتِمة، لا JWT.
Portabilityقابلية النقلThe whole surface is standard OpenID Connect — no SDK, nothing proprietary — so your integration is portable to any provider.الواجهة كلها OpenID Connect قياسي — بلا حزمة تطوير، ولا شيء احتكاري — فتكاملك قابل للنقل إلى أي مزوِّد.

The identity modelنموذج الهوية

One subject may hold many credentials. Adding a device writes a new credential and leaves the subject untouched — same sub, another key.قد يحمل الموضوع الواحد بيانات اعتماد كثيرة. وإضافة جهاز تكتب بيان اعتماد جديداً وتترك الموضوع كما هو — sub نفسه، ومفتاح آخر.

authUsers/{userID}          ← this IS the sub
authCredentials/{credID}
  userId  ─────────────────→ authUsers/{userID}
  publicKey, counter, label, transports

sub is our primary key; you store it as a foreign key pointing back at us. Your own identifiers — national ID, phone, email — are yours, and we never see them.sub مفتاحنا الأساسي؛ تخزّنه أنت مفتاحاً أجنبياً يشير إلينا. ومُعرِّفاتك أنت — الهوية الوطنية والهاتف والبريد — لك، ولا نراها أبداً.

The one rule everything rests onالقاعدة التي يقوم عليها كل شيء

Possession extends an identity; anything else creates a new one. A relying party can always stop believing a subject, and can never cause a credential to be attached to an existing one — only possession of a key already on that subject can do that. If it were otherwise, our assertion would stop meaning "the user proved possession" and start meaning "the relying party wanted this to look true."الحيازة تُوسِّع هويةً؛ وما عداها يُنشئ هويةً جديدة. يستطيع الطرف المُعتمِد دائماً أن يكفّ عن تصديق موضوع، ولا يستطيع أبداً أن يُلحِق بيان اعتماد بموضوع قائم — لا يفعل ذلك إلا حيازةُ مفتاح موجود أصلاً على ذلك الموضوع. ولو كان غير ذلك، لتوقّف تأكيدنا عن أن يعني «أثبت المستخدم الحيازة»، وصار يعني «أراد الطرف المُعتمِد أن يبدو هذا صحيحاً».

SituationالحالةMechanismالآليةResultالنتيجة
Holds a working credentialيحمل بيان اعتماد صالحاًassertion, then enrolتأكيد، ثم تسجيلsame sub, another credentialsub نفسه، بيان اعتماد آخر
Holds nothingلا يحمل شيئاًyou issue a fresh inviteتُصدر دعوة جديدةnew sub, you link itsub جديد، تربطه أنت
You want to add one for the userتريد إضافة واحد نيابةً عن المستخدمimpossible, by designمستحيل، بالتصميم
Step 1الخطوة ١

Read the discovery documentاقرأ وثيقة الاكتشاف

Point your OIDC library at the issuer and let it discover the rest.وجِّه مكتبة OIDC لديك إلى المُصدِر ودعها تكتشف الباقي.

https://auth.ali-web-services.com/.well-known/openid-configuration
Do not hard-code endpoint paths. Read them from discovery. A relying party that pinned /auth and /token broke when those became /authorize and /oauth/token, even though nothing about the protocol had changed. Discovery is the contract; the paths are not.لا تُضمِّن مسارات النقاط في الشيفرة. اقرأها من الاكتشاف. طرفٌ مُعتمِد ثبّت /auth و/token تعطّل حين صارا /authorize و/oauth/token، مع أن البروتوكول لم يتغيّر في شيء. الاكتشاف هو العقد؛ أما المسارات فلا.
Registrationالتسجيل

Two clients, and only one of them holds a secretعميلان، وواحد منهما فقط يحمل سرًّا

Every environment arrives with two clients — one named Web and one named Backend — because a browser and a backend are different kinds of caller. Treating them as one is the most common integration mistake, and the split is not a preference: it follows from what each one can keep secret. Rename either — the name is a label for your list, and the id it is registered under does not move with it.تصل كل بيئة ومعها عميلان — أحدهما باسم Web والآخر باسم Backend — لأنّ المتصفّح والخادم الخلفي نوعان مختلفان من المستدعين. ومعاملتهما كعميل واحد هي أشيع أخطاء التكامل، والفصل بينهما ليس تفضيلًا: بل ينبع مما يستطيع كلٌّ منهما إخفاءه. وغيّر اسم أيّهما شئت — فالاسم لافتةٌ لقائمتك، ولا يتغيّر معه المُعرِّف المسجَّل.

ClientالعميلSecretسرّWhat it doesوظيفته
your web appno — PKCE insteadلا — PKCE بدلًا منهsigns users inيسجّل دخول المستخدمين
your backendyesنعمissues invitationsيُصدر الدعوات

Your web app is a public client. It ships to every visitor, so anything embedded in it is published — a secret there is not a secret. PKCE does the job instead: your app generates a random verifier, sends only its hash when it starts the flow, and presents the verifier to exchange the code. An attacker who steals the code cannot exchange it, because the verifier never left the browser that started it.تطبيق الويب لديك عميل عام. يُسلَّم إلى كل زائر، فأي شيء مُضمَّن فيه منشور — والسرّ فيه ليس سرًّا. يقوم PKCE بالمهمة بدلًا من ذلك: يولّد تطبيقك مُتحقِّقًا عشوائيًا، ويرسل بصمته فقط عند بدء التدفّق، ثم يقدّم المُتحقِّق لاستبدال الرمز. ولا يستطيع مهاجم سرق الرمز استبداله، لأنّ المُتحقِّق لم يغادر المتصفّح الذي بدأ العملية.

Your backend is a confidential client. It runs on a machine you control, so it genuinely can hold a credential — and it needs one, because issuing an invitation has no browser, no user and no PKCE available to bind it. It arrives without one. Minting a secret is a deliberate act in the console: the plaintext is shown once and kept only as a hash, so nothing can show it to you again. Mint a second to rotate, and revoke the first once nothing uses it.خادمك الخلفي عميل سرّي. يعمل على جهاز تتحكّم به، فيمكنه فعلًا حفظ بيانات اعتماد — وهو يحتاجها، لأنّ إصدار دعوة لا يجري في متصفّح ولا يخصّ مستخدمًا ولا يتوفّر له PKCE لربطه. ويصل بلا سرّ. فإصدار السرّ فعلٌ مقصود في لوحة التحكّم: يُعرض النصّ الصريح مرّة واحدة ولا يُحفظ إلا كبصمة، فلا شيء يستطيع عرضه عليك ثانيةً. أصدِر سرًّا ثانياً عند التدوير، وأبطِل الأول متى لم يعد شيء يستعمله.

Allowed hosts come from your registered redirect URIs, not from a secret. We only ever send an authorization code to a URI you registered, matched exactly — no wildcards, no prefixes, no ports assumed. A loose match is an open redirect, which is the classic way an identity provider is turned against the application it protects. Register every environment's URI explicitly.المضيفات المسموح بها تأتي من عناوين إعادة التوجيه المسجّلة، لا من سرّ. لا نرسل رمز التفويض إلا إلى عنوان سجّلته، بمطابقة تامّة — بلا أحرف بدل ولا بادئات ولا افتراض للمنافذ. المطابقة المتساهلة تعني إعادة توجيه مفتوحة، وهي الطريقة الكلاسيكية لتحويل مزوّد الهوية ضدّ التطبيق الذي يحميه. سجّل عنوان كل بيئة صراحةً.
The two are separate on purpose: the client that issues an invitation is not the client that redeems it. Your backend mints, your web app receives the user. Keeping them apart means a stolen browser bundle cannot mint invitations for anybody.الفصل بينهما مقصود: العميل الذي يُصدر الدعوة ليس العميل الذي يستهلكها. خادمك يُصدر، وتطبيق الويب يستقبل المستخدم. وفصلهما يعني أنّ سرقة حزمة المتصفّح لا تتيح إصدار دعوات لأي أحد.
Step 2الخطوة ٢

Send the user to sign inأرسِل المستخدم لتسجيل الدخول

Authorization code with PKCE, run from the browser. There is no client secret, because a browser cannot keep one — PKCE is what binds the code to the application that started the flow, and it is required of every client.رمز التفويض مع PKCE، يُشغَّل من المتصفّح. لا سرّ للعميل، لأن المتصفّح لا يستطيع حفظ سرّ — وPKCE هو ما يربط الرمز بالتطبيق الذي بدأ التدفّق، وهو إلزامي على كل عميل.

  1. your app → browserتطبيقك ← المتصفّح
    Redirect to /authorize with a fresh PKCE verifier and state.إعادة توجيه إلى /authorize مع مُتحقِّق PKCE جديد وstate.
  2. on our originعلى نطاقنا
    The passkey ceremony runs on our domain. Your app never touches the credential.تجري مراسم مفتاح المرور على نطاقنا. ولا يلمس تطبيقك بيان الاعتماد قط.
  3. browser → your appالمتصفّح ← تطبيقك
    We redirect back to your exact registered redirect_uri with a code.نُعيد التوجيه إلى redirect_uri المسجَّل لديك تماماً مع code.
  4. your app → usتطبيقك ← إلينا
    Exchange the code at the token endpoint, presenting the verifier.بادِل الرمز عند نقطة الرموز، مقدِّماً المُتحقِّق.
  5. your appتطبيقك
    Verify the ID token and start your own session.تحقّق من رمز الهوية وابدأ جلستك أنت.
GET https://auth.ali-web-services.com/authorize
      ?client_id=your-client
      &redirect_uri=https://your-app.example/auth/callback
      &response_type=code
      &scope=openid
      &code_challenge=<S256 of your verifier>
      &code_challenge_method=S256
      &state=<your CSRF token>

The user completes the passkey ceremony on our domain. That is not decoration: passkeys are bound to the origin they were created on, so the signature is only valid there — and the user seeing our domain and certificate is what makes the flow resistant to phishing. Your application never touches the credential.يُتمّ المستخدم مراسم مفتاح المرور على نطاقنا. وليس هذا زخرفاً: مفاتيح المرور مرتبطة بالأصل الذي أُنشئت عليه، فلا يصحّ التوقيع إلا هناك — ورؤية المستخدم لنطاقنا وشهادتنا هي ما يجعل التدفّق عصيّاً على التصيّد. ولا يلمس تطبيقك بيان الاعتماد قط.

That page names your application, not the client. A client is named for its role — Web, Backend — and “Web wants you to sign in” names nothing a person recognises, while the thing they are deciding to trust is your application. So rename the application to change those words; renaming a client only changes your own list.تعرض تلك الصفحة اسم تطبيقك، لا اسم العميل. فالعميل يُسمّى بدوره — Web أو Backend — و«يريد Web أن تسجّل دخولك» لا يدلّ القارئ على شيء، والذي يقرّر ائتمانه هو تطبيقك. فغيّر اسم التطبيق لتغيّر تلك الكلمات؛ أمّا تغيير اسم العميل فلا يغيّر إلا قائمتك أنت.

Then exchange the codeثم بادِل الرمز

POST https://auth.ali-web-services.com/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=<code>
&redirect_uri=https://your-app.example/auth/callback
&client_id=your-client
&code_verifier=<your verifier>
Redirect URIs are matched exactly — no wildcards, no prefixes, no trailing-slash forgiveness. A loose match is an open redirect and a token-theft path, so register every URI you will actually use, including your local development one.تُطابَق عناوين إعادة التوجيه تماماً — بلا أحرف بدل، ولا بادئات، ولا تسامح مع الشرطة المائلة الأخيرة. المطابقة المتساهلة إعادةُ توجيه مفتوحة ومسلكٌ لسرقة الرموز، فسجِّل كل عنوان ستستعمله فعلاً، بما في ذلك عنوان تطويرك المحلي.

The anonymous endpoints — /authorize, the passkey ceremony, enrolment and device-add — are rate limited per source address, and a refused request answers 429 with Retry-After. It is set generously and you will not meet it: the point is to stop one machine flooding /authorize, not to meter anybody. One bucket is shared by everyone behind a NAT, so a limit tight enough to inconvenience a script would also break a busy office.النقاط المجهولة — /authorize، ومراسم المفتاح، والتسجيل، وإضافة الأجهزة — محدودة المعدّل لكل عنوان مصدر، ويُجاب الطلب المرفوض بـ429 وRetry-After. وهي مضبوطة بسخاء ولن تبلغها: المقصود منع جهاز واحد من إغراق /authorize، لا قياس استهلاك أحد. فالدلو الواحد يتقاسمه كل من خلف بوّابة NAT، فحدّ يضيق على برنامج عابث يكسر مكتباً مزدحماً أيضاً.

/oauth/token is not rate limited, and neither are discovery or JWKS. Your backend reaches the token endpoint holding a code it was just given, bound by PKCE — throttling that would choke a real integration at its busiest moment, which is exactly when it must not fail./oauth/token غير محدود المعدّل، وكذلك وثيقة الاكتشاف وJWKS. فخادمك يصل إلى نقطة الرمز وهو يحمل رمزاً توّه أُعطيه، مرتبطاً بـPKCE — وخنق ذلك يخنق تكاملاً حقيقياً في أزحم لحظاته، وهي بالضبط اللحظة التي لا يجوز أن يخفق فيها.
Step 3الخطوة ٣

Verify the ID tokenتحقّق من رمز الهوية

Verify the signature against jwks_uri, then check iss, aud and exp. Any standard library does this for you.تحقّق من التوقيع مقابل jwks_uri، ثم افحص iss وaud وexp. أي مكتبة قياسية تفعل هذا نيابةً عنك.

{
  "iss": "https://auth.ali-web-services.com",
  "sub": "3kJ9xQm2NpLvR7wYzT4a",
  "aud": "your-client",
  "amr": ["user", "mfa", "swk"],
  "auth_time": 1753689600,
  "exp": 1753693200,
  "iat": 1753689600
}

The access token is an opaque handle, not a JWT. What you verify is the ID token; the access token exists so it can be revoked.رمز الوصول مقبض مُعتِم، لا JWT. فالذي تتحقّق منه هو رمز الهوية؛ ورمز الوصول موجود ليمكن إبطاله.

amr — how they authenticatedamr — كيف استوثقوا

amr is derived from what the authenticator actually reported. user is always present (user presence). mfa means a biometric or PIN was performed — which is every real sign-in, since user verification is required. hwk versus swk is a hardware-bound key versus one synced through iCloud Keychain or Google Password Manager, and swk is the common case. Use these for a step-up check — but do not gate on hwk, or the check fails for most real users. pwd will never appear, because there are no passwords here.amr مُشتقّ ممّا أبلغ عنه الموثِّق فعلاً. user حاضر دائماً (حضور المستخدم). وmfa يعني أن سمةً حيوية أو رمز PIN قد جرى — وهو كل تسجيل دخول حقيقي، إذ التحقّق من المستخدم إلزامي. وhwk مقابل swk مفتاحٌ مرتبط بالعتاد مقابل مفتاحٍ مُزامَن عبر سلسلة مفاتيح iCloud أو مدير كلمات مرور Google، وswk هو الحالة الشائعة. استعملها للتحقّق المُعزَّز — لكن لا تشترط hwk، وإلا فشل التحقّق لأكثر المستخدمين الحقيقيين. ولن يظهر pwd أبداً، إذ لا كلمات مرور هنا.

Importantمهم

How to store the subjectكيف تخزّن الموضوع

sub is the identifier for a key-holder. Store it against your own user record — it is the only thing that links your account to a credential.sub هو مُعرِّف حامل المفتاح. خزّنه مع سجلّ المستخدم عندك — فهو الشيء الوحيد الذي يربط حسابك ببيان اعتماد.

Key on (iss, sub), never sub aloneافهرِس على (iss, sub)، لا على sub وحده

Subject identifiers are unique within an issuer, not globally. If you ever accept tokens from a second provider, a bare sub can collide.مُعرِّفات الموضوع فريدة داخل المُصدِر، لا عالمياً. وإن قبلت يوماً رموزاً من مزوِّد ثانٍ، فقد يتصادم sub مجرّداً.

One person may hold more than one subject. This is the part that differs from what most OIDC libraries assume, and nothing will warn you about it.We hold no personal data, so if someone loses every device they own, we have no way to prove the new person is the old one — nobody could. They enrol again and receive a new subject. You are the only party who can decide that both belong to the same customer, because you are the one who knows who they are.قد يحمل الشخص الواحد أكثر من موضوع. هذا هو الجزء الذي يخالف ما تفترضه معظم مكتبات OIDC، ولن يُنبِّهك إليه شيء.لا نحمل بيانات شخصية، فإن فقد أحدهم كل جهاز يملكه، فلا سبيل لنا لإثبات أن الشخص الجديد هو القديم — ولا يستطيع ذلك أحد. فيُسجِّل من جديد ويتلقّى موضوعاً جديداً. وأنت الطرف الوحيد الذي يقرّر أن كليهما لعميل واحد، لأنك أنت من يعرف هويّتهم.

So store subjects as a list, not a single value:فخزّن المواضيع قائمةً، لا قيمةً واحدة:

users/42 {
  auth_subs: ["3kJ9xQm2NpLvR7wYzT4a", "7Bn2mKp9QrXvT3wYzL8c"],
  // …whatever you know about them, which we never see
}

A user who still holds a working passkey can add more devices themselves, and those keep the same subject. The list only grows when someone has lost everything and you re-invite them.المستخدم الذي ما زال يحمل مفتاح مرور صالحاً يستطيع إضافة أجهزة بنفسه، وتبقى لها الموضوع نفسه. ولا تنمو القائمة إلا حين يفقد أحدهم كل شيء فتعيد دعوته.

A subject can stop existing, and you will not be told. Two things end one: the person deletes their own account, which takes effect immediately and cannot be undone; or they stop belonging to every application that invited them, and are deleted thirty days later. Either way the subject simply never appears in a token again.قد ينتهي وجود الموضوع، ولن نُعلمك بذلك. أمران ينهيانه: أن يحذف الشخص حسابه بنفسه، وهو نافذ فوراً ولا رجعة فيه؛ أو أن ينقطع انتماؤه إلى كل تطبيق دعاه، فيُحذف بعد ثلاثين يوماً. وفي الحالتين لا يظهر الموضوع في أي رمز بعدها.

So treat a subject you can no longer resolve as a user who must be re-invited, not as an error. Removing your own membership when somebody leaves is what starts that thirty-day clock for them — if they belong to nobody else — which is the honest behaviour and worth knowing before you automate it.فتعامل مع موضوع لم يعد قابلاً للحلّ على أنه مستخدم يحتاج دعوة جديدة، لا على أنه خطأ. وإزالتك لعضويتك حين يغادر أحدهم هي ما يبدأ عدّ الثلاثين يوماً له — إن لم يكن ينتمي لغيرك — وهذا هو السلوك الصادق، ويجدر أن تعرفه قبل أن تُؤتمِته.

Onboardingالإلحاق

Inviting a userدعوة مستخدم

Enrolment is by invitation, and you issue the invitation. That is what lets you associate a new subject with your user before the credential exists — so there is no account-linking screen for anyone to get wrong. It also means Auth is a credential provider for services that manage their own users, not a place people self-register.التسجيل بالدعوة، وأنت من يُصدر الدعوة. وهذا ما يتيح لك ربط موضوع جديد بمستخدمك قبل وجود بيان الاعتماد — فلا شاشة ربط حسابات يخطئ فيها أحد. ويعني هذا أيضاً أن Auth مزوِّد بيانات اعتماد لخدمات تُدير مستخدميها، لا مكاناً يسجّل فيه الناس أنفسهم.

  1. Exchange your client secret for a short-lived token. The secret goes to the token endpoint and nowhere else — what travels on every call after this is a token that expires on its own.بادِل سرّ عميلك برمز قصير العمر. يذهب السرّ إلى نقطة إصدار الرموز ولا شيء غيرها — وما ينتقل في كل طلب بعد ذلك رمزٌ ينتهي من تلقاء نفسه.
    POST /oauth/token
    Authorization: Basic base64(client_id:client_secret)
    
    grant_type=client_credentials
    
    → { "access_token": "…", "expires_in": 3600 }
    Your secret is minted in the console and shown once. Sending it directly to /invites is not accepted — the token is what authorises there, and it is scoped to clients in your organisation and nothing outside it.يُسكّ سرّك في لوحة التحكم ويُعرَض مرّة واحدة. ولا يُقبل إرساله مباشرةً إلى /invites — فالرمز هو ما يخوّل هناك، وهو مقصور على العملاء داخل مؤسستك ولا شيء خارجها.
  2. Mint an invite, passing your own reference for the user.اسكُك دعوة، ممرِّراً مرجعك الخاص للمستخدم.
    POST /invites
    Authorization: Bearer <access token>
    
    { "client_id": "your-client", "ref": "42" }
    
    → { "url": "https://auth.ali-web-services.com/enrol/9xK2…",
        "expires_at": "…" }
    The secret is your confidential client's own, minted in the console and shown once. It authorises inviting people to clients in your organisation and nothing outside it. ref is opaque to us and handed back unchanged — use a surrogate key, not an email or phone number. We do not want it, and it should not travel.السرّ خاصّ بعميلك السرّي، يُسكّ في لوحة التحكم ويُعرَض مرّة واحدة. يُخوّل دعوة الأشخاص إلى العملاء داخل مؤسستك ولا شيء خارجها. ref مُعتِم لدينا ويُعاد كما هو — استعمل مفتاحاً بديلاً، لا بريداً ولا رقم هاتف. لا نريده، ولا ينبغي أن يُنقَل.
  3. Deliver the link over a channel you have already verified. Treat it exactly like a password-reset link: whoever opens it becomes the subject you will trust as that user. It is single-use and expires in 24 hours.سلِّم الرابط عبر قناة تحقّقت منها سلفاً. عامله تماماً كرابط إعادة تعيين كلمة المرور: من يفتحه يصير الموضوع الذي ستثق به بوصفه ذلك المستخدم. وهو لاستعمال واحد وينتهي خلال ٢٤ ساعة.
  4. Changed your mind? Withdraw it. An invitation is a live credential to create an account, so if it went to the wrong address — or the person never redeemed it — you can take it back rather than waiting out the expiry.غيّرت رأيك؟ اسحبها. الدعوة بيان اعتماد حيّ لإنشاء حساب، فإن ذهبت إلى العنوان الخطأ — أو لم يستخدمها صاحبها — يمكنك استردادها بدل انتظار انتهائها.
    DELETE /invites/9xK2…
    Authorization: Bearer <access token>
    
    → 204 No Content
    An invitation that has already been redeemed is refused with 409 rather than deleted — it produced a subject, and the record linking the two is what you read back in the next step.الدعوة التي اُستُعمِلت تُرفَض بـ409 ولا تُحذَف — فقد أنتجت موضوعاً، والسجلّ الرابط بينهما هو ما تقرأه في الخطوة التالية.
  5. They open it and create a passkey. The ceremony runs on our domain, and the invite is spent the moment it succeeds.يفتحه المستخدم ويُنشئ مفتاح مرور. تجري المراسم على نطاقنا، وتُستهلَك الدعوة لحظة نجاحها.
  6. Read back the subject and store it against your user. Ask by your own reference rather than by the invitation token — the token is a spent one-time credential with a short life, and this answer is durable.اقرأ الموضوع وخزّنه مع مستخدمك. واسأل بمرجعك أنت لا برمز الدعوة — فالرمز اعتماد لمرة واحدة استُهلك وعمره قصير، أما هذا الجواب فدائم.
    GET /memberships?client=kafani-web&ref=42
    Authorization: Bearer <access token>
    
    → { "memberships": [
          { "subject": "3kJ9xQm2NpLvR7wYzT4a", "ref": "42",
            "application_id": "app-7f3a", "created_at": "2026-08-06T09:00:00Z" } ] }
  7. They sign in normally from then on — the ordinary authorization code flow above, with no invite involved.يسجّل الدخول عادةً بعد ذلك — تدفّق رمز التفويض المعتاد أعلاه، بلا دعوة.
An invite is pinned to the target client and is a bearer credential: whoever opens the link becomes a subject you will treat as your user, so give it a short life and deliver it only over a channel you have already verified.الدعوة مثبَّتة على العميل المستهدَف وهي بيان اعتماد لحامله: من يفتح الرابط يصير موضوعاً ستعامله كمستخدمك، فامنحها عمراً قصيراً وسلِّمها فقط عبر قناة تحقّقت منها سلفاً.

Seeing what is outstandingالاطّلاع على ما هو معلَّق

List what you have minted, so you can reconcile against your own user records — who was invited and never redeemed, who is already in. Filter with &status=pending.اعرض ما أصدرته، لتطابقه مع سجلّات مستخدميك — من دُعي ولم يستخدم دعوته، ومن دخل فعلاً. ورشِّح بـ&status=pending.

GET /invites?client=kafani-web&status=pending
Authorization: Bearer <access token>

→ { "invites": [
      { "ref": "42", "client_id": "kafani-web", "status": "pending",
        "created_at": "2026-08-06T09:00:00Z",
        "expires_at": "2026-08-07T09:00:00Z" } ] }
The listing does not return invitation tokens, and never will. A token is a bearer credential: anyone holding one can redeem it and be treated as the person it was minted for. So cancel by your own reference instead — DELETE /invites?client=kafani-web&ref=42, which withdraws whatever is still pending for that person and answers with how many it took.القائمة لا تُعيد رموز الدعوات، ولن تفعل. فالرمز بيان اعتماد لحامله: من يملكه يستطيع استخدامه ويُعامَل كالشخص الذي صدرت له الدعوة. لذا ألغِ بمرجعك أنت بدلاً من ذلك — DELETE /invites?client=kafani-web&ref=42، فيسحب ما بقي معلَّقاً لذلك الشخص ويردّ بعددها.

Who belongs to your applicationمن ينتمي إلى تطبيقك

A redeemed invitation becomes a durable membership: one record per person per application, carrying your own reference and the date it began. That is what GET /memberships above reads, and it is how you ask “who do I have?” without keeping a list of spent tokens. Drop one with DELETE /memberships/{subject}/{client} when somebody leaves.الدعوة المستخدَمة تصير عضويةً دائمة: سجلّ واحد لكل شخص في كل تطبيق، يحمل مرجعك أنت وتاريخ بدء العلاقة. وهذا ما يقرأه GET /memberships أعلاه، وبه تسأل «من لديّ؟» دون الاحتفاظ بقائمة رموز مستهلَكة. واحذف عضوية بـDELETE /memberships/{subject}/{client} حين يغادر أحدهم.

Removing a membership does not delete the person. A subject is shared across every application that invited them, so removing yours says “no longer ours” and nothing more — they keep their passkeys and stay signed in everywhere else.إزالة العضوية لا تحذف الشخص. فالموضوع مشترك بين كل التطبيقات التي دعته، وإزالة عضويتك تقول «لم يعد لدينا» ولا شيء أكثر — إذ يحتفظ بمفاتيح مروره ويبقى مسجّلاً في كل مكان آخر.
Enrolment and sign-in are deliberately separate requests. Folding them together — having the invite complete an authorization request so the user lands in your app signed in — only works when the link is opened in the same browser that started the flow. An emailed link opened on a phone has no PKCE verifier in it, and the code could not be exchanged. Rather than have a flow that works or does not depending on how the link travelled, enrolment always ends the same way and sign-in is its own step.التسجيل وتسجيل الدخول طلبان منفصلان عمداً. ودمجهما — بأن تُتِمّ الدعوة طلب تفويض فيصل المستخدم إلى تطبيقك وقد سجّل دخوله — لا ينجح إلا حين يُفتح الرابط في المتصفّح نفسه الذي بدأ التدفّق. ورابطٌ يُرسَل بالبريد ويُفتح على هاتف لا مُتحقِّق PKCE فيه، فلا يمكن مبادلة الرمز. وبدل تدفّق ينجح أو يخفق تبعاً لكيفية انتقال الرابط، ينتهي التسجيل دائماً بالطريقة نفسها ويكون تسجيل الدخول خطوته المستقلة.
Second deviceجهاز ثانٍ

Adding a device when you already have oneإضافة جهاز حين تملك واحداً بالفعل

No invite, no involvement from you, no protocol on your side. A device that is already signed in has proved possession, so it can authorise another one directly — the auth app shows a short-lived, single-use code, the new device registers against it, and the result is the same subject with a second credential. Nothing to link.لا دعوة، ولا تدخّل منك، ولا بروتوكول من جهتك. الجهاز المُسجَّل دخوله بالفعل قد أثبت الحيازة، فيستطيع تفويض جهاز آخر مباشرةً — يعرض تطبيق Auth رمزاً قصير العمر لاستعمال واحد، فيُسجَّل الجهاز الجديد مقابله، والنتيجة الموضوع نفسه ببيان اعتماد ثانٍ. لا شيء تربطه.

  1. device Aالجهاز أ
    Already signed in — possession is proved. It shows a 60-second, single-use token.مُسجَّل دخوله بالفعل — والحيازة مُثبَتة. يعرض رمزاً لمدة ٦٠ ثانية لاستعمال واحد.
  2. device Bالجهاز ب
    Scans and registers its own new key against that token.يمسح ويُسجِّل مفتاحه الجديد مقابل ذلك الرمز.
  3. resultالنتيجة
    Same sub, second credential. Your records do not change.sub نفسه، بيان اعتماد ثانٍ. ولا تتغيّر سجلّاتك.

Tell your users to keep a second credential in a different ecosystem — a hardware key alongside a phone passkey. It is the one thing that keeps them out of the total-loss path below, and the only real replacement for the recovery codes this service deliberately does not have.انصح مستخدميك بالاحتفاظ ببيان اعتماد ثانٍ في منظومة مختلفة — مفتاح عتادي إلى جانب مفتاح مرور على الهاتف. فهو الشيء الوحيد الذي يُبعدهم عن مسار الفقد الكامل أدناه، والبديل الحقيقي الوحيد لرموز الاسترداد التي تمتنع عنها هذه الخدمة عمداً.

Recoveryالاسترداد

Losing a device is your process, not oursفقدان جهاز شأنُك أنت، لا شأننا

We cannot recover an account, and we will not pretend otherwise. Recovery means proving a human is the same human, and we hold nothing to check against. You hold the identity data, so recovery belongs with you.لا نستطيع استرداد حساب، ولن ندّعي غير ذلك. الاسترداد يعني إثبات أن إنساناً هو الإنسان نفسه، ولا نحمل شيئاً نقابله به. أنت من يحمل بيانات الهوية، فالاسترداد شأنُك.

In practice it is the same operation as onboarding:وهو عملياً العملية نفسها كالإلحاق:

  • They still have a device. They add another themselves. Same subject, nothing for you to do.ما زال لديه جهاز. يُضيف آخر بنفسه. الموضوع نفسه، ولا شيء عليك فعله.
  • They have lost everything. Verify them however you already do — your own OTP, a national ID check, a support call — then issue a fresh invite. A new subject arrives; add it to the list.فقد كل شيء. تحقّق منه بما تفعله أصلاً — رمزك لمرة واحدة، أو فحص الهوية الوطنية، أو اتصال دعم — ثم أصدر دعوة جديدة. يصل موضوع جديد؛ أضِفه إلى القائمة.

Recovery is exactly as strong as the identity data you hold, which is where it should be. Tell your users to enrol a second passkey from a different ecosystem — a hardware key alongside a phone — and they will never need this path at all.الاسترداد قويٌّ بقدر بيانات الهوية التي تحملها تماماً، وهذا موضعه الصحيح. انصح مستخدميك بتسجيل مفتاح مرور ثانٍ من منظومة مختلفة — مفتاح عتادي إلى جانب هاتف — فلن يحتاجوا هذا المسار البتّة.

Stub serverخادم بديل

Test against a public stub, not the live providerاختبر مقابل خادم بديل عام، لا المزوِّد الحيّ

Build and test your integration against a public stub — a stand-in that speaks the same contract as the real provider, so your suite never depends on the live service being up. It is the same reason nobody puts real S3 in an end-to-end test.ابنِ تكاملك واختبره مقابل خادم بديل عام — بديلٌ يتحدّث العقد نفسه كالمزوِّد الحيّ، فلا تعتمد مجموعتك الاختبارية على بقاء الخدمة الحيّة عاملةً. وهو السبب ذاته الذي يمنع وضع S3 حقيقي في اختبار طرفيّ.

docker run --rm -p 8091:8091 \
  -e STUB_ISSUER=http://localhost:8091 \
  -e STUB_CLIENTS='[{"client_id":"web","redirect_uris":["http://localhost:5173/auth/callback"]}]' \
  europe-west1-docker.pkg.dev/ali-web-services/stub-servers/auth:latest

The image is public and needs no credentials — linux/amd64 and linux/arm64.الصورة عامة ولا تحتاج بيانات اعتماد — linux/amd64 وlinux/arm64.

RealحقيقيDiscovery, JWKS, exact redirect-URI matching, mandatory PKCE with genuine S256, single-use codes, refresh-token rotation, ES256 signatures. Your OIDC library is exercised for real — a stub that shortcut any of that would pass while your real integration failed.الاكتشاف، وJWKS، والمطابقة التامة لعنوان إعادة التوجيه، وPKCE الإلزامي بتحقّق S256 حقيقي، والرموز أحادية الاستعمال، وتدوير رمز التحديث، وتوقيعات ES256. تُمرَّن مكتبة OIDC لديك تمريناً حقيقياً — فبديلٌ يختصر أياً من ذلك كان لينجح بينما يفشل تكاملك الحقيقي.
Not realغير حقيقيThe passkey ceremony. Sign-in is a button, or an automatic decision. What is skipped is the human proving possession — not the protocol.مراسم مفتاح المرور. تسجيل الدخول زرٌّ، أو قرارٌ تلقائي. والمُتخطَّى هو إثبات الإنسان للحيازة — لا البروتوكول.
A stub can never be mistaken for the real provider. Its signing key is derived from a phrase published in the open, so it can never verify against real JWKS; every token it mints carries "stub": true, so asserting that claim is absent is a cheap check for a stale environment variable in production; and it refuses to boot on a publicly routable issuer.لا يمكن أن يُشتبَه بالبديل على أنه المزوِّد الحقيقي. مفتاح توقيعه مُشتقّ من عبارة منشورة علانيةً، فلا يتحقّق أبداً مقابل JWKS الحقيقي؛ وكل رمز يسكّه يحمل "stub": true، فالتأكّد من غياب هذا الادعاء فحصٌ رخيص لمتغيّر بيئة قديم في الإنتاج؛ وهو يرفض الإقلاع على مُصدِرٍ قابل للتوجيه العام.

Getting a signed-in sessionالحصول على جلسة مسجَّلة الدخول

Enrolment is invite-only, so the stub starts with nobody enrolled and every sign-in says so until somebody is. Mint an invitation with the admin token — stub-admin-token unless you changed it:التسجيل بدعوة فقط، لذا يبدأ البديل ولا أحد مسجَّل، وكل محاولة دخول تقول ذلك حتى يُسجَّل أحد. أنشئ دعوة باستخدام رمز الإدارة — stub-admin-token ما لم تُغيِّره:

curl -X POST http://localhost:8091/invites \
  -H 'Authorization: Bearer stub-admin-token' \
  -H 'Content-Type: application/json' \
  -d '{"client_id":"web","ref":"user-42"}'

Or skip the ceremony entirely when a suite only needs a session to test something else. STUB_ENROLLED='[{"sub":"e2e-user","ref":"42"}]' starts with that person already enrolled.أو تجاوز المراسم تماماً حين لا تحتاج مجموعة الاختبارات سوى جلسة لاختبار شيء آخر. يبدأ STUB_ENROLLED='[{"sub":"e2e-user","ref":"42"}]' وذلك الشخص مسجَّل بالفعل.

With more than one person enrolled, say who signs in. Pass login_hint on the authorization request and the stub signs in as that subject, so a test can seed data for an id it chose and then become it — rather than signing in first and discovering who it became.حين يكون أكثر من شخص مسجَّلاً، حدِّد من يسجّل الدخول. مرِّر login_hint في طلب التفويض فيسجّل البديل الدخول بذلك الشخص، ليتمكّن الاختبار من تهيئة بيانات لمعرّف اختاره ثم يصبح إياه — بدلاً من تسجيل الدخول أولاً ثم اكتشاف من صار.

GET /authorize?client_id=e2e&response_type=code&code_challenge=...&login_hint=e2e-user
Without it, the stub signs in as whoever sorts first. Enrolling a second person then changes who your suite is, and the flow still succeeds — as somebody else. An unknown hint is ignored rather than refused, because it is a suggestion in the protocol and the real provider treats it as one.بدونه يسجّل البديل الدخول بأول شخص ترتيباً. وعندئذٍ يغيّر تسجيل شخص ثانٍ هويةَ مجموعة اختباراتك، وينجح المسار — لكن بشخص آخر. ويُتجاهل التلميح غير المعروف بدل رفضه، لأنه اقتراح في البروتوكول ويتعامل معه المزوّد الحقيقي كذلك.
Keep driving the real flow if you are testing sign-in itself. Minting, opening the enrolment URL and completing the ceremony is what catches redirect-URI and callback mistakes. Pre-seeding is for the suites that only need a session — if it becomes your only path, the stub stops catching the class of bug it exists for.واصل تشغيل المسار الحقيقي إن كنت تختبر تسجيل الدخول نفسه. إنشاء الدعوة وفتح رابط التسجيل وإتمام المراسم هو ما يكشف أخطاء عنوان إعادة التوجيه والاستدعاء. التهيئة المسبقة لمجموعات الاختبار التي تحتاج جلسة فقط — وإن صارت مسارك الوحيد، توقّف البديل عن كشف صنف الأخطاء الذي وُجد من أجله.

The stub describes itselfالبديل يصف نفسه

Every option, its default and the exact JSON shapes come from the running binary rather than from a page that can drift from it. --help prints the schema, and GET /__config — also served at / — reports what this stub is actually configured with: the admin token, which clients are confidential, and who is enrolled.كل خيار وقيمته الافتراضية وأشكال JSON الدقيقة تأتي من الملف التنفيذي العامل لا من صفحة قد تنحرف عنه. يطبع --help المخطَّط، ويُبلِّغ GET /__config — ويُقدَّم أيضاً على / — بما هو مضبوط فعلاً: رمز الإدارة، وأي العملاء سرّي، ومن المسجَّل.

docker run --rm europe-west1-docker.pkg.dev/ali-web-services/stub-servers/auth:latest --help

curl -s http://localhost:8091/__config

A client with a client_secret is confidential and may use client_credentials; one without is public. There is no separate flag, and an unknown field is refused at start-up naming the real one rather than parsing silently and doing nothing.العميل الذي يحمل client_secret هو عميل سرّي ويمكنه استخدام client_credentials؛ ومن دونه يكون عاماً. لا توجد راية منفصلة، والحقل غير المعروف يُرفض عند الإقلاع مع تسمية الحقل الصحيح، بدلاً من تحليله بصمت دون أي أثر.

Forcing failuresافتعال الأعطال

Faults are selected by the identifier you already control — the ref on an invitation, or the subject it became — so there is no shared state, and parallel test workers cannot arm each other's faults.تُنتقى الأعطال بالمُعرِّف الذي تملكه أصلاً — ref على الدعوة، أو الموضوع الذي صارته — فلا حالة مشتركة، ولا يستطيع عمّال الاختبار المتوازون تسليح أعطال بعضهم بعضاً.

STUB_BEHAVIOURS: >-
  ref:poison-500=http:500
  ref:poison-slow=delay:30s
  ref:auto-ok=signin:approve
  ref:auto-deny=signin:reject
  ref:used-up=invite:spent
  ref:broken-setup=mint:503
  sub:ghost-*=token:invalid_grant

Every fault — an HTTP status, a delay, an expired or spent invite, a refused sign-in, a token error, a revoked refresh, a malformed body — is armed the same way, and the running stub can list its own matrix.كل عطل — حالة HTTP، أو تأخير، أو دعوة منتهية أو مستهلَكة، أو رفض تسجيل الدخول، أو خطأ رمز، أو تحديث مُبطَل، أو جسم مشوَّه — يُسلَّح بالطريقة نفسها، والبديل العامل يستطيع سرد مصفوفته بنفسه.

Ask it what was calledاسأله عمّا نُودي

The stub keeps a log of the requests it answered, so a test can assert that the flow it thinks it drove is the flow that happened — not merely that the end state looks right.يحتفظ البديل بسجلٍّ للطلبات التي أجاب عنها، فيستطيع الاختبار أن يؤكّد أن المسار الذي يظنّ أنه ساقه هو المسار الذي جرى فعلاً — لا أن الحالة النهائية تبدو صحيحة فحسب.

const { count, dropped } = await fetch(
  'http://localhost:8091/_stub/calls?ref=user-42&path=/oauth/token'
).then(r => r.json())

expect(count).toBe(1)     // called exactly once
expect(dropped).toBe(0)   // and nothing fell out of the buffer

Filter by ref, subject, client_id, path (a prefix) or method.رشِّح بـref، أو subject، أو client_id، أو path (بادئة)، أو method.

Filter by identity, or you are reading everyone's traffic. The log is shared, so an unfiltered query returns every parallel worker's requests. It still answers — that is what you want when debugging one test by hand — and marks itself unfiltered: true so an assertion built on it is not quietly wrong. Same reasoning as arming behaviours by identifier rather than globally.رشِّح بالهويّة، وإلا فأنت تقرأ حركة الجميع. السجلّ مشترك، فالاستعلام غير المرشَّح يُرجع طلبات كل عامل متوازٍ. وهو يجيب رغم ذلك — وهذا ما تريده عند تتبّع اختبار واحد يدوياً — ويُعلِّم نفسه بـunfiltered: true كي لا يكون التأكيد المبنيّ عليه خاطئاً في صمت. وهو المنطق نفسه في تسليح السلوكيات بمعرِّف بدل تسليحها عامّةً.

dropped is always reported. The log is a ring, so a long suite cannot exhaust it — but a suite that overflowed it and then asserted called once against a truncated window would pass on missing data. Headers, bodies and tokens are never recorded.يُبلَّغ عن dropped دائماً. السجلّ حلقيّ، فلا تستنفده مجموعة اختبارات طويلة — لكن مجموعةً أفاضته ثم أكّدت نُودي مرّة على نافذة مبتورة كانت ستنجح على بيانات ناقصة. ولا تُسجَّل الترويسات ولا الأجسام ولا الرموز أبداً.

Guaranteesالضمانات

What we promise, and what we cannot doما نعِد به، وما لا نستطيعه

  • We cannot enrol a credential on your behalf. Only possession of an existing passkey can add another to a subject. If we could do it, our assertion would stop meaning that the user proved anything.لا نستطيع تسجيل بيان اعتماد نيابةً عنك. لا يُضيف مفتاحاً آخر إلى موضوع إلا حيازةُ مفتاح مرور قائم. ولو أمكننا ذلك، لتوقّف تأكيدنا عن أن يعني أن المستخدم أثبت شيئاً.
  • You can always stop trusting a subject. Remove it from your records — no cooperation from us required, effective immediately.تستطيع دائماً أن تكفّ عن الوثوق بموضوع. احذفه من سجلّاتك — بلا حاجة إلى تعاون منّا، ونافذاً فوراً.
  • We never see your users' data. There is no field to put it in.لا نرى بيانات مستخدميك أبداً. لا حقل نضعها فيه.
  • No lock-in. The whole surface is standard OpenID Connect — no SDK, nothing proprietary — so your integration is portable to any provider. Switching costs a configuration change, not a rewrite.لا احتجاز. الواجهة كلها OpenID Connect قياسي — بلا حزمة تطوير، ولا شيء احتكاري — فتكاملك قابل للنقل إلى أي مزوِّد. والتحوّل يكلّف تغيير إعداد، لا إعادة كتابة.