Python Flask Authentication Part #01
Tags: python, flask, webdev, beginners
Published: 2022-08-14 10:08:00

Hello.! I hope you are doing great. In the last post we setup our application frontend workflow and in this one we are setting the authentication pages and authentication validations.

For the email validation Flask-WTF depends on <code>email-validator</code> so we have to install it for further doing so you can install it by pip.

```shell
pip install email-validator
```

We need two forms one for login and one for register so we have to create two files <code>login_form.py</code> and <code>register_form.py</code> in <code>applictaion/forms</code>.

**login_form.py**
```python
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField
from wtforms.validators import InputRequired, DataRequired


class LoginForm(FlaskForm):
    username = StringField(
        'username',
        validators=[InputRequired(), DataRequired()],
        render_kw={
            'class': 'input input-bordered w-full focus:outline-2 focus:outline-blue-700',
            'placeholder': 'Username'
        }
    )
    password = PasswordField(
        'password',
        validators=[InputRequired(), DataRequired()],
        render_kw={
            'class': 'input input-bordered w-full focus:outline-2 focus:outline-blue-700',
            'placeholder': 'Password'
        }
    )

```

**register_form.py**
```python
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, EmailField
from wtforms.validators import InputRequired, DataRequired, Email, EqualTo, Length, Regexp
from application.validators.username_exists import UsernameExists
from application.validators.email_exists import EmailExists


class RegisterForm(FlaskForm):
    name = StringField(
        'name',
        validators=[InputRequired(), DataRequired()],
        render_kw={
            'class': 'input input-bordered w-full focus:outline-2 focus:outline-blue-700',
            'placeholder': 'Name'
        }
    )
    username = StringField(
        'username',
        validators=[InputRequired(), DataRequired(), Regexp('^[a-zA-Z_0-9]\w+$', message="Only alphabets, numbers and _ are allowed."), UsernameExists()],
        render_kw={
            'class': 'input input-bordered w-full focus:outline-2 focus:outline-blue-700',
            'placeholder': 'Username'
        }
    )
    email = EmailField(
        'email',
        validators=[InputRequired(), DataRequired(), Email(), EmailExists()],
        render_kw={
            'class': 'input input-bordered w-full focus:outline-2 focus:outline-blue-700',
            'placeholder': 'Email'
        }
    )
    password = PasswordField(
        'password',
        validators=[InputRequired(), DataRequired(), Length(min=8), EqualTo('password_confirmation', message='Password should be match with confirm field.')],
        render_kw={
            'class': 'input input-bordered w-full focus:outline-2 focus:outline-blue-700',
            'placeholder': 'Password'
        }
    )
    password_confirmation = PasswordField(
        'password_confirmation',
        validators=[InputRequired(), DataRequired()],
        render_kw={
            'class': 'input input-bordered w-full focus:outline-2 focus:outline-blue-700',
            'placeholder': 'Confirm Password'
        }
    )

```

We need two WTForms custom validators for our user registration one for the <code>username</code> and other one for <code>email</code> to check both fields are already exists in the DB. Create a folder <code>validators</code> in <code>application</code> and two files in <code>validators</code> folder <code>username_exists.py</code> and <code>email_exists.py</code>

**email_exists.py**
```python
from application.models.user import User
from wtforms.validators import ValidationError


class EmailExists:

    def __init__(self, model=User, exclude=None, message=None):
        self.model = model
        self.exclude = exclude
        if not message:
            message = "Email is already in use."
        self.message = message

    def __call__(self, form, field):
        user = self.model.query.filter_by(email=field.data)
        if not self.exclude:
            user.filter_by(id=self.exclude)
        if user.first():
            raise ValidationError(self.message)

```

**username_exists.py**
```python
from application.models.user import User
from wtforms.validators import ValidationError


class UsernameExists:

    def __init__(self, model=User, exclude=None, message=None):
        self.model = model
        self.exclude = exclude
        if not message:
            message = "Username is already taken"
        self.message = message

    def __call__(self, form, field):
        user = self.model.query.filter_by(username=field.data)
        if not self.exclude:
            user.filter_by(id=self.exclude)
        if user.first():
            raise ValidationError(self.message)

```

Create a file <code>auth.py</code> for an auth controller in <code>application/controllers</code>

**auth.py**
```python
from flask import Blueprint, render_template, request
from application.forms.login_form import LoginForm
from application.forms.register_form import RegisterForm
from application.helpers.general_helper import form_errors, is_ajax

controller = Blueprint('auth', __name__, url_prefix='/auth')


@controller.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if request.method == 'POST' and is_ajax(request):
        if form.validate_on_submit():
            pass
        else:
            return {
                'error': True,
                'form': True,
                'messages': form_errors(form)
            }
    return render_template('pages/auth/login.jinja2', form=form)


@controller.route('/register', methods=['GET', 'POST'])
def register():
    form = RegisterForm()
    if request.method == 'POST' and is_ajax(request):
        if form.validate_on_submit():
            pass
        else:
            return {
                'error': True,
                'form': True,
                'messages': form_errors(form)
            }
    return render_template("pages/auth/register.jinja2", form=form)
```

Now we need a helper function which return only first error of the field from an errors array of the each fields.

Create a folder <code>helpers</code> in <code>applictaion</code> folder and add a file <code>general_helper.py</code> in the <code>helpers</code> folder and <code>form_error</code> and <code>is_ajax</code> functions into this file. <code>is_ajax</code> function will check this request is ajax or not.

**general_helper.py**
```python

def form_errors(form):
    errors = {}
    for error in form.errors:
        errors[error] = form.errors.get(error)[0]
    return errors


def is_ajax(request):
    return request.headers.get('X-Requested-With') == 'XMLHttpRequest'

```

Register <code>Auth Blueprint</code> in <code>application/settings.py</code>

```python
def register_blueprints(app):
    from application.controllers import (
        home,
        auth
    )

    app.register_blueprint(home.controller)
    app.register_blueprint(auth.controller)
```

Cerate a folder in <code>auth</code> in <code>views/pages</code> and create two files <code>login.jinja2</code> and <code>register.jinja2</code> also create a layout file <code>auth.jinja2</code> in <code>views/layouts</code>.

**auth.jinja2**
```html
<!doctype html>
<html lang="en" class="h-full scroll-smooth bg-gray-100 antialiased">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="{{ url_for('static', filename='css/app.css') }}">
    <title>{% block title %}{% endblock %}</title>
</head>
<body>

    {% block content %}{% endblock %}

    <script src="{{ url_for('static', filename='js/app.js') }}"></script>
</body>
</html>
```

**login.jinja2**
```html
{% extends 'layouts/auth.jinja2' %}

{% block title %} Login {% endblock %}

{% block content %}
    <div class="min-h-screen flex justify-center items-center">
        <div class="card md:w-2/6 w-4/5 bg-base-100 shadow-xl">
            <div class="card-body">
                <form action="{{ url_for('auth.login') }}" method="post" class="ajax-form">
                    {{ form.csrf_token() }}
                    <h1 class="card-title">Login!</h1>
                    <p class="mb-6">Welcome back! Log in to your account.</p>
                    <div class="form-control mb-3 w-full">
                        <label for="username" class="label">Username</label>
                        {{ form.username }}
                        <p class="mt-2 text-sm text-red-600 username-feedback error-feedback hidden"></p>
                    </div>
                    <div class="form-control mb-3 w-full">
                        <label for="password" class="label">Password</label>
                        {{ form.password }}
                        <p class="mt-2 text-sm text-red-600 password-feedback error-feedback hidden"></p>
                    </div>
                    <div class="card-actions justify-between items-center mt-6">
                        <a href="{{ url_for('auth.register') }}">Not have an account?</a>
                        <button type="submit" class="btn btn-primary">Login</button>
                    </div>
                </form>
            </div>
        </div>
    </div>
{% endblock %}
```

**register.jinja2**
```html
{% extends 'layouts/auth.jinja2' %}

{% block title %} Register {% endblock %}

{% block content %}
    <div class="min-h-screen flex justify-center items-center">
        <div class="card md:w-2/6 w-4/5 bg-base-100 shadow-xl">
            <div class="card-body">
                <form action="{{ url_for('auth.register') }}" method="post" class="ajax-form">
                    {{ form.csrf_token() }}
                    <h1 class="card-title">Register!</h1>
                    <p class="mb-6">Welcome back! Log in to your account.</p>
                    <div class="form-control mb-3 w-full">
                        <label for="name" class="label">Name</label>
                        {{ form.name }}
                        <p class="mt-2 text-sm text-red-600 name-feedback error-feedback hidden"></p>
                    </div>
                    <div class="form-control mb-3 w-full">
                        <label for="username" class="label">Username</label>
                        {{ form.username }}
                        <p class="mt-2 text-sm text-red-600 username-feedback error-feedback hidden"></p>
                    </div>
                    <div class="form-control mb-3 w-full">
                        <label for="email" class="label">Email</label>
                        {{ form.email }}
                        <p class="mt-2 text-sm text-red-600 email-feedback error-feedback hidden"></p>
                    </div>
                    <div class="form-control mb-3 w-full">
                        <label for="password" class="label">Password</label>
                        {{ form.password }}
                        <p class="mt-2 text-sm text-red-600 password-feedback error-feedback hidden"></p>
                    </div>
                    <div class="form-control mb-3 w-full">
                        <label for="password_confirmation" class="label">Confirm Password</label>
                        {{ form.password_confirmation }}
                        <p class="mt-2 text-sm text-red-600 password_confirmation-feedback error-feedback hidden"></p>
                    </div>
                    <div class="card-actions justify-between items-center mt-6">
                        <a href="{{ url_for('auth.login') }}">Already have an account?</a>
                        <button type="submit" class="btn btn-primary">Register</button>
                    </div>
                </form>
            </div>
        </div>
    </div>
{% endblock %}
```

Open <code>tailwind.config.js</code> add <code>applictaion/forms</code> directory in the content array because we add input classes through WTF Forms so we have to tell the tailwindcss to include these classes in the final build, so our <code>tailwind.config.js</code> will look like this.

**tailwind.config.js**
```js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
      './application/views/**/*.jinja2',
      './application/assets/js/**/*.js',
      './application/forms/**/*.py',
  ],
  theme: {
    extend: {},
  },
  plugins: [
      require('@tailwindcss/typography'),
      require('daisyui')
  ],
}
```

Add the username field in the User model after adding the username field our Model will look like this.

**user.py**
```python
from application import db


class User(db.Model):

    __tablename__ = 'users'

    id = db.Column(
        db.Integer,
        primary_key=True
    )

    name = db.Column(
        db.String(255),
        nullable=False
    )

    username = db.Column(
        db.String(255),
        nullable=False,
        unique=True,
    )

    email = db.Column(
        db.String(255),
        unique=True,
        nullable=False
    )

    password = db.Column(
        db.String(255),
        nullable=False
    )

    role = db.Column(
        db.String(50),
        nullable=False,
        server_default="user"
    )

    created_at = db.Column(
        db.DateTime,
        server_default=db.func.now(),
        nullable=False
    )

    updated_at = db.Column(
        db.DateTime,
        server_default=db.func.now(),
        nullable=False
    )
```

After adding the filed please run the migration for creating new field migration.

```shell
flask db migrate -m "Add username column in users table."
```

After the migration run the migration to effect on th DB.

```shell
flask db upgrade
```

Now its time to implementing <code>AJAX</code> in our authentication forms open <code>app.js</code> in <code>src/js/app.js</code> update your file with ajax your file will look like this.

```js
window.$ = window.jQuery = require('jquery');


const spinner = `<div role="status">
    <svg aria-hidden="true" class="w-6 h-6 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/>
        <path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/>
    </svg>
    <span class="sr-only">Loading...</span>
</div>`;

// ajax form submission
$(".ajax-form").on('submit', function (e){
    e.preventDefault();
    const url = $(this).attr("action");
    const method = $(this).attr("method");
    const payload = $(this).serializeArray();
    const is_refresh = $(this).data("refresh");
    const is_redirect = $(this).data("redirect");
    let submit_btn = $(this).find("button[type=submit]");
    let form_data = new FormData(this);
    let submit_html = submit_btn.html();
    $(this).find("input, select, button, textarea").attr("disabled", true);
    $.ajax({
       url: url,
       method: method,
       data: form_data,
       processData: false,
       contentType: false,
       cache: false,
       beforeSend: () => {
           $(this).find("input, select, textarea").removeClass("input-error focus:outline-red-600").addClass('focus:outline-blue-700');
           $(this).find(".error-feedback").addClass('hidden').text('');
           submit_btn.html(spinner);
       },
       success: (data) => {
           $(this).find("input, select, button, textarea").attr("disabled", false);
           submit_btn.html(submit_html);
           if(data.error && data.form){
               let messages = data.messages;
                Object.keys(messages).forEach(function (key) {
                  $("#" + key).addClass("input-error focus:outline-red-600").removeClass('focus:outline-blue-700');
                  $("." + key + "-feedback").removeClass('hidden').text(messages[key]);
                });
           }else{

           }
       }
    });
})
```



Rebuid your assets or start watch

```shell
yarn watch
```

Run the application

```shell
python run.py
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wz98qckqyhhq4hkbgb9g.gif)

You can get the updated code on [GitHub Repo](https://github.com/MuhammadSaim/flask_tutorial_blog)



Thanks for being with me.

See you guys in next post if you have any issue while going with this post feel free to comment.