Flask-login usermixin class with a MongoDB

10,913

Solution 1

What you need to know about Flask-login: this extension works with the application's user model, and expects certain properties and methods to be implemented in it. (source : https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-v-user-logins).

The four required items are listed below:

  • is_authenticated: a property that is True if the user has valid credentials or False otherwise.

  • is_active: a property that is True if the user's account is active or False otherwise.

  • is_anonymous: a property that is False for regular users, and True for a special, anonymous user.

  • get_id(): a method that returns a unique identifier for the user as a string

Unfortunately all the examples in the official documentation and on Miguel Grinberg's excellent blog use SQLAlchemy. Good news, it is possible to implement it with Pymongo...

THE SOLUTION

  1. routes.py

    from flask import Flask
    from flask_pymongo import PyMongo
    from flask_login import LoginManager
    from flask import render_template, url_for, request, flash
    from app.forms import Login
    from flask import request
    from werkzeug.urls import url_parse
    from werkzeug.security import generate_password_hash, check_password_hash
    from flask_login import current_user, login_user, logout_user, login_required
    
    mongo = PyMongo(app)
    login = LoginManager(app)
    login.login_view = 'login'
    
    class User:
        def __init__(self, username):
            self.username = username
    
        @staticmethod
        def is_authenticated():
            return True
    
        @staticmethod
        def is_active():
            return True
    
        @staticmethod
        def is_anonymous():
            return False
    
        def get_id(self):
            return self.username
    
        @staticmethod
        def check_password(password_hash, password):
            return check_password_hash(password_hash, password)
    
    
        @login.user_loader
        def load_user(username):
            u = mongo.db.Users.find_one({"Name": username})
            if not u:
                return None
            return User(username=u['Name'])
    
    
        @app.route('/login', methods=['GET', 'POST'])
        def login():
            if current_user.is_authenticated:
                return redirect(url_for('index'))
            form = Login()
            if form.validate_on_submit():
                user = mongo.db.Users.find_one({"Name": form.name.data})
                if user and User.check_password(user['Password'], form.password.data):
                    user_obj = User(username=user['Name'])
                    login_user(user_obj)
                    next_page = request.args.get('next')
                    if not next_page or url_parse(next_page).netloc != '':
                        next_page = url_for('index')
                    return redirect(next_page)
                else:
                    flash("Invalid username or password")
            return render_template('login.html', title='Sign In', form=form)
    
    
        @app.route('/logout')
        def logout():
            logout_user()
            return redirect(url_for('login'))
    
  2. form.py

    from flask_wtf import FlaskForm
    from wtforms import StringField, SubmitField, PasswordField
    from wtforms.validators import DataRequired
    
    
    class Login(FlaskForm):
        name = StringField('name' validators=[DataRequired()])
        password = PasswordField('Password', validators=[DataRequired()])
        login = SubmitField('Login')
    

Assuming we have, on the side of Mongodb, a collection (Users) that contains some login information. For example:

{
  Name: [username],
  Password: [hashed_password]
} 

For further explanation on what each line of code does, I recommend you to consult the following links:

Solution 2

I found the following works with flask-login, UserMixin, pymongo

Here is User model

import datetime
import uuid
from depo import bcrypt, login_manager
from flask import session, flash
from depo.common.database import Database
from depo.models.blog import Blog
from flask_login import UserMixin

class User(UserMixin):

    def __init__(self, username, email, password, _id=None):

        self.username = username
        self.email = email
        self.password = password
        self._id = uuid.uuid4().hex if _id is None else _id

    def is_authenticated(self):
        return True
    def is_active(self):
        return True
    def is_anonymous(self):
        return False
    def get_id(self):
        return self._id

    @classmethod
    def get_by_username(cls, username):
        data = Database.find_one("users", {"username": username})
        if data is not None:
            return cls(**data)

    @classmethod
    def get_by_email(cls, email):
        data = Database.find_one("users", {"email": email})
        if data is not None:
            return cls(**data)

    @classmethod
    def get_by_id(cls, _id):
        data = Database.find_one("users", {"_id": _id})
        if data is not None:
            return cls(**data)

    @staticmethod
    def login_valid(email, password):
        verify_user = User.get_by_email(email)
        if verify_user is not None:
            return bcrypt.check_password_hash(verify_user.password, password)
        return False

    @classmethod
    def register(cls, username, email, password):
        user = cls.get_by_email(email)
        if user is None:
            new_user = cls( username, email, password)
            new_user.save_to_mongo()
            session['email'] = email
            return True
        else:
            return False

    def json(self):
        return {
            "username": self.username,
            "email": self.email,
            "_id": self._id,
            "password": self.password
        }

    def save_to_mongo(self):
        Database.insert("users", self.json())

Here is routes

from flask import flash, render_template, request, session, make_response,  redirect, url_for
from depo import app, bcrypt, login_manager
from depo.models.blog import Blog
from depo.models.post import Post
from depo.models.user import User
from depo.common.database import Database
from depo.usercon.forms import RegistrationForm, LoginForm
from flask_login import login_user


@app.before_first_request
def initialize_database():
    Database.initialize()

@app.route("/register", methods=['GET', 'POST'])
def register():
    form = RegistrationForm()
    if form.validate_on_submit():

        if request.method == 'POST':
            username = request.form["username"]
            email = request.form["email"]
            password = bcrypt.generate_password_hash(request.form["password"])
                       .decode('utf-8')
            find_user =  User.get_by_email(email)
            if find_user is None:
                User.register(username, email, password)
                flash(f'Account created for {form.username.data}!', 'success')
                return redirect(url_for('home'))
            else:
                flash(f'Account already exists for {form.username.data}!', 'success')
    return render_template('register.html', title='Register', form=form)
@app.route("/login", methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        email = request.form["email"]
        password = request.form["password"]
        find_user = Database.find_one("users", {"email": email})
        if User.login_valid(email, password):
            loguser = User(find_user["_id"], find_user["email"], find_user["password"])
            login_user(loguser, remember=form.remember.data)
            flash('You have been logged in!', 'success')
            return redirect(url_for('home'))
        else:
            flash('Login Unsuccessful. Please check email and password', 'danger')
    return render_template('login.html', title='Login', form=form)

@login_manager.user_loader
def load_user(user_id):
    user =User.get_by_id(user_id)
    if user is not None:
        return User(user["_id"])
    else:
        return None

May be needed some codes for app also if already not initialized

from flask_login import LoginManager
login_manager = LoginManager(app)
Share:
10,913
Ben
Author by

Ben

Updated on June 14, 2022

Comments

  • Ben
    Ben almost 2 years

    I am working to try and build a login method for a while now. I am running a Flask app and have it working well. It all runs locally on my machine. Currently, I am using pymongo and MongoClient to make my connection to the DB. This is all working well and I would like to not change this if possible.

    I am trying to use Flask-Login to create a users class using usermixin. This is where I have been grossly unsuccessful. I have tried a few different things and my issue is how to I pull the data from my DB. I have done this previously with an SQL DB but for this project I expressly want to use MongoDB. This is the tutorial I was attempting to follow but I am having difficulty understanding everything because it is not explained well what every line is doing.

    https://medium.com/@dmitryrastorguev/basic-user-authentication-login-for-flask-using-mongoengine-and-wtforms-922e64ef87fe

    This is my connection to my DB: client = MongoClient('mongodb://localhost:27017')

    and this is my current users class that I don't have working and where I need the help.

    class User(UserMixin):
    
      def __init__(self, username, password_hash):
        self.username = username
        self.password_hash = password_hash
    
      def check_password(self, password):
        return check_password_hash(self.password_hash, password)
    
      def get_id(self):
        return self.username
    
    @login_manager.user_loader
    def load_user(user_id):
        return User.objects(pk=user_id).first()
    

    Then my last part is my login form:

    @app.route('/login', methods=["GET" , "POST"])
    def login():
      if request.method == "GET":
        return render_template("login.html", error=False)
      if request.method == "POST":
        check_user = request.form["username"]
        if check_user:
          if check_password_hash(check_user['password'], request.form["password"]):
            login_user(check_user)
            return redirect(url_for('index'))
    

    I am aware that this tutorial uses MongoEngine which I am not using, or not yet but some help here either how to get this code above to work or how to adapt it would be great. When I run this code I am not getting any errors it just doens't work. My test is I try to login and then I try to go to the logout page which is loaded with the following code:

    @app.route("/logout")
    @login_required
    def logout():
      logout_user()
      return redirect(url_for('index'))
    

    When I do it doesn't load the page and I get and Unauthorized Page notice. Thus I know that my code is not working. Lastly, I have all of templates in a static file location.

    Thanks in advance for the help and please if anything is not clear ask and I will try to add more details. The more specific the better I will be able to help.

    UPDATE:

    I realized that it is also probably important to describe how my DB is structured to make sure that I am accessing it properly because that is a major point where I am having issues. I have a DB with my collection called Users and it is structured with each document being a different user record, like this:

    {
        "_id" : 1,
        "Reset" : false,
        "FirstName" : "John",
        "LastName" : "Doe",
        "Email" : "[email protected]",
        "Username" : "",
        "admin" : false,
        "Pass" : "[hashed_password]"
    }
    {
        "_id" : 2,
        "Reset" : true,
        "FirstName" : "Jane",
        "LastName" : "Smith",
        "Email" : "[email protected]",
        "Username" : "Jane",
        "admin" : false,
        "Pass" : "[hashed_password]"
    }
    {
        "_id" : 3,
        "Reset" : true,
        "FirstName" : "Gary",
        "LastName" : "Bettman",
        "Email" : "[email protected]",
        "Username" : "HockeyGuy",
        "admin" : false,
        "Pass" : "[hashed_password]"
    }
    
  • Ben
    Ben about 5 years
    I still have a few questions, I have been looking at all of these links you provided and tried with them before posting so I am still in need of some assistance. Is there a reason that the form is in a separate py file and not just an html template, and with that why is is from app.form import Login and not just import form. Also why is everything being called out as an @staticmethod in the User Class, I've never seen this and would like to understand it. Last I think I might need to run the users call to the DB differently, see update above for how DB is structured.
  • Ben
    Ben about 5 years
    Please see the update to the original question as the DB is structured differently and I think that is part of my issue.
  • Tobin
    Tobin about 5 years
    By definition, a static method (reported by the decorator @staticmethod) is a method that does not modify the class, nor any instance of the class to which it belongs. This article explains in more detail how a static method works. The interest for me to use them here is that they only serve as utility functions that do not necessarily need to access the current class. The class in this case serves only as a "drawer" in which they are arranged.
  • Tobin
    Tobin about 5 years
    For the rest sorry but I pulled the code directly from one of my projects and I did not pay attention to imports, hence the presence of the from app.form import Login. You probably have to know the principle of modularity, the more the code is growing, the more interesting it is to split it into easy to maintain modules later. It is with this in mind that I subdivided my code into modules (a module for routes, another for forms, etc.).
  • Tobin
    Tobin about 5 years
    I urge you to browse Flask's mega tutorial, it will teach you a lot of interesting things about how to code effectively with Flask
  • Mauricio Maroto
    Mauricio Maroto over 3 years
    I think find_user["_id"] is misplaced at the loguser statement.