Reputation: 2169
I am realizing a google app engine application which implements multiple authentication (Google, Facebook and Twitter). I have a User entity in the NDB with one ID for each authentication service. The problem is that a user might have logged in with two different services, post different data (with a createdBy relationship to the current user) and then decide to merge the two different IDs. When this happens I am now looking into any entity created by the user and change the createdBy relationship in order to make it pointing at the merged user. I am wondering wether there is a more clever, fast and standard way to manage this kind of situation.
Upvotes: 2
Views: 529
Reputation: 331
This is the exact problem Google Identity Toolkit solves. It provides an easy way to integrate with popular identity providers and assigns a unique ID to the user no matter with which he/she chooses to sign in to your website. Drop an email to the discussion group if you have questions about Google Identity Toolkit.
Upvotes: 2
Reputation: 26643
There's no standard way I know. 2 pretty good libraries to use are simpleauth and/or engineauth. The author of engineauth (Kyle) might be a good person to ask about it.
It is feasible though preferably without reinventing a new user model and using webapp2_extras good user model.
class AuthHandler(BaseRequestHandler, SimpleAuthHandler):
"""Authentication handler for OAuth 2.0, 1.0(a) and OpenID."""
# Enable optional OAuth 2.0 CSRF guard
OAUTH2_CSRF_STATE = True
USER_ATTRS = {
'facebook': {
'id': lambda id: ('avatar_url',
'http://graph.facebook.com/{0}/picture?type=large'.format(id)),
'name': 'name',
'link': 'link'
},
'google': {
'picture': 'avatar_url',
'name': 'name',
'profile': 'link'
},
'windows_live': {
'avatar_url': 'avatar_url',
'name': 'name',
'link': 'link'
},
'twitter': {
'profile_image_url': 'avatar_url',
'screen_name': 'name',
'link': 'link'
},
'linkedin': {
'picture-url': 'avatar_url',
'first-name': 'name',
'public-profile-url': 'link'
},
'linkedin2': {
'picture-url': 'avatar_url',
'first-name': 'name',
'public-profile-url': 'link'
},
'foursquare': {
'photo': lambda photo: ('avatar_url', photo.get('prefix') + '100x100' + photo.get('suffix')),
'firstName': 'firstName',
'lastName': 'lastName',
'contact': lambda contact: ('email', contact.get('email')),
'id': lambda id: ('link', 'http://foursquare.com/user/{0}'.format(id))
},
'openid': {
'id': lambda id: ('avatar_url', '/img/missing-avatar.png'),
'nickname': 'name',
'email': 'link'
}
}
def _on_signin(self, data, auth_info, provider):
"""Callback whenever a new or existing user is logging in.
data is a user info dictionary.
auth_info contains access token or oauth token and secret.
"""
auth_id = '%s:%s' % (provider, data['id'])
logging.info('Looking for a user with id %s', auth_id)
user = self.auth.store.user_model.get_by_auth_id(auth_id)
_attrs = self._to_user_model_attrs(data, self.USER_ATTRS[provider])
if user:
logging.info('Found existing user to log in')
# Existing users might've changed their profile data so we update our
# local model anyway. This might result in quite inefficient usage
# of the Datastore, but we do this anyway for demo purposes.
#
# In a real app you could compare _attrs with user's properties fetched
# from the datastore and update local user in case something's changed.
user.populate(**_attrs)
user.put()
self.auth.set_session(
self.auth.store.user_to_dict(user))
else:
# check whether there's a user currently logged in
# then, create a new user if nobody's signed in,
# otherwise add this auth_id to currently logged in user.
if self.logged_in:
logging.info('Updating currently logged in user')
u = self.current_user
u.populate(**_attrs)
# The following will also do u.put(). Though, in a real app
# you might want to check the result, which is
# (boolean, info) tuple where boolean == True indicates success
# See webapp2_extras.appengine.auth.models.User for details.
u.add_auth_id(auth_id)
else:
logging.info('2 Creating a brand new user')
ok, user = self.auth.store.user_model.create_user(auth_id, **_attrs)
logging.info('2 created a brand new user %s | %s ' % (ok, user))
if ok:
self.auth.set_session(self.auth.store.user_to_dict(user))
# Remember auth data during redirect, just for this demo. You wouldn't
# normally do this.
#self.session.add_flash(data, 'data - from _on_signin(...)')
#self.session.add_flash(auth_info, 'auth_info - from _on_signin(...)')
user_dict = self.auth.get_user_by_session()
logging.info('the user_dict | %s ' % user_dict)
# Go to the profile page
self.redirect('/profile')
def logout(self):
self.auth.unset_session()
self.redirect('/')
def handle_exception(self, exception, debug):
logging.error(exception)
self.render('error.html', {'exception': exception})
def _callback_uri_for(self, provider):
return self.uri_for('auth_callback', provider=provider, _full=True)
def _get_consumer_info_for(self, provider):
"""Returns a tuple (key, secret) for auth init requests."""
return secrets.AUTH_CONFIG[provider]
def _to_user_model_attrs(self, data, attrs_map):
"""Get the needed information from the provider dataset."""
user_attrs = {}
for k, v in attrs_map.iteritems():
attr = (v, data.get(k)) if isinstance(v, str) else v(data.get(k))
user_attrs.setdefault(*attr)
return user_attrs
So it is possible to either make a "connection model" just with the connection (a list of the connected providers), negociate with projects that have been working on it (simpleauth and engineauth) or have a look at the code above and put in a method that can connect two accounts.
Upvotes: 0