Flask User Auth with Neo4j

Josh-T
The Startup
Published in
10 min readJun 30, 2020

I’ve been working on a Flask App project for the past few months. Tutorial after tutorial and book after book used the combination of Flask with SQLAlchemy to leverage databases like SQLite.

I chose a different path. Perhaps a harder path. I wanted to build a Flask App with Neo4j as my DB. I discovered that finding documentation on this combo will likely lead you to a variety of blogs, videos, GitHub Repositories created by Nicole White. Nicole created a terrific tutorial, but it’s older now. I couldn’t get it to work because things evolve. At the end of the day I decided to figure this out and post a more up to date and more in depth tutorial.

Here I’ll give a tutorial covering Flask Setup, User Registration, User Login/Logout, and User sessions.

Access the project on Github

Project Setup

Create a project folder. Mine is called “BasicApp” for this tutorial.

I suggest you create a virtual environment for the project.

If you are using PyCharm you can skip this step. If you are using another editor then create a virtual environment in the BasicApp directory.

python3 -m venv venv
source venv/bin/activate

I use PyCharm for my editor, and find that creating a new project with a virtual environment is much faster.

Create a new project and make the appropriate selections as shown below

PyCharm Flask Project

Create the following directory structure

BasicApp
- data
- services
- static
- css
- templates
- accounts
- home
- shared
- app.py
- requirements.txt

Add the following to requirements.txt

py2neo
werkzeug
flask
passlib

In the data directory create a file called db_session.py. This is where you will store your Neo4j authentication information. Be sure to enter your unique password and your IP address. If you are running Neo4j locally, enter 127.0.0.1.

See my previous article Installing Neo4j if you need some help

Add the following to db_session.py

from py2neo import Graphdef db_auth():
user = 'neo4j'
pword = 'YourNeo4jPassword'
graph = Graph("http://192.168.1.1:7474/db/data/", username=user, password=pword)
return graph

Next, create a basic CSS design. Everything in this tutorial will utilize the CSS below. You can change and enhance later.

body {
margin: 0;
padding: 0;
color: #000000;
font-family: Source Sans Pro, Helvetica, Arial, sans-serif;
font-size: 17px;
background-color: #ffffff;
}
.main_content {
background: #ffffff;
margin: 0;
padding: 0 0 50px;
}
.topnav {
overflow: hidden;
width: 100%;
background-color: black;
font-size: 14px;
box-shadow: 5px 5px 5px gray;
text-align: center;
}
.topnav a {
float: left;
display: block;
color: #EFEFEF;
text-align: center;
padding: 14px 16px;
text-decoration: none;
border-radius: 25px;
border-top: 5px solid black;
border-bottom: 5px solid black;
}
.topnav a:hover {
color: white;
}
.message h1 {
width: 100%;
padding: 10px;
margin-left: auto;
margin-right: auto;
text-align: center;
font-size: 40px;
font-weight: 800;
}
.form-control {
margin-bottom: 15px;
}
.form-wrapper {
border: 1px solid gray;
width: 25%;
margin-left: auto;
margin-right: auto;
margin-top: 30px;
border-radius: 10px;
background-color: white;
}
.form-label {
padding: 5px;
font-weight: bold;
}
form {
padding: 20px;
max-width: 450px;
margin-left: auto;
margin-right: auto;
margin-top: 10px;
border-radius: 10px;
}
form .controls {
margin-bottom: 1em;
}
input {
margin-top: 15px;
margin-bottom: 15px;
}
.actions, h2{
text-align: center;
}
.error-msg {
color: red;
font-size: 16px;
font-weight: bold;
padding-top: 5px;
text-align: center;
}
footer {
margin-top: 50px;
padding: 30px;
text-align: center;
color: black;
}
footer a {
color: darkorange;
}

I prefer to use template inheritance in my applications. This simplifies and reduces the amount of HTML code you need to write for each page. It also makes it much easier for shared design throughout all of your pages. For more information about Template Inheritance, check out this page.

Create the base template in BasicApp/templates/shared/_layout.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block title %}FlaskStarter{% endblock %}</title>
<link rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400italic,600,600italic,700,700italic|Source+Code+Pro:500">
<link rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<link rel="stylesheet" href="/static/css/site.css"/>{% block additional_css %}{% endblock %}
</head>
<body>
<div class="topnav">
<a href="/">Home</a>
<a href="/accounts/register" style="float: right"> Register</a>
<a href="/accounts/login" style="float: right">Login</a>
<a href="/accounts/logout" style="float: right">Logout</a>
<a href="/accounts/profile" style="float: right">Profile</a>
</div>
<div class="main_content">
{% block main_content %}{% endblock %}
</div>
<footer></footer><script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"
integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"
crossorigin="anonymous"></script>
<script
src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"
integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"
crossorigin="anonymous"></script>
</body>
</html>

Now create the home page in BasicApp/templates/home/index.html

