62

Android 的 AccountManager 似乎为具有不同 UID 的应用程序获取相同的缓存身份验证令牌 - 这安全吗?它似乎与 OAuth2 不兼容,因为不应该在不同客户端之间共享访问令牌。

背景/上下文

我正在构建一个 Android 应用程序,它使用 OAuth2 对我的服务器(一个 OAuth2 提供程序)的 REST API 请求进行身份验证/授权。由于该应用程序是“官方”应用程序(相对于第三方应用程序),它被认为是受信任的 OAuth2 客户端,因此我使用资源所有者密码流程来获取 OAuth2 令牌:用户(资源所有者)将他的用户名/密码输入应用程序,然后将其客户端 ID 和客户端密码以及用户凭据发送到我的服务器的 OAuth2 令牌端点,以换取可用于进行 API 调用的访问令牌,以及长- live refresh token 用于在过期时获取新的访问令牌。理由是在设备上存储刷新令牌比用户密码更安全。

我正在使用AccountManager来管理设备上的帐户和关联的访问令牌。由于我提供自己的 OAuth2 提供程序,因此我通过扩展AbstractAccountAuthenticator和其他必需组件创建了自己的自定义帐户类型,如本 Android 开发指南中所述并在 SampleSyncAdapter 示例项目中进行了演示。我能够从我的应用程序中成功添加我的自定义类型的帐户,并从“帐户和同步”Android 设置屏幕管理它们。

问题

但是,我担心 AccountManager 缓存和发布身份验证令牌的方式 - 具体来说,用户已授予访问权限的任何应用程序似乎都可以访问给定帐户类型和令牌类型的相同身份验证令牌。

要通过 AccountManager 获取身份验证令牌,必须调用AccountManager.getAuthToken(),其中传递获取身份验证令牌的Account实例和所需的authTokenType. 如果指定帐户和 authTokenType 存在身份验证令牌,并且用户授予对已发出身份验证令牌请求的应用程序的访问权限(通过“访问请求”屏幕)(在这种情况下,请求应用程序的 UID 不匹配)验证者的 UID),然后返回令牌。如果缺少我的解释,这个有用的博客条目解释得很清楚。基于该帖子,并在检查了AccountManagerAccountManagerService的来源之后(一个为 AccountManager 完成繁重工作的内部类)对于我自己来说,似乎每个 authTokenType/account 组合只存储了 1 个 auth 令牌。

因此,如果恶意应用程序知道我的身份验证器使用的帐户类型和 authTokenType(s),它可以调用 AccountManager.getAuthToken() 来访问我的应用程序存储的 OAuth2 令牌,假设用户授予对恶意程序的访问权限,这似乎是可行的。应用程序。

对我来说,问题是 AccountManager 的默认缓存实现是建立在一个范式之上的,如果我们要分层 OAuth2 身份验证/授权上下文,它会将电话/设备视为服务/资源提供者的单个 OAuth2 客户端. 然而,对我来说有意义的范例是每个应用程序/UID 都应该被视为它自己的 OAuth2 客户端。当我的 OAuth2 提供者发出访问令牌时,它是为发送了正确客户端 ID 和客户端密码的特定应用程序发出访问令牌,而不是设备上的所有应用程序。例如,用户可能同时安装了我的官方应用程序(称为应用程序客户端 A)和使用我的 API 的“许可”第三方应用程序(称为应用程序客户端 B)。对于官方客户端 A,我的 OAuth2 提供者可能会发布一个“超级”类型/范围令牌,它授予对我的 API 的公共和私有部分的访问权限,而对于第三方客户端 B,我的提供者可能会发布一个“受限”类型/scope 令牌,仅授予对公共 API 调用的访问权限。应用客户端 B 应该无法获取应用客户端 A 的访问令牌,因为,即使用户为客户端 A 的超级令牌授予客户端 B 的授权,事实仍然是我的 OAuth2 提供者仅打算将该令牌授予客户端 A。

我在这里忽略了什么吗?我认为应该基于每个应用程序/UID(每个应用程序是一个不同的客户端)发布身份验证令牌是合理/实用的,还是每个设备的身份验证令牌(每个设备都是客户端)是标准/接受的实践?

还是我对 / 周围的代码/安全限制的理解存在一些缺陷AccountManagerAccountManagerService以至于这个漏洞实际上并不存在?我已经使用AccountManager我的自定义身份验证器测试了上述客户端 A/客户端 B 场景,我的测试客户端应用程序 B 具有不同的包范围和 UID,能够获取我的服务器为我的测试颁发的身份验证令牌通过传入相同的客户端应用程序A authTokenType(在此期间我被提示“访问请求”授予屏幕,我批准了,因为我是一个用户,因此一无所知)......

可能的解决方案

一个。"Secret" authTokenType
为了获得认证令牌,authTokenType必须知道;是否应该将authTokenType其视为一种客户端机密,以便为给定机密令牌类型颁发的令牌只能由那些知道机密令牌类型的“授权”客户端应用程序获得?这似乎不太安全;在有根设备上,可以检查系统中的表auth_token_typeauthtokensaccounts数据库并检查与我的令牌一起存储的 authTokenType 值。因此,在我的应用程序(以及设备上使用的任何授权第三方应用程序)的所有安装中使用的“秘密”身份验证令牌类型将在一个中心位置公开。至少对于 OAuth2 客户端 ID/秘密,即使它们必须与应用程序一起打包,它们也会分散在不同的客户端应用程序中,并且可能会尝试混淆它们(这总比没有好)以帮助阻止那些会解包/反编译应用程序。

湾。自定义身份验证令牌
根据AccountManager.KEY_CALLER_UIDAuthenticatorDescription.customTokens的文档以及我之前引用的AccountManagerService源代码,我应该能够指定我的自定义帐户类型使用“自定义令牌”并在其中旋转我自己的令牌缓存/存储实现我的自定义身份验证器,其中我可以获取调用应用程序的 UID,以便按 UID 存储/获取身份验证令牌。基本上,我会有一个authtokens像默认实现一样的表,除了会添加一个uid列,以便在u̲i̲d̲,a̲c̲c̲o̲u̲n̲t̲和a̲u̲t̲h̲t̲o̲t̲o̲k̲e̲e̲n̲t̲y̲p̲e̲p̲e̲p̲e̲p̲e̲p̲e̲p̲e̲p̲e̲p̲e̲p̲e̲p̲e̲p̲e̲p̲e̲p̲e̲p̲e̲p̲e̲p̲e̲p̲e̲p̲e̲p̲e̲p̲e̲p̲e̲p̲e̲u̲u̲u̲u̲u̲u̲n̲t̲t̲t̲t̲t̲t̲t̲ 这似乎是比使用“秘密”authTokenTypes 更安全的解决方案,因为这将涉及authTokenTypes在我的应用程序/身份验证器的所有安装中使用相同的解决方案,而 UID 因系统而异,并且不容易被欺骗。除了编写和管理我自己的令牌缓存机制带来的快乐开销之外,这种方法在安全性方面还有哪些缺点?是不是矫枉过正?我真的保护了任何东西,还是我错过了一些东西,即使有了这样的实现,一个恶意应用程序客户端仍然很容易获得另一个应用程序客户端'AccountManagerauthTokenType(s) 不能保证是秘密的(假设所述恶意应用程序不知道 OAuth2 客户端秘密,因此不能直接获取新令牌,而只能希望获得一个已经AccountManager代表授权缓存的令牌应用程序客户端)?

C。发送带有 OAuth2 令牌的客户端 ID/机密
我可以坚持使用AccountManagerService默认令牌存储实现并接受未经授权访问我的应用的身份验证令牌的可能性,但我可以强制 API 请求始终包含 OAuth2 客户端 ID 和客户端机密,除了访问令牌,并在服务器端验证应用程序是首先为其颁发令牌的授权客户端。但是,我想避免这种情况,因为A) AFAIK,OAuth2 规范不需要对受保护的资源请求进行客户端身份验证 - 只需要访问令牌,并且B)我想避免在每个客户端上验证客户端的额外开销要求。

这在一般情况下是不可能的(服务器获得的只是协议中的一系列消息 - 无法确定生成这些消息的代码)。——迈克尔

