OAuth Authentication with Flask

Posted by
on under

Many web sites offer users the option to use a streamlined single-click registration and login built on third party authentication services, typically run by the big social networks. In this article I want to give you an introduction to the OAuth protocol, which is one of the most used third-party authentication mechanism. I will also show you a complete Flask application that implements "Sign In with Facebook" and "Sign In with Twitter" functionality. With these two implementations as a guide you should find it easy to add any other OAuth providers you may need.

Brief Introduction to OAuth

The best way to explain OAuth is by going over the list of events that occur during the sign in process:

  1. The user navigates to the application's home page, say http://www.example.com, and clicks the "Sign in with Facebook" button, which links to an application route, for example http://www.example.com/authorize/facebook.
  2. The server receives the request and responds with a redirect to Facebook's OAuth authorization URL. All OAuth providers must document a URL to redirect the user to.
  3. The user is now prompted to login to Facebook (if not logged in already). Then a request to share information is presented, where the user needs to give Facebook permission to share the requested information with the originating application. This is all done at Facebook's website and is a private transaction between Facebook and the user, the application does not participate.
  4. Once the user accepts the request to share information, Facebook redirects back to the application at a pre-configured callback URL, for example http://www.example.com/callback/facebook. The query string of the redirect URL includes an authorization code that the application can use to access the Facebook API on behalf of the user.
  5. The application uses the Facebook API to obtain user information. Of particular interest is a unique identifier for the user, which can be used to register the user in the application's database, and once the user is registered to perform a login.

You can see above that the exchange between the application and the third party service is not trivial, but for the user it is extremely simple, since all the user needs to do is log in to the third party site and give permission to share information with the application.

There are two versions of the OAuth protocol currently in use, both following the overall process described above but with some implementation differences. OAuth 1.0a, used by Twitter, is the most complex of the two. OAuth 2, used by Facebook, is a backwards incompatible revision of the protocol that eliminates much of the complexity of version 1.0a by relying on secure HTTP for encryption.

Registration with OAuth Providers

Before an application can use a third party OAuth provider it needs to register with it. For Facebook and Twitter this is done on their respective developer sites with the creation of an "app" that represents the application for users of these sites.

To create a Facebook app you can visit https://developer.facebook.com. Select "Add a New App" from the Apps dropdown, and make the type "WWW/Website". Then enter a name and category for your app. Once the application is created, go to the "App Configuration" section and set the URL of the application, which in the case of you running it on your own computer will be http://localhost:5000.

For Twitter the location is https://apps.twitter.com. You will be asked to provide the app name, description, website and callback URL. For the last two you can enter placeholders, for example http://example.com and http://example.com/callback/twitter.

Note: If you want to use other OAuth providers you will need to find the appropriate procedure to register an application in their developer documentation.

The newly created app will be assigned two codes, usually called "id" and "secret". These identify the application that is making the authentication request, and are passed in the query string of the redirect URL to the provider site, in step 2 above.

OAuth Authentication Example

In the following sections I'm going to describe a relatively simple Flask application that implements Facebook and Twitter authentication.

I'm only going to show you the important parts of the application in the article, but the complete application is available on this GitHub repository: https://github.com/miguelgrinberg/flask-oauth-example. At the end of this article I show you the instructions on how to run it.

User Model

The users of the example application are stored in a SQLAlchemy database. The application uses the Flask-SQLAlchemy extension to work with the database, and the Flask-Login extension to keep track of logged in users.

from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin

db = SQLAlchemy(app)
lm = LoginManager(app)

class User(UserMixin, db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    social_id = db.Column(db.String(64), nullable=False, unique=True)
    nickname = db.Column(db.String(64), nullable=False)
    email = db.Column(db.String(64), nullable=True)

@lm.user_loader
def load_user(id):
    return User.query.get(int(id))

The database has a single table for the users. In addition to the id that is the primary key, the users table contains three columns:

  • social_id: a string that defines a unique identifier from the third party authentication service used to login.
  • nickname: a nickname for the user. Must be defined for all users, and does not need to be unique.
  • email: the email address of the user. This column is optional.

The model also inherits from UserMixin from Flask-Login, as that gives it the methods required by that extension. The user_loader callback function, also required by Flask-Login, loads a user by its primary key.

OAuth Implementation

There are several OAuth client packages for Python. For this example I have decided to use Rauth. But even when using an OAuth package, there are many aspects of the authentication against OAuth service providers that are left up to each provider to implement, which makes the task harder.

First of all, there are the two versions of the OAuth protocol, both widely used. But even among different providers using the same OAuth version, there are many details that are not part of the specification and need to be done according to the provider's own documentation.

For this reason I have decided to implement an abstraction layer on top of Rauth, so that the Flask application can be written generically. Below you can see a simple base class under which the provider specific implementations will be written:

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):
    pass

