Search code examples
pythongoogle-cloud-platformcredentialsgoogle-cloud-runanvil

Cannot authenticate my Google Cloud Run service with a service account


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": "[email protected]",
      "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?


Solution

  • 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.