但是对于 OAuth2 流程中的初始客户端身份验证也是如此,在此期间客户端首次获得访问令牌。唯一的区别是,不仅对令牌请求进行身份验证,对受保护资源的请求也将以相同的方式进行身份验证。(请注意,客户端应用程序将能够通过loginOptions参数 of传递其 c̲l̲i̲e̲n̲t̲ ̲i̲d̲ 和 c̲l̲i̲e̲n̲t̲ ̲s̲e̲c̲r̲e̲t̲ AccountManager.getAuthToken(),根据 OAuth2 协议,我的自定义身份验证器将仅将其传递给我的资源提供者)。


关键问题

  1. 一个应用程序确实可以通过调用具有相同 authTokenType 的 AccountManager.getAuthToken()来获取另一个应用程序的authToken 吗?
  2. 如果这是可能的,这是否是 OAuth2 上下文中有效/实际的安全问题?

    您永远不能依赖给用户的身份验证令牌,该用户仍然是该用户的秘密......因此,Android 在其设计中通过模糊目标忽略这种安全性是合理的——迈克尔

    但是- 我不担心用户(资源所有者)在未经我同意的情况下获取身份验证令牌;我担心未经授权的客户(应用)。如果用户想成为自己受保护资源的攻击者,那么他可以将自己击倒。我的意思是,用户安装我的客户端应用程序,并且无意中安装了一个“冒名顶替者”客户端应用程序,因为它传入了正确的 authTokenType 并且用户是太懒/不知道/急于检查访问请求屏幕。这个类比可能有点过于简单了,但我不认为我安装的 Facebook 应用程序无法读取我的 Gmail 应用程序缓存的电子邮件是“默默无闻的安全”,这与我(用户)对我的手机进行 root 并检查缓存不同内容自己。

    用户需要接受(Android 系统提供的)应用程序访问请求才能使用您的令牌...鉴于此,Android 解决方案似乎还可以 - 应用程序不能在不询问的情况下静默使用用户的身份验证 - Michael

    但是- 这也是授权问题- 为我的“官方”客户端颁发的身份验证令牌是一组受保护资源的关键,该客户端并且只有该客户端被授权。我想有人可能会争辩说,由于用户是这些受保护资源的所有者,如果他接受来自第三方客户端的访问请求(无论是“受制裁的”合作伙伴应用程序还是某些网络钓鱼者),那么他实际上是在授权第三方-请求访问这些资源的一方客户端。但我对此有疑问:

    • 普通用户的安全意识不足,无法胜任做出此决定。我不认为我们应该仅仅依靠用户的判断来点击 Android 的访问请求屏幕上的“拒绝”来防止即使是粗暴的网络钓鱼尝试。当用户收到访问请求时,我的身份验证器可以非常详细并枚举所有类型的敏感受保护资源(只有我的客户应该能够访问),如果他接受请求,用户将授予这些资源,并且在大多数情况下,用户仍然不会意识到并且会接受。在其他更复杂的网络钓鱼尝试中,“冒名顶替”应用程序看起来太“官方”了,用户甚至无法在访问请求屏幕上挑眉。或者,这里'“不要接受这个请求!如果你看到这个屏幕,恶意应用程序正试图访问你的帐户!”希望在这种情况下,大多数用户会拒绝该请求。但是 - 为什么它甚至应该走那么远?如果 Android 只是将身份验证令牌与为其颁发的每个应用程序/UID 的范围隔离,那么这将不是问题。让我们简化一下——即使我只有一个“官方”客户端应用程序,因此我的资源提供者甚至不担心向其他第三方客户端发布令牌,作为开发人员,我应该可以选择对AccountManager,“不!锁定此身份验证令牌,以便只有我的应用程序可以访问。” 如果我沿着“自定义令牌”路线走,我可以做到这一点,但即使在这种情况下,我也无法阻止用户首先看到访问请求屏幕。至少,
    • 甚至 Android 文档也将 OAuth2 视为身份验证(并且可能是授权)的“行业标准”。OAuth2 规范明确指出,访问令牌不得在客户端之间共享或以任何方式泄露。那么,为什么默认的 AccountManager 实现/配置使客户端能够如此轻松地获得与另一个客户端最初从服务获得的相同的缓存身份验证令牌?AccountManager 中的一个简单修复是仅将缓存的令牌重新用于相同的应用程序/UID,它们最初是从服务中获取的。如果给定的 UID 没有本地缓存​​的身份验证令牌,则应从服务中获取。或者至少使它成为开发人员的可配置选项。
    • 在 OAuth 3-legged 流程(涉及用户授予对客户端的访问权限)中,它不应该是服务/资源提供者(而不是操作系统),它会到达A)对客户端和B进行身份验证)如果客户端是有效的,向用户提供授权访问请求?似乎Android(错误地)在流程中篡夺了这个角色。

    但是用户可以明确地允许应用程序重新使用以前对服务的身份验证,这对用户来说很方便。--迈克尔

    但是- 我不认为便利的投资回报率保证安全风险。如果用户的密码存储在用户的帐户中,那么实际上,为用户购买的唯一便利是,无需向我的服务发送 Web 请求以获取实际授权的新的、不同的令牌请求客户端,返回一个本地缓存的未授权给客户端的令牌。因此,用户可以在几秒钟内看到“正在登录...”进度对话框,从而获得轻微的便利,但用户可能会因资源被盗/滥用而造成极大的不便。

  3. 请记住,我致力于A)使用 OAuth2 协议来保护我的 API 请求,B)提供我自己的 OAuth2 资源/身份验证提供程序(而不是使用 Google 或 Facebook 进行身份验证),以及C)利用 Android 的 AccountManager管理我的自定义帐户类型及其令牌,我提出的任何解决方案是否有效?哪个最有意义?我是否忽略了任何优点/缺点?有没有我没有想到的有价值的替代方案?

    [使用] 替代客户端没有试图仅由官方客户端访问的秘密 API;人们会绕过这个。无论用户使用什么(未来)客户端,确保所有面向公众的 API 都是安全的——Michael

    但是- 这不是首先破坏了使用 OAuth2 的主要目的之一吗?如果所有潜在的被授权者都被授权使用相同范围的受保护资源,那么授权有什么用?

  4. 有没有其他人觉得这是一个问题,你是如何解决这个问题的?我做了一些广泛的谷歌搜索,试图找出其他人是否认为这是一个安全问题/担忧,但似乎大多数涉及 Android 的 AccountManager 和身份验证令牌的帖子/问题都是关于如何使用 Google 帐户进行身份验证,而不是具有自定义帐户类型和 OAuth2 提供程序。此外,我找不到任何人担心不同应用程序使用相同的身份验证令牌的可能性,这让我想知道这是否确实是一种可能性/值得关注(请参阅我的前 2 个“关键问题“ 以上所列)。

