Google Cloud Platform has introduced Identity Aware Proxy for protecting App Engine Flexible environment instances from public access.
However, it is not entirely clear if this can or should be used from Google Cloud Functions that are accessing GAE hosted API endpoints.
The documentation (with Python and Java examples) indicates an IAP authentication workflow consisting of 1) generating a JWT token, 2) creating an OpenID Token, 3) Then submitting requests to Google App Engine with an Authorization: Bearer TOKEN
header.
This seems quite convoluted for running cloud functions if authorisation has to happen each time a function is called.
Is there another way for Google cloud functions to access secured GAE endpoints?
You need to pass the file to Google Cloud Client Libraries, so they can generate the service account credentials at runtime. Google Cloud Client Libraries will automatically find and use the service account credentials by using the GOOGLE_APPLICATION_CREDENTIALS environment variable.
One way to control access to a function is to require that the requesting entity identify itself by using a credential. A credential is a "name" of some sort, secured by a secret that the entity knows or has access to, like a password or a hardware dongle.
There are various techniques for the authentication of Google Compute Engine API: Utilizing OAuth 2.0. Through client library. Straightforwardly with an entrance token.
The API service name is the name of your Cloud Run Endpoint gateway ENDPOINTS_SERVICE_NAME You can also activate the service through the GUI. Go to Google Cloud console, go to API & Services and select Library. Then, search for your API name ( Cloud Endpoints with API Keys if you don’t change the title in the endpoint.yaml file) and activate it.
There is several action to do for activing and using the API Keys for authentication. The API Key security is to add globally or on each path like in my example. For tests, you can replace this by x-google-allow: all for disabling the security. Now, the endpoint.yaml is fully defined, let’s go to deploy it on Cloud Endpoint service
Cloud Functions supports two types of authentication and authorization, Identity and Access Management (IAM) and OAuth 2.0. IAM is the most commonly used kind of access control in Cloud Functions. It works with two types of identities:
Using Google Cloud Console you create an OAuth 2.0 client ID (and in some cases, a client secret) for your client application, which serves as the credential. The client application sends this credential to the Google Authorization Server, which provides an access token.
If you want to make calls from GCF to IAP protected app, you should indeed be using ID tokens. There are no examples in Nodejs so I made one using this as a reference (style may be wrong since that's the first time I touch nodejs). Unlike regular JWT claims set, it should not contain scope and have 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 = "[email protected]"; // 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"
}
}
Update: Bundling service account key is not recommended, so a better option is to use the metadata server. For the below sample to work Google Identity and Access Management (IAM) API should be enabled and App Engine default service account should have Service Account Actor role (default Editor is not enough):
/**
* 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');
};
For anyone still looking at this 2020 and beyond Google has made this very easy.
Their docs have an example of how to auth IAP that works great in Cloud Functions:
// 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);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With