Третій зайвий: як ми реалізували збір пошти з використанням OAuth 2.0



«Може, тобі ще й ключ від квартири, де гроші лежать?» — приблизно так виглядає нормальна реакція людини, у якого сторонній сервіс вимагає пароль від основної пошти. Тим не менш, більшості з нас регулярно доводиться повідомляти пароль стороннім сервісам. Сьогодні я хочу розповісти про те, як ми реалізували процедуру авторизації при зборі листів з наших скриньок через OAuth 2.0, тим самим позбавивши користувачів Mail.Ru від необхідності довіряти «ключі» від своєї пошти третій стороні.

Зазвичай при налаштуванні складальника пошти, поштовий клієнт або стороннього мобільного додатка потрібно вводити ім'я, адресу скриньки та пароль. Найнеприємніше в цій процедурі — введення пароля. Якщо ви дбаєте про безпеку, ви спеціально придумали складний пароль для поштової скриньки і вводили його тільки на сайті сервісу. А зараз вам доводиться довіряти пароль третій стороні, яка буде зберігати його і передавати по мережі. Якщо з передачею все не так страшно (Пошта Mail.Ru підтримує SSL-передачу даних для IMAP-протоколу), то зберігання пароля може бути небезпечно. В якому вигляді зберігається пароль? Можуть вкрасти? Чи може хтось сторонній читати пошту? І тільки до пошті отримує доступ сторонній сервіс? Не видалить він випадково, скажімо, файли з хмари? Користувачі часто задаються такими питаннями.

Уникнути зберігання пароля на сервері стороннього ресурсу. Рішення очевидне: надати всім бажаючим можливість роботи через OAuth 2.0 при зборі пошти Mail.Ru за протоколом IMAP на ящики інших поштових провайдерів, а також при взаємодії з поштовими клієнтами та іншими мобільними додатками. І ми цей крок зробили. А тепер про все по порядку.

Коротко про OAuth
Що в загальних рисах являє собою OAuth? Повна специфікація протоколу описана в RFC 6749. Існує більше одного варіанту авторизації. Наприклад, мобільний додаток отримує доступ до ресурсу дещо інакше, ніж веб-додаток або пристрій. Ми ж для простоти викладу обмежимося приватним випадком веб-додатки.

У OAuth існує кілька ролей.

Resource owner (власник ресурсу) — це користувач, який хоче, щоб ваш додаток могло виконувати дії від його імені.

Resource server — сервер, який обслуговує те, чим володіє resource owner (наприклад, server resource-му може бути поштовий сервер, де розміщений ящик користувача).

Authorization server — сервер, який з боку OAuth-провайдера займається авторизацією. У найпростішому випадку authorization сервер і resource сервер — це одне і те ж, принаймні, з точки зору зовнішнього світу.

Client — в термінології OAuth це веб-додаток, який отримує від користувача доступ до ресурсу. Кожен клієнт повинен бути зареєстрований на сервері авторизації; при цьому він отримує client_id і client_secret. Фактично, це логін і пароль, за якими OAuth-провайдер може ідентифікувати клієнтське додаток. Важливо, що ця пара логін+пароль служить виключно для ідентифікації та жодним чином не збігається з логіном та паролем користувача. Таким чином, користувач ні при яких умовах не передає свій пароль третім особам: обмін цими даними він здійснює тільки з сервером авторизації — це так само безпечно, як увійти в свою поштову скриньку.

Як це працює
Отже, користувач (resource owner) деякого сайту (OAuth-провайдер) хоче передати іншому сайту (client) право працювати з частиною функцій від свого імені. Ця процедура називається в OAuth authorization grant. Для її здійснення клієнт просить користувача перейти на сервер OAuth-провайдера і отримати там access code, передавши певні параметри, про яких мова піде нижче. Технічно це виглядає як перенаправлення в браузері на заздалегідь відомий URL. При переході користувача з цього URL OAuth-провайдер просить користувача авторизуватись і запитує його, чи дійсно варто надати запитуваний доступ даним додатком. Якщо користувач погоджується, OAuth-провайдер перенаправляє браузер користувача назад на сервер клієнта і передає туди код доступу. Після цього клієнт формує спеціальний HTTP-запит для обміну коду авторизації на токен доступу, використовуючи свої client_id, client_secret для аутентифікації клієнта і отриманий код для обміну його на токен доступу (access_token). Запит виконується з server side. Цей маркер буде виконувати для додатка роль пароля для входу в API OAuth-провайдера.



Обмін паролями по протоколу OAuth відбувається тільки між користувачем, який володіє паролем, і єдиним сервером, який може цей пароль перевірити. Користувач вводить пароль на сервері OAuth-провайдера. Клієнтський додаток відправляє client_secret тільки OAuth-провайдера. При цьому провайдер має можливість переконатися, що саме цей користувач дав саме такий рівень доступу саме цього додатка. Додаток отримує доступ, який йому потрібен для роботи, але не знає пароля користувача. Користувач впевнений, що його пароль відомий тільки йому, оскільки ні в які треті руки він свій пароль не повідомляє.

В якості одного з параметрів у стадії authorization grant передається scope. Цей параметр визначає, які саме права хоче отримати додаток. Параметри являють собою рядок, що складається з розділених пробілом послідовностей, зрозумілих OAuth-провайдера. Примітно тут те, що access_token дозволить клієнтського додатку виконувати тільки дії, які були перераховані в параметрі scope. Цей же список дозволів OAuth-провайдер покаже користувачу, перш ніж той підтвердить згоду на передачу даних прав додатком.

Ще один цікавий параметр стадії authorization grant називається state і дозволяє уникнути неочевидною проблеми безпеки. Додаток, перенаправляючи користувача на сайт OAuth-провайдера, генерує випадковий маркер (CSRF-токен) і передає його в параметрі state. OAuth-провайдер нічого з ним не робить, а повертає його назад разом із access code. Додаток звіряє отриманий state з тим, що був відправлений, і перериває стадію authorization grant, якщо state невірний. Якби цього не відбувалося, потенційний зловмисник міг би прийняти наш додаток для доступу до свого ящика і передати свій код авторизації в наш додаток.

Припустимо, прив'язка зовнішнього облікового запису використовується для авторизації зовнішнім ящиком. У цьому випадку зловмисник зможе, залогувавшись у в свій аккаунт, отримати доступ до облікового запису жертви в нашому додатку. Тому всім, хто реалізує роботу з OAuth, ми рекомендуємо використовувати state, незважаючи на те, що цей параметр не є обов'язковим.



У деяких випадках разом з access_token OAuth-провайдер видає клієнту refresh_token. Цей маркер дозволяє отримати новий access_token або навіть кілька. У найпростішому випадку користувач дає дозвіл додатком разово. Наприклад, ваш додаток хоче додати в календар користувача певна подія. Кожен раз, коли це відбувається, користувач отримує запит: чи дозволити додатком виконати обумовлену дію? Якщо він погоджується, видається access_token на невеликий проміжок часу, наприклад, на годину. Якщо завтра ваш додаток спробує додати ще одну подію, доступ буде запитано у користувача повторно. Приблизно так працює App Store в пристроях Apple. Щоб установити програму, необхідно ввести пароль, але в наступні 15 хвилин при встановленні інших додатків цього робити не потрібно. Якщо ж спробувати встановити інший додаток пізніше, ніж через 15 хвилин, доведеться ввести пароль знову.

У ряді ж випадків користувач хоче дати додатком право працювати від його імені завжди. Яскравий приклад — як раз збирачі пошти. Незалежно від того, в онлайні користувач або вирушив у похід з алтайських гір на місяць, складальник повинен забирати пошту з одного або декількох ящиків. Ось у цій ситуації і потрібно refresh_token. Клієнтський додаток може запитати так званий offline-доступ і отримати відповіді refresh_token, а з ним і можливість авторизувати в сервісі OAuth-провайдера без участі користувача, отримуючи все нові і нові access_token-и.

Як ми це робимо: клієнт
Нещодавно ми включили підтримку роботи наших збирачів пошти з використанням OAuth. Тепер ми не змушуємо користувача вводити пароль від поштової скриньки, і, навіть збираючи пошту з ящика в Mail.Ru, складальник по відношенню до поштового сервера виступає в ролі OAuth-клієнта. Ми підтримуємо OAuth для тих сервісів, які дозволяють працювати за цим протоколом, а саме Google і Misrosoft. Для зберігання токенів ми написали внутрішній сервіс Fluor. У її задачі, крім зберігання бази токенів, входить видача їх збирачам і іншим внутрішнім споживачам за запитом з мінімальною затримкою. Обміном згоди користувача на токен з зовнішнього сервісу займається окремий демон, який відповідає за авторизацію. Він проводить користувача через процес видачі необхідних додатком прав (стадія authorization grant) і зберігає отримані токени під Fluor.

Для сервісів, які підтримують refresh_token і обмежують час життя access_token, необхідно своєчасно оновлювати токени в базі. При цьому треба не потрапити під обмеження OAuth-провайдерів за кількістю запитів у добу від однієї програми чи з одного IP. Цим завданням займається демон fluor-refresh. Сімейство демонів Fluor написаний на Perl. Запити до них обробляються асинхронно з використанням бібліотеки AnyEvent. Для взаємодії з OAuth-демоном і збирачами використовується наш власний протокол IPROTO. У нас є свій HTTP-сервер на Perl, але за необхідності парсинга заголовків продуктивність обробки запитів по IPROTO виявляється вище в п'ять разів. Найбільш критичні з точки зору процесора завдання винесені з Perl в XS. XS дозволяє писати частина коду на C і передавати результати його роботи в Perl.

В один момент часу може бути запущено кілька копій Fluor і fluor-refresh. Зберігання токенів і взаємодія між демонами ми організуємо через Tarantool (розроблений теж Mail.Ru, має відкритий вихідний код проект, про який вже не раз писали на Хабре). Tarantool — це NoSQL база даних, цілком розміщена в пам'яті сервера, але дозволяє записувати дані на диск. У Tarantool є реплікація і можливість писати досить складні процедури мовою Lua, що дуже допомагає в організації нашої специфічної черзі на оновлення токенів.

Специфіка черзі в тому, що, по-перше, вона нескінченна (токени треба оновлювати весь час), і, по-друге, завдання черзі повинні бути виконані до настання певного строку, дедлайну. При цьому необхідно стежити, щоб одне завдання у черзі не було взято двома рефрешерами відразу, інакше буде виконана даремна робота і перевищена частота запитів на сторонні сервіси. Всю відповідну логіку ми реалізували на Lua.

Fluor-refresh просто викликає функцію в Tarantool і отримує список квитків для оновлення. Для завдань він отримує свіжий access_token і зберігає його в Tarantool через іншу Lua-функцію. Lua-функції гарантують, що оновлення одного сертифіката не буде доручено кільком рефрешерам, і що завжди будуть вибиратися токени, термін закінчення яких настане в межах заданого інтервалу. Таким чином, ми економимо кілька запитів до бази, які необхідно було б зробити, якби замість Tarantool був, скажімо, memcached.

Якщо все ж станеться так, що токен для даного email не встиг оновитися і закінчився, збирач може попросити Fluor отримати новий access_token негайно, минаючи чергу. Бувають також ситуації, коли користувач відкликає доступ з боку OAuth-провайдера. Протокол OAuth не надає додаткам механізму для оповіщення про таку ситуацію. Ми дізнаємося про проблеми, коли refresh_token перестане працювати. У цьому випадку доводиться видаляти токен, а збирач при цьому переходить в стан extra_auth, яке означає, що у користувача необхідно надіслати запит повторно.

В даний час в базі Fluor зберігається 4.8 млн квитків для різних сервісів, займаючи в пам'яті, 7 Гб. У добу відбувається близько 100 мільйонів оновлень токенів. Разом з тим за добу Fluor обробляє 125 мільйонів запитів від збирачів. Фізично, з цим справляється один сервер, якщо не брати в розрахунок резервування на випадок збоїв.

Як ми це робимо:
У найпростішому випадку OAuth-сервер повинен вміти наступне:
  1. Мати можливість перевіряти авторизацію.
  2. Генерувати токени acsess і refresh, а також код авторизації.
  3. Перевіряти, зберігати, инвалидировать і видаляти токени.
  4. За refresh_token оновлювати access_token, за кодом авторизації видавати refresh_token і access_token.
Перевірка авторизації, як правило, провадиться окремим сервісом. Він авторизує користувача по парі логін + пароль, або за більш складним комбінаціям (наприклад, якщо мова йде про двофакторної аутентифікації). Якщо ви пишете OAuth, цей сервіс у вас вже є.

Генерація токенів. Загальна порада: токени мають бути максимально випадковими, рандом повинен бути криптографічно стійким.

Управління токенами. Кожен з токенів має термін життя і прив'язаний до користувача. Нескладна таблиця в БД дозволить зберігати токени, прив'язку до користувача і час життя. Даних не дуже багато, а швидкість роботи потрібна висока, тому бажана база, яка зберігає дані в оперативній пам'яті. Також знадобиться демон, який буде обходити базу і видаляти застарілі токени.

Видача нових access-токенів за refresh-токена процедура досить банальна, загострювати на ній увагу ми не будемо. Ми для цього використовуємо Tarantool. Він зберігає дані в пам'яті, забезпечує їх цілісність. А найголовніше, він інкапсулює в собі логіку видалення застарілих токенів. Це можна реалізувати на внутрішній Lua-процедури. Ще один цікавий момент — видалення токенів у випадку, якщо користувач змінив пароль. Для цього доведеться дістати всі токени, які прив'язані до користувача. Тут необхідний secondary index, який будується по користувачеві — у Tarantool, на відміну від багатьох інших БД, така можливість є.

Особливості конфігурації системи. Тут важливі три пункти: швидкість роботи, утилізація заліза, відмовостійкість. Швидкість роботи нам забезпечує Tarantool за рахунок взаємодії тільки з оперативною пам'яттю і secondary index. Для утилізації заліза ми шардим Tarantool, що дозволяє максимально використовувати процесорні ядра сервера. Відмовостійкість досягається за рахунок реплікації в різних ДЦ. Реплікація дозволяє перезапускати як окремі демони, так і машини

Отже, сьогодні ми анонсували можливість підключитися до IMAP-протокол поштового сервісу Mail.Ru, використовуючи OAuth-авторизацію. Закликаємо розробників і клієнтів для десктопних і мобільних пристроїв реалізувати її за збір пошти з наших скриньок.

Документація по підключенню доступна на нашому сайт. На даний момент ми, зі свого боку, теж збираємо пошту більш безпечним способом з сервісів, що надають таку можливість, і хочемо, щоб їх кількість збільшувалася. Сподіваємося, що незабаром робота по OAuth 2.0 стане таким же золотим стандартом для поштових сервісів, як робота по HTTPS.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.