{% extends "shared/_layout.html" %}
{% block title %}Home{% endblock %}
{% block main_content %}
<div class="page-title">
<h1>Home</h1>
</div>
{% endblock %}

Now we need to edit app.py and create our first route to our homepage.

Open app.py in the editor. We need to import a couple modules to make sure everything works.

from flask import Flask, render_template, redirect, session, url_for, flash, request
from data.db_session import db_auth
from services.accounts_service import create_user, login_user, get_profile
import os

We need to make sure that we have our first route setup for the index.html page. Add the following info to app.py

App = Flask(__name__)@app.route('/')
def index():
return render_template("home/index.html")

if __name__ == '__main__':
app.run(debug=True)

At this point, you can run your app and see if everything is working.

Time to create a registration page, the user Class, and the functions that will connect to the Neo4j database to create the user.

Add the following to app.py

You will probably notice that I do something slightly different. I separate my GET and POST methods into separate routes. I learned this in a Python course from Michael Kennedy. The idea is to separate the two for a little extra security.

To help you understand what each of the functions, I added comments below to describe things a little more in depth for you.

@app.route('/accounts/register', methods=['GET'])
def register_get():
return render_template("accounts/register.html")
@app.route('/accounts/register', methods=['POST'])
def register_post():
# Get the form data from register.html
name = request.form.get('name')
email = request.form.get('email').lower().strip()
company = request.form.get('company').strip()
password = request.form.get('password').strip()
confirm = request.form.get('confirm').strip()
# Check for blank fields in the registration form
if not name or not email or not company or not password or not confirm:
flash("Please populate all the registration fields", "error")
return render_template("accounts/register.html", name=name, email=email, company=company, password=password, confirm=confirm)
# Check if password and confirm match
if password != confirm:
flash("Passwords do not match")
return render_template("accounts/register.html", name=name, email=email, company=company)
# Create the user
user = create_user(name, email, company, password)
# Verify another user with the same email does not exist
if not user:
flash("A user with that email already exists.")
return render_template("accounts/register.html", name=name, email=email, company=company)
return render_template("accounts/register.html")

Create BasicApp/services/classes.py

from py2neo.ogm import GraphObject, Propertyclass User(GraphObject):
__primarylabel__ = "user"
__primarykey__ = "email"
name = Property()
email = Property()
company = Property()
password = Property()
hashed_password = Property()

Create BasicApp/services/accounts_service.py

from data.db_session import db_auth
from typing import Optional
from passlib.handlers.sha2_crypt import sha512_crypt as crypto
from services.classes import User
graph = db_auth()def find_user(email: str):
user = User.match(graph, f"{email}")
return user
def create_user(name: str, email: str, company: str, password: str) -> Optional[User]:
if find_user(email):
return None
user = User()
user.name = name
user.email = email
user.company = company
user.hashed_password = hash_text(password)
graph.create(user)
return user
def hash_text(text: str) -> str:
hashed_text = crypto.encrypt(text, rounds=171204)
return hashed_text
def verify_hash(hashed_text: str, plain_text: str) -> bool:
return crypto.verify(plain_text, hashed_text)
def login_user(email: str, password: str) -> Optional[User]:
user = User.match(graph, f"{email}").first()
if not user:
print(f"Invalid User - {email}")
return None
if not verify_hash(user.hashed_password, password):
print(f"Invalid Password for {email}")
return None
print(f"User {email} passed authentication")
return user
def get_profile(usr: str) -> Optional[User]:
# user = User.match(graph, f"{usr}").first()
user_profile = graph.run(f"MATCH (x:user) WHERE x.email='{usr}' RETURN x.name as name, x.company as company, x.email as email").data()
return user_profile

Last step for registration is the template with the registration form that will match up with the previous sections.

Create BasicApp/templates/accounts/register.html

{% extends "shared/_layout.html" %}
{% block title %}Register{% endblock %}
{% block main_content %}
<div class="main">
<div class="form-wrapper">
<form method="POST">
<h2>Register</h2>
<input class="form-control" type="text" name="name" placeholder=" Full Name" required
value="{{ name }}">
<input class="form-control" type="email" name="email" placeholder=" Email" required value="{{ email }}">
<input class="form-control" type="text" name="company" placeholder=" Company Name" required
value="{{ company }}">
<input class="form-control" type="password" name="password" placeholder=" Password" required
value="{{ password }}">
<input class="form-control" type="password" name="confirm" placeholder=" Password" required
value="{{ password }}">
<div class="actions">
<button type="submit" class="bt btn-info btn-sm">Register</button>
</div>
<div class="error-msg">
{% if error %}
<div class="error-msg">{{ error }}</div>
{% endif %}
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for msg in messages %}
<p>{{ msg }}</p>
{% endfor %}
{% endif %}
{% endwith %}
</div>
</form>
</div>
</div>
{% endblock %}

