.. index::
   single: session

.. _sessions_chapter:

Sessions
========

A :term:`session` is a namespace which is valid for some period of continual
activity that can be used to represent a user's interaction with a web
application.

This chapter describes how to configure sessions, what session implementations
:app:`Pyramid` provides out of the box, how to store and retrieve data from
sessions, and a session-specific feature: flash messages.

.. index::
   single: session factory (default)

.. _using_the_default_session_factory:

Using the Default Session Factory
---------------------------------

In order to use sessions, you must set up a :term:`session factory` during your
:app:`Pyramid` configuration.

A very basic, insecure sample session factory implementation is provided in the
:app:`Pyramid` core.  It uses a cookie to store session information.  This
implementation has the following limitations:

- The session information in the cookies used by this implementation is *not*
  encrypted, so it can be viewed by anyone with access to the cookie storage of
  the user's browser or anyone with access to the network along which the
  cookie travels.

- The maximum number of bytes that are storable in a serialized representation
  of the session is fewer than 4000.  This is suitable only for very small data
  sets.

It is digitally signed, however, and thus its data cannot easily be tampered
with.

You can configure this session factory in your :app:`Pyramid` application by
using the :meth:`pyramid.config.Configurator.set_session_factory` method.

.. code-block:: python
    :linenos:

    from pyramid.session import SignedCookieSessionFactory
    my_session_factory = SignedCookieSessionFactory('itsaseekreet')

    from pyramid.config import Configurator
    config = Configurator()
    config.set_session_factory(my_session_factory)

.. warning::

   By default the :func:`~pyramid.session.SignedCookieSessionFactory`
   implementation contains the following security concerns:

   - Session data is *unencrypted* (but it is signed / authenticated).

     This means an attacker cannot change the session data, but they can view it.
     You should not use it when you keep sensitive information in the session object, as the information can be easily read by both users of your application and third parties who have access to your users' network traffic.

     At the very least, use TLS and set ``secure=True`` to avoid arbitrary users on the network from viewing the session contents.

   - If you use this sessioning implementation, and you inadvertently create a cross-site scripting vulnerability in your application, because the session data is stored unencrypted in a cookie, it will also be easier for evildoers to obtain the current user's cross-site scripting token.

     Set ``httponly=True`` to mitigate this vulnerability by hiding the cookie from client-side JavaScript.

   - The default serialization method, while replaceable with something like JSON, is implemented using pickle which can lead to remote code execution if your secret key is compromised.

     To mitigate this, set ``serializer=pyramid.session.JSONSerializer()`` to use :class:`pyramid.session.JSONSerializer`. This option will be the default in :app:`Pyramid` 2.0.
     See :ref:`pickle_session_deprecation` for more information about this change.

   In short, use a different session factory implementation (preferably one which keeps session data on the server) for anything but the most basic of applications where "session security doesn't matter", you are sure your application has no cross-site scripting vulnerabilities, and you are confident your secret key will not be exposed.

.. index::
    triple: pickle deprecation; JSON-serializable; ISession interface

.. _pickle_session_deprecation:

Upcoming Changes to ISession in Pyramid 2.0
-------------------------------------------

In :app:`Pyramid` 2.0 the :class:`pyramid.interfaces.ISession` interface will be changing to require that session implementations only need to support JSON-serializable data types.
This is a stricter contract than the current requirement that all objects be pickleable and it is being done for security purposes.
This is a backward-incompatible change.
Currently, if a client-side session implementation is compromised, it leaves the application vulnerable to remote code execution attacks using specially-crafted sessions that execute code when deserialized.

For users with compatibility concerns, it's possible to craft a serializer that can handle both formats until you are satisfied that clients have had time to reasonably upgrade.
Remember that sessions should be short-lived and thus the number of clients affected should be small (no longer than an auth token, at a maximum). An example serializer:

