User authentification with Flask-Login



One important step of project preparation is Security. We plan to publish our app to Heroku where it will be accessible by anyone in the world. Let's make it more secure with Flask-Login (https://flask-login.readthedocs.io/en/latest/).

There is one post I will follow.
https://www.digitalocean.com/community/tutorials/how-to-add-authentication-to-your-app-with-flask-login

But I will simplify it and will use login flow only at this moment as we don't need registration. But for new user creation I will add cli script.

To add auth flow to our app we need to create Login View with Controller and some additional service methods for Flask-Login. Also we have to update User model a little bit.

backend/models/user.py
from models import db
from flask_login import UserMixin


class User(UserMixin, db.Model):

    __tablename__ = 'users'

    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(255), unique=True, nullable=False)
    email = db.Column(db.String(255), unique=True, nullable=False)
    password = db.Column(db.String(255), nullable=False)

You can see we added UserMixin to the User model

backend/controllers/auth.py
import click
from flask import Blueprint, request, render_template, redirect, url_for, flash
from flask_login import current_user, login_user, LoginManager, login_required, logout_user
from werkzeug.security import check_password_hash, generate_password_hash
from models import User, db


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

login_manager = LoginManager()
login_manager.login_view = 'auth.login'


@auth.route('/login', methods=['GET', 'POST'])
def login():

    if current_user.is_authenticated:
        return redirect(url_for('default.home'))

    if request.method == 'POST':
        email = request.form.get('email')
        password = request.form.get('password')
        remember = True if request.form.get('remember') else False

        user = User.query.filter_by(email=email).first()

        if not user or not check_password_hash(user.password, password):
            flash('Please check your credentials and try again.')
            return redirect(url_for('auth.login'))

        login_user(user, remember=remember)
        return redirect(url_for('default.home'))

    return render_template('auth/login.html')


@auth.route('/logout')
@login_required
def logout():
    logout_user()
    return redirect(url_for('default.home'))


@login_manager.user_loader
def load_user(user_id):
    """Check if user is logged-in on every page load."""
    if user_id is not None:
        return User.query.get(int(user_id))
    return None


@login_manager.unauthorized_handler
def unauthorized():
    """Redirect unauthorized users to Login page."""
    flash('You must be logged in to view that page.')
    return redirect(url_for('auth.login'))


@auth.cli.command("create-user")
@click.argument("username")
@click.argument("email")
@click.argument("password")
def create_user_cli(username, email, password):

    # EXAMPLE: flask auth create-user dmn1 dmn1@dmn1.com 1111

    new_user = User(username=username, email=email, password=generate_password_hash(password, method='sha256'))
    db.session.add(new_user)
    db.session.commit()
    print(new_user)


Here we have two request handlers (login and logout), special methods for Flask-Login (load_user, unauthorized) and cli command handler to create new user (create_user_cli).

To create new user just use cli command
flask auth create-user <username> <email> <password>
for example
flask auth create-user testuser testuser@test.com udr0vtFQbdGSS3l4len

And last new file is very simple login template

./backend/templates/auth/login.html
<div>
    <h3>Login</h3>
    <div>
        {% with messages = get_flashed_messages() %}
            {% if messages %}
                <div class="notification is-danger">
                    {{ messages[0] }}
                </div>
            {% endif %}
        {% endwith %}
        <form method="POST" action="">
            <div>
                <label>Email</label>
                <input type="email" name="email" placeholder="Your Email" />
            </div>

            <div>
                <label>Password</label>
                <input type="password" name="password" placeholder="Your Password" />
            </div>

            <div>
                <label class="checkbox">
                    <input type="checkbox">
                    Remember me
                </label>
            </div>

            <button>Login</button>

        </form>
    </div>
</div>

Few additional steps to make it work:

- add auth blueprint controller initialization to ./backend/app.py
...
from controllers.auth import auth as auth_blueprint  # NOQA
app.register_blueprint(auth_blueprint)
...

- add login_required to the home controller in ./backend/controllers/default.py
@default.route('/')
@login_required
def home():

    is_development_mode = os.environ.get('FLASK_ENV', 'production') == 'development'
    if is_development_mode:
        ...

Now all unauthorized users will be redirected to /auth/login page and if user provides valid credentials he will be able to use our angular frontend app.

Next time I will describe how to create base template with static header and required links from auth flow and also will add SLDS (https://www.lightningdesignsystem.com/) to make our app looks much better.



Comments

Popular posts from this blog

HTTPS in local environment for Angular + Flask project.

Salesforce Authentication 2 (Token Validation and Refresh)