The Flask session object | Learning Flask Ep. 16
Using, understanding and decoding the Flask session object, the globally available signed & encoded cookie
Sessions in Flask are a way to store information about a specific user from one request to the next. They work by storing a cryptographically signed cookie on the users browser and decoding it on every request.
The sesison object can be treated just like a dictionary that persists across requests, making it an ideal place to store non sensitive user data.
Important
The session object is NOT a secure way to store data. It’s a base64 encoded string and can easily be decoded, thus not making it a secure way to save or access sensitive information.
We’ll demonstrate decoding a session cookie shortly.
In this example, we’re going to create a very unsecure system to allow users to log in and view their profile.
The purpose is to demonstrate the session object, not a secure user management system! This guide doesn’t include any password hashing, user feedback or even a real database, it’s purely for the demonstration of working with sessions.
Login page
Let’s start by creating a template allowing our user to login by providing a username and password:
{% extends "public/templates/public_template.html" %}
{% block title %}sign up{% endblock %}
{% block main %}
<div class="container">
<div class="row">
<div class="col">
<h1>Sign in</h1>
<hr>
<form action="/sign-in" method="POST">
<div class="form-group">
<label>Username</label>
<input class="form-control" type="text" name="username">
</div>
<div class="form-group">
<label>Password</label>
<input class="form-control" type="password" name="password">
</div>
<button type="submit" class="btn btn-primary">Sign in</button>
</form>
</div>
</div>
</div>
{% endblock %}
Flask imports
We need to import a few things from Flask for this example:
from flask import render_template, request, session, redirect, url_for
render_template
- Allows us to render a template to the browserrequest
- To handle the incoming form data and URLsession
- The session objectredirect
- Allows us to redirect users to various parts of our appurl_for
- Constructs URL’s from arguments
Mock database
We’ll also need a mock database containing a couple of users (Feel free to change the values to something more familiar!):
users = {
"julian": {
"username": "julian",
"email": "julian@gmail.com",
"password": "example",
"bio": "Some guy from the internet"
},
"clarissa": {
"username": "clarissa",
"email": "clarissa@icloud.com",
"password": "sweetpotato22",
"bio": "Sweet potato is life"
}
}
We’re just using a dictionary containing 2 dictionaries to represent our users database.
Secret Key
The session object requires your app to have a value set for the SECRET_KEY
variable. You can either set it in your application config file or provide it somewhere in the file containing your views.
Whatever decision you make, it’s best to declare it as soon as you assign the app
variable.
The secret key is used to encode the session cookie, so it’s advised to use something relitively complex.
A good place to generate a secret key is with secrets.token_urlsafe()
and pass it an integer:
import secrets
secrets.token_urlsafe(16)
OCML3BRawWEUeaxcuKHLpw
Go ahead and create the `SECRET_KEY`:
app.config["SECRET_KEY"] = "OCML3BRawWEUeaxcuKHLpw"
Now that we’ve got our imports, database and secret key, let’s go ahead and build our routes.
Sign in route
We need a route to handle signing our users in and setting up the session object:
@app.route("/sign-in", methods=["GET", "POST"])
def sign_in():
if request.method == "POST":
req = request.form
username = req.get("username")
password = req.get("password")
if not username in users:
print("Username not found")
return redirect(request.url)
else:
user = users[username]
if not password == user["password"]:
print("Incorrect password")
return redirect(request.url)
else:
session["USERNAME"] = user["username"]
print("session username set")
return redirect(url_for("profile"))
return render_template("public/sign_in.html")
As you can see, it’s a relitively simple route, just for demostration.
- We check if the username is in the database with
if not username in users:
- If the user exists, we assign the user to
user
withuser = users[username]
- We check the users password matches the password for the user in the database with
if not password == user["password"]:
- If either checks fail, we redirect back to the URL of the request using
redirect(request.url)
If both checks pass, we assign the users username to the session key USERNAME
.
We can treat the session object just like a Python dictionary.
Updating the session
To set a key & value:
session["KEY"] = "VALUE"
In this case, we’ve assigned the session USERNAME
key to the users username:
session["USERNAME"] = user["username"]
If you were to print(session)
just after we set the USERNAME
key, you would see (Assuming the username of “julian”):
<SecureCookieSession {'USERNAME': 'julian'}>
You’ll also notice the redirect to profile
, using:
return redirect(url_for("profile"))
Redirect takes a URL and redirects the client to it. In this case we’ve passed it url_for("profile")
.
url_for
takes arguments and builds an endpoint, in this case we’ve just passed the name of a function, "profile"
, to which it builds a URL.
We havent’s created the profile route yet so let’s go ahead and do so:
### Getting the session object
@app.route("/profile")
def profile():
if not session.get("USERNAME") is None:
username = session.get("USERNAME")
user = users[username]
return render_template("public/profile.html", user=user)
else:
print("No username found in session")
return redirect(url_for("sign_in"))
The session object is global, meaning we can access it from any part of our application and treat it like a dictionary.
In the profile
route, we do the following:
if not session.get("USERNAME") is None:
- We use
session.get("KEY")
to check if the key exists in the session. If the key doesn’t exist,
session.get("KEY")
returnsNone
user = users[session.get("USERNAME")]
username = session.get("USERNAME")
assigns theusername
variable to the value saved in the sessionUSERNAME
keyWe then assign the user from the
users
dictionary withuser = users[username]
return render_template("public/profile.html", user=user)`
If the user is found in the session, we call
render_template
and pass it theprofile.html
file, along with theuser
else: print("No username found in session") return redirect(url_for("sign_in"))
If the
USERNAME
key is not in the session, we redirect back to the sign in page
Now we need to create the profile page!
Profile page
Create a new file called profile.html
and add the following:
{% extends "public/templates/public_template.html" %}
{% block title %}Profile{% endblock %}
{% block main %}
<div class="container">
<div class="row">
<div class="col">
<h1>Profile</h1>
<hr>
<div class="card">
<div class="card-body">
<h4>{{ user["username"] }}</h4>
<hr>
<p>{{ user["email"] }}</p>
<p class="text-muted">{{ user["bio"] }}</p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
It’s a very simple page containing some of the users details pulled from the database.
Before we go ahead and test any of our code, let’s create a route to allow our user to logout.
Popping sessions
We need to create a route that clears the USERNAME
from the session object.
As session
is just a Python object, we can pop a key from it!
Let’s create a simple sign out route:
@app.route("/sign-out")
def sign_out():
session.pop("USERNAME", None)
return redirect(url_for("sign_in"))
To pop a key from the session object:
session.pop("KEY", None)
If a user who’s been signed in visits this route, their USERNAME
variable will be removed from the session and they’ll be redirected to the sign in page.
We pass None
to session.pop
to make sure if a user who isn’t signed in visits the sign out route, they will also be redirected to the sign in route without the application throwing an error.
Without None
, the application throws a KeyError
.
Let’s test our app!
Testing it out
Before you sign in, open up the developer tools in your browser and head over to the session storage.
For Chrome users:
- Open the developer tools with
Ctrl + Shift + i
- Select the
Application
tab along the top of the toolbar - Select
Cookies
from the sidebar on the left
For Firefox users:
- Open the developer tools with
Ctrl + Shift + i
- Select the
Storage
tab along the top of the toolbar - Select
Cookies
from the sidebar on the left
Go to /sign-in
, enter one of the usernames and passwords in the database and submit the form.
You should see a cookie appear with the name session
and an encoded string in the value entry.
If all went as it should, you’ll be at the profile page for the user and see some of their details.
Now go to /sign-out
in the browser and you should be redirected to the sign in page. The session cookie should also be removed and you’ll no longer see it in the developer tools.
Session best practices
As mentioned before, the session object is NOT a secure place to store data as it can be easily decoded.
Some examples of what you’d store in the session object:
- Unique user ID’s
- Publicly visible usernames
- Tracking ID’s
- User preferences
Ideally, you’d store as much as you can in a database or local cache, such as Redis.
NEVER put passwords or ANY sensitive information in the session object! As we’ll now demonstrate why…
Decoding the session
Log back into the application and open up the developer tools.
In the developer tools, head to the Network
tab and look for the POST
request to the /sign-in
route. Click on it.
Under Response headers
, you’ll see Set-Cookie:
with a value similar to the following:
session=eyJVU0VSTkFNRSI6Imp1bGlhbiJ9.XGxnkw.0-dtOEX9rJYS9MqYgnM9reWr7dY; HttpOnly; Path=/
Copy this string and start an instance of the Python interpreter in your terminal or console.
python
Import base64:
import base64
We’re going to pass the session tookie to base64.b64decode()
which requires the string to not be 1 more than a multiple of 4 in character length.
You may have to delete a character or 2 from the string until you get the right number of characters.
Decode the string using base64.b64decode("session_cookie")
:
base64.b64decode("eyJVU0VSTkFNRSI6Imp1bGlhbiJ9.XGxnkw.0-dtOEX9rJYS9MqYgnM9reWr7dY; HttpOnly; Path=")
After stripping session=
and the trailing /
from the string, we got the following output:
b'{"USERNAME":"julian"}\\lg\x93\r\x1d\xb4\xe1\x17\xf6\xb2XK\xd3*b\t\xcc\xf6\xb7\x96\xaf\xb7X\x1e\xdbi:yr=\xaba'
As you can clearly see, we’ve decoded the string and revealed the USERNAME
value. The rest is just padding.
I hope this illustrated why you should never store anything sensitive in the session!
Wrapping up
The session object in Flask is an extremely useful tool for remembering and sharing state across an application and should be used with care.
It can be accessed globally across your application and in templates using the Jinja {{ session["KEY"] }}
syntax.
Just remember to set a secret key and keep it safe. Ideally in an app config file and out of version control!
views.py
from flask import render_template, request, session, redirect, url_for
app.config["SECRET_KEY"] = "OCML3BRawWEUeaxcuKHLpw"
users = {
"julian": {
"username": "julian",
"email": "julian@gmail.com",
"password": "example",
"bio": "Some guy from the internet"
},
"clarissa": {
"username": "clarissa",
"email": "clarissa@icloud.com",
"password": "sweetpotato22",
"bio": "Sweet potato is life"
}
}
@app.route("/sign-in", methods=["GET", "POST"])
def sign_in():
if request.method == "POST":
req = request.form
username = req.get("username")
password = req.get("password")
if not username in users:
print("Username not found")
return redirect(request.url)
else:
user = users[username]
if not password == user["password"]:
print("Incorrect password")
return redirect(request.url)
else:
session["USERNAME"] = user["username"]
print(session)
print("session username set")
return redirect(url_for("profile"))
return render_template("public/sign_in.html")
@app.route("/profile")
def profile():
if not session.get("USERNAME") is None:
username = session.get("USERNAME")
user = users[username]
return render_template("public/profile.html", user=user)
else:
print("No username found in session")
return redirect(url_for("sign_in"))
@app.route("/sign-out")
def sign_out():
session.pop("USERNAME", None)
return redirect(url_for("sign_in"))
Last modified · 28 Feb 2019
Written with StackEdit.