HTTP Basic is an unsecure but easy to implement authentication protocol. I think its good enough for a simple client in front of an SSL capable server. Google App-Engine supports SSL, and here is a recipe to set up the user-session using HTTP Basic.
gauth has the code from my not-really-a-recipe listing at: http://code.activestate.com/recipes/577217-routines-for-programmatically-authenticating-with-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | """
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()
|
Using GAE Sessions [1] I think it would be possible to add authentication for other account service providers too, perhaps prefix the user-name in that case. Forgot to add logout btw, but Basic does not specify it. It may be convenient to have a 'logout' username or something for browser users to get the cookie at the client-side reset (overwritten), but I've made the assumption the authentication is needed for other simpler non-html clients.