首页 文章

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

提问于
浏览
55

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

背景/背景

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

我正在利用AccountManager来管理设备上的帐户和相关的访问令牌 . 由于我提供自己的OAuth2提供程序,因此我通过扩展AbstractAccountAuthenticator和其他必需组件创建了我自己的自定义帐户类型,如in this Android Dev Guide所述,并在SampleSyncAdapter示例项目中进行了演示 . 我能够在我的应用程序中成功添加自定义类型的帐户,并从"Accounts and sync" Android设置屏幕管理它们 .

问题

但是,我关注AccountManager缓存和颁发身份验证令牌的方式 - 具体来说, the same auth token for a given account type and token type seems to be accessible by any app to which the user has granted access.

要通过AccountManager获取身份验证令牌,必须调用AccountManager.getAuthToken(),除其他外,传递要获取身份验证令牌和所需 authTokenTypeAccount实例 . 如果指定帐户和authTokenType存在身份验证令牌,并且用户授予访问权限(通过授权"Access Request"屏幕)到已发出身份验证令牌请求的应用程序(在这种情况下请求应用程序's UID does not match the authenticator'的UID),则令牌被返回 . 如果我的解释不足,this helpful blog entry非常清楚地解释了这一点 . 根据该帖子,在检查了AccountManagerAccountManagerService(一个为AccountManager进行繁重工作的内部类)的来源后,看来 only 1 auth token is stored per authTokenType/account combo.

因此,假设用户授予对恶意应用程序的访问权限,似乎可行 if a malicious app knew the account type and authTokenType(s) used by my authenticator, it could invoke AccountManager.getAuthToken() to obtain access my app's stored OAuth2 token, .

对我来说,问题是AccountManager的默认缓存实现 Build 在一个范例上,如果我们要对OAuth2身份验证/授权上下文进行分层,它会将电话/设备视为服务/资源提供者的单个OAuth2客户端 . Whereas, the paradigm that makes sense to me is that each app/UID should be considered as its own OAuth2 client. 当我的OAuth2提供程序发出访问令牌时,它正在为该特定应用程序发出访问令牌,该应用程序已发送正确的客户端ID和客户端密钥,而不是设备上的所有应用程序 . 例如,用户可能同时拥有我的官方应用程序(称为应用程序客户端A)和使用我的API(称为应用程序客户端B)的"licensed"第三方应用程序 . 对于官方客户端A,我的OAuth2提供商可以发出"super"类型/范围令牌,该令牌授予对我的API的公共和私有部分的访问权限,而对于第三方客户端B,我的提供商可以发出"restricted"类型/范围令牌,仅授予对公共API调用的访问权限 . It should not be possible for app Client B to obtain app Client A's access token, which the current AccountManager/AccountManagerService implementation seems to allow. 因为,即使用户授予客户B对客户A的超级令牌的授权,事实仍然是我的OAuth2提供商只打算将该令牌授予客户A.

我在这里俯瞰什么吗?我认为auth令牌应该基于每个应用程序/ UID(每个应用程序是一个独特的客户端)合理/实用,或者是每个设备的auth-tokens(每个设备是客户端)标准/接受实践?

或者我对 AccountManager / AccountManagerService 周围的代码/安全限制的理解存在一些缺陷,这样这个漏洞实际上并不存在?我已经使用 AccountManager 和我的自定义身份验证器测试了上述客户端A /客户端B方案,并且我的测试客户端应用程序B(具有不同的程序包范围和UID)能够获取我的服务器为我发出的身份验证令牌通过传入相同的 authTokenType 测试客户端应用程序A(在此期间我被提示使用"Access Request"授权屏幕,我批准了,因为我是用户因此无能为力)...

可能的解决方案

a. "Secret" authTokenType
为了获得身份验证令牌,必须知道authTokenType;是否应将 authTokenType 视为一种客户机密,以便只有那些知道秘密令牌类型的客户端应用才能获得为给定机密令牌类型发出的令牌?这似乎不太安全;在root设备上,可以检查系统 accounts 数据库中 authtokens 表的 auth_token_type 列,并检查authTokenType值是否为与我的代币一起存储 . 因此,我的应用程序的所有安装(以及设备上使用的任何授权第三方应用程序)中使用的"secret"身份验证令牌类型将在一个中心位置公开 . 至少对于OAuth2客户端ID /机密,即使它们必须与应用程序打包在一起,它们也会分散在不同的客户端应用程序中,并且可能会尝试对它们进行模糊处理(这比什么都没有好)以帮助阻止那些解压缩/反编译应用程序 .

b. Custom Auth Tokens
根据AccountManager.KEY_CALLER_UIDAuthenticatorDescription.customTokens的文档,以及前面引用的 AccountManagerService 源代码,我应该可以指定我的自定义帐户类型使用"custom tokens"并在我的自定义身份验证器中旋转我自己的令牌缓存/存储实现,其中我可以获取调用应用程序的UID按顺序存储/获取基于每个UID的身份验证令牌 . 基本上,我会有一个像默认实现的 authtokens 表,除了会添加一个 uid 列,以便令牌在UID,帐户和身份验证令牌类型上唯一索引(而不仅仅是帐户和身份验证令牌类型) . 这似乎比使用"secret" authTokenTypes更安全的解决方案,因为这将涉及在我的app / authenticator的所有安装中使用相同的 authTokenTypes ,而UID因系统而异,并且不容易欺骗 . 除了编写和管理我自己的令牌缓存机制的快乐开销之外,这种方法在安全性方面还有哪些缺点?这有点矫枉过正吗?我真的在保护任何东西,或者我错过了一些东西,即使有这样的实现,对于一个恶意应用程序客户端使用 AccountManagerauthTokenType (s)来获取另一个应用程序客户端的身份验证令牌仍然很容易保证是秘密的(假设所说的恶意应用程序不知道OAuth2客户端的秘密,因此不能直接获得新的令牌,但只能希望得到一个已经代表授权的应用客户端缓存在_754642中的那个)?

c. Send client ID/secret w/ OAuth2 token
我可以坚持使用 AccountManagerService 's default token storage implementation and accept the possibility of unauthorized access to my app'的身份验证令牌,但除了访问令牌之外,我还可以强制API请求始终包含OAuth2客户端ID和客户端密钥,并验证服务器端应用程序是否为其授权客户端令牌首先发布 . 但是,我想避免这种情况,因为_ OA42规范不需要对受保护资源请求进行客户端身份验证,因此只需要访问令牌,并且 B) 我希望避免在每个请求上验证客户端的额外开销 .

在一般情况下这是不可能的(所有服务器获取的是协议中的一系列消息 - 无法确定生成这些消息的代码) . --Michael

但可以说OAuth2流中的初始客户端身份验证也是如此,在此期间客户端首先发出访问令牌 . 唯一的区别是,不仅仅对令牌请求进行身份验证,对受保护资源的请求也将以相同的方式进行身份验证 . (请注意,客户端应用程序将能够通过 AccountManager.getAuthToken()loginOptions 参数传递其客户端ID和客户端密码,我的自定义身份验证器将根据OAuth2协议将其传递给我的资源提供程序) .


关键问题

通过使用相同的authTokenType调用AccountManager.getAuthToken()来为帐户

  • Is it indeed possible for one app to obtain another app's authToken

  • 如果可以, is this a valid/practical security concern within an OAuth2 context?

你永远不能依赖给用户保密的授权令牌......所以Android的设计默认目标忽略了这种安全性是合理的 - 迈克尔

BUT - 我关注未经授权的客户(应用程序) . 如果用户想要成为他自己受保护资源的攻击者,那么他就可以将自己淘汰出局 . 我只是因为它传入了正确的authTokenType并且用户太懒/不知道/急于检查访问请求屏幕 . 这个类比可能有点过于简单,但我不认为我安装的Facebook应用程序无法读取我的Gmail应用程序缓存的电子邮件,这与我(用户)生根我的手机并自行检查缓存内容不同 .

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

