Before deciding to use Ghost, I was thinking to create my blog platform from scratch using Flask, its extensions and MongoDB.

The first thing I thought about was sessions and users management. Flask has a nice extension, called Flask-Login, that handles all details for you, so I started to dig further into its documentation. Unfortunately, every example is written using SQLAlchemy which has a different syntax as opposed to pymongo (the official Python driver for Mongo). After some trial&error, I got a working solution.

The User class

First of all, we need a user class that implements four methods: is_authenticated, is_active, is_anonymous and get_id. We need to pay attention to the last one because it must return a unicode that uniquely identifies the user. Therefore, if you use a string, then you're good to go because Mongo stores every string as unicode; otherwise, if you use a number, be sure to convert it to a unicode string before returning it.
My implementation is the following:

from import check_password_hash

class User():

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

    def is_authenticated(self):
        return True

    def is_active(self):
        return True

    def is_anonymous(self):
        return False

    def get_id(self):
        return self.username

    def validate_login(password_hash, password):
        return check_password_hash(password_hash, password)

I added a static method to validate the inserted password before instantiating the object. For security reasons we store password's hashes, instead of clear text, therefore we use a function from werkzeug package to perform this check. In this example I'm assuming that the database already holds some hashes, but if you need to populate it, I suggest to compute the hashes with the function generate_password_hash (always from werkzeug).

The user_loader

We need a user_loader function to reload the user object through different sessions. This function must satisfy three conditions:

  • it takes the unicode ID of a user;
  • it returns the user object;
  • if the ID is invalid must return None.
from app import lm

def load_user(username):  
    u = app.config['USERS_COLLECTION'].find_one({"_id": username})
    if not u:
        return None
    return User(u['_id'])

Don't miss the function's decorator!

The views: login and logout

Finally, we write the views that will handle users access.
When a user tries to login, we need to check both its username and password. If everything is fine, we will grant him access.

from app import app  
from flask import request, redirect, render_template, url_for, flash  
from flask.ext.login import login_user, logout_user  
from .forms import LoginForm  
from .user import User

@app.route('/login', methods=['GET', 'POST'])
def login():  
    form = LoginForm()
    if request.method == 'POST' and form.validate_on_submit():
        user = app.config['USERS_COLLECTION'].find_one({"_id":})
        if user and User.validate_login(user['password'],
            user_obj = User(user['_id'])
            flash("Logged in successfully", category='success')
            return redirect(request.args.get("next") or url_for("writePost"))
        flash("Wrong username or password", category='error')
    return render_template('login.html', title='login', form=form)

Logging out is a lot easier:

def logout():  
    return redirect(url_for('login'))

Just for completeness, I write here the form used for the login process:

from import Form  
from wtforms import StringField, PasswordField  
from wtforms.validators import DataRequired

class LoginForm(Form):  
    """Login form to access writing and settings pages"""

    username = StringField('Username', validators=[DataRequired()])
    password = PasswordField('Password', validators=[DataRequired()])

To test everything out, you can write a view with the @login_required decorator and try to access it.
If you have any improvement or question, just leave a comment ;-)


I pushed the full source code to a public repo, so you can play with it

Here's the link: