Using Service-to-Service Authentication

This page shows how to use service-to-service authentication in a gRPC service by walking through a complete example, including how to configure the ESP in a gRPC service to support authenticated requests, and how to call the service from a gRPC client. It assumes that you've already read Authenticating Users, which also describes ESP's other supported authentication methods.

In our example, we'll set up and use the very simplest form of service-to-service authentication, where the client uses their Google Cloud Platform service account to generate authenticating JWTs. The approach for other authentication methods is similar, though the client-side process for getting valid authentication tokens depends on the authentication method used.

Google projects and setup

This guide uses the Bookstore example we used in our Tutorials. If you want to work along with the steps yourself:

  1. Clone the git repo where the gRPC example code is hosted:

    git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git
    
  2. Change your working directory:

    cd python-docs-samples/endpoints/bookstore-grpc/
    

We'll use deployment to Container Engine in our example, though the authentication setup is the same for Compute Engine/Docker.

In the example, we'll talk about two Google Cloud Platform projects:

  • the service producer project, which is the project that owns the gRPC Endpoints service
  • the service consumer project, which is the project that owns the gRPC client

If you're working through the example yourself, it's OK to use the same project for both. Follow the instructions in the Tutorials to set up a project if you don't have one already.

Creating the consumer service account and key

First let's create the service account and key for our consumer project, as we'll need the details for this account to set up authentication in the next section, telling ESP that clients from this service account are allowed to use our service.

  1. In the Cloud Platform Console, go to APIs & services.

    APIs & services

  2. Make sure you are in your consumer project.
  3. On the Credentials page, select Service Account Key from the Create Credentials drop-down list.
  4. In the Create service account key page, select New service account from the Service account drop-down list. If you have an existing service account that you'd like to use, you can select this instead.
  5. If you're creating a new service account, type an account name and select Roles > Project > Service Account Actor. A corresponding Service account ID is created for you: make note of the full ID, as you'll need it in the following sections.

    service-account-name@your_project_name.iam.gserviceaccount.com
    
  6. Ensure the JSON key type is selected.

  7. Click Create. Your service account JSON key file is downloaded to your local machine. Note the location and make sure it's stored securely, you'll also need to use this later to generate tokens.

Configuring authentication for the service

Now let's look at what you need to do to set up authentication requirements for our gRPC Endpoints service. If you are using two separate producer and consumer projects, don't forget to use the producer project for all steps in this section.

Set up authentication in the gRPC API Configuration

Authentication for ESP is configured in your gRPC API Configuration YAML file in its authentication section. The configuration with authentication for our example service is in api_config_auth.yaml.

authentication:
  providers:
  - id: google_service_account
    # Replace SERVICE-ACCOUNT-ID with your service account's email address.
    issuer: SERVICE-ACCOUNT-ID
    jwks_uri: https://www.googleapis.com/robot/v1/metadata/x509/SERVICE-ACCOUNT-ID
  rules:
  # This auth rule will apply to all methods.
  - selector: "*"
    requirements:
      - provider_id: google_service_account

The providers section specifies the authentication provider(s) you want to use - in this case, that you want to use a Google service account as an authentication provider. The rules section specifies that you require tokens from this provider for access to all your service's methods.

In your own copy of this file from the cloned repo:

  • change MY_PROJECT_ID to your producer project ID
  • change SERVICE-ACCOUNT-ID in the authentication section (in both the issuer and jwks_uri values) to the full consumer service account ID you noted in the last section. This tells ESP that you want to grant access to your service to users who provide valid tokens from this particular service account.

Save the file for the next step.

Deploy the configuration and service

These steps are the same as in the Getting Started with Endpoints for Container Engine:

  1. Deploy your service configuration to Endpoints: you'll need to do this even if you did so for the tutorial, as we're using a different configuration. Note the returned service name.

    gcloud beta service-management deploy out.pb api_config_auth.yaml --project PRODUCER_PROJECT
    
  2. Create a container cluster and authenticate kubectl to the cluster, if you haven't already done so.

  3. Deploy the sample API and ESP to the cluster. If you are using separate producer and consumer projects, first make sure that you have set the appropriate project within the gcloud command-line tool:

    gcloud config set project PRODUCER_PROJECT
    

Calling authenticated methods from a gRPC client

Finally, on the client side, let's look at how to use the service account key to generate a JWT token and then use the token to call an authenticated Bookstore method. If you want to work along with the examples, first install the appropriate Python requirements to both generate the token and run the example client. Ensure that you are in the python-docs-samples/endpoints/bookstore-grpc folder of your cloned client, then:

virtualenv bookstore-env
source bookstore-env/bin/activate
pip install -r requirements.txt

Generate a JWT token

In our example, the Bookstore is using service-to-service authentication where the calling service is purely authenticated by its service account, so creating an appropriate token to send with our requests is simple. Note that you can also require more stringent service-to-service authentication where the generated token must be further authenticated by Google (a Google ID token).

For this example, we've provided a Python script that can generate a token from the JSON key file downloaded earlier, using a dummy user ID and email.

#!/usr/bin/env python

# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Example of generating a JWT signed from a service account file."""

import argparse
import json
import time

import google.auth.crypt
import google.auth.jwt

"""Max lifetime of the token (one hour, in seconds)."""
MAX_TOKEN_LIFETIME_SECS = 3600


def generate_jwt(service_account_file, issuer, audiences):
    """Generates a signed JSON Web Token using a Google API Service Account."""
    with open(service_account_file, 'r') as fh:
        service_account_info = json.load(fh)

    signer = google.auth.crypt.RSASigner.from_string(
        service_account_info['private_key'],
        service_account_info['private_key_id'])

    now = int(time.time())

    payload = {
        'iat': now,
        'exp': now + MAX_TOKEN_LIFETIME_SECS,
        # aud must match 'audience' in the security configuration in your
        # swagger spec. It can be any string.
        'aud': audiences,
        # iss must match 'issuer' in the security configuration in your
        # swagger spec. It can be any string.
        'iss': issuer,
        # sub and email are mapped to the user id and email respectively.
        'sub': '12345678',
        'email': 'user@example.com'
    }

    signed_jwt = google.auth.jwt.encode(signer, payload)
    return signed_jwt


if __name__ == '__main__':
    parser = argparse.ArgumentParser(
        description=__doc__,
        formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument('--file',
                        help='The path to your service account json file.')
    parser.add_argument('--issuer', default='', help='issuer')
    parser.add_argument('--audiences', default='', help='audiences')

    args = parser.parse_args()

    signed_jwt = generate_jwt(args.file, args.issuer, args.audiences)
    print(signed_jwt)

To generate a token using the script:

  • Use the following command to generate a JWT token and assign it to the variable $JWT_TOKEN:

    JWT_TOKEN=$(python jwt_token_gen.py --file=SERVICE_ACCOUNT_FILE --audiences=SERVICE_NAME --issuer=SERVICE-ACCOUNT-ID)
    

    where:

    • SERVICE_ACCOUNT_FILE is the downloaded consumer service account JSON key file
    • SERVICE_NAME is the name of the Bookstore service that was returned when we deployed its updated service configuration to Endpoints
    • SERVICE-ACCOUNT-ID is the full consumer service account ID we got when generating our service account

Make an authenticated gRPC call

This last step uses bookstore_client.py, which is the same client used in the Tutorials. To make an authenticated call, the client passes the JWT as metadata with their method call.

def run(host, port, api_key, auth_token, timeout):
    """Makes a basic ListShelves call against a gRPC Bookstore server."""

    channel = grpc.insecure_channel('{}:{}'.format(host, port))

    stub = bookstore_pb2.BookstoreStub(channel)
    metadata = []
    if api_key:
        metadata.append(('x-api-key', api_key))
    if auth_token:
        metadata.append(('authorization', 'Bearer ' + auth_token))
    shelves = stub.ListShelves(empty_pb2.Empty(), timeout, metadata=metadata)
    print('ListShelves: {}'.format(shelves))

To run the example:

  1. Use kubectl get services to get the external IP address for our deployed Bookstore:

    #kubectl get services
    NAME                 CLUSTER-IP      EXTERNAL-IP      PORT(S)           AGE
    echo                 10.11.246.240   104.196.186.92   80/TCP            10d
    endpoints            10.11.243.168   104.196.210.50   80/TCP,8090/TCP   10d
    esp-grpc-bookstore   10.11.254.34    104.196.60.37    80/TCP            1d
    kubernetes           10.11.240.1     <none>           443/TCP           10d
    

    In this case, it's the esp-grpc-bookstore service and its external IP is 104.196.60.37.

  2. Assign the IP address to the variable EXTERNAL_IP

    EXTERNAL_IP=104.196.60.37
    
  3. Run the following command to list all the shelves from the Bookstore service:

    python bookstore_client.py --port=80 --host=$EXTERNAL_IP --auth_token=$JWT_TOKEN
    

The service should return all the shelves in the current Bookstore. You can double check it by not providing a token, or by specifying the wrong service account ID when generating the JWT: the command should fail.

Monitor your resources on the go

Get the Google Cloud Console app to help you manage your projects.

Send feedback about...

Cloud Endpoints with gRPC