سلام و عرض ادب خدمت دوستان. در این بخش به بررسی موضوع PKCE و RFC 7636 خواهیم پرداخت تا ببینیم این موضوع در SSO هایی که با OAuth 2.0 و OIDC کار میکنند، چگونه است. با توجه به کثرت موضوعات و اینکه میخوام موضوع رو طوری بیان کنم که اون عزیزانی که اطلاعات اولیه هم ندارند رو پوشش بدهد، پس جریان رو به صورت زیر بخش بندی میکنم:
بخش اول: مقدمه و ضرورت وجودی
بخش دوم: کالبدشکافی مکانیسم PKCE و فرآیند تبادل
بخش سوم: آناتومی سناریوهای حمله و تهدیدات پیش رو
بخش چهارم: چالشهای پیادهسازی در معماریهای توزیعشده
بخش پنجم: بررسی PKCE در وباپلیکیشنها و جایگاه آن در معماری مدرن وب
بخش ششم: پیادهسازی PKCE در اکوسیستمهای مدیریت هویت: اختصاصاً Keycloak
بخش هفتم: دستورالعمل اجرایی برای تیمهای مهندسی؛ هماهنگی میان کلاینت و سرور
در دنیای توسعه نرمافزار مدرن، جایی که مرزهای بین کلاینتهای قابل اعتماد و غیرقابل اعتماد بهطور فزایندهای محو شدهاند، پروتکل OAuth 2.0 به عنوان ستون فقرات تبادل دسترسیها شناخته میشود. با این حال، با ظهور اپلیکیشنهای موبایل (Native Apps) و اپلیکیشنهای تکصفحهای (SPA)، حفرههای امنیتی نوینی پدیدار شدند که در طراحی اولیه این پروتکل پیشبینی نشده بودند. مشکل اصلی در “کلاینتهای عمومی” (Public Clients) نهفته است؛ کلاینتهایی که قادر به نگهداری امن یک “Secret” نیستند، زیرا کد منبع آنها در اختیار کاربر نهایی قرار دارد. RFC 7636، که با نام PKCE (Proof Key for Code Exchange) شناخته میشود، نه یک وصله مقطعی، بلکه یک تغییر پارادایم در تأیید هویت است که تلاش میکند نقطه ضعف کلاسیکِ “رهگیری کد مجوز” را به طور کامل پوشش دهد. این مقاله به واکاوی ریاضیاتی و معماری این مکانیسم میپردازد تا روشن سازد چگونه یک لایه محافظتی مبتنی بر رمزنگاری، میتواند امنیت تبادلات در محیطهای به ظاهر ناامن را تضمین کند.
تکنیک PKCE با معرفی یک جفت پارامتر پویا به نامهای Code Verifier و Code Challenge، فرآیند سنتی صدور توکن را به یک پیوند رمزنگاریشده دوطرفه تبدیل میکند. برخلاف روشهای سنتی که صرفاً به اعتبار “Client ID” تکیه داشتند، در این متد، کلاینت پیش از آغاز فرآیند احراز هویت، یک رشته تصادفی با آنتروپی بالا تولید کرده و هشِ آن را به عنوان چالش به سمت سرور میفرستد. زمانی که سرور کد مجوز را صادر میکند و کلاینت در مرحله نهایی قصد تبادل آن با Access Token را دارد، باید خودِ رشته اصلی (Verifier) را ارائه دهد. سرور با هش کردن این رشته و مقایسه آن با چالشی که در مرحله اول دریافت کرده بود، اطمینان حاصل میکند که درخواستکننده توکن، همان نهادی است که فرآیند احراز هویت را آغاز کرده است. این فرآیند، عملاً حملات تزریق کد که در آن مهاجم سعی میکند کد مجوزِ متعلق به یک نشست دیگر را برباید، خنثی میسازد، زیرا مهاجم دسترسی به Verifier اصلی کلاینت را ندارد.

