lowcoupling
lowcoupling

Reputation: 2169

How to manage multiple authentication services in a web app (Google, Facebook, Twitter)

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

Answers (2)

mengcheng
mengcheng

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

Niklas Rosencrantz
Niklas Rosencrantz

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

Related Questions