Authenticating in Flask

by Alex
Authenticating in Flask

Authentication is one of the most important elements of web applications. This process prevents unauthorized users from reaching unintended pages. You can create your own authentication system using cookies and password hashing. This miniature project will be an excellent test of your skills. As you might have guessed, there is already an extension that can make life a lot easier. Flask-Login is an extension that allows you to easily integrate an authentication system into your Flask application. You can install it using the following command:

(env) [email protected]:~/flask_app$ pip install flask-login

Create a user model

Right now, information about users who are site administrators or editors is not stored anywhere. The first task is to create a User model to store user data. Let’s open main2.py to add a User model after the Employee model:

#..
class User(db.Model):
    __tablename__ = 'users'
   id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(100))
    username = db.Column(db.String(50), nullable=False, unique=True)
    email = db.Column(db.String(100), nullable=False, unique=True)
    password_hash = db.Column(db.String(100), nullable=False)
    created_on = db.Column(db.DateTime(), default=datetime.utcnow)
    updated_on = db.Column(db.DateTime(), default=datetime.utcnow, onupdate=datetime.utcnow)

   def __repr__(self):
	return "<{}:{}>".format(self.id, self.username)
#...

To update the database, we need to create a new migration. In the terminal to create a new migration script you must enter the following command:

(env) [email protected]:~/flask_app$ python main2.py db migrate -m "Adding users table"

Run the migration using the upgrade command:

(env) [email protected]:~/flask_app$ python main2.py db upgrade
INFO [alembic.runtime.migration] Context impl MySQLImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running upgrade 6e059688f04e -> 0f0002bf91cc,
Adding users table

(env) [email protected]:~/flask_app$

This will create a users table in the database.

Hashing passwords

Passwords should never be stored in plain text in a database. If this is done, an intruder who is able to hack the database will be able to find out both passwords and email addresses. People have been known to use the same password for different sites, which means that one combination will give an attacker access to other user accounts. Instead of storing passwords directly in a database, you should store their hashes. A hash is a string of characters that look like they were picked at random.

pbkdf2:sha256:50000$Otfe3YgZ$4fc9f1d2de2b6beb0b888278f21a8c0777e8ff980016e043f3eacea9f48f6dea

A hash is created with a one-way hashing function. It takes the length of a variable and returns a fixed length output, which we call a hash. What makes a hash safe is the fact that it cannot be used to get the original string (that’s why it’s called a one-way function). Nevertheless, for the same input a one-way hash function will return the same result. Here are the processes that are involved in creating the password hash: When the user submits the password (during the login phase), you need to hash it and store the hash in the database. When the user logs on again, the function will re-create the hash and compare it to what’s stored in the database. If they match, the user will have access to the account. Otherwise, an error will occur. Flask comes with the Werkzeug package, which has two auxiliary functions for hashing passwords.

Method Description
generate_password_hash(password) Receives a password and returns a hash. By default, uses the one-way function pbkdf2 to generate the hash.
check_password_hash(password_hash, password) Takes hash and password in the clear, then compares password and password_hash. If they are the same, it returns True.

The following code demonstrates how to work with these functions:

>>>
>>> > from werkzeug.security import generate_password_hash, check_password_hash
>>>
>>> hash = generate_password_hash("secret password")
>>>
>>> hash
'pbkdf2:sha256:50000$zB51O5L3$8a43788bc902bca96e01a1eea95a650d9d5320753a2fbd16bea984215cdf97ee'
>>>
>>> check_password_hash(hash, "secret password")
True
>>>
>>> > check_password_hash(hash, "pass")
False
>>>
>>>

Note that when check_password_hash() is called with a valid password ("secret password"), it returns True, and if it is invalid, it returns False. Next, you need to update the User model, and add password hashing to it:

#...
from werkzeug.security import generate_password_hash, check_password_hash
#...

#...
class User(db.Model):
   #...
    updated_on = db.Column(db.DateTime(), default=datetime.utcnow, onupdate=datetime.utcnow)

   def __repr__(self):
	return "<{}:{}>".format(self.id, self.username)

   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)
   #...

Let’s create users to check the hashing of passwords.

(env) [email protected]:~/flask_app$python main2.py shell
>>>
>>> > from main2 import db, User
>>>
>>> u1 = User(username='spike', email='[email protected]')
>>> u1.set_password("spike")
>>>
>>> u2 = User(username='tyke', email='[email protected]')
>>> u2.set_password("tyke")
>>>
>>> db.session.add_all([u1, u2])
>>> db.session.commit()
>>>
>>> u1, u2
(<1:spike>, <2:tyke>)
>>>
>>>
>>> > u1.check_password("pass")
False
>>> > u1.check_password("spike")
True
>>>
>>> > u2.check_password("foo")
False
>>> > u2.check_password("tyke")
True
>>>
>>>

