使用Cloud Run进行内部CLI的Google Cloud Auth

We've built several services using Cloud Run. Our goal is to build an internal CLI that allows our developers to make calls to these services. We're having trouble generating an id_token to use with the Identity Aware Proxy that sits in front of Cloud Run services.

According to the docs, making calls to your Cloud Run services can be accomplished by using gcloud and the gcloud auth print-identity-token command. This works great. This also avoids having to download and pass around service account credentials to our developers as this method leverages your application default credentials.

We've tried implementing something to replicate this print-identity-token functionality in Go with no luck. The id_token generated returns 401's to all of our Cloud Run API's. Example code for generating the token:

func GetIDToken() string {
    ctx := context.Background()
    tokenSource, err := google.DefaultTokenSource(ctx, "openid", "email")

    if err != nil {
        log.Fatal(err)
    }

    token, err := tokenSource.Token()

    if err != nil {
        log.Fatal(err)
    }

    return fmt.Sprintf("%v", token.Extra("id_token"))
}

This returns an id_token but it doesn't work with the API's. The scopes seem to be correct according to the docs.

This leaves us with two questions:

  1. Is this the correct approach for generating an Id token for the IAP?
  2. Is there a better way to implement the authentication for our developers to these internal API's?

This answer is for creating an Identity Token from a service account. This example is in Python. If requested, I will write this in Go. I just had this code already written in Python.

In the code below, the first code block is the section that takes a service account and requests the Identity Token from Google. Notice that I do not use any scopes. Scopes are used when requesting Google OAuth Access Tokens. Identity Tokens have identity stored in them. Instead you need to specify the audience (URL) that the Identity Token is destined for. Not all services require a valid audience value.

My code also shows how to decode an Identity Token to see the Header and Payload JSON. The Payload contains the identity that Google IAP validates.

Once you have the Identity Token, include the HTTP Header authorization: bearer TOKEN when making requests to Cloud Run.

import google.auth.transport.requests
import google.oauth2.service_account

credentials = google.oauth2.service_account.IDTokenCredentials.from_service_account_file(
        json_filename,
        target_audience=aud)

request = google.auth.transport.requests.Request()

credentials.refresh(request)
  1. Change the path to the service account to match your software.
  2. Change the aud to be your Cloud Run URL.

Full Source Code Example:

'''
This program creates an OIDC Identity Token from a service account
'''

import json
import base64

import google.auth.transport.requests
import google.oauth2.service_account

# The service account JSON key file to use to create the Identity Token
json_filename = '/config/service-account.json'

# The audience that this ID token is intended for (example Google Cloud Run service URL)
aud = 'http://localhost'

def pad(data):
    """ pad base64 string """

    missing_padding = len(data) % 4
    data += '=' * (4 - missing_padding)
    return data

def print_jwt(signed_jwt):
    """ Print a JWT Header and Payload """

    s = signed_jwt.decode('utf-8').split('.')

    print('Header:')
    h = base64.urlsafe_b64decode(pad(s[0])).decode('utf-8')
    print(json.dumps(json.loads(h), indent=4))

    print('Payload:')
    p = base64.urlsafe_b64decode(pad(s[1])).decode('utf-8')
    print(json.dumps(json.loads(p), indent=4))

if __name__ == '__main__':
    credentials = google.oauth2.service_account.IDTokenCredentials.from_service_account_file(
            json_filename,
            target_audience=aud)

    request = google.auth.transport.requests.Request()

    credentials.refresh(request)

    #print(dir(credentials))

    # This is debug code to show how to decode Identity Token
    print('Decoded Identity Token:')
    print_jwt(credentials.token.encode())

    # This is the actual Identity Token
    print()
    print('Identity Token:')
    print(credentials.token)