The Flask Mega-Tutorial Part V: User Logins
Posted by
on underThis 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.
- Chapter 1: Hello, World!
- Chapter 2: Templates
- Chapter 3: Web Forms
- Chapter 4: Database
- Chapter 5: User Logins (this article)
- Chapter 6: Profile Page and Avatars
- Chapter 7: Error Handling
- Chapter 8: Followers
- Chapter 9: Pagination
- Chapter 10: Email Support
- Chapter 11: Facelift
- Chapter 12: Dates and Times
- Chapter 13: I18n and L10n
- Chapter 14: Ajax
- Chapter 15: A Better Application Structure
- Chapter 16: Full-Text Search
- Chapter 17: Deployment on Linux
- Chapter 18: Deployment on Heroku
- Chapter 19: Deployment on Docker Containers
- Chapter 20: Some JavaScript Magic
- Chapter 21: User Notifications
- Chapter 22: Background Jobs
- Chapter 23: Application Programming Interfaces (APIs)
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 isTrue
if the user has valid credentials orFalse
otherwise.is_active
: a property that isTrue
if the user's account is active orFalse
otherwise.is_anonymous
: a property that isFalse
for regular users, andTrue
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.
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.
-
#26 Miguel Grinberg said
@alex: yes, that is a valid implementation. For a login form, however, I prefer to clean everything up on a failure. You'll see later in later chapters that for other forms validation errors do not trigger a redirect, specifically for the purpose of preserving the field contents.
-
#27 chutianshu said
Hello Miguel
The validation you use in this article is server-validate, in flask ,is there any easy way to use both client & server valiate together?
-
#28 Miguel Grinberg said
@chutianshu: Some of the validators from WTForms have a client-side component as well, at least in some browsers. The DataRequired validator for example, generates client-side validation as well. If you need something more involved than that, then you'll have to implement client-side validation in addition to the server-side validation, using a JS validation framework.
-
#29 ushills said
for some reason I keep getting the following error:
pysqlite.py", line 334, in dbapi
from pysqlite2 import dbapi2 as sqlite
ModuleNotFoundError: No module named 'pysqlite2'at this stage:
u = User(username='susan', email='susan@example.com')
u.set_password('cat')
db.session.add(u)
db.session.commit()No idea why!
-
#30 Miguel Grinberg said
@ushills: I think this occurs when you are using a version of Python that was compiled without sqlite support. If you compiled your Python yourself, install the sqlite development libraries and then recompile Python.
-
#31 rohit tanwar said
Thanks for the awesome tutorial!
I am posting this same comment the third time. People who commented after me got their comments published but not me :( . If I am getting rejected please atleast send a e-mail saying so.
I want to know how can I add login with google/twitter/facebook or anything else on the microblog.
Thanks again for the tutorial :)
-
#32 Miguel Grinberg said
@rohit: Weird, I have not seen any comments from you before this one. I have blogged about OAuth authentication in the past, see https://blog.miguelgrinberg.com/post/oauth-authentication-with-flask.
-
#33 rp said
seems to me if i'm following right that by this point you'll also want a 'if not current_user.is_anonymous' on the index.html template, else you'll get 'Hello, !' displayed when the user is logged out, as current_user.username is blank.
-
#34 Miguel Grinberg said
@rp: The login_required decorator prevents the index route to be accessed by anonymous users.
-
#35 jf said
Great teaching, Miguel. Thanks a lot.
I am running this all in Pycharm and it has worked perfectly, but now Ctrl-C doesn't seem to exit the app anymore. Instead, after a very long delay, I get the following (apologies for the length):
(venv) C:\Users\jonat\PycharmProjects\untitled>flask run
<hr />
* Serving Flask app "microblog"
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
127.0.0.1 - - [18/Feb/2018 16:23:25] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [18/Feb/2018 16:23:32] "GET /logout HTTP/1.1" 302 -
127.0.0.1 - - [18/Feb/2018 16:23:32] "GET /index HTTP/1.1" 302 -
127.0.0.1 - - [18/Feb/2018 16:23:32] "GET /login?next=%2Findex HTTP/1.1" 200 -
127.0.0.1 - - [18/Feb/2018 16:23:48] "POST /login?next=%2Findex HTTP/1.1" 302 -
127.0.0.1 - - [18/Feb/2018 16:23:48] "GET /login HTTP/1.1" 200 -
127.0.0.1 - - [18/Feb/2018 16:23:51] "GET /index HTTP/1.1" 302 -
127.0.0.1 - - [18/Feb/2018 16:23:51] "GET /login?next=%2Findex HTTP/1.1" 200 -Exception happened during processing of request from ('127.0.0.1', 50036)
Traceback (most recent call last):
File "c:\python27\Lib\SocketServer.py", line 284, in _handle_request_noblock
self.process_request(request, client_address)
File "c:\python27\Lib\SocketServer.py", line 310, in process_request
self.finish_request(request, client_address)
File "c:\python27\Lib\SocketServer.py", line 323, in finish_request
self.RequestHandlerClass(request, client_address, self)
File "c:\python27\Lib\SocketServer.py", line 639, in init
Traceback (most recent call last):
File "c:\python27\Lib\runpy.py", line 162, in _run_module_as_main
"main", fname, loader, pkg_name)
File "c:\python27\Lib\runpy.py", line 72, in _run_code
exec code in run_globals
File "C:\Users\jonat\PycharmProjects\untitled\venv\Scripts\flask.exe__main__.py", line 9, in <module>
File "c:\users\jonat\pycharmprojects\untitled\venv\lib\site-packages\flask\cli.py", line 513, in main
cli.main(args=args, prog_name=name)
File "c:\users\jonat\pycharmprojects\untitled\venv\lib\site-packages\flask\cli.py", line 380, in main
return AppGroup.main(self, args, kwargs)
File "c:\users\jonat\pycharmprojects\untitled\venv\lib\site-packages\click\core.py", line 697, in main
rv = self.invoke(ctx)
File "c:\users\jonat\pycharmprojects\untitled\venv\lib\site-packages\click\core.py", line 1066, in invoke
return _process_result(sub_ctx.command.invoke(sub_ctx))
File "c:\users\jonat\pycharmprojects\untitled\venv\lib\site-packages\click\core.py", line 895, in invoke
return ctx.invoke(self.callback, ctx.params)
File "c:\users\jonat\pycharmprojects\untitled\venv\lib\site-packages\click\core.py", line 535, in invoke
return callback(args, kwargs)
File "c:\users\jonat\pycharmprojects\untitled\venv\lib\site-packages\click\decorators.py", line 64, in new_func
return ctx.invoke(f, obj, *args[1:], kwargs)
File "c:\users\jonat\pycharmprojects\untitled\venv\lib\site-packages\click\core.py", line 535, in invoke
return callback(args, *kwargs)
File "c:\users\jonat\pycharmprojects\untitled\venv\lib\site-packages\flask\cli.py", line 438, in run_command
use_debugger=debugger, threaded=with_threads)
File "c:\users\jonat\pycharmprojects\untitled\venv\lib\site-packages\werkzeug\serving.py", line 814, in run_simple
inner()
File "c:\users\jonat\pycharmprojects\untitled\venv\lib\site-packages\werkzeug\serving.py", line 777, in inner
srv.serve_forever()
File "c:\users\jonat\pycharmprojects\untitled\venv\lib\site-packages\werkzeug\serving.py", line 612, in serve_forever
HTTPServer.serve_forever(self)
File "c:\python27\Lib\SocketServer.py", line 227, in serve_forever
self._handle_request_noblock()
File "c:\python27\Lib\SocketServer.py", line 286, in _handle_request_noblock
self.handle_error(request, client_address)
File "c:\users\jonat\pycharmprojects\untitled\venv\lib\site-packages\werkzeug\serving.py", line 621, in handle_error
return HTTPServer.handle_error(self, request, client_address)
File "c:\python27\Lib\SocketServer.py", line 343, in handle_error
traceback.print_exc() # XXX But this goes to stderr!
File "c:\python27\Lib\traceback.py", line 232, in print_exc
print_exception(etype, value, tb, limit, file)
File "c:\python27\Lib\traceback.py", line 125, in print_exception
print_tb(tb, limit, file)
File "c:\python27\Lib\traceback.py", line 70, in print_tb
if line: _print(file, ' ' + line.strip())
File "c:\python27\Lib\traceback.py", line 13, in _print
file.write(str+terminator)
IOError: [Errno 0] Error -
#36 Miguel Grinberg said
@jf: Sadly, interrupting a web server is a very hard task, and it is not uncommon to see issues with that. If the same application can be interrupted correctly when you start it from a terminal window, then my guess is that this is something that needs to be investigated by Jetbrains.
-
#37 Aditya said
Hello Miguel,
When I login using correct credentials, I get a 200 OK response but still gets redirected back to login page.
Also, when using the next parameter, I am gettingCould not build url for endpoint u'/index'
error.
Any idea what I might be doing wrong? -
#38 Miguel Grinberg said
@Aditya: No, I have no way to know what's wrong. You can compare your code against mine on GitHub and find any differences.
-
#39 Carlos said
Hi Miguel. First of all congrats on this exceptional work. I love it so far.
I'm new to flask and I have had a litle touch of python, so consider me to be a complete noob.
I seem to be stuck in part 5, right before user registration. When I run the app, after inserting the user, I get an error when trying to log in with the correct User/Password.[2018-02-20 13:56:21,606] ERROR in app: Exception on /login [POST]
Traceback (most recent call last):File "c:\desenvolvimento\python\projects\flask_0\venv\lib\site-packages\flask\app.py", line 1982, in wsgi_app
response = self.full_dispatch_request()
File "c:\desenvolvimento\python\projects\flask_0\venv\lib\site-packages\flask\app.py", line 1614, in full_dispatch_request
rv = self.handle_user_exception(e)
File "c:\desenvolvimento\python\projects\flask_0\venv\lib\site-packages\flask\app.py", line 1517, in handle_user_exception
reraise(exc_type, exc_value, tb)
File "c:\desenvolvimento\python\projects\flask_0\venv\lib\site-packages\flask_compat.py", line 33, in reraise
raise value
File "c:\desenvolvimento\python\projects\flask_0\venv\lib\site-packages\flask\app.py", line 1612, in full_dispatch_request
rv = self.dispatch_request()
File "c:\desenvolvimento\python\projects\flask_0\venv\lib\site-packages\flask\app.py", line 1598, in dispatch_request
return self.view_functionsrule.endpoint
File "C:\desenvolvimento\Python\projects\flask_0\app\routes.py", line 31, in login
if user is None or not user.check_password(form.password.data):
File "C:\desenvolvimento\Python\projects\flask_0\app\models.py", line 20, in check_password
return check_password_hash(self.password_hash, password)
File "c:\desenvolvimento\python\projects\flask_0\venv\lib\site-packages\werkzeug\security.py", line 245, in check_password_hashif pwhash.count('$') < 2:
AttributeError: 'NoneType' object has no attribute 'count'
127.0.0.1 - - [20/Feb/2018 13:56:21] "POST /login HTTP/1.1" 500 -I have already compared my code to yours and everything seems ok. Can you help me please.
Thanks in advance.
Carlos
-
#40 Miguel Grinberg said
@Carlos: the error means that the first argument that you are passing to the check_password_hash() function is None instead of the user's password hash. You are trying to log in with a user that does not have a password set, or you are incorrectly registering the user passwords. I think you need to check your code against mine more carefully.
-
#41 Abhishek singh said
i am getting this error
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such table: user [SQL: 'SELECT user.id AS user_id, user.username AS user_username, user.email AS user_email, user.password_hash AS user_password_hash \nFROM user \nWHERE user.username = ?\n LIMIT ? OFFSET ?'] [parameters: ('Abhishek', 1, 0)] (Background on this error at: http://sqlalche.me/e/e3q8)
it shows "no such table"
-
#42 Miguel Grinberg said
@Abhishek: Have you followed the instructions on chapter 4 to set up and create your database?
-
#43 Mack said
@Carlos: I had a similar issue and I found that I had a mistake in my models.py. Double check to make sure you aren't missing any variables, etc. My particular issue was forgetting to include 'password'.
-
#44 James Dean said
Another thing I just wanted to help point out was that storing a publicly-salted password in the database offers two modes of security:
1) Prevention against 'rainbow table' attacks.
2) Costly in terms of brute-force decryption from the iteration count of the hash (more computational work).But a major problem with human passwords is their entropy. Even if you require users to have long password with lots of characters various (NIST) estimates put passwords around the very low 21-bit score (a 48-bit password is truly random password from a 64 character alphabet of length 8). Dictionary attacks adapted to the public information about iteration count and salt exploit this.
An additional source of security is to add a 'secret-salt' otherwise called a 'pepper' to your environment variables, and not store it in the database. Then change the code to:
def set_password(self, password):
pepper = os.environ.get('SECRET_SALT')
self.password_hash = generate_password_hash(password + pepper)def check_password(self, password): pepper = os.environ.get('SECRET_SALT') return check_password_hash(self.password_hash, password + pepper)
If an attacker only gets access to the database hashes the task of brute forcing the password from the hash is now more difficult since the human password's entropy has effectively been substantially increased by the random string.
-
#45 ogah sunday said
i got this error when i click on the Register button:
ERROR in app: Exception on /register [GET]
Traceback (most recent call last):
File "/root/ogahslab/venv/lib/python3.6/site-packages/flask/app.py", line 1982, in wsgi_app
response = self.full_dispatch_request()
File "/root/ogahslab/venv/lib/python3.6/site-packages/flask/app.py", line 1614, in full_dispatch_request
rv = self.handle_user_exception(e)
File "/root/ogahslab/venv/lib/python3.6/site-packages/flask/app.py", line 1517, in handle_user_exception
reraise(exc_type, exc_value, tb)
File "/root/ogahslab/venv/lib/python3.6/site-packages/flask/_compat.py", line 33, in reraise
raise value
File "/root/ogahslab/venv/lib/python3.6/site-packages/flask/app.py", line 1612, in full_dispatch_request
rv = self.dispatch_request()
File "/root/ogahslab/venv/lib/python3.6/site-packages/flask/app.py", line 1598, in dispatch_request
return self.view_functionsrule.endpoint
File "/root/ogahslab/app/routes.py", line 40, in register
form = RegistrationForm()
File "/root/ogahslab/venv/lib/python3.6/site-packages/wtforms/form.py", line 212, in call
return type.call(cls, args, kwargs)
File "/root/ogahslab/venv/lib/python3.6/site-packages/flask_wtf/form.py", line 88, in init
super(FlaskForm, self).init(formdata=formdata, kwargs)
File "/root/ogahslab/venv/lib/python3.6/site-packages/wtforms/form.py", line 272, in init
super(Form, self).init(self._unbound_fields, meta=meta_obj, prefix=prefix)
File "/root/ogahslab/venv/lib/python3.6/site-packages/wtforms/form.py", line 52, in init
field = meta.bind_field(self, unbound_field, options)
File "/root/ogahslab/venv/lib/python3.6/site-packages/wtforms/meta.py", line 27, in bind_field
return unbound_field.bind(form=form, options)
File "/root/ogahslab/venv/lib/python3.6/site-packages/wtforms/fields/core.py", line 350, in bind
return self.field_class(self.args, **kw)
TypeError: init() got an unexpected keyword argument 'validator'
127.0.0.1 - - [13/Mar/2018 17:33:13] "GET /register HTTP/1.1" 500 -
how can i go about it -
#46 Miguel Grinberg said
@ogah: are you using "validator" instead of "validators" in one of your fields?
-
#47 ogah sunday said
everything worked upto this point.
then i try login with unreigtered account or i try to submit registered data, i got this internal error.
please how do i go about it.127.0.0.1 - - [14/Mar/2018 10:43:30] "POST /register HTTP/1.1" 200 -
[2018-03-14 10:44:16,211] ERROR in app: Exception on /register [POST]
Traceback (most recent call last):
File "/root/ogahslab/venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1193, in _execute_context
context)
File "/root/ogahslab/venv/lib/python3.6/site-packages/sqlalchemy/engine/default.py", line 507, in do_execute
cursor.execute(statement, parameters)
sqlite3.OperationalError: no such column: user.passsword_hashThe above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/root/ogahslab/venv/lib/python3.6/site-packages/flask/app.py", line 1982, in wsgi_app
response = self.full_dispatch_request()
File "/root/ogahslab/venv/lib/python3.6/site-packages/flask/app.py", line 1614, in full_dispatch_request
rv = self.handle_user_exception(e)
File "/root/ogahslab/venv/lib/python3.6/site-packages/flask/app.py", line 1517, in handle_user_exception
reraise(exc_type, exc_value, tb)
File "/root/ogahslab/venv/lib/python3.6/site-packages/flask/_compat.py", line 33, in reraise
raise value
File "/root/ogahslab/venv/lib/python3.6/site-packages/flask/app.py", line 1612, in full_dispatch_request
rv = self.dispatch_request()
File "/root/ogahslab/venv/lib/python3.6/site-packages/flask/app.py", line 1598, in dispatch_request
return self.view_functionsrule.endpoint
File "/root/ogahslab/app/routes.py", line 50, in register
if form.validate_on_submit():
File "/root/ogahslab/venv/lib/python3.6/site-packages/flask_wtf/form.py", line 101, in validate_on_submit
return self.is_submitted() and self.validate()
File "/root/ogahslab/venv/lib/python3.6/site-packages/wtforms/form.py", line 310, in validate
return super(Form, self).validate(extra)
File "/root/ogahslab/venv/lib/python3.6/site-packages/wtforms/form.py", line 152, in validate
if not field.validate(self, extra):
File "/root/ogahslab/venv/lib/python3.6/site-packages/wtforms/fields/core.py", line 204, in validate
stop_validation = self._run_validation_chain(form, chain)
File "/root/ogahslab/venv/lib/python3.6/site-packages/wtforms/fields/core.py", line 224, in _run_validation_chain
validator(form, self)
File "/root/ogahslab/app/forms.py", line 23, in validate_username
user = User.query.filter_by(username=username.data).first()
File "/root/ogahslab/venv/lib/python3.6/site-packages/sqlalchemy/orm/query.py", line 2789, in first
ret = list(self[0:1])
File "/root/ogahslab/venv/lib/python3.6/site-packages/sqlalchemy/orm/query.py", line 2581, in getitem
return list(res)
File "/root/ogahslab/venv/lib/python3.6/site-packages/sqlalchemy/orm/query.py", line 2889, in iter
return self._execute_and_instances(context)
File "/root/ogahslab/venv/lib/python3.6/site-packages/sqlalchemy/orm/query.py", line 2912, in _execute_and_instances
result = conn.execute(querycontext.statement, self._params)
File "/root/ogahslab/venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 948, in execute
return meth(self, multiparams, params)
File "/root/ogahslab/venv/lib/python3.6/site-packages/sqlalchemy/sql/elements.py", line 269, in _execute_on_connection
return connection._execute_clauseelement(self, multiparams, params)
File "/root/ogahslab/venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1060, in _execute_clauseelement
compiled_sql, distilled_params
File "/root/ogahslab/venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1200, in _execute_context
context)
File "/root/ogahslab/venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1413, in _handle_dbapi_exception
exc_info
File "/root/ogahslab/venv/lib/python3.6/site-packages/sqlalchemy/util/compat.py", line 203, in raise_from_cause
reraise(type(exception), exception, tb=exc_tb, cause=cause)
File "/root/ogahslab/venv/lib/python3.6/site-packages/sqlalchemy/util/compat.py", line 186, in reraise
raise value.with_traceback(tb)
File "/root/ogahslab/venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1193, in _execute_context
context)
File "/root/ogahslab/venv/lib/python3.6/site-packages/sqlalchemy/engine/default.py", line 507, in do_execute
cursor.execute(statement, parameters)
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such column: user.passsword_hash [SQL: 'SELECT user.id AS user_id, user.username AS user_username, user.email AS user_email, user.passsword_hash AS user_passsword_hash \nFROM user \nWHERE user.username = ?\n LIMIT ? OFFSET ?'] [parameters: ('fghh', 1, 0)] (Background on this error at: http://sqlalche.me/e/e3q8)
127.0.0.1 - - [14/Mar/2018 10:44:16] "POST /register HTTP/1.1" 500 -
127.0.0.1 - - [14/Mar/2018 10:54:35] "GET / HTTP/1.1" 302 -
127.0.0.1 - - [14/Mar/2018 10:54:36] "GET /login?next=%2F HTTP/1.1" 200 - -
#48 Miguel Grinberg said
@ogah: the error message is fairly clear. You don't have the password_hash column defined in your users table. Did you add that column?
-
#49 Ogah said
Yes. I have it clearly defined
-
#50 Miguel Grinberg said
@Ogah: you must be wrong, because SQLAlchemy says the column isn't there. Maybe you did not execute the migration that adds that column? I can't really tell you what you did wrong, you need to review your steps because clearly you missed something.