«В 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 провайдером. Допустим, у нас реализован autorization 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 лицензии.
Автор: Антон Андерсен