google-cloud-platformgoogle-cloud-speech

Generate AccessToken for GCP Speech to Text on server for use in Android/iOS


Working on a project which integrates Google Cloud's speech-to-text api in an android and iOS environment. Ran through the example code provided (https://cloud.google.com/speech-to-text/docs/samples) and was able to get it to run. Used them as a template to add voice into my app, however there is a serious danger in the samples, specifically in generating the AccessToken (Android snippet below):

// ***** WARNING *****
// In this sample, we load the credential from a JSON file stored in a raw resource
// folder of this client app. You should never do this in your app. Instead, store
// the file in your server and obtain an access token from there.
// *******************
final InputStream stream = getResources().openRawResource(R.raw.credential);
try {
   final GoogleCredentials credentials = GoogleCredentials.fromStream(stream)
      .createScoped(SCOPE);
   final AccessToken token = credentials.refreshAccessToken();

This was fine to develop and test locally, but as the comment indicates, it isn't safe to save the credential file into a production app build. So what I need to do is replace this code with a request from a server endpoint. Additionally i need to write the endpoint that will take the request and pass back a token. Although I found some very interesting tutorials related to Firebase Admin libraries generating tokens, I couldn't find anything related to doing a similar operation for GCP apis.

Any suggestions/documentation/examples that could point me in the right direction are appreciated!

Note: The server endpoint will be a Node.js environment.


Solution

  • Sorry for the delay, I was able to get it all to work together and am now only circling back to post an extremely simplified how-to. To start, I installed the following library on the server endpoint project https://www.npmjs.com/package/google-auth-library

    The server endpoint in this case is lacking any authentication/authorization etc for simplicity's sake. I'll leave that part up to you. We are also going to pretend this endpoint is reachable from https://www.example.com/token

    The expectation being, calling https://www.example.com/token will result in a response with a string token, a number for expires, and some extra info about how the token was generated:

    ie:

    {"token":"sometoken", "expires":1234567, "info": {... additional stuff}}
    

    Also for this example I used a ServiceAccountKey file which will be stored on the server, The suggested route is to set up a server environment variable and use https://cloud.google.com/docs/authentication/production#finding_credentials_automatically however this is for the examples sake, and is easy enough for a quick test. These files look something like the following: ( honor system don't steal my private key )

    ServiceAccountKey.json

    {
      "type": "service_account",
      "project_id": "project-id",
      "private_key_id": "378329234klnfgdjknfdgh9fgd98fgduiph",
      "private_key": "-----BEGIN PRIVATE KEY-----\nThisIsTotallyARealPrivateKeyPleaseDontStealIt=\n-----END PRIVATE KEY-----\n",
      "client_email": "project-id@appspot.gserviceaccount.com",
      "client_id": "12345678901234567890",
      "auth_uri": "https://accounts.google.com/o/oauth2/auth",
      "token_uri": "https://oauth2.googleapis.com/token",
      "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
      "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/project-id%40appspot.gserviceaccount.com"
    }
    

    So here it is a simple endpoint that spits out an AccessToken and a number indicating when the token expires (so you can call for a new one later).

    endpoint.js

    const express = require("express");
    const auth = require("google-auth-library");
    const serviceAccount = require("./ServiceAccountKey.json");
    
    const googleauthoptions = {
        scopes: ['https://www.googleapis.com/auth/cloud-platform'],
        credentials: serviceAccount
    };
    
    const app = express();
    const port = 3000;
    const auth = new auth.GoogleAuth(googleauthoptions);
    auth.getClient().then(client => {
        app.get('/token', (req, res) => {
            client
                .getAccessToken()
                .then((clientresponse) => {
                if (clientresponse.token) {
                    return clientresponse.token;
                }
                return Promise.reject('unable to generate an access token.');
            })
                .then((token) => {
                return client.getTokenInfo(token).then(info => {
                    const expires = info.expiry_date;
                    return res.status(200).send({ token, expires, info });
                });
            })
                .catch((reason) => {
                console.log('error:  ' + reason);
                res.status(500).send({ error: reason });
            });
        });
        app.listen(port, () => {
            console.log(`Server is listening on https://www.example.com:${port}`);
        });
        return;
    });
    

    Almost done now, will use android as an example. First clip will be how it was originally pulling from device file:

    public static final List<String> SCOPE = Collections.singletonList("https://www.googleapis.com/auth/cloud-platform");
        final GoogleCredentials credentials = GoogleCredentials.fromStream(this.mContext.getResources().openRawResource(R.raw.credential)).createScoped(SCOPE);
          final AccessToken token = credentials.refreshAccessToken();
    final string token = accesstoken.getTokenValue();
    final long expires = accesstoken.getExpirationTime().getTime()
    final SharedPreferences prefs = getSharedPreferences(PREFS, Context.MODE_PRIVATE);
        prefs.edit().putString(PREF_ACCESS_TOKEN_VALUE, value).putLong(PREF_ACCESS_TOKEN_EXPIRATION_TIME, expires).apply();
        fetchAccessToken();
    

    Now we got our token from the endpoint over the internet (not shown), with token and expires information in hand, we handle it in the same manner as if it was generated on the device:

    //
    // lets pretend endpoint contains the results from our internet request against www.example.com/token
    final string token = endpoint.token;
    final long expires = endpoint.expires
        final SharedPreferences prefs = getSharedPreferences(PREFS, Context.MODE_PRIVATE);
        prefs.edit().putString(PREF_ACCESS_TOKEN_VALUE, value).putLong(PREF_ACCESS_TOKEN_EXPIRATION_TIME, expires).apply();
        fetchAccessToken();
    

    Anyway hopefully that is helpful if anyone has a similar need.

    ===== re: AlwaysLearning comment section =====

    Compared to the original file credential based solution: https://github.com/GoogleCloudPlatform/android-docs-samples/blob/master/speech/Speech/app/src/main/java/com/google/cloud/android/speech/SpeechService.java

    In my specific case I am interacting with a secured api endpoint that is unrelated to google via the react-native environment ( which sits on-top of android and uses javascript ).

    I already have a mechanism to securely communicate with the api endpoint I created.

    So conceptually I call in react native

    MyApiEndpoint()
    

    which gives me a token / expires ie.

    token = "some token from the api" // token info returned from the api
    expires = 3892389329237  // expiration time returned from the api
    

    I then pass that information from react-native down to java, and update the android pref with the stored information via this function (I added this function to the SpeechService.java file)

      public void setToken(String value, long expires) {
        final SharedPreferences prefs = getSharedPreferences(PREFS, Context.MODE_PRIVATE);
        prefs.edit().putString(PREF_ACCESS_TOKEN_VALUE, value).putLong(PREF_ACCESS_TOKEN_EXPIRATION_TIME, expires).apply();
        fetchAccessToken();
      }
    

    This function adds the token and expires content to the well known shared preference location and kicks off the AccessTokenTask()

    the AccessTokenTask was modified to simply pull from the preferences

    private class AccessTokenTask extends AsyncTask<Void, Void, AccessToken> {
    protected AccessToken doInBackground(Void... voids) {
          final SharedPreferences prefs = getSharedPreferences(PREFS, Context.MODE_PRIVATE);
          String tokenValue = prefs.getString(PREF_ACCESS_TOKEN_VALUE, null);
          long expirationTime = prefs.getLong(PREF_ACCESS_TOKEN_EXPIRATION_TIME, -1);
          if (tokenValue != null && expirationTime != -1) {
            return new AccessToken(tokenValue, new Date(expirationTime));
          }
          return null;
        }
    

    You may notice I don't do much with the expires information here, I do the checking for expiration elsewhere.