devxlogo

Authenticate RESTful APIs with an OAuth Provider

Authenticate RESTful APIs with an OAuth Provider

A few months ago I wrote an article about developing RESTful web services using Python and Flask. In that article, I explained and demonstrated the mechanics of implementing a web service that exposes a REST API to for setting and completing goals. There was a concept of a user, but you couldn’t login and everybody could see the goals of all users. This is fine for a demo application, but in the real world you want to provide privacy to your users, as well as know which user is requesting a particular resource to provide a customized experience. In order to do that you need to authenticate the user, which means you need to verify that the active user is indeed who he or she claims to be and map him/her to the user object in your application that is authorized to access some subset of all resources.

There are many ways to do that, such as HTTP basic authentication, JWT access tokens, hard coded secrets, pass user credentials in every request, etc. Each has its own pros and cons. I’ll jump right ahead to the modern alternative of OAuth2-based authentication. Using OAuth2 allows you to authenticate users without managing their credentials. Users who log in to your application or code can securely use web services by relying on trusted identity providers such as Google, Facebook, Twitter and more. What’s so great about it? As a developer you don’t have to deal with messy area of registration, storing emails and passwords, providing password reset workflows. As a user you don’t need to create yet another set of credentials you’ll forget or write insecurely on a post-it note (or even worse use your bank account password to login to every web site).

The problem with OAuth2 is that it is extremely complicated due to issues with standardization. First, of all it is inherently complicated and on top of it many details were left for the implementers to decide. The end result is that clients need to perform various interactions with different identity providers. Luckily, libraries are available to ease the burden, but it is still not completely trivial and it takes some work to get it right. In the remainder of this article, I’ll show you how to use GitHub as an identify provider and allow user that have a GitHub account get an access token that will allow them to authenticate themselves to the over-achiever application.

Here we go. I chose the flask-oauthlib extension built on top of the excellent oauthlib (https://github.com/idan/oauthlib/) and requests-oauthlib (https://requests-oauthlib.readthedocs.org).

Preliminaries

In order to use GitHub as an identity provider for your application you need to register a developer application here: https://github.com/settings/developers. You’ll need a couple of items later.

The full source code is available here: https://github.com/the-gigi/over-achiever

First add flask-oauthlib to your requirements.txt file:

Flask==0.10.1Flask-RESTful==0.3.4Flask-SQLAlchemy==2.1Flask-OAuthlib==0.9.2mock==1.3.0

The Core GitHub OAuth Setup

To set up everything you need to wrap your app with the OAuth object. Then create a “github” object by calling the remote_app() method and passing sme arguments. You need to change the consumer_key and consumer_secret and replace them with your values. You may use mine if you wish (e.g. for playing with it), but you’ll see that when you go through the authentication process with github it will tell you that Gigi’s Over Achiever app wants access to your awesome stuff. This is not the experience you want your users to have. Finally, you need to set the _tokengetter attribute. There is a decorator that is supposedly more user-friendly, but it doesn’t work with the way I initialize the Flask app. Its job is to tell OauthLib where to find the token when a request comes in.

from flask.ext.oauthlib.client import OAuth...    oauth = OAuth(app)    github = oauth.remote_app(        'github',        consumer_key='507e57ab372adeb8051b',        consumer_secret='08a7dbaa06ac16daab00fac53724ee742c8081c5',        request_token_params={'scope': 'user:email'},        base_url='https://api.github.com/',        request_token_url=None,        access_token_method='POST',        access_token_url='https://github.com/login/oauth/access_token',        authorize_url='https://github.com/login/oauth/authorize'    )    # set the token getter for the auth client    github._tokengetter = lambda: session.get('github_token')

Once this part is done you will have an initialized github object you can pass around and can authrnticate users for you.

The Authentication Workflow Methods

Before we can do that let’s add a few required methods to the mix. The login endpoint returns a github user info that includes an access_token. You’ll pass this access token as the “Access-Token” header to all authenticated endpoints.

@app.route('/login')def login():    return app.github.authorize(callback=url_for('authorized',                                                 _external=True))

The /login/authorized endpoint is called by the github auth infrastructure according to the OAuth2 workflow. The Flask-OAuthLib extension takes care of adding the tokn to the session

@app.route('/login/authorized')def authorized():    resp = app.github.authorized_response()    if resp is None:        abort(401, message='Access denied!')    user = app.github.get('user')    user.data['access_token'] = session['github_token'][0]    return jsonify(user.data)

The /logout endpoint just pops the token from the session

@app.route('/logout')def logout():    session.pop('github_token', None)    return 'OK'

Using the GitHub Object to Provide User Level Data Access

Note, that any GitHub user can login to Over-Achiever. A user will be created automatically on the first login attempt.

The code now uses a _get_user() method to get information about the current user from GitHub using the access token. If the access token is missing from the headers it abort immediately with a 401 error. If it exists it calls the GitHub object’s get() method to get the user and from the result it extract the email and name. If a user with this email doesn’t exist it creates it on the spot.

def _get_user():    """Get the user object or create it based on the token in the session    If there is no access token abort with 401 message    """    if 'Access-Token' not in request.headers:        abort(401, message='Access Denied!')    token = request.headers['Access-Token']    user_data = github.get('user', token=dict(access_token=token)).data    email = user_data['email']    name = user_data['name']    q = _get_query()    user = q(m.User).filter_by(email=email).scalar()    if not user:        user = m.User(email=email, name=name)        s = _get_session()        s.add(user)    return user

The _get_user() helper function makes it super simple for every other endpoint to provide each user access to their goals only. For example the /v1.0/goals endpoint supports GET, POST and PUT methods. All methods call _get_user() and then filter any access to the goals table (either read or write) by the user object:

class Goal(Resource):    def get(self):        """Get all goals organized by user and in hierarchy        If user doesn't exist create it (with no goals)        """        user = _get_user()        q = _get_query()        result = {user.name: _get_goal_tree(q, user, None, {})}        return result    def post(self):        user = _get_user()        parser = RequestParser()        parser.add_argument('name', type=str, required=True)        parser.add_argument('parent_name', type=str)        parser.add_argument('description', type=str, required=False)        args = parser.parse_args()        # Get a SQL Alchemy query object        q = _get_query()        # Create a new goal        # Find parent goal by name        parent = q(m.Goal).filter_by(name=args.parent_name).scalar()        goal = m.Goal(user=user,                      parent=parent,                      name=args.name,                      description=args.description)        s = _get_session()        s.add(goal)        s.commit()    def put(self):        """Update end time"""        user = _get_user()        parser = RequestParser()        parser.add_argument('name', type=str, required=True)        args = parser.parse_args()        # Get a SQL Alchemy query object        q = _get_query()        goal = q(m.Goal).filter_by(user=user, name=args.name).one()        goal.end = datetime.now()

Conclusion

In conclusion, OAuth-based authentication takes some work to get right, but it’s worth it. Give it a try.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist