1

I deployed a service on Cloud Run where authentication is needed:

gcloud run deploy my-service --project my-project --image eu.gcr.io/my-project/rest-of-path --platform managed --region europe-west4 --no-allow-unauthenticated

This seems to work fine. However, when I try to access my service from another service (in my case it is Anvil), it gives me a Response [403], which means it refused to authorize it. My Service Account does have the right roles as far as I know: Cloud Run Invoker, Service Account Token Creator, Service Controller. Even if I to add the owner role, it's not working.

This is my code to access my service:


API_URL="https://my-url.run.app/"

def create_signed_jwt(credentials_json, run_service_url):
    iat = time.time()
    exp = iat + 3600
    payload = {
        'iss': credentials_json['client_email'],
        'sub': credentials_json['client_email'],
        'target_audience': run_service_url,
        'aud': 'https://www.googleapis.com/oauth2/v4/token',
        'iat': iat,
        'exp': exp
    }
    additional_headers = {
        'kid': credentials_json['private_key_id']
    }

    signed_jwt = jwt.encode(
        payload,
        credentials_json['private_key'],
        headers=additional_headers,
        algorithm='RS256'
    )
    return signed_jwt


def exchange_jwt_for_token(signed_jwt):
    body = {
        'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
        'assertion': signed_jwt
    }
    token_request = requests.post(
        url='https://www.googleapis.com/oauth2/v4/token',
        headers={
            'Content-Type': 'application/x-www-form-urlencoded'
        },
        data=urllib.parse.urlencode(body)
    )
    return token_request.json()['id_token']


def get_headers():
    """
    Creates the headers for each request to the API on google cloud run
    """
    credentials = {
      "type": "service_account",
      "project_id": "my-project-id",
      "private_key_id": my-key-id,
      "private_key": "-----BEGIN PRIVATE KEY----- very long token-----END PRIVATE KEY-----\n",
      "client_email": "my-credential-name@my-project-id.iam.gserviceaccount.com",
      "client_id": my-client-id,
      "auth_uri": "https://accounts.google.com/o/oauth2/auth",
      "token_uri": "https://oauth2.googleapis.com/token",
      "auth_provider_x509_cert_url": some-standard-url,
      "client_x509_cert_url": some-standard-url
    }
    token = exchange_jwt_for_token(create_signed_jwt(credentials, API_URL))
    return {
        "Authorization": f"Bearer {token}"
    }



def test_request_function():
    """ request example url"""
    response = requests.get(f'{API_URL}/health', get_headers())

print(test_request_function())

Why is it not possible to authorize?

  • Please trim your code to make it easier to find your problem. Follow these guidelines to create a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example). – Community Jun 03 '22 at 15:19

1 Answers1

0

I encourage you to consider using Google's auth library (for Python) or any other reputable auth library to generate the JWT.

As you're experiencing, crafting JWT's is gnarly and, even when you get it working, you're on the hook for supporting code that would probably be better left to others.

See: Authenticating Service-to-Service

import urllib

import google.auth.transport.requests
import google.oauth2.id_token

import os


endpoint=os.getenv("ENDPOINT")
audience=os.getenv("AUDIENCE")

req = urllib.request.Request(endpoint)

auth_req = google.auth.transport.requests.Request()
id_token = google.oauth2.id_token.fetch_id_token(auth_req, audience)

bearer = f"Bearer {id_token}"
print(bearer)

req.add_header("Authorization", bearer)
response = urllib.request.urlopen(req)
print(response.code)

NOTE You can also use Application Default Credentials with the Google library making the code shorter.

Q=72491606
python3 -m venv venv
source venv/bin/activate
python3 -m pip install google-auth
python3 -m pip install requests

BILLING=...
PROJECT=stackoverflow-${Q}
REGION=...
ACCOUNT="tester"

EMAIL="${ACCOUNT}@${PROJECT}.iam.gserviceaccount.com"

gcloud projects create ${PROJECT}

gcloud beta billing projects link ${PROJECT} \
--billing-account=${BILLING}

gcloud services enable run.googleapis.com \
--project=${PROJECT}

gcloud iam service-accounts create ${ACCOUNT} \
--project=${PROJECT}

gcloud iam service-accounts keys create ${PWD}/${ACCOUNT}.json \
--iam-account=${EMAIL} \
--project=${PROJECT}

gcloud projects add-iam-policy-binding ${PROJECT} \
--member=serviceAccount:${ACCOUNT}@${PROJECT}.iam.gserviceaccount.com \
--role=roles/run.invoker

export GOOGLE_APPLICATION_CREDENTIALS=${PWD}/${ACCOUNT}.json

# Deploy Cloud Run example
gcloud run deploy hello \
--image="gcr.io/cloudrun/hello" \
--no-allow-unauthenticated \
--region=${REGION} \
--platform=managed \
--project=${PROJECT}

# Get ENDPOINT==AUDIENCE
export ENDPOINT=$(\
 gcloud run services describe hello \
  --project=${PROJECT} \
  --region=${REGION} \
  --format="value(status.url)")
export AUDIENCE=${ENDPOINT}

python3 main.py

Yields an identity token and hopefully (200).

You can then plug the identity token into e.g. https://jwt.io to inspect it.

DazWilkin
  • 32,823
  • 5
  • 47
  • 88
  • Thanks DazWilkin! When running in a python script it works now! However, when trying to run in the Anvil app, I get the following error at line `id_token = ...`: `DefaultCredentialsError: Neither metadata server or valid service account credentials are found.` I already tried to put my GOOGLE_APPLICATION_CREDENTIALS in a secret value and retrieved it with: `GOOGLE_APPLICATION_CREDENTIALS = anvil.secrets.get_secret("GOOGLE_APPLICATION_CREDENTIALS")`. However, I still get the same error. Do you know how to solve this? – marlies_buijs Jul 13 '22 at 13:07
  • Hi DazWilkin, together with [this](https://anvil.works/forum/t/service-account-authentication-via-secret-key-json-file-for-google-cloud-platform/3834/2) your code works! Thanks for your help! – marlies_buijs Jul 15 '22 at 08:44
  • You should only need to set the environment variable in code **if** you can't set it from the environment. Setting an environment variable like this in code is an anti-pattern (discouraged). – DazWilkin Jul 15 '22 at 15:07