1 from __future__
import annotations
5 from collections
.abc
import MutableMapping
6 from datetime
import datetime
7 from datetime
import timezone
9 from itsdangerous
import BadSignature
10 from itsdangerous
import URLSafeTimedSerializer
11 from werkzeug
.datastructures
import CallbackDict
13 from .json
.tag
import TaggedJSONSerializer
15 if t
.TYPE_CHECKING
: # pragma: no cover
16 from .app
import Flask
17 from .wrappers
import Request
, Response
20 class SessionMixin(MutableMapping
):
21 """Expands a basic dictionary with session attributes."""
24 def permanent(self
) -> bool:
25 """This reflects the ``'_permanent'`` key in the dict."""
26 return self
.get("_permanent", False)
29 def permanent(self
, value
: bool) -> None:
30 self
["_permanent"] = bool(value
)
32 #: Some implementations can detect whether a session is newly
33 #: created, but that is not guaranteed. Use with caution. The mixin
34 # default is hard-coded ``False``.
37 #: Some implementations can detect changes to the session and set
38 #: this when that happens. The mixin default is hard coded to
42 #: Some implementations can detect when session data is read or
43 #: written and set this when that happens. The mixin default is hard
48 class SecureCookieSession(CallbackDict
, SessionMixin
):
49 """Base class for sessions based on signed cookies.
51 This session backend will set the :attr:`modified` and
52 :attr:`accessed` attributes. It cannot reliably track whether a
53 session is new (vs. empty), so :attr:`new` remains hard coded to
57 #: When data is changed, this is set to ``True``. Only the session
58 #: dictionary itself is tracked; if the session contains mutable
59 #: data (for example a nested dict) then this must be set to
60 #: ``True`` manually when modifying that data. The session cookie
61 #: will only be written to the response if this is ``True``.
64 #: When data is read or written, this is set to ``True``. Used by
65 # :class:`.SecureCookieSessionInterface` to add a ``Vary: Cookie``
66 #: header, which allows caching proxies to cache different pages for
70 def __init__(self
, initial
: t
.Any
= None) -> None:
71 def on_update(self
) -> None:
75 super().__init
__(initial
, on_update
)
77 def __getitem__(self
, key
: str) -> t
.Any
:
79 return super().__getitem
__(key
)
81 def get(self
, key
: str, default
: t
.Any
= None) -> t
.Any
:
83 return super().get(key
, default
)
85 def setdefault(self
, key
: str, default
: t
.Any
= None) -> t
.Any
:
87 return super().setdefault(key
, default
)
90 class NullSession(SecureCookieSession
):
91 """Class used to generate nicer error messages if sessions are not
92 available. Will still allow read-only access to the empty session
96 def _fail(self
, *args
: t
.Any
, **kwargs
: t
.Any
) -> t
.NoReturn
:
98 "The session is unavailable because no secret "
99 "key was set. Set the secret_key on the "
100 "application to something unique and secret."
103 __setitem__
= __delitem__
= clear
= pop
= popitem
= update
= setdefault
= _fail
# type: ignore # noqa: B950
107 class SessionInterface
:
108 """The basic interface you have to implement in order to replace the
109 default session interface which uses werkzeug's securecookie
110 implementation. The only methods you have to implement are
111 :meth:`open_session` and :meth:`save_session`, the others have
112 useful defaults which you don't need to change.
114 The session object returned by the :meth:`open_session` method has to
115 provide a dictionary like interface plus the properties and methods
116 from the :class:`SessionMixin`. We recommend just subclassing a dict
117 and adding that mixin::
119 class Session(dict, SessionMixin):
122 If :meth:`open_session` returns ``None`` Flask will call into
123 :meth:`make_null_session` to create a session that acts as replacement
124 if the session support cannot work because some requirement is not
125 fulfilled. The default :class:`NullSession` class that is created
126 will complain that the secret key was not set.
128 To replace the session interface on an application all you have to do
129 is to assign :attr:`flask.Flask.session_interface`::
131 app = Flask(__name__)
132 app.session_interface = MySessionInterface()
134 Multiple requests with the same session may be sent and handled
135 concurrently. When implementing a new session interface, consider
136 whether reads or writes to the backing store must be synchronized.
137 There is no guarantee on the order in which the session for each
138 request is opened or saved, it will occur in the order that requests
139 begin and end processing.
141 .. versionadded:: 0.8
144 #: :meth:`make_null_session` will look here for the class that should
145 #: be created when a null session is requested. Likewise the
146 #: :meth:`is_null_session` method will perform a typecheck against
148 null_session_class
= NullSession
150 #: A flag that indicates if the session interface is pickle based.
151 #: This can be used by Flask extensions to make a decision in regards
152 #: to how to deal with the session object.
154 #: .. versionadded:: 0.10
157 def make_null_session(self
, app
: Flask
) -> NullSession
:
158 """Creates a null session which acts as a replacement object if the
159 real session support could not be loaded due to a configuration
160 error. This mainly aids the user experience because the job of the
161 null session is to still support lookup without complaining but
162 modifications are answered with a helpful error message of what
165 This creates an instance of :attr:`null_session_class` by default.
167 return self
.null_session_class()
169 def is_null_session(self
, obj
: object) -> bool:
170 """Checks if a given object is a null session. Null sessions are
171 not asked to be saved.
173 This checks if the object is an instance of :attr:`null_session_class`
176 return isinstance(obj
, self
.null_session_class
)
178 def get_cookie_name(self
, app
: Flask
) -> str:
179 """The name of the session cookie. Uses``app.config["SESSION_COOKIE_NAME"]``."""
180 return app
.config
["SESSION_COOKIE_NAME"]
182 def get_cookie_domain(self
, app
: Flask
) -> str |
None:
183 """The value of the ``Domain`` parameter on the session cookie. If not set,
184 browsers will only send the cookie to the exact domain it was set from.
185 Otherwise, they will send it to any subdomain of the given value as well.
187 Uses the :data:`SESSION_COOKIE_DOMAIN` config.
189 .. versionchanged:: 2.3
190 Not set by default, does not fall back to ``SERVER_NAME``.
192 rv
= app
.config
["SESSION_COOKIE_DOMAIN"]
193 return rv
if rv
else None
195 def get_cookie_path(self
, app
: Flask
) -> str:
196 """Returns the path for which the cookie should be valid. The
197 default implementation uses the value from the ``SESSION_COOKIE_PATH``
198 config var if it's set, and falls back to ``APPLICATION_ROOT`` or
199 uses ``/`` if it's ``None``.
201 return app
.config
["SESSION_COOKIE_PATH"] or app
.config
["APPLICATION_ROOT"]
203 def get_cookie_httponly(self
, app
: Flask
) -> bool:
204 """Returns True if the session cookie should be httponly. This
205 currently just returns the value of the ``SESSION_COOKIE_HTTPONLY``
208 return app
.config
["SESSION_COOKIE_HTTPONLY"]
210 def get_cookie_secure(self
, app
: Flask
) -> bool:
211 """Returns True if the cookie should be secure. This currently
212 just returns the value of the ``SESSION_COOKIE_SECURE`` setting.
214 return app
.config
["SESSION_COOKIE_SECURE"]
216 def get_cookie_samesite(self
, app
: Flask
) -> str:
217 """Return ``'Strict'`` or ``'Lax'`` if the cookie should use the
218 ``SameSite`` attribute. This currently just returns the value of
219 the :data:`SESSION_COOKIE_SAMESITE` setting.
221 return app
.config
["SESSION_COOKIE_SAMESITE"]
223 def get_expiration_time(self
, app
: Flask
, session
: SessionMixin
) -> datetime |
None:
224 """A helper method that returns an expiration date for the session
225 or ``None`` if the session is linked to the browser session. The
226 default implementation returns now + the permanent session
227 lifetime configured on the application.
229 if session
.permanent
:
230 return datetime
.now(timezone
.utc
) + app
.permanent_session_lifetime
233 def should_set_cookie(self
, app
: Flask
, session
: SessionMixin
) -> bool:
234 """Used by session backends to determine if a ``Set-Cookie`` header
235 should be set for this session cookie for this response. If the session
236 has been modified, the cookie is set. If the session is permanent and
237 the ``SESSION_REFRESH_EACH_REQUEST`` config is true, the cookie is
240 This check is usually skipped if the session was deleted.
242 .. versionadded:: 0.11
245 return session
.modified
or (
246 session
.permanent
and app
.config
["SESSION_REFRESH_EACH_REQUEST"]
249 def open_session(self
, app
: Flask
, request
: Request
) -> SessionMixin |
None:
250 """This is called at the beginning of each request, after
251 pushing the request context, before matching the URL.
253 This must return an object which implements a dictionary-like
254 interface as well as the :class:`SessionMixin` interface.
256 This will return ``None`` to indicate that loading failed in
257 some way that is not immediately an error. The request
258 context will fall back to using :meth:`make_null_session`
261 raise NotImplementedError()
264 self
, app
: Flask
, session
: SessionMixin
, response
: Response
266 """This is called at the end of each request, after generating
267 a response, before removing the request context. It is skipped
268 if :meth:`is_null_session` returns ``True``.
270 raise NotImplementedError()
273 session_json_serializer
= TaggedJSONSerializer()
276 class SecureCookieSessionInterface(SessionInterface
):
277 """The default session interface that stores sessions in signed cookies
278 through the :mod:`itsdangerous` module.
281 #: the salt that should be applied on top of the secret key for the
282 #: signing of cookie based sessions.
283 salt
= "cookie-session"
284 #: the hash function to use for the signature. The default is sha1
285 digest_method
= staticmethod(hashlib
.sha1
)
286 #: the name of the itsdangerous supported key derivation. The default
288 key_derivation
= "hmac"
289 #: A python serializer for the payload. The default is a compact
290 #: JSON derived serializer with support for some extra Python types
291 #: such as datetime objects or tuples.
292 serializer
= session_json_serializer
293 session_class
= SecureCookieSession
295 def get_signing_serializer(self
, app
: Flask
) -> URLSafeTimedSerializer |
None:
296 if not app
.secret_key
:
298 signer_kwargs
= dict(
299 key_derivation
=self
.key_derivation
, digest_method
=self
.digest_method
301 return URLSafeTimedSerializer(
304 serializer
=self
.serializer
,
305 signer_kwargs
=signer_kwargs
,
308 def open_session(self
, app
: Flask
, request
: Request
) -> SecureCookieSession |
None:
309 s
= self
.get_signing_serializer(app
)
312 val
= request
.cookies
.get(self
.get_cookie_name(app
))
314 return self
.session_class()
315 max_age
= int(app
.permanent_session_lifetime
.total_seconds())
317 data
= s
.loads(val
, max_age
=max_age
)
318 return self
.session_class(data
)
320 return self
.session_class()
323 self
, app
: Flask
, session
: SessionMixin
, response
: Response
325 name
= self
.get_cookie_name(app
)
326 domain
= self
.get_cookie_domain(app
)
327 path
= self
.get_cookie_path(app
)
328 secure
= self
.get_cookie_secure(app
)
329 samesite
= self
.get_cookie_samesite(app
)
330 httponly
= self
.get_cookie_httponly(app
)
332 # Add a "Vary: Cookie" header if the session was accessed at all.
334 response
.vary
.add("Cookie")
336 # If the session is modified to be empty, remove the cookie.
337 # If the session is empty, return without setting the cookie.
340 response
.delete_cookie(
348 response
.vary
.add("Cookie")
352 if not self
.should_set_cookie(app
, session
):
355 expires
= self
.get_expiration_time(app
, session
)
356 val
= self
.get_signing_serializer(app
).dumps(dict(session
)) # type: ignore
367 response
.vary
.add("Cookie")