Welcome, guest | Sign In | My Account | Store | Cart
"""
A simple script to handle HTTP Basic authentication for Google App Engine
clients. The trick here is that there is no other session support than the 
GAE internal authorized user handling. Which however does not support HTTP
Basic, an auth protocol which is easy to use (for small, scripted clients).
"""
import os, logging, functools, base64

from google.appengine.ext.webapp.util import run_wsgi_app
from google.appengine.ext import webapp
from google.appengine.api import users

import gauth


MY_APPSPOT = 'my-app'
logger = logging.getLogger(__name__)

def user(method):
    " Prefix the user to method call args. Must have ACSID cookie to log in.  "
    @functools.wraps(method)
    def user_deco(self, *args):
        user = users.get_current_user()
        assert isinstance(user, users.User), "%r" % user
        return method(self, user, *args)              
    return user_deco

def http_basic_auth(method):
    " Prefix user to method call. Authenticates using HTTP Basic Authorization.  "
    @functools.wraps(method)
    def http_basic_auth_deco(self, *args):
        user = users.get_current_user()
        if not user:
            basic_auth = self.request.headers.get('Authorization')
            if not basic_auth:
                logger.debug("Request does not carry auth.")
                self.fail_basic_auth()
                return
            username, password = '', ''
            try:
                user_info = base64.decodestring(basic_auth[6:])
                username, password = user_info.split(':')
            except:
                raise Exception, "Could not parse HTTP Authorization. "
            cookie = None
            try:
                cookie = gauth.do_auth(MY_APPSPOT, username, password, 
                        dev=self.is_devhost())
            except gauth.AuthError, e:
                logger.info("Got a failed login attempt for Google Accounts %r", 
                        username)
                self.fail_auth()
                return
            self.response.set_status(302, 'Use Cookie')
            # Give ACSID cookie to client
            self.response.headers['Set-Cookie'] = cookie
            # Then redirect so clients logs into user-framework
            # XXX: dont use internal redir b/c it resets self.response
            self.response.headers['Location'] = self.request.path
            return
        elif 'Authorization' in self.request.headers:
            # FIXME: ignore or act on (new?) credentials
            assert 'USER_ID' in os.environ
            # XXX: just delete and ignore
            del self.request.headers['Authorization']
            del os.environ['HTTP_AUTHORIZATION']
            # XXX: this may happen once if a client retries auth on 302
            logger.warning("Ignored HTTP Authorization.")
        assert isinstance(user, users.User), "%r" % user
        return method(self, user, *args)              
    return http_basic_auth_deco


# Abstract handlers

class MyRequestHandler(webapp.RequestHandler):

    def is_devhost(self):
        return not self.request.host.endswith('.appspot.com')

class MyAuthorizedHandler(MyRequestHandler):

    def fail_auth(handler):
        handler.error(401)

    def fail_basic_auth(handler):
        handler.fail_auth()
        handler.response.headers['WWW-Authenticate'] = \
                'Basic realm="Google Accounts for %s.appspot.com."' % MY_APPSPOT

# Request handlers

class MyAuthenticationEndpoint(MyAuthorizedHandler):
    urlp = 'my-auth$'

    @http_basic_auth
    def get(self, user):
        logger.info("User was logged in: %r", user)
        self.response.out.write("%s <a href='%s'>logout</a>" % (
                user.email(),
                users.create_logout_url(self.request.url),))

class MyAuthenticatedEndpoint(MyAuthorizedHandler):
    urlp = 'some-leaf$'

    @user
    def get(self, user):
        logger.info("User %r requested %r", user, self.request.url)
        self.response.out.write("%s <a href='%s'>logout</a>" % (
                user.email(),
                users.create_logout_url(self.request.url),))


application = webapp.WSGIApplication(map(
    lambda h: ('^/'+h.urlp, h), [ # map from URL-path-pattern @ RequestHandler
        MyAuthenticationEndpoint,
        MyAuthenticatedEndpoint,
    ]), debug=True)


def webapp_main():
    """Run within webapp framework.
    """
    # Ofcourse, instead allowing solely Google Accounts and using the integrated
    # system, this recipe could be extended to also login other accounts.
    # However, there the automatic Cookie checking would obviously fail and you
    # would need to do ones own checking. Possibly by using a session instead of
    # cookies, by using other cookies, or by simply having each HTTP request
    # Authorized; unlike this recipe. This is two-legged authentication, so the 
    # latter would not be advised (doing (re)authentication for repeated requests). 
    #from gaesessions import SessionMiddleware
    #run_wsgi_app(SessionMiddleware(application))

    run_wsgi_app(application)

if __name__ == '__main__':
    webapp_main()

Diff to Previous Revision

--- revision 5 2010-05-20 21:09:14
+++ revision 6 2010-05-20 23:49:49
@@ -64,6 +64,7 @@
             # XXX: just delete and ignore
             del self.request.headers['Authorization']
             del os.environ['HTTP_AUTHORIZATION']
+            # XXX: this may happen once if a client retries auth on 302
             logger.warning("Ignored HTTP Authorization.")
         assert isinstance(user, users.User), "%r" % user
         return method(self, user, *args)              

History