如何验证 google 云函数以访问安全应用引擎端点
How to authenticate google cloud functions for access to secure app engine endpoints
Google Cloud Platform 引入了身份识别代理,用于保护 App Engine 柔性环境实例免受 public 访问。
但是,尚不完全清楚这是否可以或应该用于访问 GAE 托管的 API 端点的 Google 云功能。
documentation(带有 Python 和 Java 示例)表示 IAP 身份验证工作流程,包括 1) 生成 JWT 令牌,2) 创建 OpenID 令牌,3) 然后使用 Authorization: Bearer TOKEN
header.
向 Google App Engine 提交请求
如果每次调用函数时都必须进行授权,这对于 运行 云函数来说似乎相当复杂。
Google 云函数是否有另一种方法来访问安全的 GAE 端点?
正如本文 doc 中所讨论的,您可以使用以下方法向 Google 云平台 (GCP) API 进行身份验证:
1- 服务帐户(首选方法)- 使用与您的 GCP 项目关联的 Google 帐户,而不是特定用户。
2- 用户帐户 - 当应用需要代表最终用户访问资源时使用。
3- API 密钥 - 通常在调用不需要访问私有数据的 API 时使用。
如果您想从 GCF 调用受 IAP 保护的应用程序,您确实应该使用 ID 令牌。 Nodejs 中没有示例,所以我使用 this 作为参考制作了一个(样式可能有误,因为这是我第一次接触 nodejs)。与常规 JWT 声明集不同,它不应包含范围并且具有 target_audience.
/**
* Make IAP request
*
*/
exports.CfToIAP = function CfToIAP (req, res) {
var crypto = require('crypto'),
request = require('request');
var token_URL = "https://www.googleapis.com/oauth2/v4/token";
// service account private key (copied from service_account.json)
var key = "-----BEGIN PRIVATE KEY-----\nMIIEvQexsQ1DBNe12345GRwAZM=\n-----END PRIVATE KEY-----\n";
// craft JWT
var JWT_header = new Buffer(JSON.stringify({ alg: "RS256", typ: "JWT" })).toString('base64');
// prepare claims set
var iss = "12345@12345.iam.gserviceaccount.com"; // service account email address (copied from service_account.json)
var aud = "https://www.googleapis.com/oauth2/v4/token";
var iat = Math.floor(new Date().getTime() / 1000);
var exp = iat + 120; // no need for a long linved token since it's not cached
var target_audience = "12345.apps.googleusercontent.com"; // this is the IAP client ID that can be obtained by clicking 3 dots -> Edit OAuth Client in IAP configuration page
var claims = {
iss: iss,
aud: aud,
iat: iat,
exp: exp,
target_audience: target_audience
};
var JWT_claimset = new Buffer(JSON.stringify(claims)).toString('base64');
// concatenate header and claimset
var unsignedJWT = [JWT_header, JWT_claimset].join('.');
// sign JWT
var JWT_signature = crypto.createSign('RSA-SHA256').update(unsignedJWT).sign(key, 'base64');
var signedJWT = [unsignedJWT, JWT_signature].join('.');
// get id_token and make IAP request
request.post({url:token_URL, form: {grant_type:'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion:signedJWT}}, function(err,res,body){
var data = JSON.parse(body);
var bearer = ['Bearer', data.id_token].join(' ');
var options = {
url: 'https://1234.appspot.com/', // IAP protected GAE app
headers: {
'User-Agent': 'cf2IAP',
'Authorization': bearer
}
};
request(options, function (err, res, body) {
console.log('error:', err);
});
});
res.send('done');
};
/**
* package.json
*
*/
{
"name": "IAP-test",
"version": "0.0.1",
"dependencies": {
"request": ">=2.83"
}
}
更新:不推荐捆绑服务账户密钥,所以更好的选择是使用元数据服务器。为了使下面的示例工作 Google 身份和访问管理 (IAM) API 应该启用并且 App Engine 默认服务帐户应该具有服务帐户参与者角色(默认编辑器是不够的):
/**
* Make request from CF to a GAE app behind IAP:
* 1) get access token from the metadata server.
* 2) prepare JWT and use IAM APIs projects.serviceAccounts.signBlob method to avoid bundling service account key.
* 3) 'exchange' JWT for ID token.
* 4) make request with ID token.
*
*/
exports.CfToIAP = function CfToIAP (req, res) {
// imports and constants
const request = require('request');
const user_agent = '<user_agent_to_identify_your_CF_call>';
const token_URL = "https://www.googleapis.com/oauth2/v4/token";
const project_id = '<project_ID_where_CF_is_deployed>';
const service_account = [project_id,
'@appspot.gserviceaccount.com'].join(''); // app default service account for CF project
const target_audience = '<IAP_client_ID>';
const IAP_GAE_app = '<IAP_protected_GAE_app_URL>';
// prepare request options and make metadata server access token request
var meta_req_opts = {
url: ['http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/',
service_account,
'/token'].join(''),
headers: {
'User-Agent': user_agent,
'Metadata-Flavor': 'Google'
}
};
request(meta_req_opts, function (err, res, body) {
// get access token from response
var meta_resp_data = JSON.parse(body);
var access_token = meta_resp_data.access_token;
// prepare JWT that is {Base64url encoded header}.{Base64url encoded claim set}.{Base64url encoded signature}
// https://developers.google.com/identity/protocols/OAuth2ServiceAccount for more info
var JWT_header = new Buffer(JSON.stringify({ alg: "RS256", typ: "JWT" })).toString('base64');
var iat = Math.floor(new Date().getTime() / 1000);
// prepare claims set and base64 encode it
var claims = {
iss: service_account,
aud: token_URL,
iat: iat,
exp: iat + 60, // no need for a long lived token since it's not cached
target_audience: target_audience
};
var JWT_claimset = new Buffer(JSON.stringify(claims)).toString('base64');
// concatenate JWT header and claims set and get signature usign IAM APIs projects.serviceAccounts.signBlob method
var to_sign = [JWT_header, JWT_claimset].join('.');
// sign JWT using IAM APIs projects.serviceAccounts.signBlob method
var signature_req_opts = {
url: ['https://iam.googleapis.com/v1/projects/',
project_id,
'/serviceAccounts/',
service_account,
':signBlob'].join(''),
method: "POST",
json: {
"bytesToSign": new Buffer(to_sign).toString('base64')
},
headers: {
'User-Agent': user_agent,
'Authorization': ['Bearer', access_token].join(' ')
}
};
request(signature_req_opts, function (err, res, body) {
// get signature from response and form JWT
var JWT_signature = body.signature;
var JWT = [JWT_header, JWT_claimset, JWT_signature].join('.');
// obtain ID token
request.post({url:token_URL, form: {grant_type:'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion:JWT}}, function(err, res, body){
// use ID token to make a request to the IAP protected GAE app
var ID_token_resp_data = JSON.parse(body);
var ID_token = ID_token_resp_data.id_token;
var IAP_req_opts = {
url: IAP_GAE_app,
headers: {
'User-Agent': user_agent,
'Authorization': ['Bearer', ID_token].join(' ')
}
};
request(IAP_req_opts, function (err, res, body) {
console.log('error:', err);
});
});
});
});
res.send('done');
};
有些人使用 Google 云密钥管理服务 (KMS) 来避免在云函数中对它们进行硬编码。
对于仍在关注 2020 年及以后的人来说,Google 已经让这一切变得非常简单。
他们的 docs 有一个如何验证在 Cloud Functions 中运行良好的 IAP 的示例:
// const url = 'https://some.iap.url';
// const targetAudience = 'IAP_CLIENT_ID.apps.googleusercontent.com';
const {GoogleAuth} = require('google-auth-library');
const auth = new GoogleAuth();
async function request() {
console.info(`request IAP ${url} with target audience ${targetAudience}`);
const client = await auth.getIdTokenClient(targetAudience);
const res = await client.request({url});
console.info(res.data);
}
python 示例:
from google.auth.transport.requests import Request as google_request
from google.oauth2 import id_token
open_id_connect_token = id_token.fetch_id_token(google_request(), client_id)
其中 client_id 是字符串。导航到 GCP 的 APIs & Services 部分,然后在左侧的 select credentails,在 AIP
的 OAuth2.0 部分中有 client_id
当你想请求安全的 IAP 服务时,只需添加到 headers
{'Authorization': 'Bearer your_open_id_connect_token'}
Google Cloud Platform 引入了身份识别代理,用于保护 App Engine 柔性环境实例免受 public 访问。
但是,尚不完全清楚这是否可以或应该用于访问 GAE 托管的 API 端点的 Google 云功能。
documentation(带有 Python 和 Java 示例)表示 IAP 身份验证工作流程,包括 1) 生成 JWT 令牌,2) 创建 OpenID 令牌,3) 然后使用 Authorization: Bearer TOKEN
header.
如果每次调用函数时都必须进行授权,这对于 运行 云函数来说似乎相当复杂。
Google 云函数是否有另一种方法来访问安全的 GAE 端点?
正如本文 doc 中所讨论的,您可以使用以下方法向 Google 云平台 (GCP) API 进行身份验证:
1- 服务帐户(首选方法)- 使用与您的 GCP 项目关联的 Google 帐户,而不是特定用户。
2- 用户帐户 - 当应用需要代表最终用户访问资源时使用。
3- API 密钥 - 通常在调用不需要访问私有数据的 API 时使用。
如果您想从 GCF 调用受 IAP 保护的应用程序,您确实应该使用 ID 令牌。 Nodejs 中没有示例,所以我使用 this 作为参考制作了一个(样式可能有误,因为这是我第一次接触 nodejs)。与常规 JWT 声明集不同,它不应包含范围并且具有 target_audience.
/**
* Make IAP request
*
*/
exports.CfToIAP = function CfToIAP (req, res) {
var crypto = require('crypto'),
request = require('request');
var token_URL = "https://www.googleapis.com/oauth2/v4/token";
// service account private key (copied from service_account.json)
var key = "-----BEGIN PRIVATE KEY-----\nMIIEvQexsQ1DBNe12345GRwAZM=\n-----END PRIVATE KEY-----\n";
// craft JWT
var JWT_header = new Buffer(JSON.stringify({ alg: "RS256", typ: "JWT" })).toString('base64');
// prepare claims set
var iss = "12345@12345.iam.gserviceaccount.com"; // service account email address (copied from service_account.json)
var aud = "https://www.googleapis.com/oauth2/v4/token";
var iat = Math.floor(new Date().getTime() / 1000);
var exp = iat + 120; // no need for a long linved token since it's not cached
var target_audience = "12345.apps.googleusercontent.com"; // this is the IAP client ID that can be obtained by clicking 3 dots -> Edit OAuth Client in IAP configuration page
var claims = {
iss: iss,
aud: aud,
iat: iat,
exp: exp,
target_audience: target_audience
};
var JWT_claimset = new Buffer(JSON.stringify(claims)).toString('base64');
// concatenate header and claimset
var unsignedJWT = [JWT_header, JWT_claimset].join('.');
// sign JWT
var JWT_signature = crypto.createSign('RSA-SHA256').update(unsignedJWT).sign(key, 'base64');
var signedJWT = [unsignedJWT, JWT_signature].join('.');
// get id_token and make IAP request
request.post({url:token_URL, form: {grant_type:'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion:signedJWT}}, function(err,res,body){
var data = JSON.parse(body);
var bearer = ['Bearer', data.id_token].join(' ');
var options = {
url: 'https://1234.appspot.com/', // IAP protected GAE app
headers: {
'User-Agent': 'cf2IAP',
'Authorization': bearer
}
};
request(options, function (err, res, body) {
console.log('error:', err);
});
});
res.send('done');
};
/**
* package.json
*
*/
{
"name": "IAP-test",
"version": "0.0.1",
"dependencies": {
"request": ">=2.83"
}
}
更新:不推荐捆绑服务账户密钥,所以更好的选择是使用元数据服务器。为了使下面的示例工作 Google 身份和访问管理 (IAM) API 应该启用并且 App Engine 默认服务帐户应该具有服务帐户参与者角色(默认编辑器是不够的):
/**
* Make request from CF to a GAE app behind IAP:
* 1) get access token from the metadata server.
* 2) prepare JWT and use IAM APIs projects.serviceAccounts.signBlob method to avoid bundling service account key.
* 3) 'exchange' JWT for ID token.
* 4) make request with ID token.
*
*/
exports.CfToIAP = function CfToIAP (req, res) {
// imports and constants
const request = require('request');
const user_agent = '<user_agent_to_identify_your_CF_call>';
const token_URL = "https://www.googleapis.com/oauth2/v4/token";
const project_id = '<project_ID_where_CF_is_deployed>';
const service_account = [project_id,
'@appspot.gserviceaccount.com'].join(''); // app default service account for CF project
const target_audience = '<IAP_client_ID>';
const IAP_GAE_app = '<IAP_protected_GAE_app_URL>';
// prepare request options and make metadata server access token request
var meta_req_opts = {
url: ['http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/',
service_account,
'/token'].join(''),
headers: {
'User-Agent': user_agent,
'Metadata-Flavor': 'Google'
}
};
request(meta_req_opts, function (err, res, body) {
// get access token from response
var meta_resp_data = JSON.parse(body);
var access_token = meta_resp_data.access_token;
// prepare JWT that is {Base64url encoded header}.{Base64url encoded claim set}.{Base64url encoded signature}
// https://developers.google.com/identity/protocols/OAuth2ServiceAccount for more info
var JWT_header = new Buffer(JSON.stringify({ alg: "RS256", typ: "JWT" })).toString('base64');
var iat = Math.floor(new Date().getTime() / 1000);
// prepare claims set and base64 encode it
var claims = {
iss: service_account,
aud: token_URL,
iat: iat,
exp: iat + 60, // no need for a long lived token since it's not cached
target_audience: target_audience
};
var JWT_claimset = new Buffer(JSON.stringify(claims)).toString('base64');
// concatenate JWT header and claims set and get signature usign IAM APIs projects.serviceAccounts.signBlob method
var to_sign = [JWT_header, JWT_claimset].join('.');
// sign JWT using IAM APIs projects.serviceAccounts.signBlob method
var signature_req_opts = {
url: ['https://iam.googleapis.com/v1/projects/',
project_id,
'/serviceAccounts/',
service_account,
':signBlob'].join(''),
method: "POST",
json: {
"bytesToSign": new Buffer(to_sign).toString('base64')
},
headers: {
'User-Agent': user_agent,
'Authorization': ['Bearer', access_token].join(' ')
}
};
request(signature_req_opts, function (err, res, body) {
// get signature from response and form JWT
var JWT_signature = body.signature;
var JWT = [JWT_header, JWT_claimset, JWT_signature].join('.');
// obtain ID token
request.post({url:token_URL, form: {grant_type:'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion:JWT}}, function(err, res, body){
// use ID token to make a request to the IAP protected GAE app
var ID_token_resp_data = JSON.parse(body);
var ID_token = ID_token_resp_data.id_token;
var IAP_req_opts = {
url: IAP_GAE_app,
headers: {
'User-Agent': user_agent,
'Authorization': ['Bearer', ID_token].join(' ')
}
};
request(IAP_req_opts, function (err, res, body) {
console.log('error:', err);
});
});
});
});
res.send('done');
};
有些人使用 Google 云密钥管理服务 (KMS) 来避免在云函数中对它们进行硬编码。
对于仍在关注 2020 年及以后的人来说,Google 已经让这一切变得非常简单。
他们的 docs 有一个如何验证在 Cloud Functions 中运行良好的 IAP 的示例:
// const url = 'https://some.iap.url';
// const targetAudience = 'IAP_CLIENT_ID.apps.googleusercontent.com';
const {GoogleAuth} = require('google-auth-library');
const auth = new GoogleAuth();
async function request() {
console.info(`request IAP ${url} with target audience ${targetAudience}`);
const client = await auth.getIdTokenClient(targetAudience);
const res = await client.request({url});
console.info(res.data);
}
python 示例:
from google.auth.transport.requests import Request as google_request
from google.oauth2 import id_token
open_id_connect_token = id_token.fetch_id_token(google_request(), client_id)
其中 client_id 是字符串。导航到 GCP 的 APIs & Services 部分,然后在左侧的 select credentails,在 AIP
的 OAuth2.0 部分中有 client_id当你想请求安全的 IAP 服务时,只需添加到 headers
{'Authorization': 'Bearer your_open_id_connect_token'}