不应该Android AccountManager在每个App / UID基础上存储OAuth令牌吗?

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

背景/上下文

我正在构build一个Android应用程序,该应用程序使用OAuth2将REST API请求authentication/授权给我的服务器,这是一个OAuth2提供程序。 由于该应用程序是“官方”应用程序(而不是第三方应用程序),因此它被视为受信任的OAuth2客户端,所以我正在使用资源所有者密码stream获取OAuth2令牌:用户(资源所有者)将他的用户名/密码input到应用程序中,然后将其客户端ID和客户端密码连同用户凭证一起发送到我的服务器的OAuth2令牌端点,以交换可用于进行API调用的访问令牌,当刷新令牌到期时用于获取新的访问令牌。 理由是在设备上存储刷新令牌比用户的密码更安全。

我正在使用AccountManagerpipe理设备上的帐户和关联的访问令牌。 由于我提供了自己的OAuth2提供程序,因此我通过扩展AbstractAccountAuthenticator和其他必需的组件来创build自己的自定义帐户types,如本Android Dev Guide中所述,并在SampleSyncAdapter示例项目中演示。 我可以在我的应用程序中成功添加自定义types的帐户,并通过“帐户和同步”Android设置屏幕pipe理这些帐户。

问题

但是,我关心的是AccountManagercaching和发布授权令牌的方式,具体来说, 对于给定的账户types和令牌types同一个授权令牌似乎可以被用户授权访问的任何应用访问。

要通过AccountManager获得授权令牌,必须调用AccountManager.getAuthToken() ,除其他外,传递为其获取授权令牌和期望的authTokenType的Account实例。 如果指定帐户和authTokenType存在authentication令牌,并且用户授予对已经作出authentication令牌请求的应用的访问(通过授予“访问请求”屏幕)(在请求应用的UID不匹配的情况下authentication者的UID),那么令牌被返回。 如果我的解释缺乏, 这个有用的博客条目解释得非常清楚。 基于这篇文章,在考察了AccountManager和AccountManagerService (一个为AccountManager负责的内部类)的来源之后,看起来每个authTokenType /账户组合只有一个授权令牌存储。

所以, 如果恶意应用程序知道我的身份validation器使用的帐户types和authTokenType,则可能调用AccountManager.getAuthToken()来访问我的应用程序存储的OAuth2令牌,假设用户授予访问恶意应用程序。

对我来说,问题是AccountManager的默认caching实现build立在一个范例上,如果我们要分层的OAuth2authentication/授权上下文,它会认为电话/设备是服务/资源提供者的单个OAuth2客户端。 然而,对我来说有意义的范例是每个应用程序/ UID应被视为自己的OAuth2客户端。 当我的OAuth2提供程序发出访问令牌时,会为该特定应用程序发出访问令牌,该令牌已发送正确的客户端ID和客户端密钥,而不是设备上的所有应用程序。 例如,用户可能同时拥有我的官方应用程序(称为应用程序客户端A)和使用我的API(称为应用程序客户端B)的“许可”第三方应用程序。 对于官方客户端A,我的OAuth2提供者可能会发出一个“超级”types/范围令牌,授权访问我的API的公共和私有片段,而对于第三方客户端B,我的提供者可能会颁发“受限”types/范围令牌,只允许访问公共API调用。 应用程序客户端B不可能获得应用程序客户端A的访问令牌,目前的AccountManager / AccountManagerService实现似乎允许。 因为,即使用户向客户端A的超级令牌授予客户端B的授权,事实仍然是,我的OAuth2提供者仅仅意图将该令牌授予给客户端A.

我在这里忽略了什么? 我认为应该以每个应用程序/ UID为基础(每个应用程序是一个独立的客户端)发布授权令牌,理性/实用,或者是每个设备的授权令牌(每个设备是客户端)的标准/接受实践?

或者,在我对AccountManager / AccountManagerService周围的代码/安全性限制的理解中是否存在一些缺陷,使得这个漏洞实际上不存在? 我已经使用AccountManager和我的自定义身份validation器testing了上述客户端A /客户端Bscheme,而我的testing客户端应用程序B具有不同的包范围和UID,能够获得我的服务器为我发放的身份validation令牌testing客户端应用程序A通过传入相同的authTokenType (在此期间,我被提示“访问请求”授予屏幕,我批准,因为我是一个用户,因此无能为力)…

可能的解决scheme

一个。 “秘密”authTokenType
为了获得auth令牌,必须知道authTokenType ; 应该将authTokenType作为一种客户端秘密来处理,使得只有那些知道秘密令牌types的“已授权”的客户端应用程序才能获得给定秘密令牌types的令牌? 这似乎不是很安全; 在根设备上,可以检查系统accounts数据库中authtokens表的auth_token_type列,并检查与我的令牌一起存储的authTokenType值。 因此,在我的应用程序的所有安装(以及设备上使用的任何授权的第三方应用程序)中使用的“秘密”身份validation令牌types将已经暴露在一个中心位置。 至less在OAuth2客户端ID /秘密中,即使它们必须与应用程序打包在一起,也会分布在不同的客户端应用程序中,并且可能会尝试对其进行混淆(这总比没有好),以帮助阻止那些愿意解压缩/反编译的应用程序。

自定义身份validation令牌
根据AccountManager.KEY_CALLER_UID和AuthenticatorDescription.customTokens的文档以及前面引用的AccountManagerService源代码,我应该能够指定我的自定义帐户types使用“自定义令牌”,并在我的自定义内部自定义令牌caching/存储实现authenticator,其中我可以获得调用app的UID,以便在每个UID的基础上存储/获取授权令牌。 基本上,我会有一个像默认实现authtokens表,除了会有一个添加的uid列,以便在UID,帐户和身份validation令牌types(而不仅仅是帐户和身份validation令牌types)唯一索引。 这似乎是一个比使用“秘密”authTokenTypes更安全的解决scheme,因为这将涉及在我的应用程序/身份validation器的所有安装使用相同的authTokenTypes ,而UID因系统而异,不能轻易欺骗。 除了编写和pipe理我自己的令牌caching机制的快乐开销外,在安全性方面还有什么缺点? 这是否过分了? 我是否真正保护了任何东西,还是我错过了一些东西,即使有了这样的实现,一个恶意应用程序客户端仍然可以很容易地使用AccountManagerauthTokenType (s)获得另一个应用程序客户端的身份validation令牌保证是秘密的(假设所述恶意应用程序不知道OAuth2客户机秘密,因此不能直接得到一个新的令牌,但只能希望获得已经在AccountManager中代表授权应用程序客户机caching的那个)。

C。 发送客户端ID /秘密w / OAuth2令牌
我可以坚持AccountManagerService的默认令牌存储实现,并接受未经授权的访问我的应用程序的身份validation令牌的可能性,但我可以强制API请求始终包括OAuth2客户端ID和客户端密钥,以及访问令牌validation服务器端的应用程序是授权的客户端,首先发布令牌。 但是,我想避免这种情况,因为A) AFAIK,OAuth2规范不要求受保护的资源请求的客户端身份validation – 只有访问令牌是必需的,和B)我想避免额外的身份validation客户端开销请求。

这在一般情况下是不可能的(所有的服务器都是协议中的一系列消息 – 产生这些消息的代码无法确定)。 迈克尔

但是在客户端首次发布访问令牌的OAuth2stream程中的初始客户端身份validation也可以这样说。 唯一的区别是,不仅仅是对令牌请求进行authentication,对受保护资源的请求也将以相同的方式进行authentication。 (请注意,客户端应用程序将能够通过AccountManager.getAuthToken()loginOptionsparameter passing其客户端ID和客户端密钥,我的自定义身份validation器将通过OAuth2协议传递给我的资源提供者)。


关键问题

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

    你永远不能依赖授予给该用户剩余秘密的授权令牌,所以Android在其devise中通过默默无闻的目标来忽略这种安全性是合理的 – 迈克尔

    但是 – 我不关心用户(资源所有者)未经我的同意而获得授权令牌; 我担心未经授权的客户端 (应用程序)。 如果用户想成为他自己受保护资源的攻击者,那么他可以把自己击倒。 我在说,用户不应该有可能安装我的客户端应用程序,而且不知不觉中,一个“冒名顶替”客户端应用程序能够访问我的应用程序的身份validation令牌,只是因为它传递了正确的authTokenType,而用户是懒得/不知道/冲到检查访问请求屏幕。 这个比喻可能有点过于简单了,但我不认为我安装的Facebook应用程序无法读取我的Gmail应用程序caching的电子邮件,这与我的(用户)根植我的电话并检查caching不同内容我自己。

    用户需要接受(Android系统提供的)访问请求的应用程序使用您的令牌…鉴于此,Android解决scheme似乎确定 – 应用程序不能默默地使用用户的身份validation,而不问 – 迈克尔

    但是 – 这也是一个授权问题 – 为我的“官方”客户端颁发的身份validation令牌是一组受保护资源的关键,并且只有该客户端被授权。 我想有人可能会说,既然用户是这些受保护资源的所有者,如果他接受来自第三方客户端的访问请求(不pipe它是“被强制执行的”合作伙伴应用程序还是某个networking钓鱼者),那么他实际上是在授权第三方请求访问这些资源的第三方客户端。 但我有这个问题:

    • 普通用户的安全意识不足以胜任这个决定。 我不相信只要依靠用户的判断,在Android的访问请求屏幕上点击“拒绝”即可防止粗糙的networking钓鱼尝试。 当用户出现访问请求时,我的身份validation器可能会超级详细,并枚举所有types的敏感受保护资源(只有我的客户端应该能够访问),如果用户接受请求,并且在大多数情况下,用户仍然不知道并且将会接受。 而在其他更复杂的networking钓鱼尝试中,“冒名顶替”应用程序只是看起来太“官方”,用户甚至可以在访问请求屏幕上扬起眉毛。 或者,这里是一个更直接的例子 – 在访问请求屏幕上,我的身份validation人可以简单地说: “不要接受这个请求!如果您看到这个屏幕,恶意应用程序正试图访问您的帐户! 希望在这种情况下,大多数用户会拒绝这个请求。 但是 – 为什么它应该得到那么多? 如果Android仅仅将validation令牌保留在每个应用程序/ UID的发布范围之内,那么这就不成问题了。 让我们来简化 – 即使在我只有一个“官方”客户端应用程序的情况下,因此我的资源提供者甚至不担心将令牌发放给其他第三方客户端,作为开发者,我应该可以select对AccountManager,“不!locking这个authentication令牌,以便只有我的应用程序有权访问。 如果沿着“自定义令牌”路线行驶,我可以做到这一点,但即使在这种情况下,我也无法阻止用户首先出现访问请求屏幕。 至less应该更好地loggingAccountManager.getAuthToken()的默认实现将为所有请求的应用程序/ UID返回相同的身份validation令牌。
    • 即使Android文档也认可OAuth2作为authentication的“ 行业标准 ”(可能是授权)。 OAuth2规范明确规定,访问令牌不得在客户之间共享或以任何方式泄露。 那么为什么默认的AccountManager实现/configuration使得客户端可以很容易地获得最初由另一个客户端从服务获得的相同的caching授权令牌? 在AccountManager中的一个简单的修复将是只重用caching的令牌为它们最初从服务获得的相同的应用程序/ UID。 如果没有本地caching​​的授权令牌可用于给定的UID,那么它应该从服务中获得。 或者至less使这是开发人员可configuration的选项。
    • 在OAuth 3分支stream程(涉及到用户授予对客户端的访问权限)中,不应该是服务/资源提供者(而不是操作系统),而这个服务器/资源提供者需要A)authentication客户端和B ) 如果客户端有效,向用户提供授权访问请求? 看起来像Android是(错误地)在stream程中篡夺这个angular色。

    但是,用户可以明确地允许应用程序重新使用以前的身份validation服务,这对用户很方便

    但是 – 我不认为便利的投资回报率是安全风险。 在用户密码存储在用户帐户中的情况下,真正为用户购买的唯一便利是不是向我的服务发送一个Web请求来获得实际上被授权的新的不同的令牌请求客户端返回未被授权给客户端的本地caching的令牌。 因此,用户在稍微看到“正在login…”进程对话框的情况下获得稍微方便的几秒钟的风险,由此可能导致用户因资源被盗/误用而受到重大的不便。

  3. 请记住,我致力于A)使用OAuth2协议保护我的API请求, B)提供我自己的OAuth2资源/身份validation提供程序(而不是使用Google或Facebook进行身份validation),以及C)使用Android的AccountManagerpipe理我的自定义帐户types和它的令牌,我的build议的解决scheme是否有效? 哪个最有意义? 我忽略了任何优点/缺点? 我有没有想到的有价值的select?

    [使用]另类客户端不要有一个秘密的API试图只被官方客户端访问; 人们会绕过这个。 确保所有面向公众的API都是安全的,不pipe用户使用什么(将来)客户端 – 迈克尔

    但是 – 这不是打败了首先使用OAuth2的关键目的之一? 如果所有潜在授权人都被授权到相同范围的受保护资源,授权有多好?

  4. 有没有人觉得这是一个问题,你的工作是怎么样的? 我已经做了一些广泛的谷歌search,试图找出其他人是否觉得这是一个安全问题/关注,但似乎涉及Android的AccountManager和身份validation令牌的大多数post/问题是关于如何使用Google帐户进行身份validation,而不是使用自定义帐户types和OAuth2提供程序。 此外,我找不到任何人认为是否有可能使用不同的应用程序使用相同的身份validation令牌,这让我怀疑这是否确实是一个可能性/值得关注的问题(请参阅我的前两个关键问题“ 以上所列)。

