Objective
In the following, I am going to shows how to write a simple android application handling account management. My open-source library for account authentication in Github is used for this purpose. This library gets you free from doing the chores needed for managing android accounts such as defining a bound service, authenticator xml, etc.
For those who are interested in the library itself or would like to know more about the details of the account management in android, I strongly recommend to read the three previous posts of this series. Otherwise, there is no need to do so for using the library.
Sample App Description
This is a simple app. In the entry screen, the user is given two options for signing in or adding user.
If the user chooses ‘ADD USER’, the app should open the following registration screen. Once the user enters a username and password, an account of type ‘REGULAR_USER’ is added to android accounts after checking its authenticity with the server. Here it is assumed that the user previously knows his username and password.
If the account is added from inside the app, it automatically tries to sign in the user, which not the case for adding user from Settings→Accounts for which registration (acquiring access and refresh tokens from the server) is done without signing in the user.
It is assumed that the access and refresh tokens from the server get expired after two and five times of use respectively. In real projects this is due to security reasons so that the authorization requests (containing the access token) to the server not be the same for long to prevent account hijacking. Access token’s expiration period is in the order of days with considerably lower expiration frequency for the refresh token (e.g. yearly). The refresh token is also expired when the user changes his password or after a security-sensitive update to his credentials.
In our simple app, we set a demo counter of 15 as the maximum number of successful sign-ins above which the app is not granted further tokens. This means the user can no longer use the app, showing how the library could be used for this purpose.
Creating the Sample App
The steps for creating the sample app are as follows:
Step 1
Add this to dependencies for build.gradle of the app:
1 |
compile 'com.digigene.android:account-authenticator:1.3.0' |
Step 2
Define your authentication account type as a string in strings.xml:
1 |
<string name="auth_account_type">DigiGene</string> |
Replace ‘DigiGene’ with your own account type. This is what appears in Android Accounts in this screenshot.
Step 3
Design your registration layout for registering the users (e.g. this image):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="com.digigene.authenticatortest.MainActivity"> <EditText android:id="@+id/account_name" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_horizontal" android:hint="User Name" /> <EditText android:id="@+id/password" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@+id/account_name" android:gravity="center_horizontal" android:hint="Password" android:inputType="textPassword" /> <Button android:id="@+id/register" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@+id/password" android:text="register" android:onClick="startAuthentication"/> </RelativeLayout> |
and make a new class, say MyRegistrationActivity.java, with the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import com.digigene.accountauthenticator.activity.RegistrationActivity; public class MyRegistrationActivity extends RegistrationActivity { private EditText accountNameEditText, passwordEditText; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.registration_layout); accountNameEditText = (EditText) findViewById(R.id.account_name); passwordEditText = (EditText) findViewById(R.id.password); } public void startAuthentication(View view) { register(accountNameEditText.getText().toString(), passwordEditText.getText().toString(), null, null); } } |
Step 4
Make an entry layout as in here:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="com.digigene.authenticatortest.MainActivity"> <EditText android:id="@+id/account_name" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_horizontal" android:hint="User Name" /> <Button android:id="@+id/register" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@+id/account_name" android:text="Sign in" android:onClick="signIn"/> <Button android:id="@+id/add" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@+id/register" android:text="Add user" android:onClick="addUser"/> </RelativeLayout> |
This layout goes with the following class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import com.digigene.accountauthenticator.AuthenticatorManager; public class MainActivity extends Activity { EditText accountNameEditText; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); accountNameEditText = (EditText) findViewById(R.id.account_name); } public void signIn(View view) { AuthenticatorManager authenticatorManager = new AuthenticatorManager(MainActivity.this, getString(R.string.auth_account_type), this, MyRegistrationActivity.class, MyInterfaceImplementation.class); String authTokenType = "REGULAR_USER"; AuthenticatorManager.authenticatorManager = authenticatorManager; authenticatorManager.getAccessToken(accountNameEditText.getText().toString(), authTokenType, null); } public void addUser(View view) { AuthenticatorManager authenticatorManager = new AuthenticatorManager(MainActivity.this, getString(R.string.auth_account_type), this, MyRegistrationActivity.class, MyInterfaceImplementation.class); String authTokenType = "REGULAR_USER"; AuthenticatorManager.authenticatorManager = authenticatorManager; authenticatorManager.addAccount(authTokenType, null, null); } } |
Step 5
This is the last step in which the methods needed to connect to the server for registration and sign-in purposes and after that are implemented. In the following, contrary to a real case, server connections are mocked, just to demonstrate the functionality of the library. You may replace the following implementation with your own real one.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
import com.digigene.accountauthenticator.AbstractInterfaceImplementation; import com.digigene.accountauthenticator.AuthenticatorManager; import com.digigene.accountauthenticator.result.RegisterResult; import com.digigene.accountauthenticator.result.SignInResult; import com.digigene.accountauthenticator.result.SignUpResult; public class MyInterfaceImplementation extends AbstractInterfaceImplementation { public static int accessTokenCounter = 0; public static int refreshTokenCounter = 0; public static int demoCounter = 0; public static int accessTokenNo = 0; public static int refreshTokenNo = 0; public final int ACCESS_TOKEN_EXPIRATION_COUNTER = 2; public final int REFRESH_TOKEN_EXPIRATION_COUNTER = 5; public final int DEMO_COUNTER = 15; @Override public String[] userAccessTypes() { return new String[]{"REGULAR_USER", "SUPER_USER"}; } @Override public void doAfterSignUpIsUnsuccessful(Context context, Account account, String authTokenType, SignUpResult signUpResult, Bundle options) { Toast.makeText(context, "Sign-up was not possible due to the following:\n" + signUpResult .errMessage, Toast.LENGTH_LONG).show(); AuthenticatorManager.authenticatorManager.addAccount(authTokenType, null, options); } @Override public void doAfterSignInIsSuccessful(Context context, Account account, String authTokenType, String authToken, SignInResult signInResult, Bundle options) { demoCounter = demoCounter + 1; Toast.makeText(context, "User is successfully signed in: \naccessTokenNo=" + accessTokenNo + "\nrefreshTokenNo=" + refreshTokenNo + "\ndemoCounter=" + demoCounter, Toast.LENGTH_SHORT).show(); } @Override public SignInResult signInToServer(Context context, Account account, String authTokenType, String accessToken, Bundle options) { accessTokenCounter = accessTokenCounter + 1; SignInResult signInResult = new SignInResult(); signInResult.isSuccessful = true; synchronized (this) { try { this.wait(2000); } catch (InterruptedException e) { e.printStackTrace(); } } if ((accessTokenCounter > ACCESS_TOKEN_EXPIRATION_COUNTER || demoCounter > DEMO_COUNTER)) { signInResult.isSuccessful = false; signInResult.isAccessTokenExpired = true; if (demoCounter < DEMO_COUNTER) { signInResult.errMessage = "Access token is expired"; return signInResult; } } return signInResult; } @Override public SignUpResult signUpToServer(Context context, Account account, String authTokenType, String refreshToken, Bundle options) { SignUpResult signUpResult = new SignUpResult(); synchronized (this) { try { this.wait(2000); } catch (InterruptedException e) { e.printStackTrace(); } } refreshTokenCounter = refreshTokenCounter + 1; signUpResult.isSuccessful = true; signUpResult.accessToken = "ACCESS_TOKEN_NO_" + accessTokenNo; signUpResult.refreshToken = "REFRESH_TOKEN_NO_" + refreshTokenNo; if (demoCounter > DEMO_COUNTER) { signUpResult.isSuccessful = false; signUpResult.errMessage = "You have reached your limit of using the demo version. " + "Please buy it for further usage"; return signUpResult; } if (refreshTokenCounter > REFRESH_TOKEN_EXPIRATION_COUNTER) { refreshTokenCounter = 0; signUpResult.isSuccessful = false; signUpResult.errMessage = "User credentials have expired, please login again"; return signUpResult; } if (accessTokenCounter > ACCESS_TOKEN_EXPIRATION_COUNTER) { accessTokenCounter = 0; accessTokenNo = accessTokenNo + 1; signUpResult.accessToken = "ACCESS_TOKEN_NO_" + accessTokenNo; } return signUpResult; } @Override public RegisterResult registerInServer(Context context, Account account, String password, String authTokenType, String[] requiredFeatures, Bundle options) { RegisterResult registerResult = new RegisterResult(); registerResult.isSuccessful = false; synchronized (this) { try { this.wait(2000); } catch (InterruptedException e) { e.printStackTrace(); } } if (true) { // password is checked here and, if true, refresh token is generated for the // user refreshTokenNo = refreshTokenNo + 1; accessTokenNo = accessTokenNo + 1; registerResult.isSuccessful = true; registerResult.refreshToken = "REFRESH_TOKEN_NO_" + refreshTokenNo; } return registerResult; } @Override public boolean setDoesCallbackRunInBackgroundThread() { return false; } } |
Results
The following shows the library in action. As explained before, you can see access and refresh tokens are expired and replaced with new ones when the counters are reached.
After the 15th successful sign-in, the user is showing the following toast on the demo version expiration.
That’s all for the account authentication using this library. I hope you find it easy enough to work with as I do, and please let me know of anything about the library you think could make it better.
Nice sample, thanks!
Is it meant for resource owner password flow?
Thank you.
Yes, it is, and for other purposes as well.
Hi Ali,
Thanks for writing such a good tutorial. This helped me a lot.
Could you please add server side code as well & its corresponding calls in android to avoid mocked server ?
There are many options available, I am not sure which one to use.
Thanks
Hi,
Thanks for your reading and interest.
Unfortunately, writing the server code is beyond the scope of this article. However, writing the server part and its corresponding android code is as simple as writing a RESTFUL webservice.
Regards,
Ali
great tutorial¡
where should the calls to the server be?
thanks
Thank you.
They should be implemented in a class extending AbstractInterfaceImplementation.
Process: linus.com.accountmanagerdemo, PID: 6764
Caused by: java.lang.SecurityException: uid 10638 cannot explicitly add accounts of type: demo.com.accountmanagerdemo
at android.os.Parcel.readException(Parcel.java:1693)
at android.os.Parcel.readException(Parcel.java:1646)
When I try to add account, I got this exception, can you help to tell me why?
I have account and authenticator delcared in my mainfest.
Hi,
Sorry for late reply.
I think you’re missing the service definition part in your manifest. Please check http://www.digigene.com/android/accounts-in-android-part-two/ for the details.
hi there, this is an awesome account manager library. i have successfully made a REST service and make calls to my server using Retrofit2 in a class extending AbstractInterfaceImplementation.
when i call RegisterResult registerInServer my app force closes with this error.
Java.lang.RuntimeException: User registration is not successful in authenticator due to the following error:/nnull
at com.eduscence.app.accountauthenticator.activity.RegistrationActivity$RegisterAsync.(RegistrationActivity.java:100)
but
Thanks for your interest.
Have you checked the input params? If so, I guess the problem maybe because of inappropriate response (as needrd in RegisterResult) from the server to your registerInServer call. It would be good to check the example bundled with the library to see whether you are returning the correct format from the server.
Hi Ali,
Can we able to allow multiple google accounts logins at a time and also sing out of individual accounts seperatly in Android. I.e user can able to login with multiple google accounts one by one with out singing of last logged in account.
Hi,
Sorry for late reply.
No, it is not possible to login to an app with multiple Google accounts at the same time on one device.
Hi Ali
Thanks for this great post about Android Accounts!
Could we share the account for other 3rd party apps on the same device? which allowing SSO without signing on again.