Ця стаття базується на моєму досвіді участі в одному з проєктів.
Для початку пройдемось по теорії — що таке авторизаційний сервер та навіщо він потрібен. Далі спробуємо обрати підходяще рішення. Я розповім про основні користувальницькі флоу авторизації та варіанти їх використання. І насамкінець виділю ключові концепції розробки кастомного авторизаційного сервера та поширені помилки.
Але перед цим — невелика ремарка.
Авторизаційний сервер, айдентіті-сервер, аус-сервер — під цими поняттями зазвичай мають на увазі software, який на основі юзер-логіна та юзер-пароля видає токен або щось подібне для авторизації в системі.
У цій статті ви можете помітити різні терміни, але відмінностей між ними немає.
Наша історія вибору авторизаційного сервера почалася з роботи над одним із давніх проєктів — legacy-моноліту на .NET 3.5. Авторизація в ньому була виконана у вигляді звичайної сторінки на ASP, де користувач вводить логін та пароль. В code-behind була встановлена сесія, куди записувалась необхідна інформація. Це й була аутентифікація та авторизація користувача.
Флоу дуже простий: система зверталася до бази, порівнювала збережені раніше та введені зараз дані і повертала дозвіл або заборону на доступ. При цьому програма ні з чим не інтегрується.
Однак згодом ми зіткнулися з труднощами. Це були і перфоманс-проблеми, і перфоманс-команди і багато іншого. В результаті на додавання нового функціоналу йшли тижні, якщо не місяці — тобто кілька спринтів навіть на невелику фічу. Система виходила все більш заплутаною та перевантаженою, кількість багів у ній зростала. Незадоволені були і девелопери, і менеджери.
Менеджерам не подобався повільний розвиток продукту, а розробники були невдоволені складнощами при роботі над застосунком.
Команда вирішила докорінно змінити ситуацію та перейти до мікросервісів. Для цього ми поділили великий моноліт на кілька частин.
Фактично він і так був поділений, але одну частину ми винесли як окремий проєкт. Визнаю, у цей момент про авторизацію ніхто не думав. Ми, скоріше, розглядали новий проєкт як Proof of Concept. Нам хотілося спробувати зробити частину колишнього моноліту на нових технологіях, з фронтендом, на сучасному фреймворку. Ми собі думали так: якщо ідея спрацює і всі будуть задоволені, поширимо цей підхід і на інші частини системи.
Для PoC-проєкту ми вигадали наступну схему. Є фронтенд та гейтвей, під ним розташовується оркестратор, которий пов’язаний із мікросервісами. Програма отримує запит від фронтенду і на його основі робить запити до відповідних мікросервісів:
Наша команда зробила фронтед на Angular, сервіси та оркестратор — на .NET 6, а також додала API Gateway, котрий працював як AuthGuard.
Якщо користувач не авторизований, гейтвей не пропускає запит і повертає помилку 401. При цьому легасі залишався таким, як був. Ми не мали не то що можливості, а навіть мети оверінженерити. Нам був потрібний лише PoC. Тому для авторизації ми використовували токен із легасі. Для цього додали в проєкт нову функціональність. Вона генерувала токен, а фронтенд забирав його та відправляв реквести з ним через API Gateway. Той, своєю чергою, парсив токен для оцінки валідності і пропускав запит далі.
Експеримент показав гарні результати. Ми поступово переклали частину системи на мікросервіси, і це позитивно оцінили всі сторони проєкту.
Однак зі збільшенням кількості цих елементів з’явилася проблема: не можна використовувати той ендпойнт на легасі, який видавав токени.
Так, технічно це можна було б реалізувати. Однак для цього потрібно дописати таку функціональність, яка була б повноцінним авторизаційним сервером.
Ми замислились про підготовку якогось long-term-рішення. Це дозволило б закрити питання авторизації та аутентифікації в системі, застосувати однаковий підхід на всіх мікросервісах та на всіх поточних і майбутніх клієнтах. Але «переїзд» вимагав інфраструктурних змін. Тому стало зрозуміло: якісна аутентифікація та авторизація необхідні тут і зараз.
Виходячи зі свого досвіду, скажу так: якщо ваша команда не розробляла аус-сервер, то вам необхідно проговорити кілька загальних понять. Усім учасникам процесу варто говорити однією мовою, тому майте при собі такий короткий словник:
Читайте також: Підробка неможлива: як влаштований токен і які завдання можна вирішити за допомогою JWT-авторизації
Окремо хочу поговорити про цю частину протоколу OAuth. Існує кілька часто використовуваних грант тайпів:
Вказані моделі закривають усі основні пойнти. Таким чином ви можете розібратися, які з них та в яких ситуаціях варто використовувати.
Це один із перших грант тайпів, проте його вже не рекомендують використовувати. Незабаром цю модель можуть взагалі прибрати з OAuth-протоколу. Але розповісти про неї слід вже для того, аби ви розуміли, як робити не треба.
Як відбувається процес отримання токена в Password Grant Type? Уявімо користувача, який переходить на клієнта (припустимо, фронтенд-застосунок). На клієнті є спеціальна сторінка, де користувач вводить свої логін та пароль. Після цього клієнт надсилає на айдентіті-сервер таку інформацію: ClientSecret, ClientName, UserName тощо. Сервер після порівняння даних повертає аксес-токен. Це легкий шлях отримання доступу, адже тут потрібні лише налаштовані на сервері ClientId та ClientSecret, отримані від користувача UserName та UserPassword, а також Scopes — ідентифікатори визначення доступних користувачеві даних.
Як бачите, модель доволі швидка для реалізації. Завдяки цьому свого часу Password Grant Type стандартизував процес отримання аксес-токена. До цього можна було спостерігати зоопарк різних механізмів того, як, де і чому використовувати UserName та UserPassword. Інколи доходило до того, що логін та пароль відправлялися мало не в незашифрованому вигляді в хедерах до реквесту.
Сьогодні ж Password Grant Type вже застарів. Його ключовий недолік — прогалини в безпеці. Ви можете помітити, що в цій схемі клієнт може десь зберігати логіни та паролі. При цьому ендпойнт через стандартизованість грант тайпу добре відомий. Тому зловмисники можуть спробувати встановити в систему NPM-пакет для збору всіх даних і перехопити токен, що видається.
Так злочинець дізнається, де хоститься сервіс авторизації, а також знатиме UserName та UserPassword. У результаті система буде скомпрометована. Більш того: хакер постійно отримуватиме валідний токен, бо матиме логін та пароль.
Також є проблема зберігання ClientId та ClientSecret на стороні клієнта. Це не найбезпечніше рішення для фронтенду, оскільки тут не здійснюється жодних редиректів. Прив’язка йде лише на основі цих параметрів. Якщо їх вкрасти, новий клієнт зможе підключити користувача до стороннього сайту та ввести ClientId і ClientSecret. І тоді користувачі будуть логінитися там й отримувати аксес-токен, який також буде викрадений. Усе це перекреслює попередні переваги Password Grant Type.
Цей підхід значно безпечніший, тому якийсь час він був дуже популярним. Implicit Flow досить зручний, його можна встановити без особливих зусиль. Однак він має кілька проблем із захищеністю, які сьогодні роблять цей механізм у протоколі OAuth 2.0 небажаним.
Для розуміння проблеми розберемо флоу покроково:
Однак у цієї схеми є значний недолік. В Implicit Flow все побудовано на редиректах, тобто на GET-запитах. Через це на фінальному кроці клієнт отримує аксес-токен із Query-параметрів. А тому шкідливе програмне забезпечення на клієнті може дістати токен із коду так саме, як ми це робимо за допомогою бібліотек для отримання доступу до системи. А це вже дуже серйозна проблема.
Хоча набагато гірше інше — ризик викрадення рефреш-токену. Аксес-токен надає доступ на 10 хвилин. Це чимало, але обмежує зловмисника, змушуючи його повторювати свої дії. З рефреш-токеном хакер вільний. Цей токен позбавляє користувача необхідності повторного редиректу на айдентіті-сервер після закінчення сесії.
По суті, рефреш-токен дає змогу самій системі сходити на сервер авторизації за новим аксес-токеном. Як наслідок, зловмисник заволодіє аккаунтом клієнта на невідомий період. Усе буде залежати від сесії користувача, коли він вийшов із застосунку та коли повернеться до нього. Тому рефреш-токен багато в чому важливіший за аксес-токен. Приходить він також у Query-параметрах, через що його легко розпарсити.
Це рекомендований OAuth-підхід. Його сценарій схожий на Implicit Flow: користувач робить логін-клік на клієнта та перенаправляється на /authorize на айдентіті-сервер. Останній після підтвердження валідності логіна та пароля повертає не аксес- і рефреш-токени, а авторизаційний код в URL. Далі клієнт відправляє на айдентіті-сервер POST-запит із ClientId, ClientSecret та Authorization Code та отримує у відповідь POST-реквест із аксес- і рефреш-токенами.
Через те, що такий реквест більш захищений, ніж Query-параметри, вся схема виходить більш надійною:
Маємо ідеальний флоу для отримання аксес- та рефреш-токенів, але завжди є «але».
Головне питання вже до команди, яка інтегрується з цим флоу: де зберігати аксес- та рефреш-токени?
Якщо в пам’яті, то при повторному вході треба заново авторизуватися. А якщо покласти токени в cookies, session чи local storage, то шкідливе malware-ПО легко вкраде їх. У результаті отримання доступу влаштовано набагато безпечніше, ніж раніше, але зберігання доступів вразливе. Я думаю, багато хто стикався з цією проблемою. Кожний вирішує її по-своєму, тому далі я розповім, як ми вирішили це питання.
Раніше я описував схеми отримання доступу до API від клієнта (web, desktop, mobile). Client Credentials необхідний для доступу однієї програми API до іншої програми API.
Флоу організовано дуже просто: застосунок, який запитує доступ, відправляє ClientId та ClientSecret на айдентіті-сервер, а той після успішної перевірки цих параметрів повертає аксес-токен:
Така простота можлива тому, що при взаємодії API не потрібна інтерактивність — її фактично нікуди додати. Апішки працюють без фронтендів.
До того ж, сама по собі API — це захищена частина системи, куди не так просто потрапити. Тому ми можемо спокійно записати параметри до сеттингів або в пам’ять та за необхідності дістати їх звідти для аус-сервера, щоб він видав токен.
Після оцінки основних авторизаційних флоу можна переходити до вибору сервера. Спочатку наша команда зупинилася на трьох кандидатах:
Кожен із них цікавий по-своєму.
Наприклад, Identity Server 4 — це опенсорсний продукт, де можна легко налаштовувати різні моменти. Особисто нашій команді він був добре знайомий з попередніх проєктів.
Опенсорним сервером є й Keycloak — один із найпопулярніших на сьогодні. Хоча ми були готові до вибору платної програми, яка дає всі механізми OAuth-протоколу «з коробки». У цьому випадку нас зацікавив Cognito, оскільки ми мали інфраструктуру на AWS.
При виборі сервера для нас важливими були наступні параметри:
До його переваг відносяться просте та гнучке налаштування, підтримка SSO і кастомних флоу та адмінпанель прямо «з коробки», що заощаджує багато часу.
Але як мінімум для нашої команди, Keycloak має мінуси:
При оцінці цих показників ми пам’ятали, що розробляємо long-term support продукт. Якби надалі виникли проблеми або нам просто була б незрозуміла поведінка системи, то довелося б щось змінювати в коді. Це призвело б до серйозних проблем та затримок, тому ми відмовилися від Keycloak.
Цей продукт має багато переваг. Наприклад, це дешеве рішення для нашої кількості користувачів: активних і запланованих на майбутнє. Також у Cognito дуже швидкий сетап. Ви просто створюєте User Pool та клієнтів — і фронтенд-застосунок фактично готовий до авторизації. Сервер відразу можна використовувати.
Але і Cognito має низку недоліків, чи, скоріше, підозр на недоліки.
До прикладу, нам було важко зрозуміти межі змін системи при авторизації користувача. Наш проєкт потребував гнучкого підходу між введенням логіна та пароля і видачею аксес-токена. Також нам не сподобалася документація на AWS, оскільки в ній важко щось знайти.
Виникли деякі складнощі при міграції користувачів. Припустимо, юзер вперше входить у систему. Вона перенаправляє його на сторінку Cognito для введення логіна та пароля і перевіряє, чи існує такий користувач у юзер-пулі. Його, звісно, немає, адже він уперше тут. Тоді на наступному етапі має бути лямбда-функція, яка звертається до легасі-застосунку з введеними логіном та паролем та перевіряє їх валідність. Якщо дані помилкові, видається Error. Якщо все правильно, то AWS, запам’ятавши ще на першому етапі логін та пароль, створює користувача в юзер-пулі після підтвердження лямбда-функцією всіх даних — і повертає аксес-токен:
Тепер уявімо, що користувач повторно заходить у систему і вводить вже знайомі для AWS логін та пароль. AWS перевіряє існування користувача в юзер-пулі (наприклад, за мейлом), у разі позитивного результату звіряє логін і пароль та за умови їх правильності видає аксес-токен. Нехитра та ефективна схема.
Проте, як я вже відзначив вище, у цій моделі є один підводний камінь, який насправді практично на поверхні. Після міграції користувачів на цю айдентіті-систему потрібно забезпечити їхню синхронізацію з легасі-системою. І якщо старий та новий портал із новими клієнтами повинні працювати одночасно, вам доведеться серйозно попрацювати над налаштуванням такої синхронізації.
Через усі ці недоліки ми вирішили не брати Cognito.
Наша команда вже мала досвід роботи з Identity Server 4, то спочатку це могло здатися «тим самим» рішенням. Цей авторизаційний сервер показав себе гнучким, із простою інтеграцією в .NET-застосунок. Також до його безумовних плюсів варто віднести наявність кастомних флоу, підтримку SSO та хорошу документацію.
Тут теж треба подбати про хостинг, але більш серйозним недоліком є припинення підтримки цього продукту після 22 листопада 2022 року. Враховуючи довгострокову перспективу розвитку нашого проєкту, подібне обмеження було неприйнятним. Ми рушили далі.
Порятунком для нас став цей авторизаційний сервер. Duende — це фактично продовження Identity Server 4 з усіма його перевагами, але без проблеми «22 листопада». Також маємо BFF Security Framework. Детальну інформацію про нього можете почитати на офіційному сайті.
Я ж виокремлю найцікавіші моменти:
Усе це дозволило нам прийняти правильне рішення. Duende виявився оптимальним авторизаційним сервером для наших завдань.
Отже, у нас вийшла наведена нижче схема. У продукті є один айдентіті-сервер та кілька фронтендів. На схемі їх три для простоти оцінки моделі, але насправді таких елементів набагато більше.
При цьому ми прибрали API Getway, і фронтенд спілкується безпосередньо з оркестратором. До оркестратору ми додали BFF Security Framework. Усе хоститься на одному домені — це вимога BFF Security Framework. За рахунок цього токен надається в оркестратор, що дозволило побудувати ще й antiforgery-захист фронтенду. Далі оркестратор аутентифікує та авторизує користувача та відправляє реквести на мікросервіс. Усе просто і, що найважливіше, надійно:
Якщо узагальнити результати всієї нашої дослідницької та девелоперської роботи, то ми виграли відразу в кількох моментах:
Звісно, у вашому випадку все може скластися інакше, і ви оберете, скажімо, Keycloak. Більш того, якщо вам не потрібні додаткові механізми редиректів та й загалом немає складної логіки, тоді й Cognito або Azure Active Directory чудово підійдуть. Тому при виборі айдентіті-сервера передусім раджу грамотно оцінювати свої завдання та можливості кожного існуючого авторизаційного сервера.
Блогер та розробник Джозеф Круз розповів, чому не варто писати ідеальний код та чому це…
Днями я завзято нила про щось ChatGPT (експериментую між сеансами з живим терапевтом). І от…
«Крутіть колесо, щоб отримати знижку до 50%!» «Натисніть тут, щоб відкрити таємничу пропозицію!» «Зареєструйтесь зараз,…
Дуже хочеться робити якісь десктопні апки. Сумую за часами коли всі програми були offline-first, і…
Надсилаючи криптовалюту, багато новачків ставлять запитання: як працюють комісії та чому вони відрізняються в різних…
Нова афера набирає обертів — ось детальний розбір того, як фальшиві потенційні роботодавці намагаються вкрасти…