class TwitterSignIn(OAuthSignIn):
    pass

The OAuthSignIn base class defines the structure that the subclasses that implement each provider must follow. The constructor initializes the provider's name, and the application id and secret assigned by it, which are obtained from the configuration. Below you can see how the example application is configured (of course you will need to replace these codes with your own when you try this application):

app.config['OAUTH_CREDENTIALS'] = {
    'facebook': {
        'id': '470154729788964',
        'secret': '010cc08bd4f51e34f3f3e684fbdea8a7'
    },
    'twitter': {
        'id': '3RzWQclolxWZIMq5LJqzRZPTl',
        'secret': 'm9TEd58DSEtRrZHpz2EjrV9AhsBRxKMo8m3kuIZj3zLwzwIimt'
    }
}

At a high level there are two significant events supported by this class that are common to all OAuth providers:

  1. Initiation of the authentication process. For this the application needs to redirect to the provider's web site to let the user authenticate there. This is represented by the authorize() method.
  2. Once the authentication is completed the provider redirects back to the application. This is handled in the callback() method. Since the provider does not have direct access to the internal methods of the application, it will be redirecting to a URL that will call it. The URL that the provider needs to redirect to is returned by the get_callback_url() method and is built using the provider name, so that each provider gets its own dedicated route.

The get_provider() class method is used to lookup the correct OAuthSignIn instance given a provider name. This method uses introspection to find all the OAuthSignIn subclasses, and then saves an instance of each in a dictionary.

OAuth Authentication with Rauth

Rauth represent OAuth providers with an object of class OAuth1Service or OAuth2Service, depending on the version of the protocol that it uses. I create an object of this class in each provider's OAuthSignIn subclass. The implementations for Facebook and Twitter are shown below:

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/'
        )

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/'
        )

For Facebook, a provider that implements OAuth 2, the OAuth2Service class is used. The service object is initialized with the name of the service and several OAuth specific arguments. The client_id and client_secret arguments are the ones assigned to the application in Facebook's developer site. The authorize_url and access_token_url are URLs defined by Facebook for applications to connect to during the authentication process. Finally, the base_url sets the prefix URL for any Facebook API calls once the authentication is complete.

Twitter implements OAuth 1.0a, so class OAuth1Service is used instead. In OAuth 1.0a the id and secret codes are called consumer_key and consumer_secret, but are otherwise identical in functionality to the OAuth 2 counterparts. The OAuth 1 protocol requires providers to expose three URLs instead of two, there is an additional one called request_token_url. The name and base_url arguments are identical to the ones used in OAuth 2 services. I should note that Twitter offers two options for the authorize_url parameter. The URL shown above, https://api.twitter.com/oauth/authorize, is the most secure, as it will present the user with a screen in which they need to give permission to the app to access Twitter every time. Changing that URL to https://api.twitter.com/oauth/authenticate will make Twitter ask for permission just the first time, and then will silently allow access for as long as the user is logged in to Twitter.

Note that there is no standardization for the OAuth entry point URLs, OAuth providers define these as they like. To add a new OAuth provider you will need to obtain these URLs from the provider's documentation.

OAuth Authorization Phase

When the user clicks the "Login in with ..." link to initiate an OAuth authentication the following application route is invoked:

@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()

This route first ensures that the user is not logged in, and then simply obtains the OAuthSignIn subclass appropriate for the given provider, and invokes its authorize() method to initiate the process. The authorize() implementation for Facebook and Twitter is shown below:

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

class TwitterSignIn(OAuthSignIn):
    # ...
    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]))

For OAuth 2 providers like Facebook the implementation simply issues a redirect to a URL generated by rauth's service object. The scope is provider specific, in this case I am asking that I want Facebook to provide the user's email. The response_type=code argument tells the OAuth provider that the application is a web application (there are other possible values for different authentication workflows). Finally, the redirect_uri argument is set to the application route that the provider needs to invoke after it completes the authentication.

OAuth 1.0a providers use a slightly more complicated process that involves obtaining a request token from the provider, which is a list of two items, the first of which is then used as an argument in the redirect. The entire request token is saved to the user session because it will be needed again in the callback.

OAuth Callback Phase

The OAuth provider redirects back to the application after the user authenticates and gives permission to share information. The route that handles this callback is shown below:

@app.route('/callback/<provider>')
def oauth_callback(provider):
    if not current_user.is_anonymous():
        return redirect(url_for('index'))
    oauth = OAuthSignIn.get_provider(provider)
    social_id, username, email = oauth.callback()
    if social_id is None:
        flash('Authentication failed.')
        return redirect(url_for('index'))
    user = User.query.filter_by(social_id=social_id).first()
    if not user:
        user = User(social_id=social_id, nickname=username, email=email)
        db.session.add(user)
        db.session.commit()
    login_user(user, True)
    return redirect(url_for('index'))

This route instantiates the OAuthSignIn provider class and invokes its callback() method. This method has the function to complete the authentication with the provider and obtain the user information. The return value is a tuple with three values, a unique id (called social_id to differentiate it from the id primary key), the user's nickname and the user's email. The id and the nickname are mandatory, but in this example application I made the email optional, since Twitter never shares that information with applications.

The user is searched in the database by the social_id field, and if not found, a new user is added to the database with the information obtained from the provider, effectively registering new users automatically. The user is then logged with the login_user() function of Flask-Login, and finally redirected to the home page.

The implementation of the callback() method for the Facebook and Twitter OAuth providers is shown below:

class FacebookSignIn(OAuthSignIn):
    # ...
    def callback(self):
        def decode_json(payload):
            return json.loads(payload.decode('utf-8'))

        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()},
            decoder=decode_json
        )
        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 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

In the callback() method the provider passes a verification token that the application can use to contact the provider's APIs. In the case of OAuth 2 this comes as a code argument, while for OAuth 1.0a it is oauth_verifier, both given in the query string. This code is used to obtain an oauth_session with the provider from the service object from rauth.

Note that in recent versions of the Facebook API, the session token is returned in JSON format. The default format that rauth expects for this token is to be provided in the query string of the request instead. For that reason, it is necessary to add a decoder argument that points to a function that decodes the JSON payload. In Python 2, it is sufficient to pass json.loads, but in Python 3 we need an additional step because the payload is returned as bytes, which the json parser does not understand. The conversion from bytes to string is done in the decode_json inner function.

The oauth_session object can be used to make API requests to the provider. Here it is used to request user information, which has to be done in a provider specific way. Facebook exposes a user id and an email, but does not give out usernames, so the username for the application is created from the left portion of the email address. Twitter provides the id and the username, but does not share emails, so the email is returned as None. The data obtained from the provider is finally returned as a three element tuple to the view function.

Note how in both cases the id value from the provider is prepended with "facebook$" or "twitter$" before it is returned, to make it unique across all providers. Since this is what the application will store as social_id in the database, it is necessary to do this to ensure that two providers that assign the same id to two different users do not collide in the application's database.

Conclusion

As I mentioned above, the example application allows any user to register and login with either a Facebook or a Twitter account. The application demonstrates how to register and login users without them having to enter any information, all they need to do is to login with the provider and authorize the sharing of information.

If you want to try this example, you need to follow some preparatory steps:

  • Clone or download the project's repository: https://github.com/miguelgrinberg/flask-oauth-example
  • Create a virtual environment and install the packages in the requirements.txt file (you can use Python 2.7 or 3.4).
  • Register "apps" with Facebook and Twitter, as described above.
  • Edit app.py with the id and secret codes of your Facebook and Twitter apps.

After you complete these instructions, you can run the application with python app.py, and then visit http://localhost:5000 in your browser.

I hope this article is useful in demystifying OAuth a little bit. If you have any questions feel free to write them below.

Miguel

Become a Patron!

Hello, and thank you for visiting my blog! If you enjoyed this article, please consider supporting my work on this blog on Patreon!

