OAuth 2 on Android (Principles using Google Apis)
Author’s highlight
Mathias thinks you, as Android developer, can be interested by the following article : Rx Android everywhere… why, but why ?
OAuth 2.0 on Android
This document explain how to set OAuth2.0 between an Android native application and a GoogleApi (GDrive). It aims to show in a general way how to implement OAuth on Android.
We only focuse on establishing a OAuth communication between our application and GDrive, we don’t introduce the Account (Android AccountManager) to store this stoken, not yet a next document will explain that and what are the goals of using the Account.
The code can be found here :
https://github.com/MathiasSeguy-Android2EE/AndroidOAuth2_Github_POC
If you want to use it, you need to register yourself on the google console (I means at least you the SHA-1 of your Debug key).
Warning: If you want to use GDrive Api on Android, just don’t do that, use the ApiClient of the GooglePlay services. It’s less less work. Here is an exemple to use OAuth, not to access to GDrive.
So let’s begin.
OAuth 2 basics principles
This is a 3 steps authentication,
- you register your application in the google console and then receive your clientID
- then you talk to an authorization server with your clientID, which returns you a code,
- using this code, you then can talk to the api server and ask for the token.
When you have your token, you can use it to access protected data, on google drive, using the OAuth protocol.
We’ll see how to make each of this steps.
Google Console Registration
It’s always the case, for all OAuth communication, you need to register your application on the server you want to use.
Create the project
So you go there : https://console.developers.google.com/apis/
Activate your API
You create a new project and you choose which Api you want to access :
In our exemple, we choose GDrive api. So you click on it and activate it (the button will change itself into Desactiver).
Create your OAuth client ID
Then you go, using the left tabs, to the identification part:
And you can create your ID (by clicking on create id):
You have several choices, choose “ID Client OAuth”. (the console asks you to give a name to your project, give it).
Then you are in front the following form:
You choose Android and you need to give the following information:
- the root package of your application
- and the SHA-1 of your signature key
Both are obvious…
Perhaps the SHA-1 not. KeyTool is a tool of your Java instalation, you can find it in your Java sdk. So go in your Java/sdk/bin and run the command line and then run the command ahead (keytool gnagnagna).
When you have done that you have the ClientId to use when communicating with the authorization server.
Back to our application. First step: Talk with the authorization server
We have our ClienttId, we can now communicate with the Authorization server… but in fact not, not directly.
We need to use a browser to talk to the authorization server (because browser are trusted application and yours is not). So we will launch a browser with the url (of the auth server) and the parameter needed for the authorization server and wait for the answer…
But how the answer will reach back your application ? OAuth use a concept called redirect_url. When the authorization server wants to answer, it will use this redirect_uri to send you the information…
But wait, I am an Android application, I am not a server, I can not be reached using a redirect_url.
To resolve that, some OAuth services, like those of the google apis, allow you to give them a redirect_url which is in fact, not and url but a uri/a schema. The code sent back to the browser will them build an Intent, with the schema you gave to the server, and launch this Intent in the Android system. So you just have to register a Service or an Activity to be activated when this specific Intent is launched by the system. And it’s done.
The next paragraphs will explain how to do that in the details.
key point:
The authorization server must allowed schema as redirect_url. If it does not, you’re screwed and you need to define your own server just to handle the authorization server’s answer and to make a push from your server to the application. So if you build your own authorization server, please allow schema !!!
Launching the browser with the right information for the authorization server
First, we need to know, what are the information expected by the authorization server.
Most of the time we have the following elements to send to it:
- Your Client_ID (it needs to know who is talking to)
- Your redirect_uri (how can the server can send you back the information)
- Your scope (what do you want to do)
- Your response type (what format do you expect for the answer).
For native application, at least for GoogleApis, your response type is always equals to “code”. The server will send you a code you’ll use when asking for the token.
Your scope depends on which Apis you use and what you want to do, in my exemple, I use https://www.googleapis.com/auth/drive which is the “I can do anything on your GDrive”.
You have your Client_Id from the registration step.
Now, we just have to take a look at that Redirect_URI parameter.
Redirect_URI
Most of the time, an https url is expected. But with the revolution of the smartphones and Android specifically, some actors of the IT world make evolve the specifications and allow URI (based on schema).
So they allow us to give an URI. How does it works:
If I say, my schema (==URI) is
com.android2ee.oauth.githubsample:/oauth2redirect
Then an Intent will be launched by the system with action =”View” and schema=”com.android2ee.oath.githubsample”.
To listen for that Intent and launch my treatment, I just need to add an Activity or a Service that wakes up each time the Intent is sent by the system. To do that, every things happen in your manifest.xml file in your application. You just have to declare it like that:
<activity android:name=".view.LoginActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="com.renaultnissan.acms.platform.oauth.githubsample" />
</intent-filter>
</activity>
Launching the browser with the right parameters and URL
On Android it’s obvious how to launch the native browser and ask it to display a specific url, you just need to send an Intent in the system:
First, build your URL:
HttpUrl authorizeUrl = HttpUrl.parse("https://accounts.google.com/o/oauth2/v2/auth") //
.newBuilder() //
.addQueryParameter("client_id", CLIENT_ID)
.addQueryParameter("scope", API_SCOPE)
.addQueryParameter("redirect_uri", REDIRECT_URI)
.addQueryParameter("response_type", CODE)
.build();
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse(String.valueOf(authorizeUrl.url())));
startActivity(i);
HttpUrl authorizeUrl = HttpUrl.parse("https://accounts.google.com/o/oauth2/v2/auth") //
.newBuilder() //
.addQueryParameter("client_id", CLIENT_ID)
.addQueryParameter("scope", API_SCOPE)
.addQueryParameter("redirect_uri", REDIRECT_URI)
.addQueryParameter("response_type", CODE)
.build();
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse(String.valueOf(authorizeUrl.url())));
startActivity(i);/**
* You client id, you have it from the google console when you register your project
* https://console.developers.google.com/a
*/
private static final String CLIENT_ID = "1020597890643-n3m1t7fplcv2t0f78g7miachq7lgnbrv.apps.googleusercontent.com";
/**
* The redirect uri you have define in your google console for your project
*/
private static final String REDIRECT_URI = "com.android2ee.oauth.githubsample:/oauth2redirect";
/**
* The redirect root uri you have define in your google console for your project
* It is also the scheme your Main Activity will react
*/
private static final String REDIRECT_URI_ROOT = "com.android2ee.oauth.githubsample";
/**
* You are asking to use a code when autorizing
*/
private static final String CODE = "code";
/**
* The scope: what do we want to use
* Here we want to be able to do anything on the user's GDrive
*/
public static final String API_SCOPE = "https://www.googleapis.com/auth/drive";
And it’s done.
Remark: You use an Https connection.
Listening for the Authorization server response
So, in your manifest, you declared an Activity (or a Service) to be woke up when the system send an Intent with the schema you gave as redirect_uri.
When the server reply to the browser, it just ask the browser to launch an Intent with the same schema. So it immediately wakes up your activity.
Now, you just have to analyse the response in your Activity. So in your onCreate :
Uri data = getIntent().getData();
if (data != null && !TextUtils.isEmpty(data.getScheme())) {
if (REDIRECT_URI_ROOT.equals(data.getScheme())) {
code = data.getQueryParameter(CODE);
error=data.getQueryParameter(ERROR_CODE);
Log.e(TAG, "onCreate: handle result of authorization with code :" + code);
if (!TextUtils.isEmpty(code)) {
getTokenFormUrl();
}
if(!TextUtils.isEmpty(error)) {
//a problem occurs, the user reject our granting request or something like that
Toast.makeText(this, R.string.loginactivity_grantsfails_quit,Toast.LENGTH_LONG).show();
Log.e(TAG, "onCreate: handle result of authorization with error :" + error);
//then die
finish();
}
}
}
You find the Intent that wakes you up. You look in its bundle, if it has the good schema. If it does, it’s our use case, we analyse it, we retrieve the code and the error. One of those is always null.
If you have a code, you can go to the next step which is ask for the token. If you have an error, that means, your user doesn’t want to grant you those rights or a problem occurs when connecting to the server, or… but you are screwed, you can’t do anything.
Second step: Ask for the OAuth token
This step is straight, you just have to send a request to the server api with your code (the one you just received), your clientId (the one you had at your registration), your redirectUri (the same as before) and your expected grantType:
The grantType is “authorization_code” because you use a code.
How can we do that on Android in a proper way : we use Retrofit.
Using retrofit
Using Retrofit is the clever way to handle your http communication, so we will use it.
Define your Interface
public interface OAuthServerIntf {
/**
* The call to request a token
*/
@FormUrlEncoded
@POST("oauth2/v4/token")
Call<OAuthToken> requestTokenForm(
@Field("code")String code,
@Field("client_id")String client_id,
@Field("redirect_uri")String redirect_uri,
@Field("grant_type")String grant_type);
Here we just define that the method requestTokenForm is
- making a post to rootURL/oauth2/v4/token (rootUrl is not defined yet)
- returning a Call that encapsulated an OAuthToken (a specific class you write yourself that represents in an Object way the server’s response)
- filled with the parameters code, clientId, redirectUri and GrantType and those parameters are wrap as a Form within your request.
Where the class OAuthToken is :
public class OAuthToken {
/***********************************************************
* Attributes
**********************************************************/
@Json(name = "access_token")
private String accessToken;
@Json(name = "token_type")
private String tokenType;
@Json(name = "expires_in")
private long expiresIn;
private long expiredAfterMilli = 0;
@Json(name = "refresh_token")
private String refreshToken;
Instantiate your interface using Retrofit
Here, we simply, ask to Retrofit to build a instance of the interface with all the elements needed to make it works:
public class RetrofitBuilder {
/**
* Root URL
* (always ends with a /)
*/
public static final String BASE_URL = "https://www.googleapis.com/";/***********************************************************
* Getting OAuthServerIntf instance using Retrofit creation
**********************************************************/
/**
* A basic client to make unauthenticated calls
* @param ctx
* @return OAuthServerIntf instance
*/
public static OAuthServerIntf getSimpleClient(Context ctx) {
//Using Default HttpClient
Retrofit retrofit = new Retrofit.Builder()
.client(getSimpleOkHttpClient(ctx))
.addConverterFactory(new StringConverterFactory())
.addConverterFactory(MoshiConverterFactory.create())
.baseUrl(BASE_URL)
.build();
OAuthServerIntf webServer = retrofit.create(OAuthServerIntf.class);
return webServer;
}
So we just build the Retrofit object by giving him :
- an Http client
- a converter to automaticly convert JSon into Object
- the base URL for our requests
And using this retrofit object, we create our “webServer” by insanciating our interface into a real class that can do the job.
Use your retorfit webServer
Now, we just need to use it:
/**
* Retrieve the OAuth token
*/
private void getTokenFormUrl() {
OAuthServerIntf oAuthServer = RetrofitBuilder.getSimpleClient(this);
Call<OAuthToken> getRequestTokenFormCall = oAuthServer.requestTokenForm(
code,
CLIENT_ID,
REDIRECT_URI,
GRANT_TYPE_AUTHORIZATION_CODE
);
getRequestTokenFormCall.enqueue(new Callback<OAuthToken>() {
@Override
public void onResponse(Call<OAuthToken> call, Response<OAuthToken> response) {
Log.e(TAG, "===============New Call==========================");
Log.e(TAG, "The call getRequestTokenFormCall succeed with code=" + response.code() + " and has body = " + response.body());
//ok we have the token
response.body().save();
startMainActivity(true);
} @Override
public void onFailure(Call<OAuthToken> call, Throwable t) {
Log.e(TAG, "===============New Call==========================");
Log.e(TAG, "The call getRequestTokenFormCall failed", t); }
});
}
So using our webServer, we create our Call using the right parameters and we make the request (using enqueue).
The call is done in an asynchronous way and in the onResponse method we retrieve our OAuth object. We store it to use latter.
Now we have our token !!! we are allowed to use the services provides by the api server.
Last step: Use the Apis services of the server using your token
Now, when we want to call an end-point of the api (server side), we just need to add in our header our token.
Retrofit Interface
To do that, we, once again use RetroFit. So in our interface, we add the different rest calls we want to make:
public interface OAuthServerIntf {
/**
* The call to request a token
*/
@FormUrlEncoded
@POST("oauth2/v4/token")
Call<OAuthToken> requestTokenForm(
@Field("code")String code,
@Field("client_id")String client_id,
@Field("redirect_uri")String redirect_uri,
@Field("grant_type")String grant_type);
/**
* The call to retrieve the files of our User in GDrive
*/
@GET("drive/v3/files")
Call<GDriveFiles> listFiles();}
We define for that a method called listFiles which:
- makes a Get on drive/v3/files
- returns a Call that encapsulate GDriveFiles (a class you write yourself that is the representation of the JSon object send by the server)
Instantiate your interface using Retrofit
Again, it’s simple:
/**
* An autenticated client to make authenticated calls
* The token is automaticly added in the Header of the request
* @param ctx
* @return OAuthServerIntf instance
*/
public static OAuthServerIntf getOAuthClient(Context ctx) {
// now it's using the cach
// Using my HttpClient
Retrofit raCustom = new Retrofit.Builder()
.client(getOAuthOkHttpClient(ctx))
.baseUrl(BASE_URL)
.addConverterFactory(new StringConverterFactory())
.addConverterFactory(MoshiConverterFactory.create())
.build();
OAuthServerIntf webServer = raCustom.create(OAuthServerIntf.class);
return webServer;
}
The trick here is that you don’t use a simple Http client, no, you use one that automatically set the header of your request as expected:
public static OkHttpClient getOAuthOkHttpClient(Context ctx) {
// Define the OkHttp Client with its cache!
// Assigning a CacheDirectory
File myCacheDir=new File(ctx.getCacheDir(),"OkHttpCache");
// You should create it...
int cacheSize=1024*1024;
Cache cacheDir=new Cache(myCacheDir,cacheSize);
Interceptor oAuthInterceptor=new OAuthInterceptor();
HttpLoggingInterceptor httpLogInterceptor=new HttpLoggingInterceptor();
httpLogInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
return new OkHttpClient.Builder()
.cache(cacheDir)
.addInterceptor(oAuthInterceptor)
.addInterceptor(httpLogInterceptor)
.build();
}
You added a oAuthInterceptor, which is in fact a simple class:
public class OAuthInterceptor implements Interceptor {
private static final String TAG = "OAuthInterceptor";
private String accessToken,accessTokenType;
@Overridepublic Response intercept(Chain chain) throws IOException {
//find the token
OAuthToken oauthToken=OAuthToken.Factory.create();
accessToken=oauthToken.getAccessToken();
accessTokenType=oauthToken.getTokenType();
//add it to the request
Request.Builder builder = chain.request().newBuilder();
if (!TextUtils.isEmpty(accessToken)&&!TextUtils.isEmpty(accessTokenType)) {
Log.e(TAG,"In the interceptor adding the header authorization with : "+accessTokenType+" " + accessToken);
builder.header("Authorization",accessTokenType+" " + accessToken);
}else{
Log.e(TAG,"In the interceptor there is a fuck with : "+accessTokenType+" " + accessToken);
//you should launch the loginActivity to fix that:
Intent i = new Intent(MyApplication.instance, MainActivity.class);
i.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
MyApplication.instance.startActivity(i);
}
//proceed to the call
return chain.proceed(builder.build());
}}
This class intercepts each request sent and adds “”Authorization”,accessTokenType+” “ + accessToken” to the header of the request.
Simple, straight, easy.
Remark that if the token is empty, we relaunch the LoginActivity which is charge of managing the token (the whole authentication process is handle by it).
Make your call
Once again, it’s really simple:
OAuthServerIntf server=RetrofitBuilder.getOAuthClient(this);
Call<GDriveFiles> listFilesCall=server.listFiles();
listFilesCall.enqueue(new Callback<GDriveFiles>() {
@Override
public void onResponse(Call<GDriveFiles> call, Response<GDriveFiles> response) {
Log.e(TAG,"The call listFilesCall succeed with [code="+response.code()+" and has body = "+response.body()+" and message = "+response.message()+" ]");
//ok we have the list of files on GDrive
if(response.code()==200&&response.body()!=null){
txvResult.setText(response.body().toString());
}else if(response.code()==400){
txvResult.setText(response.message()+"\r\n"+getString(R.string.http_code_400));
}else if(response.code()==401){
txvResult.setText(response.message()+"\r\n"+getString(R.string.http_code_401));
}else if(response.code()==403){
txvResult.setText(response.message()+"\r\n"+getString(R.string.http_code_403));
}else if(response.code()==404){
txvResult.setText(response.message()+"\r\n"+getString(R.string.http_code_404));
}
} @Override
public void onFailure(Call<GDriveFiles> call, Throwable t) {
Log.e(TAG,"The call listFilesCall failed",t);
}
});
You instantiate your interface using your RetrofitBuilder and take care to use the OAuthClient (not the simple one).
You instantiate your Call, you do it using enqueue and in the call back you have your server’s response.
And you did it !!
Important details
Managing the token expiration
Your token will expired, so you need to check it’s expiration date and to renew it.
To do that,I made my OAuthToken object smart, he has two method save and restore, where:
public class OAuthToken {
/***********************************************************
* Attributes
**********************************************************/
@Json(name = "access_token")
private String accessToken;
@Json(name = "token_type")
private String tokenType;
@Json(name = "expires_in")
private long expiresIn;
private long expiredAfterMilli = 0;
@Json(name = "refresh_token")
private String refreshToken;
When we save it, we calculate the expirationDate in millis.
When we restore it, we look at this expiration date, if the date is past, we return set the field token to null.
When the login activity (first activity launch of your application) wants to know if we had a token, there is two cases.
- OAuthToken is null => we need to run from the first step
- OAuthToken !=null but token == null => we need to refresh the token
- OAuthToken =!= null and token !null => the token is still good, we can use it
So when we detect OAuthToken != null and token ==null, we launch the refresh process, which is quiet simple. We just need to send again to the apis server the refreshToken (we received with the token), our clientId and the grantType (which is still “authorization_code”).
Ok, to do that, guess what, we still use retrofit:
public interface OAuthServerIntf {
...
/**
* The call to refresh a token
*/
@FormUrlEncoded
@POST("oauth2/v4/token")
Call<OAuthToken> refreshTokenForm(
@Field("refresh_token")String refresh_token,
@Field("client_id")String client_id,
@Field("grant_type")String grant_type);
...
Same as before, we instantiate this interface using Retrofit, and then we use it like that:
/**
* Refresh the OAuth token
*/
private void refreshTokenFormUrl(OAuthToken oauthToken) {
OAuthServerIntf oAuthServer = RetrofitBuilder.getSimpleClient(this);
Call<OAuthToken> refreshTokenFormCall = oAuthServer.refreshTokenForm(
oauthToken.getRefreshToken(),
CLIENT_ID,
GRANT_TYPE_REFRESH_TOKEN
);
refreshTokenFormCall.enqueue(new Callback<OAuthToken>() {
@Override
public void onResponse(Call<OAuthToken> call, Response<OAuthToken> response) {
Log.e(TAG, "===============New Call==========================");
Log.e(TAG, "The call refreshTokenFormUrl succeed with code=" + response.code() + " and has body = " + response.body());
//ok we have the token
response.body().save();
startMainActivity(true);
} @Override
public void onFailure(Call<OAuthToken> call, Throwable t) {
Log.e(TAG, "===============New Call==========================");
Log.e(TAG, "The call refreshTokenFormCall failed", t); }
});
}
And it’s done, you have refereshed your token.
Storing the Token
To store the token, I use the SharedPreferences object:
/***********************************************************
* Managing Persistence
**********************************************************/
public void save() {
Log.e(TAG, "Saving the following element " + this);
//update expired_after
expiredAfterMilli = System.currentTimeMillis() + expiresIn * 1000;
Log.e(TAG, "Savng the following element and expiredAfterMilli =" + expiredAfterMilli+" where now="+System.currentTimeMillis()+" and expired in ="+ expiresIn);
SharedPreferences sp = MyApplication.instance.getSharedPreferences(OAUTH_SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor ed = sp.edit();
ed.putString(SP_TOKEN_KEY, accessToken);
ed.putString(SP_TOKEN_TYPE_KEY, tokenType);
ed.putLong(SP_TOKEN_EXPIRED_AFTER_KEY, expiredAfterMilli);
ed.putString(SP_REFRESH_TOKEN_KEY, refreshToken);
ed.commit();
}
The point here, which is a security breach, is I don’t encrypt the token when storing it.
This class show you how to implement encryption based on cipher and Base64: https://github.com/sveinungkb/encrypted-userprefs/blob/master/src/SecurePreferences.java
Application inner workflow
A big question for Android developers is how I shape my code to implement properly this workflow.
I think, the good practice is to have a LoginActivity which here to handle all the OAuth process describes in this document. This LoginActivity is the start activity. It checks the state of the token, fix it if needed and then launch your mainActivity.
This way, you don’t have piece of OAuth code all around your appluication. This is a really important element to take into account.
Next step: AccountManager
How long your token should last ?
This question is simple but the answer is not.
To have a good user experience we perhaps need to make calls in background to retrieve information and display them to the user when he launchs the application (even if he has no connection at that moment). So for us, token should not die. For information, it’s the way Google, CaptainTrain, Twitter, Facebook… are doing.
If we want to be able to do such a stuff, which leads to great user experience, we need to store our token in an system object called Account, where you can store in a secure way your credentials.