Software systems are gaining popularity, and their utility is increasing daily. Nowadays, designing a sound software system means getting the functional requirements (why software exists) and the non-functional requirements correct (how the software exists). Nobody wants to use unsafe software that can leak private information like identity-sensitive data.
Security is a non-functional requirement, but taking care of security requires one to design the system carefully, affecting the functional requirements implementation. This topic will show two basic security measures: authentication and authorization.
Understanding authentication and authorization
Authentication is derived from authenticity, which means being credible or original. Thus, authentication is verifying the credibility or originality of an object/thing. Similarly, authorization is derived from authority, which means having the rights. Thus, authorization is the process of verifying the rights of an object/thing. An easy-to-understand example is at airports.
When you arrive at the airport, security checks your ID to authenticate you for travel. Then, once you reach the gate, the flight staff checks your boarding pass to authorize your journey. So you must be the rightly identifiable person with the proper access while traveling.
In software, authentication verifies who the user is, while authorization confirms what the user is supposed to have access to. This can all be tied to a resource like a database. You need to prove your identity for authentication and then your access rights for authorization—failure in either leads to denied access to the resource.
Simple authentication with username and password
The simplest example of authentication is using a login username/email with a password, called single factor authentication. We cannot store passwords as plain text, which will be a security risk. Instead, passwords are stored as hashes. A hash represents an encrypted form of a simple text password. Flask supports password encryption via werkzeug.security, as shown below.
from werkzeug.security import generate_password_hash, check_password_hash
plain_text_password = "My random password"
hashed_password = generate_password_hash(plain_text_password) # hashed password
print("Hashed password ==>", hashed_password)
# check if hashed password matches the plain text password
print("Checking password ==>", check_password_hash(hashed_password, plain_text_password))Flask Login with a working example
Flask supports user login/signup via different externally developed Flask projects, aka middleware that integrates login/signup ability in the Flask app. We will see Flask-Login, a very popular middleware for login/signup in Flask for this topic.
Here, we will create an app that displays a lucky number to its logged-in users when they visit the website. This means we will show a lucky number per timestamp of their website visit. Additionally, we will have a signup and login workflow allowing one to register, log in, and logout. We will omit the HTML code to manage complexity and show website images instead. Our focus would be on Flask server-side code.
Our website looks as follows.
The backend represents this form with the following Flask-WTForm class; refer to the prerequisites if you need a refresher.
class LoginForm(FlaskForm):
email = EmailField("EMAIL", validators=[DataRequired()])
password = PasswordField("PASSWORD", validators=[DataRequired()])The signup and login flow using Flask-Login
The following points are necessary to understand the signup and login flow.
The user should be able to sign up using email and password and then log in using email and password;
If a user is not signed up and attempts to log in, we warn them first to sign up;
Incorrect credentials while logging in after signing up are treated as failures.
How to do this via Flask-Login?
Flask Login requires us to define our user object with all properties and methods we want; however, it has some mandatory properties and methods as a convention for the process. Some of these methods are:
is_authenticatedchecks if the user object has valid credentialsis_activechecks if a user is active in addition to being authenticated. Being active means the application has no condition of suspending or rejecting the user. In the real world, you can get your account deactivated on social media websites if you violate their terms of service.is_anonymousChecks if a user is anonymousget_idGets the id from a user object Id is a mandatory string field on the user object. This is used to load users in a session.
So we will create the user once they sign up to the website, and then Flask-login will help us log them in by loading the user in the session.
Then, we can ensure only logged-in users can access specific personalized endpoints or require authentication.
Finally, when we want to log out, we can use Flask-login.
Thus Flask-login helps us with utilities like logging in and out and exposing endpoints to authenticated users only. The entire flow can be summarized as a diagram:
Implementation details
First, we should install Flask-Login using pip install flask-login. And then, we chalk out different phases of development.
Adding a Flask login to the app:
We need to add the app to Flask-Login via LoginManager.
from flask import Flask
from flask_login import LoginManager
app = Flask(__name__)
app.config["SECRET_KEY"] = 'MY-SECRET-KEY'
login_manager = LoginManager()
login_manager.init_app(app)The user class and its loading by Flask-Login
We create a simple user class with two fields: email and password. This class inherits the UserMixIn from Flask-Login, so all required properties and methods are inherited.
from flask_login import UserMixin
from werkzeug.security import generate_password_hash
class User(UserMixin):
def __init__(self, user_id, email, password):
self.id = user_id
self.email = email
self.password = generate_password_hash(password)When a user is created, we take the email and password from the form object of Flask-WTForm and then hash the email using Python's hash function to get a unique ID and store the password as a hash using generate_password_hash from werkzeug.security.
def create_and_save_new_user(form):
email = form.data["email"]
password = form.data["password"]
user_id = str(hash(email))
new_user = User(user_id, email, generate_password_hash(password))
user_store[user_id] = new_user
return new_userNext, we need to configure a user loader to load a User object based on a string ID. We will use an in-memory dictionary to simulate and read a database for our hashed email. This function is internally called by flask_login to determine the correct user.
user_store = {}
@login_manager.user_loader
def load_user(user_id):
return user_store.get(user_id)For the first time, when we sign in or log in, we need to check that the information in the Flask WTForm object is mapping to a user or not in the user_store. For this, we have the following utility function.
def get_user_from_user_store(form):
email = form.data["Email"]
hash_email = str(hash(email))
return user_store.get(hash_email)Now, with all the foundation work done, we can dive into the signup flow
The signup flow
As part of signup, we will get the information submitted by a user and validated in the backend by Flask-WTForm validations. If the inputs by the user are valid, we will create a new user object of the User class and save it using the create_and_save_new_user method. The following code does it. Also, if the user tries to sign up twice, we warn them to log in and not sign up again. We use the Flask flash function to write updates to the client or front end. The images below show the user entering information and getting a success message after signup.
@app.route('/signup', methods=["POST"])
def do_signup():
form = LoginForm()
if form.validate_on_submit():
user = get_user_from_user_store(form)
if user is None:
new_user = create_and_save_new_user(form)
flash(f"User with email {new_user.email} is signed up successfully, please login to continue", "info")
else:
flash(f"User with email {user.email} already exists, consider logging in", "warning")
return redirect('/')The login flow
In the login flow, we will fetch the user from the user_store based on the WTForm object. We will show warnings if we don't find a user or the form object is invalid. In case we find a user, we will log them in using the login_user function in Flask-Login. Once the user is logged in, the user can see the contents of other endpoint URLs where authentication is needed, for example, the /read endpoint.
@app.route('/login', methods=["POST"])
def do_login():
form = LoginForm()
if form.validate_on_submit():
user = get_user_from_user_store(form)
if user is not None:
login_user(user)
flash('Logged in successfully', "success")
return redirect('/read')
else:
flash(f"User email {form.data['email']} does not exist, please sign up and then log in.", "danger")
else:
flash('Login failed, check input data', "danger")
return redirect('/')The read flow
Once the user is logged in, we can show their lucky number. The logic in the backend is implemented to pick a random number between 1 and 255. And then display it with the current timestamp and user email. The following code does it. Check the @login_required decorator. It recognizes whether the endpoint should be accessible to a non-logged-in user. Also, the current_user variable tracks the user logged in via Flask-Login. The image below shows the read flow after login.
from flask_login import current_user, login_required
import datetime
@app.route('/read', methods=["GET"])
@login_required
def read_fact():
cur_time = datetime.datetime.now()
messages = [
f"User --> {current_user.email}",
f"Lucky number --> {random.randint(1, 255)}",
f"Date of query --> {cur_time.date().isoformat()}",
f"Time of query --> {cur_time.time().isoformat()}",
]
return render_template('read.html', msgs=messages)The logout flow
In the logout flow, we log the user out via logout_user function in flask-login. We save a message to show the email of the user being logged out. And, of course, you need to be logged in to be logged out, hence the @login_required. The below image shows the system after the user has logged out from the read flow screen.
from flask_login import logout_user
@app.route('/logout', methods=["POST"])
@login_required
def do_logout():
flash(f"The user with email {current_user.email} is logged out", "info")
logout_user()
return redirect('/')Conclusion
In the given implementation of the app, Flask login has been utilized to enable users to sign up, log in, and log out. In practical scenarios, the user object would be implemented as a database table class, which is required in commonly used Object Relational Mappers such as SQLAlchemy.
To retrieve the User object, @login_manager.user_loader will query the database using the user ID. The Signup and Login processes will be similar, relying on user credentials. Similarly, Logout and URL access for authenticated users will follow a comparable approach. Authentication and authorization are complex topics with multiple solutions, and relying solely on one implementation may not be sufficient. To ensure secure software development, analyzing different systems and understanding their vulnerabilities and the measures taken to mitigate them is essential.