Доступ к Google CloudTasks API без использования Google SDK

Пытаюсь использовать облачные задачи Google от работников Cloudflare. Это среда JS, ограниченная стандартами веб-работников, за исключением некоторых вещей, которые Cloudflare не реализовал. Итог - я не могу использовать SDK, предоставленные Google в этой среде. Я пытаюсь вызвать API, используя простую выборку, но всегда терпит неудачу в части аутентификации.

В документе обнаружения говорится, что

"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"
   }
}

Поэтому я попытался вызвать api с параметром запроса ?key=MY_API_KEY. Не сработало.

Я также попытался создать токен с помощью загруженного json-файла учетной записи службы с этой библиотекой. Не сработало.

Я пробовал следовать этому руководству для генерации токена доступа oauth, о чем мне говорилось в сообщении об ошибке. Но

  1. выполнение команды gcloud auth application-default print-access-token вернуло ошибку:
    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
    
    
    Переменная env, указанная выше, правильно установлена ​​для json-файла служебной учетной записи.
  2. Даже если это сработало, я не понимал, как мне его использовать из моего кода, хотя он использует инструмент cli gcloud

Итак, мой вопрос: как я могу получить доступ к облачным API Google от рабочих Cloudflare (env javascript web-worker), в частности, меня интересуют Cloudtasks, без использования какого-либо инструмента CLI или Google SDK. Более конкретно - как я могу сгенерировать требуемый токен доступа oauth2?


person danbars    schedule 21.05.2021    source источник
comment
Какие области Compute Engine включены для пункта 1?   -  person John Hanley    schedule 22.05.2021
comment
AFAIK, Cloud Tasks не принимает ключи API для авторизации. Вы должны использовать токен доступа OAuth.   -  person John Hanley    schedule 22.05.2021
comment
@JohnHanley Похоже, ты прав. Вопрос в том, как я могу сгенерировать токен доступа OAuth без использования Google SDK?   -  person danbars    schedule 22.05.2021
comment
Поиск Гугл. Есть много хороших статей о генерации токенов. Я написал несколько статей с исходным кодом на своем сайте.   -  person John Hanley    schedule 22.05.2021
comment
Конечно, я сначала искал в Google. Тем не менее, не смог найти. Ваш намек на ваш собственный сайт дал мне отправную точку. После переноса кода с python на js и поиска подходящей области для облачных задач (которая была скрыта в api discovery json) я наконец заставил его работать. Спасибо за подсказки. Добавлю ответ на случай, если он кому-то понадобится   -  person danbars    schedule 02.06.2021


Ответы (1)


На основе блога @ john-hanley. post мне удалось заставить работать следующий код:

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

person danbars    schedule 01.06.2021