Authentication of OAuth2 in the application through Google Sign-In. Continuous Access to Google APIs

“From April 20, 2017, sending authorization requests from built-in browsers will be blocked.”
Such a message from March 1 can be seen in some applications where authorization is required. Google wrote about this in his blog in August 2016, and this means that soon many applications will have to rewrite the registration implementation. It’s not very pleasant, but there is a way out - use the recommended Google Sign-in authorization method.

This method will be discussed in the lesson, as well as how to get the tokens necessary for working with the Google API.

The lesson will include a partial translation of official documents. But first, a little background from my practice and the first work with OAuth2, maybe someone will be in a similar situation.

I needed for the application to get a chat from YouTube live broadcast. And then I found out that in order to send broadcast requests (and only then chat), it is necessary to authenticate the user with OAuth2. I started to search. Information on this topic is very small, it is scattered, not suitable for my case, and of course everything was in English. Mostly the information was for working with the most popular APIs: Drive, Cloud, Google Plus. There is a ready-made code in the official YouTube API documentation, take it and use it, but it doesn’t work for Android. Having spent a considerable amount of time, by trial and error I came to a working solution. The first thing I wanted to do afterwards was to collect the information “in a heap” and put it on the shelves, which encouraged me to write this lesson.

Initially, my decision began with the fact that a WebView was opened for the user to authorize (enter email, password). Next, permission was requested to use the data, and only after permission did the authentication code (AuthCode) come in the response, more on what will be done with it later. The url that opened in WebView was as follows:

https://accounts.google.com/o/oauth2/auth?
client_id=60*********5ad3np.apps.googleusercontent.com
&redirect_uri=urn:ietf:wg:oauth:2.0:oob
&access_type=offline&response_type=code
&scope=https://www.googleapis.com/auth/youtube.readonly

This is nothing more than a post request, in response to which a page containing authCode came in, with the code in the page header. Everything, as per the recommendation to the API, and the actions to hide this code from the user were left to the developer.

Everything worked fine, the application was published, but one day I saw the following:


By clicking on the link "Details" we get to a blog where it says that in the name of security, authentication through WebView will not work since April 20. Well, I think, I just did it and will have to redo it through Sign-In. And initially I tried to make the implementation through this service. However, with the existing knowledge of “what and why” it turned out pretty quickly. So, let's begin.

1. Obtaining credentials

In the API Manager, create a new project (or select an existing one):


For authorization, you will need a configuration file, which can be obtained in the wizard :


Fill in the fields application name and package. Next, we select which service we connect (Google Sign-In), here you need to enter the SHA1 key of the application, getting it is simple: in Android Studio we find the Gradle tab, open the Tasks-android-signingReport tabs. We double-click, and the information about the keys appears in the logs. Find the key SHA1, copy.



Click the "Generate configuration file" button, and after "Download google-services.json". This json file is saved in the project folder “app”.

Important! If you are going to publish the application on Google Play, the debug key SHA1 will need to be replaced with the release key, respectively, and replace the configuration file.

We go into the API Manager, we see that the keys and OAuth client identifiers were generated. We need only Web client data (client identifier and client secret).


In the “OAuth access request window” tab, you can change the email and the product name - this is what will be written when permission is requested “Application **** asks: ...”

2. Configuring Client Sign-In

To access the Google Api Client, add to the gradle app file depending on:

compile 'com.google.android.gms:play-services-auth:10.2.0'

And the plugin (at the end of the file):

apply plugin: 'com.google.gms.google-services'

To the gradle project file depending:

classpath 'com.google.gms:google-services:3.0.0'

Configure options:

GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
				.requestServerAuthCode(getString(R.string.server_client_id))
				.requestEmail()
				.requestScopes(new Scope("https://www.googleapis.com/auth/youtube.readonly"))
				.build();

Here, the lines of greatest interest are:

requestServerAuthCode(getString(R.string.server_client_id))- we request authCode, passing the client identifier parameter (all completely), which we received above.

requestScopes(new Scope("***"))- we request the access area / areas necessary for the API used. There are some already defined areas in Scopes, but if you didn’t find one there, you can set your own, as in my case. For the user it will be displayed as access to what the application wants to receive.

Configure the client:

GoogleApiClient mApiClient = new GoogleApiClient.Builder(this)
				.enableAutoManage(this, this)
				.addApi(Auth.GOOGLE_SIGN_IN_API, gso)
				.build();

Here everything is according to the standard from the documentation:

enableAutoManage(this, this)- the activity and the connection listener are transferred to the parameters (we implement the GoogleApiClient.OnConnectionFailedListener interface).

addApi(Auth.GOOGLE_SIGN_IN_API, gso)- indicate that we use Sign In api and the previously created options object.

Now to start authorization you need a button (or any other element). You can use the standard Sign In Button. Xml markup:


It looks like this:



In activity, the button is defined like all other views, we hang the listener on it and click the button to execute the method:

@Override
public void onClick(View view) {
	switch (view.getId()) {
		case R.id.activity_button_sign_in:
			signIn();
			break;
	}
}

The code of the called method is the creation of intent and the activation call for authorization:

public void signIn() {
		Intent signInIntent = Auth.GoogleSignInApi.getSignInIntent(mApiClient);
		startActivityForResult(signInIntent, RC_AUTH_CODE);
}

We pass the configured mApiClient to the parameter. RC_AUTH_CODE any number, as always, to track the result of activity.

When you click on the button, you will be asked to select an account to log in, or add a new one. After selection, the application will request permission:


3. Obtaining Auth code

After the user gives permission, in onActivityResult we get his account data:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
	if (requestCode == RC_AUTH_CODE) {
		GoogleSignInResult result = Auth.GoogleSignInApi.getSignInResultFromIntent(data);
		if (result.isSuccess()) {
			GoogleSignInAccount acct = result.getSignInAccount();
			String authCode = acct.getServerAuthCode();
			getAccessToken(authCode);
		} 
	}
}

As a result, we get auth code as a regular line, it looks something like this:

4/iHhVmqtxccXh0Qs*********oo5XG8OjaNsWu_kEKyw

You can also get the user's email, username and avatar from the account:

acct.getEmail ()
acct.getDisplayName ()
acct.getPhotoUrl ()

This data may be needed, for example, to insert it into the header NavigationView.

4. Getting Access Token and Refresh Token

We got auth code, now it needs to be changed to the tokens necessary for API requests. To do this, we form a request at the address. https://www.googleapis.com/oauth2/v4/token.For example, I will do this using OkHttp.

public void getAccessToken(String authCode) {
	OkHttpClient client = new OkHttpClient();
	RequestBody requestBody = new FormEncodingBuilder()
			.add("grant_type", "authorization_code")
			.add("client_id", getString(R.string.server_client_id))
			.add("client_secret", getString(R.string.client_secret))
			.add("code", authCode)
			.build();
	final Request request = new Request.Builder()
			.url("https://www.googleapis.com/oauth2/v4/token")
			.header("Content-Type", "application/x-www-form-urlencoded")
			.post(requestBody)
			.build();
	client.newCall(request).enqueue(new Callback() {
		@Override
		public void onFailure(Request request, IOException e) {}
		@Override
		public void onResponse(Response response) throws IOException {
			try {
				JSONObject jsonObject = new JSONObject(response.body().string());
				mAccessToken = jsonObject.get("access_token").toString();
				mTokenType = jsonObject.get("token_type").toString();
				mRefreshToken = jsonObject.get("refresh_token").toString();
			} catch (JSONException e) {
				e.printStackTrace();
			}
		}
	});
}

Consider the parameters in more detail. In Request.Builder () We pass the url by which we get the tokens:

url("https://www.googleapis.com/oauth2/v4/token")

In the header, specify the Content-Type:

header("Content-Type", "application/x-www-form-urlencoded")

We indicate that this is the POST method, we pass body to it:

post(requestBody)

The generated requestBody must contain the following parameters:

"grant_type", "authorization_code"- indicate that we will pass the auth code
"client_id", getString(R.string.server_client_id)- the parameter is the client id received in the API Manager
"client_secret", getString(R.string.client_secret)- the client secret received in the API Manager
"code", authCode- the received code itself.

The request is asynchronous, in the response we get the usual json with all the data needed to work:

{ "access_token":"ya29.GlsfBJNMTfGy…",
"token_type":"Bearer",
"expires_in":3600,
"refresh_token":"1\/72OqA7zYuyY__XhGij5oA2nEb7…",
"id_token":"eyJhbGciOiJSUzI1NiIsImtpZ…" }

"access_token"- access token, for the sake of which everything was carried out
"expires_in"- access token lifetime, by default the token lives for 1 hour, and 25 tokens can be received per day upon request, no more.
"token_type"- the type of token, it is also necessary to remember it, it is also inserted into the request for api in the future.
"refresh_token"- a token for updating the access token when an hour of life has passed. Refresh token is unchanged. Often on the forums I saw a problem that I myself had encountered: this token did not come in the request. Errors consist of incorrect credentials, or incorrect requests. If authorization was carried out through WebView, and in the url such an important parameter as access_type = offline was not indicated, then the refresh token simply did not come.

5. Update Access Token

An hour has passed, access token is no longer active, a new one is needed. After that errors 401 or 403 will fall in, the server will say that the user is not authorized or does not have access. Requesting a new permission is not good if we need a continuous session, for example, like mine, we need to continuously receive messages from the chat during the broadcast, and this is several hours. What to do? Send a request for a new token.

The request is basically the same as in paragraph 4, with the exception of some parameters:

private void getNewAccessToken() {
	OkHttpClient client = new OkHttpClient();
	RequestBody requestBody = new FormEncodingBuilder()
				.add("refresh_token", mRefreshToken)
				.add("client_id", getString(R.string.server_client_id))
			        .add("client_secret", getString(R.string.client_secret))
				.add("grant_type", "refresh_token")
				.build();
	final Request request = new Request.Builder()
				.url("https://www.googleapis.com/oauth2/v4/token")
				.header("Content-Type", "application/x-www-form-urlencoded")
				.post(requestBody)
				.build();
	client.newCall(request).enqueue(new Callback() {
		@Override
		public void onFailure(Request request, IOException e) {}
		@Override
		public void onResponse(Response response) throws IOException {
			try {
				JSONObject jsonObject = new JSONObject(response.body().string());
				mAccessToken = jsonObject.get("access_token").toString();
			} catch (JSONException e) {
				e.printStackTrace();
			}
		}
	});
}

Here are the important parameters:

"grant_type", "refresh_token"- in the type we indicate that we are sending the refresh token
"refresh_token", mRefreshToken- and the token itself

The response will be json containing a new access token, which can again be accessed by the API:

{ "access_token":"ya29.GlsfBM7Y...",
"token_type":"Bearer",
"expires_in":3600,
"id_token":"eyJhbGciOiJ..." }

On this authorization and user authentication is completed.

For example, I’ll show you what the request to the API looks like, as well as how I update the token.
To request an API, I use Retrofit2 + RxAndroid. This is how the request to receive live chat chat looks like:

@GET(GoogleApiUrls.Youtube.CHAT)
Observable getChat(@Header("Authorization") String token, @Query("liveChatId") String liveChatId, @Query("part") String part);

It is important to note here that the type of token and the access token itself must be transmitted in the header using the Authorization key. That is, like this:

Authorization Bearer ya29.GlsfBJNMTfGy…

Next, I make a request through RxAndroid, and since all kinds of errors come to the onError callback, the HttpException error comes with the same code 401 Unauthorized after an hour. Here I process it, check if this is the same error, then I convert to the appropriate type, check if this is really 401 code and execute the method of obtaining a new token, then repeat the request.

@Override
public void onError(Throwable e) {
	if (e instanceof HttpException) {
		HttpException exception = (HttpException) e;
		if (exception.code() == 401) {
			getNewAccessToken();
		}
	}
}

There is also a GET request for checking the token:

https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=ya29.GlsfBJNMTfGy…

The response will contain data about the token, if it is still active, or an error if its lifetime has expired.

Again, the implementation of the token update by Google is left to the developer.

Logging out and revoking authorization is much easier; it’s not difficult to find in the official documentation if necessary.

I recommend that before starting work, check requests in a third-party application / extension, for example Postman, to make sure that the parameters are entered correctly and the answers are received. I will be very glad if someone finds the lesson useful!

Links to the documentation used:

1. Google Sign-In for Android
2. Youtube API. OAuth 2.0 flows

Also popular now: