Adding authentication to a Flask application
Tags: python, programming, webdev, tutorial
Published: 2021-11-23 09:38:54

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`:
```javascript
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.
```javascript
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.

```javascript
from flask import Blueprint

auth = Blueprint('auth', __name__, template_folder='templates')

from . import views
```
###models.py
```javascript
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.py
```javascript
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.
```javascript
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.py
```javascript
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
```javascript
{% 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 %}

```
###register.html
```javascript
{% 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__.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.
```javascript
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.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`.
```javascript
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.
![ValueError](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0dwk5840l92swxepafbg.png)
###or
![Alter error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j3grroeav3vuxodiaadd.png)

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](https://github.com/ashishnitinpatil/udacity_fsnd004_item_catalog_application/commit/c0adae86653e5e92c6807e7c3f8eaa114a022b3c)
###__init__.py
```javascript
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.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.
```javascript
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 👇, 
![finished application](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7crb5wumfi6y35o0766g.png)
head to [Github](https://github.com/Faruqt/Flask-Complete-Tutorial/tree/master), 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/akffaoe9mgf4v7atebgi.gif)

If you have any questions, feel free to drop them as a comment or send me a message on [Linkedin](https://www.linkedin.com/in/faruq-abdulsalam) or [Twitter](https://twitter.com/Ace_II) and I'll ensure I respond as quickly as I can. Ciao 👋