【问题标题】:google cloud authentication with bearer token via nodejs通过 nodejs 使用不记名令牌进行谷歌云身份验证
【发布时间】:2023-12-05 10:39:01
【问题描述】:

我的客户有一个在 Google 云上运行的 GraphQL API。

我已收到用于身份验证以及访问 gcloud 命令行工具的服务帐户。

当像这样使用 gcloud 命令行时:

gcloud auth print-identity-token

我可以生成一个可用于向 api 发出 post 请求的令牌。这很有效,我可以从邮递员、失眠症和我的 nodejs 应用程序向 api 发出成功的发布请求。

但是,当我将 JWT 身份验证与“googleapis”或“google-auth”npm 库一起使用时:

var { google } = require('googleapis')

let privatekey = require('./auth/google/service-account.json')

let jwtClient = new google.auth.JWT(
  privatekey.client_email,
  null,
  privatekey.private_key,
  ['https://www.googleapis.com/auth/cloud-platform']
)

jwtClient.authorize(function(err, _token) {
  if (err) {
    console.log(err)
    return err
  } else {
    console.log('token obj:', _token)
  }
})

这会输出一个“承载”令牌:

token obj: {
  access_token: 'ya29.c.Ko8BvQcMD5zU-0raojM_u2FZooWMyhB9Ni0Yv2_dsGdjuIDeL1tftPg0O17uFrdtkCuJrupBBBK2IGfUW0HGtgkYk-DZiS1aKyeY9wpXTwvbinGe9sud0k1POA2vEKiGONRqFBSh9-xms3JhZVdCmpBi5EO5aGjkkJeFI_EBry0E12m2DTm0T_7izJTuGQ9hmyw',
  token_type: 'Bearer',
  expiry_date: 1581954138000,
  id_token: undefined,
  refresh_token: 'jwt-placeholder'
}

但是,这个不记名令牌不能像上面那样工作,并且在发出与 gcloud 命令“gcloud auth print-identity-token”相同的请求时总是给出“未经授权的错误 401”。

请帮忙,我不确定为什么第一个不记名令牌有效,但使用 JWT 生成的不有效。

编辑

我也尝试过获取身份令牌,而不是像这样的访问令牌:

let privatekey = require('./auth/google/service-account.json')

let jwtClient = new google.auth.JWT(
  privatekey.client_email,
  null,
  privatekey.private_key,
  []
)

jwtClient
  .fetchIdToken('https://my.audience.url')
  .then((res) => console.log('res:', res))
  .catch((err) => console.log('err', err))

这会打印一个身份令牌,但是,使用它也只会给出“401 unauthorized”消息。

编辑以显示我如何调用端点

顺便说一句,下面的这些方法中的任何一个都可以使用命令行身份令牌,但是当通过 JWT 生成时,它会返回 401

方法一:

 const client = new GraphQLClient(baseUrl, {
        headers: {
          Authorization: 'Bearer ' + _token.id_token
        }
      })
      const query = `{
        ... my graphql query goes here ...
    }`
      client
        .request(query)
        .then((data) => {
          console.log('result from query:', data)
          res.send({ data })
          return 0
        })
        .catch((err) => {
          res.send({ message: 'error ' + err })
          return 0
        })
    }

方法 2(使用我用 google-auth 创建的“授权”客户端):

  const res = await client.request({
    url: url,
    method: 'post',
    data: `{
        My graphQL query goes here ...
    }`
  })
  console.log(res.data)
}

【问题讨论】:

  • 我认为您的问题是身份令牌受众。在你的最后一个例子中,观众是什么?此代码是在 Cloud Run 中运行还是您正在调用 Cloud Run 服务?如果您正在调用 Cloud Run 服务,则受众值必须与如下所示的 Assigned by Cloud Run 网址匹配:https://example-ylyxpergiq-uc.a.run.app,您可以从 Google Cloud Console 复制该网址。您的第一个示例将永远不会工作,因为它会生成访问令牌。您的第二个示例是创建正确类型的令牌,即身份令牌。
  • 使用 jwt.io 解码身份令牌并在您的问题中显示这些值(屏蔽项目和电子邮件等敏感信息)。
  • 最后一项,编辑您的问题并显示您如何在代码中调用端点。
  • @JohnHanley 确定
  • 您在我的第一条评论中没有回答我的问题。您没有从我的第二条评论中提供身份令牌信息。如果没有详细信息,您将继续得到只是猜测的答案。

