Many web applications require users to sign in and out in order to perform important tasks (like administration duties). In this article, we'll create an authentication system for our application.
In the previous article, we built a contact page using the Flask-WTF and Flask-Mail extensions. We'll use Flask-WTF, once again, this time to validate a user's username and password. We'll save these credentials into a database using yet another extension called Flask-SQLAlchemy.
You can find the source code for this tutorial on GitHub. While following along with this tutorial, when you see a caption, such as Checkpoint: 13_packaged_app
, it means that you can switch to the GIT branch named "13_packaged_app" and review the code at that point in the article.
Growing the Application
So far, our Flask app is a fairly simple application. It consists of mostly static pages; so, we've been able to organize it as a Python module. But now, we need to reorganize our application to make it easier to maintain and grow. The Flask documentation recommends that we reorganize the app as a Python package, so let's start there.
Our app is currently organized like this:
flaskapp/ └── app/ ├── static/ ├── templates/ ├── forms.py ├── routes.py └── README.md
To restructure it as a package, let's first create a new folder inside app/
named intro_to_flask/
. Then move static/
, templates/
, forms.py
and routes.py
into intro_to_flask/
. Also, delete any .pyc files that are hanging around.
flaskapp/ └── app/ ├── intro_to_flask/ │ ├── static/ │ ├── templates/ │ ├── forms.py │ ├── routes.py └── README.md
Next, create a new file named __init__.py
and place it inside intro_to_flask/
. This file is required to make Python treat the intro_to_flask/
folder as a package.
flaskapp/ └── app/ ├── intro_to_flask/ │ ├── __init__.py │ ├── static/ │ ├── templates/ │ ├── forms.py │ ├── routes.py └── README.md
When our app was a Python module, the application-wide imports and configurations were specified in routes.py
. Now that the app is a Python package, we'll move these settings from routes.py
into __init__.py
.
app/intro_to_flask/__init__.py
from flask import Flask app = Flask(__name__) app.secret_key = 'development key' app.config["MAIL_SERVER"] = "smtp.gmail.com" app.config["MAIL_PORT"] = 465 app.config["MAIL_USE_SSL"] = True app.config["MAIL_USERNAME"] = '[email protected]' app.config["MAIL_PASSWORD"] = 'your-password' from routes import mail mail.init_app(app) import intro_to_flask.routes
The top of routes.py
now looks like this:
app/intro_to_flask/routes.py
from intro_to_flask import app from flask import render_template, request, flash from forms import ContactForm from flask.ext.mail import Message, Mail mail = Mail() . . . # @app.route() mappings start here
We previously had app.run()
inside of routes.py
, which allowed us to type $ python routes.py
to run the application. Since the app is now organized as a package, we need to employ a different strategy. The Flask docs recommend adding a new file named runserver.py
and placing it inside app/
. Let's do that now:
flaskapp/ └── app/ ├── intro_to_flask/ │ ├── __init__.py │ ├── static/ │ ├── templates/ │ ├── forms.py │ ├── routes.py ├── runserver.py └── README.md
Now take the app.run()
call from routes.py
and place it inside of runserver.py
.
app/runserver.py
from intro_to_flask import app app.run(debug=True)
Now you can type $ python runserver.py
and view the app in the browser. From the top, here's how you'll enter your development environment and run the app:
$ cd flaskapp/ $ . bin/activate $ cd app/ $ python runserver.py
The app is now organized as a package, we're ready to move on and install a database to manage user credentials.
— Checkpoint: 13_packaged_app
—
Flask-SQLAlchemy
We'll use MySQL for our database engine and the Flask-SQLAlchemy extension to manage all of the database interaction.
Flask-SQLAlchemy uses Python objects instead of SQL statements to query the database. For example, instead of writing SELECT * FROM users WHERE firstname = "lalith"
, you would write User.query.filter_by(username="lalith").first()
.
The moral of this aside is to not completely rely on, or abandon a database abstraction layer like Flask-SQLAlchemy, but to be aware of it, so that you can determine when it's useful for your needs.
But why can't we just write raw SQL statements? What's the point of using this weird syntax? As with most things, using Flask-SQLAlchemy, or any database abstraction layer, depends on your needs and preferences. Using Flask-SQLAlchemy allows you to work with your database by writing Python code instead of SQL. This way you don't have SQL statements scattered amidst your Python code, and that's a good thing, from a code quality perspective.
Also, if implemented correctly, using Flask-SQLAlchemy will help make your application to be database-agnostic. If you start building your app on top of MySQL and then decide to switch to another database engine, you shouldn't have to rewrite massive chunks of sensitive database code. You could simply switch out Flask-SQLAlchemy with your new database abstraction layer without much of an issue. Being able to easily replace components is called modularity, and it's a sign of a well designed application.
On the other hand, it might be more intuitive and readable if you write raw SQL statements instead of learning how to translate it into Flask-SQLAlchemy's Expression Language. Fortunately, it's possible to write raw SQL statements in Flask-SQLAlchemy too, if that's what you need.
The moral of this aside is to not completely rely on, or abandon a database abstraction layer like Flask-SQLAlchemy, but to be aware of it, so that you can determine when it's useful for your needs. For the database queries in this article, I'll show you both the Expression Language version and the equivalent SQL statement.
Installing MySQL
Check to see if your system already has MySQL by running the following command in your terminal:
$ mysql --version
If you see a version number, you can skip to the "Creating a Database" section. If the command was not found, you'll need to install MySQL. With the large variety of different operating systems out there, I'll defer to Google to provide installation instructions that work for your OS. The installation usually consists of running a command or an executable. For example, the Linux command is:
$ sudo apt-get install mysql-server mysql-client
Creating a Database
Once MySQL is installed, create a database for your app called 'development
'. You can do this from a web interface like phpMyAdmin or from the command line, as shown below:
$ mysql -u username -p Enter password: mysql> CREATE DATABASE development;
Installing Flask-SQLAlchemy
Inside the isolated development environment, install Flask-SQLAlchemy.
$ pip install flask-sqlalchemy
When I tried to install Flask-SQLAlchemy, I received an error stating that the installation had failed. I searched the error and found that others had resolved the problem by installing libmysqlclient15-dev
, which installs MySQL's development files. If your Flask-SQLAlchemy installation fails, Google the error for solutions or leave a comment and we'll try to help you figure it out.
Configuring Flask-SQLAlchemy
Just as we did with Flask-Mail, we need to configure Flask-SQLAlchemy so that it knows where the development
database lives. First, create a new file named models.py
, along with adding in the following code
app/intro_to_flask/models.py
from flask.ext.sqlalchemy import SQLAlchemy db = SQLAlchemy()
Here we import the SQLAlchemy
class from Flask-SQLAlchemy (line one) and create a variable named db
, containing a usable instance of the SQLAlchemy
class (line three).
Next, open __init__.py
and add the following lines after mail.init_app(app)
and before import intro_to_flask.routes
.
app/intro_to_flask/__init__.py
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://your-username:your-password@localhost/development' from models import db db.init_app(app)
Let's go over this:
- Line one tells the Flask app to use the '
development
' database. We specify this through a data URI which follows the pattern of:mysql://username:password@server/database
. The server is 'localhost' because we're developing locally. Make sure to fill in your MySQLusername
andpassword
. -
db
, the usable instance of theSQLAlchemy
class we created inmodels.py
, still doesn't know what database to use. So we import it frommodels.py
(line three) and bind it to our app (line four), so that it also knows to use the 'development
' database. We can now query the 'development
' database through ourdb
object.
Now that our configuration is complete, let's ensure that everything works. Open routes.py
and create a temporary URL mapping so that we can perform a test query.
app/intro-to-flask/routes.py
from intro_to_flask import app from flask import Flask, render_template, request, flash, session, redirect, url_for from forms import ContactForm, SignupForm, SigninForm from flask.ext.mail import Message, Mail from models import db . . . @app.route('/testdb') def testdb(): if db.session.query("1").from_statement("SELECT 1").all(): return 'It works.' else: return 'Something is broken.'
First we import the database object (db
) from models.py
(line five). We then create a temporary URL mapping (lines 9-14) wherein we issue a test query to ensure that the Flask app is connected to the 'development
' database. Now when we visit the URL /testdb
, a test query will be issued (line 11); this is equivalent to the SQL statement SELECT 1;
. If all goes well, we'll see "It works" in the browser. Otherwise, we'll see an error message stating what went wrong.
I received an error when I visited the /testdb
URL: ImportError: No module named MySQLdb
. This meant that I didn't have the mysql-python library installed, so I tried to install it by typing the following:
$ pip install mysql-python
That installation failed, too. The new error message suggested that I first run easy_install -U distribute
and then try the mysql-python installation again. So I did, just like below:
$ easy_install -U distribute $ pip install mysql-python
This time the mysql-python installation succeeded, and then I received the "It works" success message in the browser. Now the reason I'm recounting the errors I've received and what I did to solve them is because installing and connecting to databases can be a tricky process. If you get an error message, please don't get discouraged. Google the error message or leave a comment, and we'll figure it out.
Once the test query works, delete the temporary URL mapping from routes.py
. Make sure to retain the "from models import db
"" part, because we'll need it next.
— Checkpoint: 14_db_config
—
Create a User Model
It's not a good idea to store passwords in plain text, for security reasons.
Inside the 'development
' database, we need to create a users table where we can store each user's information. The information we want to collect and store are the user's first name, last name, email, and password.
It's not a good idea to store passwords in plain text, for security reasons. If an attacker gains access to your database, they would be able to see each user's login credentials. One way to defend against such an attack is to encrypt passwords with a hash function and a salt
(some random data), and store that encrypted value in the database instead of the plain text password. When a user signs in again, we'll collect the password that was submitted, hash it, and check if it matches the hash in the database. Werkzeug, the utility library on which Flask is built, provides the functions generate_password_hash
and check_password_hash
for these two tasks, respectively.
With this in mind, here are the columns we'll need for the users table:
Column | Type | Constraints |
uid | int | Primary Key, Auto Increment |
firstname | varchar(100) | |
lastname | varchar(100) | |
varchar(120) | Unique | |
password | varchar(54) |
Just like before, you can create this table from a web interface such as phpMyAdmin or from the command line, as shown below:
mysql> CREATE TABLE users ( uid INT NOT NULL PRIMARY KEY AUTO_INCREMENT, firstname VARCHAR(100) NOT NULL, lastname VARCHAR(100) NOT NULL, email VARCHAR(120) NOT NULL UNIQUE, pwdhash VARCHAR(100) NOT NULL );
Next, in models.py
, let's create a class to model a user with attributes for a user's first name, last name, email, and password.
app/intro_to_flask/models.py
from flask.ext.sqlalchemy import SQLAlchemy from werkzeug import generate_password_hash, check_password_hash db = SQLAlchemy() class User(db.Model): __tablename__ = 'users' uid = db.Column(db.Integer, primary_key = True) firstname = db.Column(db.String(100)) lastname = db.Column(db.String(100)) email = db.Column(db.String(120), unique=True) pwdhash = db.Column(db.String(54)) def __init__(self, firstname, lastname, email, password): self.firstname = firstname.title() self.lastname = lastname.title() self.email = email.lower() self.set_password(password) def set_password(self, password): self.pwdhash = generate_password_hash(password) def check_password(self, password): return check_password_hash(self.pwdhash, password)
We use the set_password() function to set a salted hash of the password, instead of using the plain text password itself.
Lines one and four already existed in models.py
, so we'll start on line two by importing the generate_password_hash
and check_password_hash
security functions from Werkzeug. Next, we create a new class named User
, inheriting from the database object db
's Model
class (line six.)
Inside of our User
class, we create attributes for the table's name, primary key, and the user's first name, last name, email, and password (lines 10-14). We then write a constructor which sets the class attributes (lines 17-20). We save names in title case and email addresses in lowercase to ensure a match regardless of how a user types in his credentials on subsequent sign ins.
We use the set_password
function (lines 22-23) to set a salted hash of the password, instead of using the plain text password itself. Lastly, we have a function named check_password
that uses check_password_hash
, to check a user's credentials on any subsequent sign ins (lines 25-26).
— Checkpoint: 15_user_model
—
Sweet! We've created a users table and a user model, thereby laying down the foundation of our authentication system. Now let's build the first user-facing component of the authentication system: the signup page.
Building a Signup Page
Planning
Take a look at Fig. 1
below, to see how everything will fit together.
Implement SSL site-wide so that passwords and session tokens cannot be intercepted.
Let's go over the figure from above:
- A user visits the URL
/signup
to create a new account. The page is retrieved through an HTTP GET request and loads in the browser. - The user fills in the form fields with his first name, last name, email, and password.
- The user clicks the "Create account" button, and the form submits to the server with an HTTP POST request.
- On the server, a function validates the form data.
- If one or more fields do not pass validation, the signup page reloads with a helpful error message, prompting the the user to try again.
- If all fields are valid, a new
User
object will be created and saved into the database. The user will then be signed in and redirected to a profile page.
This sequence of steps should look familiar, as it's identical to the sequence of steps we took to create a contact form. Here, instead of sending an email at the end, we save a user's credentials to the database. The previous article already explained creating a form in detail, I'll move more quickly in this section so that we can get to the more exciting parts, faster.
Creating a Signup Form
We installed Flask-WTF in the previous article, so let's proceed with creating a new form inside forms.py
.
app/intro_to_flask/forms.py
from flask.ext.wtf import Form, TextField, TextAreaField, SubmitField, validators, ValidationError, PasswordField from models import db, User . . . class SignupForm(Form): firstname = TextField("First name", [validators.Required("Please enter your first name.")]) lastname = TextField("Last name", [validators.Required("Please enter your last name.")]) email = TextField("Email", [validators.Required("Please enter your email address."), validators.Email("Please enter your email address.")]) password = PasswordField('Password', [validators.Required("Please enter a password.")]) submit = SubmitField("Create account") def __init__(self, *args, **kwargs): Form.__init__(self, *args, **kwargs) def validate(self): if not Form.validate(self): return False user = User.query.filter_by(email = self.email.data.lower()).first() if user: self.email.errors.append("That email is already taken") return False else: return True
We start by importing one more Flask-WTF class named PasswordField
(line one), which is like TextField
except that it generates a password textbox. We'll need the db
database object and the User
model to handle some custom validation logic inside the SignupForm
class; so we import them too (line two).
Then we create a new class named SignupForm
containing a field for each piece of user information we wish to collect (lines 7-11). There's a presence validator on each field to ensure it's filled in, and a format validator which requires that email addresses match the pattern: [email protected]
.
Next, we write a simple constructor for the class that just calls the base class' constructor (lines 13-14).
So we've added some presence and format validators to our form fields, but we need an additional validator that ensures an account does not already exist with the user's email address. To do this we hook into Flask-WTF's validation process (lines 16-25).
Now inside of the validate()
function, we first ensure the presence and format validators run by calling the base class' validate()
method; if the form is not filled in properly, validate()
returns False
(lines 16-17).
Next we define the custom validator. We start by querying the database with the email that the user submitted (line 18). If you remember from our models.py
file, the email address is converted to lowercase to ensure a match regardless of how it was typed in. This Flask-SQLAlchemy expression corresponds to the following SQL statement:
SELECT * FROM users WHERE email = self.email.data.lower() LIMIT 1
If a user record already exists with the submitted email, validation fails giving the following error message: "That email is already taken" (lines 21-22).
Using the Signup Form
Let's now create a new URL mapping and a new web template for the signup form. Open routes.py
and import the newly created signup form so that we can use it.
app/intro_to_flask/routes.py
from intro_to_flask import app from flask import render_template, request, flash from forms import ContactForm, SignupForm
Next, create a new URL mapping.
app/intro_to_flask/routes.py
@app.route('/signup', methods=['GET', 'POST']) def signup(): form = SignupForm() if request.method == 'POST': if form.validate() == False: return render_template('signup.html', form=form) else: return "[1] Create a new user [2] sign in the user [3] redirect to the user's profile" elif request.method == 'GET': return render_template('signup.html', form=form)
Inside the signup()
function, we create a variable named form
that contains a usable instance of the SignupForm
class. If a GET request has been issued, we'll return the signup.html
web template containing the signup form for the user to fill out.
Otherwise, we'll see just a temporary placeholder string. For now, the temp string lists the three actions that should take place when the form has been successfully submitted. We'll come back and replace this string with real code in "The First Signup" section below.
Now that we've created a URL mapping, the next step is to create the web template signup.html
and place it inside the templates/
folder.
app/intro_to_flask/templates/signup.html
{% extends "layout.html" %} {% block content %} <h2>Sign up</h2> {% for message in form.firstname.errors %} <div class="flash">{{ message }}</div> {% endfor %} {% for message in form.lastname.errors %} <div class="flash">{{ message }}</div> {% endfor %} {% for message in form.email.errors %} <div class="flash">{{ message }}</div> {% endfor %} {% for message in form.password.errors %} <div class="flash">{{ message }}</div> {% endfor %} <form action="{{ url_for('signup') }}" method=post> {{ form.hidden_tag() }} {{ form.firstname.label }} {{ form.firstname }} {{ form.lastname.label }} {{ form.lastname }} {{ form.email.label }} {{ form.email }} {{ form.password.label }} {{ form.password }} {{ form.submit }} </form> {% endblock %}
This template looks just like contact.html
. We first loop through and display any error messages if necessary. We then let Jinja2 generate most of the HTML form for us. Remember how in the Signup
form class we appended the error message "That email is already taken" to self.email.errors
? That's the same object that Jinja2 loops through in this template.
The one difference from the contact.html
template is the omission of the if...else
logic.
In this template, we want to register and sign in the user on a successful form submission. This takes place on the back-end, so the if...else
statement is not needed here.
Finally, add in these CSS rules to your main.css
file so that the signup form looks nice and pretty.
app/intro_to_flask/static/css/main.css
/* Signup form */ form input#firstname, form input#lastname, form input#password { width: 400px; background-color: #fafafa; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; border: 1px solid #cccccc; padding: 5px; font-size: 1.1em; } form input#password { margin-bottom: 10px; }
Let's check out the newly created signup page by typing:
$ cd app/ $ python runserver.py
And browse to http://localhost:5000/signup in your favorite web browser.
Excellent! We just created a signup form from scratch, handled complex validation, and created a good looking signup page with helpful error messages.
— Checkpoint: 16_signup_form
—
If any of these steps were unclear, please take a moment to review the previous article. It covers each step in greater detail, and I followed the same steps from that article, to create this signup form.
The First Signup
Let's start by replacing the temporary placeholder string in routes.py
's signup()
function with some real code. Upon a successful form submission, we need to create a new User
object, save it to the database, sign the user in, and redirect to the user's profile page. Let's take this step by step, starting with creating a new User
object and saving it to the database.
Saving a New User Object
Add in lines five and 17-19 to
routes.py.
app/intro_to_flask/routes.py
from intro_to_flask import app from flask import render_template, request, flash, session, url_for, redirect from forms import ContactForm, SignupForm from flask.ext.mail import Message, Mail from models import db, User . . . @app.route('/signup', methods=['GET', 'POST']) def signup(): form = SignupForm() if request.method == 'POST': if form.validate() == False: return render_template('signup.html', form=form) else: newuser = User(form.firstname.data, form.lastname.data, form.email.data, form.password.data) db.session.add(newuser) db.session.commit() return "[1] Create a new user [2] sign in the user [3] redirect to the user's profile" elif request.method == 'GET': return render_template('signup.html', form=form)
First, we import the User
class from models.py so that we can use it in the signup()
function (line five). Then we create a new User
object called newuser
and populate it with the signup form's field data (line 17).
Next, we add newuser
to the database object's session (line 18), which is Flask-SQLAlchemy's version of a regular database transaction. The add()
function generates an INSERT
statement using the User
object's attributes. The equivalent SQL for this Flask-SQLAlchemy expression is:
INSERT INTO users (firstname, lastname, email, pwdhash) VALUES (form.firstname.data, form.lastname.data, form.email.data, form.password.data)
Lastly, we update the database with the new user record by committing the transaction (line 19).
Signing in the User
Next, we need to sign in the user. The Flask app needs to know that subsequent page requests are coming from the browser of the user who has successfully signed up. We can accomplish this by setting a cookie in the user's browser containing some sort of ID and associating that key with the user's credentials in the Flask app.
This way, the ID in the browser's cookie will be passed to the app on each subsequent page request, and the app will look up the ID to determine whether it maps to valid user credentials.
If it does, the app allows access to the parts of the website that you need to be signed in for. This combination of having a key stored on the client and a value stored on the server is called a session.
Flask has a session
object that accomplishes this functionality. It stores the session key in a secure cookie on the client and the session value in the app. Let's use it in our signup()
function.
app/intro_to_flask/routes.py
from flask import render_template, request, flash, session . . . @app.route('/signup', methods=['GET', 'POST']) def signup(): form = SignupForm() if request.method == 'POST': if form.validate() == False: return render_template('signup.html', form=form) else: newuser = User(form.firstname.data, form.lastname.data, form.email.data, form.password.data) db.session.add(newuser) db.session.commit() session['email'] = newuser.email return "[1] Create a new user [2] sign in the user [3] redirect to the user's profile" elif request.method == 'GET': return render_template('signup.html', form=form)
We start by importing Flask's session
object on line one. Next, we associate the key 'email
' with the value of the newly registered user's email (line 17). The session
object will take care of hashing 'email
' into an excrypted ID and storing it in a cookie on the user's browser. At this point, the user is signed in to our app.
Redirecting to a Profile page
The last step is to redirect the user to a Profile page after signing in. We'll use the url_for
function (which we've seen in layout.html
and contact.html
) in conjunction with Flask's redirect()
function.
app/intro_to_flask/routes.py
from intro_to_flask import app from flask import render_template, request, flash, session, url_for, redirect . . . @app.route('/signup', methods=['GET', 'POST']) def signup(): form = SignupForm() if request.method == 'POST': if form.validate() == False: return render_template('signup.html', form=form) else: newuser = User(form.firstname.data, form.lastname.data, form.email.data, form.password.data) db.session.add(newuser) db.session.commit() session['email'] = newuser.email return redirect(url_for('profile')) elif request.method == 'GET': return render_template('signup.html', form=form)
on line two, we import Flask's url_for()
and redirect()
functions. Then on line 19, we replace our temporary placeholder string with a redirect to the URL /profile
. We don't have a URL mapping for /profile
yet, so let's create that next.
app/intro_to_flask/routes.py
@app.route('/profile') def profile(): if 'email' not in session: return redirect(url_for('signin')) user = User.query.filter_by(email = session['email']).first() if user is None: return redirect(url_for('signin')) else: return render_template('profile.html')
Here we can finally see sessions in action. We start on line four by fetching the browser's cookie and checking if it contains a key named 'email
'. If it doesn't exist, that means the user is not authenticated, so we redirect the user to a signin page (we'll create this in the next section).
If the 'email
' key does exist, we look up the server-side user email value associated with the key using session['email']
, and then query the database for a registered user with this same email address (line seven). The equivalent SQL for this Flask-SQLAlchemy expression is:
SELECT * FROM users WHERE email = session['email'];
If no registered user exists, we'll redirect to the signup page. Otherwise, we render the profile.html
template. Let's create profile.html
now.
app/intro_to_flask/templates/profile.html
{% extends "layout.html" %} {% block content %} <div class="jumbo"> <h2>Profile<h2> <h3>This is {{ session['email'] }}'s profile page<h3> </div> {% endblock %}
I've kept this profile template simple. If we focus in on line five — you'll see that we can use Flask's session
object inside Jinja2 templates. Here, I've used it to create a user-specific string, but you could use this ability to pull other types of user-specific information instead.
We're finally ready to see the result of all our hard work. Type the following into your terminal:
$ python runserver.py
Go to http://localhost:5000/ in your favorite web browser, and complete the sign up process. You should be greeted with a profile page that looks like the following screenshot:
Signing up users is a huge milestone for our app. We can adapt the code in our /signup()
function and round out our authentication system by allowing users to sign in and out of the app.
— Checkpoint: 17_profile_page
—
Building a Signin Page
Creating a signin page is similar to creating a signup page — we'll need to create a signin form, a URL mapping, and a web template. Let's start by creating the SigninForm
class in forms.py
.
app/intro_to_flask/forms.py
class SigninForm(Form): email = TextField("Email", [validators.Required("Please enter your email address."), validators.Email("Please enter your email address.")]) password = PasswordField('Password', [validators.Required("Please enter a password.")]) submit = SubmitField("Sign In") def __init__(self, *args, **kwargs): Form.__init__(self, *args, **kwargs) def validate(self): if not Form.validate(self): return False user = User.query.filter_by(email = self.email.data.lower()).first() if user and user.check_password(self.password.data): return True else: self.email.errors.append("Invalid e-mail or password") return False
The SigninForm
class is similar to the SignupForm
class. To sign a user in, we need to capture their email and password, so we create those two fields with presence and format validators (lines 2-3). Then we define our custom validator inside the validate()
function (lines 10-15). This time the validator needs to make sure the user exists in the database and has the correct password. If a record does exist with the supplied information, we check to see if the password matches (line 14). If it does, the validation check passes (line 15), otherwise they get an error message.
Next, let's create a URL mapping in routes.py
.
app/intro_to_flask/routes.py
... from forms import ContactForm, SignupForm, SigninForm . . . @app.route('/signin', methods=['GET', 'POST']) def signin(): form = SigninForm() if request.method == 'POST': if form.validate() == False: return render_template('signin.html', form=form) else: session['email'] = form.email.data return redirect(url_for('profile')) elif request.method == 'GET': return render_template('signin.html', form=form)
Once again, the signin()
function is similar to the signup()
function. We import SigninForm
(line two), so that we can use it in the signin()
function. Then in signin()
, we return the signin.html
template for GET requests (lines 17-18).
If the form has been POSTed and any validation check fails, the signin form reloads with a helpful error message (lines 11-12). Otherwise, we sign in the user by creating a new session and redirecting to their profile page (lines 14-15).
Lastly, let's create the web template signin.html
.
app/intro_to_flask/templates/signin.html
{% extends "layout.html" %} {% block content %} <h2>Sign In</h2> {% for message in form.email.errors %} <div class="flash">{{ message }}</div> {% endfor %} {% for message in form.password.errors %} <div class="flash">{{ message }}</div> {% endfor %} <form action="{{ url_for('signin') }}" method=post> {{ form.hidden_tag() }} {{ form.email.label }} {{ form.email }} {{ form.password.label }} {{ form.password }} {{ form.submit }} </form> {% endblock %}
Similar to signup.html
template, we first loop through and display any error messages, then we let Jinja2 generate the form for us.
And that does it for the signin page. Visit http://localhost:5000/signin to check it out. Go ahead and sign in, you should get redirected to your profile page.
— Checkpoint: 18_signin_form
—
Signing Out
In "The First Signup" section above, we saw that "signing in" meant setting a cookie in the user's browser containing an ID and associating that ID with the user's data in the Flask app. Therefore, "signing out" means clearing the cookie in the browser and dissociating the user data.
This can be accomplished in one line: session.pop('email', None)
.
We don't need a form or even a web template to sign out. All we need is a URL mapping in routes.py
, which terminates the session and redirects to the Home page. The mapping, therefore, is short and sweet:
app/intro_to_flask/routes.py
@app.route('/signout') def signout(): if 'email' not in session: return redirect(url_for('signin')) session.pop('email', None) return redirect(url_for('home'))
The user is not authenticated if the browser's cookie does not contain a key named 'email
', in that case, we just redirect to the signin page (lines 4-5). Otherwise, we terminate the session (line seven) and redirect back to the home page (line eight).
You can test the sign out functionality by visiting http://localhost:5000/signout. If you're signed in, the app will sign you out and redirect to http://localhost:5000/. Once you've signed out, try visiting the profile page http://localhost:5000/profile. You shouldn't be allowed to see the profile page if you're not signed in, and the app should redirect you back to the Signin page.
— Checkpoint: 19_signout
—
Tidying Up
Now we need to update the site header with navigation links for "Sign Up", "Sign In", "Profile", and "Sign Out". The links should change based on whether the user is signed in or not. If the user is signed out, links for "Sign Up" and "Sign In" should be visible. When the user is signed in, we want links for "Profile" and "Sign Out" to appear, while hiding the "Sign Up" and "Sign In" links.
So how can we do this? Think back to the profile.html
template where we used Flask's session
object. We can use the session
object to show navigation links based on the user's authentication status. Let's open layout.html
and make the following changes:
app/intro_to_flask/templates/layout.html
<!DOCTYPE html> <html> <head> <title>Flask App</title> <link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}"> </head> <body> <header> <div class="container"> <h1 class="logo">Flask App</h1> <nav> <ul class="menu"> <li><a href="{{ url_for('home') }}">Home</a></li> <li><a href="{{ url_for('about') }}">About</a></li> <li><a href="{{ url_for('contact') }}">Contact</a></li> {% if 'email' in session %} <li><a href="{{ url_for('profile') }}">Profile</a></li> <li><a href="{{ url_for('signout') }}">Sign Out</a></li> {% else %} <li><a href="{{ url_for('signup') }}">Sign Up</a></li> <li><a href="{{ url_for('signin') }}">Sign In</a></li> {% endif %} </ul> </nav> </div> </header> <div class="container"> {% block content %} {% endblock %} </div> </body> </html>
Starting on line 17, we use Jinja2's if...else
syntax and the session()
object to check if the browser's cookie contains the 'email
' key. If it does, then the user is signed in and therefore should see the "Profile" and "Sign Out" navigation links. Otherwise, the user is signed out and should see links to "Sign Up" and "Sign In".
Now give it a try! Check out how the navigation links appear and disappear by signing in and out of the app.
The last task remaining is a similar issue: when a user is signed in, we don't want him to be able to visit the signup and signin pages. It makes no sense for a signed in user to authenticate themselves again. If signed in users try to visit these pages, they should instead be redirected to their profile page. Open routes.py
and add the following piece of code to the beginning of the signup()
and signin()
functions:
if 'email' in session: return redirect(url_for('profile'))
Here's what your routes.py
file will look like after adding in that snippet of code:
app/intro_to_flask/routes.py
@app.route('/signup', methods=['GET', 'POST']) def signup(): form = SignupForm() if 'email' in session: return redirect(url_for('profile')) . . . @app.route('/signin', methods=['GET', 'POST']) def signin(): form = SigninForm() if 'email' in session: return redirect(url_for('profile')) ...
And with that we're finished! Try visiting the the "signup" or "signin" pages while you are currently signed in, to test it out.
— Checkpoint: 20_visibility_control
—
Conclusion
We've accomplished a lot in this article. We've taken our Flask app from being a simple Python module and turned it into a well organized application, capable of handling user authentication.
There are several directions in which you can take this app from here. Here are some ideas:
- Let users sign in with an existing account, such as their Google account, by adding support for OpenID.
- Give users the ability to update their account information, as well as delete their account.
- Let users reset their password if they forget it.
- Implement an authorization system.
- Deploy to a production server. Note that when you deploy this app to production, you will need to implement SSL site-wide so that passwords and session tokens cannot be intercepted. If you deploy to Heroku, you can use their SSL certificate.
So go forth and continue to explore Flask, and build your next killer app! Thanks for reading.
Comments