Shouldn't Android AccountManager Store OAuth Tokens on a Per-App/UID Basis?
Solution 1:
Is this a valid/practical security concern?
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
In the general case, you could never rely on an auth token given to a user remaining secret from that user. For example - the user could be running a rooted phone, and read off the token, gaining access to your private API. Ditto if the user's system was compromised (the attacker could read off the token in this case).
Put another way, there's no such thing as a "private" API that is at the same time accessible to any authenticated user, so it's reasonable for Android to ignore this security by obscurity goal in its design.
a malicious app ... could obtain access to my app's stored OAuth2 token
For the malicious app case, it begins to sound more reasonable that a malicious app shouldn't be able to use the client's token, as we expect Android's permission system to provide isolation of malicious apps (provided the user read / cared about the permissions they accepted when they installed it). However, as you say 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, but the user can explicitly allow apps to re-use a previous authentication to a service, which is convenient for the user.
Possible Solutions Review
"Secret" authTokenType ... does not seem very secure
Agreed - it's just another layer of security through obscurity; it sounds like any app wishing to share your authentication would have had to look up what the authTokenType was anyway, so adopting this approach just makes it a bit more awkward for this hypothetical app developer.
Send client ID/secret w/ OAuth2 token ... [to] verify server-side that the app is the authorized client
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). In this specific instance, it might protect against the more limited threat of a (non-root) alternative client / malicious app - I'm not familiar enough with the AccountManager to comment (ditto for your custom auth tokens solutions).
Suggestion
You described two threats - malicious apps that a user doesn't want to have access to their account, and alternative clients that you (the developer) doesn't want using parts of the API.
Malicious apps: Consider how sensitive the service you are providing is, and if it's not more sensitive than e.g. Google / twitter accounts, just rely on Android's protections (permissions on install, Access Request screen). If it is more sensitive, consider whether your constraint of utilizing 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's account details in online banking).
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.
Solution 2:
Your observation is correct. Authenticator will run with same UID as the installing app. When another app connects to Account manager and get token for this authenticator, it will bind to your provided authenticator service. It will run as your UID, so new accounts will be related to this Authenticator. When app calls for getAuthToken, binding will happen and Authenticator will still run in same UId. Default built in permissions check for account's UID, so that different Authenticator could not access another account from different Authenticator.
You can solve this issue with using "Calling UID" for addAccount and GetAuthToken since account manager service adds that to bundle. Your authenticator implementation can check that.
@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));
...
}
I suggest to follow authorization flow instead of storing client secret in your native app, because other developers can extract that secret. Your app is not a web app and should not have secrets.
When you are adding an account, you can query the callingUId as well. You need to setUserData at your addAccount related activity which will be running as your app's UID, so it can call setUserData.
getUserData and setUserData uses built in sqllite database, so you don't need to build cache by yourself. You can only store string type, but you can parse json and store extra info per account.
When different third party app queries account and calls for getAuthtoken with your account, you can check UID in the account' userdata. If calling UID is not listed, you can do the prompt and/or other things to get permission. If it is permitted, you can add new UID to the account.
Sharing tokens between apps: Each app is normally registered with different clientid and they should not share token. Token is for a client app.
Storage: AccountManager is not encrypting your data. If you need more secure solution, you should encrypt the tokens and then store it.
Solution 3:
I'm facing the same architectural problem for an app.
The solution that I got is to associate/hash the oauth token, with the app vendor token (ex. the token that facebook give to an app), and to device id (android_id
). So only the app authorized, for the device is able to use the token from account manager.
Of course, it's just a new layer of security, but no bullet proof.
Solution 4:
I reckon @Michael answered the question perfectly; however, to make the answer more sensible and short to those looking for a quick answer I am writing this.
Your concern about the security of android AccountManager
is correct, but this is what OAuth is meant to be, upon which android AccountManager
relies.
In other words, if you are looking for a very secure authentication mechanism this would not be a good option for you. You should not rely on any cached tokens for authentication, since they can be easily revealed to the intruder in case there is any security vulnerability on the user's device such as inadvertently granting access permission to the intruder, running a rooted device, etc.
The better alternative to OAuth in more secure authentication systems, e.g. online banking apps, is using asymmetric encryption using public and private keys, in which the user is required to enter their password every time for using the services. The password is then encrypted using the public key on the device and sent to the server. Here, even if the intruder gets known of the encrypted password, he cannot do anything with that because he cannot decrypt it with that public key and needs only the private key of the server.
Anyway, if one wants to make use of the AccountManager
system of the android as well as maintain high level of security, it would be possible by not saving any tokens on the device. The getAuthToken
method from AbstractAccountAuthenticator
can then be overriden like this:
@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();
}
In this method, neither case 1, case 2 nor case 4 holds true because there is no saved token, even though the account
is there. Therefore, only case 3 will be returned which can then be set in the relevant callback to open an Activity
in which the user enters username and password for authentication.
I am not sure of being on the right track in further describing this here, but my website posts on AccountManager
may help just in case.