OverIQ.com

Sessions in Django

Last updated on July 27, 2020


In the last chapter (Cookies in Django), we have learned how cookies allow us to store data in browser easily. Although no doubt cookies are useful, they have following problems.

  1. An attacker can modify contents of a cookie that could potentially break your application
  2. We can't store sensitive data.
  3. We can only store a limited amount of data in cookies. Most browsers don't allow a cookie to store more than 4KB of data. Breaking data into multiple cookies causes too much overhead in each request. Further, you can't even rely on a number of cookies allowed by the browser for each domain.

We can overcome these problems easily using Sessions. This is how session work:

When we use sessions the data is not stored directly in the browser instead it is stored in the server. Django creates a unique random string called session id or SID and associates SID with the data. The server then sends a cookie named sessionid containing SID as value to the browser. On requesting a page, the browser sends the request along with the cookie with SID to the server. Django then uses this SID to retrieve session data and makes it accessible in your code. SID generated by Django is a 32 characters long random string, so it is almost impossible to guess by an attacker.

Setting Up Sessions #

In Django, sessions are implemented using middleware. Open settings.py file and locate MIDDLEWARE list variable. If it has 'django.contrib.sessions.middleware.SessionMiddleware' as one of the elements then you are good to go. If you can't see it, just add it now to the MIDDLEWARE list. At this point, the MIDDLEWARE list should look like this:

TGDB/django_project/django_project/settings.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#...
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'django.middleware.common.BrokenLinkEmailsMiddleware',
]
#...

The 'django.contrib.sessions.middleware.SessionMiddleware' is responsible for generating unique SID.

Django provides an app called 'django.contrib.sessions' whose role is to store the session data into the database. So make sure you have 'django.contrib.sessions' in the INSTALLED_APPS list. If for some reason you don't have 'django.contrib.sessions' in INSTALLED_APPS, add it right now and run python manage.py migrate command. This command will create the necessary table to store the session data.

TGDB/django_project/django_project/settings.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#...
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog',
    'cadmin',
]
#...

Testing Sessions #

We have already discussed that users can configure their browsers to not accept any cookie. As a result, Django provides some convenience methods to check cookies support in the browser. The request.sessions object provides the following three methods to check cookies support in the browser.

Method What it does?
set_test_cookie() sets the cookie in the browser
test_cookie_worked() returns True if browser accepted the cookie(a browser accepted the cookie means, it has sent the cookie to the server in the next request), Otherwise False
delete_test_cookie() delete the test cookie

Let's try them!

Open blog app's views.py file and add following two views at the end of the file.

TGDB/django_project/blog/views.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#...
def stop_tracking(request):
#...

def test_session(request):
    request.session.set_test_cookie()
    return HttpResponse("Testing session cookie")


def test_delete(request):
    if request.session.test_cookie_worked():
        request.session.delete_test_cookie()
        response = HttpResponse("Cookie test passed")
    else:
        response = HttpResponse("Cookie test failed")
    return response

The test_session() view sets the cookie and test_delete() checks whether the browser accepted the cookie or not.

Add the following two URL patterns at the beginning of urlpatterns list in blog's urls.py.

TGDB/django_project/blog/urls.py

1
2
3
4
5
6
7
#...
urlpatterns = [    
    url(r'^test-delete/$', views.test_delete, name='test_delete'),
    url(r'^test-session/$', views.test_session, name='test_session'),
    url(r'^stop-tracking/$', views.stop_tracking, name='stop_tracking'),
    #...
]

Point your browser to http://127.0.0.1:8000/test-session/ and you should see a page like this:

Open Developer Tools hitting Ctrl+Shift+J and visit Application tab again. If your browser accepted the cookie it should look like this:

Now visit http://127.0.0.1:8000/test-delete/, if everything went fine, you would see "Cookie test passed" response.

On the other hand, if you get "Cookie test failed" message. Check the browser settings and allow websites to save cookies.

Reading and Writing Session Data #

To read and write session data we use session attribute of the request object. The session attribute acts like a dictionary.

Here is how we can save, read, and delete session data.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# set session data

request.session['name'] = 'tom'
request.session['age'] = '18'

# read session data

request.session.get('name') # returns 'tom'
request.session.get('age') # returns '18'

## delete session data

del request.session['name']
del request.session['age']

Open blog app's views.py and add the following three views at the end of the file.

TGDB/django_project/blog/views.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#...
def test_delete(request):
    #...

def save_session_data(request):
    # set new data
    request.session['id'] = 1
    request.session['name'] = 'root'
    request.session['password'] = 'rootpass'
    return HttpResponse("Session Data Saved")


def access_session_data(request):
    response = ""
    if request.session.get('id'):
        response += "Id : {0} <br>".format(request.session.get('id'))
    if request.session.get('name'):
        response += "Name : {0} <br>".format(request.session.get('name'))
    if request.session.get('password'):
        response += "Password : {0} <br>".format(request.session.get('password'))

    if not response:
        return HttpResponse("No session data")
    else:
        return HttpResponse(response)


def delete_session_data(request):
    try:
        del request.session['id']
        del request.session['name']
        del request.session['password']
    except KeyError:
        pass

    return HttpResponse("Session Data cleared")

here is what happen when save_session_data() view is called.

  1. 'django.contrib.sessions.middleware.SessionMiddleware' middleware creates a new random session id or SID and associates the session data with it.
  2. 'django.contrib.sessions.middleware.SessionMiddleware' uses 'django.contrib.sessions' app to store the session data in the database
  3. At last, a cookie named sessionid with a random value(i.e SID) generated in step 1, is sent to the browser.
  4. From now on, the browser would send this sessionid cookie with every request to the server, allowing Python code to access session data in views using request.session

Before we test this code add the following URL pattern to the blog's urls.py file:

TGDB/django_project/blog/urls.py

1
2
3
4
5
6
7
8
#...
urlpatterns = [
    url(r'^save-session-data/$', views.save_session_data, name='save_session_data'),
    url(r'^access-session-data/$', views.access_session_data, name='access_session_data'),
    url(r'^delete-session-data/$', views.delete_session_data, name='delete_session_data'),
    url(r'^test-delete/$', views.test_delete, name='test_delete'),
    #...
]

Visit http://127.0.0.1:8000/save-session-data/ and you should get the following output.

At this point, the browser should have a cookie named sessionid with a random SID.

To access session data visit http://127.0.0.1:8000/access-session-data/ and you should get the following output:

To delete the session data visit http://127.0.0.1:8000/delete-session-data/.

Visit http://127.0.0.1:8000/access-session-data/ again and you should see "No session data".

It is important to note that deleting session data from the database doesn't delete the session cookie in the browser. As a result, if you visit http://127.0.0.1:8000/save-session-data/, Django will reuse the SID sent by the browser to associate the session data with it again.

Modifying Session Data #

Here comes one of the most important thing you need to remember while using sessions:

Django saves the session data into the database only when it is modified.

By modified, Django means following actions:

  1. Adding new key-value pair to request.session dictionary.

    1
    2
    3
    >>> item_list = { 'eggs': '10', 'apples': '20' , 'mango': '30' }
    >>> request.session['newkey'] = 'newval'
    >>> request.session['items_to_buy'] = item_list
    
  2. Changing the value of an existing key.

    >>> request.session['newkey'] = 'newval100'
    
  3. Deleting the a key-value pair.

    >>> del request.session['newkey']
    

    Unfortunately, the following is not a modification.

    >>> request.session['items_to_buy']['eggs'] = '100'
    

    But why ? Here we are not modifying request.session dictionary instead we are modifying request.session['items_to_buy'].

    If you want Django to save these changes to the database, you have to set modified attribute of session object to True.

    request.session.modified = True
    

    To change this default behavior permanently, set SESSION_SAVE_EVERY_REQUEST to True in settings.py. When SESSION_SAVE_EVERY_REQUEST is set to True Django will save session data to the database on every request, even if you don't modify session data at all.

    Another important point worth mentioning here is that Django sends the session cookie to the browser only when session data is modified, in the process also updates the cookie expiry time.

    If SESSION_SAVE_EVERY_REQUEST is set to True, Django will send the session cookie on every request.

