DEV Community

Cover image for Adding authentication to a Flask application
Faruq Abdulsalam
Faruq Abdulsalam

Posted on • Updated on

Adding authentication to a Flask application

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

models.py

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))
Enter fullscreen mode Exit fullscreen mode

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.py

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.')

Enter fullscreen mode Exit fullscreen mode

You need to install the package that'll validate the email address submitted by the user.

pip install email_validator
Enter fullscreen mode Exit fullscreen mode

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.py

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'))
Enter fullscreen mode Exit fullscreen mode

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';">&times;</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 %}

Enter fullscreen mode Exit fullscreen mode

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 %}

Enter fullscreen mode Exit fullscreen mode

init.py

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
Enter fullscreen mode Exit fullscreen mode

The path to the login view function is assigned to the initialised LoginManager class and the auth blueprint is registered with the application.

models.py (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'))
Enter fullscreen mode Exit fullscreen mode

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.
ValueError

or

Alter error

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.py

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

Enter fullscreen mode Exit fullscreen mode

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.py

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)

Enter fullscreen mode Exit fullscreen mode

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 👇,
finished application
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.

Cheers

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 👋

Discussion (0)