Authentication between services

You can implement authentication between services by using a service account in a gRPC service. This page demonstrates service-to-service authentication by walking you through a complete example, including how to configure the Extensible Service Proxy (ESP) in a gRPC service to support authenticated requests and how to call the service from a gRPC client.

In order for any service to make authenticated calls into an Cloud Endpoints API, the calling service must have a service account and it must send an auth token in the call. The caller must use a Google ID token or a custom JSON Web Token (JWT) that is signed only by the service account of the caller. ESP validates that the iss claim in the JWT matches the issuer setting in the service configuration. ESP doesn't check for Identity and Access Management permissions that have been granted on the service account.

In our example, you set up and use the simplest form of service-to-service authentication, where the client uses their Google Cloud 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.

Before you begin

This guide uses the Bookstore example used in our Tutorials.

  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/
    
  3. Follow the instructions in the Tutorials to set up a project if you don't have one already.

In this example, you use deployment to Google Kubernetes Engine, though the authentication setup is the same for Compute Engine.

In the example, there are two Google Cloud Platform projects that are referenced:

  • The service producer project, which is the project that owns the Cloud Endpoints for gRPC service.
  • The service consumer project, which is the project that owns the gRPC client.

Creating the consumer service account and key

To create the service account and key for the consumer project:

  1. In the Google Cloud console, go to APIs & services.

    APIs & services

    Make sure you are in your consumer project.
  2. On the Credentials page, in the Create Credentials drop-down list, select Service Account Key.
  3. On the Create service account key page, if you have an existing service account that you'd like to use, select it. Otherwise, in the Service account drop-down list, select New service account and type an account name.

    A corresponding Service account ID is created for you. Make note of the ID, as it is needed in the following sections. For example:

    service-account-name@YOUR_PROJECT_ID.iam.gserviceaccount.com
    
  4. Click the Role drop-down list and select the following roles:

    • Service Accounts > Service Account User
    • Service Accounts > Service Account Token Creator
  5. Ensure the JSON key type is selected.

  6. Click Create. Your service account JSON key file is downloaded to your local machine. Note the location and make sure it's stored securely because it is used later to generate tokens.

Configuring authentication for the service

Use the producer project for all steps in this section.

Set up authentication in the gRPC API configuration

Authentication for ESP is configured in the authentication section of your gRPC API configuration YAML file. The configuration with authentication for this 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 consumer service account ID you noted in the previous section. This tells ESP that you want to grant access to your service to users who provide valid tokens from this particular service account.
  • Optionally, add jwt_locations under the providers element. You can use this value to define a custom JWT location. The default JWT locations are the Authorization metadata (prefixed by "Bearer ") and the X-Goog-Iap-Jwt-Assertion metadata.

Save the file for the next step.

Deploy the configuration and service

These steps are the same as in Getting started with gRPC on GKE:

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

    gcloud endpoints services deploy api_descriptor.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, you can use the service account key to generate a JWT token and then use the token to call an authenticated Bookstore method. 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 this 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 (using a Google Id token).

For this example, the provided Python script 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) 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': issuer,
        '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.decode('utf-8'))

To generate a token using the script:

  • 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 you deployed its updated service configuration to Endpoints.
    • [SERVICE-ACCOUNT-ID] is the full consumer service account ID when you generated your 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, use_tls, servername_override, ca_path):
    """Makes a basic ListShelves call against a gRPC Bookstore server."""

    if use_tls:
        with open(ca_path, 'rb') as f:
            creds = grpc.ssl_channel_credentials(f.read())
        channel_opts = ()
        if servername_override:
            channel_opts += ((
                        'grpc.ssl_target_name_override', servername_override,),)
        channel = grpc.secure_channel(f'{host}:{port}', creds, channel_opts)
    else:
        channel = grpc.insecure_channel(f'{host}:{port}')

    stub = bookstore_pb2_grpc.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(f'ListShelves: {shelves}')

To run the example:

  1. Use kubectl get services to get the external IP address for the 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. List all the shelves from the Bookstore service:

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

    The service returns 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.

What's next