Get the code as a kickoff site, or follow along.
Now, this is a great starting point
A minimal yet scalable Flask starter application with all user authentication (subscription) use cases already implemented and running.
Our white canvas Flask templates have a new hero: the “Minimal+User” project — a fully functional website with ready-to-use code [and forms] for sign-up, login, forgot password, user profile, etc.
The “Minimal+User” project is an almost empty Flask site, and you can grab its code and use it as a starting point when you need to create a new application from scratch. It has a basic layout that can be easily wiped out and uses a tiny PostgreSQL database (with a table containing links to cat pictures and a table to keep information about users).
The site’s homepage is shown below: you can try our up-and-running demo and get the complete code in this GitHub public repository.
The site is responsive, lightweight, and SEO-friendly; it displays a navbar, a footer, breadcrumb navigation, ready-to-use popups, and a full-fledged cookie banner.
Furthermore, the website is divided into sections; each section corresponds to a Flask Blueprint, a neat way of organizing the site’s pages in separate modules.
The “Minimal+User” project has been developed as an evolution of our two most basic white canvas sites: one with the bare minimum, which we call “Minimal”; the other with added code for a lightweight database connection, which we call “Minimal + DB”.
As “Minimal+User” was made starting from “Minimal+DB”, and since these three projects are very similar to each other (at least in fundamental aspects), it might be helpful to read the article above regarding:
- general project and page structure;
- more insight about blueprints;
- basic requirements and dependencies,
- details about database connection and data access methods;
- use of Bootstrap 5 for buttons, cards, and other layout components, sticky navbar, modal popups, and error alerts;
- the cookie alert and other goodies.
All that cleared out, the following sections will explain the essential Python code regarding sign-up/login/log-out and other forms.
1. User authentication and authorization use cases
The use cases handled by our Python code are classic for user subscriptions.
- The user signs up for the site; the user’s email address is required for this registration. The user email is used for future login operations.
- On sign-up, the site grants the user a nickname, an access key (password), and ***BONUS***, a custom avatar tile (in SVG format).
- Immediately after sign-up, the user receives a confirmation email sent to the given email address; the user may confirm her email by clicking a link in the email body.
- The user logs in and out from the app or site;
- If logged in, the user can change the nickname;
- If logged in, the user can change the access key;
- If logged in, the user can change the registration email.
- If the user forgot/lost his access key, she could reset it after receiving an email sent to her registered email address.
Quite essential but complete.
2. The project’s architecture
The main idea of how the whole thing works is sketched in Figure 2. The bl_home.py
and bl_photoalbum.py
blueprint files contain the endpoints for the standard pages of the site, like the home page or the main image list of the photo album. By the way, the cat photo album is the minimalistic core functionality of the “Minimal+User” template, which works as a placeholder. Obviously, it must be replaced by the real core of the project that will be built on top of the “Minimal+User” template.
The bl_auth.py
file contains all the endpoints for pages and forms involved in the authentication and authorization use cases, i.e., the user’s subscription and profile management. All use cases will be described one by one in the next chapter; for the moment, note that the architecture includes an external mailer service component used to validate user emails (more about the mailer in chapter 5).
The site is designed to use an external cloud repository — an AWS S3 bucket, to be precise— that contains the custom SVG tile avatars, which are automatically created for each user during sign-up. The s3_operation.py
file has the Python code to upload and save the SVG icon tiles on AWS S3 (the code also includes the procedure to delete a file from the repository).
Please refer to the article below to learn more about how the “Minimal+User” site assigns tiles and nicknames to subscribers.
The site architecture also includes a small database which, in this case, is a PostgreSQL instance. It contains just two tables: tbl_images
and tbl_auth
; the former holds the info about the cats’ images in the photo album, and the latter stores all information about the user/subscriber. The tables’ structure is defined by the SQL code below, which speaks for itself.
CREATE TABLE minimaluser.tbl_image (
img_id bigint NOT NULL,
img_name character varying(124),
img_caption character varying(255),
img_filename character varying(255) NOT NULL,
img_tstamp timestamp with time zone DEFAULT now() NOT NULL,
img_onair boolean DEFAULT true NOT NULL, <-- show/hide image
img_seqno integer DEFAULT 0, <-- for image order
img_is_in_hp boolean DEFAULT false <-- true if image is in home
);CREATE TABLE minimaluser.tbl_auth (
aut_id bigint NOT NULL,
aut_email character varying(255) NOT NULL,
aut_key character varying(128) NOT NULL,
aut_name character varying(100),
aut_isvalid boolean DEFAULT true NOT NULL,
aut_confirmed boolean DEFAULT false,
aut_timestamp timestamp with time zone DEFAULT now(),
aut_otp character varying(10), <-- used for email reset
aut_key_temp character varying(128), <-- for debug purposes only!
aut_tile character varying(50) <-- filename of tile avatar
);
The script files (in the
/database scripts
folder) contain the commands needed to create a complete instance of our demo PostgreSQL database. See section 7 for further details.
The db_*
files of the project hold the functions that directly access the database tables and views: you may consider these db_*
files as a layer that intermediates between the site's core operations and the underlying data entities.
3 How the site responds to page requests
Whenever the browser asks for a page, the “Minimal+User” site determines the user's state at that precise moment. As shown in Figure 3, a user can only be in one of three states:
- not logged and tracked as GUEST (with user id equal to 0);
- logged to the site, but the login email has not yet been confirmed,
- fully logged in, the email is confirmed.
To check in which of these states the user happens to be, the Flask application checks if a specific and valid JWT token is present in the session and, depending on the situation, sets a group of global variables.
If there is any problem with the JWT token, the application resets the token, treating the user as “GUEST”.
If the user is logged in, the specific JWT token is linked to the user’s identity because it contains encrypted data (like the user id and email) and is cryptographically signed by the Flask application with a dedicated secret.
If you are wondering who sets the user-related JWT token in the first place, Figure 3 shows the answer: the Flask application creates a JWT token whenever users sign up, log in, or confirm their email.
Table 1 shows how the above-mentioned global variables are set depending on the JWT token situation (the “from db” caption means that the value for the corresponding variable depends on the user’s data in the database). The variables that describe the user’s state are the booleans g.user_is_logged
and g.user_confirmed
; see chapter 4 below for an example of their use.
When a global variable is needed, it’s nice to use the Flask global object g
, which can be accessed in any section of the application’s code. The logic that assigns the global g.* variables as a function of the JWT token nature is implemented in the auth.py
and auth_db.py
Python files: take a look at the db_set_g_from_token()
function to see how all cases are treated.
4 Reacting to the user’s state
Depending on the “business logic” of your app, you may grant the user some functionalities depending on whether she is logged or not. Some other functionalities may be unlocked if she is logged in but has not yet “confirmed” her email, and probably additional functionalities if she is logged in and “confirmed”.
The “Minimal+User” site reacts to the user’s state in two ways: by using the global variables (g.user_is_logged
, g.user_confirmed,
and the like) or using Flask views’ decorators.
As an example of the use of globals, the navbar menu displays different content depending on whether the user is logged in. The Jinja code in the incl_navbar.html
template (which contains the HTML tags for the navbar menu) uses lines like {% if g.user_is_logged %}...{% else %}...{% endif %}
to adapt the HTML menu to the different user’s states.
The other way to enforce policies that depend on the user’s state is via Flask view decorators. We defined two of them in the auth.py
Python file: one is named @login_required
, and the other is @confirmation_required
. If, as an example, a route of the site should be accessible only to logged users, you may “decorate” the route endpoint with @login_required
like in this snippet:
@bp.route('/imagedetails/<int:img_id>',methods=('GET', 'POST'))
@login_required
@manage_cookie_policy
def imagedetails(img_id): mc = set_menu("imagedetails") ...
Same thing if you want an endpoint reachable only by logged users that already confirmed their email; in this case, use @confirmation_required
.
[In the above snippet, the second decorator enforces the compulsory setting of the cookie policies via the @manage_cookie_policy
instruction].
Note: when applying more than one decorator to a view, the order in which they are used matters. The wrappers with greater priority should be placed closest to the view function definition. In the above example, the cookie policy has a bigger priority than the login_required constraint and is thus placed immediately above the
imagedetails()
function.
5 All the use cases explained, one by one
All the Python code that implements the subscription use cases is found in two files: the bl_auth.py
blueprint file and the db_auth.py
database-related file. In particular, the routes/endpoints of the “Minimal+User” site are implemented in the bl_auth.py
file, while all functions that operate on the user state and data are found in the db_auth.py
file.
5.1 When the user signs up
The flowchart in Figure 5 shows all the steps during sign-up. The application validates the address when the user submits the required email address for the site’s subscription. If everything is ok, a nickname, an access key, and a custom SVG tile avatar are created for the user.
A new record is added to the database; a confirmation email is sent behind the curtains, and the user’s state is promoted to “logged”.
The code below shows the signup()
function in all its details. Some minor parts of the code are left over, replaced by … for brevity. The full code is in the bl_auth.py
Python file of the Flask project.
The function calls follow the steps of the flowchart in Figure 5. The data submitted by the user is read with the read_data_from_form()
function and is then validated via the db_validate_signup()
procedure. If no errors occur, an access key (password), a nickname, and an SVG tile are created for the user.
When all the elements needed for the new user’s record are available, the db_create_user_entry()
call saves all the data in the tbl_auth
table of the database. In particular, the user’s access key is hashed to keep data private and secure. [A plain-text copy of the password is also saved in the record for debugging purposes of the demo site. Make sure to remove that field from the database and the code in the final version of your project!].
Crucial Tip📌 : NEVER store users’ passwords as plain text. If a malicious hacker steals your database data, you and your users will be in big trouble.
Flask has a handy way to create hash values for passwords and then check the plain-text value against the hashes. The methods make clever use of the “salt” concept: you can read a summary on how to use the methods here.
Once the new user’s record has been inserted into the database, the custom SVG tile is saved in the AWS S3 bucket as an SVG image file. All the code that manages S3 buckets is gathered in the s3_operations.py
file.
The flowchart includes a “send email confirmation” block; in fact, we crafted a small Mailer service just for sending confirmation emails for our applications. The way “Minimal+User” sends emails (lines 24 to 31 in the above code snippet) will be the subject of the next section.
Finally, the user’s state is updated via the promote_user_to_logged()
function (remember the diagram in Figure 3). This function works as expected:
def promote_user_to_logged(user_obj, secret):
token = build_user_token(user_obj, secret)
session["ut"] = token
It builds a JWT token, encrypting the essential user data with a secret. Then saves the token in the Flask session. That’s it!
5.2 How confirmation emails are sent
Here is how the external email service is involved.
Step 1. The confirmation email will contain a link the user will click to confirm the email address used for the site’s subscription. The link will look something like this:
To compose this link, some data regarding the user is needed: this data will be part of the red parameter of the link.
For this purpose, the user’s data is packed and encoded in an ad hoc JWT token, which will be passed to the external mailer service. This JWT token is created in line 25 of the above code snippet:email_link_token = create_email_link_token(new_id, email,...)
and should not be confused with the token saved in the session (the latter is used to determine the user’s state).
Step 2. The email_link_token
of Step 1 is passed as a parameter to the ext_send_email()
function. The ext_ prefix suggests that the mailer functionality is delegated to an external service — yes, our Variance Mailer application.
Let’s consider the other parameters passed to the ext_send_email()
function:
email
, the user’s email address;user_aut_key_or_otp
, the user’s access key, which will be visible in the confirmation email text;- the parameter of point 2 may contain a one-time password (OTP); see section 5.6;
email_link_url
, the endpoint URL of “Minimal+User” that “hides” behind the link, part of the href of the link (blue and green string in the above image);email_blueprint
: which mailer section must be used (the mailer is capable of responding to different apps, and this parameter tells which of these apps must be served);email_service
: this parameter informs the mailer about which specific email template must be used to craft the confirmation email required.
All this data is needed to craft the correct confirmation email and call the mailer's correct service. Now here is how the ext_send_email()
function uses all this data:
def ext_send_email(user_email, user_aut_key_or_otp, email_link_url, email_blueprint, email_service, email_link_token): #PACK ALL INFO IN YET ANOTHER JWT TOKEN
payloadJWT={}
payloadJWT["user_email"]=user_email
payloadJWT["user_aut_key_or_otp"]=user_aut_key_or_otp
payloadJWT["email_link_url"]=email_link_url
payloadJWT["email_link_token"]=email_link_token
#PACKING AN ALREADY ENCODED TOKEN!
mailersecret = os.environ["JWT_MAILER_SECRET"]
#Convert payload into JWT token
encoded_payload = jwt.encode(payloadJWT, mailersecret,
algorithm='HS256')
#CALL MAILER
mailer_url = os.environ["MAILER_URL"] # complete url with final /
r = requests.get(
mailer_url+email_blueprint+'/'+email_service+'/'+encoded_payload
)
As you can see, all the bits and pieces that are needed to craft the confirmation email are packed in a new JWT token. But wait! This new token has the previous email_link_token
as part of its payload.
It’s like a box in a box.
This new token (that contains everything) is passed as a parameter of the correct HTTP GET call to the mailer via the requests.get()
function. Note how the requests.get()
parameter is composed using the values passed to ext_send_email()
.
The mailer receives the GET call and does its job.
The
requests.get()
is one of the valuable methods of the powerful “requests” external Python package. This package must be installed in the runtime environment: it’s already part of the requirements.txt file of our project.
All other details regarding the Mailer component are explained in the article below. The mailer code is available, and the link to its GitHub repository is in the article.
5.3 When the user logs in (and out)
The login endpoint is straightforward; when the user fills in and submits the form with her email and access key, the data is read with the read_data_from_form()
and validated via the db_check_user()
function.
@bp.route('/login',methods=('GET', 'POST'))
@manage_cookie_policy
def login(): mc = set_menu("login") #highlights menu if request.method == 'POST':
if 'btn_unlock' in request.form:
form_data = read_data_from_form()
user = db_check_user(form_data)
if user:
user_login(user,os.environ["JWT_SECRET_HTML"])
flash("You are logged in :)")
return redirect(url_for('bl_home.index'))
else:
user_logout(os.environ["JWT_SECRET_HTML"])
flash("Could not login!") return render_template('auth/login_frm.html', mc=mc)
If a valid user record is returned, the user’s state is changed with the user_login()
as seen in Figure 3; there, a JWT token is generated by encoding the user’s essential data, and that token is saved in the session.
def user_login(user, secret):
token = build_user_token(user, secret)
session["ut"] = token
The reverse transition happens when the user logs out: the user’s token is removed from the session, and the user's state is swapped to “GUEST”.
@bp.route('/logout')
@manage_cookie_policy
def logout(): user_logout(os.environ["JWT_SECRET_HTML"])
flash("See you soon!")
return redirect(url_for('bl_home.index'))...def user_logout(secret):
#log off user, the user becomes GUEST
promote_user_to_guest(secret)
5.4 When the user changes nickname or password
The user’s profile page allows users to change their nickname and password. These changes do not affect the user’s state. For the nickname, the below snippet applies.
@bp.route('/userprofile',methods=('GET', 'POST'))
@login_required
@manage_cookie_policy
def userprofile(): error = 0
mc = set_menu("userprofile") #highlights menu
... form_data = read_data_from_form()
user_name = form_data['username']
error = db_check_username(user_name)
if error == 120:
flash("User name too short")
elif error == 121:
flash("User name too logn")
elif error == 122:
flash("User name already in use")
else:
db_update_user_name(g.user_id, user_name)
#MUST UPDATE THE GLOBAL
g.user_name = user_name
flash("User name updated!")
...
Here again, the data is read with the read_data_from_form()
and validated via the db_check_username()
function. If no error occurs, the user’s information is updated with the new nickname via the db_update_user_name()
function.
Note that the g.user_name
variable is set directly.
To change the user’s password, the code snippet is somehow similar. Here the data is collected in the usual way, and further checks are made to verify if the old access key is correct and if the new key is feasible.
form_data = read_data_from_form()
confirm_access_key = form_data['confirmaccesskey']
new_access_key = form_data['newaccesskey']
current_access_key = form_data['accesskey']
if new_access_key!=confirm_access_key:
error = 106
elif db_check_user({'email':g.user_email,
'key':current_access_key}) is None:
#"recycling" check_user
error = 105
else:
error = check_valid_key(new_access_key)
if error == 0:
db_update_user_key(g.user_id, new_access_key)
flash("Access key updated!")
else:
error = 107 #errors 108, 109, 110 -> 107
If no errors emerge, the new access key is saved in the database — once hashed!
5.5 Changing the email (used to log in)
The user’s profile page allows users to change their registered email addresses. This is a more delicate procedure because it involves resending the confirmation email.
In this process, a new access key is created for the user and is sent along with the confirmation email.
5.6 Forgot/lost access key
The recovery of a lost access key is a two-phase process that presumes the user is not (necessarily) logged in.
In the first phase, the user is asked to insert the email address with which she subscribed. If the email address exists in the tbl_auth
table, the application creates a six-digit one-time password (OTP): this OTP is saved in the record connected to the given email address and sent to that address via email.
Furthermore, the user’s unique id, which is associated with the given email address, is wrapped in a JWT token. The token is passed to the second phase (as a parameter of the redirect to the resetkey()
endpoint): as we will see, the token will be crucial to validate the user input in phase 2.
All phase 1 is accomplished by the forgotkey()
endpoint in the bl_auth.py
blueprint file.
In the second phase, implemented in the resetkey()
endpoint of the bl_auth.py
blueprint file, the user is asked to insert the OTP delivered via email and a newly invented access key. If the inserted OTP matches the OTP saved in the user’s record having as a unique id the one encoded in the JWT token created in phase 1, the new access key is saved in that record.
The OTP is canceled from the user’s record.
5.7 The user deletes the account
The code to let the user permanently delete the account is quite simple.
@bp.route('/deleteaccount')
@login_required
def deleteaccount():
tile_to_be_removed = db_delete_account(g.user_id) user_logout(os.environ["JWT_SECRET_HTML"])
delete_file_from_s3(tile_to_be_removed,
os.environ["AWS_TILES_BUCKET_NAME"])
flash("Account deleted") return redirect(url_for('bl_home.index'))
The db_delete_account()
procedure deletes all user’s related data from the database, returning the user tile’s filename that must be removed from the cloud repository. The user’s state is reset via the user_logout()
function, seen in section 5.3.
Finally, the tile’s file is removed from the AWS S3 repository via the delete_file_from_s3()
function.
6. A note about Bootstrap 5
We use Bootstrap 5 to add basic standard functionality to our demo sites rapidly (e.g., navbar, buttons, cards, etc.). Nevertheless, Bootstrap 5 integration for “Minimal+User” is relatively lightweight. All Bootstrap libraries and stuff are taken from Bootstrap’s CDNs, thanks to a few lines of HTML code placed in the base.html
template file.
If you do not want to use Bootstrap, remove the related lines of HTML code from base.html
and adapt all other HTML pages to show the desired graphics output without using Bootstrap classes.
7. How to run the Flask project locally
First: clone the GitHub repository of the site (you’ll find “Minimal+User” here):
git clone https://github.com/VarianceDigital/minimal-user.git
cd minimal-user
These commands will create the folder with the Flask project and step into its directory (with the cd
command).
The “Minimal+User” site has its list of Python requirements, as seen in the requirements.txt
file: these are the packages needed to run the application. The requirements include the Flask-related packages (to run the project locally) and the Gunicorn package for production deployment.
It is well known that Flask comes with a small server for debugging purposes, but a serious WSGI HTTP Server is needed for online deployment, and Gunicorn is a good choice.
In addition, the requirements of the “Minimal+User” site include the psycopg2
package, which is needed to access the PostgreSQL database instance.
Before installing the requirements, we suggest creating a virtual environment (see the official reference). One can pick any name for the virtual environment’s folder; we decided on “venv”, which is a common practice:
[On Mac]
python3 -m venv venv
[On Windows]
py -m venv venv
Then, activate the virtual environment:
[On Mac]
source venv/bin/activate
[On Windows]
venv\scripts\activate
Now install the site’s requirements:
pip install -r requirements.txt
As the site needs its related database, you should first install PostgreSQL [if it’s not already installed on your machine]. A simple way to do this is to install pgAdmin, a free tool to manage PostgreSQL databases (see the official site).
Using pgAdmin, create a new database named my_local_db
and, once completed, spawn PgAdmin’s “Query Tool” window from the my_local_db
database. A copy of the database used by “Minimal+User” can be recreated locally using the scripts in the files 1-create-database.sql
, 2-fill-database-minimaluser.sql
, and 3-fill-database-namer.sql
(the scripts are placed in the \database scripts
folder). Copy and paste the text of the files in the “Query Tool” window, and press the “Execute” button: first, use 1-create-database.sql
, then 2-fill-database-minimaluser.sql
and 3-fill-database-namer.sql
.
The name
"my_local_db"
is used in the sample code.
Once this database is set up, the “Minimal+User” site can be started with the following:
[On Mac]
python3 main.py
[On Windows]
py main.py
8. FAQs — TL;DR :)
> Where can I get the complete code of the Flask “Minimal+User” site?
- Please feel free to follow us on Medium :) and fetch the code in this public GitHub repo.
> Are this demo site running somewhere?
- Sure! The “Minimal+User” demo site is here.
> How can I run the site locally?
- See section 7 above for complete instructions. The sample database for the “Minimal+User” site can be created using the scripts files in the
/database scripts
folder. Read the README.md file in the same folder.
> How to deploy (a modified version of) this site online?
- It’s easy to deploy our template Flask projects on Heroku, Azure, or AWS Beanstalk: read the article below to see how.
> Why do your Flask projects have that peculiar structure?
- All our projects stem from our “Minimal” Flask template projects, which are described in the following article:
> How can this site send emails to users?
- We have made a small mailer application to do that, called Variance Mailer. Please read the dedicated article to learn more.
> How can this site create custom tile avatars?
- Have a look at the related article.