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),然后返回令牌。如果缺少我的解释,这个有用的博客条目解释得很清楚。基于该帖子,并在检查了AccountManager和AccountManagerService的来源之后(一个为 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(每个应用程序是一个不同的客户端)发布身份验证令牌是合理/实用的,还是每个设备的身份验证令牌(每个设备都是客户端)是标准/接受的实践?
还是我对 / 周围的代码/安全限制的理解存在一些缺陷AccountManager
,AccountManagerService
以至于这个漏洞实际上并不存在?我已经使用AccountManager
我的自定义身份验证器测试了上述客户端 A/客户端 B 场景,我的测试客户端应用程序 B 具有不同的包范围和 UID,能够获取我的服务器为我的测试颁发的身份验证令牌通过传入相同的客户端应用程序A authTokenType
(在此期间我被提示“访问请求”授予屏幕,我批准了,因为我是一个用户,因此一无所知)......
可能的解决方案
一个。"Secret" authTokenType
为了获得认证令牌,authTokenType
必须知道;是否应该将authTokenType
其视为一种客户端机密,以便为给定机密令牌类型颁发的令牌只能由那些知道机密令牌类型的“授权”客户端应用程序获得?这似乎不太安全;在有根设备上,可以检查系统中的表auth_token_type
列authtokens
accounts
数据库并检查与我的令牌一起存储的 authTokenType 值。因此,在我的应用程序(以及设备上使用的任何授权第三方应用程序)的所有安装中使用的“秘密”身份验证令牌类型将在一个中心位置公开。至少对于 OAuth2 客户端 ID/秘密,即使它们必须与应用程序一起打包,它们也会分散在不同的客户端应用程序中,并且可能会尝试混淆它们(这总比没有好)以帮助阻止那些会解包/反编译应用程序。
湾。自定义身份验证令牌
根据AccountManager.KEY_CALLER_UID和AuthenticatorDescription.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 因系统而异,并且不容易被欺骗。除了编写和管理我自己的令牌缓存机制带来的快乐开销之外,这种方法在安全性方面还有哪些缺点?是不是矫枉过正?我真的保护了任何东西,还是我错过了一些东西,即使有了这样的实现,一个恶意应用程序客户端仍然很容易获得另一个应用程序客户端'AccountManager
authTokenType
(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 协议,我的自定义身份验证器将仅将其传递给我的资源提供者)。
关键问题
- 一个应用程序确实可以通过调用具有相同 authTokenType 的 AccountManager.getAuthToken()来获取另一个应用程序的authToken 吗?
如果这是可能的,这是否是 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 请求以获取实际授权的新的、不同的令牌请求客户端,返回一个本地缓存的未授权给客户端的令牌。因此,用户可以在几秒钟内看到“正在登录...”进度对话框,从而获得轻微的便利,但用户可能会因资源被盗/滥用而造成极大的不便。
请记住,我致力于A)使用 OAuth2 协议来保护我的 API 请求,B)提供我自己的 OAuth2 资源/身份验证提供程序(而不是使用 Google 或 Facebook 进行身份验证),以及C)利用 Android 的 AccountManager管理我的自定义帐户类型及其令牌,我提出的任何解决方案是否有效?哪个最有意义?我是否忽略了任何优点/缺点?有没有我没有想到的有价值的替代方案?
[使用] 替代客户端没有试图仅由官方客户端访问的秘密 API;人们会绕过这个。无论用户使用什么(未来)客户端,确保所有面向公众的 API 都是安全的——Michael
但是- 这不是首先破坏了使用 OAuth2 的主要目的之一吗?如果所有潜在的被授权者都被授权使用相同范围的受保护资源,那么授权有什么用?
有没有其他人觉得这是一个问题,你是如何解决这个问题的?我做了一些广泛的谷歌搜索,试图找出其他人是否认为这是一个安全问题/担忧,但似乎大多数涉及 Android 的 AccountManager 和身份验证令牌的帖子/问题都是关于如何使用 Google 帐户进行身份验证,而不是具有自定义帐户类型和 OAuth2 提供程序。此外,我找不到任何人担心不同应用程序使用相同的身份验证令牌的可能性,这让我想知道这是否确实是一种可能性/值得关注(请参阅我的前 2 个“关键问题“ 以上所列)。
感谢您的意见/指导!
作为回应...
迈克尔的回答- 我认为我对你的回答的主要困难是:
我仍然倾向于将应用程序视为服务的独立的、不同的客户端,而不是用户/电话/设备本身是一个“大”客户端,因此,一个已为一个应用程序授权的令牌不应该通过默认,可转让给没有的。似乎您可能暗示将每个应用程序视为不同的客户端是没有实际意义的,因为有可能,
用户可能正在运行有根手机,并读取令牌,获得对您的私有 API 的访问权限...... [或]如果用户的系统受到威胁(在这种情况下,攻击者可以读取令牌)
因此,从总体上看,我们应该将设备视为服务的客户端,因为我们无法保证设备本身上的应用程序之间的安全性。如果系统本身已被入侵,则无法保证从该设备向服务发送验证/授权请求。但同样可以说,比如 TLS;如果端点本身不能得到保护,则传输安全是无关紧要的。对于绝大多数没有受到影响的 Android 设备,我认为将每个应用程序客户端视为一个不同的端点会更安全,而不是通过共享相同的身份验证令牌将它们全部归为一个。
- 当出现“访问请求”屏幕时(类似于我们在同意和安装之前总是通读的软件用户许可协议),我不相信用户的判断来区分恶意/未经授权的客户端应用程序和非恶意/未经授权的客户端应用程序。