We need to add our Neo4j connection and authentication to app.py. We also need to add a secret key. We will create a login for users in the coming steps. When they login we will use a component from flask called “session”. This establishes a cookie for users so their login session is maintained as they browse from page to page in the application. To get this working, a secret_key must be created to ensure the connection has a unique key.

Update app.py

app = flask(__name__)
app.secret_key = os.urandom(24)
graph = db_auth()

Create BasicApp/templates/accounts/login.html

{% extends "shared/_layout.html" %}
{% block title %}Login{% endblock %}
{% block main_content %}
<div class="main">
<div class="form-wrapper">
<form action="" method="POST">
<h2>Login</h2>
<input name="email" type="text" placeholder=" Your email address" class="form-control"
value="{{ email }}">
<input name="password" type="password" placeholder=" Password" class="form-control"
value="{{ password }}">
<button type="submit" class="btn btn-danger">Login</button>
<div style="clear: both;"></div>
{% if error %}
<div class="error-msg">{{ error }}</div>
{% endif %}
</form>
</div>
</div>
{% endblock %}

A good way to check if a user is logged in is to present their User Profile with the information they enter at registration. This will help test a couple things.

  1. A user is authenticated
  2. A user is able to access data that is restricted to their user account only.
  3. A user session is established

Create BasicApp/templates/accounts/index.html

{% extends "shared/_layout.html" %}
{% block title %}Home{% endblock %}
{% block main_content %}
<div class="main">
<div class="form-wrapper">
<form method="post">
<h2>Profile</h2>
{% for info in user_profile %}
<input class="form-control" type="text" name="name" placeholder=" Full Name" value="{{ info.name }}">
<input class="form-control" type="text" name="email" placeholder=" email" value="{{ info.email }}">
<input class="form-control" type="text" name="company" placeholder=" Company Name" value="{{ info.company }}">
{% endfor %}
<div class="actions">
<button type="submit" class="bt btn-info btn-sm">Save</button>
</div>
</form>
</div>
</div>
{% endblock %}

Now that the login page is created, we need to add a route for it.

Edit app.py and add the following for user login

@app.route('/accounts/login', methods=['GET'])
def login_get():
# Check if the user is already logged in. if yes, redirect to profile page.
if "usr" in session:
return redirect(url_for("profile_get"))
else:
return render_template("accounts/login.html")
@app.route('/accounts/login', methods=['POST'])
def login_post():
# Get the form data from login.html
email = request.form['email']
password = request.form['password']
if not email or not password:
return render_template("accounts/login.html", email=email, password=password)
# Validate the user
user = login_user(email, password)
if not user:
flash("No account for that email address or the password is incorrect", "error")
return render_template("accounts/login.html", email=email, password=password)
# Log in user and create a user session, redirect to user profile page.
usr = request.form["email"]
session["usr"] = usr
return redirect(url_for("profile_get"))

Add the following to app.py for the user profile page.

Note: The profile page is pre-built so that you can add more to the page later on. Ideally you would add additional form fields for users to populate and save. The update piece is not finished out in this tutorial. The page will only present the user profile information that was captured during registration.

@app.route('/accounts/profile', methods=['GET'])
def profile_get():
# Make sure the user has an active session. If not, redirect to the login page.
if "usr" in session:
usr = session["usr"]
session["usr"] = usr
user_profile = get_profile(usr)
return render_template("accounts/index.html", user_profile=user_profile)
else:
return redirect(url_for("login_get"))
@app.route('/accounts/profile', methods=['POST'])
def profile_post():
# Make sure the user has an active session. If not, redirect to the login page.
if "usr" in session:
usr = session["usr"]
session["usr"] = usr
user_profile = get_profile(usr)
return render_template("accounts/index.html", user_profile=user_profile)
else:
return redirect(url_for("login_get"))

Next, we need to add the logout function of the app.
Add the following to app.py

@app.route('/accounts/logout')
def logout():
session.pop("usr", None)
flash("You have successfully been logged out.", "info")
return redirect(url_for("login_get"))

Ready to test.

Run the application and visit the page in your browser 127.0.0.1:5000

Home Page

Register a user and test error handling

Leave some of the form fields blank to make sure that error messages are prompting

Type in two different passwords to make sure that the password confirmation is working.

Finish a registration and then login. After login, you should be redirected to the profile page.

To check that your user session is working, click on ‘Home’ and then go back to ‘Profile’. This should maintain your session and present your profile information.

Now ‘Logout’ and then click ‘Profile’. This should redirect you to the login page again because you do not have an active session any longer.

Finally, register another user, but try to use the same email address as you used last time. This should create a duplicate email address error.

Final item to look at.

When you are logged into Neo4j from your browser you can look at the user that was created.

You can check that user passwords are hashed for security purposes.

Hopefully this gets you down the road to creating a new and exciting app with Flask and Neo4j. I will have a lot more to share as I continue my projects

Enjoy!

--

--

Josh-T
The Startup

Cyber security expert who has suddenly fallen in love with learning Python.