标签: google-cloud-platform gcloud google-api-nodejs-client google-cloud-run


【解决方案1】:

对于那些不想因为缺乏文档而浪费一整天工作的人。这是当今世界公认的答案,因为 JWT 类不再接受构造函数中的受众。

import { JWT } from "google-auth-library"

const client = new JWT({
  forceRefreshOnFailure: true,
  key: service_account.private_key,
  email: service_account.client_email,
})

const token = await client.fetchIdToken("cloud run endpoint")
const { data } = await axios.post("cloud run endpoint"/path, payload, {
  headers: {
    Authorization: `Bearer ${token}`
  }
}) 

return data

【讨论】:

  • 嘿,我可以在浏览器上做同样的事情,而不接触服务器吗?
  • 我看不出您无法在浏览器上加载 google-auth-library 库的原因,但您不想这样做,因为您会泄露您的 service_account.private_key
  • 感谢@Sebastian Serrano,我明白了,这完全是为了保密凭据,谷歌很难防止像我这样的开发人员错误地暴露他们的身份
【解决方案2】:

这是 node.js 中的一个示例,该示例正确地创建了具有正确受众的身份令牌以调用 Cloud Run 或 Cloud Functions 服务。

修改此示例以适应 GraphQLClient。不要忘记在每个调用中包含 Authorization 标头。

    // This program creates an OIDC Identity Token from a service account
    // and calls an HTTP endpoint with the Identity Token as the authorization
    
    var { google } = require('googleapis')
    const request = require('request')
    
    // The service account JSON key file to use to create the Identity Token
    let privatekey = require('/config/service-account.json')
    
    // The HTTP endpoint to call with an Identity Token for authorization
    // Note: This url is using a custom domain. Do not use the same domain for the audience
    let url = 'https://example.jhanley.dev'
    
    // The audience that this ID token is intended for (example Google Cloud Run service URL)
    // Do not use a custom domain name, use the Assigned by Cloud Run url
    let audience = 'https://example-ylabperdfq-uc.a.run.app'
    
    let jwtClient = new google.auth.JWT(
        privatekey.client_email,
        null,
        privatekey.private_key,
        audience
    )
    
    jwtClient.authorize(function(err, _token) {
        if (err) {
            console.log(err)
            return err
        } else {
            // console.log('token obj:', _token)
    
            request(
                {
                    url: url,
                    headers: {
                        "Authorization": "Bearer " + _token.id_token
                    }
                },
                function(err, response, body) {
                    if (err) {
                        console.log(err)
                        return err
                    } else {
                        // console.log('Response:', response)
                        console.log(body)
                    }
                }
            );
        }
    })

【讨论】:

  • 谢谢@John,我会试试这个,让你知道结果!
  • 感谢@John Hanley,此解决方案有效。有趣的是,我之前确实尝试过这个,但是我没有使用谷歌云运行生成的 url,而是使用了谷歌云全球平台范围 url,根据他们的文档,它提供了对所有谷歌服务的完全访问权限。我从来没有看到任何关于观众是云运行生成的 url,因为我什至尝试了云运行自定义域但没有成功。感谢您的解决方案,因为没有针对此特定用例的适当文档,它为我节省了很多时间!
  • 嘿,我可以在浏览器上做同样的事情,而不接触服务器吗?
  • @InzamamMalik 你在浏览器中是什么意思?我的示例使用 node.js。您可以在桌面上设置 node.js,然后运行它。如果你的意思是用 JavaScript 创建一个网页,是的,但我不知道所需的库支持。我建议创建一个新问题。
【解决方案3】:

您可以找到节点OAuth2的官方文档

A complete OAuth2 example:

const {OAuth2Client} = require('google-auth-library');
const http = require('http');
const url = require('url');
const open = require('open');
const destroyer = require('server-destroy');

// Download your OAuth2 configuration from the Google
const keys = require('./oauth2.keys.json');

/**
 * Start by acquiring a pre-authenticated oAuth2 client.
 */
