pythonclassflaskrauthsocial-authentication

Flask Social Authentication Class Issue


I am working off of a Miguel Grinberg tutorial on social authentication.

On the homepage template I have this code, and I removed the twitter portion from the tutorial:

    <h2>I don't know you!</h2>
    <p><a href="{{ url_for('oauth_authorize', provider='facebook') }}">Login with Facebook</a></p>
{% endif %}

So when you click that link, you pass Facebook as the provider through this view function:

@app.route('/authorize/<provider>')
def oauth_authorize(provider):
    if not current_user.is_anonymous():
        return redirect(url_for('index'))
    oauth = OAuthSignIn.get_provider(provider)
    return oauth.authorize()

Now, in a different file, oauth.py, I have the following and my issue is this. I keep getting an error when I click the Facebook link UNLESS the TwitterSignIn class is removed. I guess I am curious as to why the TwitterSignIn class needs to be removed for this to work, because no data is being passed to it, right? Even if Facebook wasn't the only option, why would clicking the Facebook sign-in link pass any data to the TwitterSignIn class?

from rauth import OAuth1Service, OAuth2Service
from flask import current_app, url_for, request, redirect, session


class OAuthSignIn(object):
    providers = None

    def __init__(self, provider_name):
        self.provider_name = provider_name
        credentials = current_app.config['OAUTH_CREDENTIALS'][provider_name]
        self.consumer_id = credentials['id']
        self.consumer_secret = credentials['secret']

    def authorize(self):
        pass

    def callback(self):
        pass

    def get_callback_url(self):
        return url_for('oauth_callback', provider=self.provider_name,
                       _external=True)

    @classmethod
    def get_provider(self, provider_name):
        if self.providers is None:
            self.providers = {}
            for provider_class in self.__subclasses__():
                provider = provider_class()
                self.providers[provider.provider_name] = provider
        return self.providers[provider_name]


class FacebookSignIn(OAuthSignIn):
    def __init__(self):
        super(FacebookSignIn, self).__init__('facebook')
        self.service = OAuth2Service(
            name='facebook',
            client_id=self.consumer_id,
            client_secret=self.consumer_secret,
            authorize_url='https://graph.facebook.com/oauth/authorize',
            access_token_url='https://graph.facebook.com/oauth/access_token',
            base_url='https://graph.facebook.com/'
        )

    def authorize(self):
        return redirect(self.service.get_authorize_url(
            scope='email',
            response_type='code',
            redirect_uri=self.get_callback_url())
        )

    def callback(self):
        if 'code' not in request.args:
            return None, None, None
        oauth_session = self.service.get_auth_session(
            data={'code': request.args['code'],
                  'grant_type': 'authorization_code',
                  'redirect_uri': self.get_callback_url()}
        )
        me = oauth_session.get('me').json()
        return (
            'facebook$' + me['id'],
            me.get('email').split('@')[0],  # Facebook does not provide
                                            # username, so the email's user
                                            # is used instead
            me.get('email')
        )


class TwitterSignIn(OAuthSignIn):
    def __init__(self):
        super(TwitterSignIn, self).__init__('twitter')
        self.service = OAuth1Service(
            name='twitter',
            consumer_key=self.consumer_id,
            consumer_secret=self.consumer_secret,
            request_token_url='https://api.twitter.com/oauth/request_token',
            authorize_url='https://api.twitter.com/oauth/authorize',
            access_token_url='https://api.twitter.com/oauth/access_token',
            base_url='https://api.twitter.com/1.1/'
        )

    def authorize(self):
        request_token = self.service.get_request_token(
            params={'oauth_callback': self.get_callback_url()}
        )
        session['request_token'] = request_token
        return redirect(self.service.get_authorize_url(request_token[0]))

    def callback(self):
        request_token = session.pop('request_token')
        if 'oauth_verifier' not in request.args:
            return None, None, None
        oauth_session = self.service.get_auth_session(
            request_token[0],
            request_token[1],
            data={'oauth_verifier': request.args['oauth_verifier']}
        )
        me = oauth_session.get('account/verify_credentials.json').json()
        social_id = 'twitter$' + str(me.get('id'))
        username = me.get('screen_name')
        return social_id, username, None   # Twitter does not provide email

Some additional information-

The specific error is this:

File "/Users/metersky/code/mylastapt/app/oauth.py", line 29, in get_provider
provider = provider_class()
File "/Users/metersky/code/mylastapt/app/oauth.py", line 73, in __init__
super(TwitterSignIn, self).__init__('twitter')
File "/Users/metersky/code/mylastapt/app/oauth.py", line 10, in __init__
credentials = current_app.config['OAUTH_CREDENTIALS'][provider_name]
KeyError: 'twitter'

And this is where the I think the issue might be happening:

app.config['OAUTH_CREDENTIALS'] = {
    'facebook': {
        'id': 'XXX',
        'secret': 'XXXX'
    }
}

Solution

  • The problem is in OAuthSignIn.get_provider.

    @classmethod
    def get_provider(self, provider_name):
        if self.providers is None:
            self.providers = {}
            for provider_class in self.__subclasses__():
                provider = provider_class()
                self.providers[provider.provider_name] = provider
        return self.providers[provider_name]
    

    The first time you call it from within your view

    oauth = OAuthSignIn.get_provider(provider)
    

    the method caches the providers you've defined. It does this by checking for all of OAuthSignIn's subclasses.

    for provider_class in self.__subclasses__():
    

    When you include TwitterSignIn, it will be included as a subclass. You'll then instantiate an instance of the class

    provider = provider_class()
    

    Inside OAuthSignIn.__init__, you load the provider's settings with current_app.config['OAUTH_CREDENTIALS'][provider_name]. Since Twitter isn't included, you get the KeyError.

    If you don't want to support Twitter, just remove the class. If you want to protect your application a little more so that providers can be removed from your settings without updating code, you'll need to check for the exception. You could do the check inside OAuthSignIn.__init__, but there probably isn't much value to including an unsupported provider in OAuthSignIn.providers. You're better off putting the check in OAuthSignIn.get_provider.

    @classmethod
    def get_provider(cls, provider_name):
        if cls.providers is None:
            cls.providers = {}
            for provider_class in cls.__subclassess__():
                try:
                    provider = provider_class()
                except KeyError:
                    pass  # unsupported provider
                else:
                    cls.providers[provider.provider_name] = provider
        return cls.providers[provider_name]