"""
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)