Author avatar

Chidiebere Nnadi

User Model and Authentication Controller for a Simple File Storage Service Using VueJS, Flask, and RethinkDB

Chidiebere Nnadi

  • Aug 9, 2018
  • 14 Min read
  • Aug 9, 2018
  • 14 Min 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.

1import rethinkdb as r
3from flask import current_app
5conn = r.connect(db="papers")
7class RethinkDBModel(object):
8  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.

1class User(RethinkDBModel):
2  _table = 'users'
4  @classmethod
5  def create(cls, **kwargs):
6      fullname = kwargs.get('fullname')
7      email = kwargs.get('email')
8      password = kwargs.get('password')
9      password_conf = kwargs.get('password_conf')
10      if password != password_conf:
11        raise ValidationError("Password and Confirm password need to be the same value")
12      password = cls.hash_password(password)
13      doc = {
14        'fullname': fullname,
15        'email': email,
16        'password': password,
17        'date_created':'+01:00')),
18        'date_modified':'+01:00'))
19        }
20      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/ module. Find the definition of ValidationError below:

1class ValidationError(Exception):
2    pass

We're using named exceptions because they are easier to track.

Notice how we used'+01:00')) here? I faced some issues when I used 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 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.

1from passlib.hash import pbkdf2_sha256
3class User(RethinkDBModel):
4  _table = 'users'
6  @classmethod
7  def create(cls, **kwargs):
8     fullname = kwargs.get('fullname')
9     email = kwargs.get('email')
10     password = kwargs.get('password')
11     password_conf = kwargs.get('password_conf')
12     if password != password_conf:
13         raise ValidationError("Password and Confirm password need to be the same value")
14     password = cls.hash_password(password)
15     doc = {
16         'fullname': fullname,
17         'email': email,
18         'password': password,
19         'date_created':'+01:00')),
20         'date_modified':'+01:00'))
21     }
22     r.table(cls._table).insert(doc).run(conn)
24  @staticmethod
25  def hash_password(password):
26      return pbkdf2_sha256.encrypt(password, rounds=200000, salt_size=16)
28  @staticmethod
29  def verify_password(password, _hash):
30      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 will look like when we're done adding in the logic.

1import os
2import rethinkdb as r
3from jose import jwt
4from datetime import datetime
5from passlib.hash import pbkdf2_sha256
7from flask import current_app
9from api.utils.errors import ValidationError
11conn = r.connect(db="papers")
13class RethinkDBModel(object):
14    pass
17class User(RethinkDBModel):
18  _table = 'users'
20  @classmethod
21  def create(cls, **kwargs):
22    fullname = kwargs.get('fullname')
23    email = kwargs.get('email')
24    password = kwargs.get('password')
25    password_conf = kwargs.get('password_conf')
26    if password != password_conf:
27        raise ValidationError("Password and Confirm password need to be the same value")
28    password = cls.hash_password(password)
29    doc = {
30        'fullname': fullname,
31        'email': email,
32        'password': password,
33        'date_created':'+01:00')),
34        'date_modified':'+01:00'))
35    }
36    r.table(cls._table).insert(doc).run(conn)
38  @classmethod
39  def validate(cls, email, password):
40    docs = list(r.table(cls._table).filter({'email': email}).run(conn))
42    if not len(docs):
43        raise ValidationError("Could not find the e-mail address you specified")
45    _hash = docs[0]['password']
47    if cls.verify_password(password, _hash):
48        try:
49            token = jwt.encode({'id': docs[0]['id']}, current_app.config['SECRET_KEY'], algorithm='HS256')
50            return token
51        except JWTError:
52            raise ValidationError("There was a problem while trying to create a JWT token.")
53        else:
54            raise ValidationError("The password you inputted was incorrect.")
56  @staticmethod
57  def hash_password(password):
58      return pbkdf2_sha256.encrypt(password, rounds=200000, salt_size=16)
60  @staticmethod
61  def verify_password(password, _hash):
62      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 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/ file.

1from flask_restful import Resource
3class AuthLogin(Resource):
4  def post(self):
5    pass
7class AuthRegister(Resource):
8  def post(self):
9    pass

Next up, we will be creating these routes and referencing the respective classes in our /api/ file.

1from flask import Flask, Blueprint
2from flask_restful import Api
4from api.controllers import auth
5from config import config
7def create_app(env):
8  app = Flask(__name__)
9  app.config.from_object(config[env])
11  api_bp = Blueprint('api', __name__)
12  api = Api(api_bp)
14  api.add_resource(auth.AuthLogin, '/auth/login')
15  api.add_resource(auth.AuthRegister, '/auth/register')
17  app.register_blueprint(api_bp, url_prefix="/api/v1")
19  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.

1from flask_restful import reqparse, abort, Resource
3from api.models import User
4from api.utils.errors import ValidationError
6class AuthLogin(Resource):
7  def post(self):
8     parser = reqparse.RequestParser()
9     parser.add_argument('email', type=str, help='You need to enter your e-mail address', required=True)
10     parser.add_argument('password', type=str, help='You need to enter your password', required=True)
12     args = parser.parse_args()
14     email = args.get('email')
15     password = args.get('password')
17     try:
18        token = User.validate(email, password)
19         return {'token': token}
20     except ValidationError as e:
21         abort(400, message='There was an error while trying to log you in -> {}'.format(e.message))
23class AuthRegister(Resource):
24  def post(self):
25     parser = reqparse.RequestParser()
26     parser.add_argument('fullname', type=str, help='You need to enter your full name', required=True)
27     parser.add_argument('email', type=str, help='You need to enter your e-mail address', required=True)
28     parser.add_argument('password', type=str, help='You need to enter your chosen password', required=True)
29     parser.add_argument('password_conf', type=str, help='You need to enter the confirm password field', required=True)
31     args = parser.parse_args()
33     email = args.get('email')
34     password = args.get('password')
35     password_conf = args.get('password_conf')
36     fullname = args.get('fullname')
38     try:
39         User.create(
40             email=email,
41             password=password,
42             password_conf=password_conf,
43             fullname=fullname
44         )
45         return {'message': 'Successfully created your account.'}
46     except ValidationError as e:
47         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 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.