Sessions API #

Django provides an API which allows you to access session data outside of views.

1
2
3
4
5
6
7
8
>>>
>>> from django.contrib.sessions.models import Session
>>>
>>> s = Session.objects.get(pk='t2m5w8ng9x55fa2a78b8s680dc1ubxpl')
>>> s
>>>
<Session: t2m5w8ng9x55fa2a78b8s680dc1ubxpl>
>>>

The long string passed to the get() method is SID. You can copy it either from the browser or session_key column from the django_session table. By default, session data is stored in the django_session table. It consists of three columns as follows:

Column Explanation
session_key To store unique random session id or SID
session_data Django stores the session data in the encoded format. To get the data the raw data use get_decoded() method of the session object
expire_date expiration date of the session cookie

Once you have access to session object you can use it to query for other information.

1
2
3
4
5
>>>
>>> s.expire_date
datetime.datetime(2017, 5, 1, 8, 51, 11, 922337, tzinfo=<UTC>)
>>>
>>>
1
2
3
4
5
>>>
>>> s.session_data   # get the session data in encoded format
'MzM0MjViODhhNTU3Yjg5NjA2YjVmODdiNjJkOGIxYTdkNzIxNmVkYzp7InBhc3N3b3JkIjoicm9vdHB
hc3MifQ=='
>>>
1
2
3
4
>>>
>>> s.get_decoded() # get decoded session data
{}
>>>

As you can see, get_decoded() method returns an empty dictionary because in my case there is no data associated with the given SID. Had there been data associated with the SID the get_decoded() would have returned a dictionary like this:

1
2
3
4
>>>
>>> s.get_decoded()
{'password': 'rootpass', 'id': 1, 'name': 'root'}
>>>

Setting Expiration Time for Session Cookie #

We can control session cookies expiration time by setting the following two variables in settings.py file:

Variable Name Explanation
SESSION_COOKIE_AGE This variable is used to set cookie expiration time in seconds. By default, it is set to 1209600 seconds or 2 weeks. If SESSION_EXPIRE_AT_BROWSER_CLOSE is not set then Django uses this variable to set cookie expiration time. Here is how you can set session cookie expiration time to 5 days: SESSION_COOKIE_AGE = 3600*24*5
SESSION_EXPIRE_AT_BROWSER_CLOSE This variable controls whether to expire the session cookie when the user closes the browser. By default, it is set to False. If set to True, session cookie lasts until the browser is closed, irrespective of the value of SESSION_COOKIE_AGE.

Clearing Sessions #

Session data accumulates pretty fast especially if your site relies on sessions for different functionalities. It is a good idea to search for expired sessions regularly clean them before they start cluttering up your tables. Fortunately, Django provides clearsessions command to clear expired sessions from the tables.

(env) C:\Users\Q\TGDB\django_project>python manage.py clearsessions

Let's conclude this chapter by creating a small project - The lousy login form.

The Lousy Login #

Sessions are commonly used to create login systems. The motive of this section is to show you how login systems are implemented using sessions.

Open blog's views.py and add lousy_login(), lousy_secret() and lousy_logout() views to the end of the file.

TGDB/django_project/blog/views.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#...
from django_project import helpers
from django.contrib import messages

#...
def delete_session_data(request):
    #...

def lousy_login(request):
    if request.method == 'POST':
        username = request.POST.get('username')
        password = request.POST.get('password')
        if username == "root" and password == "pass":
            request.session['logged_in'] = True
            return redirect('lousy_secret')
        else:
            messages.error(request, 'Error wrong username/password')
    return render(request, 'blog/lousy_login.html')


def lousy_secret(request):
    if not request.session.get('logged_in'):
        return redirect('lousy_login')
    return render(request, 'blog/lousy_secret_page.html')


def lousy_logout(request):
    try:
        del request.session['logged_in']
    except KeyError:
        return redirect('lousy_login')
    return render(request, 'blog/lousy_logout.html')

Next create three new templates lousy_login.html, lousy_secret_page.html and lousy_logout.html with the following content.

TGDB/django_project/blog/templates/blog/lousy_login.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
{% extends "blog/base.html"  %}

{% block title %}
    Lousy Login - {{ block.super }}
{% endblock %}

{% block content %}

    <div class="content">
        <div class="section-inner clearfix">

        <h3>Login Form</h3>

        {% if messages %}
        <ul class="messages">
            {% for message in messages %}
            <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
            {% endfor %}
        </ul>
        {% endif %}

        <form action="" method="post">
            {% csrf_token %}
            <table>
                <tr>
                    <td><label for="id_username">Enter Username</label></td>
                    <td><input type="text" id="id_username" name="username"></td>
                </tr>
                <tr>
                    <td><label for="id_password">Enter Password</label></td>
                    <td><input type="text" id="id_password" name="password"></td>
                </tr>
                <tr>
                    <td></td>
                    <td><input type="submit" name="submit" value="Submit"></td>
                </tr>
            </table>
        </form>

        </div>
    </div>

{% endblock %}

TGDB/django_project/blog/templates/blog/lousy_secret_page.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
{% extends "blog/base.html"  %}

{% block title %}
    Lousy Secret Page - {{ block.super }}
{% endblock %}

{% block content %}

    <div class="content">
        <div class="section-inner clearfix">

        {% if request.session.logged_in %}
            <p>Welcome to the lousy secret page. You are seeing this page because you are logged in.</p>

            <p>To logout click <a href="{% url 'lousy_logout' %}">here</a></p>
        {% endif %}

        </div>
    </div>

{% endblock %}

TGDB/django_project/blog/templates/blog/lousy_logout.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
{% extends "blog/base.html"  %}

{% block title %}
    Blog - {{ block.super }}
{% endblock %}

{% block content %}

    <div class="content">
        <div class="section-inner clearfix">

        <p>You have been logged out click <a href="{% url 'lousy_login' %}">here</a> to login again</p>

        </div>
    </div>

{% endblock %}

Finally, add the following three URL patterns at the beginning of urlpatterns list in blog's urls.py.

TGDB/django_project/blog/urls.py

1
2
3
4
5
6
7
8
#...
urlpatterns = [   
    url(r'^lousy-login/$', views.lousy_login, name='lousy_login'),
    url(r'^lousy-secret/$', views.lousy_secret, name='lousy_secret'),
    url(r'^lousy-logout/$', views.lousy_logout, name='lousy_logout'),
    url(r'^save-session-data/$', views.save_session_data, name='save_session_data'),
    #...
]

Open the browser and visit http://127.0.0.1:8000/lousy-secret/, you will be redirected to http://127.0.0.1:8000/lousy-login/ because currently we are not authorized to view this page.

Type wrong a username/password combination or just hit "Submit" without entering anything and you would get "Error wrong username/password" error above the form as follows:

Enter correct username/password (i.e root and pass) and hit submit, this time you will be redirected to http://127.0.0.1:8000/lousy-secret/.

Click the logout link to logout from the page and this time you will be redirected to http://127.0.0.1:8000/lousy-logout/.

Obviously, this is a lousy way of logging in users. Nonetheless, it perfectly describes the role of sessions in creating login systems. Django has an authentication framework which makes logging in user into your app pretty easily. Django authentication framework is discussed in the next chapter.

Note: To checkout this version of the repository type git checkout 28a.