Welcome, guest | Sign In | My Account | Store | Cart

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.

Python, 166 lines
  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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
"""
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

Besides using the token to request Google user data, the cookie will also login a client at GAE integrated Google Accounts API too. Ie, after authenticating an (in my case gmail) account and setting the cookie, the next request GAE initializes the USER_EMAIL/USER_ID correctly into os.environ, from where users.User picks it up. See http://code.activestate.com/recipes/577235-gae-user-session-with-http-basic-authentication/