پروتکل OAuth2.0: مفاهیم

چهارشنبه، 3 مهر 1398

oauth2.0
security
authentication
authorization
Photo by Matthew Henry on Unsplash
Photo by Matthew Henry on Unsplash

مقدمه

یکی از تکراری ترین مولفه هایی که در تمامِ برنامه ها باید پیاده کنیم دسترسیِ کاربرهاست. رسم به اینه که کاربر اول باید عضو بشه، بعد می تونه از امکاناتِ برنامه استفاده کنه، تازه اونم نه از تمام امکانات! بلکه فقط اونایی که اجازشو داره. مثلاً در یک فروشگاهِ اینترنتی کاربرای عادی (خریدار) می تونن بعد از عضو شدن خرید انجام بدن و کاربرهای ادمین می تونن کالا اضافه کنن، قیمت ها رو تغیر بدن و غیره. پروتکلِ OAuth (بخونید اُ آث) برای همین  منظور تعریف شده. یعنی اختیاراتِ کاربرها در یک محیطِ شبکه ای.

Authentication و Authorization

همین مثالِ ساده ی فروشگاهِ اینترنتی رو در نظر بگیرید. در این مثال دو نوع کاربر داریم: کاربرِ ادمین و کاربرِ خریدار. این دو نوع کاربر از نظر اختیارات با هم تفاوت هایی دارن، اما یک چیز بین شون مشترکه: هر دو قبل از استفاده از سیستم باید خودشون رو معرفی کنن، یه به زبونی ساده تر؛ باید وارد سیستم بشن. به فرآیندِ "وارد شدن" در حوزه ی امنیت Authentication می گن. بعد از ورودِ کاربر به سیستم، هر کاربری بر اساس مجوزهایی که داره می تونه کارهای مشخصی انجام بده. به فرآیندِ "تعین اختیارات" هم Authorization گفته می شه.

Authentication چیست؟ فرآیندی که در اون هویتِ کاربر مشخص می شه. مثل "ورود" در خیلی از سایت ها. همیشه هم با صفحه ی Login اتفاق نمی افته. شاید با یک دستگاهِ اثرِ انگشت انجام بشه.

Authorization چیست؟ فرآیندی که در اون اختیاراتِ کاربر مشخص می شه. مثل زمانی که کاربرِ "خریدار" اختیارِ خرید کردن رو داره و کاربرِ "ادمین" اختیارِ وارد کردنِ کالاهای جدید به سایت.

پروتکل OAuth2.0

ورودِ تجهیزات و تکنولوژی های Cloud صنعتِ نرم افزار رو خیلی متحول کرد. قبل از ورودِ Cloud به صنعتِ نرم افزار اگر می خواستیم کارهایی مثل "ورودِ کاربرها" و "مدیریتِ مجوزها" و غیره رو انجام ندیم مجبور بودیم از Libraryها و ابزارهای آماده استفاده کنیم. اما با فراگیر شدن راهکارها و نرم افزارهایی که به صورت سرویس ارائه می شن این دغدغه رفع و رجوع شده. با استفاده از سرویس های امنیتی می تونیم این دغدغه ها رو از برنامه ی خودمون خارج کنیم.

پروتکل OAuth در چنین محیطی تعریف شد. هدف اصلی این پروتکل انجام Delegated Authorization است. به فارسی می شه گفت "اختیاردهی وکالتی". در ادامه منظور از "اختیاردهی وکالتی" رو کامل روشن می کنم.

تاریخ این پروتکل بر می گرده به زمانی که شرکت Twitter برای اولین بار می خواست به برنامه های جانبیش امکان استفاده از اطلاعات کاربرها رو بده. بدیهیه که برای این کار اول باید از کاربر اجازه گرفت. بدون اجازه که نمی شه! بنابراین باید روندی تعریف می شد که در اون سه تا مولفه با هم درگیر باشن: کاربر، Twitter و برنامه ی جانبی (Third Party).

اجازه برای استفاده از اطلاعات کاربرانِ Twitter
اجازه برای استفاده از اطلاعات کاربرانِ Twitter

توجیه کنید که این رابطه به هیچ شکل دیگه ای نمی تونه باشه. برنامه ی جانبی نمی تونه بین کاربر و Twitter قرار بگیره. Twitter باید به صورت مستقیم از خودِ کاربر بپرسه که آیا به فلان برنامه اجازه ی دسترسی به اطلاعاتش رو می ده یا نه. این نیازِ خاص در نهایت منجر به تعریف پروتکلی به نام OAuth شد. اولین بار نسخه ی اولِ این پروتکل در سال 2010 منتشر شد و نسخه ی دوم هم که بسیار کامل تر بود و باعث منسوخ شدنِ نسخه ی اول شد، در سال 2012 انتشار یافت.

از سال 2010 کم کم تکرارِ تصویرِ زیر در سایت های مختلف زیاد شد. بعد از Twitter،‏ Google و دیگران هم این جور سرویس ها رو ارائه دادن، تا به امروز... امروز حضور پروتکل های OAuth2.0 و OpenID Connect کمکِ بزرگی به همه ی برنامه نویس ها کرده. برنامه نویس ها می تونن بدون اینکه اصلا درگیر کد نوشتن برای قابلیت های Authentication و Authorization بشن از سرویس های آماده در این حوزه به راحتی استفاده کنن. واقعیت امر اینه که برای استفاده از این سرویس ها حتی نیاز به دونستنِ پروتکل هم نیست، اما به نظرم داشتنِ یه کم اطلاعات بد نیست!

ورود با Facebook و Google
ورود با Facebook و Google

نقش ها

تا اینجای کار فهمیدیم که OAuth2.0 پروتکلیه برای ایجاد امکان دسترسی محدود برای برنامه ها از منابع خاص. برای فهمِ بیش تر باید نقشِ هر کدوم از مولفه های موجود در این پروتکل رو خوب شناخت. مولفه ها رو همراه با همون مثال فروشگاهِ اینترنتی توضیح می دم. فرض کنید می خواید در یک فروشگاهِ اینترنتی به نام Coolest Shop عضو بشید و ازش خرید کنید. سایت بهتون امکان عضویت با حسابِ Google رو می ده. یعنی چیزی شبیه تصویر بالا. این سایت رو توی ذهنتون داشته باشید تا هر کدوم از مولفه های پروتکلِ OAuth2.0 رو توضیح بدم.

Resource

منبعی که قراره به صورتِ کنترل شده در اختیار دیگران قرار بگیره. در سایتِ Coolest Shop حضورِ دکمه ی Sign in with Google به این معنیه که اگر شما بخواید و اجازش رو بدید، سایت می تونه اطلاعاتتون رو از Google دریافت کنه. مثل نام، عکس پروفایل، contactها و غیره. بنابراین اطلاعاتِ شما در این مثال Resource هستن. یا به بیان فنی تر Resourceها همون APIهایی هستن که Google از طریقشون این اطلاعات رو در اختیار دیگران می زاره.

Resource Owner

صاحبِ resourceها. در مثال فروشگاه صاحبِ resource شما هستید. یعنی شما مشخص می کنید که آیا Coolest Shop اجازه داره اطلاعاتتون رو از Google بخونه یا نه. 

Client

همون برنامه ای که قصد دسترسی به resource رو داره، یعنی سایتِ Coolest Shop.

Authorization Server

سیستمی که در نهایت مجوزِ دسترسی رو با اجازه ی Resource Owner صادر می کنه. در این مثال Google این نقش رو داره.

Resource Server

سیستمی که resource رو ارائه می ده. در این مثال این نقش رو هم Google بازی می کنه. توجه کنید لزوماً قرار نیست سیستمی که مجوز رو صادر می کنه (Authorization Server) با سیستمی که APIها رو در اختیار داره (Resource Server) یکی باشن.

