Search code examples
pythongoogle-cloud-platformgoogle-cloud-functionsgoogle-authenticationgoogle-api-gateway

How to initialize a Google BigQuery Client within a Cloud Function using the JWT of a service account passed by API Gateway to the Cloud Function


Goal:
I have set up a Google API Gateway. The backend for the API is a cloud function (written in python). The cloud function should query data from Google BigQuery (BQ). To do that, I want to create a BQ Client (google.cloud.bigquery.Client()). The API should be accessed by different applications using different service accounts. The service accounts have permission to access only specific datasets within my project. Therefore, the service accounts/applications should only be able to query the datasets they have the permission for. Therefore, the BQ Client within the cloud function should be initialized with the service account that sends the request to the API.

What I tried:
The API is secured with the following OpenAPI definition so that a JWT signed by the service account SA-EMAIL is required to send a request there:

securityDefinitions:
  sec-def1:
    authorizationUrl: ""
    flow: "implicit"
    type: "oauth2"
    x-google-issuer: "SA-EMAIL"
    x-google-jwks_uri: "https://www.googleapis.com/robot/v1/metadata/x509/SA-EMAIL"
    x-google-audiences: "SERVICE"

For the path that uses my cloud function, I use the following backend configuration:

x-google-backend:
  address: https://PROJECT-ID.cloudfunctions.net/CLOUD-FUNCTION
  path_translation: CONSTANT_ADDRESS 

So in the cloud function itself I get the forwarded JWT as X-Forwarded-Authorization and also the already verified base64url encoded JWT payload as X-Apigateway-Api-Userinfo from the API Gateway.
I tried to use the JWT from X-Forwarded-Authorization to obtain credentials:

bearer_token = request.headers.get('X-Forwarded-Authorization')
token = bearer_token.split(" ")[1]
cred = google.auth.credentials.Credentials(token)

At first, this seems to work since cred.valid returns True, but when trying to create the client with google.cloud.bigquery.Client(credentials=cred) it returns the following error in the logs:

google.auth.exceptions.RefreshError: The credentials do not contain 
the necessary fields need to refresh the access token. You must 
specify refresh_token, token_uri, client_id, and client_secret.

I do not have much experience with auth/oauth at all, but I think I do not have the necessary tokens/attributes the error is saying are missing available in my cloud function. Also, I am not exactly sure why there is a RefreshError, since I don't want to refresh the token (and don't do so explicitly) and just use it again (might be bad practice?).

Question:
Is it possible to achieve my goal in the way I have tried or in any other way?


Solution

  • Your goal is to catch the credential that called the API Gateway, and to reuse it in your Cloud Functions to call BigQuery.

    Sadly, you can't. Why? Because API Gateway prevent you to achieve that (and it's a good news for security reason). The JWT token is correctly forwarded to your Cloud Functions, but the signature part has been removed (you receive only the header and the body of the JWT token).

    The security verification has been done by API Gateway and you have to rely on that authentication.


    What's the solution?

    My solution is the following: In the truncated JWT that you receive, you can get the body and get the Service Account email. From there, you can use the Cloud Functions service account, to impersonate the Service Account email that you receive.

    Like that, the Cloud Functions service account only needs the permission to impersonate these service account, and you keep the permission provided on the original service account.

    I don't see other solutions to solve your issue.