company_banner

Третий лишний: как мы реализовали сбор почты с использованием 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 (например, resource server-ом может быть почтовый сервер, где размещен ящик пользователя).

    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 и Microsoft. Для хранения токенов мы написали внутренний сервис 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. Генерировать токены access и 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.
    Mail.ru Group
    1129.34
    Строим Интернет
    Share post

    Comments 30

      0
      Тем не менее, большинству из нас регулярно приходится сообщать пароль сторонним сервисам


      А можно поподробнее? Не могу припомнить, чтобы я или кто-то из моих знакомых сообщал свой пароль сторонним сервисам.
        +5
        Пользователи сообщают пароли почтовым программам или другим почтовым сервисам при настройке сбора почты.
        Мы хотим прекратить эту практику, для этого сделали шаг со своей стороны.
          –6
          Программа — это не сервис, это продукт. Сервис — то, что предоставляет услуги.
          Автомобиль — продукт, такси — сервис.
            +2
            А как же такие феномены как SaaS?
              –4
              А вы часто предоставляете saas'ам пароль от основной почты?
                +3
                От моего ответа зависит станет ли программа продуктом или сервисом?
            0
            А почтовые клиенты, вроде Thunderbird, умеют OAuth?
            Единственный кейс, которыя я вижу при котором нужен пароль — это гипер-удобные SaaS почтовые клиенты, которые пользователь предпочтет стандартным, однако я таких не знаю, поэтому буду рад ссылочке, если такие существуют.
              +4
              К сожалению, тут есть проблемы, но они потенциально решаемые.

              В настоящее время стандартом де-факто Oauth 2.0 в IMAP и SMTP является расширение SASL XOAUTH2 предложенное Google. Это расширение во-первых не стандартизовано, во-вторых не позволяет полноценного получения токена и не отвечает на вопрос где и как его получать. Т.е. Почтовая программа должна как-то самостоятельно определять где и как получать токен по HTTP. С версии 38.0.1 в Thunderbird вшита поддержка Oauth но только для серверов GMail. Как будет обстоять поддержка с OAuth других почтовых служб (Outlook.com, mail.ru) пока не ясно.

              Имеется черновой стандарт draft-ietf-kitten-sasl-oauth продвигаемый Microsoft, над которым в настоящее время идет работа. Этот стандарт хорош тем, что не зависит от почтовой службы, клиентскому приложению достаточно будет реализовать его. Когда стандарт более-менее устаканится, можно будет реализовывать его поддержку как со стороны серверов, так и со стороны клиентов. Пока он не реализован даже самим Microsoft.
            –5
            Сотрудница по ошибке ввела логин и пароль от почты на Яндексе (собственный домен) в интерфейсе Mail.ru. На что Mail.ru ответил «введите имя и фамилию». После чего стал показывать почту с чужого почтового ящика с другого почтового сервиса. В приличном обществе это называется фишинг и является уголовным преступлением.
              +3
              Интересно, а если бы она ввела логин-пароль в Thunderbird — это тоже фишинг? А если вместо Thunderbird был бы некий сайт, который ведет себя так же как он?
                –2
                Thunderbird — программа почтовый клиент. Mail.ru — почтовый сервис. Не надо делать вид, что между ними нет разницы. Почему-то другие почтовые сервисы сообщают, что введенный неправильно почтовый ящик у них не зарегистрирован.

                PS. Если вы сотрудник Mail.ru, значит точно не идиот, а самый настоящий вредитель и преступник.
                  +1
                  Я думаю ваш сотрудник не знал терминов «почтовый клиент» и «почтовый сервис». Он не делал вид, что между ними нет разницы, возможно потому что он просто не видит разницы.
                  Я конечно всячески приветствую просвещение масс, но вы должны знать: пользователя это все не волнует. У него свои проблемы, свои термины, о которых мы с вами не знаем и не хотим знать. А он просто хочет прочитать почту.
                  Другой пример: возможно, вы бы хотели воспользоваться другим сотовым оператором, не меняя номера телефона. В этом случае вас бы сильно волновало почему DEF-префикс не позволяет вам так вот просто это сделать?
                    –1
                    Можно сколько угодно валять дурака, и делать вид, что это нормально. Но все прекрасно понимают разницу между программой для доступа к произвольному почтовому сервису (которая еще и спрашивает имя сервера для получения и отправки сообщений) и сайтом почтового сервиса. Вы просто самые настоящие воры. Можете минусовать сколько угодно, ворьем вы быть не перестанете.
                      +1
                      Воу воу, полегче. Мы же начинали беседу с того, что происходит «в приличном обществе».
            –1
            Тем не менее, большинству из нас регулярно приходится сообщать пароль сторонним сервисам.

            Нет.
              –2
              Зачем нужны эти все глючащие сборщики почты, если можно просто настроить пересылку (какие-то странные сервисы её ограничивают, но на этот случай спасает фильтр вида «Если тема не содержит dfssghdfsghdflk5$67%s пересылать по адресу») и не следить за актуальностью паролей и т.д? Да и работает пересылка быстрее сборщиков
                +7
                Пересылка может быть несовместима с некоторыми стандартами, например с SPF аутентификацией в DMARC, из-за чего возрастает вероятность, что пересланное письмо попадет под спам-фильтр + пересылка хороша в том случае, если адрес с которого она установлена не используется. OAuth позволяет производить аутентификацию не только получения, но и отправку почты от имени пользователя, не нарушая SPF/DKIM/DMARC. Cборщик почты в таком случае работает как полноценный почтовый клиент.
                  0
                  Более того, при настройке пересылки по IMAP/POP3 требуется авторизация.
                    0
                    Я про сбор*
                +11
                Такой mail.ru мне нравится.
                  0
                  > Общий совет: токены должны быть максимально случайными, рандом должен быть криптографически стойким.

                  UUID подойдет?
                    0
                    да, если при его составлении используется криптографически стойкий генератор псевдослучайных чисел
                    0
                    Для чего нужен обмен кода авторизации на токен доступа? Почему сервер сразу токен не выдает?
                      0
                      т.о. исключается риск компроментации токена через браузер пользователя(история браузера, referer заголовки и т.д. и т.п.)
                        0
                        А чем это отличается от компрометации кода?
                          0
                          код обменивается на токен серверсайд запросом и токен выдается в обмен на code+redirect_uri+client_id+client_secret
                          т.е. есть еще аутентификация клиента(client_secret знает только клиент), которая дополнительно защищает код от компроментации
                            0
                            Ага, понял, спасибо.
                      0
                      В дополнение к замечанию про CSRF и state — также необходимо процесс инициировать только по POST запросу. Иначе есть уязвимость типа RECONNECT (see sakurity.com/blog/2015/03/05/RECONNECT.html). Mailru уязвим, например (click No Referer)
                        0
                        А как POST защитит запрос от CSRF?
                          0
                          В нормальных фреймворках все POST запросы требуют токен. Это де факто стандарт проектировки

                      Only users with full accounts can post comments. Log in, please.