async function main() {
  const oAuth2Client = await getAuthenticatedClient();
  // Make a simple request to the People API using our pre-authenticated client. The `request()` method
  // takes an GaxiosOptions object.  Visit https://github.com/JustinBeckwith/gaxios.
  const url = 'https://people.googleapis.com/v1/people/me?personFields=names';
  const res = await oAuth2Client.request({url});
  console.log(res.data);

  // After acquiring an access_token, you may want to check on the audience, expiration,
  // or original scopes requested.  You can do that with the `getTokenInfo` method.
  const tokenInfo = await oAuth2Client.getTokenInfo(
    oAuth2Client.credentials.access_token
  );
  console.log(tokenInfo);
}


/**
 * Create a new OAuth2Client, and go through the OAuth2 content
 * workflow.  Return the full client to the callback.
 */
function getAuthenticatedClient() {
  return new Promise((resolve, reject) => {
    // create an oAuth client to authorize the API call.  Secrets are kept in a `keys.json` file,
    // which should be downloaded from the Google Developers Console.
    const oAuth2Client = new OAuth2Client(
      keys.web.client_id,
      keys.web.client_secret,
      keys.web.redirect_uris[0]
    );

    // Generate the url that will be used for the consent dialog.
    const authorizeUrl = oAuth2Client.generateAuthUrl({
      access_type: 'offline',
      scope: 'https://www.googleapis.com/auth/userinfo.profile',
    });

    // Open an http server to accept the oauth callback. In this simple example, the
    // only request to our webserver is to /oauth2callback?code=<code>
    const server = http
      .createServer(async (req, res) => {
        try {
          if (req.url.indexOf('/oauth2callback') > -1) {
            // acquire the code from the querystring, and close the web server.
            const qs = new url.URL(req.url, 'http://localhost:3000')
              .searchParams;
            const code = qs.get('code');
            console.log(`Code is ${code}`);
            res.end('Authentication successful! Please return to the console.');
            server.destroy();

            // Now that we have the code, use that to acquire tokens.
            const r = await oAuth2Client.getToken(code);
            // Make sure to set the credentials on the OAuth2 client.
            oAuth2Client.setCredentials(r.tokens);
            console.info('Tokens acquired.');
            resolve(oAuth2Client);
          }
        } catch (e) {
          reject(e);
        }
      })
      .listen(3000, () => {
        // open the browser to the authorize url to start the workflow
        open(authorizeUrl, {wait: false}).then(cp => cp.unref());
      });
    destroyer(server);
  });
}

main().catch(console.error);

编辑

另一个云运行示例。

// sample-metadata:
//   title: ID Tokens for Cloud Run
//   description: Requests a Cloud Run URL with an ID Token.
//   usage: node idtokens-cloudrun.js <url> [<target-audience>]

'use strict';

function main(
  url = 'https://service-1234-uc.a.run.app',
  targetAudience = null
) {
  // [START google_auth_idtoken_cloudrun]
  /**
   * TODO(developer): Uncomment these variables before running the sample.
   */
  // const url = 'https://YOUR_CLOUD_RUN_URL.run.app';
  const {GoogleAuth} = require('google-auth-library');
  const auth = new GoogleAuth();

  async function request() {
    if (!targetAudience) {
      // Use the request URL hostname as the target audience for Cloud Run requests
      const {URL} = require('url');
      targetAudience = new URL(url).origin;
    }
    console.info(
      `request Cloud Run ${url} with target audience ${targetAudience}`
    );
    const client = await auth.getIdTokenClient(targetAudience);
    const res = await client.request({url});
    console.info(res.data);
  }

  request().catch(err => {
    console.error(err.message);
    process.exitCode = 1;
  });
  // [END google_auth_idtoken_cloudrun]
}

const args = process.argv.slice(2);
main(...args);

【讨论】:

  • 这是一个很好的例子,但针对的是不同的问题。问题是使用服务帐户。您的示例是使用用户凭据。不同的授权流程。
  • 谢谢@marian,但是,我没有客户端密码。我想这可能是最后的手段,但是,它并不能解决我当前的问题,因为我的目标是使用带有 JWT 的 service-account.json 文件进行服务到服务身份验证。
  • @marian.vladoi 谢谢,但是解决方案不起作用。我尝试了上面的 JWT 解决方案,但是,我得到了 400 或 401 错误,具体取决于我的尝试(使用范围或不使用范围,然后使用受众字符串请求身份令牌,仍然不起作用)
  • 更新的部分也不适用于该问题。您的代码会生成一个访问令牌。这不适用于需要身份令牌的服务。它们是不同形式的授权。
最近更新 更多