BUT - 这也是授权问题 - 为我的"official"客户端发出的身份验证令牌是一组受保护资源的关键,该客户端和该客户端仅被授权 . 我想有人可能会争辩说,由于用户是这些受保护资源的所有者,如果他接受来自第三方客户端的访问请求(无论是合作伙伴应用程序还是某些网络钓鱼者),那么他就是有效地授权发出请求的第三方客户端访问这些资源 . 但我有这个问题:

  • 普通用户的安全意识不足以能够胜任做出这一决定 . 我没有't believe we should depend solely on the user'判决点击"Deny"在Android 's access request screen to prevent even a crude phishing attempt. When the user is presented with the access request, my authenticator could be super-detailed and enumerate all the types of sensitive protected resources (that only my client should be able to access) for which the user will be granting should he accept the request, and in most cases, the user will still be too unaware and is going to accept. And in other, more sophisticated phishing attempts, the 754661 app is just going to look too 754662 for the user to even raise an eyebrow at the access request screen. Or, here'上是一个更直率的例子 - 在访问请求屏幕上,我的身份验证员可以简单地说,"Do not accept this request! If you are seeing this screen, a malicious app is trying to gain access to your account!"希望在这种情况下,大多数用户会拒绝该请求 . 但是 - 为什么它甚至会走得那么远?如果Android只是将auth标记保持隔离到发布它们的每个app / UID的范围,那么这将是一个非问题 . 让我们简化 - 即使在我只有一个"official"客户端应用程序的情况下,因此我的资源提供者甚至不担心向其他第三方客户发放令牌,作为开发人员,我应该可以选择向AccountManager说, "No! Lock-down this auth token so that only my app has access."如果我沿着"custom tokens"路线行驶,我可以这样做,但即使在这种情况下,我也无法阻止用户首先看到访问请求屏幕 . 至少,应该更好地记录,AccountManager.getAuthToken()的默认实现将为所有请求的应用程序/ UID返回相同的身份验证令牌 .

  • 甚至Android文档也认为OAuth2是用于身份验证的“industry standard”(并且可能是授权) . OAuth2规范明确规定访问令牌不会在客户端之间共享或以任何方式泄露 . 那么,为什么默认的AccountManager实现/配置使客户端很容易获得最初由另一个客户端从服务获得的相同缓存身份验证令牌? AccountManager中的一个简单修复是仅重新使用缓存令牌,用于最初从服务获取它们的相同应用程序/ UID . 如果给定UID没有可用的本地缓存身份验证令牌,则应从该服务获取该令牌 . 或者至少使这成为开发人员的可配置选项 .

  • 在OAuth 3-legged流程中(涉及用户授予对客户端的访问权限),是不是它应该是 A) 验证客户端的服务/资源提供者(而不是操作系统) B) if 客户端是否有效,向用户显示授权访问请求?似乎Android(错误地)在流程中篡夺了这个角色 .

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

BUT - 我没有't think the ROI in convenience warrants the security risk. In cases where the user'的密码存储在用户的帐户中,然后真的,为用户购买的唯一便利是,而不是向我的服务发送Web请求以获取实际授权的新的,不同的令牌对于请求客户端,将返回未授权客户端的本地缓存令牌 . 因此,用户可以在几秒钟内轻松地看到"Signing In..."进度对话框,从而存在用户因资源被盗/误用而造成严重不便的风险 .

  • 请记住,我承诺 A) 使用OAuth2协议保护我的API请求, B) 提供我自己的OAuth2资源/身份验证提供程序(而不是使用Google或Facebook进行身份验证), C) 利用Android的AccountManager来管理我的自定义帐户类型及其令牌,是 any of my proposed solutions valid? 哪个最有意义?我忽略了任何优点/缺点吗?我还没有想过有 Value 的替代方案吗?

[使用]备用客户端没有秘密API,只能尝试访问官方客户端;人们会绕过这个 . 无论用户使用的是什么(未来)客户端,都要确保所有面向公众的API都是安全的 - 迈克尔

BUT - 这首先是否打败了使用OAuth2的关键目的之一?如果所有潜在的授权人都被授权使用同一范围的受保护资源,授权有什么用呢?

  • Has anyone else felt this was an issue, and how did your work around it? 我've done some extensive Googling to try to find if others have felt this to be a security issue/concern, but it seems that most posts/questions involving Android'的AccountManager和身份验证令牌是关于如何使用Google帐户进行身份验证,而不是使用自定义帐户类型和OAuth2提供程序进行身份验证 . 此外,我找不到任何人对于不同应用程序使用相同身份验证令牌的可能性感到不满,这让我想知道这是否确实是一种可能/值得关注的问题(请参阅上面列出的我的第一个"Key Questions" ) .

