OAuth2 with Flask, Ember and torii

I love Flask, and I love EmberJS. While the Ember community is awesome and there are many resources to go around and find out how to accomplish various tasks, it seems like not enough focus is put on the backend side of things. It seems like every article just assumes you only have the front-end side and that's it.

In this article I try to give a no-bullshit, working example of setting up OAuth2 authentication using EmberJS as the frontend, and Flask as the backend.

If you can't bear to read it all and are impatient to check out the final result, it's here on Github.

We'll be mainly using:

Bootstrapping the Project

We create the following directory structure:

 - proj/
   |
   +- flask_app/
      |
      +- app.py
      +- static/
   +- frontend/

Where flask_app will hold the backend side, and frontend will contain the Ember app.

Under frontend we bootstrap the ember app (I won't cover basic usage or installation of ember-cli here -- you can find plenty of docs on their website):

$ ember init
$ ember install ember-cli-simple-auth
$ ember install ember-cli-simple-auth-torii

When building our app, we will make sure to save the outputs to the static directory:

$ ember build --watch --output-path=../flask_app/static/

One small change we need to make to ember-cli is setting locationType to hash, since we're only serving the index page through flask:

/* environment.js */
var ENV = {
   ...
   locationType: 'hash',
   ...

As for the Flask part, our app.py file starts like any other minimalistic Flask application:

from flask import Flask, send_from_directory

app = Flask(__name__)


@app.route("/")
def index():
    return send_from_directory(current_app.static_folder, 'index.html')


if __name__ == "__main__":
    app.config.update({
        'DEBUG': True,
        'SECRET_KEY': 'some-secret-key'
    })
    app.run(port=8000)

Now we have a basic app that can serve the built Ember application properly.

Configuring ember-simple-auth

ember-simple-auth provides the mechanism we need to require authentication for a specific route. Let's assume we want to protect our private route and require login for it.

We'll need to add the necessary mixins to our application route and protected route:

/* frontend/app/routes/application.js */
import ApplicationRouteMixin from 'simple-auth/mixins/application-route-mixin';
import Ember from 'ember';

export default Ember.Route.extend(ApplicationRouteMixin, {
});
/* frontend/app/private/route.js */
import Ember from 'ember';
import AuthenticatedRouteMixin from 'simple-auth/mixins/authenticated-route-mixin';

export default Ember.Route.extend(AuthenticatedRouteMixin, {
});

ember-simple-auth will now redirect to a route called login to perform the login process. We need to create it:

<!-- frontend/app/login/template.hbs -->
<button >Sign in with Google</button>

Our googleLogin action will trigger the authentication process. Let's take a look at the route and controller for login:

/* frontend/app/login/controller.js */
import Ember from 'ember';
import LoginControllerMixin from 'simple-auth/mixins/login-controller-mixin';

export default Ember.Controller.extend(LoginControllerMixin, {
    authenticator: 'authenticator:torii'
});

Nothing much here - we rely on LoginControllerMixin to do the heavy lifting, and tell it the authenticator to use - which is torii.

Our route does the actual triggering:

/* frontend/app/login/route.js */
import Ember from 'ember';

export default Ember.Route.extend({

    actions: {

        googleLogin: function() {
            let self = this;

            self.get('torii').open('google-oauth2').then(function(auth) {
                return self.get('session').authenticate('authenticator:token', auth);
            });
        }
    }
});

This is where it gets interesting. The googleLogin action starts the authentication with Google, and when receiving an auth code, calls our authenticator called token to proceed with the authentication process. The reason for this is that we actually have to turn to our backend code to complete the authentication. We create app/authenticators/token.js with our authenticator:

/* app/authenticators/token.js */
import Base from 'simple-auth/authenticators/base';
import Ember from "ember";

export default Base.extend({
  restore: function(credentials) {
      return Ember.$.ajax({
          type: "POST",
          url: "/reauth",
          contentType : 'application/json',
          data: JSON.stringify(credentials)
      });
  },
  authenticate: function(credentials) {
      return Ember.$.ajax({
          type: "POST",
          url: "/login",
          contentType : 'application/json',
          data: JSON.stringify(credentials)
      });
  },
});

Authenticators need to take care of two scenarios - authenticating from scratch and attempting to authenticate from a previous session. We use two Flask routes for these tasks, which we'll implement soon.

Configuring Torii for OAuth2

You will need to go to Google's developer console and create your webapp. During the process you'll receive the client id for your app, which is a long string of characters, and your secret. Your client can be written in your source code (as in the torii confid we'll write soon), but the secret should not be accessible to anyone except for trusted developers or system administrators. Normally you would put it in a server-local configuration, but that is out of the scope of this blog post. For simplicity, we'll include it in app.py soon.

As for torii, we need to configure it with the correct client id:

/* frontend/config/environment.js */
  var ENV = {
    modulePrefix: 'frontend',
    ...
      torii: {
        providers: {
            'google-oauth2': {
                apiKey: '<YOUR CLIENT ID STRING HERE>',
                scope: 'email profile'
            }
        }
    }
  };

We need to pull a simple hack to configure the redirect URI (i.e. the URI your browser will use to redirect with the authorization code):

/* frontend/app/app.js */
App = Ember.Application.extend({
...
});

loadInitializers(App, config.modulePrefix);
config.torii.providers['google-oauth2'].redirectUri = window.location.origin;
export default App;

The reason for the above hack is that we don't know in advance (during asset compilation) where the redirect URI should go to. We could have simply hard-coded the expected website URI, but then it would make it harder to test, so we take this approach.

Note: the redirect URIs that you use in both development and production should be properly entered in the redirect URIs section of the developer console. Don't be frustrated if you get bad_redirect_uris at first - the developer console takes a few minutes to update and is very sensitive to ending slashes and minor changes.

Finishing the Authentication Process

Now to write the code for /login and /reauth:

# flask_app/app.py
from flask import request, abort, jsonify
...
@app.route("/login", methods=['POST'])
def login():

    auth_code = (request.json or {}).get('authorizationCode')
    if auth_code is None:
        abort(401)

    user_info = _get_oauth2_identity(auth_code)
    if not user_info:
        abort(401)

    token = _get_token_serializer().dumps({'user_info': user_info})

    return jsonify({
        'auth_token': token,
        'user_info': user_info,
    })

The route expects to receive the auth code from the authenticator we wrote, and then uses _get_oauth2_identity to check the validity of the code and get the user information from it. We'll get to it in a second. Assuming it goes well, it uses itsdangerous to just sign the user information with the current timestamp, and uses that as a token.

The result from the login route will be later saved via ember-simple-auth to session.secure in our frontend app, so it's useful to include the raw user_info dict there.

Signing with itsdangerous ensures that nobody tempers with our token or changes details in it (we'll be using it in reauth soon enough so that is important to prevent session hijacking.

The signing code is pretty straightforward:

# flask_app/app.py
...
from itsdangerous import TimedSerializer, BadSignature
...
def _get_token_serializer():
    return TimedSerializer(current_app.config['SECRET_KEY'])

Note: You'll have to set your Flask app's SECRET_KEY for this to work. You should be well aware of the security concerns about this key - having it compromised will expose your app to session hijacks and other hazards.

Let's turn our attention to _get_oauth2_identity:

# flask_app/app.py
...
from apiclient.discovery import build
from httplib2 import Http
from oauth2client.client import OAuth2WebServerFlow
...

def _get_oauth2_identity(auth_code):

    client_id = current_app.config.get('OAUTH2_CLIENT_ID')
    client_secret = current_app.config.get('OAUTH2_CLIENT_SECRET')
    if not client_id:
        # Error - No OAuth2 client id configured
        return

    if not client_secret:
        # Error - No OAuth2 client secret configured
        return

    flow = OAuth2WebServerFlow(
        client_id=client_id,
        client_secret=client_secret,

        scope='https://www.googleapis.com/auth/userinfo.profile',
        redirect_uri=request.host_url[:-1])

    credentials = flow.step2_exchange(auth_code)

    info = _get_user_info(credentials)
    return info


def _get_user_info(credentials):
    http_client = Http()
    if credentials.access_token_expired:
        credentials.refresh(http_client)
    credentials.authorize(http_client)
    service = build('oauth2', 'v2', http=http_client)
    return service.userinfo().get().execute()

The above code uses Google's api client and the oauth2 flow to validate an auth code and get the associated user information with it. On success, it returns a dictionary with lots of information about the user - including email, avatar, full name and more.

The /reauth code is not that surprising given what we've learned:

# flask_app/app.py

@app.route("/reauth", methods=['POST'])
def reauth():
    token = (request.json or {}).get('auth_token')
    if token is None:
        abort(401)
    try:
        token_data = _get_token_serializer().loads(
            token, max_age=_MAX_TOKEN_AGE)
    except BadSignature:
        abort(401)

    return jsonify({
        'auth_token': token,
        'user_info': token_data['user_info'],
    })

/reauth simply returns the token information as before if (and only if) the signature verification succeeds. The _MAX_TOKEN_AGE constant can be set to your choosing, as it controls the session expiration period.

That's it! You should now be able to access your webapp when it runs, browse to your private route and perform the login successfully...

P.S. I'd like to thank Roey Darwish for helping out figuring some of the quirks involved in the process mentioned above.

Comments

blog comments powered by Disqus