Tom
Tom

Reputation: 44633

How do I migrate users from OpenID to Google OAuth2/OpenID Connect using Python Social Auth?

Google is deprecating the OpenID endpoint I was using (v1.0 I think, via the django_openid_auth module) and I need to update my app and migrate my users' accounts to use Google OAuth2.

I've changed the app to use python-social-auth and have it authenticating with social.backends.google.GoogleOAuth2 successfully.

I've written a pipeline function to find associated OpenID urls from the old table and this is working for the other backends I care about but Google:

def associate_legacy_user(backend, response, uid=None, user=None,
                          *args, **kwargs):
    if uid and not user:
        # Try to associate accounts registered in the old openid table
        identity_url = None

        if backend.name == 'google-oauth2':
            # TODO: this isn't working
            identity_url = response.get('open_id')

        else:
            # for all other backends, see if there is a claimed_id url
            # matching the identity_url use identity_url instead of uid
            # as uid may be the user's email or username
            try:
                identity_url = response.identity_url
            except AttributeError:
                identity_url = uid

        if identity_url:
            # raw sql as this is no longer an installed app
            user_ids = sql_query.dbquery('SELECT user_id '
                                         'FROM django_openid_auth_useropenid '
                                         'WHERE claimed_id = %s',
                                         (identity_url,))

            if len(user_ids) == 1:
                return {'user': User.objects.get(id=user_ids[0]['user_id'])}

As best I can tell from reading Google's migration guide, I need to add an openid.realm to the request, which I've done as follows in settings.py:

SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS \
    = {'openid.realm': 'http://example.com/'}

But this doesn't seem to be returning the open_id value in the response passed into my pipeline function.

I seem to be stuck on Step 3:

Some more details:

Try as I might, I can't find any examples of people doing a similar migration with python-social-auth, but it must be happening to lots of people.

Any ideas?

Upvotes: 4

Views: 970

Answers (1)

mastak
mastak

Reputation: 353

Solution works for python social auth 0.1.26

In new versions (0.2.*) of python social auth, there is GoogleOpenIdConnect, but it does not work fine (at least I did not succeed). And my project has some legacy, so I can't use new version of social.

I wrote custom GoogleOpenIdConnect backend:

import datetime
from calendar import timegm

from jwt import InvalidTokenError, decode as jwt_decode

from social.backends.google import GoogleOAuth2
from social.exceptions import AuthTokenError


class GoogleOpenIdConnect(GoogleOAuth2):
    name = 'google-openidconnect'

    ACCESS_TOKEN_URL = 'https://www.googleapis.com/oauth2/v3/token'
    DEFAULT_SCOPE = ['openid']
    EXTRA_DATA = ['id_token', 'refresh_token', ('sub', 'id')]
    ID_TOKEN_ISSUER = "accounts.google.com"

    def user_data(self, access_token, *args, **kwargs):
        return self.get_json(
            'https://www.googleapis.com/plus/v1/people/me/openIdConnect',
            params={'access_token': access_token, 'alt': 'json'}
        )

    def get_user_id(self, details, response):
        return response['sub']

    def request_access_token(self, *args, **kwargs):
        """
        Retrieve the access token. Also, validate the id_token and
        store it (temporarily).
        """
        response = self.get_json(*args, **kwargs)
        response['id_token_parsed'] = self.validate_and_return_id_token(response['id_token'])
        return response

    def validate_and_return_id_token(self, id_token):
        """
        Validates the id_token according to the steps at
        http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation.
        """
        try:
            id_token = jwt_decode(id_token, verify=False)
        except InvalidTokenError as err:
            raise AuthTokenError(self, err)

        # Verify the token was issued in the last 10 minutes
        utc_timestamp = timegm(datetime.datetime.utcnow().utctimetuple())
        if id_token['iat'] < (utc_timestamp - 600):
            raise AuthTokenError(self, 'Incorrect id_token: iat')

        return id_token

Notes:

  1. get_user_id – An identifier for the user, unique among all Google accounts and never reused.
  2. request_access_token – there is I add id_token_parsed to response, and it will be used in pipeline.
  3. validate_and_return_id_token – validate of jwt is disabled, because in google developers console I have registered Client ID as web application so, I have no certificates for validate this data.

Then I created pipelines:

def social_user_google_backwards(strategy, uid, *args, **kwargs):
    """
    Provide find user that was connect with google openID, but is logging with google oauth2
    """
    result = social_user(strategy, uid, *args, **kwargs)
    provider = strategy.backend.name
    user = result.get('user')

    if provider != 'google-openidconnect' or user is not None:
        return result

    openid_id = kwargs.get('response', {}).get('id_token_parsed', {}).get('openid_id')
    if openid_id is None:
        return result

    social = _get_google_openid(strategy, openid_id)
    if social is not None:
        result.update({
            'user': social.user,
            'is_new': social.user is None,
            'google_openid_social': social
        })
    return result


def _get_google_openid(strategy, openid_id):
    social = strategy.storage.user.get_social_auth('openid', openid_id)
    if social:
        return social
    return None


def associate_user(strategy, uid, user=None, social=None, *args, **kwargs):
    result = social_associate_user(strategy, uid, user, social, *args, **kwargs)
    google_openid_social = kwargs.pop('google_openid_social', None)
    if google_openid_social is not None:
        google_openid_social.delete()
    return result

And changed my SOCIAL_AUTH_PIPELINE and AUTHENTICATION_BACKENDS settings:

AUTHENTICATION_BACKENDS = (
    ...
    #'social.backends.open_id.OpenIdAuth' remove it
    'social_extension.backends.google.GoogleOpenIdConnect',  # add it
    ...
)

and

SOCIAL_AUTH_PIPELINE = (
    'social.pipeline.social_auth.social_details',
    'social.pipeline.social_auth.social_uid',
    'social.pipeline.social_auth.auth_allowed',
    # 'social.pipeline.social_auth.social_user',  remove it
    'social_extension.pipeline.social_user_google_backwards',  # add it
    'social.pipeline.user.get_username',
    ...
    # 'social.pipeline.social_auth.associate_user', remove it
    'social_extension.pipeline.associate_user',  # add it
    'social.pipeline.social_auth.load_extra_data',
    ...
)

Upvotes: 1

Related Questions