Authorization Grant

همون مجوزی که Authorization Server صادر می کنه.

اول به تصویر زیر دقت کنید. کلیاتِ ماجرا به همراهِ نقشِ هر کدوم از مولفه ها در این تصویر تشریح شده: 

عملکردِ OAuth2.0 به صورت کلی
عملکردِ OAuth2.0 به صورت کلی

توجه کنید که این تصویر فقط بیانی شماتیک از چگونگیِ کارکردِ این پروتکله. به صورت اجرایی و فنی شکلِ کارکردِ پروتکلِ OAuth2.0 شبیه این نیست! برای درک چگونگیِ کارکردِ باید با جزئیاتِ بیش تری آشنا شد. اولیش درکِ Flow یا Grant Type است.

Flow یا Grant Type

در پروتکلِ OAuth2.0 می شه با ترکیب های مختلف روندِ تعامل مولفه ها رو تغیر داد تا طبق نیازمون کار کنن. یه جورایی شبیه به یک پازل! به هر کدوم از این روندها یک flow می گن. در واقع flow مشخص می کنه که هر مولفه ای چگونه نقشش رو بازی می کنه. در پروتکلِ OAuth2.0 تعدادی flow تعریف شده که من فقط سه تاشون رو اینجا بررسی می کنم. باقیشون رو بلد نیستم!

Authorization Code

فهمیدن این flow فهم باقی flowها رو راحت تر می کنه. نام دیگه ی این flow،‏ Code هم هست و بیش ترین کاربرد بین flowها رو داره. استفاده ی اصلیش هم برای برنامه های تحت وبه.

فرض کنید قراره Coolest Shop با استفاده از این flow با Google کار کنه. اولین کار اینه که سایت رو به عنوان Client (به معنایی که در قسمتِ نقش ها توضیح دادم) به Google معرفی کنیم. بعد از انجامِ ثبتِ نام، Google دو تا مقدارِ مهم در اختیارمون قرار می ده: ClientID و Client Secret.‏ ClientID شناسه اییه که معرفِ سایته و Client Secret هم یه جورایی نقش گذرواژه رو بازی می کنه:

ClientID=123
Client_Secret=abc

با انجام این کار Google سایتِ Coolest Shop رو به عنوان کسی که قراره از طرف دیگران به اطلاعات شون دسترسی داشته باشه، به رسمیت می شناسه. یعنی همون "اختیارات وکالتی" که در بالا گفتم. یعنی سایت Coolest Shop می تونه با اجازه ی کاربرها، اطلاعات شون رو از APIهای Google بخونه.

مرحله ی بعد فرستادنِ یک پیام به سمتِ Google است مبنی بر اینکه فلان کاربر قصد داره به سایت Coolest Shop اجازه ی خوندن اطلاعاتش رو بده. زیاد سخت نیست. کافیه لینکی مثل زیر برای دکمه ی Sign in with Google بنویسیم:

http://api.google.com/oauth/v2/auth?client_id=123&redirect_uri=https://coolestshop.com/signinwithgoogle&scope=contacts.read&response_type=code

بعد از کلیک روی دکمه، مرورگر کاربر رو به صفحه ای برای وارد کردنِ اطلاعاتش راهنمایی می کنه که بهش می گن Consent Screen. حتما تا حالا هزار بار دیدید:

Consent Screen
Consent Screen

بعد از ورودِ اطلاعات توسط کاربر و کلیک روی دکمه ی Allow،‏ Google نام کاربری و گذرواژه رو در Authorization Server بررسی می کنه و بعد از اون مرورگر رو به redirect_uri هدایت می کنه و مقدار code رو هم به صورت query ارسال می کنه. مقدار redirect_uri در لینک بالا مشخص شده. در واقع خودمون به Google می گیم که بعد از انجامِ کار مرورگر رو به چه آدرسی هدایت کنه (3XX HTTP Response).

دریافتِ access token به وسیله ی code
دریافتِ access token به وسیله ی code

پارامترِ code چیست؟

با redirect شدن عملا مرورگر آدرسِ ارسال شده از سمتِ Google رو برای سرورِ Coolest Shop می فرسته. سرور مقدار code رو بر می داره و برای تکمیل فرآیند، اینبار خودش به صورت مستقیم با Authorization Server ارتباط برقرار می کنه. Authorization Server مقدارِ code رو بررسی می کنه و در ازاش یک access token برای سرورِ Coolest Shop ارسال می کنه. access token کلیدِ حلِ ماجراست. یعنی همون وکالتیه که کاربر از سمتِ خودش به Client‏ (Coolest Shop) داده. حالا سرورِ Coolest Shop با استفاده از access token می تونه به APIهای Resource Server دسترسی داشته باشه.

دسترسی به Resource با داشتنِ access token
دسترسی به Resource با داشتنِ access token

به این ترتیب سرورِ Coolest Shop از این به بعد مقدارِ access token رو همراه با درخواست هاش برای Resource Server ارسال می کنه، Resource Server هم با بررسی کردنِ tokenها در ازاش resource ِ مورد نظر رو در اختیارِ Coolest Shop قرار می ده.

به زبون دیگه بین Google‏ (Resource Server) و Coolest Shop‏ (Client) دو حالت امکان بروز داره. در یکی Coolest Shop به Google مراجعه می کنه و می گه "اطلاعات یکی از کاربرات به نام علی رو نیاز دارم"، سرور درخواست رو بررسی می کنه و می گه "شرمنده، اطلاعات علی متعلق به خودشه، کسِ دیگه ای نمی تونه بهشون دسترسی داشته باشه". در حالت دیگه Coolest Shop به Google مراجعه می کنه و می گه "به اطلاعات علی نیاز دارم، علی خودش بهم وکالت داده تا اونارو بخونم، اینم وکالتش: TOK..."

احتمالا دیگه مفهومِ Delegated Authorization (اختیاردهی وکالتی) کاملا روشن شده. مقدارِ access token یه چیزیه شبیه به این رشته:

MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3

حالا چند تا سوال پیش میاد:

GET https://google.com/api/contacts?userid=ali-doustkani
Authorization: Bearer MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3

Implicit Flow

Authorization Code امن ترین Flow است اما استفاده ازش همیشه هم ممکن نیست! مثلا وقتی برنامه به صورتِ SPA‏ (Single Page Application) نوشته شده. چراکه در SPAها سرورِ میانی امکانِ دسترسی و نگهداریِ access tokenها رو نداره. این جور برنامه ها باید از flowی Implicit استفاده کنن. flowی Implicit مدل ساده شده ی authorization code است. در این flow اصلا از code استفاده نمی شه و access token به صورت مستقیم در اختیارِ Client قرار می گیره.

دریافتِ access token به وسیله ی مرورگر
دریافتِ access token به وسیله ی مرورگر

همون طور که از شکلِ بالا مشخصه این flow فقط از امکاناتِ Front Channel استفاده می کنه. حالا سوال اینجاست که آیا برنامه هایی که از این flow استفاده می کنن مشکل امنیتی دارن؟ نه لزوماً. با ایجاد محدودیت هایی می شه ضعف های این flow رو جبران کرد. مثلاً با کمتر کردنِ طولِ عمرِ access tokenها. یا استفاده از SSL و گواهینامه های معتبر. با انجام این ملاحظاتِ امنیتی در نهایت به یک راه حلِ قابل قبول و امن می رسیم. جایی واسه نگرانی نیست.

Client Credentials Flow

این flow برای شرایطی تعبیه شده که کاربری در کار نیست! مثل وقتی که سرور باید برای تکمیل کارش به یک API خاص متصل بشه، یا به بیانی دیگه زمانی که تعاملات بین ماشین هاست. مثل تعامل Microserviceها با هم. بنابراین سرور قرار نیست با وکالت از سمتِ کسی به API دسترسی داشته باشه بلکه خودش به صورت مستقیم از API استفاده می کنه.