The output shows that everything is working as it should and there are now two users in the database.

Integrating Flask-Login

To run Flask-Login, we need to import the LoginManager class from the flask_login package and create a new LoginManager instance:

#...
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import LoginManager

app = Flask(__name__)
app.debug = True
app.config['SECRET_KEY'] = 'a really really really long secret key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:[email protected]/flask_app_db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['MAIL_SERVER'] = 'smtp.googlemail.com'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = '[email protected]'
app.config['MAIL_DEFAULT_SENDER'] = ' [email protected]'
app.config['MAIL_PASSWORD'] = ' password'

manager = Manager(app)
manager.add_command('db', MigrateCommand)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
mail = Mail(app)
login_manager = LoginManager(app)
#...

Flask-Login requires the addition of several methods to the User class to verify users. These methods are listed in the following table:

The method Description
is_authenticated() Returns True if the user is authenticated (i.e., logged in with the correct password). Otherwise it returns False.
is_active() Returns True if the account is not suspended.
is_anonymous() Return True for unauthenticated users.
get_id() Returns the unique identifier of the User object.

Flask-Login offers a default implementation of these methods using the UserMixin class. So, instead of defining them manually, you can set them to be inherited from the UserMixin class. Let’s open main2.py to change the User model header:

#...
from flask_login import LoginManager, UserMixin

#...
class User(db.Model, UserMixin):
    __tablename__ = 'users'
#...

All that remains is to add a callback to user_loader. The corresponding method can be added over the User model.

#...
@login_manager.user_loader
def load_user(user_id):
   return db.session.query(User).get(user_id)
#...

The function taking user_loader decorator as argument will be called with every request to the server. It loads the user from the user id into the session cookie. Flask-Login makes the loaded user available with the current_user proxy. To use current_user, it must be imported from the flask_login package. It behaves as a global variable and is available in both view functions and templates. At any given time, current_user refers to either a logged in or an anonymous user. You can distinguish between the two by using the is_authenticated attribute of the current_user proxy. For anonymous users, is_authenticated will return False. Otherwise it returns True.

Restricting viewer access

For now there is no administration panel on this site. In this tutorial it will be presented as a normal page. To prevent unauthorized users from accessing protected pages, Flask-Login has a login_required decorator. Let’s add the following code in the main2.py file right after the updating_session() view:

#...
from flask_login import LoginManager, UserMixin, login_required
#...
@app.route('/admin/')
@login_required
def admin():
   return render_template('admin.html')
#...

The login_required decorator ensures that the admin() view function is only called if the user is authorized. By default, if an anonymous user tries to access a protected page, he will get a 401 “Not authorized” error. You need to start the server and go to https://localhost:5000/login to see how it works. This will open a page like this:Authenticating in Flask Instead of showing the user a 401 error, it is better to redirect him to the login page. To do this, pass the login_view attribute of the LoginManager instance to the login() view function:

#...
migrate = Migrate(app, db)
mail = Mail(app)
login_manager = LoginManager(app)
login_manager.login_view = 'login'

class Faker(Command):
   'A command to add fake data to the tables'
#...

The login() function is now defined as follows (but it will need to be changed):

#...
@app.route('/login/', methods=['post', ' get'])
def login():
    message = ''
   if request.method == 'POST':
	print(request.form)
	username = request.form.get('username')
	password = request.form.get('password')

	if username == 'root' and password == 'pass':
	    message = "Correct username and password"
	else:
	    message = "Wrong username or password"
    
   return render_template('login.html', message=message)
#...

If you now go to https://localhost:5000/admin/, you will be redirected to the login page: Authenticating in FlaskFlask-Login also sets up a popup message when the user is redirected to the login page, but now there is no message because the login template(template/login.html) does not display any message. You have to open login.html and add the following code before the <form> tag:

#...
    {% endif %}

    {% for category, message in  get_flashed_messages(with_categories=true) %}
	<spam class="{{ category }}">{{ message }}</spam>
    {% endfor %}

    <form action="" method="post">
#...

If you go to https://localhost:5000/admin/ again , the page will display a message.Authenticating in Flask To change the content of the message, you must pass the new text to the login_message attribute of the LoginManager instance. At the same time, why not create a template for admin() view function. Let’s create a new template admin.html with the following code:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<h2>Logged in User details</h2>