173 comments
  • #26 Slash said

    Hi Miguel, thanks for the article. How should one go about integrating this with flasky. Also would it be easy to have this along with the authentication system shown in flasky?

  • #27 Miguel Grinberg said

    @Slash: sure, you can offer the two authentication options. You will need to expand the user model to hold the data used by both methods, then add the "login with ..." buttons to the login form.

  • #28 Patrice Bertrand said

    Hello and thanks Miguel. I found your example implementation beautiful, but needed something compatible with GAE, and as you mention above, flask-oauth is not very lively lately, while oauthlib and flask-oauthlib are. So I tried to adapt your general scheme to be used with flask-oauthlib. It works great too. For those who are interested, it's here: https://github.com/PatriceBertrand/flask-oauthlib/tree/master/example/contrib/flask-social-multi Any comments welcome, since I am relatively new to Python and Flask.

  • #29 Duc Nguyen said

    Hi,
    I have this problem " type object 'OAuthSignIn' has no attribute 'providers' " when calling the function get_provider

  • #30 Miguel Grinberg said

    @Duc: sorry about that, the code printed in this article was missing a line that declares the "providers" attribute. I have corrected it now.

  • #31 SurveillanceTips said

    Thanks Miguel for your lectures. I've subscribed to Safari Online Books because I want to watch your other videos and books. Kindly check the Github code for this example. Running the cloned version says your Facebook App ... "Application has been deleted." The Twitter link also throws decoder failure to authenticate.

    Thanks

  • #32 Miguel Grinberg said

    @SurveillanceTips: you have to create your own Facebook/Twitter apps and enter your own id and secret codes into the app.

  • #33 Gary said

    Miguel,
    Love your blog, love this article! You have to be the best source to understand Flask! Thanks much for taking the time to share. I was looking for a donate button but happily bought your ebook instead.
    Gary

  • #34 Cédric said

    Hi Miguel,

    first of all, I want to thank you for your amazing tutorials! Wrapping my head around everything still takes some time, but it has been made much more enjoyable with your tutorials.

    I was wondering if you could quickly explain how to add the person's profile picture of the user?
    Also, if you check out the procedures on facebook, they are quite different and include a lot of JS. Is there a reason you do it differently?

    Thanks a lot again!

  • #35 Miguel Grinberg said

    @Cédric: OAuth does not define a uniform format to access personal information. You need to see how the provider that you are talking to exposes the profile pictures and use their API to get it. This is similar to how I obtain user information in the callback() methods above, each provider exposes its own API for this.

    Regarding the Facebook SDK, that is a client-side framework, and it is obviously specific to Facebook. The method I show here is more generic, as it works with any OAuth provider. Facebook also documents the method I outline above: https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/v2.3

  • #36 Jordan said

    As everyone has said, your tutorials are great introductions to using Flask.

    I am trying to understand the OAuth 2.0 process by using your tutorial and changing it to a Google+ Login. I keep getting a decoder error: KeyError: 'Decoder failed to handle access_token with data as returned by provider. A different decoder may be needed.' Now I am struggling to identify where the decoder is identified in the code. Can you point me in the right direction? Thanks!

  • #37 Miguel Grinberg said

    @Jordan: see the docs on the get_access_token method here: https://rauth.readthedocs.org/en/latest/api/#rauth.OAuth2Service.get_access_token. I think adding "decoder=json.loads" as an argument inside that call will address your problem.

  • #38 Jordan said

    Thanks for the link, reading the documentation is helping me get a grasp on the entire oauth flow. I think I understand what is happening, but I am failing to put the final pieces together. Let me try to explain where I am...

    With get_access_token(decoder=json.loads, data=...) I am getting the access token returned as a string. How do I use that string to build a session object? In your code you have it using the self.service.get_auth_session(...) to save the session object as oauth_session. But I don't see anywhere in the documentation that allows me to pass in the token as an argument. Does that make any sense?

  • #39 Miguel Grinberg said

    Jordan: Here is where rauth obtains the session from the token: https://github.com/litl/rauth/blob/master/rauth/service.py#L360

  • #40 Gary said

    As with your other posts, this post was extremely useful in moving my flask application from OpenID authentication to OAuth, as originally detailed in your mega-tutorial.

    Once the OAuth sign-in flow begins I lose the context of the originally requested page (http://...?next=requested_page) and I redirect the user to a default resource after sign-in.

    It is clear that I cannot and should not be passing this context to my OAuth provider. Would the best approach be to store this context in the users session?

    Thanks,
    Gary

  • #41 Miguel Grinberg said

    @Gary: I think you have two options. One is writing the "next" argument to the user session, as you suggest. The other, that may or may not work depending on the provider, is to include the next argument in the callback URL that you pass to the OAuth provider. This last option may not work with some providers, if they expect the callback to be given in advance in the app configuration. So user session is probably safer.

  • #42 SantyXDz said

    Hi Miguel,
    I followed all steps of this article but even when in localhost all works perfectly I had problems when I deployed to heroku. In heroku the urls mark 5XX error, according with that I found it's because Python 2.7.9 has got problems with rauth, request and urllib3 libraries.... I don't know how to fix that and heroku doesn't let me downgrade to 2.7.7 or lower.
    The errors that marks are "init() got an unexpected keyword argument 'server_hostname'" and "init() got an unexpected keyword argument '_context'"
    Thanks for all.

  • #43 Miguel Grinberg said

    @SantyXDz: do you have any links that have information on the problems with Python 2.7.9? Also can you show you the complete stack trace of the error?

  • #44 Manu said

    Hi Miguel,

    Thanks for this post that gives a second youth to the Part V of the Flask Mega Tutorial. It actually comes very handy since Google has stopped supporting OpenId, which removed the biggest OpenId provider supported.

    Everything has been working fine for me in this OAuth tutorial, it even works without breaking the OpenId implementation.

    Side Question: do you use any web-admin to browse your DB ?

    My two cents:

    1/
    The SQLAlchemy-migrate has complained about having social_id as nullable=False:

    sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) Cannot add a NOT NULL column with default value NULL [SQL: u'\nALTER TABLE user ADD social_id VARCHAR(64) NOT NULL']
    
    (I have just made it nullable)
    

    2/
    Facebook has released another way to login-with. It makes use of a JS SDK and is much simpler. Another tutorial for you ? You could then replace FB in this one with Google, LinkedIn, ...

    Thanks!
    Emmanuel

  • #45 Miguel Grinberg said

    Side Question: do you use any web-admin to browse your DB ?

    I do not use a web admin interface, it's easy enough for me to type SQL at the command prompt.

    The SQLAlchemy-migrate has complained about having social_id as nullable=False

    Sure, that depends on the application. If your application accepts other forms of login, then the social_id needs to allow null values. For the example in this article, however, the social login is the only allowed option, so nullable=False is correct.

    Facebook has released another way to login-with

    The JS framework is not new, it's been around for a long time. If you only need to support FB then it is a viable option, but if you need to support other OAuth providers it makes more sense to use a single implementation that is provider-agnostic, as I present in this article.

  • #46 Nicholas K said

    Hello Miguel, before I reveal my problem I just want to say that I'm a big fan of your work. You have taught me a lot over the past year. A big thank you from me.

    After cloning your git repo and changing the files only to add provider secret and id, I am unable to login via facebook. Here are the details:

    Traceback (most recent call last):
    ...
    ...
    File "/home/nick/dev/learn/flask-oauth-example/oauth.py", line 65, in callback
    me.get('email').split('@')[0], # Facebook does not provide
    AttributeError: 'NoneType' object has no attribute 'split'

    on line 64 i have insert:
    print(me)
    which returns:
    {u'name': u'My Name', u'id': u'12345678901234567'}

    As you can see, me['email'] does not exist. I have double checked my facebook permissions and my app is allowed to collect email addresses.

  • #47 Miguel Grinberg said

    @Nicholas: I did not consider this, but I wonder if it is possible to create a Facebook account without an email address. Are you sure this test account you are using has an email in the profile?

  • #48 Nicholas K said

    @Miguel, yes facebook does have my email registered and I do give the app permission to access it during that step. I'll go over everything (double check permissions, code, etc...) again when I wake up tomorrow.

    If me = oauth_session.get('me').json() is indeed the only way to pull the data then I'm not sure what else could be wrong.

  • #49 Nicholas K said

    @Miguel: I made a dummy facebook account and have the same problem with that account.

    Here is my apps permissions: http://i.imgur.com/m4cCSsX.png

    Everything is correct. I'm not sure what else I can do.

  • #50 Miguel Grinberg said

    @Nicholas: It's very odd. Unless something changed in the Facebook API recently, or your application is for some reason prevented from obtaining emails. I see many similar reports, so I guess FB sometimes does that. Some people use <username>@facebook.com as email when a real email isn't provided.

Leave a Comment