我感谢你的input/指导!


回应…

迈克尔的答案 – 我认为我的答案的主要困难是:

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

    用户可能正在运行根深蒂固的电话,并读取令牌,获得对您的私有API的访问权限…… [或]如果用户的系统受到攻击(攻击者可以在此情况下读取令牌)

    因此,在事物的macros伟计划中,我们应该考虑将设备作为服务的客户端,因为我们无法保证设备本身之间的应用程序之间的安全性。 如果系统本身已经被破坏,那么这是真的,那么就不能保证对从该设备发送到服务的请求进行authentication/授权。 但是也可以说,TLS; 如果端点本身不能保证,交通安全是无关紧要的。 对于绝大多数Android设备而言,我认为将每个应用程序客户端视为一个独立的端点是更为安全的,而不是通过共享相同的身份validation令牌将它们集中在一起。

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

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

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

在一般情况下,您永远不能依赖授予给该用户剩余机密的用户身份validation令牌。 例如 – 用户可以运行一个根植的电话,并读取令牌,获得访问您的私人API。 同样,如果用户的系统被攻破(攻击者可以在这种情况下读取令牌)。

换句话说,没有任何一个“私有”API可以同时被任何authentication的用户访问,所以Android在devise中通过隐藏目标来忽略这种安全是合理的。

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

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

鉴于此,Android解决scheme似乎没有问题 – 应用程序无需默默地使用用户的身份validation,但用户可以明确地允许应用程序重新使用以前的身份validation服务,这对用户非常方便。

可能的解决scheme回顾

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

同意 – 这是通过默默无闻的另一层安全。 这听起来像任何应用程序希望分享您的身份validation将不得不查找什么authTokenType无论如何,所以采用这种方法只是使这个假设的应用程序开发人员更尴尬。

发送客户端ID /秘密w / OAuth2令牌… [以]validation服务器端,该应用程序是授权的客户端

这在一般情况下是不可能的(所有的服务器都是协议中的一系列消息 – 产生这些消息的代码无法确定)。 在这个特定的例子中,它可以防止(非根)替代客户端/恶意应用程序的更有限的威胁 – 我不熟悉帐户pipe理器评论(同样为您的自定义validation令牌解决scheme)。