.. code-block:: python
    :linenos:

    from pyramid.session import JSONSerializer
    from pyramid.session import PickleSerializer
    from pyramid.session import SignedCookieSessionFactory

    class JSONSerializerWithPickleFallback(object):
        def __init__(self):
            self.json = JSONSerializer()
            self.pickle = PickleSerializer()

        def dumps(self, value):
            # maybe catch serialization errors here and keep using pickle
            # while finding spots in your app that are not storing
            # JSON-serializable objects, falling back to pickle
            return self.json.dumps(value)

        def loads(self, value):
            try:
                return self.json.loads(value)
            except ValueError:
                return self.pickle.loads(value)

    # somewhere in your configuration code
    serializer = JSONSerializerWithPickleFallback()
    session_factory = SignedCookieSessionFactory(..., serializer=serializer)
    config.set_session_factory(session_factory)

.. index::
   single: session object

Using a Session Object
----------------------

Once a session factory has been configured for your application, you can access
session objects provided by the session factory via the ``session`` attribute
of any :term:`request` object.  For example:

.. code-block:: python
    :linenos:

    from pyramid.response import Response

    def myview(request):
        session = request.session
        if 'abc' in session:
            session['fred'] = 'yes'
        session['abc'] = '123'
        if 'fred' in session:
            return Response('Fred was in the session')
        else:
            return Response('Fred was not in the session')

The first time this view is invoked produces ``Fred was not in the session``.
Subsequent invocations produce ``Fred was in the session``, assuming of course
that the client side maintains the session's identity across multiple requests.

You can use a session much like a Python dictionary.  It supports all
dictionary methods, along with some extra attributes and methods.

Extra attributes:

``created``
  An integer timestamp indicating the time that this session was created.

``new``
  A boolean.  If ``new`` is True, this session is new.  Otherwise, it has been
  constituted from data that was already serialized.

Extra methods:

``changed()``
  Call this when you mutate a mutable value in the session namespace. See the
  gotchas below for details on when and why you should call this.

``invalidate()``
  Call this when you want to invalidate the session (dump all data, and perhaps
  set a clearing cookie).

The formal definition of the methods and attributes supported by the session
object are in the :class:`pyramid.interfaces.ISession` documentation.

Some gotchas:

- Keys and values of session data must be *pickleable*.  This means, typically,
  that they are instances of basic types of objects, such as strings, lists,
  dictionaries, tuples, integers, etc.  If you place an object in a session
  data key or value that is not pickleable, an error will be raised when the
  session is serialized. Please also see :ref:`pickle_session_deprecation`.

- If you place a mutable value (for example, a list or a dictionary) in a
  session object, and you subsequently mutate that value, you must call the
  ``changed()`` method of the session object. In this case, the session has no
  way to know that it was modified.  However, when you modify a session object
  directly, such as setting a value (i.e., ``__setitem__``), or removing a key
  (e.g., ``del`` or ``pop``), the session will automatically know that it needs
  to re-serialize its data, thus calling ``changed()`` is unnecessary. There is
  no harm in calling ``changed()`` in either case, so when in doubt, call it
  after you've changed sessioning data.

.. index::
   single: pyramid_redis_sessions
   single: session factory (alternates)

.. _using_alternate_session_factories:

Using Alternate Session Factories
---------------------------------

The following session factories exist at the time of this writing.

======================= ======= =============================
Session Factory         Backend   Description
======================= ======= =============================
pyramid_nacl_session_   PyNaCl_ Defines an encrypting,
                                pickle-based cookie
                                serializer, using PyNaCl to
                                generate the symmetric
                                encryption for the cookie
                                state.
pyramid_redis_sessions_ Redis_  Server-side session library
                                for Pyramid, using Redis for
                                storage.
pyramid_beaker_         Beaker_ Session factory for Pyramid
                                backed by the Beaker
                                sessioning system.
======================= ======= =============================

.. _pyramid_nacl_session: https://pypi.org/project/pyramid_nacl_session/
.. _PyNaCl: https://pynacl.readthedocs.io/en/latest/secret/

.. _pyramid_redis_sessions: https://pypi.org/project/pyramid_redis_sessions/
.. _Redis: https://redis.io/

.. _pyramid_beaker: https://pypi.org/project/pyramid_beaker/
.. _Beaker: https://beaker.readthedocs.io/en/latest/

.. index::
   single: session factory (custom)

Creating Your Own Session Factory
---------------------------------

If none of the default or otherwise available sessioning implementations for
:app:`Pyramid` suit you, you may create your own session object by implementing
a :term:`session factory`.  Your session factory should return a
:term:`session`.  The interfaces for both types are available in
:class:`pyramid.interfaces.ISessionFactory` and
:class:`pyramid.interfaces.ISession`. You might use the cookie implementation
in the :mod:`pyramid.session` module as inspiration.

.. index::
   single: flash messages

.. _flash_messages:

Flash Messages
--------------

"Flash messages" are simply a queue of message strings stored in the
:term:`session`.  To use flash messaging, you must enable a :term:`session
factory` as described in :ref:`using_the_default_session_factory` or
:ref:`using_alternate_session_factories`.

Flash messaging has two main uses: to display a status message only once to the
user after performing an internal redirect, and to allow generic code to log
messages for single-time display without having direct access to an HTML
template. The user interface consists of a number of methods of the
:term:`session` object.

.. index::
   single: session.flash

Using the ``session.flash`` Method
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

To add a message to a flash message queue, use a session object's ``flash()``
method:

.. code-block:: python

    request.session.flash('mymessage')

The ``flash()`` method appends a message to a flash queue, creating the queue
if necessary.

``flash()`` accepts three arguments:

.. method:: flash(message, queue='', allow_duplicate=True)

The ``message`` argument is required.  It represents a message you wish to
later display to a user.  It is usually a string but the ``message`` you
provide is not modified in any way.

The ``queue`` argument allows you to choose a queue to which to append the
message you provide.  This can be used to push different kinds of messages into
flash storage for later display in different places on a page.  You can pass
any name for your queue, but it must be a string. Each queue is independent,
and can be popped by ``pop_flash()`` or examined via ``peek_flash()``
separately.  ``queue`` defaults to the empty string.  The empty string
represents the default flash message queue.

.. code-block:: python

    request.session.flash(msg, 'myappsqueue')

The ``allow_duplicate`` argument defaults to ``True``.  If this is ``False``,
and you attempt to add a message value which is already present in the queue,
it will not be added.

.. index::
   single: session.pop_flash

Using the ``session.pop_flash`` Method
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Once one or more messages have been added to a flash queue by the
``session.flash()`` API, the ``session.pop_flash()`` API can be used to pop an
entire queue and return it for use.

To pop a particular queue of messages from the flash object, use the session
object's ``pop_flash()`` method. This returns a list of the messages that were
added to the flash queue, and empties the queue.

.. method:: pop_flash(queue='')

.. testsetup::

   from pyramid import testing
   request = testing.DummyRequest()

.. doctest::

   >>> request.session.flash('info message')
   >>> request.session.pop_flash()
   ['info message']

Calling ``session.pop_flash()`` again like above without a corresponding call
to ``session.flash()`` will return an empty list, because the queue has already
been popped.

.. doctest::

   >>> request.session.flash('info message')
   >>> request.session.pop_flash()
   ['info message']
   >>> request.session.pop_flash()
   []

.. index::
   single: session.peek_flash

Using the ``session.peek_flash`` Method
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Once one or more messages have been added to a flash queue by the
``session.flash()`` API, the ``session.peek_flash()`` API can be used to "peek"
at that queue.  Unlike ``session.pop_flash()``, the queue is not popped from
flash storage.

.. method:: peek_flash(queue='')

.. doctest::

   >>> request.session.flash('info message')
   >>> request.session.peek_flash()
   ['info message']
   >>> request.session.peek_flash()
   ['info message']
   >>> request.session.pop_flash()
   ['info message']
   >>> request.session.peek_flash()
   []
