Welcome, guest | Sign In | My Account | Store | Cart
"""
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()
 

History