Using remote_api with OpenID authentication

When we recently released integrated OpenID support for App Engine, one unfortunate side-effect for apps that enable it was disruption to authenticated, programmatic access to your App Engine app. Specifically, if you've switched your app to use OpenID for authentication, remote_api - and the remote_api console - will no longer work.

The bad news is that fixing this is tough: OpenID is designed as a browser-interactive authentication mechanism, and it's not clear what the best way to do authentication for command line tools like the remote_api console is going to be. Quite likely the solution will involve our OAuth support and stored credentials - stay tuned!

The good news, though, is that there's a workaround that you can use right now, without compromising the security of your app. It's a bit of a hack, though, so brace yourself!

The essential insight behind the hack is that if we can trick the SDK into thinking that it's authenticating against the development server instead of production, it will prompt the user for an email address and password, then send that email address embedded in the 'dev_appserver_login' cookie with all future requests. We can then use the email field to instead hold a secret key that we can use to authenticate you to your app.

This requires only a simple modification to the server component of remote_api, using the techniques I previously outlined, and no modification at all to your SDK. Without further ado, here's the patch:

from google.appengine.ext.remote_api import handler
from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app

import re


MY_SECRET_KEY = 'topsecret'


cookie_re = re.compile('^"([^:]+):.*"$')


class ApiCallHandler(handler.ApiCallHandler):
  def CheckIsAdmin(self):
    login_cookie = self.request.cookies.get('dev_appserver_login', '')
    match = cookie_re.search(login_cookie)
    if (match and match.group(1) == MY_SECRET_KEY
        and 'X-appcfg-api-version' in self.request.headers):
      return True
    else:
      self.redirect('/_ah/login')
      return False


application = webapp.WSGIApplication([('.*', ApiCallHandler)])


def main():
  run_wsgi_app(application)


if __name__ == '__main__':
  main()

This should be fairly easy to understand, but let's go through it piece by piece. First, we define a constant, 'MY_SECRET_KEY'. I highly recommend setting this to something other than the string 'topsecret', as we have in this demo! Then, we define a regular expression that matches the email part of the dev_appserver_login cookie.

The class ApiCallHandler extends the regular remote_api handler as we demonstrated in the previous article, and overrides its "CheckIsAdmin" method. This method is responsible for doing authentication, and usually it simply checks that the user is logged in as an administrator using the Users API. In this case, though, we instead look for the dev_appserver_login cookie, and if it exists, check if the secret key matches. If it does not, we send the user a redirect to '/_ah/login', and indicate that authentication was not successful.

It's this redirect that is essential to the hack. The way the SDK determines if it's authenticating against a development server or production is by checking the nature of the redirect sent back from unauthenticated requests; if it gets a redirect to '/_ah/login', it assumes it's the dev_appserver it's talking to, and assembles the login cookie that we check for above.

Only one other change is required to make use of this hack - you must update your mapping in app.yaml to send remote_api requests to our custom handler, rather than the standard one. Save the above file as remote_api.py, and replace the remote_api handler in app.yaml with the following:

- url: /remote_api
  script: remote_api.py

Note that we didn't include the usual "login: admin" declaration in this handler. It's important that you leave that out: without it, our handler will do the authentication itself using our hack, but if you leave it in, execution never reaches our handler - the App Engine infrastructure checks for a regular OpenID login, and refuses to execute our code if it's not present.

Comments

blog comments powered by Disqus