2018-01-02T17:20:33Z

The Flask Mega-Tutorial Part V: User Logins

This is the fifth installment of the Flask Mega-Tutorial series, in which I'm going to tell you how to create a user login subsystem.

For your reference, below is a list of the articles in this series.

In Chapter 3 you learned how to create the user login form, and in Chapter 4 you learned how to work with a database. This chapter will teach you how to combine the topics from those two chapters to create a simple user login system.

The GitHub links for this chapter are: Browse, Zip, Diff.

Password Hashing

In Chapter 4 the user model was given a password_hash field, that so far is unused. The purpose of this field is to hold a hash of the user password, which will be used to verify the password entered by the user during the log in process. Password hashing is a complicated topic that should be left to security experts, but there are several easy to use libraries that implement all that logic in a way that is simple to be invoked from an application.

One of the packages that implement password hashing is Werkzeug, which you may have seen referenced in the output of pip when you install Flask, since it is one of its core dependencies. Since it is a dependency, Werkzeug is already installed in your virtual environment. The following Python shell session demonstrates how to hash a password:

>>> from werkzeug.security import generate_password_hash
>>> hash = generate_password_hash('foobar')
>>> hash
'pbkdf2:sha256:50000$vT9fkZM8$04dfa35c6476acf7e788a1b5b3c35e217c78dc04539d295f011f01f18cd2'

In this example, the password foobar is transformed into a long encoded string through a series of cryptographic operations that have no known reverse operation, which means that a person that obtains the hashed password will be unable to use it to obtain the original password. As an additional measure, if you hash the same password multiple times, you will get different results, so this makes it impossible to identify if two users have the same password by looking at their hashes.

The verification process is done with a second function from Werkzeug, as follows:

>>> from werkzeug.security import check_password_hash
>>> check_password_hash(hash, 'foobar')
True
>>> check_password_hash(hash, 'barfoo')
False

The verification function takes a password hash that was previously generated, and a password entered by the user at the time of log in. The function returns True if the password provided by the user matches the hash, or False otherwise.

The whole password hashing logic can be implemented as two new methods in the user model:

app/models.py: Password hashing and verification

from werkzeug.security import generate_password_hash, check_password_hash

# ...

class User(db.Model):
    # ...

    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)

With these two methods in place, a user object is now able to do secure password verification, without the need to ever store original passwords. Here is an example usage of these new methods:

>>> u = User(username='susan', email='susan@example.com')
>>> u.set_password('mypassword')
>>> u.check_password('anotherpassword')
False
>>> u.check_password('mypassword')
True

Introduction to Flask-Login

In this chapter I'm going to introduce you to a very popular Flask extension called Flask-Login. This extension manages the user logged-in state, so that for example users can log in to the application and then navigate to different pages while the application "remembers" that the user is logged in. It also provides the "remember me" functionality that allows users to remain logged in even after closing the browser window. To be ready for this chapter, you can start by installing Flask-Login in your virtual environment:

(venv) $ pip install flask-login

As with other extensions, Flask-Login needs to be created and initialized right after the application instance in app/__init__.py. This is how this extension is initialized:

app/__init__.py: Flask-Login initialization

# ...
from flask_login import LoginManager

app = Flask(__name__)
# ...
login = LoginManager(app)

# ...

Preparing The User Model for Flask-Login

The Flask-Login extension works with the application's user model, and expects certain properties and methods to be implemented in it. This approach is nice, because as long as these required items are added to the model, Flask-Login does not have any other requirements, so for example, it can work with user models that are based on any database system.

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 (unicode, if using Python 2).

I can implement these four easily, but since the implementations are fairly generic, Flask-Login provides a mixin class called UserMixin that includes generic implementations that are appropriate for most user model classes. Here is how the mixin class is added to the model:

app/models.py: Flask-Login user mixin class

# ...
from flask_login import UserMixin

class User(UserMixin, db.Model):
    # ...

User Loader Function

Flask-Login keeps track of the logged in user by storing its unique identifier in Flask's user session, a storage space assigned to each user who connects to the application. Each time the logged-in user navigates to a new page, Flask-Login retrieves the ID of the user from the session, and then loads that user into memory.

Because Flask-Login knows nothing about databases, it needs the application's help in loading a user. For that reason, the extension expects that the application will configure a user loader function, that can be called to load a user given the ID. This function can be added in the app/models.py module:

app/models.py: Flask-Login user loader function

from app import login
# ...

@login.user_loader
def load_user(id):
    return User.query.get(int(id))

The user loader is registered with Flask-Login with the @login.user_loader decorator. The id that Flask-Login passes to the function as an argument is going to be a string, so databases that use numeric IDs need to convert the string to integer as you see above.

Logging Users In

Let's revisit the login view function, which as you recall, implemented a fake login that just issued a flash() message. Now that the application has access to a user database and knows how to generate and verify password hashes, this view function can be completed.

app/routes.py: Login view function logic

# ...
from flask_login import current_user, login_user
from app.models import User

# ...

@app.route('/login', methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user is None or not user.check_password(form.password.data):
            flash('Invalid username or password')
            return redirect(url_for('login'))
        login_user(user, remember=form.remember_me.data)
        return redirect(url_for('index'))
    return render_template('login.html', title='Sign In', form=form)

The top two lines in the login() function deal with a weird situation. Imagine you have a user that is logged in, and the user navigates to the /login URL of your application. Clearly that is a mistake, so I want to not allow that. The current_user variable comes from Flask-Login and can be used at any time during the handling to obtain the user object that represents the client of the request. The value of this variable can be a user object from the database (which Flask-Login reads through the user loader callback I provided above), or a special anonymous user object if the user did not log in yet. Remember those properties that Flask-Login required in the user object? One of those was is_authenticated, which comes in handy to check if the user is logged in or not. When the user is already logged in, I just redirect to the index page.

In place of the flash() call that I used earlier, now I can log the user in for real. The first step is to load the user from the database. The username came with the form submission, so I can query the database with that to find the user. For this purpose I'm using the filter_by() method of the SQLAlchemy query object. The result of filter_by() is a query that only includes the objects that have a matching username. Since I know there is only going to be one or zero results, I complete the query by calling first(), which will return the user object if it exists, or None if it does not. In Chapter 4 you have seen that when you call the all() method in a query, the query executes and you get a list of all the results that match that query. The first() method is another commonly used way to execute a query, when you only need to have one result.

If I got a match for the username that was provided, I can next check if the password that also came with the form is valid. This is done by invoking the check_password() method I defined above. This will take the password hash stored with the user and determine if the password entered in the form matches the hash or not. So now I have two possible error conditions: the username can be invalid, or the password can be incorrect for the user. In either of those cases, I flash an message, and redirect back to the login prompt so that the user can try again.

If the username and password are both correct, then I call the login_user() function, which comes from Flask-Login. This function will register the user as logged in, so that means that any future pages the user navigates to will have the current_user variable set to that user.

To complete the login process, I just redirect the newly logged-in user to the index page.

Logging Users Out

I know I will also need to offer users the option to log out of the application. This can be done with Flask-Login's logout_user() function. Here is the logout view function:

app/routes.py: Logout view function

# ...
from flask_login import logout_user

# ...

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

To expose this link to users, I can make the Login link in the navigation bar automatically switch to a Logout link after the user logs in. This can be done with a conditional in the base.html template:

app/templates/base.html: Conditional login and logout links

    <div>
        Microblog:
        <a href="{{ url_for('index') }}">Home</a>
        {% if current_user.is_anonymous %}
        <a href="{{ url_for('login') }}">Login</a>
        {% else %}
        <a href="{{ url_for('logout') }}">Logout</a>
        {% endif %}
    </div>

The is_anonymous property is one of the attributes that Flask-Login adds to user objects through the UserMixin class. The current_user.is_anonymous expression is going to be True only when the user is not logged in.

Requiring Users To Login

Flask-Login provides a very useful feature that forces users to log in before they can view certain pages of the application. If a user who is not logged in tries to view a protected page, Flask-Login will automatically redirect the user to the login form, and only redirect back to the page the user wanted to view after the login process is complete.

For this feature to be implemented, Flask-Login needs to know what is the view function that handles logins. This can be added in app/__init__.py:

# ...
login = LoginManager(app)
login.login_view = 'login'

The 'login' value above is the function (or endpoint) name for the login view. In other words, the name you would use in a url_for() call to get the URL.

The way Flask-Login protects a view function against anonymous users is with a decorator called @login_required. When you add this decorator to a view function below the @app.route decorators from Flask, the function becomes protected and will not allow access to users that are not authenticated. Here is how the decorator can be applied to the index view function of the application:

app/routes.py: @login\_required decorator

from flask_login import login_required

@app.route('/')
@app.route('/index')
@login_required
def index():
    # ...

What remains is to implement the redirect back from the successful login to the page the user wanted to access. When a user that is not logged in accesses a view function protected with the @login_required decorator, the decorator is going to redirect to the login page, but it is going to include some extra information in this redirect so that the application can then return to the first page. If the user navigates to /index, for example, the @login_required decorator will intercept the request and respond with a redirect to /login, but it will add a query string argument to this URL, making the complete redirect URL /login?next=/index. The next query string argument is set to the original URL, so the application can use that to redirect back after login.

Here is a snippet of code that shows how to read and process the next query string argument:

app/routes.py: Redirect to "next" page

from flask import request
from werkzeug.urls import url_parse

@app.route('/login', methods=['GET', 'POST'])
def login():
    # ...
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user is None or not user.check_password(form.password.data):
            flash('Invalid username or password')
            return redirect(url_for('login'))
        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('index')
        return redirect(next_page)
    # ...

Right after the user is logged in by calling Flask-Login's login_user() function, the value of the next query string argument is obtained. Flask provides a request variable that contains all the information that the client sent with the request. In particular, the request.args attribute exposes the contents of the query string in a friendly dictionary format. There are actually three possible cases that need to be considered to determine where to redirect after a successful login:

  • If the login URL does not have a next argument, then the user is redirected to the index page.
  • If the login URL includes a next argument that is set to a relative path (or in other words, a URL without the domain portion), then the user is redirected to that URL.
  • If the login URL includes a next argument that is set to a full URL that includes a domain name, then the user is redirected to the index page.

The first and second cases are self-explanatory. The third case is in place to make the application more secure. An attacker could insert a URL to a malicious site in the next argument, so the application only redirects when the URL is relative, which ensures that the redirect stays within the same site as the application. To determine if the URL is relative or absolute, I parse it with Werkzeug's url_parse() function and then check if the netloc component is set or not.

Showing The Logged In User in Templates

Do you recall that way back in Chapter 2 I created a fake user to help me design the home page of the application before the user subsystem was in place? Well, the application has real users now, so I can now remove the fake user and start working with real users. Instead of the fake user I can use Flask-Login's current_user in the template:

app/templates/index.html: Pass current user to template

{% extends "base.html" %}

{% block content %}
    <h1>Hi, {{ current_user.username }}!</h1>
    {% for post in posts %}
    <div><p>{{ post.author.username }} says: <b>{{ post.body }}</b></p></div>
    {% endfor %}
{% endblock %}

And I can remove the user template argument in the view function:

app/routes.py: Do not pass user to template anymore

@app.route('/')
@app.route('/index')
@login_required
def index():
    # ...
    return render_template("index.html", title='Home Page', posts=posts)

This is a good time to test how the login and logout functionality works. Since there is still no user registration, the only way to add a user to the database is to do it via the Python shell, so run flask shell and enter the following commands to register a user:

>>> u = User(username='susan', email='susan@example.com')
>>> u.set_password('cat')
>>> db.session.add(u)
>>> db.session.commit()

If you start the application and go to the application's / or /index URLs, you will be immediately redirected to the login page, and after you log in using the credentials of the user that you added to your database, you will be returned to the original page, in which you will see a personalized greeting.

User Registration

The last piece of functionality that I'm going to build in this chapter is a registration form, so that users can register themselves through a web form. Let's begin by creating the web form class in app/forms.py:

app/forms.py: User registration form

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
from app.models import User

# ...

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('Please use a different username.')

    def validate_email(self, email):
        user = User.query.filter_by(email=email.data).first()
        if user is not None:
            raise ValidationError('Please use a different email address.')

There are a couple of interesting things in this new form related to validation. First, for the email field I've added a second validator after DataRequired, called Email. This is another stock validator that comes with WTForms that will ensure that what the user types in this field matches the structure of an email address.

The Email() validator from WTForms requires an external dependency to be installed:

(venv) $ pip install email-validator

Since this is a registration form, it is customary to ask the user to type the password two times to reduce the risk of a typo. For that reason I have password and password2 fields. The second password field uses yet another stock validator called EqualTo, which will make sure that its value is identical to the one for the first password field.

When you add any methods that match the pattern validate_<field_name>, WTForms takes those as custom validators and invokes them in addition to the stock validators. I have added two of those methods to this class for the username and email fields. In this case I want to make sure that the username and email address entered by the user are not already in the database, so these two methods issue database queries expecting there will be no results. In the event a result exists, a validation error is triggered by raising an exception of type ValidationError. The message included as the argument in the exception will be the message that will be displayed next to the field for the user to see.

To display this form on a web page, I need to have an HTML template, which I'm going to store in file app/templates/register.html. This template is constructed similarly to the one for the login form:

app/templates/register.html: Registration template

{% extends "base.html" %}

{% block content %}
    <h1>Register</h1>
    <form action="" method="post">
        {{ form.hidden_tag() }}
        <p>
            {{ form.username.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 }}<br>
            {{ form.email(size=64) }}<br>
            {% for error in form.email.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>
            {{ form.password.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 }}<br>
            {{ form.password2(size=32) }}<br>
            {% for error in form.password2.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>{{ form.submit() }}</p>
    </form>
{% endblock %}

The login form template needs a link that sends new users to the registration form, right below the form:

app/templates/login.html: Link to registration page

    <p>New User? <a href="{{ url_for('register') }}">Click to Register!</a></p>

And finally, I need to write the view function that is going to handle user registrations in app/routes.py:

app/routes.py: User registration view function

from app import db
from app.forms import RegistrationForm

# ...

@app.route('/register', methods=['GET', 'POST'])
def register():
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    form = RegistrationForm()
    if form.validate_on_submit():
        user = User(username=form.username.data, email=form.email.data)
        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('login'))
    return render_template('register.html', title='Register', form=form)

And this view function should also be mostly self-explanatory. I first make sure the user that invokes this route is not logged in. The form is handled in the same way as the one for logging in. The logic that is done inside the if validate_on_submit() conditional creates a new user with the username, email and password provided, writes it to the database, and then redirects to the login prompt so that the user can log in.

Registration Form

With these changes, users should be able to create accounts on this application, and log in and out. Make sure you try all the validation features I've added in the registration form to better understand how they work. I am going to revisit the user authentication subsystem in a future chapter to add additional functionality such as to allow the user to reset the password if forgotten. But for now, this is enough to continue building other areas of the application.

578 comments

  • #526 Miguel Grinberg said 2021-05-17T14:20:48Z

    @Gitau: you need to have a single user loader function. You also need to have unique IDs across all your users. If you have duplicate IDs between users and teachers then there is no way for you to know where to load the user from.

  • #527 Gitau Harrison said 2021-05-17T15:09:06Z

    An attempt to load a logged in student when the user loader is only the teacher's creates a 404 error. I mean, if I have only

    @login.user_loader
    def load_teacher(id):
        return Teacher.query.get(int(id))
    

    The teacher can be logged in successfully. A teacher's profile URL (which has a dynamic username) will look like this:

    localhost:5000/profile/gitau
    

    But, trying to load a logged in student by the ID when only the Teacher ID is being returned by the user_loader creates a 404 error:

    localhost:5000/profile/
    

    The student's username portion of the URL is empty meaning the student's ID from the database is not being returned hence the 404 error.

    My problem is how to load the student (in a similar manner as the teacher).

  • #528 Miguel Grinberg said 2021-05-18T11:31:24Z

    @Gitau: I explained this in my previous message. You have to have unique IDs across teachers and students. For example, you can use IDs with the format t1, t2, etc. for teachers, and s1, s2, etc. for students. In the user loader function you have to look at the id that you receive and determine if you need to load a user or a teacher and what is the numeric database id for them.

  • #529 Phil said 2021-05-21T18:40:08Z

    Hello! Thank you so much for this amazing tutorial. I'm really loving rediscovering using this stack. Can you give me a little advice? On the registration page, if I enter a previously used Username or Email, I get the correct error messages, but I am not seeing any error messages, when password and password2 don't match - it just clears the fields. Any Insights? Thanks, again!!

    from forms.py: 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('Please use a different username.')
    
    def validate_email(self, email):
        user = User.query.filter_by(email=email.data).first()
        if user is not None:
            raise ValidationError('Please use a different email address.')
    

    register.html: {% extends "base.html" %}

    {% block content %} <h1>Register</h1> <form action="" method="post"> {{ form.hidden_tag() }}

    {{ form.username.label }}<br> {{ form.username(size=32) }}<br> {% for error in form.username.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %}

    {{ form.email.label }}<br> {{ form.email(size=64) }}<br> {% for error in form.email.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %}

    {{ form.password.label }}<br> {{ form.password(size=32) }}<br> {% for error in form.password.errors %} <span style="color:red;">[{{ error }}]</span> {% endfor %}

    {{ form.password2.label }}<br> {{ form.password2(size=32) }}<br> {% for error in form.password.errors %} <span style="color:red;">[{{ error }}]</span> {% endfor %}

    {{ form.submit() }}

    </form> {% endblock %}

  • #530 Lokesh said 2021-05-29T10:20:54Z

    Hey @Miguel Grinberg. I am getting this error, please help me out, Thank you. ImportError: cannot import name 'login' from partially initialized module 'app.routes' (most likely due to a circular import)

  • #531 Miguel Grinberg said 2021-05-29T11:57:45Z

    @Lokesh: You have changed things in a way that cause a circular dependency error. I can't really help you with this. The fully working code is available from a download link at the top of this article. Get my code and compare it against yours to find the mistake.

  • #532 Vamsi said 2021-06-05T16:21:44Z

    Is it more secure to use flask-bcrypt hashing instead of werkzeug?

  • #533 Miguel Grinberg said 2021-06-05T18:50:26Z

    @Vamsi: I haven't heard of either one ever being hacked. Werkzeug uses PBKDF2 which is also highly recommended. Between bcrypt and PBKDF2 I'm not sure you will have a significance difference in terms of security, so pick the one you like best.

  • #534 Vamsi said 2021-06-12T19:07:04Z

    Couple of questions,

    1. Why does the form tag in login.html file has 'novalidate' attribute but register.html doesn't?
    2. Why are we doing the username/email existence validation within view function while attempting to login, but do similar check within RegisterForm for registrations?
  • #535 Miguel Grinberg said 2021-06-12T23:32:02Z

    @Vamsi

    1. This is explained in the web forms chapter: "Using novalidate is entirely optional, but for this first form it is important that you set it because this will allow you to test server-side validation later in this chapter."

    2. No particular reason other than teaching you different ways to validate. If you want to move the login validation logic to the form, or move the registration validation to the view function, you totally can.

  • #536 kovi201 said 2021-07-08T15:51:11Z

    Hi Miguel thx a lot again for your blog :) but not only that, i see this comments section really valuable and powerful because in fact you provide support for people .. awesome..i would have question: User loader function is called for logged in users to load them from db to memory, i suppose purpose is to get objects like current_user etc.. is that correct? or is there any other purpose to load user to memory ?.. and sub-question :) .. when there is incoming session from client, how does system knows that user is logged in or not (so that to run loader or not)?..where does this information come from.. is it from cookies which is coming from client, or is there some part of information in session memory on server side ? ..when i checked cookies i see some _fresh: True or false info, id, csfr_token, some user identifier.. so i can imagine that from cookies system can get user ID, then check in session memory if user is logged in, and then run loader ( or not run if not logged in) ..but i really dont know if its correct ..it would mean there has to be some info hidden in session memory thx

  • #537 Miguel Grinberg said 2021-07-08T18:30:35Z

    @kovi201: the user_loader function is used to set the current_user variable. The information about the logged in state is stored in the user session, which by default Flask stores in a cookie.

  • #538 Nash said 2021-07-16T17:17:12Z

    Hello, really a gold mine of articles for flask developers. I have a couple of questions:

    In the bass.html you are checking whether user has logged in using: if current_user.is_anonymous. Could you have instead used current_user.is_authenticated ?

    You have made a check for - url_parse(next_page).netloc != ''. What is the harm if user is allowed to make request by entering a different domain url in the 'next' querystring parameter?

    How is current user available to the html page when you have not passed it via the render_template?

    In the register route you are doing - form.validate_on_submit()

    I believe that the validations performed here will the the same as those defined when creating the registration form class. So effectively these are server side validation. How can I make client side validation? Is there any way to tell wtf_forms whether the validation is client side or server side?

    Thank you.

  • #539 Nash said 2021-07-16T17:19:11Z

    May I ask whether you have covered authentication via azure token, oauth and if yes, then in which section please?

  • #540 Nash said 2021-07-16T17:28:07Z

    You have applied decorator to the function as shown below:

    @login.user_loader def load_user(id): return User.query.get(int(id))

    This means the load_user is wrapped with the decorator. I have learnt that decorator is concept that is used to run some code (@login.user_loader) before and after the function code (load_user).

    Have I understood the above concept correctly?

    How does flask_login package know that this decorated function is defined in the models.py file?

  • #541 Miguel Grinberg said 2021-07-17T10:06:20Z

    @Nash:

    Could you have instead used current_user.is_authenticated ?

    Yes.

    What is the harm if user is allowed to make request by entering a different domain url in the 'next' querystring parameter?

    An attacker could send the user to another site, without the user noticing.

    How is current user available to the html page when you have not passed it via the render_template?

    The Flask-Login extension registers current_user with Jinja.

    Is there any way to tell wtf_forms whether the validation is client side or server side?

    For client-side validation you should use a client-side validation framework in JS. WTForms adds some basic hints for the browser to validate against empty fields and other simple checks, but that is it. To enable this you have to remove the novalidate attribute in your form.

    May I ask whether you have covered authentication via azure token, oauth and if yes, then in which section please?

    No, there is no Azure or OAuth in this tutorial. Sorry.

    I have learnt that decorator is concept that is used to run some code (@login.user_loader) before and after the function code (load_user).

    There is one more use case for decorators, which is to run code at the time the function is defined. This is used to register the decorated function with Flask-Login. This works in the same way as the app.route decorator from Flask.

  • #542 Alpie said 2021-08-13T19:15:23Z

    Hi Miguel,

    I am late to the party but hey, better than never, eh? My upfront thanks for this awesome tutorial.

    Now comes the question. While reading through this chapter, I thought we need to implement following four methods in the User class.

    1) is_authenticated 2) is_active 3) is_anonymous 4) get_id

    However, code snippets in here does not show these implementations. I thought, perhaps you were trying to save space, so I checked the github code base, and to my surprise User class is missing those method implementations there too.

    Am I missing something? Aren't we supposed to implement those methods by ourselves?

    [Not sure if you are still checking these questions out or will have time to answer at all. But I still want to thank you regardless]

  • #543 Miguel Grinberg said 2021-08-15T21:51:59Z

    @Alpie: the four methods are implemented in the UserMixin class from Flask-Login. Those implementations work for 99% of the situations.

  • #544 David Lorenzo said 2021-09-01T08:42:59Z

    Hi Miguel,

    Loving the content so far. I'm stuck with the register function. On file routes.py, on the line:

    user = User(username=form.username.data, email=form.email.data)

    I get a "Unexpected argument" error on each of the arguments. I've tried just copy-pasting your files just to check I wasn't mistyping anything but it doesn't seem to solve the problem. This makes the register not work at all, since I just get redirected to the register.html template (as expected).

    Many thanks in advance. David

  • #545 Miguel Grinberg said 2021-09-01T09:27:34Z

    @David: there is a download link for the working files in the introduction section of this article. Your problem is likely in the definition of the User class in the models.py file.

  • #546 Faruq Sandi said 2021-10-03T14:12:35Z

    Hi Miguel! Thanks for your tutorials. I am in chapter 8. But why current_user is not passed to render_template()?

  • #547 Miguel Grinberg said 2021-10-03T16:42:11Z

    @Faruq: Flask-Login makes current_user automatically available in all templates as part of the Jinja context, so there is no need to pass it as an argument.

  • #548 Avner said 2021-10-06T16:41:40Z

    Hi Miguel! Thank you for the thorough tutorial. Consider a case where the user signs-in as a guest without having to fill in a username and a password, say to demo a web app. Could you recommend a Flask pattern to handle such temporary guest account? - make the account unique for cases of multiple guests that are logged-in simultaneously - expire the guest account after a specified time

  • #549 Loch said 2021-10-07T06:51:07Z

    Hi Migule,

    I am getting this error, once I try to run the code. "RuntimeError: The session is unavailable because no secret key was set. Set the secret_key on the application to something unique and secret." I set the secret key very beginning of the tutorial. When I close and reopen the vscode, I am getting the error. How can I resolve this.

  • #550 Miguel Grinberg said 2021-10-07T22:46:51Z

    @Avner: You should create a real user, assign a made-up username such as guestNNNN with NNNN being a random number, and mark the user as a guest with a boolean attribute in the User model. You can then have a cron job that deletes guest user accounts that are older than a day or so.

Leave a Comment