在Google Cloud Run中使用默认凭据的域范围委派



我正在使用一个自定义服务帐户(使用deploy命令中的--service-account参数(。该服务帐户已启用域范围委派,并且已安装在"G应用程序管理"面板中。

我试过这个代码:

app.get('/test', async (req, res) => {
const auth = new google.auth.GoogleAuth()
const gmailClient = google.gmail({ version: 'v1' })
const { data } = await gmailClient.users.labels.list({ auth, userId: 'user@domain.com' })
return res.json(data).end()
})

如果我在我的机器上运行它(将GOOGLE_APPLICATION_CREDENTIALSenv var设置为分配给Cloud run服务的同一服务帐户的路径(,它就会工作,但当它在Cloud run中运行时,我会得到以下响应:

{
"code" : 400,
"errors" : [ {
"domain" : "global",
"message" : "Bad Request",
"reason" : "failedPrecondition"
} ],
"message" : "Bad Request"
}

我看到了这个解决方案,但它是针对Python的,我不知道如何使用Node库复制这种行为。

经过几天的研究,我终于找到了一个可行的解决方案(移植Python实现(:
async function getGoogleCredentials(subject: string, scopes: string[]): Promise<JWT | OAuth2Client> {
const auth = new google.auth.GoogleAuth({
scopes: ['https://www.googleapis.com/auth/cloud-platform'],
})
const authClient = await auth.getClient()
if (authClient instanceof JWT) {
return (await new google.auth.GoogleAuth({ scopes, clientOptions: { subject } }).getClient()) as JWT
} else if (authClient instanceof Compute) {
const serviceAccountEmail = (await auth.getCredentials()).client_email
const unpaddedB64encode = (input: string) =>
Buffer.from(input)
.toString('base64')
.replace(/=*$/, '')
const now = Math.floor(new Date().getTime() / 1000)
const expiry = now + 3600
const payload = JSON.stringify({
aud: 'https://accounts.google.com/o/oauth2/token',
exp: expiry,
iat: now,
iss: serviceAccountEmail,
scope: scopes.join(' '),
sub: subject,
})
const header = JSON.stringify({
alg: 'RS256',
typ: 'JWT',
})
const iamPayload = `${unpaddedB64encode(header)}.${unpaddedB64encode(payload)}`
const iam = google.iam('v1')
const { data } = await iam.projects.serviceAccounts.signBlob({
auth: authClient,
name: `projects/-/serviceAccounts/${serviceAccountEmail}`,
requestBody: {
bytesToSign: unpaddedB64encode(iamPayload),
},
})
const assertion = `${iamPayload}.${data.signature!.replace(/=*$/, '')}`
const headers = { 'content-type': 'application/x-www-form-urlencoded' }
const body = querystring.encode({ assertion, grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer' })
const response = await fetch('https://accounts.google.com/o/oauth2/token', { method: 'POST', headers, body }).then(r => r.json())
const newCredentials = new OAuth2Client()
newCredentials.setCredentials({ access_token: response.access_token })
return newCredentials
} else {
throw new Error('Unexpected authentication type')
}
}

这是Victor答案的镜像,只是它使用IACredentialsClient来签名令牌,而不是不推荐使用的iam.projects.serviceAccounts.signBlob。此代码是一个完整的TS文件,因此您可以将其放入项目中,并按如下方式使用:

const prov = new ApplicationDefaultCredentials(workspaceAdminEmail@mydomain.com)
return prov.getAccessToken(scopes)

请注意,只有当您需要通过域范围的委派访问Workspace API时,才需要该电子邮件。换句话说,这个类可以在任何需要生成访问谷歌资源的令牌的地方工作,即使你没有提供电子邮件。如果您通过Google库访问资源,则不需要此类,但如果您使用RESTAPI并需要获取访问令牌,则此类非常方便。

/**
* This class mimics the Application Default Credentials flow and works both in devel and production.
* * It exposes a getAccessToken() method that can be used to get a Bearer token for Authentication and Authorization.
* * The generated token will also work for google workspace APIs calls that are authorized with domain wide delegation.
*   When requiring access to Workspace APIs with domain wide delegation, an emailToImpersonate must be provided to the constructor.
* * In Development mode, you will need to get set the GOOGLE_APPLICATION_CREDENTIALS to a key file that
*   you created for the service account
* * In production, you do not need to use a service account key at all because this code will generate the key dynamically
*   based on the service account specified with the Application Default Credentials.  It does this by generating a JWT token,
*   using the service account to sign it, and then requesting a token based on that.
* * To use this class, Create an instance via:
*       const provider = new ApplicationDefaultCredentials(emailToImpersonate?: string))
*   Then use it like this:
*       const bearerToken = provider.getAccessToken(scopes)
* */
import { Compute, GoogleAuth, JWT } from 'google-auth-library'
import { IAMCredentialsClient } from '@google-cloud/iam-credentials'
const querystring = require('querystring')
export class ApplicationDefaultCredentials {
scopes: string[] = []
emailToImpersonate?: string
token?: any
tokenExpiry?: Date
constructor(emailToImpersonate?: string) {
this.emailToImpersonate = emailToImpersonate
this.token = undefined
this.tokenExpiry = undefined
}
async getAccessToken(scopes: string[]): Promise<any> {
// Use this hack because "this" in the context of the Promise's anonymous function does not refer to this class.
const self = this
return new Promise(async (resolve, reject) => {
if (self.scopes !== scopes) {
self.token = undefined
self.tokenExpiry = undefined
}
self.scopes = scopes
if (self.token && self.tokenExpiry && self.tokenExpiry > new Date()) {
console.log('Reusing token')
return resolve(self.token)
}
try {
let clientOptions: any = {}
if (self.emailToImpersonate) clientOptions = { subject: self.emailToImpersonate }
const auth = new GoogleAuth({
scopes: scopes,
clientOptions
})
const client = await auth.getClient()
if (client instanceof JWT) {
const json = await client.getAccessToken()
self.token = json.token
// the token expiry does not seem to be returned.  If it is undefined, the token will simply not be cached
self.tokenExpiry = json.res?.data.tokenExpiry
resolve(self.token)
return
}
if (!(client instanceof Compute))
throw new Error(`Unexpected authentication type: ${client.constructor!.name}`)
try {
// Create a JWT Token signed with the service account
// Translated this code from https://stackoverflow.com/questions/60435998/domain-wide-delegation-using-default-credentials-in-google-cloud-run?rq=2
// Also took advice from https://developers.google.com/identity/protocols/oauth2/service-account#delegatingauthority
const serviceAccountEmail = (await auth.getCredentials()).client_email
// Create the JWT Payload
const now = Math.floor(new Date().getTime() / 1000)
const expiry = now + 3600
const payload = JSON.stringify({
aud: 'https://oauth2.googleapis.com/token',
exp: expiry,
iat: now,
iss: serviceAccountEmail,
scope: scopes.join(' '),
sub: self.emailToImpersonate
})
const header = JSON.stringify({
alg: 'RS256',
typ: 'JWT'
})
const iamPayload = `${this.unpaddedB64encode(header)}.${this.unpaddedB64encode(payload)}`
// get the JWT Payload signature
const credentialsClient = new IAMCredentialsClient()
const request = {
name: `projects/-/serviceAccounts/${serviceAccountEmail}`,
payload: this.unpaddedB64encode(iamPayload)
}
const [data] = await credentialsClient.signBlob(request)
if (!data.signedBlob) return reject('Could not sign blob')
// send the signed JWT token
const blob64 = this.unpaddedB64encode(data.signedBlob)
const assertion = `${iamPayload}.${blob64}`
const headers = { 'content-type': 'application/x-www-form-urlencoded' }
const body = querystring.encode({
assertion,
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer'
})
const res = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers,
body
})
let json = await res.json()
if (json.error) throw new Error(`${json.error}: ${json.error_description}`)
if (!json.access_token) throw new Error(`No Access token returned: ${JSON.stringify(json)}`)
self.token = json.access_token
let expiresAt = json.expires_at
? json.expires_at * 1000
: json.expires_in
? json.expires_in * 1000 + Date.now()
: undefined
if (expiresAt) self.tokenExpiry = new Date(expiresAt)
resolve(json.access_token)
} catch (error: any) {
console.error('Error generating JWT Token:', error)
reject(error.message ?? error)
}
} catch (err: any) {
console.error('Error in getAccessToken:', err)
reject(err.message ?? err)
}
})
}
unpaddedB64encode(input: string | Uint8Array) {
return Buffer.from(input).toString('base64').replace(/=*$/, '')
}
}

您可以在yaml文件中定义ENV变量,如本文档所述,将GOOGLE_APPLICATION_CREDENTIALS设置为JSON密钥的路径。

然后使用像这里提到的代码。

const authCloudExplicit = async ({projectId, keyFilename}) => {
// [START auth_cloud_explicit]
// Imports the Google Cloud client library.
const {Storage} = require('@google-cloud/storage');
// Instantiates a client. Explicitly use service account credentials by
// specifying the private key file. All clients in google-cloud-node have this
// helper, see https://github.com/GoogleCloudPlatform/google-cloud-node/blob/master/docs/authentication.md
// const projectId = 'project-id'
// const keyFilename = '/path/to/keyfile.json'
const storage = new Storage({projectId, keyFilename});
// Makes an authenticated API request.
try {
const [buckets] = await storage.getBuckets();
console.log('Buckets:');
buckets.forEach(bucket => {
console.log(bucket.name);
});
} catch (err) {
console.error('ERROR:', err);
}
// [END auth_cloud_explicit]
};

或者采用类似于这里提到的方法。

'use strict';
const {auth, Compute} = require('google-auth-library');

async function main() {
const client = new Compute({
serviceAccountEmail: 'some-service-account@example.com',
});
const projectId = await auth.getProjectId();
const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`;
const res = await client.request({url});
console.log(res.data);
}
main().catch(console.error);

最新更新