Sessions in Django

In the last chapter we have learned how cookies allows 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) Can't store sensitive data.

3) We can only store 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 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 request along with the cookie with SID to the server. Django then uses this SID to retieve 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 MIDDLEWARE list should look like this:

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',
]

This '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 would create necessary table to store the session data.

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 also 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 support for cookies 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 try them !

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

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

Our 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.

urlpatterns = [    
    url(r'^test-delete/$', views.test_delete, name='test_delete'),
    url(r'^test-session/$', views.test_session, name='test_session'),
    ...
]

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+I and click on the network tab. If you 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".

[]

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.

# 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.

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 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:

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'),
    ...
]

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

[]

At this point browser should have a cookie named sessionid with some 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 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 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 is one of the 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 things:

Django sends the session cookie to the browser only when session data is modified. By modified Django means following things:

1) Adding new key-value pair to the session dictionary.

    >>> 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 session dictionary instead we are modifying item_list.

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 it 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.

>>>
>>> from django.contrib.sessions.models import Session
>>>
>>> s = Session.objects.get(pk='t2m5w8ng9x55fa2a78b8s680dc1ubxpl')
>>> s
>>>
<Session: t2m5w8ng9x55fa2a78b8s680dc1ubxpl>
>>>

The long string passed to get() method is SID. You can copy it either from the browser or session_key column from django_session table. By default session data is stored in 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 encoded format. To get the data the raw data use get_decoded() method on session object
expire_date expiration date of session cookie

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

>>>
>>> s.expire_date
datetime.datetime(2017, 5, 1, 8, 51, 11, 922337, tzinfo=<UTC>)
>>>
>>>

>>>
>>> s.session_data   # get the session data in encoded format
'MzM0MjViODhhNTU3Yjg5NjA2YjVmODdiNjJkOGIxYTdkNzIxNmVkYzp7InBhc3N3b3JkIjoicm9vdHB
hc3MifQ=='
>>>

>>>
>>> s.get_decoded() # get decoded session data
{}
>>>

Notice that here get_decoded() returns an empty dictionary because we have cleared the session data by visiting http://127.0.0.1:8000/delete-session-data/. Let try and see what it would return in case session data is not cleared.

First visit http://127.0.0.1:8000/save-session-data/ to create some session data. Come back to comand prompt/terminal and execute the following command.

>>>
>>> s = Session.objects.get(pk='1mjdck7y4o6tj94s8bqwwn9d6ff7ry9i')
>>>
>>> s.session_data
'YzU0ODM1NDE2ZDEzNDJiNjg0ODU4YWNjMGMzMjVmMzA5NjYwMmJiZDp7InBhc3N3b3JkIjoicm9vdHB
hc3MiLCJpZCI6MSwibmFtZSI6InJvb3QifQ=='
>>>
>>> s.get_decoded()
{'password': 'rootpass', 'id': 1, 'name': 'root'}
>>>
>>>

As expected get_decoded() returns dictionary containing session data.

Setting Expiration Time for Session Cookie

We can control expiration time for session cookies by using 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 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 especailly 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 you tables. Fortunately Django provides clearsessions command to clear expired sessions from the tables.

(env) H:\Q\ws2\django_project>python manage.py clearsessions

Let's finish this chapter by creating a lousy login form.

The Lousy Login

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

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

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.

lousy_login.html

{% 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 %}

lousy_secret_page.html

{% 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 %}

lousy_logout.html

{% 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.

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'),
    ...
]

Open 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 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 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 logout 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 easy. Django authentication framework is discussed in the next chapter.