Home » Tutorials » How to Set Up a User Authentication System with Flask

How to Set Up a User Authentication System with Flask

User Authentication is a critical component of most web applications, serving as the front line of security and personalized user experience. It’s the process that verifies if someone is who they claim to be online. Here’s why it’s so important and how you can handle it in Python with Flask.

Today, we’re going to build a simple user registration and login system using Flask. which will serve as a starting point for building more complex web apps with Flask. Let’s get started!

Table of Contents

Before we Start

You need to create a ‘templates‘ file directory that will contain your HTML files on your Flask project file and a ‘static‘ file directory that will contain your CSS file.

Necessary Libraries

We are going to use the Flask framework, so make sure to install this library via the terminal or your command prompt for the code to function properly:

$ pip install flask 

Imports

We begin by importing the main Flask class, which we will customize later. Alongside, we import the request module for handling incoming request data, and the render_template function to render HTML templates. Additionally, we import the redirect function, which redirects the user to a different endpoint, the url_for function for generating URLs for Flask routes, and the session object, which stores data.

from flask import Flask, render_template, request, redirect, url_for, session

Next, we import generate_password_hash and check_password_hash functions which as their names imply they hash and verify passwords securely.

from werkzeug.security import generate_password_hash, check_password_hash

Flask Application Instance

First, we create a Flask application instance to customize the main class imported earlier. Next, we define a secret key for our application, which will be used to securely sign session cookies.

app = Flask(__name__)
app.secret_key = 'your_secret_key'  # Change this to a random string

User Data

Now, we create a dictionary called users to store usernames and their hashed passwords. This approach allows each username (e.g., ‘user1’, ‘user2’) to have its own hashed password, enabling our application to support multiple users. Typically, in an actual application, this data would be stored in a database for enhanced security. However, we are using a dictionary for demonstration purposes.

users = {
   # Example user data, replace with a proper database in a real application
   'user1': generate_password_hash('password1'),
   'user2': generate_password_hash('password2')
}

Defining Routes

Route for the Home Page

This creates a route to the homepage; essentially, when a user visits the specified URL, our Flask application will invoke the index() function. This function, in turn, calls the render_template function to render the index.html template, which provides the homepage with its appearance and functionality.

If you want to learn more about routes and requests check out this tutorial.

@app.route('/')
def index():
   return render_template('index.html')

Route for User Login

This defines the login route, leading the user to the login page when they visit a specific URL or submit a form on the login page. We have configured this route to accept both POST and GET requests. The route’s logic is managed by the login() function.

The function begins by determining if a POST request has been made, which would indicate that the user has submitted the login form. If a POST request is detected, the function retrieves the submitted username and password from the form using request.form.

Next, it verifies the existence of the username in the user’s dictionary and checks if the submitted password matches the corresponding hashed password, utilizing the check_password_hash() function. If all credentials match, the username is stored in the session, and the user is redirected to the dashboard. If either the username or the password does not match, an error message is displayed to the user.

@app.route('/login', methods=['GET', 'POST'])
def login():
   if request.method == 'POST':
       username = request.form['username']
       password = request.form['password']
       if username in users and check_password_hash(users[username], password):
           session['username'] = username
           return redirect(url_for('dashboard'))
       else:
           error = 'Invalid username or password'
           return render_template('login.html', error=error)
   return render_template('login.html')

Route for User Registration

The route for user registration uses the GET request to retrieve the registration HTML when the user visits the page. It then verifies whether a registration form has been submitted through a POST request. If such a request is made, the function retrieves the username and password from the submitted data.

Next, it checks if these credentials already exist in the user’s dictionary. If they don’t, the credentials are stored, and the user is redirected to the login page. However, if the credentials do exist, an error message appears, and the user will remain on the registration page.

@app.route('/register', methods=['GET', 'POST'])
def register():
   if request.method == 'POST':
       username = request.form['username']
       password = request.form['password']
       if username not in users:
           users[username] = generate_password_hash(password)
           return redirect(url_for('login'))
       else:
           error = 'Username already exists'
           return render_template('register.html', error=error)
   return render_template('register.html')

Route for the Dashboard

This route leads to the dashboard page, where we define the dashboard() function. This function verifies whether the user is logged in by checking if their username is stored in the session. If the username is found, indicating the user is logged in, the function calls render_template to render the dashboard template and passes the username to it for display. If the username is not found, meaning the user is not logged in, the function redirects the user to the login page.

@app.route('/dashboard')
def dashboard():
   if 'username' in session:
       return render_template('dashboard.html', username=session['username'])
   return redirect(url_for('login'))

Route for User Logout

In this route, we define a function named logout. This function is triggered when the user clicks the logout link. It removes the stored username from the session and redirects the user to the homepage.

@app.route('/logout')
def logout():
   session.pop('username', None)
   return redirect(url_for('index'))

Running the Flask Application

This part ensures that the script is run directly and not imported as a module in which case it will not function, while also enabling debugging.

if __name__ == '__main__':
   app.run(debug=True)

Full Code

from flask import Flask, render_template, request, redirect, url_for, session
from werkzeug.security import generate_password_hash, check_password_hash


app = Flask(__name__)
app.secret_key = 'your_secret_key'  # Change this to a random string


users = {
   # Example user data, replace with a proper database in a real application
   'user1': generate_password_hash('password1'),
   'user2': generate_password_hash('password2')
}


@app.route('/')
def index():
   return render_template('index.html')


@app.route('/login', methods=['GET', 'POST'])
def login():
   if request.method == 'POST':
       username = request.form['username']
       password = request.form['password']
       if username in users and check_password_hash(users[username], password):
           session['username'] = username
           return redirect(url_for('dashboard'))
       else:
           error = 'Invalid username or password'
           return render_template('login.html', error=error)
   return render_template('login.html')


@app.route('/register', methods=['GET', 'POST'])
def register():
   if request.method == 'POST':
       username = request.form['username']
       password = request.form['password']
       if username not in users:
           users[username] = generate_password_hash(password)
           return redirect(url_for('login'))
       else:
           error = 'Username already exists'
           return render_template('register.html', error=error)
   return render_template('register.html')


@app.route('/dashboard')
def dashboard():
   if 'username' in session:
       return render_template('dashboard.html', username=session['username'])
   return redirect(url_for('login'))


@app.route('/logout')
def logout():
   session.pop('username', None)
   return redirect(url_for('index'))


if __name__ == '__main__':
   app.run(debug=True)

Styles CSS

/* styles.css */

/* Body styles */
body {
   font-family: Arial, sans-serif;
   margin: 0;
   padding: 0;
}

/* Header styles */
header {
   background-color: #333;
   color: #fff;
   padding: 10px;
   text-align: center;
}

/* Main content styles */
.container {
   max-width: 800px;
   margin: 20px auto;
   padding: 20px;
}

/* Form styles */
form {
   margin-bottom: 20px;
}

input[type="text"],
input[type="password"],
button {
   padding: 10px;
   margin: 5px 0;
   width: 100%;
   box-sizing: border-box;
}

button {
   background-color: #333;
   color: #fff;
   border: none;
   cursor: pointer;
}

button:hover {
   background-color: #555;
}

/* Error message styles */
.error {
   color: red;
   margin-bottom: 10px;
}

/* Dashboard styles */
.dashboard {
   text-align: center;
}

/* Logout link styles */
.logout-link {
   color: #333;
   text-decoration: none;
   border: 1px solid #333;
   padding: 5px 10px;
   border-radius: 5px;
}

.logout-link:hover {
   background-color: #333;
   color: #fff;
}

Body Style:

Here we will set the font to Arial and ensure a consistent layout across browsers by removing the default margin and padding.

/* Body styles */
body {
   font-family: Arial, sans-serif;
   margin: 0;
   padding: 0;
}

Header Style:

Here we will align the header to the center and give it a 10-pixel margin, a background color, and a different text color.

/* Header styles */
header {
   background-color: #333;
   color: #fff;
   padding: 10px;
   text-align: center;
}

Main Content Style:

This part is to set the maximum width of the container that will contain the main content of the page to 800 pixels with a 20 pixels padding and an auto margin as well as align the container to the center.

/* Main content styles */
.container {
   max-width: 800px;
   margin: 20px auto;
   padding: 20px;
}

Form Styles:

We will set the bottom margin to 20 pixels to create spacing between the forms and other content.

/* Form styles */
form {
   margin-bottom: 20px;
}

Input and Button Styles:

Here we will start by styling the input fields for the username and the password with padding, margin, width, and border-box together with the buttons, then we will add to styling the buttons with background color, text color, no border, and a pointer cursor on a hover.

input[type="text"],
input[type="password"],
button {
   padding: 10px;
   margin: 5px 0;
   width: 100%;
   box-sizing: border-box;
}

button {
   background-color: #333;
   color: #fff;
   border: none;
   cursor: pointer;
}

button:hover {
   background-color: #555;
}

Error Message Style:

Here we will give the error message text the color red and set its margin-bottom to 10 pixels to add spacing.

/* Error message styles */
.error {
   color: red;
   margin-bottom: 10px;
}

Dashboard Style:

We will align the text to the center.

/* Dashboard styles */
.dashboard {
   text-align: center;
}

Logout Link Style:

We will style the link by setting the text color and adding a border, while also removing the text decoration. Next, we will introduce padding and a border radius to give it the appearance of a button. Finally, we will create a hover effect that changes both the background and text colors.

/* Logout link styles */
.logout-link {
   color: #333;
   text-decoration: none;
   border: 1px solid #333;
   padding: 5px 10px;
   border-radius: 5px;
}

.logout-link:hover {
   background-color: #333;
   color: #fff;
}

Index HTML

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Welcome - The Pycodes</title>
   <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
   <style>
       /* Custom styles for the index page */
       .container {
           max-width: 600px;
           margin: 0 auto;
           padding: 20px;
           text-align: center;
       }
       h1 {
           font-size: 36px;
           margin-bottom: 20px;
           color: #333;
       }
       .btn {
           display: inline-block;
           padding: 10px 20px;
           margin: 10px;
           background-color: #333;
           color: #fff;
           text-decoration: none;
           border-radius: 5px;
           transition: background-color 0.3s;
       }
       .btn:hover {
           background-color: #555;
       }
   </style>
</head>
<body>
   <div class="container">
       <h1>The Pycodes</h1>
       <h1>Welcome to our Application</h1>
       <p>This is a basic platform for registration and login.</p>
       <a href="{{ url_for('login') }}" class="btn">Login</a>
       <a href="{{ url_for('register') }}" class="btn">Register</a>
   </div>
</body>
</html>

Document Type Declaration and Language Setting:

This part is pretty straightforward: it declares that the document is an HTML and the language is English.

<!DOCTYPE html>
<html lang="en">

Head Section: 

This part specifies the character encoding of the document as well as sets the page title while also linking the ‘styles.css‘ file with the url_for function for styling the index page.

<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Welcome - The Pycodes</title>
   <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">

Container and Heading Styles:

In this part, we will further style the index page, so we start by styling the container for the index page with a maximum width of 600 pixels, an auto margin, and 20 pixels padding and aligning the text to the center.

   <style>
       /* Custom styles for the index page */
       .container {
           max-width: 600px;
           margin: 0 auto;
           padding: 20px;
           text-align: center;
       }

Next, it goes to styling the Heading in the index page with a font size of 36 pixels and a margin-bottom of 20 pixels to add spacing and color.

       h1 {
           font-size: 36px;
           margin-bottom: 20px;
           color: #333;
       }

Lastly, it goes to styling the buttons in the index page with padding, margin, background color, text color, border radius, and hover effect.

       .btn {
           display: inline-block;
           padding: 10px 20px;
           margin: 10px;
           background-color: #333;
           color: #fff;
           text-decoration: none;
           border-radius: 5px;
           transition: background-color 0.3s;
       }
       .btn:hover {
           background-color: #555;
       }
   </style>
</head>

Body Section:

This is basically the content in the container of the index page, which is two headings and one paragraph as well as two links styled as buttons one leads to the login page, and the other leads to the registration page thanks to the url_for function.

<body>
   <div class="container">
       <h1>The Pycodes</h1>
       <h1>Welcome to our Application</h1>
       <p>This is a basic platform for registration and login.</p>
       <a href="{{ url_for('login') }}" class="btn">Login</a>
       <a href="{{ url_for('register') }}" class="btn">Register</a>
   </div>
</body>
</html>

Login HTML

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Login - The Pycodes</title>
   <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>
<body>
   <h1>Login</h1>
   {% if error %}
       <p class="error">{{ error }}</p>
   {% endif %}
   <form action="{{ url_for('login') }}" method="POST">
       <input type="text" name="username" placeholder="Username" required><br>
       <input type="password" name="password" placeholder="Password" required><br>
       <button type="submit">Login</button>
   </form>
</body>
</html>

This HTML shares the same document type and language setting as well as the same head section with only the title of the page being different as the previous HTML.

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Login - The Pycodes</title>
   <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>

Body Section:

This part starts by displaying login as a heading, then goes to display an error if there is an error, then it requires the user to provide a username and a password, and it also allows the user to submit these two data with a login button for the user to login.

<body>
   <h1>Login</h1>
   {% if error %}
       <p class="error">{{ error }}</p>
   {% endif %}
   <form action="{{ url_for('login') }}" method="POST">
       <input type="text" name="username" placeholder="Username" required><br>
       <input type="password" name="password" placeholder="Password" required><br>
       <button type="submit">Login</button>
   </form>
</body>
</html>

Register HTML

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Register - The Pycodes</title>
   <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>
<body>
   <h1>Register</h1>
   {% if error %}
       <p class="error">{{ error }}</p>
   {% endif %}
   <form action="{{ url_for('register') }}" method="POST">
       <input type="text" name="username" placeholder="Username" required><br>
       <input type="password" name="password" placeholder="Password" required><br>
       <button type="submit">Register</button>
   </form>
</body>
</html>

This one too shares the same document type and language setting as well as the same head section with only the title of the page being different as the previous two HTML files.

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Register - The Pycodes</title>
   <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>

Body Section:

This part works the same as the body section of the login HTML with certain differences being the heading of the page and the name of the button, so instead of logging in, the user is registering.

<body>
   <h1>Register</h1>
   {% if error %}
       <p class="error">{{ error }}</p>
   {% endif %}
   <form action="{{ url_for('register') }}" method="POST">
       <input type="text" name="username" placeholder="Username" required><br>
       <input type="password" name="password" placeholder="Password" required><br>
       <button type="submit">Register</button>
   </form>
</body>
</html>

Dashboard HTML

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Dashboard - The Pycodes</title>
   <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>
<body>
   <h1 class="dashboard">Welcome, {{ username }}</h1>
   <a href="{{ url_for('logout') }}" class="logout-link">Logout</a>
</body>
</html>

Just like the previous HTML files this one too shares the same document type and language setting as well as the head section differing only in the title of the page.

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Dashboard - The Pycodes</title>
   <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>

Body Section:

Similar to the previous ones this part contains the main content of the page, here it contains a greeting to the user with his username, as well as a logout link to redirect the user to the homepage.

<body>
   <h1 class="dashboard">Welcome, {{ username }}</h1>
   <a href="{{ url_for('logout') }}" class="logout-link">Logout</a>
</body>
</html>

Example

Wrapping Up

To wrap up, we’ve taken a crucial step in web development by implementing user authentication in Flask. This foundation not only secures our app but also sets the stage for more advanced features. You’re ready to dive deeper and expand your Flask projects.

Happy coding!

Subscribe for Top Free Python Tutorials!

Receive the best directly.  Elevate Your Coding Journey!

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top
×