<ul>
    <li>Username: {{ current_user.username }}</li>
    <li>Email: {{ current_user.email }}</li>
    <li>Created on: {{ current_user.created_on }}</li>
    <li>Updated on: {{ current_user.updated_on }}</li>
</ul>

</body>
</html>

Here we use the variable current_user to display details about the authorized user.

Create Authorization Form

Before authorizing, we will need to create a form. It will have three fields: username, password, and remember me. Let’s open forms.py to add a LoginForm class below the ContactForm class:

#...
from wtforms import StringField, SubmitField, TextAreaField, BooleanField, PasswordField
#...
#...
class LoginForm(FlaskForm):
    username = StringField("Username", validators=[DataRequired()])
    password = PasswordField("Password", validators=[DataRequired()])
    remember = BooleanField("Remember Me")
    submit = SubmitField()

User Authorization

Flask-Login provides the login_user() function for user authorization. It accepts a user object. If successful, it returns True and sets the session. Otherwise it returns False. By default, the session set by login_user() ends when the browser closes. To allow users to stay logged in for longer, you need to pass remember=True to login_user() when the user is logged in. Let’s open main2.py to change the login() view function:

#...
from forms import ContactForm, LoginForm
#...
from flask_login import LoginManager, UserMixin, login_required, login_user, current_user

#...
@app.route('/login/', methods=['post', 'get'])
def login():
    form = LoginForm()
   if form.validate_on_submit():
	user = db.session.query(User).filter(User.username == form.username.data).first()
	if user and user.check_password(form.password.data):
	    login_user(user, remember=form.remember.data)
	   return redirect(url_for('admin'))

	flash("Invalid username/password", 'error')
	return redirect(url_for('login'))
   return render_template('login.html', form=form)
#...

Next we need to update login.html to use the LoginForm() class. We need to add the following changes to the file:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>

    {% for category, message in  get_flashed_messages(with_categories=true) %}
	<spam class="{{ category }}">{{ message }}</spam>
    {% endfor %}

    <form action="" method="post">
	{{ form.csrf_token }}
	<p>
	    {{ form.username.label() }}
	    {{ form.username() }}
	    {% if form.username.errors %}
		{% for error in form.username.errors %}
		    {{ error }}
		{% endfor %}
	    {% endif %}
	</p>
	<p>
	    {{ form.password.label() }}
	    {{ form.password() }}
	    {% if form.password.errors %}
		{% for error in form.password.errors %}
		    {{ error }}
		{% endfor %}
	    {% endif %}
	</p>
	<p>
	    {{ form.remember.label() }}
	    {{ form.remember() }}
	</p>
	<p>
	    {{ form.submit() }}
	</p>
    </form>
    
</body>
</html>

Now you can login. If you go to https://localhost:5000/admin, you will be redirected to the login page.Authenticating in Flask Enter the correct username and password and press sumbit. You will be redirected to the admin page, which should look like this.Authenticating in Flask If you do not click “Remember Me” when logging in, the site will log out of the account when you close your browser. If you click, the login remains. If you enter the wrong username or password, you will be redirected to the login page with a popup message:Authenticating in Flask

Ending user sessions (logging out of accounts)

The logout_user() function in Flask-Login terminates a user’s session by removing their ID from the session. In the main2.py file you need to add the following code under login() view function:

#...
from flask_login import LoginManager, UserMixin, login_required, login_user, current_user, logout_user
#...
@app.route('/logout/')
@login_required
def logout():
    logout_user()
    flash("You have been logged out.")
   return redirect(url_for('login'))
#...

Next, you need to update the admin.html template to add a link to the logout route:

#...
<ul>
   <li> Username: {{current_user.username }}</li>
   <li> Email: {{current_user.email }}</li>
   <li> Created on: {{current_user.created_on }}</li>
   <li> Updated on: {{current_user.updated_on }} </li>
</ul>

<p><a href="{{ url_for('logout') }}">Logout</a></p>

</body>
</html>

If you go to https://localhost:5000/admin/ now (being logged in), there should be a link at the bottom of the page to log out of your account.Authenticating in Flask If you click it, you will be redirected to the login pageAuthenticating in Flask

The final touches

There is one small problem with the login page. Right now, if an authorized user goes to https://localhost:5000/login/, he will see the login page again. There is no point in showing the form to the authorized user. To solve this problem, you need to add the following changes to the login() view function:

#...
@app.route('/login/', methods=['post', 'get'])
def login():
   if current_user.is_authenticated:
	return redirect(url_for('admin'))
    form = LoginForm()
   if form.validate_on_submit():
#...

After these changes, if the authorized user goes to the login page, he will be redirected to the admin page.

Related Posts

LEAVE A COMMENT