PROGRAMMING

Ferris 2 (Google App Engine), JSON Web Token and CORS

#python , #google app engine

When I started working on my new project I decided to use Google App Engine and Ferris 2. Since App Engine had to be used only for backend, the next decision was AngularJS and JSON Web Tokens. At the moment I switched to Ferris 3 (it is a great tool!) but I will write here only about how to solve the problem with CORS.

Let’s start with Ferris 2 authorization chain. This method is responsible for verification of a user token. It is very simple, it gets Authorization header, gets secret token, tries to decode token, checks expiration time, returns user in controller.user if token is correct, otherwise it returns False. You can read more here.

from datetime import datetime
import jwt

from ferris import settings


def require_user(controller):
    token = controller.request.headers.get('Authorization')
    if not token:
        return False

    try:
        payload = jwt.decode(
            token, settings.get('app_config').get('token_secret')
        )
    except jwt.DecodeError:
        return False

    if datetime.fromtimestamp(payload['exp']) < datetime.now():
        return False

    controller.user = payload['user']

    return True

Of course, I had a problem with CORS when javascript client wanted to make a request to the backend. I will put improved decorator firstly posted here. Decorator from Google groups doesn’t work, i don’t remember why but below is a correct version (it worked for me ;-)).

import functools

def allow_options(function):
    @functools.wraps(function)
    def inner(self, *args, **kwargs):
        if self.request.method == 'OPTIONS':
            self.response.headers.add_header(
                'Access-Control-Allow-Origin',
                'http://localhost:9000'
            )
            self.response.headers.add_header(
                'Access-Control-Allow-Methods',
                'POST, GET, OPTIONS'
            )
            self.response.headers.add_header(
                'Access-Control-Max-Age',
                str(1000)
            )

Of course you need to replace http://localhost:9000 with your frontend address and you can change values in Access-Control-Allow-Methods and Access-Control-Max-Age if you need this. I just tried it only on localhost (because I moved to Ferris 3 and endpoints) but it should work correctly. It just catches a requests with OPTIONS method, adds required headers and return response so the next request should get correct answer instead of error about domain not allowed by Access-Control-Allow-Origin.

Short example how to use it:

from ferris.core.controller import add_authorizations
from ferris import route
from app.utils.auth import (
    allow_options,
    require_user
)


@allow_options
@route
@add_authorizations(require_user):
def profile(self, username):
    user = self.controller.user