Password hashing using Bcrypt in Python

abbyesmith

abbyesmith

Posted on April 25, 2023

Password hashing using Bcrypt in Python

Hey there tech enthusiasts!

I am making my way through the Flatiron School bootcamp for software engineering. It is hard to believe that I am now 80% of my way through the course and am approaching my final capstone project. Thus far I have learned JavaScript, React, Python and Flask and just finished my first fill stack project. My final project is going to be a tool for students to use in math class to make graphing homework easier to do on a computer. I know that I want to allow my users to have an account to save their work, so I will be implementing password hashing using bcrypt. This will add a level of security for the users as their passwords will be encoded before saving them to my user table. As many people reuse passwords for different accounts, it’s important that I do my part to ensure their password does not get discovered by a person who would use it for unethical purposes.

Whether you’re new to programming or a seasoned developer, understanding how to implement bcrypt in Python can be a valuable skill to have in your toolbox. So let’s get started and explore the ins and outs of password hashing with bcrypt in Python!

Once you have a good understanding of using bcrypt, I strongly encouraging you to add salt to your hashing. Come back in a little bit and I'll add an additional blog post about increasing the security of password storage with salt!


**Goals:

  • Add password hashing to SQLAlchemy tables running in Flask (Python)

**Previous Knowledge:

  • Creating tables in SQLAlchemy (reminders are provided)
  • Using PostMan
  • Importing libraries
  • Setting up the server side of an app

*Part 1) Installations:
*

Bcrypt is an installation package that you can install via the command line. If you are using Python, use the following command line:
$ pipenv install bcrypt

I am using a Flask framework, so I will install the Flask version of bcryt:

$ pipenv install flask-bcrypt

Now that you have bcrypt in your pipfile, generate your secret key by passing the following in the command line:

$ python -c 'import os; print(os.urandom(16))

This is going to be the key your program uses to encode the password when it is added to the database. Similarly, the secret key will be used to decode the password the next time the user tries to log in. You will need access to this string of random characters soon!

*Part 2) Config File
*

I personally like to have a config.py file in order to cut down on the clutter in my models.py. This is a space to handle all of your imports and metadata boilerplate text. Here is what I have in my config.py:

from flask import Flask
from flask_bcrypt import Bcrypt
from flask_migrate import Migrate
from flask_restful import Api
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import MetaData
from flask_cors import CORS


app = Flask(__name__)
app.secret_key = b'<<This is where your secret key string goes>>'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.json.compact = False

metadata = MetaData(naming_convention={
    "ix": "ix_%(column_0_label)s",
    "uq": "uq_%(table_name)s_%(column_0_name)s",
    "ck": "ck_%(table_name)s_`%(constraint_name)s`",
    "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
    "pk": "pk_%(table_name)s"
    })
db = SQLAlchemy(metadata=metadata)

migrate = Migrate(app, db)
db.init_app(app)

bcrypt = Bcrypt(app)

api = Api(app)
CORS(app)
Enter fullscreen mode Exit fullscreen mode

You will be inserting the secret key into the space indicated.

Please note that some of these imports are for creating my SQLAlchemy tables. Make the needed adjustments for your project.

*Part 3) Models
*

I always like to think about models as what I am modeling my data after. Here is where we are going to form the structure of our tables. For this walk through, I am only going to show a very basic User table with only a username and password.

At the top of the file, handle your imports.

from sqlalchemy.ext.hybrid import hybrid_property

from config import db, bcrypt
Enter fullscreen mode Exit fullscreen mode

Create your User table. You will not be storing the password as the user typed it in. Instead your are storing the encoded version of their password. To acknowledge this in the table, use the column name "_password_hash".

class User(db.Model):
    __tablename__ = 'users'

    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String, unique = True, nullable = False)
    _password_hash = db.Column(db.String)
Enter fullscreen mode Exit fullscreen mode

Now it's time to start hashing the password. The following boilerplate text is encoding the user's password so your table will store something that doesn't even resemble what was typed in when the user signed up for your site. When the user tries to sign back in, the secret key is used to check if the password input results in the same string of grarbildy goop as what is stored in the table.

class User(db.Model):
    __tablename__ = 'users'

    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String, unique = True, nullable = False)
    _password_hash = db.Column(db.String)

    @hybrid_property
    def password_hash(self):
        raise AttributeError('Password hashes may not be viewed.')

    @password_hash.setter
    def password_hash(self, password):
        password_hash = bcrypt.generate_password_hash(
            password.encode('utf-8')
        )
        self._password_hash = password_hash.decode('utf-8')

    def authenticate(self, password):
        return bcrypt.check_password_hash(
            self._password_hash, password.encode('utf-8')
Enter fullscreen mode Exit fullscreen mode

*Part 4) App
*

The app.py file is where we are adding functionality to our routes. Let's focus on creating a 'POST' for a Signup class and a 'POST' for a Login class. We will be using Postman to check the functionality of the work.

Begin (as always!) with the needed imports:

#!/usr/bin/env python3

from flask import request, session, make_response
from flask_restful import Resource
from sqlalchemy.exc import IntegrityError


from config import app, db, api
from models import User
Enter fullscreen mode Exit fullscreen mode

Before you go too much further, hop back to models.py and make your tables. As a reminder, here are the command lines if you are in flask.

# export FLASK_APP=app.py
# export FLASK_RUN_PORT=5555
# flask db init
# flask db revision --autogenerate -m 'Create tables' 
# flask db upgrade 
Enter fullscreen mode Exit fullscreen mode

Be sure your user table is showing up in your database before moving onto the next step.

Let's create a Signup class that can POST a new user to our database.


class Signup(Resource):
    def post(self):

        request_json = request.get_json()

        username = request_json.get('username')
        password = request_json.get('password')

        user = User(
            username = username
        )

        user.password_hash = password

        try:
            db.session.add(user)
            db.session.commit()

            session ['user_id']=user.id

            print (user.to_dict(), 201)

        except IntegrityError:
            print ('nope')

            return {'error': '422 Unprocessable Entity'}, 422

api.add_resource(Signup, '/signup', endpoint='signup')

if __name__ == '__main__':
    app.run(port=5555, debug=True)
Enter fullscreen mode Exit fullscreen mode

*Part 5) Making the magic happen!
*

You should now be able to go to postman and run http://127.0.0.1:5555/signup POST. Enter in a username and password to test. If you get a 'null' message after running a POST, then congrats! You did it!! If not, double check your table set up. It's possible a small typo is holding your back.

Image description

To check if you are successful, go over to your table in your database. You should see a new row with username as whatever you entered and a string of nonsense. This is the hashed password.

To check the functionality of signing in with your hashed password, add a new Login class in app.py:

class Login(Resource):

    def post(self):

        request_json = request.get_json()

        username = request_json.get('username')
        password = request_json.get('password')

        user = User.query.filter(User.username == username).first()

        if user:
            if user.authenticate(password):
                print("authenticate")
                session['user_id'] = user.id
                return user.to_dict(), 200

        return make_response({'error': '401 Unauthorized'},401) 

api.add_resource(Login, '/login', endpoint='login')
Enter fullscreen mode Exit fullscreen mode

Head back to postman. Change the path to http://127.0.0.1:5555/login and enter the same username and password information you used in the signup. You should get back an object with the username and hashed password.


Way to go! You just helped out your users by making their passwords more secure in your database. If you were able to make it this far, I suggest you look into adding salt to your password hashing method for an extra layer of security. I'll follow up with a blog post on salt shortly!

💖 💪 🙅 🚩
abbyesmith
abbyesmith

Posted on April 25, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related