Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Google App Engine: secure inter-app communication

I have two apps on Google App Engine, both running under the same account, and one invokes services provided by the other over HTTPS. What is the recommended way of ensuring that only the first app is permitted to invoke the second?

Alternatively, is there a way to specify that given endpoint can only be invoked by an app running under the same GAE account?

like image 503
narthi Avatar asked Nov 06 '12 06:11

narthi


2 Answers

Have your application check for the 'X-Appengine-Inbound-Appid' header and make sure the app ID is correct. This header only exists if the request was made by another Google App Engine app and cannot be modified by users.

If you are using Python, you could do the following:

import webapp2

AUTHORIZED_APPS = ('my-first-app', 'my-other-app')

class MyHandler(webapp2.RequestHandler):
    def dispatch(self):
        app_id = self.request.headers.get('X-Appengine-Inbound-Appid', None)

        if app_id in AUTHORIZED_APPS:   
            super(MyHandler, self).dispatch()
        else:
            self.abort(403)

That will raise a 403 for any request that does not have an X-Appengine-Inbound-Appid in its header.

Also, when making requests from one application to another using urlfetch, make sure you set follow_redirects=False or the header does not get added.

like image 67
Aaron Hampton Avatar answered Oct 13 '22 21:10

Aaron Hampton


As other people have pointed out, relying on the header X-Appengine-Inbound-Appid being filled out is the easiest solution. I recently had a similar problem, but I could not use X-Appengine-Inbound-Appid because URLFetch was not available (GAE Node.js). Here is how to solve the problem using service accounts authenticated through OAuth.

On the sender side you need set up a service account: https://developers.google.com/identity/protocols/OAuth2ServiceAccount#creatinganaccount

Then in that app you will need to get the service account credentials: https://developers.google.com/identity/protocols/application-default-credentials

Then you can use the credentials to make an authClient, that you can use to send the request. You will need to add an OAuth scope to your authClient. The mostly logical one to use is https://www.googleapis.com/auth/userinfo.email. This will enable the receiver to get the email address of the sender's service account. https://developers.google.com/identity/protocols/googlescopes

Here is the code for making it work in (sender) Node.js:

process.env.GOOGLE_APPLICATION_CREDENTIALS = <PATH TO CREDENTIALS FILE>

google.auth.getApplicationDefault((err, authClient) => {
  if (err) {
    console.log("Failed to get default credentials: ", String(err));
    return;
  }

  if (authClient.createScopedRequired && authClient.createScopedRequired()) {
    authClient = authClient.createScoped([
      'https://www.googleapis.com/auth/userinfo.email'
    ]);
  }

  auth_client.request({
    url: <RECEIVER URL>,
    method: "GET"
  }, (error, result, response) => {
    // Process response
  });
});

Then on the receiver side you need to verify the email address matches the email address of the sender's service account. When the app engine is invoked locally the OAuth request is not authenticated properly, so if you want to test locally you will have to do a url fetch to verify the request yourself.

Receiver Python:

scope = "https://www.googleapis.com/auth/userinfo.email"
allowed_users = set([
    "<SENDER SERVICE ACCOUNT EMAIL>"
])
IS_DEV = os.environ["SERVER_SOFTWARE"][:3] == "Dev"

class MyHandler(webapp2.RequestHandler):
    def get(self, clientId):
        user = self.get_current_user()
        if user in allowed_users:
            # Do whatever you wanted

    def get_current_user(self):
        if IS_DEV:
            token = self.request.headers.get("Authorization")[len("Bearer "):]
            response = urlfetch.fetch(
                "https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=%s" % token
            )
            return json.loads(response.content)["email"]
        else:
            return oauth.get_current_user(scope)
like image 33
misterwilliam Avatar answered Oct 13 '22 21:10

misterwilliam