I'm struggling to understand the Android AccountManager API. As far as I got thinks working I can use the blockingGetAuthToken method and specify whether Android should provide a notification for user to allow or deny the request. Another possibility is to use getAuthToken and check if KEY_INTENT is returned. If that's the case I could start a new Activity where the user can confirm my request.
My problem is that I would like to call one of these two methods from within a Service. Is there any chance to get a callback once the user has made a decision?
Thanks for your help
If you want a callback after the user has made a decision it's probably better to use the asynchronous version:
AccountManager mgr = AccountManager.get(getApplicationContext());
Account[] accounts = mgr.getAccountsByType("com.mydomain");
// assert that accounts is not empty
You'll want to use an AccountManagerFuture<Bundle> to hold results of the authentication token. This has to be async since the Android device may ask the user to login in the meantime:
private AccountManagerFuture<Bundle> myFuture = null;
private AccountManagerCallback<Bundle> myCallback = new AccountManagerCallback<Bundle>() {
#Override public void run(final AccountManagerFuture<Bundle> arg0) {
try {
myFuture.getResult().get(AccountManager.KEY_AUTHTOKEN); // this is your auth token
} catch (Exception e) {
// handle error
}
}
}
Now you can ask for the auth token asynchronously:
myFuture = mgr.getAuthToken(accounts[0], AUTH_TOKEN_TYPE, true, myCallback, null);
Related
I've been trying to figure out how to authenticate users for my android app. It is based on a website which already has a developed api, using JWT to authenticate.
I have come against the problem of refreshing tokens. Let's say I want to fetch something from the API and I need the auth token for that. I check my current auth token. If it is expired, I need to get a new one using some sort of refresh token.
However, it seems like almost no matter how I think of trying to implement it, I run into a few problems:
I don't want the UI thread to wait while I get a new token
I would prefer that I don't have to explicitly check whether the token
is there (and then refresh it) before making any API call
I've come up with one solution that solves #1 and at least minimizes the pain of #2. I can have some sort of getToken method. As an example, using JS style promises because they're easier for me to understand:
function getToken() {
return new Promise((resolve) => {
// Check for token, and return if valid.
// Otherwise, go to the server and get a new one
...
resolve(token)
}
}
// When making an API call
getToken().then((token) => {
// Call API
})
I think I can work this out so that the request will never be running on the UI thread, which solves #1, and as far as #2, it's at least bearable.
My question is this: is there a better way to do this? It kind of seems like AccountManager might be able to handle this sort of thing for me, but the documentation for it is subpar at best, so I'm not sure how I would even implement it. If AccountManager can do it and you know of a good tutorial for it, please comment with that.
A way to accomplish this is intercept a 401 status code and refresh token.
If you are using Volley, you can extend Request class and override parseNetworkEror(VolleyError error) method. If need be, schedule a Job which will refresh the token (JobDispatcher) and trigger an event to communicate UI about the change (EventBus).
The following example is using OAuth authentication, but can be easily changed to implement JWT.
#Override
protected VolleyError parseNetworkError(VolleyError volleyError) {
if (getDataAccess().shouldRefreshToken(volleyError)) {
if (!EventBus.getDefault().hasSubscriberForEvent(TokenRefreshedEvent.class)) {
EventBus.getDefault().register(this);
}
CSApplication app = CSApplication.getInstance();
FirebaseJobDispatcher dispatcher = app.getJobDispatcher(app.getApplicationContext());
Job myJob = dispatcher.newJobBuilder()
.setService(JobRefreshToken.class)
.setTag("REFRESH_TOKEN")
.setTrigger(Trigger.NOW)
.setRetryStrategy(RetryStrategy.DEFAULT_EXPONENTIAL)
.setConstraints(Constraint.ON_ANY_NETWORK)
.build();
int result = dispatcher.schedule(myJob);
if (result == FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS) {
LogUtils.log(LogUtils.Type.JOB, GsonRequest.class, "Scheduling job refresh token");
} else {
LogUtils.log(LogUtils.Type.JOB, GsonRequest.class, "Error on schedule refresh token");
}
}
return super.parseNetworkError(volleyError);
}
public boolean shouldRefreshToken(VolleyError error) {
boolean shouldRefreshToken = error.networkResponse != null && error.networkResponse.statusCode == 401;
if (shouldRefreshToken) {
Map<String, String> headers = error.networkResponse.headers;
if (headers.containsKey("WWW-Authenticate")) {
String value = headers.get("WWW-Authenticate");
boolean issuerInvalid = value.contains("The issuer is invalid");
shouldRefreshToken = !issuerInvalid;
if (issuerInvalid) {
log(LogUtils.Type.VOLLEY, DataAccess.class, "Issuer do token é inválido");
}
}
}
return shouldRefreshToken;
}
Job Code
getDataAccess().refreshToken(getApplicationContext(), new VolleyCallback<Void>() {
#Override
public void onSuccess(Void aVoid) {
EventBus.getDefault().post(new TokenRefreshedEvent(true));
job.jobFinished(params, false);
log(LogUtils.Type.JOB, JobRefreshToken.class, "Refresh Token job finished");
}
#Override
public void onError(VolleyError error) {
super.onError(error);
EventBus.getDefault().post(new TokenRefreshedEvent(false));
job.jobFinished(params, false);
}
});
return true;
}
What I ended up doing was creating a method getToken which either returns the current token or gets a new one (blocking). With this strategy, I need to make sure that it never gets called from the UI thread. I created a Retrofit2 interceptor which calls getToken. The benefit of this method is that I can just call my Retrofit methods without worrying about the token at all, and it checks for expiration and gets a new one as necessary.
I have two apps that work with a same account type. I want below page to be shown when the user opens the second app for first time and one account exists:
But nothing happens when I run this code:
final AccountManagerFuture<Bundle> future = mAccountManager.getAuthToken(account, authTokenType, null, this, null, null);
new Thread(new Runnable() {
#Override
public void run() {
try {
Bundle bnd = future.getResult();
final String authtoken = bnd.getString(AccountManager.KEY_AUTHTOKEN);
showMessage((authtoken != null) ? "SUCCESS!\ntoken: " + authtoken : "FAIL");
Log.d("udinic", "GetToken Bundle is " + bnd);
} catch (Exception e) {
e.printStackTrace();
showMessage(e.getMessage());
}
}
}).start();
The above code works correctly when I run it from the app that has the authenticator. When I run below code instead, system generates a notification that when I click on it, the above picture appears.
final AccountManagerFuture<Bundle> future = mAccountManager
.getAuthToken(account, authTokenType, null, true,
null, handler);
Clicking allow button returns the AuthToken correctly. However I want to see the grant permission page (above picture) when calling getAuthToken, not by clicking on notification. How can I do that?
I used this method instead of previous one and now I see the confirmation dialog:
accountManager.getAuthToken(account, AUTH_TOKEN_TYPE_FULL_ACCESS, null, true, new AccountManagerCallback<Bundle>() {
#Override
public void run(AccountManagerFuture<Bundle> future) {
try {
Bundle bundle = future.getResult();
String authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN);
} catch (OperationCanceledException | IOException | AuthenticatorException e) {
}
}
}, null);
Note that the second app must have different signature. If both apps have a same signature, no confirmation is required and authToken will retrieve.
A few things to go over here. Using a Thread in Android is generally considered bad practice, per Android docs, it is recommended to use a Async task or Handler. Now for the Auth message per Android documentation the expected output is a notification.
getAuthToken(Account account, String authTokenType, Bundle options, boolean notifyAuthFailure, AccountManagerCallback<Bundle> callback, Handler handler)
Gets an auth token of the specified type for a particular account,
optionally raising a notification if the user must enter credentials.
Notice how getAuthToken has a Handler parameter? This would be the preferred method to handle the task async. The issue here is you CAN NOT have a full screen message on a handler thread, because it can't interrupt the UI thread. In your first example you actually call call mAccountManager on the UI thread, so it allows it to take over the UI and send a full screen allow or deny message, however this cannot be done with a handler, since a handler cannot use the UI thread (will throw a error at runtime).
My proposed solution? Don't use a handler if you want a full screen interruptive message, do the action on the UI thread, similar to your first code snippet.
AccountManagerFuture<Bundle> future = mAccountManager.getAuthToken(account, authTokenType, null, this, callback, null);
//USE implements and implement a listener in the class declaration and
//use 'this' in the callback param OR create a new callback method for it
I need to identify user who made request to my endpoint api, via Android client. Though I am able to follow best practice by keeping my Api and App within one project using gradle and android studio. Also I am able to send request to my endpoint api and receive response without authorization.
Basically I need to send authorization token as header in the request, people suggest that, merely by adding instance of "GoogleAccountCredential" along with the request will do the trick as in the code below. The class in which below code is present that extends android.os.AsyncTask; I have been following https://cloud.google.com/appengine/docs/java/endpoints/consume_android#using_the_account_picker, but code fragments are not very clear.
#Override
protected String doInBackground(Pair<Context, String>... params) {
..
MyApi.Builder builder = new MyApi.Builder(AndroidHttp.newCompatibleTransport(), new AndroidJsonFactory(), credential).setRootUrl("https://myapp.appspot.com/_ah/api/");
..
What I have :
I have an Activity called ExpandedListViewActivity
another thing is ExpandedListAdaptor, which populates views dynamically ( form ).
My Objective
When User clicks on submit present in the form.
Android should be able to find the google account and its credentials and attach it with the request.
If it does not find then show account selector view, so that user can select account, if we can do it silently without user consent that would be very nice.
Extra methods that I have in :
class EndpointsAsyncTask extends AsyncTask, Void,
String>
void chooseAccount() {
mActivity.startActivityForResult(credential.newChooseAccountIntent(),
REQUEST_ACCOUNT_PICKER);
}
protected String fetchToken() throws IOException {
try {
return GoogleAuthUtil.getToken(mActivity, mEmail, mScope);
} catch (UserRecoverableAuthException userRecoverableException) {
// GooglePlayServices.apk is either old, disabled, or not present
// so we need to show the user some UI in the activity to recover.
userRecoverableException.printStackTrace();
} catch (GoogleAuthException fatalException) {
// Some other type of unrecoverable exception has occurred.
// Report and log the error as appropriate for your app.
}
return null;
}
public void getSettings(){
Log.d(APP, "get Settings ");
settings = mActivity.getSharedPreferences("Api", 0);
credential = GoogleAccountCredential.usingAudience(mActivity,
"server:client_id:Android-clientId.apps.googleusercontent.com");
setSelectedAccountName(settings.getString(PREF_ACCOUNT_NAME, null));
}
// setSelectedAccountName definition
private void setSelectedAccountName(String accountName) {
SharedPreferences.Editor editor = settings.edit();
editor.putString(PREF_ACCOUNT_NAME, accountName);
editor.commit();
credential.setSelectedAccountName(accountName);
this.accountName = accountName;
}
Please take into account that my endpoint server side is properly configured and running.
It should be straight forward, but I am not able to solve this, Please point the mistake or show me a direction to solve this..
Thanks for reading.
Shashank
This solves my problem.
http://android-developers.blogspot.in/2013/01/verifying-back-end-calls-from-android.html
https://cloud.google.com/appengine/docs/java/endpoints/auth
Specify the client IDs (clientIds) of apps authorized to make requests to your API backend.
Add a User parameter to all exposed methods to be protected by authorization.
Generate the client library again for any Android clients
Redeploy your backend API. <-- This was the key, to solve this problem.
Thanks,
Shashank
I've got an application in Android and I'm trying to use AccountManager to get the AuthToken and do things with Facebook or Twitter. So I've got this:
AccountManager am = AccountManager.get(this);
Account[] accounts = am.getAccountsByType("com.facebook.auth.login");
Bundle options = new Bundle();
Account myAccount=null;
for (int i=0;i<accounts.length;i++) {
if (accounts[i].type.equals("com.facebook.auth.login")) myAccount=accounts[i];
//options.putString("facebookUser", accounts[i].name);
}
am.getAuthToken(
myAccount, // Account retrieved using getAccountsByType()
"Manage your tasks", // Auth scope
options, // Authenticator-specific options
this, // Your activity
new OnTokenAcquired(), // Callback called when a token is successfully acquired
new Handler(new OnError()));
My two callbacks are onTokenAcquired:
public class OnTokenAcquired implements AccountManagerCallback<Bundle> {
#Override
public void run(AccountManagerFuture<Bundle> result) {
try {
Bundle bundle = result.getResult();
Log.e("onTokenAcquired",bundle.getString(AccountManager.KEY_AUTHTOKEN));
} catch (OperationCanceledException e) {
Log.e("onTokenAcquired","operationcanceled");
} catch (AuthenticatorException e) {
Log.e("onTokenAcquired","authenticatorexception");
} catch (IOException e) {
Log.e("onTokenAcquired","IOException");
}
}
}
and OnError:
public class OnError implements Callback {
#Override
public boolean handleMessage(Message msg) {
Log.e("onError","ERROR");
return false;
}
}
I'm following the Android Developer guide (http://developer.android.com/intl/es/training/id-auth/authenticate.html). So, I've got two options, on error or on token acquired, in each one I've got a Log.e() to read SOMETHING, but none is being writed.
Can anybody help me? If I was getting an error or the token was not being acquired at least I would have something to work on, but I just don't know what's happening.
It is not totally obvious from the documentation, but the variant of getAuthToken you are calling will never call the callback if user intervention is required. There are some workarounds here:
https://code.google.com/p/android/issues/detail?id=25473
I don't know if that is specifically the problem you're having, but it probably isn't helping.
I do not think getAuthToken is supported with the Facebook authenticator. Also the Auth scope "Manage your tasks" that you are using is the scope for "Google Tasks" and would most likely not be the correct scope to use if getAuthToken was supported.
I suggest that you use the Facebook SDK for Android instead. With this it is very easy to get the auth token. The SDK also have a fallback for users that doesn't have the official facebook installed, or a facebook account added to the phone which is very neat.
Please see also: How to retrieve an Facebook-AuthToken from the accounts saved on Android
So, my question restated is when you go to Settings -> Accounts & Sync and select the an account that was created that your SyncAdapter is syncing with a cloud server, and select remove account, what happens as far as your SyncAdapter is concerned? There is a dialog that displays asking you to confirm and that the data on the phone associated with that account will be removed. I cannot easily believe that the framework can automatically remove the data my SyncAdapter has stored in the local database, but it seems to imply that removing the account will (and I would agree that is should) remove that data. Is there a addition to my SyncAdapter that will serve sort of as the callback for the account removal to handle deleting all the appropriate data from the local database? Maybe it has to be done through the AccountManager instead; my AccountManager gets notified when the account gets removed and from there I can trigger the data deletion without the SyncAdapter.
EDIT:
On a related note, is the sync manager calling my SyncAdapter for each account that it synchronizes when a new account is added? I see a onPerformSync(...) being executed for previously added accounts along with the just added account when I add an account, and would like to stop that.
I discovered the solution is to make the app's ContentProvider implement OnAccountsUpdateListener. Attach the ContentProvider as a listener in its onCreate method with account_manager.addOnAccountsUpdatedListener(this, null, false) and then implement the interface method like
#Override
public void onAccountsUpdated(final Account[] accounts) {
Ln.i("Accounts updated.");
final Iterable<String> account_list = new Iterable<String>() {
#Override
public Iterator<String> iterator() {
return new Iterator<String>() {
private final Iterator<Account> account_list = Arrays.asList(accounts).iterator();
#Override
public boolean hasNext() {
return account_list.hasNext();
}
/** Extracts the next account name and wraps it in single quotes. */
#Override
public String next() {
return "'" + account_list.next().name + "'";
}
#Override
public void remove() { throw new UnsupportedOperationException("Not implemented"); }
};
}
};
final String account_set = TextUtils.join(", ", account_list);
Ln.i("Current accounts: %s", account_set);
// Removes content that is associated with accounts that are not currently connected
final SelectionBuilder builder = new SelectionBuilder();
builder.table(Tables.CALENDARS)
.where(Calendars.CALENDAR_USER + " NOT IN (?)", account_set);
new SafeAsyncTask() {
#Override
public Void call() throws Exception {
_model.openWritableDatabase();
_model.delete(builder);
return null;
}
}.execute();
getContext().getContentResolver().notifyChange(Calendars.NO_SYNC_URI, null, false);
}
I construct a String of the currently connected accounts, then build a SQL query with that String. I perform a delete on the database in a background thread on that query to remove the data associated with accounts not currently connected. And I notify that content changed, but does not need to synchronized with the server.
No, but your Authenticator does[1]. This method is called before the account is removed:
AbstractAccountAuthenticator.getAccountRemovalAllowed(AccountAuthenticatorResponse, Account)
the Account param is the account being deleted - the default behaviour is to allow removal of the account:
return super.getAccountRemovalAllowed(response, account); // returns Bundle[{booleanResult=true}]
..but I guess it's a hook that you can use to tidy things up or block the account being removed should you wish to.
[1] - this is a dirty hack; please see Dandre's comment.
Another option is to register for the android.accounts.LOGIN_ACCOUNTS_CHANGED broadcast that the AccountManager sends out. Unfortunately, this broadcast is sent out whenever any account is changed and the broadcast does not deliver further information what has changed either.
So you'd have to query the account manager and look how many of "your" accounts it has left and delete the data of the missing ones.