Учетная запись Android AccountManager отображает один и тот же кэшированный токен аутентификации для приложений с разными UID - это безопасно? Он не кажется совместимым с OAuth2, поскольку токены доступа не должны использоваться совместно между разными клиентами.
Фон/Контекст
Я создаю приложение для Android, которое использует OAuth2 для аутентификации/авторизации запросов REST API на мой сервер, который является поставщиком OAuth2. Поскольку приложение является "официальным" приложением (в отличие от стороннего приложения), он считается доверенным клиентом OAuth2, поэтому я использую поток паролей владельца ресурса для получения токена OAuth2: пользователь (владелец ресурса) вводит его имя пользователя/пароль в приложение, которое затем отправляет свой идентификатор клиента и секрет клиента вместе с учетными данными пользователя на конечную точку маркера OAuth2 на сервере в обмен на токен доступа, который может использоваться для совершения вызовов API, обновленный токен, используемый для получения новых токенов доступа, когда они истекают. Обоснованием является то, что более безопасно хранить токен обновления на устройстве, чем пароль пользователя.
Я использую AccountManager для управления учетной записью и ассоциированным токеном доступа на устройстве. Поскольку я предоставляю свой собственный поставщик OAuth2, я создал свой собственный тип учетной записи, расширив AbstractAccountAuthenticator и другие необходимые компоненты, как объяснено в этом руководстве Android Dev Guide и продемонстрирован в примере примера SampleSyncAdapter. Я могу успешно добавлять учетные записи своего пользовательского типа из своего приложения и управлять ими с экрана настроек Android "Учетные записи и синхронизация".
Проблема
Однако я обеспокоен тем, как AccountManager кэширует и выдает токены auth - в частности, что тот же токен аутентификации для данного типа учетной записи и типа токена представляется доступным для любого приложения, которому предоставлен пользователь доступ.
Чтобы получить токен auth через AccountManager, необходимо вызвать AccountManager.getAuthToken(), передав, помимо прочего, Account экземпляр для получения токена аутентификации и желаемого authTokenType
. Если для указанной учетной записи и authTokenType существует токен аутентификации, и если пользователь предоставляет доступ (через экран "Access Request" на приложение, которое произвело запрос токена аутентификации (в тех случаях, когда идентификатор запрашивающего приложения не соответствует идентификатору аутентификатора), тогда возвращается токен. В случае отсутствия моего объяснения, эта полезная запись в блоге объясняет это очень четко. На основании этого сообщения и после изучения источника AccountManager и AccountManagerService (внутренний класс, который делает тяжелую работу для AccountManager) для себя, кажется, что только 1 токен аутентификации хранится в каждой команде authTokenType/account.
Итак, представляется возможным, что , если злонамеренное приложение знало тип учетной записи и authTokenType (ы), используемые моим аутентификатором, он мог вызывать AccountManager.getAuthToken(), чтобы получить доступ к моему токену OAuth2, хранящемуся в приложении, предполагая, что пользователь предоставляет доступ к вредоносному приложению.
Для меня проблема заключается в том, что реализация кэширования по умолчанию AccountManager построена на парадигме, на которой, если бы мы должны были наложить аутентификационный/авторизационный контекст OAuth2, он рассмотрел бы телефон/устройство как единственный клиент OAuth2 для службы/поставщик ресурсов. Принимая во внимание, что парадигма, которая имеет для меня смысл, заключается в том, что каждое приложение /UID следует рассматривать как свой собственный клиент OAuth2.Когда мой поставщик OAuth2 выдает токен доступа, он выдает токен доступа для этого конкретного приложения, которое отправило правильный идентификатор клиента и секрет клиента, а не все приложения на устройстве. Например, у пользователя может быть как мое официальное приложение (назовите его app Client A), так и "лицензированное" стороннее приложение, которое использует мой API (назовите приложение "Клиент B" ). Для официального клиента A мой провайдер OAuth2 может выдавать маркер типа "супер" /тип, который предоставляет доступ как к публичным, так и к частным частям моего API, тогда как для стороннего клиента B мой провайдер может выдавать "ограниченный" тип /scope, который предоставляет только доступ к общедоступным API-вызовам. Клиент App B не должен быть доступен для получения приложения. Клиентский токен доступа, который, по-видимому, разрешает текущая реализация AccountManager/AccountManagerService.. Даже если пользователь предоставляет разрешение клиенту B для клиента. A super токен, факт остается фактом: мой поставщик OAuth2 предназначен только для предоставления этого токена клиенту A.
Я что-то забыл? Является ли мое убеждение, что токены auth должны быть выпущены на основе каждого приложения /UID (каждое приложение, являющееся отдельным клиентом) рационально/практично, или являются аутентификационными марками для каждого устройства (каждое устройство является клиентом) стандартным/принятым практика?
Или есть некоторые недостатки в моем понимании ограничений кода/безопасности вокруг AccountManager
/AccountManagerService
, так что эта уязвимость фактически не существует? Я протестировал вышеупомянутый сценарий клиента A/Client B с помощью AccountManager
и моего пользовательского аутентификатора, а мое тестовое клиентское приложение B, которое имеет разную область пакета и UID, смогло получить токен аутентификации, который был выпущен моим сервером для моего тестового клиентского приложения A, передав тот же самый authTokenType
(во время которого мне был предложен экран предоставления доступа "Access Request", который я одобрил с тех пор, как я являюсь пользователем и, следовательно, не знаю)...
Возможные решения
а. "Секретный" authTokenType
Чтобы получить токен аутентификации, authTokenType
должен быть известен; должен ли authTokenType
рассматриваться как тип секретности клиента, так что токен, выданный для данного типа секретного токена, может быть получен только теми "авторизованными" клиентскими приложениями, которые знают секретный токен? Это не кажется очень безопасным; на корневом устройстве можно было бы изучить столбец auth_token_type
таблицы authtokens
в базе данных accounts
системы и проверить значения authTokenType, которые хранятся вместе с моими токенами. Таким образом, "секретные" типы токенов аутентификации, используемые во всех установках моего приложения (и любых разрешенных сторонних приложений, используемых на устройстве), будут отображаться в одном центральном месте. По крайней мере, с идентификаторами/секретами идентификаторов OAuth2, даже если они должны быть упакованы вместе с приложением, они распространяются среди разных клиентских приложений, и можно попытаться запутать их (что лучше, чем ничего), чтобы помочь отпугнуть тех, кто распаковать/декомпилировать приложение.
б. Пользовательские маркеры Auth
Согласно документам для AccountManager.KEY_CALLER_UID и AuthenticatorDescription.customTokens, и исходный код AccountManagerService
, на который я ссылался ранее, я должен уметь указать, что мой собственный тип учетной записи использует "пользовательские токены" и включает мою собственную реализацию кэширования/хранения токенов в моем пользовательском аутентификаторе, где я могу получить UID вызывающее приложение в порядке хранения/выборки токенов auth для каждого UID. В принципе, у меня бы была таблица authtokens
, такая как реализация по умолчанию, за исключением того, что был добавлен столбец uid
, чтобы токены были уникально индексированы по идентификатору UID, учетной записи и Auth Token Type (в отличие от типа учетной записи и Auth Token Type). Это похоже на более безопасное решение, чем использование "секретных" authTokenTypes, поскольку это предполагает использование одного и того же authTokenTypes
для всех установок моего приложения/аутентификатора, тогда как UID варьируются от системы к системе и не могут быть легко подделаны. Помимо радостных накладных расходов, связанных с получением и управлением собственным механизмом кэширования токенов, какие недостатки существуют для этого подхода с точки зрения безопасности? Это слишком много? Я действительно что-то защищаю, или мне не хватает чего-то такого, что даже при такой реализации на месте все равно будет достаточно просто, чтобы один клиент вредоносного приложения мог получить еще один токен аутентификации клиента приложения с помощью AccountManager
и authTokenType
(s), которые не гарантируют себя секретными (если предположить, что указанное вредоносное приложение не знает секрет клиента OAuth2 и поэтому не может напрямую получить новый токен, но может надеяться получить тот, который уже был кэширован в AccountManager
от имени авторизованный клиент приложения)?
с. Отправить идентификатор клиента/секретный токен/маркер OAuth2
Я мог бы использовать реализацию хранилища токенов AccountManagerService
по умолчанию и принять возможность несанкционированного доступа к токену моего приложения, но я мог заставить запросы API всегда включать идентификатор клиента OAuth2 и секрет клиента, в дополнение к токену доступа, и убедитесь, что на стороне сервера установлено, что приложение является авторизованным клиентом, для которого в первую очередь был выпущен токен. Тем не менее, я хотел бы избежать этого, потому что A) AFAIK, спецификация OAuth2 не требует аутентификации клиента для защищенных запросов ресурсов - требуется только токен доступа и B) Я бы хотел избежать дополнительных накладных расходов на аутентификацию клиента по каждому запросу.
Это невозможно в общем случае (все серверы - это серия сообщений в протоколе - код, сгенерированный этими сообщениями, не может быть определен). - Майкл
Но то же самое можно сказать об аутентификации первоначального клиента в потоке OAuth2, в течение которого клиенту сначала выдают токен доступа. Единственное различие заключается в том, что вместо аутентификации только на токен-запросе запросы на защищенные ресурсы также будут аутентифицироваться одинаково. (Обратите внимание, что клиентское приложение сможет передать свой идентификатор клиента и секрет клиента через параметр loginOptions
AccountManager.getAuthToken()
, который мой пользовательский аутентификатор просто передаст моему поставщику ресурсов по протоколу OAuth2).
Ключевые вопросы
- Возможно ли, чтобы одно приложение могло получить другое приложение authToken для учетной записи, вызвав AccountManager.getAuthToken() с тем же authTokenType?
-
Если это возможно, - это действительная/практическая проблема безопасности в контексте OAuth2?
Вы никогда не сможете полагаться на токен аутентификации, предоставленный пользователю, оставшийся секретным от этого пользователя... поэтому разумно для Android игнорировать эту безопасность по цели безвестности в своем дизайне - Майкл
НО- Я не беспокоюсь о том, что пользователь (владелец ресурса) получает токен авторизации без моего согласия; Меня беспокоят несанкционированные клиенты (приложения). Если пользователь хочет стать злоумышленником своих защищенных ресурсов, то он может выбить себя. Я говорю, что не должно быть возможным, чтобы пользователь установил мое клиентское приложение и, невольно, клиентское приложение "imposter", которое может получить доступ к токену аутентификации приложения просто потому, что оно передано в правильном authTokenType, и пользователь был слишком ленив/не знал/бросился проверять экран запроса доступа. Эта аналогия может быть немного упрощена, но я не считаю ее "безопасностью по неизвестности", что мое установленное приложение Facebook не может читать электронные письма, кэшированные моим приложением Gmail, которое отличается от меня (пользователя), укореняющего мой телефон и проверяющего кеш содержание.
Пользователю необходимо принять (доступ к системе Android) запрос доступа для использования вашего токена... Учитывая, что решение для Android кажется ОК - приложения не могут молча использовать аутентификацию пользователя без запроса - Майкл
НО. Это также проблема авторизации - токен авторизации, выданный для моего "официального" клиента, является ключом к набору защищенных ресурсов, для которых этот клиент и только тот клиент авторизован. Я полагаю, можно утверждать, что, поскольку пользователь является владельцем этих защищенных ресурсов, если он принимает запрос доступа от стороннего клиента (будь то "спрятанное" партнерское приложение или какой-либо фишер), то он фактически разрешает третьим сторонам, который сделал запрос на доступ к этим ресурсам. Но у меня есть проблемы с этим:
- Средний пользователь недостаточно сознателен, чтобы грамотно принять это решение. Я не думаю, что мы должны полагаться исключительно на суждение пользователя, чтобы нажать "Отклонить" на экране запроса на Android, чтобы предотвратить даже грубую попытку фишинга. Когда пользователю предоставляется запрос на доступ, мой аутентификатор может быть очень подробным и перечислять все типы чувствительных защищенных ресурсов (к которым должен иметь доступ только мой клиент), для которых пользователь будет предоставлять, если он примет запрос, и в большинстве случаев пользователь все равно будет слишком не осведомлен и собирается принять. И в других, более сложных попытках фишинга, приложение "imposter" просто будет выглядеть слишком "официальным", чтобы пользователь даже поднял бровь на экране запроса доступа. Или, здесь более тупой пример - на экране запроса доступа мой аутентификатор может просто сказать: "Не принимайте этот запрос! Если вы видите этот экран, злоумышленное приложение пытается получить доступ к вашей учетной записи!" Надеюсь, в таком случае большинство пользователей отклонят запрос. Но... почему он должен так далеко? Если Android просто сохранил токены авторизации, выделенные для каждого приложения /UID, для которого они были выпущены, тогда это будет не проблема. Пусть упрощается - даже в случае, когда у меня есть только одно "официальное" клиентское приложение, и поэтому мой поставщик ресурсов даже не беспокоится о выдаче токенов другим, сторонним клиентам, как разработчику, я должен иметь возможность сказать AccountManager, "Нет! Блокируйте этот токен авторизации, чтобы доступ только к моему приложению". Я могу сделать это, если я пойду по маршруту "пользовательских токенов", но даже в этом случае я не смогу помешать пользователю сначала предоставить экран запроса доступа. По крайней мере, должно быть лучше документировано, что реализация AccountManager.getAuthToken() по умолчанию возвращает один и тот же токен аутентификации для всех запрашивающих приложений /UID.
- Даже Android-устройства распознают OAuth2 как "отраслевой стандарт "для аутентификации (и, предположительно, авторизации). Спецификация OAuth2 четко указывает, что токены доступа не должны делиться между клиентами или разглашать каким-либо образом. Почему же тогда реализация/конфигурация AccountManager по умолчанию упрощает клиенту получение того же кэшевого токена аутентификации, который был первоначально получен из службы другим клиентом? Простое исправление в AccountManager будет состоять только в повторном использовании кэшированных токенов для того же приложения /UID, под которым они были первоначально получены из службы. Если для данного UID не существует локального кэшированного токена аутентификации, он должен быть получен из службы. Или, по крайней мере, сделать это настраиваемым вариантом для разработчика.
- В потоке OAuth 3-legged (который включает пользователя, предоставляющего доступ клиенту), не предполагается, что он является поставщиком услуг/ресурсов (а не, скажем, ОС), который получает A ) аутентифицировать клиента и B), если клиент действителен, предоставить пользователю запрос доступа к гранту? Похоже, что Android (неправильно) узурпирует эту роль в потоке.
Но пользователь может явно разрешить приложениям повторно использовать предыдущую аутентификацию для службы, которая удобна для пользователя.-- Michael
НО. Я не думаю, что рентабельность инвестиций оправдывает угрозу безопасности. В случаях, когда пароль пользователя хранится в учетной записи пользователя, действительно, единственное удобство, которое покупается для пользователя, заключается в том, что вместо отправки веб-запроса моей службе, чтобы получить новый, отличный токен, который фактически разрешен для запрашивающий клиент, локально кэшированный токен, не авторизированный для клиента, возвращается. Таким образом, пользователь получает небольшое удобство видеть диалог "Подпись в..." на пару секунд меньше, рискуя стать причиной серьезного неудобства пользователя, если его ресурсы будут украдены/использованы неправильно.
-
Помня о том, что я привержен A), используя протокол OAuth2 для защиты моих запросов API, B), предоставляя мой собственный поставщик ресурсов/аутентификации OAuth2 (в отличие от аутентификации, скажем, Google или Facebook) и C), используя Android AccountManager для управления моим настраиваемым типом учетной записи и ее токенами, любое из моих предложенных решений действительно? Какой смысл? Могу ли я игнорировать любые плюсы и минусы? Есть ли полезные альтернативы, о которых я не думал?
[Использовать] Альтернативные клиенты У вас нет секретного API, который пытается быть доступен только для официального клиента; люди обойдутся этим. Убедитесь, что все ваши общедоступные API-интерфейсы защищены независимо от того, какой (будущий) клиент использует - Michael
НО. Разве это не побеждает одну из ключевых целей использования OAuth2 в первую очередь? Какая польза от авторизации, если все потенциальные авторизованные полномочия будут разрешены к той же области защищенных ресурсов?
-
Кто-нибудь еще почувствовал, что это проблема, и как ваша работа вокруг нее?. Я сделал несколько обширных Googling, чтобы попытаться найти, считают ли другие, что это безопасность проблема/проблема, но, похоже, что большинство сообщений/вопросов, связанных с Android AccountManager и токенами аутентификации, - это то, как аутентифицироваться с учетной записью Google, а не с настраиваемым типом учетной записи и поставщиком OAuth2. Более того, я не мог найти никого, кто соглашался бы на возможность того, что один и тот же токен аутентификации используется разными приложениями, что заставляет меня задаться вопросом, действительно ли это действительно/заслуживает внимания (см. Мои первые 2 "Основные вопросы" ", перечисленные выше).
Я ценю ваш ввод/руководство!
В ответ на...
Michael Answer. Я думаю, что основные трудности, которые я испытываю с вашим ответом:
-
Я все еще склонен думать о том, что приложения являются отдельными отдельными клиентами службы, в отличие от самого пользователя/телефона/устройства, являющегося одним "большим" клиентом, и, следовательно, токена, который был разрешен для приложений одно приложение не должно по умолчанию переноситься на тот, у которого его нет. Похоже, вы можете намекнуть, что обсуждать каждое приложение как отдельный клиент из-за возможности того, что
пользователь может работать с корневым телефоном и считывать токен, получая доступ к вашему частному API... [или], если система пользователя была взломана (злоумышленник мог в этом случае прочитать токен)
и поэтому, по большому счету, мы должны рассматривать устройство как клиент сервиса, поскольку мы не можем гарантировать безопасность между приложениями на самом устройстве. Это правда, если сама система была скомпрометирована, тогда не может быть гарантии аутентификации/авторизации запросов, отправляемых с этого устройства на службу. Но то же самое можно сказать, скажем, TLS; безопасность на транспорте не имеет значения, если сами конечные точки не могут быть защищены. И для подавляющего большинства устройств Android, которые не подвержены риску, я считаю, что более безопасно рассматривать каждый клиент приложения как отдельную конечную точку, вместо того, чтобы сбрасывать их все в один, разделяя один и тот же токен аутентификации.
- При представлении экрана "запрос на доступ" (аналогично лицензионному соглашению пользователя программного обеспечения, которое мы всегда тщательно читаем перед согласием и установкой), я не доверяю суждению пользователя, чтобы отличить вредоносное/неавторизованное клиентское приложение от того, которое нет.