感谢您的意见/指导!


作为回应...

迈克尔的回答- 我认为我对你的回答的主要困难是:

  1. 我仍然倾向于将应用程序视为服务的独立的、不同的客户端,而不是用户/电话/设备本身是一个“大”客户端,因此,一个已为一个应用程序授权的令牌不应该通过默认,可转让给没有的。似乎您可能暗示将每个应用程序视为不同的客户端是没有实际意义的,因为有可能,

    用户可能正在运行有根手机,并读取令牌,获得对您的私有 API 的访问权限...... [或]如果用户的系统受到威胁(在这种情况下,攻击者可以读取令牌)

    因此,从总体上看,我们应该将设备视为服务的客户端,因为我们无法保证设备本身上的应用程序之间的安全性。如果系统本身已被入侵,则无法保证从该设备向服务发送验证/授权请求。但同样可以说,比如 TLS;如果端点本身不能得到保护,则传输安全是无关紧要的。对于绝大多数没有受到影响的 Android 设备,我认为将每个应用程序客户端视为一个不同的端点会更安全,而不是通过共享相同的身份验证令牌将它们全部归为一个。

  2. 当出现“访问请求”屏幕时(类似于我们在同意和安装之前总是通读的软件用户许可协议),我不相信用户的判断来区分恶意/未经授权的客户端应用程序和非恶意/未经授权的客户端应用程序。
4

4 回答 4

10

这是一个有效/实际的安全问题吗?

对于官方客户端 A,我的 OAuth2 提供者可能会发布一个“超级”类型/范围令牌,授予对我的 API 的公共和私有部分的访问权限

在一般情况下,您永远不能依赖从该用户提供给用户的身份验证令牌。例如 - 用户可能正在运行有根手机,并读取令牌,获得对您的私有 API 的访问权限。如果用户的系统被攻破(在这种情况下攻击者可以读取令牌),则同上。

换句话说,没有任何经过身份验证的用户可以同时访问的“私有”API 这样的东西,因此 Android 在其设计中通过模糊目标忽略这种安全性是合理的。

恶意应用程序...可以访问我的应用程序存储的 OAuth2 令牌

对于恶意应用案例,恶意应用不应该能够使用客户端令牌听起来更合理,因为我们希望 Android 的权限系统能够提供恶意应用的隔离(前提是用户阅读/关心他们的权限)当他们安装它时接受)。但是,正如您所说,用户需要接受(Android 系统提供的)应用程序访问请求才能使用您的令牌。

鉴于此,Android 解决方案似乎没问题 - 应用程序不能在不询问的情况下静默使用用户的身份验证,但用户可以明确允许应用程序重新使用以前的服务身份验证,这对用户来说很方便。

可能的解决方案审查

“秘密” authTokenType ...似乎不是很安全

同意——这只是通过默默无闻的另一层安全;听起来任何希望共享您的身份验证的应用程序无论如何都必须查找 authTokenType 是什么,因此采用这种方法只会让这个假设的应用程序开发人员更加尴尬。

发送带有 OAuth2 令牌的客户端 ID/秘密 ... [以] 验证服务器端应用程序是授权客户端

这在一般情况下是不可能的(服务器获得的只是协议中的一系列消息 - 无法确定生成这些消息的代码)。在这种特定情况下,它可以防止(非根)替代客户端/恶意应用程序的更有限的威胁 - 我对 AccountManager 的评论不够熟悉(对于您的自定义身份验证令牌解决方案也是如此)。

建议

您描述了两种威胁 - 用户不希望访问其帐户的恶意应用程序,以及您(开发人员)不希望使用部分 API 的替代客户端。

  • 恶意应用程序:考虑您提供的服务有多敏感,如果它不比谷歌/推特帐户更敏感,则只需依靠 Android 的保护(安装权限、访问请求屏幕)。如果它敏感,请考虑您使用 Android 的 AccountManager 的限制是否合适。为了有力地保护用户免受恶意使用其帐户的影响,请尝试对危险操作进行两因素身份验证(参见在网上银行中添加新收款人的帐户详细信息)。

  • 替代客户端:没有试图仅由官方客户端访问的秘密 API;人们会绕过这个。无论用户使用什么(未来)客户端,都要确保所有面向公众的 API 都是安全的。

于 2013-01-22T22:00:03.813 回答
2

你的观察是正确的。Authenticator 将使用与安装应用程序相同的 UID 运行。当另一个应用程序连接到 Account manager 并获取此身份验证器的令牌时,它将绑定到您提供的身份验证器服务。它将作为您的 UID 运行,因此新帐户将与此 Authenticator 相关联。当应用程序调用 getAuthToken 时,将发生绑定并且 Authenticator 仍将在相同的 UId 中运行。默认内置权限检查帐户的 UID,以便不同的 Authenticator 无法从不同的 Authenticator 访问另一个帐户。

您可以通过对 addAccount 和 GetAuthToken 使用“调用 UID”来解决此问题,因为客户经理服务会将其添加到捆绑包中。您的身份验证器实现可以检查这一点。

   @Override
    public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account,
            String authTokenType, Bundle loginOptions) throws NetworkErrorException {
        Log.v(
                TAG,
                "getAuthToken() for accountType:" + authTokenType + " package:"
                        + mContext.getPackageName() + "running pid:" + Binder.getCallingPid()
                        + " running uid:" + Binder.getCallingUid() + " caller uid:"
                        + loginOptions.getInt(AccountManager.KEY_CALLER_UID));
          ...
}

