2014-11-17T15:40:16Z

OAuth Authentication with Flask

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 my Flask Mega-Tutorial I showed you how to use one of these protocols, called OpenID.

In this article I want to give you an introduction to the OAuth protocol, which these days has replaced OpenID as the preferred 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.ext.sqlalchemy import SQLAlchemy
from flask.ext.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

148 comments

  • #1 azu said 2014-11-18T08:43:44Z

    A word for people trying this tutorial out. Facebook doesn't allow localhost for apps(URL when registering your app with Facebook.) So you'll have to use an application like Local tunnel or ngrok(which i use).

  • #2 Miguel Grinberg said 2014-11-20T00:34:28Z

    @azu: I tested Facebook login from localhost without any issues, so I do not believe you are correct.

  • #3 Liam Jones said 2014-11-23T05:32:52Z

    Hi Miguel, First, thanks for creating this guide. However, running locally after registering apps in Facebook and Twitter as suggested and updating the id/secrets, I'm having a little trouble. With Twitter, it looks like the callback isn't providing a nickname as expected? 'IntegrityError: (IntegrityError) users.nickname may not be NULL'. With Facebook, I'm seeing 'Invalid redirect_uri: Given URL is not permitted by the application configuration.'. I'm still doing my own troubleshooting but just thought you might want to know.

  • #4 Liam Jones said 2014-11-23T06:11:48Z

    I've posted an issue on GitHub with what I believe is the fix for Twitter, and regarding Facebook you may want to link to this StackOverflow article (http://stackoverflow.com/questions/4532721/facebook-development-in-localhost/7493806#7493806) to assist users in getting it running. Thanks again for the guide!

  • #5 Miguel Grinberg said 2014-11-23T08:09:12Z

    @Liam: thanks. There were two completely unrelated issues. The order in which the callback function returns the three values did not match the order that the caller in the route expected, this is what cause your Twitter error. And for Facebook, it is required that you enter a Site URL, and that needs to match the server of the callback URL. I updated the article to reflect this requirement. Thanks again!

  • #6 Manuel Godoy said 2014-11-29T21:14:15Z

    Hi Miguel. Great post as usual. If you were to integrate this in the flask microblog for example, where would you declare your SignIn classes? Thanks!

  • #7 Jake Madison said 2014-11-29T23:25:00Z

    Ah, what great timing! I'm in the middle of putting together a flask app, which is very indebted to your great Flask Mega Tutorial, and was dismayed when putting it on a public site to find that Google is deprecating the Openid 2.0 auth. Can't wait to dig into oauth using this guide as well! Cheers! Jake

  • #8 Miguel Grinberg said 2014-11-30T17:08:28Z

    @Manuel: I put the sign-in helper classes in a separate oauth.py module. I would just take this module as is and put it in the "app" package of microblog. You'll also need to add the configuration of your oauth services in config.py.

  • #9 Manuel Godoy said 2014-12-01T03:24:23Z

    Miguel. Thanks for your answer. I see one downside about using Rauth which is that requests is not supported by GAE.

  • #10 Miguel Grinberg said 2014-12-01T05:50:17Z

    @Manuel: you may be able to use Flask-OAuth instead of Rauth. It's a library that hasn't been maintained lately, but if you are on Python 2.7 it'll probably still work, and it isn't that much different from Rauth.

  • #11 Mike Haldas said 2014-12-10T20:41:17Z

    Miguel, Thanks for putting together this awesome tutorial! As far as I can tell, this app is only using Twitter for authentication. It is not making the 2nd request to the twitter api to convert the request token to an access token (because there was no need for your example). I would like to extend your app to make this second call and get the access tokens so that my app can post to the users twitter timeline. I would add two additional columns to the sqlite DB for the oauth_token and oauth_token_secret. This is recommended and documented in the final step on this twitter api page. https://dev.twitter.com/web/sign-in/implementing Do you have any recommendations on how to implement the second POST to get these values while working within your framework? Thanks! -Mike

  • #12 Miguel Grinberg said 2014-12-11T06:15:18Z

    @Mike: actually, the get_auth_session() function is the one that gets the access token and wraps it into a session. See how I then use this session to get the user's information. See this example: https://github.com/litl/rauth/blob/master/examples/twitter-timeline-cli.py. It's not exactly the same as it is a command line app, but you can see how the session is used there to work with the timeline.

  • #13 Mike Haldas said 2014-12-12T14:49:26Z

    @Miguel: thank you so much for your fast reply! That is just what I needed to point me in the right direction. I modified the sqlite table to store the access_token and access_token_secret after the user authenticates. Now when a user is logged in, the app can tweet on their behalf. Works perfect!

  • #14 ganesshkumar said 2014-12-21T10:31:53Z

    @Miguel. Thanks for the tutorial. When logging in via twitter, twitter kept asking for everytime even though the user had given the permission before. By digging more, I found that the authorize_url for twitter must be 'https://api.twitter.com/oauth/authenticate' now. I am not sure when twitter made the change.

  • #15 Barry Barley said 2014-12-29T20:05:10Z

    Thanks for such a good tutorial. I have a question regarding the "Login" link on the index page. Can we ever use Javascript SDK of Facebook and run FB.login() function when the user clicks the link and fetch Facebook's response from the client? Other than that, it is not clear, how to use GraphAPI when I do not have the access token. Or did i miss something? In short, I would prefer having a popup login window using FB.login() instead of a redirection. In addition, with @login_required for my pages. Can you suggest any solution? Thanks in advance.

  • #16 Miguel Grinberg said 2014-12-30T00:54:56Z

    @Barry: This is a good question. I decided to not use provider specific frameworks partly because I want to have a generic solution that can be extended to other OAuth providers. Also, a good reason to run the authentication in the server (versus FB's javascript solution) is that it is easier to integrate FB's user information with your own database. If you do the authentication in the client then you have to pass user information to your server, and you need to do it securely, so it is an additional complication. I'm sure it can be done, it just seems more complicated. Regarding the access token, see comment #12 above, the token is available if you need it.

  • #17 Nidhin said 2015-01-14T07:40:14Z

    It would be a great help if you could add one more class for Google. Like class GoogleSignIn(OAuthSignIn)

  • #18 Miguel Grinberg said 2015-01-14T15:48:56Z

    @Nidhin: the point is that with these two examples you should be able to add others easily. Google uses OAuth2, so you can use the Facebook class as a model. The docs for Google OAuth are here: https://developers.google.com/accounts/docs/OAuth2.

  • #19 Santosh Joshi said 2015-02-07T14:25:53Z

    Hi Miguel, Your articles are my best guides to learn Flask. This one is also very nice. I have a point wrt to this article. In your get_provider class method in the article you are equating the provider_class.provider_name, which is causing error. I think which is the if condition is invalid as FacebookSignIn is not yet instantiated. (I am not a expert in Python, so just a guess). Also, in the Git Repo you have a different version of the get_provider function, which is working fine. I think updating the same function here and relevant explanation will surely help as I learn by typing along with tutorial. Best Regards, Santosh

  • #20 Miguel Grinberg said 2015-02-07T18:30:37Z

    @Santosh: good catch, looks like I had an outdated (and invalid!) version of that method in the article. Thanks, I have now fixed it.

  • #21 Frank Valcarcel said 2015-02-09T02:03:01Z

    Miguel, great article. Got it working and understand it completely 5 minutes after cloning. Kudos! I want to modify it so to allow the user to authenticate with multiple providers (linking both Facebook, Twitter, etc.) do you think it's best to have multiple 'social_id's in the user object? Maybe a PickleType column to keep each id associated to the name of the provider and maybe a timestamp if necessary. What are your thoughts?

  • #22 Miguel Grinberg said 2015-02-09T16:41:53Z

    @Frank: You should definitely store all the social IDs. My choice of implementation would not be to write a pickled column, because that is not query friendly. I would create another table, let's say we call it "social_ids". This table will have its primary key, a foreign key to the user, and then the social id. The relationship between the users and social_ids tables is a one-to-many, so you can add/remove social ids as necessary.

  • #23 Dmitry said 2015-03-03T21:56:20Z

    I'm always getting great pleasure reading your articles. Thanks a lot for sharing your knowledge. BTW, please, check the github's repo, I've just pushed a little pull request. Thanks again!

  • #24 Nelson said 2015-03-05T01:02:50Z

    As the reader which made a comment before me, I also thank you for sharing your knowledge. I am also very happy to say that your book has been a very helpful resource to me. About the example code: there is a missing flash import in app.py. Your code also assume that an email is supplied, which might not be the case because the user may choose not to give it. Except for the points above the code worked very fine and I am certain that saved me some googling hours. Thank you once again and keep the good job.

  • #25 Miguel Grinberg said 2015-03-05T07:33:48Z

    @Nelson: The code does not assume the email is available. Some OAuth providers (Twitter, for example) do not report user emails. The application needs to be prepared to not receive some data, I believe the example I provided with this article does that.

Leave a Comment