Ep.16 The Flask session object

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 browser
  • request - To handle the incoming form data and URL
  • session - The session object
  • redirect - Allows us to redirect users to various parts of our app
  • url_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 with user = 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") returns None

    user = users[session.get("USERNAME")]
    
  • username = session.get("USERNAME") assigns the username variable to the value saved in the session USERNAME key

  • We then assign the user from the users dictionary with user = 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 the profile.html file, along with the user

    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.

Flask session cookie

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.