我建议遵循授权流程,而不是将客户端密码存储在您的本机应用程序中,因为其他开发人员可以提取该密码。你的应用不是网络应用,不应该有秘密。

添加账号时,也可以查询callingUId。您需要在addAccount相关活动中设置用户数据,活动将作为应用的 UID 运行,因此它可以调用setUserData

getUserDatasetUserData使用内置的 sqllite 数据库,因此您不需要自己构建缓存。您只能存储字符串类型,但您可以解析 json 并存储每个帐户的额外信息。

当不同的第三方应用查询账号并使用您的账号调用getAuthtoken时,您可以在账号的userdata中查看UID。如果未列出调用 UID,您可以执行提示和/或其他操作以获得许可。如果允许,您可以向帐户添加新的 UID。

在应用程序之间共享令牌:每个应用程序通常使用不同的 clientid 注册,它们不应共享令牌。令牌用于客户端应用程序。

存储:AccountManager 未加密您的数据。如果您需要更安全的解决方案,您应该加密令牌然后存储它。

于 2014-03-14T07:52:00.280 回答
1

我面临着同样的应用程序架构问题。

我得到的解决方案是将 oauth 令牌与应用程序供应商令牌(例如 facebook 提供给应用程序的令牌)和设备 ID(android_id)关联/散列。因此,只有经过授权的应用程序才能使用来自客户经理的令牌。

当然,这只是一个新的安全层,但不是防弹的。

于 2014-03-24T08:39:00.047 回答
1

我认为@Michael 完美地回答了这个问题;但是,为了让那些寻求快速答案的人更明智、更简短,我正在写这篇文章。

您对 android 安全性的担忧AccountManager是正确的,但这就是 OAuth 的意义所在,androidAccountManager依赖于它。

换句话说,如果您正在寻找一种非常安全的身份验证机制,这对您来说不是一个好选择。您不应该依赖任何缓存的令牌进行身份验证,因为如果用户设备上存在任何安全漏洞,例如无意中授予入侵者访问权限、运行 root 设备等,它们很容易暴露给入侵者。

在更安全的身份验证系统(例如在线银行应用程序)中,OAuth 的更好替代方案是使用使用公钥和私钥的非对称加密,其中用户每次使用服务时都需要输入密码。然后使用设备上的公钥加密密码并发送到服务器。在这里,即使入侵者知道了加密密码,他也无法对此做任何事情,因为他无法使用该公钥解密它,并且只需要服务器的私钥。

无论如何,如果一个人想要使用AccountManagerandroid 系统并保持高水平的安全性,则可以通过不在设备上保存任何令牌来实现。然后可以像这样覆盖getAuthTokenfrom的方法:AbstractAccountAuthenticator

@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String
        authTokenType, Bundle options) throws NetworkErrorException {
    AuthenticatorManager authenticatorManager = AuthenticatorManager.authenticatorManager;
    Bundle result;
    AccountManager accountManager = AccountManager.get(context);
    // case 1: access token is available
    result = authenticatorManager.getAccessTokenFromCache(account, authTokenType,
            accountManager);
    if (result != null) {
        return result;
    }
    final String refreshToken = accountManager.getPassword(account);
    // case 2: access token is not available but refresh token is
    if (refreshToken != null) {
        result = authenticatorManager.makeResultBundle(account, refreshToken, null);
        return result;
    }
    // case 3: neither tokens is available but the account exists
    if (isAccountAvailable(account, accountManager)) {
        result = authenticatorManager.makeResultBundle(account, null, null);
        return result;
    }
    // case 4: account does not exist
    return new Bundle();
}

在这种方法中,情况 1、情况 2 和情况 4 都不成立,因为没有保存的令牌,即使account存在。因此,只会返回 case 3,然后可以在相关回调中设置它以打开Activity用户输入用户名和密码进行身份验证的情况。

我不确定在此处进一步描述这一点是否走在正确的轨道上,但我在网站上发布的帖子AccountManager可能会有所帮助,以防万一。

于 2016-12-10T11:45:07.373 回答