Search code examples
google-cloud-platformgoogle-apigoogle-cloud-tasks

Accessing Google CloudTasks API without using Google's SDKs


I'm trying to use Google cloudtasks from Cloudflare workers. This is a JS environment limited to web-workers standards minus some things that Cloudflare didn't implement. Bottom line - I can't use Google's provided SDKs in that environment. I'm trying to call the API using simple fetch, but always fail on the authentication part.

The discovery document says that

"parameters": {
   ...
   "key": {
      "description": "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.",
      "location": "query",
      "type": "string"
   }
}

So I tried calling the api with ?key=MY_API_KEY query param Didn't work.

I also tried generating a token using Service Account downloaded json file with this library Didn't work.

I tried following this guide to generate oauth access token which was what the error message told me that I need. But

  1. running the command gcloud auth application-default print-access-token returned the error:
    WARNING: Compute Engine Metadata server unavailable onattempt 1 of 3. Reason: timed out
    WARNING: Compute Engine Metadata server unavailable onattempt 2 of 3. Reason: timed out
    WARNING: Compute Engine Metadata server unavailable onattempt 3 of 3. Reason: [Errno 64] Host is down
    WARNING: Authentication failed using Compute Engine authentication due to unavailable metadata server.
    ERROR: (gcloud.auth.application-default.print-access-token) Could not automatically determine credentials. Please set GOOGLE_APPLICATION_CREDENTIALS or explicitly create credentials and re-run the application. For more information, please see https://cloud.google.com/docs/authentication/getting-started
    
    
    The env variable above is set correctly to the service-account json file.
  2. Even if it worked, I didn't understand how am I supposed to use it from my code, while it uses the cli tool gcloud

So my question is this - how can I access Google's cloud APIs from Cloudflare workers (web-workers javascript env.), specifically I'm interested in Cloudtasks, without using any CLI tool or Google SDK. More specifically - how can I generate the required oauth2 access token?


Solution

  • Based on @john-hanley blog post I was able to make the following code work:

    const fetch = require('node-fetch'); //in cloudflare workers env. this is not needed. 'fetch' is globally available
    const jwt = require('jsonwebtoken');
    const q = require('querystring');
    
    async function main() {
        const project = 'xxxx';
        const location = 'us-central1';
        const scopes = "https://www.googleapis.com/auth/cloud-platform"
        
        const queue = 'queueName';
        const parent = `projects/${project}/locations/${location}/queues/${queue}`
        const url = `https://cloudtasks.googleapis.com/v2/${parent}/tasks`
    
        const sjwt = await createSignedJwt(json.private_key, json.private_key_id, json.client_email, scopes);
        const {token} = await exchangeJwtForAccessToken(sjwt)
    
        const headers = { Authorization: `Bearer ${token}` }
    
        const body = Buffer.from(JSON.stringify({"c":"b"})).toString('base64'); //Note this is not a string!
        const task = { //in my case the task is HTTP request that google will send to my service outside GCP. You can create an appEngine task instead
            httpRequest: {
                "url": "https://where-google-should-send-the-task.com",
                "httpMethod": "POST",
                "headers": {
                    "Content-Type": "application/json; charset=UTF-8",
                    "Authorization": "only-if-you-need-it"
                },
                "body": body
            }
        };
        const request = {
            parent: parent,
            task: task
        };
    
        const response = await fetch(url, {
            method: "POST",
            body: JSON.stringify(request),
            headers
        })
    
        const res =  await response.json()
        console.log(res)
    }
    
    async function createSignedJwt (pkey, pkey_id, email, scope) {
        const authUrl = "https://oauth2.googleapis.com/token"
        const options = {
            algorithm: "RS256",
            keyid: pkey_id,
            expiresIn: 3600,
            audience: authUrl,
            issuer: email
            // header: { //this is not needed because it's jsonwebtoken's default behavior to add the correct typ when the payload is a json
            //     "typ": "JWT"
            // }
        }
        const payload = {
            "scope": scope
        }
        return jwt.sign(payload, pkey, options)
    }
    
    /**
     * This function takes a Signed JWT and exchanges it for a Google OAuth Access Token
     */
    async function exchangeJwtForAccessToken(signedJwt) {
        const authUrl = "https://oauth2.googleapis.com/token"
        const params = {
            "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
            "assertion": signedJwt
        }
        const body = q.stringify(params);
        const res = await fetch(authUrl, {
            method: "POST",
            headers: {
                "Content-Type": "application/x-www-form-urlencoded"
            },
            body
        })
        if (!res.ok) {
            return {
                error: "Could not fetch access token. " + await res.text()
            }
        }
        const resJson = await res.json();
        return {
            token: resJson.access_token
        }
    }
    
    // for convenience, I'm placing the JSON here. For production it should be stored in secret-manager or injected via environment variable
    const json = {
        "type": "service_account",
        "project_id": "xxxx",
        "private_key_id": "xxxx",
        "private_key": "-----BEGIN PRIVATE KEY-----xxxx-----END PRIVATE KEY-----\n",
        "client_email": "xxxx@xxxx.iam.gserviceaccount.com",
        "client_id": "xxxx",
        "auth_uri": "https://accounts.google.com/o/oauth2/auth",
        "token_uri": "https://oauth2.googleapis.com/token",
        "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
        "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/xxxx%40xxxx.iam.gserviceaccount.com"
    }
    
    
    main()