Authentication between services

Along with authenticating users, you might need to allow other services to interact with your API. While client applications can provide users with a web sign-in prompt to submit their credentials, you need another approach for secure service-to-service communication. This page shows the approach that we recommend to implement authentication between services and provides sample code.

Overview

To identify a service that sends requests to your API, you use a service account. The calling service uses the service account's private key to sign a secure JSON Web Token (JWT) and sends the signed JWT in the request to your API.

To implement service-to-service authentication in your API and calling service:

  1. Create a service account and key for the calling service to use.
  2. Add support for authentication in the OpenAPI document for your Cloud Endpoints service.
  3. Add code to the calling service that:

    • Creates a JWT and signs it with the service account's private key.
    • Sends the signed JWT in a request to the API.

ESP validates that the claims in the JWT match the configuration in your OpenAPI document before forwarding the request to your API. ESP doesn't check for Cloud Identity permissions that you have granted on the service account.

Prerequisites

This page assumes that you have already:

Creating a service account with a key

You need a service account with a private key file that the calling service uses to sign the JWT. If you have more than one service sending requests to your API, you can create one service account to represent all the calling services. If you need to differentiate between the services—for example, they might have different permissions—you can create a service account and key for each calling service.

This section shows how to use the Google Cloud Platform Console and the gcloud command-line tool to create the service account and private key file and to assign the service account the Service Account Token Creator role. For information on using an API to do this task, see Creating and managing service accounts.

To create a service account with a key:

GCP Console

  1. In the GCP Console, go to the Create service account key page.

    Go to the Create Service Account Key page

  2. Select the project that you want to use.
  3. From the Service account drop-down menu, select New service account.
  4. In the Service account name field, enter a name.
  5. From the Role drop-down menu, select Service Account > Service Account Token Creator.
  6. For the Key type, use the default type, JSON.
  7. Click Create. A JSON file that contains the service account's private key downloads to your computer.

gcloud

You can run the following commands by using the Cloud SDK on your local machine, or within Cloud Shell.

  1. Set the default account for gcloud. If you have more than one account, make sure to choose the account that is in the GCP project that you want to use.

    gcloud auth login
    
  2. Display the project IDs for your GCP projects.

    gcloud projects list
    
  3. Set the default project. Replace PROJECT_ID with the GCP project ID that you want to use.

    gcloud config set project PROJECT_ID
  4. Create a service account. Replace SA_NAME and SA_DISPLAY_NAME with the name and display name that you want to use.

    gcloud iam service-accounts create SA_NAME \
      --display-name "SA_DISPLAY_NAME"
    
  5. Display the email address for the service account that you just created.

    gcloud iam service-accounts list
    
  6. Add the Service Account Token Creator role. Replace SA_EMAIL_ADDRESS with the service account's email address.

    gcloud projects add-iam-policy-binding PROJECT_ID \
      --member serviceAccount:SA_EMAIL_ADDRESS \
      --role roles/serviceAccountTokenCreator
    
  7. Create a service account key file in the current working directory. Replace FILE_NAME with the name that you want to use for key file. By default, the gcloud command creates a JSON file.

    gcloud iam service-accounts keys create FILE_NAME.json \
      --iam-account SA_EMAIL_ADDRESS
    

See the gcloud reference for more information about the previous commands.

For information on safeguarding the private key, see Best practices for managing credentials.

Configuring your API to support authentication

You must have a security requirement object and a security definitions object in your OpenAPI document for ESP to validate the claims in the signed JWT.

  1. Add the service account as an issuer in your OpenAPI document.

    securityDefinitions:
      DEFINITION_NAME:
        authorizationUrl: ""
        flow: "implicit"
        type: "oauth2"
        x-google-issuer: "SA_EMAIL_ADDRESS"
        x-google-jwks_uri: "https://www.googleapis.com/robot/v1/metadata/x509/SA_EMAIL_ADDRESS"
    
    • Replace DEFINITION_NAME with a string that identifies this security definition. You might want to replace it with the service account name or a name that identifies the calling service.
    • Replace SA_EMAIL_ADDRESS with the service account's email address.
    • You can define multiple security definitions in your OpenAPI document, but each definition must have a different x-google-issuer. If you have created separate service accounts for each calling service, you can create a security definition for each service account, for example:
    securityDefinitions:
      service-1:
        authorizationUrl: ""
        flow: "implicit"
        type: "oauth2"
        x-google-issuer: "service-1@example-project-12345.iam.gserviceaccount.com"
        x-google-jwks_uri: "https://www.googleapis.com/robot/v1/metadata/x509/service-1@example-project-12345.iam.gserviceaccount.com"
      service-2:
        authorizationUrl: ""
        flow: "implicit"
        type: "oauth2"
        x-google-issuer: "service-2@example-project-12345.iam.gserviceaccount.com"
        x-google-jwks_uri: "https://www.googleapis.com/robot/v1/metadata/x509/service-2@example-project-12345.iam.gserviceaccount.com"
    
  2. Optionally, add x-google-audiences to the securityDefinitions section. If you don't add x-google-audiences, ESP requires that the "aud" (audience) claim in the JWT is in the format https://SERVICE_NAME, where SERVICE_NAME is the name of your Endpoints service, which you have configured in the host field of your OpenAPI document.

  3. Add a security section at either the top level of the file (not indented or nested) to apply to the entire API, or at the method level to apply to a specific method. If you use security sections at both the API level and at the method level, the method-level settings override the API-level settings.

    security:
      - DEFINITION_NAME: []
    
    • Replace DEFINITION_NAME with the name that you used in the securityDefinitions section.
    • If you have more than one definition in the securityDefinitions section, add them in the security section, for example:

      security:
        - service-1: []
        - service-2: []
      
  4. Deploy your updated OpenAPI document. Replace OPENAPI_DOC with the name of your OpenAPI document.

    gcloud endpoints services deploy OPENAPI_DOC

Before ESP forwards a request to your API, ESP verifies:

  • The signature of the JWT by using the public key, which is located at the URI specified in the x-google-jwks_uri field in your OpenAPI document.
  • That the "iss" (issuer) claim in the JWT matches the value specified in the x-google-issuer field.
  • That the "aud" (audience) claim in the JWT contains your Endpoints service name or matches one of the values that you specified in the x-google-audiences field.
  • That the token isn't expired by using the "exp" (expiration time) claim.

For more information about x-google-issuer, x-google-jwks_uri, and x-google-audiences, see OpenAPI extensions.

Making an authenticated request to an Endpoints API

To make an authenticated request, the calling service sends a JWT signed by the service account that you specified in the OpenAPI document. The calling service must:

  1. Create a JWT and sign it with the service account's private key.
  2. Send the signed JWT in a request to the API.

The following sample code demonstrates this process.

  1. In the calling service, add the following function and pass it the following parameters:
    Java
    • saKeyfile: The full path to the service account's private key file.
    • saEmail: The service account's email address.
    • audience: If you added the x-google-audiences field to your OpenAPI document, set audience to one of the values that you specified for x-google-audiences. Otherwise, set audience to https://SERVICE_NAME, where SERVICE_NAME is your Endpoints service name.
    • expiryLength: The JWT expiration time, in seconds.
    Python
    • sa_keyfile: The full path to the service account's private key file.
    • sa_email: The service account's email address.
    • audience: If you added the x-google-audiences field to your OpenAPI document, set audience to one of the values that you specified for x-google-audiences. Otherwise, set audience to https://SERVICE_NAME, where SERVICE_NAME is your Endpoints service name.
    • expiry_length: The JWT expiration time, in seconds.
    Go
    • saKeyfile: The full path to the service account's private key file.
    • saEmail: The service account's email address.
    • audience: If you added the x-google-audiences field to your OpenAPI document, set audience to one of the values that you specified for x-google-audiences. Otherwise, set audience to https://SERVICE_NAME, where SERVICE_NAME is your Endpoints service name.
    • expiryLength: The JWT expiration time, in seconds.

    The function creates a JWT, signs it by using the private key file, and returns the signed JWT.

    Java
    /**
     * Generates a signed JSON Web Token using a Google API Service Account
     * utilizes com.auth0.jwt.
     */
    public static String generateJwt(final String saKeyfile, final String saEmail,
        final String audience, final int expiryLength)
        throws FileNotFoundException, IOException {
    
      Date now = new Date();
      Date expTime = new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(expiryLength));
    
      // Build the JWT payload
      JWTCreator.Builder token = JWT.create()
          .withIssuedAt(now)
          // Expires after 'expiraryLength' seconds
          .withExpiresAt(expTime)
          // Must match 'issuer' in the security configuration in your
          // swagger spec (e.g. service account email)
          .withIssuer(saEmail)
          // Must be either your Endpoints service name, or match the value
          // specified as the 'x-google-audience' in the OpenAPI document
          .withAudience(audience)
          // Subject and email should match the service account's email
          .withSubject(saEmail)
          .withClaim("email", saEmail);
    
      // Sign the JWT with a service account
      FileInputStream stream = new FileInputStream(saKeyfile);
      GoogleCredential cred = GoogleCredential.fromStream(stream);
      RSAPrivateKey key = (RSAPrivateKey) cred.getServiceAccountPrivateKey();
      Algorithm algorithm = Algorithm.RSA256(null, key);
      return token.sign(algorithm);
    }
    Python
    def generate_jwt(sa_keyfile,
                     sa_email='account@project-id.iam.gserviceaccount.com',
                     audience='your-service-name',
                     expiry_length=3600):
    
        """Generates a signed JSON Web Token using a Google API Service Account."""
    
        now = int(time.time())
    
        # build payload
        payload = {
            'iat': now,
            # expires after 'expirary_length' seconds.
            "exp": now + expiry_length,
            # iss must match 'issuer' in the security configuration in your
            # swagger spec (e.g. service account email). It can be any string.
            'iss': sa_email,
            # aud must be either your Endpoints service name, or match the value
            # specified as the 'x-google-audience' in the OpenAPI document.
            'aud':  audience,
            # sub and email should match the service account's email address
            'sub': sa_email,
            'email': sa_email
        }
    
        # sign with keyfile
        signer = google.auth.crypt.RSASigner.from_service_account_file(sa_keyfile)
        jwt = google.auth.jwt.encode(signer, payload)
    
        return jwt
    Go
    
    // generateJWT creates a signed JSON Web Token using a Google API Service Account.
    func generateJWT(saKeyfile, saEmail, audience string, expiryLength int64) (string, error) {
    	now := time.Now().Unix()
    
    	// Build the JWT payload.
    	jwt := &jws.ClaimSet{
    		Iat: now,
    		// expires after 'expiraryLength' seconds.
    		Exp: now + expiryLength,
    		// Iss must match 'issuer' in the security configuration in your
    		// swagger spec (e.g. service account email). It can be any string.
    		Iss: saEmail,
    		// Aud must be either your Endpoints service name, or match the value
    		// specified as the 'x-google-audience' in the OpenAPI document.
    		Aud: audience,
    		// Sub and Email should match the service account's email address.
    		Sub:           saEmail,
    		PrivateClaims: map[string]interface{}{"email": saEmail},
    	}
    	jwsHeader := &jws.Header{
    		Algorithm: "RS256",
    		Typ:       "JWT",
    	}
    
    	// Extract the RSA private key from the service account keyfile.
    	sa, err := ioutil.ReadFile(saKeyfile)
    	if err != nil {
    		return "", fmt.Errorf("Could not read service account file: %v", err)
    	}
    	conf, err := google.JWTConfigFromJSON(sa)
    	if err != nil {
    		return "", fmt.Errorf("Could not parse service account JSON: %v", err)
    	}
    	block, _ := pem.Decode(conf.PrivateKey)
    	parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
    	if err != nil {
    		return "", fmt.Errorf("private key parse error: %v", err)
    	}
    	rsaKey, ok := parsedKey.(*rsa.PrivateKey)
    	// Sign the JWT with the service account's private key.
    	if !ok {
    		return "", errors.New("private key failed rsa.PrivateKey type assertion")
    	}
    	return jws.Encode(jwsHeader, jwt, rsaKey)
    }
    
  2. In the calling service, add the following function to send the signed JWT in the Authorization: Bearer header in the request to the API:
    Java
    /**
     * Makes an authorized request to the endpoint.
     */
    public static String makeJwtRequest(final String singedJwt, final URL url)
        throws IOException, ProtocolException {
    
      HttpURLConnection con = (HttpURLConnection) url.openConnection();
      con.setRequestMethod("GET");
      con.setRequestProperty("Content-Type", "application/json");
      con.setRequestProperty("Authorization", "Bearer " + singedJwt);
    
      InputStreamReader reader = new InputStreamReader(con.getInputStream());
      BufferedReader buffReader = new BufferedReader(reader);
    
      String line;
      StringBuilder result = new StringBuilder();
      while ((line = buffReader.readLine()) != null) {
        result.append(line);
      }
      buffReader.close();
      return result.toString();
    }
    Python
    def make_jwt_request(signed_jwt, url='https://your-endpoint.com'):
        """Makes an authorized request to the endpoint"""
        headers = {
            'Authorization': 'Bearer {}'.format(signed_jwt),
            'content-type': 'application/json'
        }
        response = requests.get(url, headers=headers)
    
        response.raise_for_status()
        return response.text
    Go
    
    // makeJWTRequest sends an authorized request to your deployed endpoint.
    func makeJWTRequest(signedJWT, url string) (string, error) {
    	client := &http.Client{
    		Timeout: 10 * time.Second,
    	}
    
    	req, err := http.NewRequest("GET", url, nil)
    	if err != nil {
    		return "", fmt.Errorf("failed to create HTTP request: %v", err)
    	}
    	req.Header.Add("Authorization", "Bearer "+signedJWT)
    	req.Header.Add("content-type", "application/json")
    
    	response, err := client.Do(req)
    	if err != nil {
    		return "", fmt.Errorf("HTTP request failed: %v", err)
    	}
    	defer response.Body.Close()
    	responseData, err := ioutil.ReadAll(response.Body)
    	if err != nil {
    		return "", fmt.Errorf("failed to parse HTTP response: %v", err)
    	}
    	return string(responseData), nil
    }
    

When you send a request by using a JWT, for security reasons, we recommend that you put the authentication token in the Authorization: Bearer header. For example:

curl --request POST \
  --header "Authorization: Bearer ${TOKEN}" \
  "${ENDPOINTS_HOST}/echo"

where ENDPOINTS_HOST and TOKEN are environment variables containing your API host name and authentication token, respectively.

If you can't use the header when sending the request, you can put the authentication token in a query parameter called access_token. For example:

curl --request POST \
  "${ENDPOINTS_HOST}/echo?access_token=${TOKEN}"

Receiving authentication details in your API

ESP forwards all headers it receives, including the original authorization header, to the API. In addition, ESP sends the authentication result in the X-Endpoint-API-UserInfo header to the API. This header is base64 encoded and contains the following JSON object:

{
  "issuer": TOKEN_ISSUER,
  "id": USER_ID,
  "email" : USER_EMAIL
}

What's next

Was this page helpful? Let us know how we did:

Send feedback about...

Cloud Endpoints with OpenAPI
Need help? Visit our support page.