در این حالت Client به جای اینکه با وکالتِ کاربر access token دریافت کنه، با استفاده از ClientID و Client Secret ِ مختص به خودش یک access token از Authorization Server درخواست می کنه و سپس با استفاده از اون token به صورت مستقیم با Resource Server وارد تعامل می شه. 

دریافتِ access token توسط خودِ client
دریافتِ access token توسط خودِ client

تعددِ Resource Serverها

اکثرِ برنامه های مدرن نیاز به سرویس های متعددی دارن. حتی خودِ برنامه ها هم این روزا مثل گذشته به صورت تک واحدی (monolith) پیاده سازی نمی شن، بلکه به تعدادی سرویس های مشخص و کوچیک شکسته می شن (مثل معماری Microservices). این حرف به این معنیه که تعدادِ Resource Serverها در حال افزایشه و این می تونه دسترسی به Resourceها رو دچار سختی هایی کنه، چراکه استفاده از resource در هر کدوم از Resource Serverها بدون اجازه ممکن نیست. همین طور اینکه برنامه ها برای انجامِ یک کارِ ساده نیاز به استفاده از resourceهای متعددی دارن. شاید این مسئله در ابتدا سخت و غیر قابل پیاده سازی به نظر بیاد اما با وجود پروتکلِ OAuth2.0 سخت نیست. بنابراین با یک بار صدورِ access token برنامه می تونه از APIهای متعددی در Resource Serverهای متعدد استفاده کنه.

OpenID Connect: OIDC

با گذشت زمان پروتکلِ OAuth2.0 معروف تر شد و استفاده ازش هم خیلی زیادتر. شرکت ها از این پروتکل نه تنها برای Authorization بلکه برای Authentication هم استفاده می کردن. در حالی که OAuth2.0 در اصل برای Authorization طراحی شده بود. همین اتفاق باعث شد بین پیاده سازی های مختلف شرکت ها تفاوت هایی وجود داشته باشه. به عنوان مثال اگر می خواستید Authentication ِ سایتِ Coolest Shop رو هم به Google و هم Twitter وصل کنید احتمالا با دو پیاده سازی متفاوت رو به رو بودید. Google فرمولِ خاصِ خودش رو داشت و Twitter هم فرمولِ خاصِ خودش. دلیل این تفاوت به ذات پروتکلِ OAuth2.0 بر می گرده چون دغدغه ی OAuth2.0 اصلاً Authentication نیست بلکه Delegated Authorization است. همون طور که در ابتدای مقاله نوشتم، در Authorization ما با اختیارات کاربر طرفیم و در Authentication با هویتش. یک ورودِ ساده به یک سایت مسئله ی اختیارات نیست، بلکه تنها مسئله ی هویته. به زبون دیگه، برای ورود به سایتِ Coolest Shop کاربر باید فقط خودش رو معرفی کنه. یعنی بگه "من کی هستم". Coolest Shop هم برای بررسی هویت کاربر نیاز به اطلاعاتش داره. در صورتی که چیزی با عنوان "اطلاعات کاربر" اصلا در پروتکلِ OAuth2.0 تعریف نشده! دلیل عدم هماهنگی بین شرکت های مختلف هم دقیقاً همین موضوع بود. هر کدوم از این شرکتا باید یه طوری OAuth2.0 رو سفارشی می کردن تا بتونه به یک شکلی "اطلاعات کاربر" رو به دستِ Client برسونه.

نکته: پروتکلِ OpenID با OpenID Connect یکی نیست.

پروتکلِ OIDC راه حلِ این مشکل بود. OIDC در حقیقت پروتکلِ جداگانه ای نیست. فقط یک لایه ی خیلی نازک روی OAuth2.0 است که آن را برای کاربردهای Authentication به شکلی استاندارد سازگار می کنه. در این پروتکل سه تا مولفه به OAuth2.0 اضافه شده که عبارتند از: 

  1. ID Token:‏ tokenی که حاوی اطلاعات کاربره.
  2. User info endpoint:‏ اگر اطلاعاتِ مورد نیازمون درباره ی کاربر در ID Token موجود نبود از این endpoint در خواست می کنیم.
  3. scopeهای استاندارد: مثل openid،‏ profile و email که ازشون برای دریافت اطلاعات متفاوت کاربر استفاده می شه.

برای درک بیشتر به لینک زیر دقت کنید. بین این لینک و لینکِ قبلی تفاوتی وجود نداره اما مقدار scope ِ اون نشون می ده که پروتکلِ مورد استفادش OIDC است:

https://authserver.com?redirect_url=XXX&response_type=code&scope=profile

بر اساس اینکه چه scopeی رو برای Authorization Server می فرستیم اطلاعات مختلفی در ID Token برای Client ارسال می شه. مثلا اگه scope ارسالی profile باشه اطلاعاتی مشابه زیر باید در token برگرده:

name, family_name, given_name, middle_name, nickname, picture

ID Token

پاسخِ درخواست OIDC،‏ access token نیست. بلکه ID Token است. تفاوت شان چیست؟ ID Tokenها از یک فرمت مشخص به نام JSON Web Token‏ (JWT - جات هم خونده می شه) ساخته می شن. هر JWT از سه قسمت تشکیل شده:

  1. header: یک سری اطلاعاتِ کلی درباره ی token رو در خودش داره. مثل نامِ الگوریتمِ رمزنگاری و غیره.
  2. payload: اطلاعات کاربر. اینکه چه اطلاعاتی در payload قرار می گیره بستگی به scope ِ درخواستی داره. به اطلاعاتِ کاربر در این بخش claim هم گفته می شه.
  3. signature: امضای دیجیتال یا در حقیقت مقدارِ رمز شده ی کل token. به هر حال باید همیشه مطمئن بود که token ِ صادر شده واقعاً از طرف Authorization Server باشه وگرنه هر کسی می تونه با ایجادِ یک token ِ جعلی اطلاعاتِ کاربرِ دیگه ای رو به ما بده. چگونگی امضای token بستگی به الگوریتمِ رمزنگاری داره. این الگوریتم هم می تونه symmetric باشه و هم asymmetric، اما معمولاً از الگوریتم های asymmetric استفاده می شه تا دچار دردسرِ دسترسی به secret key نشیم.

برای مثال یک JWT به این شکله:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

این مقدار رو در سایت jwt.io کپی کنید تا محتویاتش رو ببینید. مقدار header و payload و signature با نقطه از هم جدا شدن.

کلام آخر

چیزی که توی این پست نوشتم کلیات کارکردِ OAuth2.0 و OIDC است. تقریبا به هر شکلی که بخواید از OAuth2.0 استفاده کنید اینا حداقل چیزیه که باید بدونید. از یه login ِ ساده با Google گرفته تا پیاده سازیِ یک Authorization Server.

پیشنهاد من در رابطه با مسائل مربوط به مدیریت کاربرها و مسائل امنیتی اینه که تا حد ممکن این چیزا رو خودتون پیاده سازی نکنید و از سرویس های آماده مثل Auth0 استفاده کنید. به چند دلیل: یکی اینکه پیاده سازیِ پروتکل های امنیتی کارِ خیلی سختیه و احتمالِ اینکه اشتباهاتش جبران ناپذیر باشه خیلیه و دوم اینکه زمان و انرژی خودتون رو صرفِ مسئله ای می کنید که برنامه تون قراره حل کنه و با این کار کل مبحث مربوط به authorization رو برون سپاری می کنید.

به هر حال اگر به دلیلی نیاز به پیاده سازی پروتکل OAuth2.0 داشتید حتما به farmeworkهایی مثل IdentityServer رو بررسی کنید.