I needed to access my restricted Google App Engine API from an Android App using a Service account. Let's say, I don't want to ask my users to authenticate with their Google accounts to my Android App, and I also don't want any third party access my App Engine API. All the documentation I found online was about how to authenticate with backend using users, but nothing about using service accounts. Even on many StackOverFlow articles people say it is not possible.
So, here is summary of steps:
- Create a Service Account (Google Developer Console, select your project, from left nav bar select APIs & Auth, then Credentials)
- Make note of "Client ID" (which will be used in the App Engine API) and "Email Address" (which will be used in the Android client)
- Keep a copy of the P12 Key generated for this Service Account to use in the Android Client
- In your App Engine API, add the "Client ID" of your service account as one of the trusted "clientIds"
- Secure your App Engine API method by adding a "User user" parameter to the signature and checking its value for null (a null value passed in user parameter means request is not authenticated)
- In the Android Client, use the "GoogleCredential.Builder()" to generate a credential object based on "Email Address" and P12 Key of the service account
- Use this credential object to instantiate the Builder object for your API
Here are some code snippets showing the details. Please note that I'm using the Google App Engine Endpoint library for Android.
The backend API is defined and secured using annotations:
package com.example.Massoud.myapplication.backend;
import com.google.api.server.spi.config.Api;
import com.google.api.server.spi.config.ApiMethod;
import com.google.api.server.spi.config.ApiNamespace;
import com.google.api.server.spi.response.UnauthorizedException;
import com.google.appengine.api.users.User;
import javax.inject.Named;
@Api(name = "myApi",
version = "v1",
namespace = @ApiNamespace(ownerDomain = "backend.myapplication.Massoud.example.com",
ownerName = "backend.myapplication.Massoud.example.com",
packagePath = ""),
clientIds = {Constants.WEB_CLIENT_ID, Constants.ANDROID_CLIENT_ID,
Constants.IOS_CLIENT_ID, Constants.SERVICE_ACCOUNT_ID},
audiences = {Constants.ANDROID_AUDIENCE}
)
public class MyEndpoint {
@ApiMethod(name = "sayHi")
public MyBean sayHi(@Named("name") String name, User user) throws UnauthorizedException {
if (user == null)
throw new UnauthorizedException("Unauthorized Access");
MyBean response = new MyBean();
response.setData("Hi, " + name);
return response;
}
}
On the Android side, I added the P12 key to my resources as a raw resource, and then it can be loaded using this code:
PrivateKey serviceAccountPrivateKey;
try {
KeyStore keystore = KeyStore.getInstance("PKCS12");
keystore.load(_resources.openRawResource(R.raw.private_key), "notasecret".toCharArray());
serviceAccountPrivateKey = (PrivateKey) keystore.getKey("privatekey", "notasecret".toCharArray());
} catch (Exception e) {
Log.e(TAG, e.getMessage());
return null;
}
Using the above private key and the Email Address of our Service account, we can create a credential object:
Credential = new GoogleCredential.Builder()
.setTransport(AndroidHttp.newCompatibleTransport())
.setJsonFactory(JacksonFactory.getDefaultInstance())
.setServiceAccountPrivateKey(serviceAccountPrivateKey)
.setServiceAccountId(_resources.getString(R.string.google_account_email))
.setServiceAccountScopes(Collections.singleton("https://www.googleapis.com/auth/userinfo.email"))
.build();
try {
Credential.refreshToken();
} catch (IOException e) {
Log.e(TAG, e.getMessage());
return null;
}
And finally we can call our App Engine API using an async task:
package com.tachyotech.cloudtest;
import android.content.Context;
import android.os.AsyncTask;
import android.util.Pair;
import android.widget.Toast;
import com.example.massoud.myapplication.backend.myApi.MyApi;
import com.google.api.client.extensions.android.http.AndroidHttp;
import com.google.api.client.extensions.android.json.AndroidJsonFactory;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import java.io.IOException;
public class EndpointsAsyncTask extends AsyncTask<Pair<Context, String>, Void, String> {
private static MyApi myApiService = null;
private Context context;
@Override
protected String doInBackground(Pair<Context, String>... params) {
context = params[0].first;
String name = params[0].second;
if(myApiService == null) { // Only do this once
try {
GoogleCredential credential = CloudAuthManager._instance.Credential;
MyApi.Builder builder = new MyApi.Builder(AndroidHttp.newCompatibleTransport(),
new AndroidJsonFactory(), credential)
.setApplicationName("myClientApp")
.setRootUrl("https://<your-app-name>.appspot.com/_ah/api/");
myApiService = builder.build();
} catch (Exception e) {
return e.getMessage();
}
}
try {
return myApiService.sayHi(name).execute().getData();
} catch (IOException e) {
return e.getMessage();
}
}
@Override
protected void onPostExecute(String result) {
Toast.makeText(context, result, Toast.LENGTH_LONG).show();
}
}
CloudAuthManager is a singleton class which contains my credential object.
3c96e42e-6c16-4158-a652-9526592865d3|3|5.0|96d5b379-7e1d-4dac-a6ba-1e50db561b04