1
votes

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?
2
Are you using User Credentials (Google Account / G Suite) or a Service Account for your identity? Is the code running on your desktop outside of Google Cloud or on a Compute instance? These details change the type of code that generates/fetches an Identity Token.John Hanley
@JohnHanley Yes, we are using user credentials (not a service account) and the code is running on our desktops in a CLI. We're building a developer CLI tool to call these services.Kevin O'Hara
Server to server uses service accounts not user accounts. Your CLI should also be using a service account. You will get warnings and quota violations for using user accounts. Some services will NOT accept user account OAuth Tokens. However, post your code that shows the entire authentication process and not one function. When you exchange the OAuth Code, if the initial request was correct you will get three tokens: Access Token, ID Token and Refresh Token.John Hanley
It seems like the audience parameter you're providing is not the url of the Cloud Run service. Also do you have gcloud present in your environment? Why are you trying to re-implement gcloud print-identity-token?ahmet alp balkan
Also it's unclear where IAP got into the picture in your question? Cloud Run does not support IAPs.ahmet alp balkan

2 Answers

2
votes

It's unclear why you are trying to reimplement gcloud auth print-identity-token. I highly recommend you do not try to do that.

If you're building a developer CLI, your job will be much easier if you make gcloud present in the dev machine, and call out to this command to retrieve an id_token.

Reimplementing print-identity-token is really going to be difficult. Because gcloud makes a call to the OAuth token endpoint with the internal refresh_token it's been storing to get an id_token as well as an access_token. (Run command with --log-http to see more, and set the relevant gcloud config to not to redact tokens from req/resp body.)

Alternatively, without gcloud present and authenticated with GSuite account on the developer machine, your only other option is to distribute IAM service account keys to each developer machine. (On the flipside, you need to make sure these keys are rotated properly, and kept secure.)

You can use the IAM Service Account key to make a POST to https://www.googleapis.com/oauth2/v4/token with proper fields (like client_secret, grant_type etc) get an id_token. I believe the simplest way to do that is to set GOOGLE_APPLICATION_CREDENTIALS env variable in your Go program to the key file, specify the correct audience parameter in token exchange request and then use token.Extra("id_token") as you've done.

Aside: I also notice your identity field is not set to Cloud Run service’s URL in your example code.

0
votes

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)