بزرگترین تهدید امنیتی که PKCE برای مقابله با آن طراحی شده است، حمله “Authorization Code Interception” است. در سیستمعاملهای موبایل، کلاینتها برای دریافت پاسخ از سرور OAuth، از URIهای سفارشی استفاده میکنند. مهاجمان میتوانند با ثبت یک URI مشابه در سیستمعامل، کدهای مجوزی که برای اپلیکیشن مقصد ارسال میشود را رهگیری کنند. بدون PKCE، این کد برای دریافت توکن کافی است و مهاجم میتواند به سادگی به حساب کاربری قربانی دسترسی یابد. اما با پیادهسازی این استاندارد، کدی که مهاجم سرقت کرده است، به دلیل عدم انطباق با مقدار Verifier که فقط در اختیار اپلیکیشن اصلی است، برای سرور احراز هویت بیاعتبار خواهد بود. علاوه بر این، بررسی دقیقتر نشان میدهد که PKCE در برابر حملات جعل نشست (Session Hijacking) نیز تا حد قابل قبولی مقاوم است، چرا که پیوند بین مرحله اول (درخواست کد) و مرحله دوم (درخواست توکن) از نظر زمانی و منطقی قفل شده است.
پیادهسازی موفقیتآمیز PKCE مستلزم هماهنگی دقیق بین لایه کلاینت و سرورِ صادرکننده توکن (Authorization Server) است. توسعهدهندگان باید توجه داشته باشند که استفاده از الگوریتمهای هشینگ مانند SHA-256 در حال حاضر استاندارد طلایی محسوب میشود و متدهای ضعیفتر باید کنار گذاشته شوند. علاوه بر آن، مدیریت وضعیت (State Management) در سمت کلاینت برای ذخیره موقت Code Verifier تا زمان بازگشت از محیط احراز هویت، یکی از نقاط حساس است که اگر به درستی مدیریت نشود، میتواند منجر به خطاهای غیرمنتظره در تجربه کاربری شود. در سیستمهای بزرگمقیاس، این موضوع نه تنها یک چالش امنیتی، بلکه یک ضرورت در معماری سیستمهای Zero-Trust است که باید در تمامی اپلیکیشنهای موبایل و کلاینتهای غیرقابل اعتماد پیادهسازی شود.
اگرچه PKCE در ابتدا با هدف حل مسئله امنیتی کلاینتهای عمومی، بهویژه اپلیکیشنهای موبایل و دسکتاپ طراحی شد، اما در سالهای اخیر اهمیت آن در وباپلیکیشنها نیز بهشدت افزایش یافته است. دلیل این تغییر، تحول بنیادین در معماری وب است. وباپلیکیشنهای مدرن دیگر صرفاً صفحات ایستای سمت سرور نیستند؛ بلکه اغلب به صورت SPA، PWA یا ترکیبی از رندر سمت سرور و client-side rendering پیادهسازی میشوند و همین موضوع باعث میشود مرز میان client confidential و public بهصورت کلاسیک دیگر چندان روشن نباشد. در چنین فضایی، PKCE نه فقط یک سازوکار تکمیلی، بلکه در بسیاری از سناریوها یک لایه حفاظتی ضروری برای جلوگیری از سوءاستفاده از authorization code محسوب میشود.
در معماری سنتی وب، زمانی که اپلیکیشن در سمت سرور اجرا میشد و secret میتوانست بهصورت امن در محیط backend نگهداری شود، فرآیند OAuth 2.0 معمولاً با یک client secret همراه بود و این secret برای اثبات هویت کلاینت کافی تلقی میشد. اما در وباپلیکیشنهای مدرن، بهویژه آنهایی که بخش عمده منطق خود را در مرورگر اجرا میکنند، این فرض دیگر معتبر نیست. هر چیزی که در browser bundle قرار بگیرد، عملاً در معرض مشاهده، استخراج یا مهندسی معکوس قرار دارد. بنابراین در بسیاری از وباپلیکیشنها، حتی اگر ظاهراً با یک کلاینت وب طرف باشیم، از دید امنیتی با یک public client مواجه هستیم. PKCE دقیقاً در همین نقطه معنا پیدا میکند، زیرا به جای تکیه بر secret ثابت و قابل افشا، از یک وابستگی موقت، تصادفی و رمزنگاریشده برای اتصال مرحله آغازین و مرحله تبادل توکن استفاده میکند.
در وباپلیکیشنها، استفاده از PKCE بهویژه زمانی اهمیت مییابد که کاربر از طریق مرورگر به یک Authorization Server هدایت میشود و پس از ورود، با یک authorization code به اپلیکیشن بازمیگردد. در یک سناریوی ساده، مهاجم میتواند تلاش کند از طریق دستکاری redirect URI، تزریق نشست، یا ربودن response code، کنترل فرآیند تبادل را به دست بگیرد. در نبود PKCE، اگر تنها authorization code برای گرفتن access token کافی باشد، هر موجودیتی که بتواند آن code را به دست آورد، بالقوه میتواند توکن را نیز دریافت کند. اما PKCE این زنجیره را میشکند، زیرا authorization code بهتنهایی بیارزش میشود مگر آنکه code verifier متناظر نیز ارائه شود. در نتیجه، حتی اگر مهاجم بتواند پاسخ احراز هویت را رهگیری کند، بدون داشتن verifier نمیتواند مرحله نهایی را کامل کند.
از منظر طراحی سیستم، PKCE در وباپلیکیشنها بهویژه در سه الگوی رایج نقش حیاتی دارد. نخست، در SPAها که تمام یا بخش اعظم منطق در مرورگر اجرا میشود، استفاده از client secret عملاً بیمعناست و PKCE تبدیل به مکانیزم اصلی حفاظت از exchange flow میشود. دوم، در اپلیکیشنهایی که از backend-for-frontend یا BFF architecture استفاده میکنند، PKCE میتواند بین مرورگر و backend نقش یک لایه امنیتی موقت را بازی کند و exchange code را بهصورت ایمنتری به سرور قابل اعتماد منتقل نماید. سوم، در PWAها و معماریهای ترکیبی، PKCE به اپلیکیشن کمک میکند تا حتی در حالی که وابسته به مرورگر است، سطحی از اطمینان را در اتصال authorization request و token request حفظ کند. در هر سه حالت، نکته اصلی این است که PKCE جایگزین کامل امنیت لایههای دیگر نیست، اما شکافهای بنیادین OAuth در کلاینتهای غیرقابل اعتماد را بهخوبی میبندد.
البته باید توجه داشت که در وباپلیکیشنها، PKCE بهتنهایی تضمینکننده امنیت کامل نیست. اگر redirect URIها بهدرستی محدود نشده باشند، اگر state parameter نادیده گرفته شود، اگر cross-site scripting در برنامه وجود داشته باشد، یا اگر توکنها در storageهای ناامن مانند localStorage ذخیره شوند، آنگاه PKCE تنها یکی از اجزای دفاعی خواهد بود و نه همه آن. از این منظر، PKCE باید در کنار اصولی مانند use of HTTPS، کنترل دقیق redirect endpointها، مدیریت صحیح session، و طراحی مناسب token lifecycle دیده شود. مهندسی امنیت در وب، هیچگاه با یک مکانیزم واحد تکمیل نمیشود؛ PKCE فقط یکی از اجزای ضروری این پازل است.
در نهایت، میتوان گفت PKCE در وباپلیکیشنها بیش از آنکه یک انتخاب اختیاری باشد، به یک استاندارد عملی برای کاهش ریسک در flows مبتنی بر authorization code تبدیل شده است. هرچه معماری وب به سمت توزیعشدگی بیشتر، client-side execution بیشتر و reliance کمتر بر secretهای ایستا حرکت میکند، نقش PKCE پررنگتر میشود. از دید یک معمار نرمافزار، PKCE در وب نه یک جزئیات فنی حاشیهای، بلکه بخشی از طراحی دفاعی سیستم است که باید از ابتدا در معماری authentication و authorization لحاظ شود.
در دنیای واقعی و در میان معماران نرمافزار، پروتکلها زمانی اهمیت واقعی خود را نشان میدهند که در قالب ابزارهای مدیریت هویت (Identity Providers) همچون Keycloak پیادهسازی میشوند. Keycloak به عنوان یک راهکار Enterprise، مسئولیت سنگینِ انطباق با استانداردهای پیچیده OAuth 2.0 و OIDC را بر دوش دارد. در این سیستمها، PKCE دیگر یک گزینه اختیاری نیست، بلکه بخشی از سیاستهای امنیتی یکپارچه (Security Policies) محسوب میشود. زمانی که یک معمار تصمیم میگیرد کلاینتی را در Keycloak تعریف کند، گزینهای به نام “PKCE Enforcement” پیشروی او قرار میگیرد که فعالسازی آن، سیستم را از حالتِ اتکا به رمزهای استاتیک به سمت یک مدل اعتماد پویا سوق میدهد.
در ساختار Keycloak، فرآیند احراز هویت با فعال بودن PKCE، به شکلی کاملاً سختگیرانه مدیریت میشود. سرور در لحظه دریافت اولین درخواست، پارامترهای code_challenge و code_challenge_method را اعتبارسنجی میکند. اگر کلاینت این پارامترها را ارسال نکند و سرور در حالت “اجباری” قرار داشته باشد، Keycloak بلافاصله درخواست را با خطای امنیتی رد میکند. این سطح از سختگیری برای محیطهای تولیدی (Production) حیاتی است، زیرا مانع از آن میشود که توسعهدهندگان بهصورت سهوی از جریانهای قدیمی و آسیبپذیر استفاده کنند. در واقع، Keycloak نقش یک بازرس امنیتی را بازی میکند که اطمینان حاصل میکند پیش از صدور توکن، پیوند رمزنگاری بین درخواست اول و دوم به درستی برقرار شده است.
علاوه بر این، Keycloak با ارائه گزینههای متعددی برای متدهای هشینگ، دست معماران را برای افزایش سطح امنیت باز میگذارد. در حالی که استاندارد پایه ممکن است استفاده از S256 را توصیه کند، سیستمهای پیشرفتهای مانند Keycloak این امکان را فراهم میکنند که حتی سیاستهای پیچیدهتری بر اساس نوع کلاینت اعمال شود. برای مثال، کلاینتهای موبایل که ریسک بالاتری دارند، میتوانند مجبور به استفاده از متدهای هشینگ قویتر شوند. این انعطافپذیری در پیکربندی، PKCE را از یک کد ساده به یک ابزار مدیریتی تبدیل میکند که میتواند در طول چرخه حیاتِ یک اپلیکیشن، بسته به نیازهای امنیتی تغییر کند و ارتقا یابد.
نکته قابل تأمل در استفاده از ابزارهایی نظیر Keycloak، تأثیر PKCE بر لاگهای حسابرسی (Audit Logs) و مانیتورینگ است. در یک سیستم متمرکز، ردیابیِ شکستهای مربوط به PKCE میتواند نشانهای از حملاتِ فعال (مانند Attempted Hijacking) باشد. وقتی Keycloak گزارشی از عدم تطابق code_verifier ارائه میدهد، تیمهای عملیاتی و امنیتی میتوانند این رویداد را به عنوان یک تلاش ناموفق برای سرقت توکن شناسایی کنند. در حقیقت، PKCE در کنار یک Identity Provider قدرتمند، نه تنها امنیتِ حین اجرا (Runtime) را تأمین میکند، بلکه سطح دید و قابلیت ردیابی (Observability) سیستم را در برابر تهدیدات سایبری بهشکل قابلتوجهی افزایش میدهد.
در نهایت، استفاده از PKCE در محیطهایی که Keycloak یا سیستمهای مشابه در آنها نقش هسته مرکزی را دارند، به معنای پذیرش یک رویکرد دفاعی مدرن است. این رویکرد به جای اعتمادِ کورکورانه به کلاینتها، آنها را ملزم به اثبات مداوم هویت خود از طریق توابع ریاضی میکند. برای معماران، پیادهسازی PKCE در چنین پلتفرمهایی، انتقال بارِ مسئولیت احراز هویت از لایههای پراکنده به یک لایه مرکزی و مدیریتشده است که در آن امنیت، نه یک ویژگی جانبی، بلکه بخشی از ساختارِ زیربنایی تبادل دادههاست.
پیادهسازی PKCE در سطح کلان سازمانی، بیش از آنکه یک چالش رمزنگاری باشد، یک چالش هماهنگی میان تیمهای توسعه فرانتاند و بکاند است. از دیدگاه معمار، این پروتکل زمانی به درستی عمل میکند که دو تیم، تعریفی واحد و یکپارچه از “قراردادِ تبادل” داشته باشند. برای تیم فرانتاند، PKCE به معنای مدیریت چرخه حیاتِ یک جفت رشته موقت (Verifier و Challenge) در طول فرآیند احراز هویت است، در حالی که برای تیم بکاند یا تیم پلتفرم، این به معنای اجبار به رعایت سختگیرانه قوانین RFC 7636 در لایه Authorization Server و عدم صدور توکن در صورت عدم انطباق یا نقص در فرآیند است.
تیم فرانتاند باید مسئولیت تولید و نگهداری موقت Code Verifier را بر عهده بگیرد. در این متدولوژی، فرانتاند موظف است پیش از آغازِ Redirect به سمت سرور هویت، یک رشته تصادفی با آنتروپی کافی (حداقل ۴۳ کاراکتر و حداکثر ۱۲۸ کاراکتر بر اساس استاندارد) تولید کند. نکته کلیدی اینجاست که فرانتاند نباید این مقدار را در حافظههای پایدار مانند localStorage ذخیره کند، زیرا این امر خطر نشت داده را افزایش میدهد؛ بلکه باید از حافظههای موقت نشست (Session Storage) یا حافظه رمِ اپلیکیشن استفاده کند. پس از تولید Verifier، فرانتاند باید مقدار Code Challenge را از طریق تابع هش SHA-256 محاسبه کرده و آن را در پارامترهای درخواستِ اولیه ارسال کند. دقت در اجرای دقیقِ Base64URL encoding در هر دو سمت، محل بروز بسیاری از باگهای اولیه است که باید در تستهای واحد (Unit Testing) هر دو تیم لحاظ شود.
از سوی دیگر، تیم بکاند در لایه Identity Provider وظیفه دارد تا Code Challenge و متد استفاده شده را در دیتابیسِ نشستهای موقت خود ذخیره کند. این وظیفه حیاتی است که بکاند، کدهای مجوز (Authorization Codes) صادر شده را بدون در نظر گرفتن چالشِ مرتبط با آنها به ثبت نرساند. هنگامی که کلاینت در مرحله دوم برای دریافت Access Token مراجعه میکند، بکاند باید بلافاصله مقدار Code Verifier دریافتی را با همان متد هشینگِ اعلام شده در مرحله اول پردازش کند. اگر هشِ تولید شده با مقدارِ ذخیره شده در دیتابیس مطابقت نداشت، بکاند باید نه تنها درخواست را رد کند، بلکه این رخداد را به عنوان یک هشدار امنیتی در سیستم مانیتورینگ ثبت نماید. هرگونه اغماض در این مرحله از سوی بکاند، کل فلسفه PKCE را زیر سوال میبرد.
در مباحث مربوط به پروتکل و ارتباطات، تیمهای فرانتاند و بکاند باید بر سر «محدودیتهای زمانی» یا همان Expiration Window به توافق برسند. کد مجوزِ صادر شده برای PKCE نباید طول عمر بالایی داشته باشد. اگر فاصله زمانی بین مرحله اول (صدور کد) و مرحله دوم (تبادل توکن) بیش از حد استاندارد شود، سرور باید کد را ابطال کند. همچنین، برای جلوگیری از حملات بازپخش (Replay Attacks)، پیادهسازیِ “یکبار مصرف بودن” کد مجوز توسط بکاند الزامی است؛ به این معنا که پس از اولین تلاش برای مبادله توکن (چه موفق و چه ناموفق)، آن کد مجوز باید بلافاصله از سیستم حذف شود. این سطح از سختگیریِ هماهنگشده است که باعث میشود یک سیستم نرمافزاری در برابر مهاجمانِ حرفهای مقاومت کند.
در نهایت، برای رسیدن به یک پیادهسازی بدون نقص، توصیه میشود که تیمها از کتابخانههای استاندارد و تستشده برای OAuth 2.0 استفاده کنند و از پیادهسازی دستی منطق هشینگ یا تولید رشتههای تصادفی پرهیز نمایند. پیادهسازی PKCE یک فرآیند “بلاکمحور” است؛ یعنی اگر یکی از مراحلِ زنجیره اعتبارسنجی شکست بخورد، کل فرآیند باید متوقف شود. معماران باید در جلسات طراحی (Design Review)، روی این موضوع تأکید کنند که خطاهای مربوط به PKCE نباید اطلاعات حساسی را به مهاجم نشت دهند؛ بلکه باید پیامهای خطای استاندارد و خنثی بازگردانده شود تا از تکنیکهای Enumeration جلوگیری شود. تعاملِ دقیقِ فرانتاند و بکاند در این حوزه، عملاً تضمینکننده آن است که امنیت در اپلیکیشن شما، فراتر از یک شعارِ بازاریابی، در لایههای کد جاری است.