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 PKCEOpenID Connect — رمز التفويض مع PKCE |
|---|---|
| Signingالتوقيع | ES256, published at jwks_uriES256، منشور في 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 إلزامي |
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. لا بيانات كهذه لملئها، والإعلان عن نطاق لا نستطيع الوفاء به ليس إلا دعوةً للاعتماد عليه. وإن احتجت يوماً ادعاءً عن مستخدم، فهو لك تُرفقه، لا لنا نخزّنه.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, transportssub 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مستحيل، بالتصميم | — |
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/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، مع أن البروتوكول لم يتغيّر في شيء. الاكتشاف هو العقد؛ أما المسارات فلا.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 app | no — PKCE insteadلا — PKCE بدلًا منه | signs users inيسجّل دخول المستخدمين |
| your backend | yesنعم | 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 لربطه. ويصل بلا سرّ. فإصدار السرّ فعلٌ مقصود في لوحة التحكّم: يُعرض النصّ الصريح مرّة واحدة ولا يُحفظ إلا كبصمة، فلا شيء يستطيع عرضه عليك ثانيةً. أصدِر سرًّا ثانياً عند التدوير، وأبطِل الأول متى لم يعد شيء يستعمله.
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 هو ما يربط الرمز بالتطبيق الذي بدأ التدفّق، وهو إلزامي على كل عميل.
- your app → browserتطبيقك ← المتصفّح
Redirect to/authorizewith a fresh PKCE verifier andstate.إعادة توجيه إلى/authorizeمع مُتحقِّق PKCE جديد وstate. - on our originعلى نطاقنا
The passkey ceremony runs on our domain. Your app never touches the credential.تجري مراسم مفتاح المرور على نطاقنا. ولا يلمس تطبيقك بيان الاعتماد قط. - browser → your appالمتصفّح ← تطبيقك
We redirect back to your exact registeredredirect_uriwith acode.نُعيد التوجيه إلىredirect_uriالمسجَّل لديك تماماً معcode. - your app → usتطبيقك ← إلينا
Exchange the code at the token endpoint, presenting the verifier.بادِل الرمز عند نقطة الرموز، مقدِّماً المُتحقِّق. - 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.يُتمّ المستخدم مراسم مفتاح المرور على نطاقنا. وليس هذا زخرفاً: مفاتيح المرور مرتبطة بالأصل الذي أُنشئت عليه، فلا يصحّ التوقيع إلا هناك — ورؤية المستخدم لنطاقنا وشهادتنا هي ما يجعل التدفّق عصيّاً على التصيّد. ولا يلمس تطبيقك بيان الاعتماد قط.
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>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 — وخنق ذلك يخنق تكاملاً حقيقياً في أزحم لحظاته، وهي بالضبط اللحظة التي لا يجوز أن يخفق فيها.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 أبداً، إذ لا كلمات مرور هنا.
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 مجرّداً.
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.المستخدم الذي ما زال يحمل مفتاح مرور صالحاً يستطيع إضافة أجهزة بنفسه، وتبقى لها الموضوع نفسه. ولا تنمو القائمة إلا حين يفقد أحدهم كل شيء فتعيد دعوته.
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.فتعامل مع موضوع لم يعد قابلاً للحلّ على أنه مستخدم يحتاج دعوة جديدة، لا على أنه خطأ. وإزالتك لعضويتك حين يغادر أحدهم هي ما يبدأ عدّ الثلاثين يوماً له — إن لم يكن ينتمي لغيرك — وهذا هو السلوك الصادق، ويجدر أن تعرفه قبل أن تُؤتمِته.
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 مزوِّد بيانات اعتماد لخدمات تُدير مستخدميها، لا مكاناً يسجّل فيه الناس أنفسهم.
- 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.بادِل سرّ عميلك برمز قصير العمر. يذهب السرّ إلى نقطة إصدار الرموز ولا شيء غيرها — وما ينتقل في كل طلب بعد ذلك رمزٌ ينتهي من تلقاء نفسه.
Your secret is minted in the console and shown once. Sending it directly toPOST /oauth/token Authorization: Basic base64(client_id:client_secret) grant_type=client_credentials → { "access_token": "…", "expires_in": 3600 }/invitesis not accepted — the token is what authorises there, and it is scoped to clients in your organisation and nothing outside it.يُسكّ سرّك في لوحة التحكم ويُعرَض مرّة واحدة. ولا يُقبل إرساله مباشرةً إلى/invites— فالرمز هو ما يخوّل هناك، وهو مقصور على العملاء داخل مؤسستك ولا شيء خارجها. - Mint an invite, passing your own reference for the user.اسكُك دعوة، ممرِّراً مرجعك الخاص للمستخدم.
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.POST /invites Authorization: Bearer <access token> { "client_id": "your-client", "ref": "42" } → { "url": "https://auth.ali-web-services.com/enrol/9xK2…", "expires_at": "…" }refis 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مُعتِم لدينا ويُعاد كما هو — استعمل مفتاحاً بديلاً، لا بريداً ولا رقم هاتف. لا نريده، ولا ينبغي أن يُنقَل. - 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.سلِّم الرابط عبر قناة تحقّقت منها سلفاً. عامله تماماً كرابط إعادة تعيين كلمة المرور: من يفتحه يصير الموضوع الذي ستثق به بوصفه ذلك المستخدم. وهو لاستعمال واحد وينتهي خلال ٢٤ ساعة.
- 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.غيّرت رأيك؟ اسحبها. الدعوة بيان اعتماد حيّ لإنشاء حساب، فإن ذهبت إلى العنوان الخطأ — أو لم يستخدمها صاحبها — يمكنك استردادها بدل انتظار انتهائها.
An invitation that has already been redeemed is refused withDELETE /invites/9xK2… Authorization: Bearer <access token> → 204 No Content409rather than deleted — it produced a subject, and the record linking the two is what you read back in the next step.الدعوة التي اُستُعمِلت تُرفَض بـ409ولا تُحذَف — فقد أنتجت موضوعاً، والسجلّ الرابط بينهما هو ما تقرأه في الخطوة التالية. - They open it and create a passkey. The ceremony runs on our domain, and the invite is spent the moment it succeeds.يفتحه المستخدم ويُنشئ مفتاح مرور. تجري المراسم على نطاقنا، وتُستهلَك الدعوة لحظة نجاحها.
- 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" } ] } - They sign in normally from then on — the ordinary authorization code flow above, with no invite involved.يسجّل الدخول عادةً بعد ذلك — تدفّق رمز التفويض المعتاد أعلاه، بلا دعوة.
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" } ] }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} حين يغادر أحدهم.
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 رمزاً قصير العمر لاستعمال واحد، فيُسجَّل الجهاز الجديد مقابله، والنتيجة الموضوع نفسه ببيان اعتماد ثانٍ. لا شيء تربطه.
- device Aالجهاز أ
Already signed in — possession is proved. It shows a 60-second, single-use token.مُسجَّل دخوله بالفعل — والحيازة مُثبَتة. يعرض رمزاً لمدة ٦٠ ثانية لاستعمال واحد. - device Bالجهاز ب
Scans and registers its own new key against that token.يمسح ويُسجِّل مفتاحه الجديد مقابل ذلك الرمز. - resultالنتيجة
Samesub, 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.انصح مستخدميك بالاحتفاظ ببيان اعتماد ثانٍ في منظومة مختلفة — مفتاح عتادي إلى جانب مفتاح مرور على الهاتف. فهو الشيء الوحيد الذي يُبعدهم عن مسار الفقد الكامل أدناه، والبديل الحقيقي الوحيد لرموز الاسترداد التي تمتنع عنها هذه الخدمة عمداً.
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.الاسترداد قويٌّ بقدر بيانات الهوية التي تحملها تماماً، وهذا موضعه الصحيح. انصح مستخدميك بتسجيل مفتاح مرور ثانٍ من منظومة مختلفة — مفتاح عتادي إلى جانب هاتف — فلن يحتاجوا هذا المسار البتّة.
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:latestThe 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.مراسم مفتاح المرور. تسجيل الدخول زرٌّ، أو قرارٌ تلقائي. والمُتخطَّى هو إثبات الإنسان للحيازة — لا البروتوكول. |
"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-userThe 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/__configA 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_grantEvery 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 bufferFilter by ref, subject, client_id, path (a prefix) or method.رشِّح بـref، أو subject، أو client_id، أو path (بادئة)، أو method.
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 دائماً. السجلّ حلقيّ، فلا تستنفده مجموعة اختبارات طويلة — لكن مجموعةً أفاضته ثم أكّدت نُودي مرّة على نافذة مبتورة كانت ستنجح على بيانات ناقصة. ولا تُسجَّل الترويسات ولا الأجسام ولا الرموز أبداً.
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 قياسي — بلا حزمة تطوير، ولا شيء احتكاري — فتكاملك قابل للنقل إلى أي مزوِّد. والتحوّل يكلّف تغيير إعداد، لا إعادة كتابة.