User Model and Authentication Controller for a Simple File Storage Service Using VueJS, Flask, and RethinkDB
Aug 9, 2018 • 14 Minute Read
Getting Started
For more information about how to set up your workspace, checkout the first guide in this series: Introduction and Setup to Building a Simple File Storage Service Using VueJS, Flask and RethinkDB.
User Model
We'll be adding in code for our models. For this application, we need just two models to start. For this step, we will be creating just the user model.
We start off by connecting to RethinkDB and creating a connection object to our database.
import rethinkdb as r
from flask import current_app
conn = r.connect(db="papers")
class RethinkDBModel(object):
pass
We referenced the database name from the application config. In flask, we have the current_app variable which holds a reference to the currently running application instance.
Why did I create a blank RethinkDBModel class? Well, there might be a couple of things we might want to share across model classes; this class is here in case cross-model sharing is necessary.
Our User class will inherit from this empty base class. In User, we will have a few functions which we'll use to interact with the database from the controllers.
We start with the create() function. This function will be called when we need to create a user document in the table.
class User(RethinkDBModel):
_table = 'users'
@classmethod
def create(cls, **kwargs):
fullname = kwargs.get('fullname')
email = kwargs.get('email')
password = kwargs.get('password')
password_conf = kwargs.get('password_conf')
if password != password_conf:
raise ValidationError("Password and Confirm password need to be the same value")
password = cls.hash_password(password)
doc = {
'fullname': fullname,
'email': email,
'password': password,
'date_created': datetime.now(r.make_timezone('+01:00')),
'date_modified': datetime.now(r.make_timezone('+01:00'))
}
r.table(cls._table).insert(doc).run(conn)
Here, we are making use of the classmethod decorator. This decorator enables us have access to the class instance from within the method body. We will be using the class instance to access the _table property from within the method. The _table stores the table name for that model.
We have also added code here to make sure that the password and password_conf fields are the same. When this happens, a ValidationError will be thrown. The exceptions will be stored in the /api/utils/errors.py module. Find the definition of ValidationError below:
class ValidationError(Exception):
pass
We're using named exceptions because they are easier to track.
Notice how we used datetime.now(r.make_timezone('+01:00')) here? I faced some issues when I used datetime.now() without the timezone. RethinkDB requires that time zone information be set on date fields in documents. The Python function does not supply this for us by default unless we specify this as a parameter to the now() function (See here for more). Using the r.make_timezone('+01:00') we are able to create a timezone object that we can use for the datetime.now() function.
If all goes well and no exceptions are encountered, we call the insert() method on the table object that r.table(table_name) returns. This method takes a dictionary containing the data. This data will be stored as a new document in the table selected.
We have made a call to the hash_password() method in our class. This method makes use of the hash.pbkdf2_sha256 module in the passlib package to hash the password fairly securely. In addition to that, we will need to create a method for verifying passwords.
from passlib.hash import pbkdf2_sha256
class User(RethinkDBModel):
_table = 'users'
@classmethod
def create(cls, **kwargs):
fullname = kwargs.get('fullname')
email = kwargs.get('email')
password = kwargs.get('password')
password_conf = kwargs.get('password_conf')
if password != password_conf:
raise ValidationError("Password and Confirm password need to be the same value")
password = cls.hash_password(password)
doc = {
'fullname': fullname,
'email': email,
'password': password,
'date_created': datetime.now(r.make_timezone('+01:00')),
'date_modified': datetime.now(r.make_timezone('+01:00'))
}
r.table(cls._table).insert(doc).run(conn)
@staticmethod
def hash_password(password):
return pbkdf2_sha256.encrypt(password, rounds=200000, salt_size=16)
@staticmethod
def verify_password(password, _hash):
return pbkdf2_sha256.verify(password, _hash)
The pbkdf2_sha256.encrypt() method is called with the password and values for rounds and salt_size. See here for details on how you can customize your encryption and how the library works. Just to give some context on the decision to use PBKDF2:
Security-wise, PBKDF2 is currently one of the leading key derivation functions, and has no known security issues.
-- Quotes from the passlib documentation
The verify_password method will be called using the password string and a hash. It will return true or false if the password is valid.
We will now move on the the validate() function. This function will be called in the login method with the email address and password. The function will check that the document exists using the email field as index and then compare the password hash against the password supplied.
In addition to that, since we're going to be making use of JWT (JSON Web Token) for token-based authentication, we will be generating a token if the user supplies valid information. This is how the entire models.py will look like when we're done adding in the logic.
import os
import rethinkdb as r
from jose import jwt
from datetime import datetime
from passlib.hash import pbkdf2_sha256
from flask import current_app
from api.utils.errors import ValidationError
conn = r.connect(db="papers")
class RethinkDBModel(object):
pass
class User(RethinkDBModel):
_table = 'users'
@classmethod
def create(cls, **kwargs):
fullname = kwargs.get('fullname')
email = kwargs.get('email')
password = kwargs.get('password')
password_conf = kwargs.get('password_conf')
if password != password_conf:
raise ValidationError("Password and Confirm password need to be the same value")
password = cls.hash_password(password)
doc = {
'fullname': fullname,
'email': email,
'password': password,
'date_created': datetime.now(r.make_timezone('+01:00')),
'date_modified': datetime.now(r.make_timezone('+01:00'))
}
r.table(cls._table).insert(doc).run(conn)
@classmethod
def validate(cls, email, password):
docs = list(r.table(cls._table).filter({'email': email}).run(conn))
if not len(docs):
raise ValidationError("Could not find the e-mail address you specified")
_hash = docs[0]['password']
if cls.verify_password(password, _hash):
try:
token = jwt.encode({'id': docs[0]['id']}, current_app.config['SECRET_KEY'], algorithm='HS256')
return token
except JWTError:
raise ValidationError("There was a problem while trying to create a JWT token.")
else:
raise ValidationError("The password you inputted was incorrect.")
@staticmethod
def hash_password(password):
return pbkdf2_sha256.encrypt(password, rounds=200000, salt_size=16)
@staticmethod
def verify_password(password, _hash):
return pbkdf2_sha256.verify(password, _hash)
A couple of things to take note of in the validate() method. Firstly, the filter() function was used here on the table object. This command takes in a dictionary that is used to search through the table. This function can also take a predicate, in some cases. This predicate can be a lambda function and will be used similarly to what is done with the python filter function or any other function that takes a function as an argument. The function returns a cursor that can be used to access all the documents that are returned by the query. The cursor is iterable and as such we can iterate the cursor object using the for...in loop. In this case, we have chosen to convert the iterable object to a list using the Python list function.
As you would expect, we basically do two things here. We want to know if the email address exists at all and then if the password is correct. For the first part, we basically count the collection. If this is empty, we raise an error. For the second part, we call the verify_password() function to compare the password supplied with the hash in the database. We raise an exception if these don't match.
Also noteworthy is how we have used jwt.encode() to create a JWT token and return it to the controller. This method is fairly straightforward and you can see the documentation here.
That does it for the model. Let's move on to the controllers. We have tried to obey the principle of having Fat models and Slim controllers, in this model. Most of the logic is in the models. This way, our controllers only focus on routing and error reporting to the API end user.
Authentication Controller
For the authentication controller, we need to add in Flask RESTful resource sub classes. Django web development is similar to class-based views. It's simply created as a subclass of the flask_restful.Resource class. Your subclasses will have methods that map to respective HTTP verbs. For instance, if we wanted to implement a GET action, we will be creating a get() method in our Resource subclass. The process is completed by mapping URLs to the respective classes using the api.add_resource() method.
Now let's add in two classes; one to take care of POST action to the login route and one to take care of POST action to the register route.
We'll start by creating the required classes. These should be stored in the /api/controllers/auth.py file.
from flask_restful import Resource
class AuthLogin(Resource):
def post(self):
pass
class AuthRegister(Resource):
def post(self):
pass
Next up, we will be creating these routes and referencing the respective classes in our /api/__init__.py file.
from flask import Flask, Blueprint
from flask_restful import Api
from api.controllers import auth
from config import config
def create_app(env):
app = Flask(__name__)
app.config.from_object(config[env])
api_bp = Blueprint('api', __name__)
api = Api(api_bp)
api.add_resource(auth.AuthLogin, '/auth/login')
api.add_resource(auth.AuthRegister, '/auth/register')
app.register_blueprint(api_bp, url_prefix="/api/v1")
return app
Now let's head back to the controller file to add in some logic. The logic required here is similar to what you will do with authentication systems in general.
from flask_restful import reqparse, abort, Resource
from api.models import User
from api.utils.errors import ValidationError
class AuthLogin(Resource):
def post(self):
parser = reqparse.RequestParser()
parser.add_argument('email', type=str, help='You need to enter your e-mail address', required=True)
parser.add_argument('password', type=str, help='You need to enter your password', required=True)
args = parser.parse_args()
email = args.get('email')
password = args.get('password')
try:
token = User.validate(email, password)
return {'token': token}
except ValidationError as e:
abort(400, message='There was an error while trying to log you in -> {}'.format(e.message))
class AuthRegister(Resource):
def post(self):
parser = reqparse.RequestParser()
parser.add_argument('fullname', type=str, help='You need to enter your full name', required=True)
parser.add_argument('email', type=str, help='You need to enter your e-mail address', required=True)
parser.add_argument('password', type=str, help='You need to enter your chosen password', required=True)
parser.add_argument('password_conf', type=str, help='You need to enter the confirm password field', required=True)
args = parser.parse_args()
email = args.get('email')
password = args.get('password')
password_conf = args.get('password_conf')
fullname = args.get('fullname')
try:
User.create(
email=email,
password=password,
password_conf=password_conf,
fullname=fullname
)
return {'message': 'Successfully created your account.'}
except ValidationError as e:
abort(400, message='There was an error while trying to create your account -> {}'.format(e.message))
As mentioned earlier, the majority of the logic and database interaction has been pushed to the model. The controller logic is relatively simple.
To summarize what was done, for the login controller AuthLogin, we created a post() function which accepts the e-mail address and password, validates the fields using reqparse, and calls User.validate() which validates the information sent and returns a token. If an error occurs, we catch it and respond with an error message.
Similarly, for the AuthRegister, we collect information from the user and call a model create() function. In this case, we create a collection for the email address, password, password confirm, and full name fields. We pass all these values to the User.create() function and, as before, this function will throw an error if anything goes wrong.
All things being equal, everything should work just fine. Run the server using python run.py runserver to test it out. You should be able to access the two endpoints that we've created here, and it should work very well.
Next Steps
Next up, we'll be creating the models for our files. Continue on to the next guide in this series - File and Folder Models.