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

per*_*ist 58 security authentication android oauth accountmanager

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

背景/上下文

I am building an Android app which uses OAuth2 for authentication/authorization of REST API requests to my server, which is an OAuth2 provider. Since the app is the "official" app (as opposed to a 3rd-party app), it is considered a trusted OAuth2 client, so I am using the resource owner password flow for obtaining an OAuth2 token: the user (the resource owner) enters his username/password into the app, which then sends its client ID and client secret along with the user credentials to my server's OAuth2 token endpoint in exchange for an access token that can be used to make API calls, as well as a long-lived refresh token used to get new access tokens when they expire. The rationale is that it is more secure to store the refresh token on the device than the user's password.

I am utilizing AccountManager for managing the account and associated access token on the device. Since I am providing my own OAuth2 provider, I have created my own custom account type by extending AbstractAccountAuthenticator and other required components, as explained in this Android Dev Guide and demonstrated in the SampleSyncAdapter sample project. I am able to successfully add accounts of my custom type from within my app and manage them from the "Accounts and sync" Android settings screen.

The Issue

However, I am concerned with the way the AccountManager caches and issues auth tokens - specifically, that 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.

To obtain an auth token through the AccountManager, one must invoke AccountManager.getAuthToken(), passing, among other things, the Account instance for which to obtain the auth token and the desired authTokenType. If an auth token exists for the specified account and authTokenType, and if the user grants access (via the grant "Access Request" screen) to the app which has made the auth token request (in such cases where the requesting app's UID does not match the authenticator's UID), then the token is returned. In case my explanation is lacking, this helpful blog entry explains it very clearly. Based on that post, and after examining the source of AccountManager and AccountManagerService (an internal class which does the heavy lifting for AccountManager) for myself, it appears that only 1 auth token is stored per authTokenType/account combo.

So, it seems feasible that 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, assuming that the user grants access to the malicious app.

To me, the problem is that AccountManager's default caching implementation is built on a paradigm on which, if we were to layer an OAuth2 authentication/authorization context, it would consider the phone/device to be a single OAuth2 client for a service/resource provider. Whereas, the paradigm that makes sense to me is that each app/UID should be considered as its own OAuth2 client. When my OAuth2 provider issues an access token, it is issuing an access token for that particular app which has sent the correct client ID and client secret, not all apps on the device. For instance, the user might have both my official app (call it app Client A), and a "licensed" third-party app which uses my API (call it app Client B) installed. For the official Client A, my OAuth2 provider may issue a "super" type/scope token which grants access to both public and private pieces of my API, whereas for the third-party Client B, my provider may issue a "restricted" type/scope token which only grants access to the the public API calls. 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. For, even if the user grants authorization to Client B for Client A's super token, the fact remains that my OAuth2 provider only intended to grant that token to Client A.

Am I overlooking something here? Is my belief that auth tokens to should be issued on a per-app/UID basis (each app being a distinct client) rational/practical, or are auth-tokens-per-device (each device being a client) the standard/accepted practice?

Or is there some flaw in my understanding of the code/security restrictions around AccountManager/AccountManagerService, such that this vulnerability does not actually exist? I've tested the above Client A/Client B scenario with the AccountManager and my custom authenticator, and my test client app B, which has a different package scope and UID, was able to obtain the auth token that my server had issued for my test client app A by passing-in the same authTokenType (during which I was prompted with "Access Request" grant screen, which I approved since I'm a user and therefore clueless)...

Possible Solutions

a. "Secret" authTokenType
In order to obtain the auth token, the authTokenType must be known; should the authTokenType be treated as a type of client secret, such that a token issued for a given secret token type may be obtained by only those "authorized" client apps which know the secret token type? This does not seem very secure; on a rooted device, it would be possible to examine the auth_token_type column of authtokens table in the system's accounts database and examine authTokenType values that are stored with my tokens. Thus, the "secret" auth token types used across all installations of my app (and any authorized third-party apps used on the device) will have been exposed in one central location. At least with OAuth2 client IDs/secrets, even if they must be packaged with the app, they are spread out among different client apps, and some attempt may be made to obfuscate them (which is better than nothing) to help discourage those who would unpackage/decompile the app.

b. Custom Auth Tokens
According to the docs for AccountManager.KEY_CALLER_UID and AuthenticatorDescription.customTokens, and the AccountManagerService source code I referenced earlier, I should be able to specify that my custom account type uses "custom tokens" and spin my own token caching/storage implementation within my custom authenticator, wherein I can obtain the UID of the calling app in order store/fetch auth tokens on a per-UID basis. Basically, I would have an authtokens table like the default implementation, except there would be an added uid column so that tokens are uniquely indexed on U?I?D?, a?c?c?o?u?n?t?, and A?u?t?h? ?T?o?k?e?n? ?T?y?p?e? (as opposed to just a?c?c?o?u?n?t? and A?u?t?h? ?T?o?k?e?n? ?T?y?p?e?). This seems like a more secure solution than using "secret" authTokenTypes, since that would involve using the same authTokenTypes across all installations of my app/authenticator, whereas UIDs vary from system-to-system, and cannot be easily spoofed. Aside from the joyful overhead of getting to write and manage my own token caching mechanism, what downsides are there to this approach in terms of security? Is it overkill? Am I really protecting anything, or am I missing something such that even with such an implementation in place, it would still be easy enough for one malicious app client to obtain another app client's auth token using the AccountManager and authTokenType(s) which are not guaranteed to be secret (assuming that said malicious app does not know the OAuth2 client secret, and therefore cannot directly get a fresh token but could only hope to get one that was already cached in the AccountManager on behalf of the authorized app client)?

c. Send client ID/secret w/ OAuth2 token
I could stick with the AccountManagerService's default token storage implementation and accept the possibility of unauthorized access to my app's auth token, but I could force API requests to always include the OAuth2 client ID and client secret, in addition to the access token, and verify server-side that the app is the authorized client for which the token was issued in the first place. However, I would like to avoid this because A) AFAIK, the OAuth2 spec does not require client authentication for protected resource requests - only the access token is required, and B) I would like to avoid the additional overhead of authenticating the client on each request.

This isn't possible in the general case (all the server gets is a series of messages in a protocol - the code that generated those messages can't be determined). --Michael

But the same could be said of the initial client authentication in the OAuth2 flow during which the client is first issued the access token. The only difference is that instead of authenticating on just the token request, requests for protected resources would also be authenticated in the same way. (Note that the client app would be able to pass in its c?l?i?e?n?t? ?i?d? and c?l?i?e?n?t? ?s?e?c?r?e?t? through the loginOptions parameter of AccountManager.getAuthToken(), which my custom authenticator would just pass to my resource provider, per the OAuth2 protocol).


Key Questions

  1. Is it indeed possible for one app to obtain another app's authToken for an account by invoking AccountManager.getAuthToken() with the same authTokenType?
  2. If this is possible, is this a valid/practical security concern within an OAuth2 context?

    You could never rely on an auth token given to a user remaining secret from that user...so it's reasonable for Android to ignore this security by obscurity goal in its design --Michael

    BUT - I'm not concerned about the user (the resource owner) getting the auth token without my consent; I'm concerned about unauthorized clients (apps). If the user wants to be an attacker of his own protected resources, then he can knock himself out. I'm saying it should not be possible that a user installs my client app and, unwittingly, an "imposter" client app that is able to gain access to my app's auth token simply because it passed-in the correct authTokenType and the user was too lazy/unaware/rushed to examine the access request screen. This analogy may be a bit oversimplified, but I don't consider it "security by obscurity" that my installed Facebook app cannot read emails cached by my Gmail app, which is different from me (the user) rooting my phone and examining the cache contents myself.

    The user needed to accept an (Android system provided) access request for the app to use your token... Given that, the Android solution seems OK - apps can't silently use a user's authentication without asking --Michael

    BUT - This is also a problem of authorization - the auth token issued for my "official" client is the key to a set of protected resources for which that client and only that client is authorized. I suppose one could argue that since the user is the owner of those protected resources, if he accepts the access request from a third party client (be it a "sactioned" partner app or some phisher), then he is effectively authorizing the third-party client that made the request to access those resources. But I have issues with this:

    • The average user is not security-conscious enough to be able to competently make this decision. I don't believe we should depend solely on the user's judgment to tap "Deny" on 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 "imposter" app is just going to look too "official" for the user to even raise an eyebrow at the access request screen. Or, here's a more blunt example - on the access request screen, my authenticator could simply say, "Do not accept this request! If you are seeing this screen, a malicious app is trying to gain access to your account!" Hopefully, in such a case, most users would deny the request. But - why should it even get that far? If Android simply kept auth tokens isolated to the scope of each app/UID for which they were issued, then this would be a non-issue. Let's simplify - even in the case where I have just one "official" client app, and therefore my resource provider does not even worry about issuing tokens to other, third-party clients, as a developer I should have the option of saying to the AccountManager, "No! Lock-down this auth token so that only my app has access." I can do this if I go along the "custom tokens" route, but even in that case, I would not be able to prevent the user from first being presented with the access request screen. At the very least, it should be better-documented that the default implementation of AccountManager.getAuthToken() will return the same auth token for all requesting apps/UIDs.
    • Even the Android docs recognize OAuth2 as the "industry standard" for authentication (and presumably authorization). The OAuth2 spec clearly states that access tokens are not to be shared between clients or divulged in any way. Why, then, does the default AccountManager implemenation/configuration make it so easy for a client to obtain the same cached auth token that was originally obtained from the service by another client? A simple fix within the AccountManager would be to only re-use cached tokens for the same app/UID under which the they were originally obtained from the service. If there is no locally cached auth token available for a given UID, then it should be obtained from the service. Or at least make this a configurable option for the developer.
    • In the OAuth 3-legged flow (which involves the user granting access to the client), isn't it supposed to be the service/resource provider (and not, say, the OS) which gets to A) authenticate the client and B) if the client is valid, present the user with the grant access request? Seems like Android is (incorrectly) usurping this role in the flow.

    But the user can explicitly allow apps to re-use a previous authentication to a service, which is convenient for the user.--Michael

    BUT - I don't think the ROI in convenience warrants the security risk. In cases where the user's password is being stored in the user's account, then really, the only convenience that is being bought for the user is that instead of sending a web request to my service to get a new, distinct token that is actually authorized for the requesting client, a locally cached token that is not authorized for the client is returned. So the user gains the slight convenience of seeing a "Signing In..." progress dialog for a couple of seconds fewer, at the risk of the user being majorly inconvenienced by having his resources stolen/misused.

  3. Keeping in mind that I am committed to A) using the OAuth2 protocol for securing my API requests, B) providing my own OAuth2 resource/authentication provider (as opposed to authenticating with say, Google or Facebook), and C) utilizing Android's AccountManager to manage my custom account type and its token(s), are any of my proposed solutions valid? Which makes the most sense? Am I overlooking any of the pros/cons? Are there worthwhile alternatives that I have not thought of?

    [Use] Alternative clients Don't have a secret API that attempts to only be accessible to an official client; people will get around this. Ensure all your public facing APIs are secure no matter what (future) client the user is using --Michael

    BUT - Doesn't this defeat one of the key purposes of using OAuth2 in the first place? What good is authorization if all potential authorizees would be authorized to the same scope of protected resources?

  4. Has anyone else felt this was an issue, and how did your work around it? I'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's AccountManager and auth tokens are about how to authenticate with a Google account, and not with a custom account type and OAuth2 provider. Moreover, I could not find anyone that was concerend about the possibility of the same auth token being used by different apps, which makes me wonder whether this is indeed a po

Mic*_*ael 10

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

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

在一般情况下,你永远无法靠给用户剩余秘密的身份验证令牌的用户.例如,用户可能正在运行root电话,并读取令牌,获得对私有API的访问权限.如果用户的系统遭到入侵则同样(攻击者可以在这种情况下读取令牌).

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

恶意应用...可以获取对我应用存储的OAuth2令牌的访问权限

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

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

可行解决方案评论

"秘密"authTokenType ......似乎不太安全

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

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

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

建议

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

  • 恶意应用:考虑您提供的服务有多敏感,如果它不比Google/Twitter帐户更敏感,只需依靠Android的保护(安装权限,访问请求屏幕).如果比较敏感,请考虑使用Android的AccountManager的约束是否合适.要强烈保护用户免遭恶意使用其帐户,请尝试对危险操作进行双因素身份验证(在网上银行中添加新收件人的帐户详细信息).

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