Welcome to the last part of the series. Here, you'll learn how to add authentication to your flask application. The todo application built in part 2 will be used here. So if you come across this part first, do well to check out parts 1 and 2.
Let's get started!!
Install the flask extension Flask-login
:
pip install flask-login
Next, open the__init__.py
file in the core directory. Import the Login Manager
class from the installed package and initialise the application with it.
from flask import Flask
from config import Config
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager #new line
app = Flask(__name__)
app.config.from_object(Config)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
login = LoginManager(app) #new line
auth blueprint
The authentication section will be created as a mini-application as well. So create a new directory auth
in the core directory and add the following files: __init__.py
, forms.py
, models.py
, and views.py
.
Remember that this is going to be an application on its own so you need to create a templates
folder in the auth directory as well. Create a new folder auth
in it and within it create two files login.html
and register.html
.
auth blueprint
Let's start with the __init__.py
script. This will be set up the same way as that of the task
blueprint.
from flask import Blueprint
auth = Blueprint('auth', __name__, template_folder='templates')
from . import views
models
from .. import db
from werkzeug.security import generate_password_hash, check_password_hash
from .. import login
from flask_login import UserMixin
from ..models import Todo
class User(UserMixin, db.Model):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
email = db.Column(db.String(120), index=True, unique=True)
password_hash = db.Column(db.String(128))
todo = db.relationship('Todo', backref='author', lazy='dynamic')
def __repr__(self):
return '<User {}>'.format(self.username)
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
@login.user_loader
def load_user(id):
return User.query.get(int(id))
Then set up the User model for the database using the UserMixin
class.
The todo field is initialized with db.relationship
which is like a one-to-many
relationship. The first argument Todo
passed here is the many
side of the relationship and the one
is author
. This will create an author field in every todo you create.
The relationship established here just means that there will be many
posts linked to just one
user. This will ensure that one user doesn't have access to the to-do list of another user.
The load_user function stores the id of the user so that the user can navigate to another page while logged in. If this is absent, whenever the user navigates to a new page, the user will be prompted to log in again.
forms
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField, ValidationError
from wtforms.validators import DataRequired, Length, Email, EqualTo
from .models import User
class LoginForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Length(1, 64),
Email()])
password = PasswordField('Password', validators=[DataRequired()])
remember_me = BooleanField('Remember Me')
submit = SubmitField('Sign In')
class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired()])
password2 = PasswordField(
'Repeat Password', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('Register')
def validate_username(self, username):
user = User.query.filter_by(username=username.data).first()
if user is not None:
raise ValidationError('Username already in use.')
def validate_email(self, email):
user = User.query.filter_by(email=email.data).first()
if user is not None:
raise ValidationError('Email already registered.')
You need to install the package that'll validate the email address submitted by the user.
pip install email_validator
Create the Login and Registration forms and import the User
model for validation purposes. This checks whether the email or username is already in the database and raises an error if a similar email or username is found.
views
from flask import render_template, flash, redirect, url_for, request
from flask_login import login_user, logout_user, login_required, \
current_user
from . import auth
from .forms import RegistrationForm, LoginForm
from .models import User
from .. import db
from werkzeug.urls import url_parse
@auth.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('task.tasks'))
form = RegistrationForm()
if form.validate_on_submit():
user = User(username=form.username.data.lower(), email=form.email.data.lower())
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('Congratulations, you are now a registered user!')
return redirect(url_for('auth.login'))
return render_template('auth/register.html', title='Register', form=form)
@auth.route('/login', methods=['GET', 'POST'])
def login():
nologin = False
if current_user.is_authenticated:
return redirect(url_for('task.tasks'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data.lower()).first()
if user is None or not user.check_password(form.password.data):
nologin = True
else:
login_user(user, remember=form.remember_me.data)
next_page = request.args.get('next')
if not next_page or url_parse(next_page).netloc != '':
next_page = url_for('task.tasks')
return redirect(next_page)
return render_template('auth/login.html', title='Sign In', form=form, message=nologin)
@auth.route('/logout')
def logout():
logout_user()
return redirect(url_for('index'))
Let's go through each function:
i) Register: if the user navigates to the \register
URL, the register function is executed and the first condition provided checks if the user is already logged in. Then the user gets redirected to the index page of the application if this evaluates to true
. Else, the register page is loaded and the form is rendered. Upon submission, the form is validated and the user data is stored. Next, the user is redirected to the login page.
ii)Login: if the user navigates to the \login
URL, the login function is executed and a similar process is repeated here.
iii)Logout: When the user clicks on the logout button and is redirected to the logout
URL, the user gets logged out.
The HTML template files are written in the same format as the other template files in parts 1 and 2 so they are self-explanatory
login html
{% extends "base.html" %}
{% block content %}
<a class="brand-logo" href="{{ url_for('index') }}">
<img class="logo" src="{{ url_for('static', filename='Logo.svg') }}">
<div class="brand-logo-name"><strong> ToDo </strong> </div>
</a>
<!-- Display login error message-->
{% if message %}
<div class="alert alert-warning" role="alert">
<span class="closebtns" onclick="this.parentElement.style.display='none';">×</span>Invalid username or password
</div>
{% endif %}
<div class="login">
<form action="" method="post" novalidate class="p-3 border border-2">
{{ form.hidden_tag() }}
<div class="Login-Header">
<h4 class="mb-5">Login</h4>
</div>
<p>
{{ form.email.label }}<br>
{{ form.email(size=32) }}
{% for error in form.email.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password.label }}<br>
{{ form.password(size=32) }}
{% for error in form.password.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
<div class="loginbtn">
{{ form.submit(class="btn btn-primary mt-3") }}
</div>
</form>
<div class=logged_in>
<span>Dont have an account yet?</span>
<a href="{{ url_for('auth.register') }}"> <i class="fa fa-hand-o-right" aria-hidden="true"></i>Register</a>
</div>
</div>
{% endblock %}
register html
{% extends "base.html" %}
{% block content %}
<a class="brand-logo" href="{{ url_for('index') }}">
<img class="logo" src="{{ url_for('static', filename='Logo.svg') }}">
<div class="brand-logo-name"><strong> ToDo </strong> </div>
</a>
<div class="register">
<form action="" method="post" class="p-3 border border-2">
{{ form.hidden_tag() }}
<div class="Register-Header">
<h4 class="has-text-centered mb-5 is-size-3">Register</h4>
</div>
<p>
{{ form.username.label(class="label") }}<br>
{{ form.username(size=32) }}<br>
{% for error in form.username.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.email.label(class="label") }}<br>
{{ form.email(size=64) }}<br>
{% for error in form.email.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password.label(class="label") }}<br>
{{ form.password(size=32) }}<br>
{% for error in form.password.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password2.label(class="label") }}<br>
{{ form.password2(size=32) }}<br>
{% for error in form.password2.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<div class="registerbtn">
{{ form.submit(class="btn btn-primary mt-3") }}
</div>
</form>
<div class="registered">
<span>Already registered?</span>
<a href="{{ url_for('auth.login') }}"><i class="fa fa-hand-o-right" aria-hidden="true"></i>Login</a>
</div>
</div>
{% endblock %}
init
Finally, you need to register the auth
blueprint in the __init__.py
file in the core directory. Add the following lines of code above the task
blueprint.
from flask import Flask
from config import Configuration
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
app = Flask(__name__)
app.config.from_object(Configuration)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
login = LoginManager(app)
login.login_view = 'auth.login' #new line
# blueprint for auth routes in our app #new blueprint
from .auth import auth as auth_blueprint
app.register_blueprint(auth_blueprint)
# blueprint for non-authentication parts of the app
from .task import task as task_blueprint
app.register_blueprint(task_blueprint)
from core import views, models
The path to the login view function is assigned to the initialised LoginManager
class and the auth
blueprint is registered with the application.
models (base)
Since you already established a relationship between the User model and Todo model. You need to head to the models.py
file in the core directory and create the user_id
field that'll be linked to the User model via a ForeignKey
.
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
But the issue now is that SQLite database does not support dropping or altering columns. When you try to migrate and upgrade the db you get either a naming convention
or ALTER of constraints
error.
or
There are two ways you can solve this.
i) Delete the migrations folder and also the db file in your root directory. This is not advisable if you already have a lot of data in your db.
ii) Create a naming convention for all your database columns in the __init__.py
file in the core directory. Solution can be found here
init
from flask import Flask
from config import Configuration
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
from sqlalchemy import MetaData #new line
app = Flask(__name__)
app.config.from_object(Configuration)
db = SQLAlchemy(app)
#new line
naming_convention = {
"ix": 'ix_%(column_0_label)s',
"uq": "uq_%(table_name)s_%(column_0_name)s",
"ck": "ck_%(table_name)s_%(column_0_name)s",
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
"pk": "pk_%(table_name)s",
}
db = SQLAlchemy(metadata=MetaData(naming_convention=naming_convention))
migrate = Migrate(app, db, render_as_batch=True) #new line
login = LoginManager(app)
login.login_view = 'auth.login'
# blueprint for auth routes in our app
from .auth import auth as auth_blueprint
app.register_blueprint(auth_blueprint)
# blueprint for non-authentication parts of the app
from .task import task as task_blueprint
app.register_blueprint(task_blueprint)
from core import views, models
Now run the following commands: flask db stamp head
, flask db migrate
and flask db upgrade
to migrate all the changes to your db. The naming convention error should no longer exist.
views
The final step is to make changes to the task view function so that users can only view the page if they are logged in. Make the following changes to the views.py
file in the task directory.
from flask import render_template, flash, redirect, url_for, request
from flask_login import login_required, current_user #new line
from .models import Category
from ..models import Todo
from . import task
from .forms import TaskForm
from .. import db
from datetime import datetime
@task.route('/create-task', methods=['GET', 'POST'])
@login_required #new line
def tasks():
check= None
user = current_user #new line
todo= Todo.query.filter_by(author=user) #new line
date= datetime.now()
now= date.strftime("%Y-%m-%d")
form= TaskForm()
form.category.choices =[(category.id, category.name) for category in Category.query.all()]
if request.method == "POST":
if request.form.get('taskDelete') is not None:
deleteTask = request.form.get('checkedbox')
if deleteTask is not None:
todo = Todo.query.filter_by(id=int(deleteTask)).one()
db.session.delete(todo)
db.session.commit()
return redirect(url_for('task.tasks'))
else:
check = 'Please check-box of task to be deleted'
elif form.validate_on_submit():
selected= form.category.data
category= Category.query.get(selected)
todo = Todo(title=form.title.data, date=form.date.data, time= form.time.data, category= category.name, author=user) #new line
db.session.add(todo)
db.session.commit()
flash('Congratulations, you just added a new note')
return redirect(url_for('task.tasks'))
return render_template('task/tasks.html', title='Create Tasks', form=form, todo=todo, DateNow=now, check=check)
Import the login_required
function and also the current_user
variable. Then assign the login_required
function as a decorator to the task
view function. The current logged in user is obtained via the current_user variable imported from the flask_login
package.
The user variable is used to filter the Todo List
in the database for todos
created by the particular logged in user and it is also assigned to each todo created by the user.
You can see the authentication feature that was just added in action by running the application. Try to navigate to the \create-task
and you'll get redirected to the login page.
Register as a new user and log in to the application. Once you are successfully logged in, you'll automatically get redirected to the create-task
page. If you try to navigate to the /login
or /register
page while still logged in, you still get redirected to the create-task
page.
You have successfully learnt how to add authentication to your application. With what you learnt in this series, you have all the ammunition required to build a great application now.
If you want to add styling to your application to make it look like this 👇, head to Github, clone the repository, and make the necessary changes to your static and template files. Good luck!!
Congratulations!! We have come to the end of the journey. I hope you enjoyed the ride.
If you have any questions, feel free to drop them as a comment or send me a message on Linkedin or Twitter and I'll ensure I respond as quickly as I can. Ciao 👋