Setting up a token service for iOS apps

This tutorial shows how to connect your iOS app to Google Cloud using a component called a token service. The token service provides short-lived OAuth 2.0 tokens, which apps can use to communicate with the APIs on Google Cloud. Apps need an OAuth 2.0 token because the APIs require the caller to be authenticated and authorized before sending a request.

Problem statement

Client apps need to send authenticated requests to APIs on Google Cloud to complete their work. You could just embed the credentials in the mobile app code to authenticate your requests. However, if you use this approach, a malicious actor could retrieve the credentials stored in the app on the device and use them for unintended purposes.

Instead, you can use a token service to ensure that credentials are not stored in the devices. The service can provide short-lived tokens that apps can use to authenticate the requests. When using a service, you must pay special attention to how the tokens are delivered to the apps in the mobile devices to avoid sending tokens to the wrong recipient.

The token service goals are:

  • To avoid the risk of including credentials in the code of the app.
  • To reduce the risk involved in sending tokens to remote apps.

Solution overview

The token service is implemented on Cloud Functions for Firebase. The function handles requests from client apps and retrieves or creates OAuth 2.0 token credentials using the IAM API. The tokens are then stored in a Firestore database to make them available for future requests until they expire. To ensure clients receive valid tokens, the service checks that credentials haven't expired before sending the token in a push notification by using Firebase Cloud Messaging.

The following diagram shows the flow to request a token from the service:

Solution high-level architecture

The flow includes the following tasks:

  1. The client app sends a request to the token service, which includes the client's device ID.
  2. To process the request, the token service tries to retrieve a token from the Firestore database.
    1. If the token hasn't expired, then the token service triggers a push notification back to the device with the token.
    2. If a token doesn’t exist in the database or if it has expired, then the token service requests a new token from IAM and stores it in the database.
  3. The service uses Firebase Cloud Messaging to send the token to the client in a push notification along with the expiration time.
  4. The client app receives the token and it’s expiration time in the push notification payload. The client app can use the token to send authenticated requests to the APIs on Google Cloud.

Using the token service as explained provides the following advantages:

  • The credentials aren't embedded in the app installed on the device.
  • Firebase Cloud Messaging uses the device ID to ensure that the push notification is delivered to the device that initiated the request.

Costs

Consider the following potential costs associated with running the samples in this tutorial:

  • Firebase has a free level of usage. If your usage of these services is less than the limits specified in the Spark plan, there is no charge for using Firebase. For more information, check Firebase Pricing plans.
  • Firebase defines quotas for Cloud Functions usage that specify resource, time, and rate limits. For more information, check Quotas and limits in the Firebase documentation.

You can also estimate the costs using the Google Cloud Pricing Calculator.

Before you begin

To complete this tutorial, you need the following software:

Cloning the sample code

Clone the token service code:

git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git

Creating a Firebase project

  1. Create a Firebase account or log into an existing account.

  2. Click Add project.

  3. In Project name, enter: token-service-example. Make a note of the Project ID assigned to your project, it's used in multiple steps in this tutorial.

  4. Follow the remaining setup steps and click Create project.

  5. After the wizard provisions your project, click Continue.

  6. In the Overview page of your project, click the Settings gear and then click Project settings.

  7. Click Add Firebase to your iOS app.

  8. In iOS bundle ID, enter the bundle ID of your iOS app.

  9. Click Register app.

  10. Follow the steps in the Download config file section to add the GoogleService-Info.plist file to the root of your Xcode project.

  11. Click Next in the Download config file section.

  12. Click Next in the Add Firebase SDK section.

  13. Click Next in the Add initialization code section.

  14. Click Skip this step in the Run your app to verify installation section.

  15. Complete the steps to configure Apple push notifications with Firebase Cloud Messaging.

Enabling anonymous authentication for the Firebase project

There are a variety of sign-in providers you can configure to connect to your Firebase project. This tutorial uses anonymous authentication for demonstration purposes.

  1. From the left menu of the Firebase console, click Authentication in the Develop group. If the left menu is not displayed, click your project from the list of available projects.

  2. Click Set up sign-in method.

  3. Select Anonymous.

  4. Turn the Enable toggle on and click Save.

Creating a service account

The token service uses the IAM API to request short-lived credentials that provide client access to another API. For more information, check Creating short-lived service account credentials.

To create a service account with the required permissions:

  1. From the left menu of the Firebase console, next to the token-service-example project home, select the Settings gear and then Project settings.

  2. Select Service accounts and then Manage service account permissions.

  3. Click Create Service Account.

  4. Configure the following settings:

    1. In Service account name, enter token-service-client.
    2. In Role, select Project > Dialogflow API Client.
  5. Click Create.

Adding token creator permissions

The Cloud Function for Firebase runs under the App Engine default service account, which requires permissions to create short-lived credentials. To add the required permissions, add the Service Account Token Creator role to the App Engine default service account:

  1. Open the IAM page in the Cloud Console.

    Open the IAM page

  2. Find the entry that displays App Engine default service account in the Name column and click the Edit member button at the end of the row.

  3. Click Add Another Role.

  4. From the Select a role menu, select Service Accounts > Service Account Token Creator.

  5. Click Save.

Enabling billing and APIs for the Google Cloud project

This tutorial requires the IAM API. To enable the API:

  1. In the Google Cloud Console, select the token-service-example project.

    Go to the Projects page

  2. Make sure that billing is enabled for your Cloud project. Learn how to confirm that billing is enabled for your project.

  3. Enable the IAM API.

    Enable the API

Building and deploying the token service

To build and deploy the token service, you must install the Firebase CLI, create the project structure for the Cloud Function for Firebase, and replace the default function with the token service function.

  1. Install the Firebase CLI.

    npm install -g firebase-tools
    
  2. Sign in to Firebase.

    firebase login
    
  3. Create the project structure for the function.

    firebase init functions
    
  4. Replace the default index.js file in the project structure with the index.js file in the functions/tokenservice/functions folder of the nodejs-docs-samples repository.

  5. Replace the SERVICE-ACCOUNT-NAME and YOUR_PROJECT_ID placeholders in the generateAccessToken() function with token-service-client and your Firebase project ID, respectively. The placeholders are part of the service account name, which is in the form:

    SERVICE-ACCOUNT-NAME@YOUR_PROJECT_ID.iam.gserviceaccount.com
    
  6. (Optional) Check the declaration of the OAuth 2.0 scope in the index.js file, which is set to https://www.googleapis.com/auth/dialogflow to allow access to the Dialogflow API. To find the scope required for other APIs, check OAuth 2.0 Scopes for Google APIs.

  7. Deploy the token service function.

    firebase deploy --only functions
    

Exploring the code

Client apps can get short-lived credentials by calling the getOAuthToken() function of the token service. The getOAuthToken() method performs the following tasks:

  1. Checks that the current request is authenticated. The function throws an error if the request isn't authenticated.
  2. Tries to retrieve a token from the database and performs one of the following actions:
    1. If there's a token in the database and the token doesn't expire within five minutes, then the function returns the token to the client.
    2. If the database doesn't have a token or the token expires within five minutes, then the function creates a new token using the IAM API and passes it to the client. The function saves the token in the database to serve future requests.

The following code example shows the getOAuthToken() function:

exports.getOAuthToken = functions.https.onCall(async (data, context) => {
  // Checking that the user is authenticated.
  if (!context.auth) {
    // Throwing an HttpsError so that the client gets the error details.
    throw new functions.https.HttpsError(
      'failed-precondition',
      'The function must be called ' + 'while authenticated.'
    );
  }
  // Retrieve the token from the database
  const docRef = db.collection('ShortLivedAuthTokens').doc('OauthToken');

  const doc = await docRef.get();

  try {
    if (doc.exists && isValid(doc.data().expireTime)) {
      //push notification
      pushNotification(
        data['deviceID'],
        doc.data().accessToken,
        doc.data().expireTime
      );
      return doc.data();
    } else {
      const result = await retrieveCredentials(context);
      console.log('Print result from retrieveCredentials functions');
      console.log(result);
      pushNotification(
        data['deviceID'],
        result['accessToken'],
        result['expireTime']
      );
      return result;
    }
  } catch (err) {
    console.log('Error retrieving token', err);
    pushNotification(data['deviceID'], 'Error retrieving token', 'Error');
    // return 'Error retrieving token';
    return 'Error retrieving token';
  }
});

Apps running on your instances can authorize and interact with Google Cloud APIs through a service account. Service accounts with the necessary Identity and Access Management (IAM) roles can allow your app code to execute specific API requests. For more information, check Storing and Retrieving Instance Metadata.

Before we can create a new OAuth 2.0 access token for the client, the token service first must retrieve the credentials for the service account that has the proper permissions to create a new token. To do this, the token service makes a request to the /computeMetadata/v1/instance/service-accounts/default/token endpoint of the IAM API, as shown in the following code:

const retrieveCredentials = context => {
  return new Promise(resolve => {
    // To create a new access token, we first have to retrieve the credentials
    // of the service account that will make the generateTokenRequest().
    // To do that, we will use the App Engine Default Service Account.
    const options = {
      host: 'metadata.google.internal',
      path: '/computeMetadata/v1/instance/service-accounts/default/token',
      method: 'GET',
      headers: {'Metadata-Flavor': 'Google'},
    };

    const get_req = http.get(options, res => {
      let body = '';

      res.on('data', chunk => {
        body += chunk;
      });

      res.on('end', async () => {
        const response = JSON.parse(body);
        const result = await generateAccessToken(
          context,
          response.access_token,
          response.token_type
        );
        return resolve(result);
      });
    });
    get_req.on('error', e => {
      //console.log('Error retrieving credentials', e.message);
      return `Error retrieving token${e.message}`;
    });
    get_req.end();
  });
};
exports.retrieveCredentials = retrieveCredentials;

You can use the credentials of the App Engine default service account to create an OAuth 2.0 token on behalf of the token-service-client account. By creating the token using the App Engine default service account you can limit the OAuth scopes assigned to the token. The following code example shows how the token service uses the generateAccessToken() function to create an OAuth 2.0 token:

const generateAccessToken = (
  context,
  serviceAccountAccessToken,
  serviceAccountTokenType
) => {
  // With the service account's credentials, we can make a request to generate
  // a new token for a 2nd service account that only has the permission to
  // act as a Dialogflow Client
  return new Promise(resolve => {
    const post_options = {
      host: 'iamcredentials.googleapis.com',
      path:
        '/v1/projects/-/serviceAccounts/SERVICE-ACCOUNT-NAME@YOUR_PROJECT_ID.iam.gserviceaccount.com:generateAccessToken',
      method: 'POST',
      headers: {
        // Set Service Account Credentials
        Authorization: `${serviceAccountTokenType} ${serviceAccountAccessToken}`,
      },
    };

    // Set up the request
    let oauthToken = '';
    const post_req = https.request(post_options, res => {
      res.setEncoding('utf8');
      res.on('data', chunk => {
        oauthToken += chunk;
      });
      res.on('end', () => {
        // Next step in pipeline
        saveOAuthToken(context, JSON.parse(oauthToken));
        return resolve(JSON.parse(oauthToken));
      });
    });

    post_req.on('error', e => {
      console.log('ERROR generating new token', e.message);
      return 'Error retrieving token';
    });

    // Sets up the scope that we want the end user to have.
    const body = {
      delegates: [],
      scope: ['https://www.googleapis.com/auth/dialogflow'],
      lifetime: '3599s',
    };

    // post the data
    post_req.write(JSON.stringify(body));
    post_req.end();
  });
};

Now that the token has been created, the service can send it back to the calling client in a push notification. The following code example shows the pushNotification() function, which sends the token to the client:

const pushNotification = (deviceID, accessToken, expiryDate) => {
  //Passing the device id of the requested device which has requested for PN
  const tokens = [deviceID];
  //Push notification payload with expiry date as title and access token as body
  //Though payload can be consructed in different ways just for simplicity we had choosen this
  const payload = {
    notification: {
      title: expiryDate,
      body: accessToken,
      sound: 'default',
      badge: '1',
    },
  };
  //triggers push notification to the targeted devices.
  return admin.messaging().sendToDevice(tokens, payload);
};

For more details about the token service, check the index.js file that defines the Cloud Function for Firebase.

Calling the token service from the client app

  1. Set up the Auth Library for Swift by adding the following line to your pod file:

    pod 'AuthLibrary', :git => 'https://github.com/googleapis/google-auth-library-swift.git'
    
  2. Call the getToken() method of FCMTokenProvider passing the device ID as a parameter. To learn how to get the device ID, check access the registration token in the Firebase documentation.

    FCMTokenProvider.getToken(deviceID: deviceID) { (shouldWait, token, error) in }
    

    The getToken() method returns the token if it's available locally. Otherwise, it returns shouldWait = true, to indicate that the token will be received in a push notification.

Once your app has a valid token, you can use it to make authenticated calls by including the token in the Authorization header of your HTTP requests. For examples on how to make authenticated requests to the Google Cloud APIs, check the Stopwatch and Speech to Speech code samples.

Cleaning up

To avoid incurring charges to your Google Cloud account for the resources used in this tutorial:

Delete the Google Cloud and Firebase project

The simplest way to stop billing charges is to delete the project you created for this tutorial. Although you created the project in the Firebase Console, you can also delete it in the Google Cloud console, since the Firebase and Google Cloud projects are one and the same.

  1. In the Cloud Console, go to the Manage resources page.

    Go to the Manage resources page

  2. In the project list, select the project that you want to delete and then click Delete .
  3. In the dialog, type the project ID and then click Shut down to delete the project.

Delete non-default versions your App Engine app

If you don't want to delete your Google Cloud and Firebase project, you can reduce costs by deleting the non-default versions of your App Engine flexible environment app.

  1. In the Cloud Console, go to the Versions page for App Engine.

    Go to the Versions page

  2. Select the checkbox for the non-default app version you want to delete.
  3. Click Delete to delete the app version.