expires_at = modification + timedelta(seconds=settings.SESSION_COOKIE_AGE)
get_expiry_date
()
Returns the date this session will expire. For sessions with no custom
expiration (or those set to expire at browser close), this will equal the
date SESSION_COOKIE_AGE
seconds from now.
This function accepts the same keyword arguments as
get_expiry_age()
, and similar notes on usage apply.
get_expire_at_browser_close
()
Returns either True
or False
, depending on whether the user’s
session cookie will expire when the user’s web browser is closed.
cycle_key
()
Creates a new session key while retaining the current session data.
django.contrib.auth.login()
calls this method to mitigate against
session fixation.
Session serialization
By default, Django serializes session data using JSON. You can use the
SESSION_SERIALIZER
setting to customize the session serialization
format. Even with the caveats described in Write your own serializer, we highly
recommend sticking with JSON serialization especially if you are using the
cookie backend.
For example, here’s an attack scenario if you use pickle
to serialize
session data. If you’re using the signed cookie session backend and SECRET_KEY
(or any key of
SECRET_KEY_FALLBACKS
) is known by an attacker (there isn’t an
inherent vulnerability in Django that would cause it to leak), the attacker
could insert a string into their session which, when unpickled, executes
arbitrary code on the server. The technique for doing so is simple and easily
available on the internet. Although the cookie session storage signs the
cookie-stored data to prevent tampering, a SECRET_KEY
leak
immediately escalates to a remote code execution vulnerability.
Bundled serializers
class serializers.
JSONSerializer
A wrapper around the JSON serializer from django.core.signing
. Can
only serialize basic data types.
In addition, as JSON supports only string keys, note that using non-string
keys in request.session
won’t work as expected:
>>> # initial assignment
>>> request.session[0] = "bar"
>>> # subsequent requests following serialization & deserialization
>>> # of session data
>>> request.session[0] # KeyError
>>> request.session["0"]
'bar'
Similarly, data that can’t be encoded in JSON, such as non-UTF8 bytes like
'\xd9'
(which raises UnicodeDecodeError
), can’t be stored.
See the Write your own serializer section for more details on limitations
of JSON serialization.
Write your own serializer
Note that the JSONSerializer
cannot handle arbitrary Python data types. As is often the case, there is a
trade-off between convenience and security. If you wish to store more advanced
data types including datetime
and Decimal
in JSON backed sessions, you
will need to write a custom serializer (or convert such values to a JSON
serializable object before storing them in request.session
). While
serializing these values is often straightforward
(DjangoJSONEncoder
may be helpful),
writing a decoder that can reliably get back the same thing that you put in is
more fragile. For example, you run the risk of returning a datetime
that
was actually a string that just happened to be in the same format chosen for
datetime
s).
Your serializer class must implement two methods,
dumps(self, obj)
and loads(self, data)
, to serialize and deserialize
the dictionary of session data, respectively.
Session object guidelines
Use normal Python strings as dictionary keys on request.session
. This
is more of a convention than a hard-and-fast rule.
Session dictionary keys that begin with an underscore are reserved for
internal use by Django.
Don’t override request.session
with a new object, and don’t access or
set its attributes. Use it like a Python dictionary.
Examples
This simplistic view sets a has_commented
variable to True
after a user
posts a comment. It doesn’t let a user post a comment more than once:
def post_comment(request, new_comment):
if request.session.get("has_commented", False):
return HttpResponse("You've already commented.")
c = comments.Comment(comment=new_comment)
c.save()
request.session["has_commented"] = True
return HttpResponse("Thanks for your comment!")
This simplistic view logs in a “member” of the site:
def login(request):
m = Member.objects.get(username=request.POST["username"])
if m.check_password(request.POST["password"]):
request.session["member_id"] = m.id
return HttpResponse("You're logged in.")
else:
return HttpResponse("Your username and password didn't match.")
…And this one logs a member out, according to login()
above:
def logout(request):
try:
del request.session["member_id"]
except KeyError:
return HttpResponse("You're logged out.")
The standard django.contrib.auth.logout()
function actually does a bit
more than this to prevent inadvertent data leakage. It calls the
flush()
method of request.session
.
We are using this example as a demonstration of how to work with session
objects, not as a full logout()
implementation.
Setting test cookies
As a convenience, Django provides a way to test whether the user’s browser
accepts cookies. Call the set_test_cookie()
method of request.session
in a view, and call
test_cookie_worked()
in a subsequent view –
not in the same view call.
This awkward split between set_test_cookie()
and test_cookie_worked()
is necessary due to the way cookies work. When you set a cookie, you can’t
actually tell whether a browser accepted it until the browser’s next request.
It’s good practice to use
delete_test_cookie()
to clean up after
yourself. Do this after you’ve verified that the test cookie worked.
Here’s a typical usage example:
from django.http import HttpResponse
from django.shortcuts import render
def login(request):
if request.method == "POST":
if request.session.test_cookie_worked():
request.session.delete_test_cookie()
return HttpResponse("You're logged in.")
else:
return HttpResponse("Please enable cookies and try again.")
request.session.set_test_cookie()
return render(request, "foo/login_form.html")
The examples in this section import the SessionStore
object directly
from the django.contrib.sessions.backends.db
backend. In your own code,
you should consider importing SessionStore
from the session engine
designated by SESSION_ENGINE
, as below:
>>> from importlib import import_module
>>> from django.conf import settings
>>> SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
An API is available to manipulate session data outside of a view:
>>> from django.contrib.sessions.backends.db import SessionStore
>>> s = SessionStore()
>>> # stored as seconds since epoch since datetimes are not serializable in JSON.
>>> s["last_login"] = 1376587691
>>> s.create()
>>> s.session_key
'2b1189a188b44ad18c35e113ac6ceead'
>>> s = SessionStore(session_key="2b1189a188b44ad18c35e113ac6ceead")
>>> s["last_login"]
1376587691
SessionStore.create()
is designed to create a new session (i.e. one not
loaded from the session store and with session_key=None
). save()
is
designed to save an existing session (i.e. one loaded from the session store).
Calling save()
on a new session may also work but has a small chance of
generating a session_key
that collides with an existing one. create()
calls save()
and loops until an unused session_key
is generated.
If you’re using the django.contrib.sessions.backends.db
backend, each
session is a normal Django model. The Session
model is defined in
django/contrib/sessions/models.py. Because it’s a normal model, you can
access sessions using the normal Django database API:
>>> from django.contrib.sessions.models import Session
>>> s = Session.objects.get(pk="2b1189a188b44ad18c35e113ac6ceead")
>>> s.expire_date
datetime.datetime(2005, 8, 20, 13, 35, 12)
Note that you’ll need to call
get_decoded()
to get the session
dictionary. This is necessary because the dictionary is stored in an encoded
format:
>>> s.session_data
'KGRwMQpTJ19hdXRoX3VzZXJfaWQnCnAyCkkxCnMuMTExY2ZjODI2Yj...'
>>> s.get_decoded()
{'user_id': 42}
When sessions are saved
By default, Django only saves to the session database when the session has been
modified – that is if any of its dictionary values have been assigned or
deleted:
# Session is modified.
request.session["foo"] = "bar"
# Session is modified.
del request.session["foo"]
# Session is modified.
request.session["foo"] = {}
# Gotcha: Session is NOT modified, because this alters
# request.session['foo'] instead of request.session.
request.session["foo"]["bar"] = "baz"
In the last case of the above example, we can tell the session object
explicitly that it has been modified by setting the modified
attribute on
the session object:
request.session.modified = True
To change this default behavior, set the SESSION_SAVE_EVERY_REQUEST
setting to True
. When set to True
, Django will save the session to the
database on every single request.
Note that the session cookie is only sent when a session has been created or
modified. If SESSION_SAVE_EVERY_REQUEST
is True
, the session
cookie will be sent on every request.
Similarly, the expires
part of a session cookie is updated each time the
session cookie is sent.
The session is not saved if the response’s status code is 500.
Browser-length sessions vs. persistent sessions
You can control whether the session framework uses browser-length sessions vs.
persistent sessions with the SESSION_EXPIRE_AT_BROWSER_CLOSE
setting.
By default, SESSION_EXPIRE_AT_BROWSER_CLOSE
is set to False
,
which means session cookies will be stored in users’ browsers for as long as
SESSION_COOKIE_AGE
. Use this if you don’t want people to have to
log in every time they open a browser.
If SESSION_EXPIRE_AT_BROWSER_CLOSE
is set to True
, Django will
use browser-length cookies – cookies that expire as soon as the user closes
their browser. Use this if you want people to have to log in every time they
open a browser.
This setting is a global default and can be overwritten at a per-session level
by explicitly calling the set_expiry()
method
of request.session
as described above in using sessions in views.
Some browsers (Chrome, for example) provide settings that allow users to
continue browsing sessions after closing and reopening the browser. In
some cases, this can interfere with the
SESSION_EXPIRE_AT_BROWSER_CLOSE
setting and prevent sessions
from expiring on browser close. Please be aware of this while testing
Django applications which have the
SESSION_EXPIRE_AT_BROWSER_CLOSE
setting enabled.
Clearing the session store
As users create new sessions on your website, session data can accumulate in
your session store. If you’re using the database backend, the
django_session
database table will grow. If you’re using the file backend,
your temporary directory will contain an increasing number of files.
To understand this problem, consider what happens with the database backend.
When a user logs in, Django adds a row to the django_session
database
table. Django updates this row each time the session data changes. If the user
logs out manually, Django deletes the row. But if the user does not log out,
the row never gets deleted. A similar process happens with the file backend.
Django does not provide automatic purging of expired sessions. Therefore,
it’s your job to purge expired sessions on a regular basis. Django provides a
clean-up management command for this purpose: clearsessions
. It’s
recommended to call this command on a regular basis, for example as a daily
cron job.
Note that the cache backend isn’t vulnerable to this problem, because caches
automatically delete stale data. Neither is the cookie backend, because the
session data is stored by the users’ browsers.
Settings
A few Django settings give you control over session
behavior:
SESSION_CACHE_ALIAS
SESSION_COOKIE_AGE
SESSION_COOKIE_DOMAIN
SESSION_COOKIE_HTTPONLY
SESSION_COOKIE_NAME
SESSION_COOKIE_PATH
SESSION_COOKIE_SAMESITE
SESSION_COOKIE_SECURE
SESSION_ENGINE
SESSION_EXPIRE_AT_BROWSER_CLOSE
SESSION_FILE_PATH
SESSION_SAVE_EVERY_REQUEST
SESSION_SERIALIZER
Session security
Subdomains within a site are able to set cookies on the client for the whole
domain. This makes session fixation possible if cookies are permitted from
subdomains not controlled by trusted users.
For example, an attacker could log into good.example.com
and get a valid
session for their account. If the attacker has control over bad.example.com
,
they can use it to send their session key to you since a subdomain is permitted
to set cookies on *.example.com
. When you visit good.example.com
,
you’ll be logged in as the attacker and might inadvertently enter your
sensitive personal data (e.g. credit card info) into the attacker’s account.
Another possible attack would be if good.example.com
sets its
SESSION_COOKIE_DOMAIN
to "example.com"
which would cause
session cookies from that site to be sent to bad.example.com
.
Technical details
The session dictionary accepts any json
serializable value when using
JSONSerializer
.
Session data is stored in a database table named django_session
.
Django only sends a cookie if it needs to. If you don’t set any session
data, it won’t send a session cookie.
The SessionStore
object
When working with sessions internally, Django uses a session store object from
the corresponding session engine. By convention, the session store object class
is named SessionStore
and is located in the module designated by
SESSION_ENGINE
.
All SessionStore
classes available in Django inherit from
SessionBase
and implement data manipulation methods,
namely:
exists()
create()
save()
delete()
load()
clear_expired()
In order to build a custom session engine or to customize an existing one, you
may create a new class inheriting from SessionBase
or
any other existing SessionStore
class.
You can extend the session engines, but doing so with database-backed session
engines generally requires some extra effort (see the next section for
details).
Extending database-backed session engines
Creating a custom database-backed session engine built upon those included in
Django (namely db
and cached_db
) may be done by inheriting
AbstractBaseSession
and either SessionStore
class.
AbstractBaseSession
and BaseSessionManager
are importable from
django.contrib.sessions.base_session
so that they can be imported without
including django.contrib.sessions
in INSTALLED_APPS
.
class base_session.
AbstractBaseSession
The abstract base session model.
session_key
Primary key. The field itself may contain up to 40 characters. The
current implementation generates a 32-character string (a random
sequence of digits and lowercase ASCII letters).
expire_date
A datetime designating when the session expires.
Expired sessions are not available to a user, however, they may still
be stored in the database until the clearsessions
management
command is run.
encode
(session_dict)
Returns the given session dictionary serialized and encoded as a string.
Encoding is performed by the session store class tied to a model class.
save
(session_key, session_dict, expire_date)
Saves session data for a provided session key, or deletes the session
in case the data is empty.
Customization of SessionStore
classes is achieved by overriding methods
and properties described below:
class backends.db.
SessionStore
Implements database-backed session store.
classmethod get_model_class
()
Override this method to return a custom session model if you need one.
create_model_instance
(data)
Returns a new instance of the session model object, which represents
the current session state.
Overriding this method provides the ability to modify session model
data before it’s saved to database.
class backends.cached_db.
SessionStore
Implements cached database-backed session store.
cache_key_prefix
A prefix added to a session key to build a cache key string.
Example
The example below shows a custom database-backed session engine that includes
an additional database column to store an account ID (thus providing an option
to query the database for all active sessions for an account):
from django.contrib.sessions.backends.db import SessionStore as DBStore
from django.contrib.sessions.base_session import AbstractBaseSession
from django.db import models
class CustomSession(AbstractBaseSession):
account_id = models.IntegerField(null=True, db_index=True)
@classmethod
def get_session_store_class(cls):
return SessionStore
class SessionStore(DBStore):
@classmethod
def get_model_class(cls):
return CustomSession
def create_model_instance(self, data):
obj = super().create_model_instance(data)
try:
account_id = int(data.get("_auth_user_id"))
except (ValueError, TypeError):
account_id = None
obj.account_id = account_id
return obj
If you are migrating from the Django’s built-in cached_db
session store to
a custom one based on cached_db
, you should override the cache key prefix
in order to prevent a namespace clash: