Service Account Credentials API: A solution to different issues

IAM (Identity and Access Management) is a pillar of Google Cloud. It authenticates and authorizes accounts (user account or service account) to access to resources. I already mentioned some limitations of this service, especially when you want to avoid the service account key file usage, and thus to avoid major security risks.

To help on this, Google Cloud has introduced a new API: Service Account Credentials API. Actually, not really new (release in June 2018 in Beta), but poorly known or understood. However, it can help in 2 typical use cases.

  • Generate anid_token without metadata server (locally for example)
  • Generate Cloud Storage signed url without service account private key

Metadata server not available

The metadata server provides contextual information on the running platform. It’s only available on all Google Cloud services, and useful to find information on the service context (the project ID, the location) or to get the credentials information from the service’s service account.

For example, to perform “service-to-service” call from Cloud Run, you have to use it as described in the documentation.

However, the metadata server is only accessible from the Google Cloud service, the base URL is clear http://metadata.google.internal

How to test code with metadata server locally?

To ensure the same behavior locally and on the cloud, the constrain is to have the exact same code on all environments.

To achieve this, you need to avoid the metadata server usage, at least to generate the required id_token to access to private serverless services such as:

  • Private Cloud Run
  • Private Cloud Functions
  • App Engine behind IAP

To access to these private serverless services, you need to have a valid id_token with the correct audience. You can generate it only on a service account:

  • With a service account key file, which is a bad security idea
  • With the metadata server, when it is available (i.e. not locally)
  • With the Service Account Credentials API

It’s not possible to generate a valid id_token with a user credential, as explained in my previous article

On the other hand, to access to Google API, such as Service Account Credentials API, Storage API, or even GMail API (…), you need an access_token and not an id_token. This difference is important, because you can generate an access_token with

  • A service account with key file or through metadata server
  • A user account, therefore locally with the ADC (Application Default Credential)

In summary, you can generate an access_token on any condition.

So now, you have to replace the metadata server call by the Service Account Credentials API to build a suitable alternative

  1. Get the access token to perform a secure request to the Service Account Credentials API
  2. Get the service account email on which you want to generate an id_token
  3. Call the Service Account Credentials API with the correct audience to generate the id_token

To do this, here a code sample in Python to generate the id_token
google-auth and requests dependencies need to be installed

import google.auth
from google.auth.transport.requests import AuthorizedSession
import json
# IAP audience is the ClientID of IAP-App-Engine-app in
# the API->credentials page
# Cloud Function and Cloud Run need the base URL of the service
audience = 'YOUR AUDIENCE'
# #1 Get the default credential to generate the access token
credentials, project_id = google.auth.default(
scopes='https://www.googleapis.com/auth/cloud-platform')

# #2 To use the current service account email
service_account_email = credentials.service_account_email
# Don't work with user account, so define manually the email
# service_account_email = 'MY SERVICE ACCOUNT EMAIL'
# #3 prepare the call the the service account credentials API
sa_credentials_url = f'https://iamcredentials.googleapis.com/' \
f'v1/projects/-/serviceAccounts/' \
f'{service_account_email}:generateIdToken'
headers = {'Content-Type': 'application/json'}

# Create an AuthorizedSession that includes
# automatically the access_token based on your credentials
authed_session = AuthorizedSession(credentials)
# Define the audience in the request body
# add the parameter "'includeEmail':true" for IAP access
body = json.dumps({'audience': audience})
# Make the call
token_response = authed_session.request('POST',sa_credentials_url,
data=body, headers=headers)

jwt = token_response.json()
id_token = jwt['token']

Now that you have the id_token, you can call the private serverless service API that you want, for example

import requestsheaders = {'Authorization': f'bearer {id_token}'}
service_response = requests.get(service_url, headers=headers)

Now, you have it! This code works locally and in the cloud almost seamlessly. Indeed, locally, with the user credentials, you need to define the service account to use.

When you perform this operation, you need to have the correct permissions set because

You ask the account (identified by the access_token) to generate an ID token on the service account email behalf

Therefore, the requester (identified by the access_token) needs to have the role Service Account Token Creator on the service account.

On Stack Overflow, Fabian published a question on Cloud Build. The behavior of Cloud Build is not standard: it allows you to generate access_token, based on the metadata server, but not the id_token. Something like a technical limitation.
And Fabian needed to call a private Cloud Functions during his build, and therefore, he needed an id_token in the request header!

So, by reusing this solution and by avoiding the metadata server usage for the id_token generation, he has been able to achieve his private Cloud Functions call!

Create signed url without private key

The second use case is also a security concern. Indeed, when you want to generate a Cloud Storage signed url, a service account private key is required.

But, what is the purpose of this private key?

A private key can be used to

  • Decipher encrypted data (encrypted with the public key) in case of asymmetric encryption; it’s not the use case
  • Sign data to proof the signer identity (the signature is validated against the public key)

So that, the Service Account Credentials API allows you to sign data on the service account behalf. Let’s use it!

This time, I won’t rewrite the SignedUrl method, but if you deep dive in the Python library, you can see this (extract from _signing.py)

...
if access_token and service_account_email:
signature = _sign_message(string_to_sign, access_token,
service_account_email)
...

If you provide the access_token and the service_account_email in the generate_signed_url parameters, the _sign_message is called

And, if you follow the _sign_message you can see this

url = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{}:signBlob?alt=json".format(service_account_email)

Exactly what we need! So now, we simply have to call correctly the initial generate_signed_url method

from google.cloud import storage
from datetime import datetime, timedelta
import google.auth
from google.auth.transport import requests
# Get the default credential on the current environment
credentials, project_id = google.auth.default()

# Perform a refresh request to get the access token
# of the current credentials (Else, its value is 'None')
r = requests.Request()
credentials.refresh(r)

# Create your storage object to sign
client = storage.Client()
bucket = client.get_bucket('MY_BUCKET')
blob = bucket.get_blob('my_path/my_file.txt')
expires = datetime.now() + timedelta(seconds=86400)

# In case of user credential usage, you have to define manually
# the service account to use (for development purpose only)
service_account_email = "YOUR DEV SERVICE ACCOUNT"
# If you use a service account credential, you can use it directly
if hasattr(credentials, "service_account_email"):
service_account_email = credentials.service_account_email

url = blob.generate_signed_url(expiration=expires,
service_account_email=service_account_email,
access_token=credentials.token)
print(url)

Boom, you generate a signed url without any private key locally stored! You use the same Cloud Storage feature and you increase your security (no secret to manage).

This Cloud Storage client library capacity is available in Python. It’s not yet available in Go. Check the signedUrl code in the client library of your favorite language to validate this capacity.

Use it when you can!

This Service Account Credentials API can help you in many cases where you need operation only possible with a service account key files. You increase your application security by performing a simple API call! Awesome, isn’t it?

In addition of this 2 useful features and use cases, you can also, for example, impersonate a service account or sign a JWT token with this API.

I hope this API is now less mysterious because it is really helpful!

Google Cloud - Community

Google Cloud community articles and blogs

Google Cloud - Community

A collection of technical articles and blogs published or curated by Google Cloud Developer Advocates. The views expressed are those of the authors and don't necessarily reflect those of Google.

guillaume blaquiere

Written by

GDE Google Cloud Platform, scrum master, speaker, writer and polyglot developer, Google Cloud platform 3x certified, serverless addict and Go fan.

Google Cloud - Community

A collection of technical articles and blogs published or curated by Google Cloud Developer Advocates. The views expressed are those of the authors and don't necessarily reflect those of Google.