感谢您的意见/指导!


响应......

Michael's Answer - 我认为我对你答案的主要困难是:

  • 我仍然倾向于认为应用程序是服务的独立,不同的客户端,而不是用户/电话/设备本身是一个“大”客户端,因此一个应用程序授权的令牌不应该,默认情况下,可以转移到没有的转移 . 看起来你可能暗示将每个应用程序视为一个独特的客户端是没有意义的可能性,

用户可以运行root电话,并读取令牌,获取对私有API的访问权限...... [或]如果用户的系统遭到入侵(攻击者可以在这种情况下读取令牌)

因此,在宏观方案中,我们应该将设备视为服务的客户,因为我们无法保证设备本身的应用之间的安全性 . 如果系统本身已被泄露,则确实无法保证对从该设备发送到服务的请求进行身份验证/授权 . 但同样可以说是TLS;如果无法保护 endpoints 本身,那么传输安全就无关紧要了 . 对于绝大多数没有受到攻击的Android设备,我认为将每个应用客户端视为一个独特的 endpoints 更安全,而不是通过共享相同的身份验证令牌将它们全部集中在一起 .

  • 当出现"access request"屏幕时(类似于我们在同意和安装之前总是仔细阅读的软件用户许可协议),我不会判断恶意/未经授权的客户端应用程序与非恶意/非授权客户端应用程序的区别 .

4 回答

  • 2

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

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

    在一般情况下,您永远不能依赖授予该用户保密的用户的身份验证令牌 . 例如,用户可能正在运行root电话,并读取令牌,获得对私有API的访问权限 . 如果用户的系统遭到入侵,则同上(攻击者可以在这种情况下读取令牌) .

    换句话说,没有任何经过身份验证的用户可以同时访问的“私有”API,因此Android在其设计中通过默默无闻的目标忽略此安全性是合理的 .

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

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

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

    可能的解决方案评论

    “秘密”authTokenType ......似乎不太安全

    同意 - 它只是通过默默无闻的另一层安全;听起来任何希望分享您的身份验证的应用程序都必须查找authTokenType的内容,因此采用这种方法只会让这个假想的应用程序开发人员更加尴尬 .

    使用OAuth2令牌发送客户端ID /密码... [以]验证服务器端应用程序是否为授权客户端

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

    建议

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

    • 恶意应用:考虑您提供的服务的敏感程度,以及它是否具有保护功能(安装权限,访问请求屏幕) . 如果它更敏感,请考虑您是否在网上银行中使用Android 's AccountManager is appropriate. To strongly protect the user against malicious use of their account, try two factor authentication for dangerous actions (c.f. adding a new recipient'帐户详细信息 .

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

  • 1

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

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

    @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 相关活动 setUserData ,该活动将作为您应用的UID运行,因此它可以调用 setUserData .

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

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

    Sharing tokens between apps :每个应用程序通常都注册了不同的clientid,他们不应该共享令牌 . 令牌用于客户端应用程序 .

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

  • 1

    我面临着一个app的架构问题 .

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

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

  • 10

    我估计@Michael完美地回答了这个问题;然而,为了让那些寻求快速回答的人更加明智和简短,我正在写这篇文章 .

    你对android AccountManager 的安全性的关注是正确的,但这就是OAuth的意思,android AccountManager 依赖于它 .

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

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

    无论如何,如果想要使用android的 AccountManager 系统以及保持高级别的安全性,那么可以通过不在设备上保存任何令牌来实现 . 然后可以像这样覆盖 AbstractAccountAuthenticator 中的 getAuthToken 方法:

    @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 在那里 . 因此,只返回案例3,然后可以在相关的回调中设置以打开 Activity ,其中用户输入用户名和密码进行身份验证 .

    我不确定是否正在进一步描述这一点,但my website posts on AccountManager可能有助于以防万一 .

相关问题