Google Login

In the next few days we'll be rolling out Google OAuth authentication so users can login with their Google accounts rather than having to remember yet another password (although everyone should just be using 1Password).

I started off using Flask OAuth but it doesn't properly support Python3 and seems to be largely unmaintained these days. So after getting 80% of the way with Flask OAuth, I then switched to a fork of it: Flask OAuthlib which seems much better maintained and supports Python3.

So firstly I created a generic OAuth class which I hoped would handle any OAuth I needed including Twitter and Facebook if I decided to implement them in the future.

I put this generic class in a file called oauth/model.py. I'm not sure if it's technically a model or not but it seemed to make sense to me to call it that.

from flask import url_for, session
from flask_oauthlib.client import OAuth
from flask.ext.login import login_user

from project import app, db, random_str
from project.models import User

class OAuthSignIn():

    providers = None

    def __init__(self, provider_name):
        self.provider_name = provider_name

    def authorize(self):
        return self.service.authorize(self.get_callback_url())

    def authorized_response(self):
        return self.service.authorized_response()

    def get_callback_url(self):
        return url_for('.authorized', 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]

    @staticmethod
    def get_token(token=None):
        return session.get('oauth_token')

    def get_session_data(self, data):
        pass

    def get_user_data(self):
        return None

So we initalise the class with the name of the provider (Google, Twitter etc). The get_provider method returns the relevant sub class which is a handy way of generically instantiating a subclass. So instead of calling:

oauth = TwitterLogin()

You can call:

oauth = OAuthSignIn('twitter')

And it will initialise the TwitterLogin class.

The authorize and authorized_response methods just return the respective OAuthlib method and self.service is just an OAuth object that is set in the child classes.

The final method that probably requires a little expiation is get_token. The OAuthlib documentation recommends using a decorator to specify a function that will return the required token (which is stored as a session variable in this case). I set it manually in the parent class so there is no need for a decorator which can become tricky when trying to create a generic interface.

We then extend that class with a Google specific one:

class GoogleSignIn(OAuthSignIn):
    def __init__(self):
        super().__init__('google')
        oauth = OAuth()
        self.service = oauth.remote_app(
            'google',
            consumer_key=app.config['GOOGLE_CLIENT_ID'],
            consumer_secret=app.config['GOOGLE_CLIENT_SECRET'],
            request_token_params={
                'scope': 'email'
            },
            base_url='https://www.googleapis.com/oauth2/v1/',
            request_token_url=None,
            access_token_method='POST',
            access_token_url='https://accounts.google.com/o/oauth2/token',
            authorize_url='https://accounts.google.com/o/oauth2/auth',
        )
        self.service.tokengetter(GoogleSignIn.get_token)

    def get_session_data(self, data):
        return (
            data['access_token'],
            ''
        )

    def get_user_data(self):
        access_token = session.get('oauth_token')
        token = 'OAuth ' + access_token[0]
        headers = {b'Authorization': bytes(token.encode('utf-8'))}
        data = self.service.get(
            'https://www.googleapis.com/oauth2/v1/userinfo', None,
            headers=headers)
        return data.data

    def get_user(self):
        data = self.get_user_data()
        user = User.query.filter_by(email=data['email']).first()
        if user is None:
            user = User(data['name'], data['email'], random_str(30), None)
            db.session.add(user)
            db.session.commit()

        login_user(user)
        return url_for('home')

So in the above class we start by running the __init__ method of the parent class then create an OAuth object and set the relevant Google variables for the a Google OAuth request. I also set the tokengetter function which is just a static method (rather than a decorator as mentioned above).

Then we just add a Google-specific session_data. That will be used to populate the oauth_token in the session that is used for subsequent API calls.

I won't go into too much detail about the last two methods. The first one simply sends a request to the Google API for user data. The second either logs the user in or creates a new user depending on if they are registering or logging in. I create a dummy random string for the password as it's a required field but the user will never need to know it.

Finally we have our oauth/view.py file (which is the way I structure my apps but you can do what you like).

from .model import OAuthSignIn
from flask import redirect, url_for, request, flash, Blueprint, session

oauth_blueprint = Blueprint(
    'oauth', __name__,
    template_folder='templates'
)


@oauth_blueprint.route('/login/<provider>')
def login(provider):
    oauth = OAuthSignIn.get_provider(provider)
    return oauth.authorize()


@oauth_blueprint.route('/authorized/<provider>')
def authorized(provider):
    oauth = OAuthSignIn.get_provider(provider)
    resp = oauth.authorized_response()
    next_url = request.args.get('next') or url_for('home')
    if resp is None:
        flash('We weren\'t able to log you in I\'m afraid.')
        return redirect(next_url)

    session['oauth_token'] = oauth.get_session_data(resp)
    oauth.get_user()
    return redirect(next_url)


@oauth_blueprint.route("/logout")
def logout():
    session.pop('oauth_token', None)
    return redirect(url_for('home'))

I think that's all pretty self explanatory. To logout I just remove the session variable but I move this into my users/logout method.

Anyway, hopefully that helps someone.