如何使用Python Social Auth将用户从OpenID迁移到Google OAuth2 / OpenID Connect?

时间:2015-02-25 11:01:22

标签: google-openid python-social-auth google-oauth2

Google正在弃用我正在使用的OpenID端点(我认为是v1.0,通过django_openid_auth模块),我需要更新我的应用并迁移我的用户'帐户使用Google OAuth2。

我已将应用更改为使用python-social-auth,并已成功通过social.backends.google.GoogleOAuth2进行身份验证。

我已经编写了一个管道功能来查找旧表中的相关OpenID网址,这适用于我关心的其他后端但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'])}

我最好通过阅读Google's migration guide告诉我,我需要在请求中添加openid.realm,我已在settings.py中执行以下操作:

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

但是这似乎没有返回传递给我的管道函数的open_id中的response值。

我似乎被卡在Step 3

  • 我尝试对后端进行子类化以更改RESPONSE_TYPE以添加id_token但是返回了空响应:

    import social.backends.google
    class CustomGoogleOAuth2(social.backends.google.GoogleOAuth2):
        RESPONSE_TYPE = 'code id_token'
    
  • 我尝试向https://www.googleapis.com/oauth2/v3/token添加类似于this example的额外请求,但我真的不知道如何将它们放在一起并进行调试。

更多细节:

  • Google OpenID用户的旧claimed_id内容如下:https://www.google.com/accounts/o8/id?id=AItOawmAW18QuHDdn6PZzaiI5BWUb84mZzNB9eo
  • 如果这是一个更简单的解决方案,我很乐意使用social.backends.google.GoogleOpenIdConnect或类似的替代后端。虽然它似乎更接近Google文档所说的内容,但当我尝试时,我无法让它工作:
    • 我收到400错误:invalid_request此消息类型不允许参数:nonce
    • 我可以使用noncesocial.backends.google.GoogleOpenIdConnect添加id_token,但我在RESPONSE_TYPE中收到AuthMissingParameter错误作为请求的端点,GET和POST为空。 (已尝试过代码id_token','令牌id_token',' id_token',...)
  • 我不想使用/complete/google-openidconnect/,因为它与我当前的登录表单不能很好地集成。
  • 最糟糕的情况是,我应该可以使用social.backends.google.GooglePlusAuth,但我只有80%的用户拥有电子邮件地址,因此会留下很多人拥有新帐户并需要支持才能手动关联

尽可能地尝试,我无法找到人们使用social.pipeline.social_auth.associate_by_email进行类似迁移的任何示例,但必须发生在很多人身上。

有什么想法吗?

1 个答案:

答案 0 :(得分:1)

解决方案适用于python social auth 0.1.26

在python social auth的新版本(0.2。*)中,有 GoogleOpenIdConnect ,但它不能正常工作(至少我没有成功)。我的项目有一些遗产,所以我不能使用新版本的社交。

我写了自定义 GoogleOpenIdConnect 后端:

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

注意:

  1. get_user_id - 用户的标识符,在所有Google帐户中都是唯一的,永不重复使用。
  2. request_access_token - 我将id_token_parsed添加到响应中,它将在管道中使用。
  3. validate_and_return_id_token - 禁用了jwt的验证,因为在google开发者控制台中我已将客户端ID注册为Web应用程序,因此,我没有用于验证此数据的证书。
  4. 然后我创建了管道:

    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
    

    并更改了我的SOCIAL_AUTH_PIPELINE和AUTHENTICATION_BACKENDS设置:

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

    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',
        ...
    )