"""
Routines for programmatically authenticating with the Google Accounts system at
Google App-Engine.
This takes two calls, one to the ClientLogin service of Google Accounts,
and then a second to the login frontend of App Engine.
User credentials are provided to the first, which responds with a token.
Passing that token to the _ah/login GAE endpoint then gives the cookie that can
be used to make further authenticated requests.
Give the ACSID cookie to the client so it stays logged in with the GAE integrated users
system.
One last issue, after succesful authentication the current user's ID is still
missing; User(email).user_id() won't work. Here I think a HTTP redirect
should make the client re-request (using the cookie) and login, but the client
would need to support that. Alternatively the ID can be fetched within the
current request by a r/w round trip to the datastore, see:
http://stackoverflow.com/questions/816372/how-can-i-determine-a-user-id-based-on-an-email-address-in-app-engine
See also: http://markmail.org/thread/tgth5vmdqjacaxbx
"""
import logging, md5, urllib, urllib2
def do_auth(appname, user, password, dev=False, admin=False):
"This is taken from bits of appcfg, specifically: "
" google/appengine/tools/appengine_rpc.py "
"It returns the cookie send by the App Engine Login "
"front-end after authenticating with Google Accounts. "
if dev:
return do_auth_dev_appserver(user, admin)
# get the token
try:
auth_token = get_google_authtoken(appname, user, password)
except AuthError, e:
if e.reason == "BadAuthentication":
logging.error( "Invalid username or password." )
if e.reason == "CaptchaRequired":
logging.error(
"Please go to\n"
"https://www.google.com/accounts/DisplayUnlockCaptcha\n"
"and verify you are a human. Then try again.")
if e.reason == "NotVerified":
logging.error( "Account not verified.")
if e.reason == "TermsNotAgreed":
logging.error( "User has not agreed to TOS.")
if e.reason == "AccountDeleted":
logging.error( "The user account has been deleted.")
if e.reason == "AccountDisabled":
logging.error( "The user account has been disabled.")
if e.reason == "ServiceDisabled":
logging.error( "The user's access to the service has been "
"disabled.")
if e.reason == "ServiceUnavailable":
logging.error( "The service is not available; try again later.")
raise
# now get the cookie
cookie = get_gae_cookie(appname, auth_token)
assert cookie
return cookie
def do_auth_dev_appserver(email, admin):
"""Creates cookie payload data.
Args:
email, admin: Parameters to incorporate into the cookie.
Returns:
String containing the cookie payload.
"""
admin_string = 'False'
if admin:
admin_string = 'True'
if email:
user_id_digest = md5.new(email.lower()).digest()
user_id = '1' + ''.join(['%02d' % ord(x) for x in user_id_digest])[:20]
else:
user_id = ''
return 'dev_appserver_login="%s:%s:%s"; Path=/;' % (email, admin_string, user_id)
def get_gae_cookie(appname, auth_token):
"""
Send a token to the App Engine login, again stating the name of the
application to gain authentication for. Returned is a cookie that may be used
to authenticate HTTP traffic to the application at App Engine.
"""
continue_location = "http://localhost/"
args = {"continue": continue_location, "auth": auth_token}
host = "%s.appspot.com" % appname
url = "https://%s/_ah/login?%s" % (host,
urllib.urlencode(args))
opener = get_opener() # no redirect handler!
req = urllib2.Request(url)
try:
response = opener.open(req)
except urllib2.HTTPError, e:
response = e
if (response.code != 302 or
response.info()["location"] != continue_location):
raise urllib2.HTTPError(req.get_full_url(), response.code,
response.msg, response.headers, response.fp)
cookie = response.headers.get('set-cookie')
assert cookie and cookie.startswith('ACSID')
return cookie.replace('; HttpOnly', '')
def get_google_authtoken(appname, email_address, password):
"""
Make secure connection to Google Accounts and retrieve an authorisation
token for the stated appname.
The token can be send to the login front-end at appengine using
get_gae_cookie(), which will return a cookie to use for the user session.
"""
opener = get_opener()
# get an AuthToken from Google accounts
auth_uri = 'https://www.google.com/accounts/ClientLogin'
authreq_data = urllib.urlencode({ "Email": email_address,
"Passwd": password,
"service": "ah",
"source": appname,
"accountType": "HOSTED_OR_GOOGLE" })
req = urllib2.Request(auth_uri, data=authreq_data)
try:
response = opener.open(req)
response_body = response.read()
response_dict = dict(x.split("=")
for x in response_body.split("\n") if x)
return response_dict["Auth"]
except urllib2.HTTPError, e:
if e.code == 403:
body = e.read()
response_dict = dict(x.split("=", 1) for x in body.split("\n") if x)
raise AuthError(req.get_full_url(), e.code, e.msg,
e.headers, response_dict)
else:
raise
class AuthError(urllib2.HTTPError):
"""Raised to indicate there was an error authenticating."""
def __init__(self, url, code, msg, headers, args):
urllib2.HTTPError.__init__(self, url, code, msg, headers, None)
self.args = args
self.reason = args["Error"]
def get_opener(cookiejar=None):
opener = urllib2.OpenerDirector()
opener.add_handler(urllib2.ProxyHandler())
opener.add_handler(urllib2.UnknownHandler())
opener.add_handler(urllib2.HTTPHandler())
opener.add_handler(urllib2.HTTPDefaultErrorHandler())
opener.add_handler(urllib2.HTTPErrorProcessor())
opener.add_handler(urllib2.HTTPSHandler())
if cookiejar:
opener.add_handler(urllib2.HTTPCookieProcessor(cookiejar))
return opener
Diff to Previous Revision
--- revision 1 2010-05-20 14:06:42
+++ revision 2 2010-05-20 20:39:50
@@ -8,6 +8,18 @@
User credentials are provided to the first, which responds with a token.
Passing that token to the _ah/login GAE endpoint then gives the cookie that can
be used to make further authenticated requests.
+
+Give the ACSID cookie to the client so it stays logged in with the GAE integrated users
+system.
+
+One last issue, after succesful authentication the current user's ID is still
+missing; User(email).user_id() won't work. Here I think a HTTP redirect
+should make the client re-request (using the cookie) and login, but the client
+would need to support that. Alternatively the ID can be fetched within the
+current request by a r/w round trip to the datastore, see:
+http://stackoverflow.com/questions/816372/how-can-i-determine-a-user-id-based-on-an-email-address-in-app-engine
+
+See also: http://markmail.org/thread/tgth5vmdqjacaxbx
"""
import logging, md5, urllib, urllib2
@@ -50,20 +62,7 @@
# now get the cookie
cookie = get_gae_cookie(appname, auth_token)
assert cookie
- return parseCookie(cookie)
-
-def parseCookie(s):
- ret = {}
- tmp = s.split(';')
- for t in tmp:
- coppia = t.split('=')
- k = coppia[0].strip()
- v = coppia[1].strip()
- ret[k]=v
- if 'ACSID' in ret:
- return 'ACSID='+ret['ACSID']
- else:
- raise 'No cookie'
+ return cookie
def do_auth_dev_appserver(email, admin):
"""Creates cookie payload data.
@@ -109,7 +108,9 @@
raise urllib2.HTTPError(req.get_full_url(), response.code,
response.msg, response.headers, response.fp)
- return response.headers.get('set-cookie')
+ cookie = response.headers.get('set-cookie')
+ assert cookie and cookie.startswith('ACSID')
+ return cookie.replace('; HttpOnly', '')
def get_google_authtoken(appname, email_address, password):
"""
@@ -117,7 +118,7 @@
token for the stated appname.
The token can be send to the login front-end at appengine using
- get_gae_cookie(), which will return a cookie to use during the session.
+ get_gae_cookie(), which will return a cookie to use for the user session.
"""
opener = get_opener()