Massoud Mazar

Sharing The Knowledge

NAVIGATION - SEARCH

Restrict Access to your Google App Engine API using Service Account

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:

  1. Create a Service Account (Google Developer Console, select your project, from left nav bar select APIs & Auth, then Credentials)
  2. 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)
  3. Keep a copy of the P12 Key generated for this Service Account to use in the Android Client
  4. In your App Engine API, add the "Client ID" of your service account as one of the trusted "clientIds"
  5. 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)
  6. In the Android Client, use the "GoogleCredential.Builder()" to generate a credential object based on "Email Address" and P12 Key of the service account
  7. 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.

Comments (12) -

excellent post, solved my problem!

Reply

Excellent. I read through it. I hope it works. It is very interesting that no one really knows it.

Reply

Wonderful post
I was struggling with same source from last 15 days but not able to get how to do it
Your code cleared everything only thing how to get p12 key can you please provide code it really help me a lot

Reply

Massoud Mazar

To generate the P12 key, you should login to Google Cloud Console. From the menu, select "API Manager", and then select "Credentials". Either use an existing "Service Account Key" you have already created, or create a new one (Click on "Create Credentials" and then select "Service Account Key"). When Creating the service account, you have the option to get the key in JSON format or P12 format right at the creation time.
For existing keys, click on "Manage Service Accounts", select the key and click on the options menu (3 vertical dots) on the right and select "Create Key".

Reply

Your code helps me lot
I did service acount of app-engine and thru its email and p12 key connected directly to android for google drive service .
I would like to make my app where google drive works as storage  without popup of authentication and autorization screen
https://github.com/RupamShaw/GogleApplication
Someone can get help for service account

Reply

Massoud Mazar

Thanks for sharing.

Reply

Hello,

Currently, useful information on this subject is really scarce; so thanks for the great post.

I am using both google cloud storage and google cloud endpoints in the same android application. Having one service account for both of the credentials is enough? Or should I keep 2 service accounts in developer account, also 2 credential objects in the application.

Currently having this error but I think it's probably because of my service account info in developer account:

        "error" : "deleted_client",
        "error_description" : "The OAuth client was deleted."

Thanks in advance.

Reply

Massoud Mazar

Makdut,

Many people suggest you should provide your own server side APIs to access resources like cloud storage from your app. Your API will act as the gatekeeper and you are controlling access to those backend services.
Please note the approach I explained here is not very secure. Putting the private key in the source code is not a good idea and a determined hacker can easily extract that and use it to access your backend.
A token exchange authentication with expiration date would be a more secure approach.

Massoud

Reply

Hello again,

Thanks for the response and explanation. But still a working example (project on github maybe) with these codes would be awesome Smile

Reply

Massoud,
Thanks for posting this. As many have mentioned, your post has given me hope that this is actually possible. There only difference between your use case and mine is the client is web instead Android for me. What are the roles required for the service account to access the application level APIs hosted in an App Engine ?
I tried all the steps here. But Im getting a permission issue here when i try to access the API. And the User object is always null. Im assuming the service account has to be given appropriate permissions.

Reply

Thanks for the comment Vinoth,

My post here is more than 2 years old, and was specific to Android clients. My solution was based on including the P12 private key in the Android code which is not secure. Google has made some new libraries available which I suggest you review: cloud.google.com/.../about-cloud-endpoints-frameworks

But to answer your question, see my reply to Rupam (above) on how to create the P12 key. If the Service account is only accessing your API, I do not believe there was special permission required.

Massoud

Reply

Massoud,
Thanks for a prompt reply. I appreciate it. Due to the limitations with  our architecture, Cloud endpoints are not an option. After a long struggle, finally i had figured out the issue, Service Accounts are not listing Client IDs anymore, I had mistaken Private key fingerprint for client id. It lists the client id, only when we choose the domain wide delegation option. Once I chose that, it showed the client id. Not adding the correct Client ID in my client code, made everything work. Its the shame that it is not documented properly. Lot of folks are facing this issue. Im posting it here for the benefit of everyone.

Reply

Add comment