image

«В API ВКонтакте для получения ключа доступа используется открытый протокол OAuth 2.0. При этом пользователь не передает логин и пароль приложению, поэтому его аккаунт не может быть скомпрометирован»документация VK API.

«ОАuth — это открытый протокол, предоставляющий простой и безопасный способ авторизации для мобильных, десктопных и веб приложений» — вольный перевод слогана oauth.net.

К сожалению, во многих случаях эти утверждения являются ложными. О том как сделать работу через OAuth более безопасной, как с точки зрения конечного пользователя, так и при реализации собственного OAuth провайдера — читайте под катом. Будут рассмотрены такие аспекты безопасности, которым на текущий момент уделено незаслуженно мало внимания в открытых публикациях.

Материал насыщен специфической терминологией и рассчитан на подготовленного читателя.

От автора


Автор не является экспертом в области информационной безопасности и не претендует на 100% правоту. Эссе написано скорее с целью пробудить в читателе здоровое любопытство и критическое отношение к используемым технологиям и областям их применимости, чем в попытке разжечь пламя войны с какой-либо сложившейся парадигмой. Все нижесказанное в той или иной степени справедливо для других подобных протоколов и способов контроля доступа к публичному API.

Исторические предпосылки


Мы живем в эпоху стремительно развивающихся технологий. Получая в распоряжение что-то новое, мы начинаем активно его эксплуатировать, раздвигать границы применимости, решать все более комплексные и разнообразные задачи, часто при этом руководствуясь методом познания из серии: «Работает и ладно», «Это помогло мне решить ту задачу, значит поможет и с этой», «Этим пользуется много людей, значит это хорошо». Такой подход естественен, так как работает в большинстве ситуаций и позволяет получать результаты не затрачивая излишнее количество времени и усилий, особенно в случае когда результатом нашего труда будут пользоваться небольшое количество людей, и в случае если цена ошибки невелика. Одной из технологий, границы использования которой были раздвинуты подобным образом, на мой взгляд, является OAuth.

Для начала немного истории: если верить википедии, то работа над протоколом началась в ноябре 2006 года, а OAuth версии 1.0 был утвержден 4 декабря 2007 года. То было время когда Firefox начал основательно теснить Internet Explorer не только на компьютерах веб-разработчиков и интернет-гиков, но и на машинах обычных людей; Facebook стал доступен для всех пользователей интернета, появился ВКонтакте, а Gmail открыл регистрацию без инвайтов; мобильный интернет был медленным, а смартфоны — малораспространенными. Таким образом, разработчики стандарта OAuth естественным образом полагали, что браузер — безопасный, доверенный и единственный способ доступа пользователя к интернет-ресурсам. Ответственность же за доверенность и безопасность браузера возлагалась на пользователя (в виде необходимости установки обновлений и слежения за отсутствием вирусов на ПК) и разработчиков (в виде необходимости поставлять эти самые обновления). Такая система была актуальна и достаточно хорошо работала до тех пор, пока не произошел очередной скачок технологий: в июне 2007 года поступает в продажу первый iPhone, затем в сентябре 2008 выходит первая версия ОС Android.

Безопасность пользователей


Почему же выход на рынок (а точнее — повсеместное распространение) мобильных платформ делает предположение о доверенности браузера неактуальным?

Разработчики мобильных платформ предоставили программистам достаточно широкие возможности для написания приложений, в том числе и свободный доступ к стеку TCP/IP. В результате пользователь не может теперь быть уверенным, где же на самом деле он вводит свой логин и пароль, для него не существует способа узнать, действительно ли данная веб-форма открыта с нужного веб-сайта (если например используется WebView), не перехватываются ли нажатия клавиш или введенные данные недобросовестным разработчиком приложения. Более того, разработчики мобильных платформ, неохотно расширяя API для встроенных браузеров, только усугубляют такое положение дел.

Можно возразить, что пользователю надо быть внимательнее, не стоит вводить логин и пароль от своей социальной сети в первом встречном приложении которые вы установили с маркета, пусть даже форма ввода пароля и выглядит как настоящая. И вообще, если у вас запрашивают пароль — это явный признак того, что что-то идет не так, ведь честное приложение наверняка использует SDK авторизации соответствующей социальной сети. Но на практике пользователи так привыкли к кнопочкам «Зайти через ВКонтакте» и «Login with Facebook» у себя в браузерах, что их не смутит, если по такой же кнопке в приложении появится соответственно выглядящая форма для ввода логина и пароля. Даже при том, что они будут мучительно вспоминать когда-то давно измененный и один-два раза введенный пароль. Таким образом проблема фишинга, которая достаточно остро стояла для OAuth даже в случае его использования через браузеры, поднимается на совершенно новый уровень.

Что же на это говорят нам разработчики OAuth? В OAuth 2.0, увидевшем свет в октябре 2012, о мобильных приложениях нет ни слова. В предварительном же варианте документа под названием OAuth 2.0 for Native Apps, появившемся лишь в феврале 2016, разработчикам приложений и OAuth провайдеров предлагается постараться сделать так, чтобы пользователю не приходилось часто вводить пароль или показывать какую-то информацию связанную с аккаунтом с которого был произведен вход. Таким образом пользователь сможет заподозрить что-то неладное в случае со зловредным приложением.

С другой стороны, все крупные OAuth провайдеры в том или ином виде предоставляют SDK для мобильных приложений, которое позволяет авторизовать пользователя через приложение провайдера, а не через OAuth. На первый взгляд все хорошо, но в случае если пользователь не авторизован в приложении провайдера, то предлагается ввести логин и пароль, а в случае если приложение провайдера не установлено, то SDK скорее всего начнет проводить уже знакомую нам OAuth авторизацию, что в свою очередь предоставляет новое поле деятельности для фишинга.

Несмотря на эти проблемы, у осведомленного пользователя интернет-сервисов все не так уж и плохо, если следовать простому правилу: вводить свой пароль и логин только на сайте или приложении OAuth провайдера и только в том случае если пользователь попал туда самостоятельно, а не автоматически. Да, и все это в дополнение к обновлениям, контролю устанавливаемого ПО и, в некоторых случаях, антивирусу.

Проблемы OAuth провайдера


Все становится гораздо интереснее, если мы сами захотим стать (или уже являемся) OAuth провайдером. Допустим, у нас реализован Authorization code flow и API используется только для подконтрольных сервисов — вроде бы пока все неплохо. Затем появилась необходимость сделать доступ для внешних сервисов: «Конечно, без проблем, OAuth ведь для этого и предназначен, разве нет?» — думаем мы. Но тут закрадывается мысль: а какие конкретно внешние сервисы будут нами пользоваться? А вдруг эти сервисы будут на самом деле мобильными приложениями? Как в таком случае приложения смогут гарантировать безопасность своих client_id и client_secret? Как в таком случае мы сможем узнать — не прикидывается ли какое-нибудь зловредное приложение вполне безобидным и от его имени творит всякие безобразия?

К сожалению, OAuth2 не дает ответа на этот вопрос, лишь в пункте 10.1 RFC6749 говорится о том, что запрещено (!!!) использовать пароль (речь идет про client_secret) для аутентификации OAuth клиентов, реализованных в виде мобильных или клиентских приложений, за исключением случая, когда этот пароль (и уникальный client_id) выдаются отдельно для каждой конкретной установки приложения на пользовательское устройство (но нас вряд ли интересует такой случай). Драфт «OAuth для мобильных клиентов» только лишь начиная с версии от 2 марта 2017 (пункты 8.8, 8.9) предлагает использовать App-claimed HTTPS URI Redirection, но не говорит о том, что безопасная версия такого подхода доступна только для iOS 8, Android 6.0 и выше. Такой подход хоть и оберегает пользователя от зловредных приложений, но некоим образом не помогает OAuth провайдеру обнаружить зловредный клиент, завладевший чужим client_id. При этом стандарт не дает никаких рекомендаций по поводу того, как же наша реализация провайдера будет отличать мобильные приложения от серверных. Т.е. фактически, безопасность client_secret целиком в руках клиента, который может быть и вне нашего контроля.

Ну что ж, раз стандарт нам не помощник, то будем пользоваться собственным здравым смыслом. Допустим, нам нужно пускать только серверные приложения — тут все относительно просто: нужно к каждому client_id привязать IP адрес (или несколько) с которого он может отправлять запросы на получение токена. Можно даже предоставить пользовательский интерфейс для заполнения этого списка, главное только контролировать, чтобы этих адресов было не слишком много. А то введет кто-нибудь целую подсеть какого-нибудь хостинг-провайдера, и прости-прощай вся безопасность: злоумышленнику будет достаточно поселиться с клиентом в одном дата-центре.

Хорошо, а что делать, если нужно все-таки предоставлять доступ и мобильным приложениям? В случае если пользование нашим сервисом подразумевает под собой некоторую плату и приложение находится под нашим контролем, то можно задействовать механизм счетов, предоставляемых AppStore и Play Market-ом по факту совершения покупки. Эти счета содержат Bundle ID приложения, привязаны к конкретному пользователю и их можно проверить на стороне сервера, отправив запрос в Apple или Google. Проверку счета можно связать, например, с обновлением access или refresh токена.

Если же наш сервис бесплатен, то этот способ может и не подойти: не смотря на то, что можно совершить «бесплатную» покупку, пользователь приложения должен будет ввести данные своей банковской карточки. Также этот способ не подойдет, если мобильное приложение не принадлежит нам: разработчики платформ не предоставляют механизмов контроля покупок в сторонних приложениях.

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

Если все-таки круг пользователей приложения достаточно широк, и мы не хотим заставлять пользователей совершать покупки и вводить данные своей карточки? Некоторое время назад у нас оставалась только одна возможность: обфускация client_id (client_secret) в нашем приложении и затруднение перехвата нешифрованного трафика в сетевом стеке ОС. Устойчивая ко взлому реализация такой задачи требует как высокого уровня подготовки специалистов, так и больших временных затрат; и таким образом практически недоступна для небольших компаний и программистов-фрилансеров.

К счастью, начиная с некоторого момента, компания Google предоставляет сервис SafetyNet, который позволяет узнать отпечаток ключа, которым было подписано приложение, а также его Bundle ID и проверить эти данные на стороне сервера (увы, при проверке работоспособности сервиса не получилось получить ответ отличный от {"isValidSignature": false}). Этот API поставляется как часть Google Play Services и, теоретически, доступен начиная с Android 2.3 при условии обновления Play сервисов.

Остается открытым вопрос, позволяет ли SafetyNet проверять данные приложений опубликованных другими разработчиками, и, следовательно, можем ли мы использовать его для контроля доступа к нашему API со стороны других приложений? Документация не дает явного ответа на этот вопрос, но написана в ключе, который не подразумевает такого сценария. Также Google Play Services доступны не для всех стран и могут отсутствовать на телефонах китайских производителей.

К сожалению, компания Apple не предоставляет аналогичных сервисов на текущий момент. Но в любом случае, это большой шаг вперед, и в некоторых случаях использование SafetyNet может быть удобнее нежели чем другие механизмы аутентификации клиентских устройств.

Что же делать, если ни один из предложенных вариантов нам не подходит? Тогда нам, как разработчикам публичного API, придется иметь в виду, что достоверно различать приложения клиентов мы не можем, и пользователь фактически не имеет контроля над тем, какие ресурсы API каким приложениям он предоставляет. То есть мы не можем опираться на концепции scope и client_id из OAuth, что составляет примерно половину возможностей, предоставляемых протоколом. Либо, при наличии достаточной компетенции и ресурсов, мы можем внедрить на серверной стороне какие-либо эвристические алгоритмы для определения запросов с поддельным client_id.

Надеюсь, что крупные OAuth провайдеры используют один из последних двух (остальные накладывают слишком жесткие ограничения), предложенных здесь способов обхода проблем OAuth. Косвенно об этом может свидетельствовать к примеру то, что раньше ВКонтакте давал доступ к электронной почте пользователя только определенным доверенным приложениям, теперь же такой запрос включен в публичный API.

Итого


Из всего вышесказанного можно сделать вывод, что OAuth на самом деле очень далек от того уровня безопасности, который мы привыкли подразумевать, используя такие промышленные технологии как TLS или SSH. При реализации OAuth провайдера необходимо очень аккуратно взвесить пользу от его внедрения и все потенциальные проблемы безопасности. Также требуется реализация подходящих механизмов обхода описанных выше проблем, так как ни одна из известных автору библиотек для популярных web-фреймворков их не учитывает. Для доступа к API со стороны мобильных приложений может иметь смысл разработка собственного SDK, использующего наиболее безопасные для пользователя методы взаимодействия с OAuth.

Автор благодарит своих друзей и коллег за конструктивные комментарии и помощь при подготовке данного материала и отдельно — Сергея Маккену за помощь в исследовании возможностей Apple iOS SDK.

Логотип OAuth разработан Chris Messina и распространяется по Creative Commons Attribution ShareAlike 3.0 лицензии.
Поделиться с друзьями
-->

Комментарии (29)


  1. NaHCO3
    13.04.2017 08:00

    Этак мы до кербероса доберёмся. kinit — получил токены — поехали.


  1. NexOtaku
    13.04.2017 08:40
    -2

    Если твой API предоставляет возможность сделать что-то нежелательное от имени пользователя, то это проблема кривого API, а не авторизации OAuth.


    1. lair
      13.04.2017 09:29
      +1

      То есть АПИ соцсети не должен позволять написать пост от имени пользователя?


      1. NexOtaku
        13.04.2017 10:11

        Вы меня не поняли. Объясню по-другому.

        Вы пишете, что «сторонние приложения могут подключиться к API и от имени пользователя что-то сделать».

        Если API написан правильно, то никаких проблем сервису это не доставит. Потому что при правильном API сервису без разницы, какое именно приложение используется пользователь — «родное», «стороннее» или вообще самописное.


        1. lair
          13.04.2017 11:20

          Вы пишете, что «сторонние приложения могут подключиться к API и от имени пользователя что-то сделать».

          Я? Я этого не пишу.


          Потому что при правильном API сервису без разницы, какое именно приложение используется пользователь — «родное», «стороннее» или вообще самописное.

          Технически — без разницы. Но практически разница есть: если вернуться к примеру соцсетей, то про "родное" приложение точно известно, что оно постит именно то, что просил пользователь. А неизвестное стороннее — может постить что угодно, и тем самым совершать то самое "нежелательное", от которого и соцсети, и пользователю хотелось бы защититься.


          1. NexOtaku
            16.04.2017 11:40
            +1

            Это проблема пользователя, что он ставит такие приложения, а не OAuth. Обвинять в этом OAuth — всё равно, что обвинять протокол HTTP в том, что через него взламывают сайты.


  1. http3
    13.04.2017 09:27
    -14

    1. На мобильном в интернеты не выхожу.
    2. На мобильном в интернетах сидят в основном люди, которым нечем себя занять.


    1. http3
      13.04.2017 15:39
      -6

      Минусовали сидящие с мобилочек? :)


      1. olegi
        17.04.2017 11:53

        да. а что?


        1. http3
          18.04.2017 10:29

          Читай п.2 :)


          1. olegi
            18.04.2017 12:25
            +1

            а что там?


  1. Smithson
    13.04.2017 11:02

    Либо я вас не понял, либо вы перегнули палку.
    client_id и client_secret — это, фактически, имя и пароль для авторизации.
    То есть ваш вопрос звучит «как нам узнать, что именем и паролем пользователя не завладел злоумышленник?».
    Общий ответ — никак. Можно придумать двухшаговую авторизацию (по двум независимым каналам; можно десятишаговую, по десяти разным каналам) и немедленно проиграть в удобстве пользования.
    А можно сказать «пользователь сам охраняет свои данные».
    Или я вас понял неверно?


    1. lair
      13.04.2017 11:21

      Проблема в том, что для мобильных (да и вообще любых, находящихся под контролем злоумышленника) приложений нет надежного способа охранять эти данные.


    1. TriAn
      13.04.2017 11:38

      Разумное замечание. Но это только одна из освещаемых проблем.

      Насколько я понимаю, OAuth как раз и создавался «безопасным» в частности в том смысле, что завладеть вашим паролем (client_secret) было невозможно, если клиентом являлось серверное приложение (что на сколько я понимаю, отражено в OAuth 1.0 и в OAuth 2.0 Authorization code flow). Затем появилась необходимость использования не только серверных клиентов, где безопасность client_secret не может быть гарантирована (OAuth 2.0 Implicit flow). Но стандарт не дает никаких рекомендаций о том, как мы можем гарантировать, что клиент действительно серверный и соответственно, что мы можем ему позволить обращаться к какому-то чувствительному в этом смысле API.

      Так же, некоторые популярные библиотеки, реализующие OAuth провайдер вообще не уделяют внимания необходимости разделения таких клиентов, например FOS OAuthServerBundle. Можно сказать, что направление пользователя по правильному пути это не задача библиотеки, но например django-oauth-toolkit делает на этом явный акцент.


    1. NaHCO3
      13.04.2017 12:48

      Так всё очень просто. Если у вас один логин/пароль выдаёт доступ к нескольким серверам, то нельзя его вводить ни на каком из этих сервисов. Нужно проверенное стороннее приложение для получения токенов. А OAuth позволяет встроить ввод логина/пароля в одно из этих приложений, что плохо.

      Если у вас логин/пароль для каждого сервиса разный, то эта тонкость не важна — если ты доверяешь сервису, значит ничего не потеряешь, доверяя его клиенту.


  1. NeoCode
    13.04.2017 11:29

    Кстати а кто нибудь знает когда авторизация через Твиттер перейдет на oauth2?


  1. ildarz
    13.04.2017 12:53

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

    Клиентские сертификаты можно использовать когда угодно. Совершенно ничто не мешает при регистрации пользователя сгенерить на устройстве, с которого он регистрируется, самоподписанный сертификат. Проблема тут не в том, хорошо нам известен круг пользователей или нет, а в том, как этот сертификат защищать и что делать, когда пользователь захочет ходить с нескольких устройств.


    1. lair
      13.04.2017 13:23

      … а если у вас пользователь не регистрируется с устройства?


      1. ildarz
        13.04.2017 14:43

        А как?


        1. lair
          13.04.2017 15:37

          Например, регистрируется через веб, а потом просто использует мобильный клиент для работы с сайтом.


          1. ildarz
            13.04.2017 16:33

            А браузер, чтобы зарегистрироваться через веб, он где запускает? :) А если у нас одно устройство уже зарегистрировано, на втором регистрация (или просто временная работа, в зависимости от ситуации) может идти с подтверждением через первое.

            Есть, конечно, сценарий «регистрируемся через браузер на заведомо недоверенном устройстве» (публичный компьютер, скажем), но тут я вообще не вижу каких-то способов безопасной регистрации.


            1. lair
              13.04.2017 16:57
              +1

              А браузер, чтобы зарегистрироваться через веб, он где запускает?

              Где угодно.


              А если у нас одно устройство уже зарегистрировано, на втором регистрация (или просто временная работа, в зависимости от ситуации) может идти с подтверждением через первое.

              Подтверждением чего? Пользователя? Так это не то, что нам надо, нам надо знать, что клиентское приложение доверенное.


              1. ildarz
                13.04.2017 17:22

                Где угодно.

                Это "где угодно" в контексте обсуждения является устройством. :)


                нам надо знать, что клиентское приложение доверенное

                Не понял. Доверенное в каком смысле, что на сторону данные не сливает? Если клиентское приложение выпускает сам поставщик сервиса (или контролирует тех, кто их пишет), то он, очевидно, знает, доверенное оно или нет. Если же поставщик сервиса просто предоставляет API, под которое кто угодно пишет приложения, и никак эти приложения не контролирует, то очевидно, что вопрос доверия приложению ложится целиком на пользователя — либо верим издателю, либо нет. Разве есть ещё варианты?


                1. lair
                  13.04.2017 17:35
                  +1

                  Это "где угодно" в контексте обсуждения является устройством

                  Почему это вдруг?


                  Если клиентское приложение выпускает сам поставщик сервиса (или контролирует тех, кто их пишет), то он, очевидно, знает, доверенное оно или нет.

                  … и как нам определить, что к нам обратилось именно то приложение, которое мы выпустили?


                  вопрос доверия приложению ложится целиком на пользователя — либо верим издателю, либо нет. Разве есть ещё варианты?

                  Вот это и обсуждается в статье. И пока что вариантов особо нет, что и плохо.


                  1. ildarz
                    13.04.2017 18:14

                    Почему это вдруг?

                    Потому что браузеру надо на чем-то исполняться.


                    С приложениями я наконец понял, о чем вы, просто это немного вне контекста моего исходного комментария о сертификатах. :) Если клиент у нас ходит из неконтролируемой среды, то в целом по барабану, делается ли аутентификация по сертификату или чему-то ещё, проблемы будут схожие.


                    1. lair
                      13.04.2017 18:19
                      +1

                      С приложениями я наконец понял, о чем вы, просто это немного вне контекста моего исходного комментария о сертификатах.

                      Я боюсь, что тогда ваш исходный комментарий о сертификатах немного вне контекста поста. Потому что пост как раз о неконтролируемой (точнее, контролируемой злоумышленником) среде.


                      1. ildarz
                        14.04.2017 10:28

                        Эмм… это ведь именно в статье сертификаты преподносятся как решение для сред с ограниченным кругом пользователей, а я наоборот сразу написал, а потом и ещё и уточнил , что, хотя использовать их можно их где угодно, вопрос защиты в неконтролируемой среде они никак не решают. Ладно, раз мы теперь во всем согласны, замнем для ясности. :))


    1. TriAn
      13.04.2017 13:54

      Когда круг пользователей хорошо контролируется, то мы можем развернуть доверенный центр сертификации и выдавать проверяемые сертификаты либо пользователям, либо приложениям (клиентам). Так же при наличии соответствующей Enterprise инфраструктуры можно устанавливать и обновлять сертификат пользователя на нескольких пользовательских устройствах.

      К сожалению, самоподписанные сертификаты сложно контролировать и врядли они будут полезны в этом случае.

      А если говорить о генерации чего-либо во время регистрации, то можно просто генерировать client_id и client_secret, но это, к сожалению, не решает описанных в статье проблем.


      1. ildarz
        13.04.2017 14:43

        PKI тут не нужна и даже совершенно лишняя. Клиент привязан к конкретному сертификату, что именно нам нужно проверять у третьей стороны? Технически это то же самое, что аутентификация в SSH по ключам, например.


        Повторюсь, что основная проблема, которую я тут вижу — защита этого самого сертификата, который лежит на стороне клиента (и да, она в произвольном случае не решается). А у энтерпрайза тут главное преимущество не в том, что круг пользователей контролируется, а в том, что круг устройств контролируется (если же устройства не контролируются, то хоть публичный сектор, хоть энтерпрайз, проблемы будут те же).