Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to authenticate google cloud functions for access to secure app engine endpoints

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?

like image 310
songololo Avatar asked Aug 20 '17 23:08

songololo


People also ask

How do I authenticate Google Cloud API?

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.

How to secure a Google Cloud function?

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.

What are the different methods for the authentication of Google Compute Engine API?

There are various techniques for the authentication of Google Compute Engine API: Utilizing OAuth 2.0. Through client library. Straightforwardly with an entrance token.

How to activate Google Cloud API service?

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.

How to use API keys for authentication on cloud endpoint service?

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

What types of authentication and authorization does Cloud Functions support?

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:

How does OAuth work with Google Cloud console?

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.


2 Answers

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');
};
like image 150
Nikita Uchaev Avatar answered Nov 15 '22 09:11

Nikita Uchaev


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);
}
like image 32
RayB Avatar answered Nov 15 '22 08:11

RayB