build议

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

  • 恶意应用程序:考虑您提供的服务有多敏感,如果不是比Google / Twitter帐户更敏感,只需依靠Android的保护(安装权限,访问请求屏幕)即可。 如果它更敏感,考虑你的使用Android的AccountManager的约束是否合适。 要强烈保护用户免受恶意使用他们的帐户,请尝试两个因素的身份validation危险的行动(比较在网上银行添加一个新的收件人的帐户详细信息)。

  • 另类客户:没有一个秘密的API试图只能被官方客户访问; 人们会绕过这个。 确保所有面向公众的API都是安全的,无论用户使用什么(将来)客户端。

你的观察是正确的。 身份validation器将使用与安装应用程序相同的UID运行。 当另一个应用程序连接到帐户pipe理器并获得该authentication器的令牌时,它将绑定到您提供的authentication器服务。 它将作为你的UID运行,所以新帐户将与这个Authenticator有关。 当应用程序调用getAuthToken时,将发生绑定,并且Authenticator仍将在相同的UId中运行。 内置默认权限检查帐户的UID,以便不同的authentication者不能访问来自不同的authentication者的另一个帐户。

您可以使用“Calling UID”for addAccount和GetAuthToken来解决此问题,因为帐户pipe理器服务会将其添加到绑定中。 您的validation器实施可以检查。

  @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)); ... } 

我build议遵循授权stream程,而不是将客户机密钥存储在本地应用程序中,因为其他开发人员可以提取该秘密。 你的应用程序不是一个Web应用程序,不应该有秘密。

在添加帐户时,您也可以查询调用UI。 您需要在您的addAccount相关活动中设置UserUserData活动将作为您的应用程序的UID运行,因此可以调用setUserData

getUserDatasetUserData使用内置的sqllite数据库,所以你不需要自己构buildcaching。 您只能存储stringtypes,但是您可以parsingjson并为每个帐户存储额外的信息。

当不同的第三方应用程序查询帐户,并与您的帐户调用getAuthtoken,您可以检查帐户userdata中的UID。 如果调用UID没有列出,您可以执行提示和/或其他事情来获得许可。 如果允许,您可以添加新的UID到帐户。

在应用程序之间共享令牌 :每个应用程序通常使用不同的clientid进行注册,并且不应共享令牌。 令牌是为客户端应用程序。

存储 :AccountManager不encryption您的数据。 如果您需要更安全的解决scheme,您应该encryption令牌,然后将其存储。

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

我得到的解决scheme是将oauth标记与应用程序供应商令牌(例如,Facebook提供给应用程序的令牌)和设备标识( android_id )进行关联/散列。 所以只有授权的应用程序,设备才能够使用来自客户经理的令牌。

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

我认为@迈克尔完美地回答了这个问题。 然而,为了让答案更加明智和简短,以寻求一个快速的答案我写这个。

您对android AccountManager的安全性的担忧是正确的,但这正是OAuth的意思,是Android AccountManager依赖的。

换句话说,如果你正在寻找一个非常安全的authentication机制,这对你来说不是一个好的select。 您不应该依赖任何caching令牌来进行身份validation,因为如果用户设备上存在安全漏洞,例如无意中授予入侵者访问权限,运行根目录设备等,则可以轻松地向入侵者泄露该密码。

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

无论如何,如果想要利用android的AccountManager系统并保持高度的安全性,就不可能在设备上保存任何令牌。 AbstractAccountAuthenticatorgetAuthToken方法可以被重写,如下所示:

 @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(); } 

在这种方法中,即使account在那里,情况1,情况2和情况4都不成立,因为没有保存的令牌。 因此,只有情况3将被返回,然后可以在相关callback中设置以打开用户input用户名和密码进行authentication的活动。

我不确定是否在这里进一步描述这个正确的轨道,但我在AccountManager上的网站post可能会帮助以防万一。