google-cloud-functionsjwtservice-accounts

Trigger Google Cloud Function using JWT


I am trying to trigger a Google cloud function using an HTTP request and a service account. I followed these steps :

  1. Create a service account
  2. Download JSON key file
  3. Create a Google Cloud function that requires authentication (this is a test function that just returns "Hello world !"
  4. In the Cloud function permissions I added my service account with the role Cloud Functions Invoker

If I generate an identity token with this service account (gcloud auth print-identity-token) and use it to trigger my Cloud function (with Authorization: Bearer $ID_TOKEN in headers) it works as expected. But now I want to be able to do it in production. So what I did is generate a JWT, use it to get an access token and use this access token just like I did with the identity token but I get 401 Unauthorized response.

Here is my code (Python)

import jwt
import time
import requests

# These elements are found in the service account JSON file (step 2)
PRIVATE_KEY_ID_FROM_JSON = "123ab4cdefghi56jk789123456789f123456m7n"
PRIVATE_KEY_FROM_JSON = "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
SERVICE_ACCOUNT_EMAIL = "ACCOUNT_NAME@PROJECT_ID.iam.gserviceaccount.com"

# Create JWT
iat = int(time.time())
exp = iat + 3600

payload = {
    'iss': SERVICE_ACCOUNT_EMAIL,
    'sub': SERVICE_ACCOUNT_EMAIL,
    'aud': "https://www.googleapis.com/oauth2/v4/token",
    'target_audience ': 'https://REGION-PROJECT_ID.cloudfunctions.net/FUNCTION_NAME',
    'iat': iat,
    'exp': exp,
    "scope": "https://www.googleapis.com/auth/cloud-platform"
}
additional_headers = {'kid': PRIVATE_KEY_ID_FROM_JSON}

signed_jwt = jwt.encode(
    payload,
    PRIVATE_KEY_FROM_JSON,
    headers=additional_headers,
    algorithm='RS256'
)

# Use JWT to get access token
resp = requests.post(
    f"https://www.googleapis.com/oauth2/v4/token"
    f"?grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion={signed_jwt}",
    headers={
        "Authorization": f"Bearer {signed_jwt}",
        "Content-Type": "application/x-www-form-urlencoded"
    }
)

access_token = resp.json()["access_token"]

# Trigger Cloud Function using this access token
resp_cf = requests.post(
    "https://europe-west1-cuure-265415.cloudfunctions.net/Test_service_account",
    headers={
        "Authorization": f"Bearer {access_token}"
    }
)

print(resp_cf.content)

The result is the following

b'\n<html><head>\n<meta http-equiv="content-type" content="text/html;charset=utf-8">\n<title>401 Unauthorized</title>\n</head>\n<body text=#000000 bgcolor=#ffffff>\n<h1>Error: Unauthorized</h1>\n<h2>Your client does not have permission to the requested URL <code>/CLOUD FUNCTION NAME</code>.</h2>\n<h2></h2>\n</body></html>\n'

I don't want to use Python libraries to authenticate my request with the service account because my aim is to be able to reproduce this in Bubble.io, a no code development platform.

Anyone knows what I'm doing wrong?


Solution

  • As mentioned in the Answer :

    There are three types of OAuth tokens. Refresh Token, Access Token, Identity Token. You are trying to use an OAuth Access Token instead of an OAuth Identity Token. For Authorization, Google Cloud Functions uses an OAuth Identity Token in the authorization: bearer HTTP header.

    You can also refer to the documentation and answer where exchanging a self-signed JWT for a Google-signed ID token is well explained.

    You can manage access to an HTTP Cloud Function using IAM roles. You will want to assign the roles/cloud functions.invoker role to a service account, and have the caller provide the oauth credentials to the function in the Authorization header.