Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/flask/sessions.py: 53%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3import collections.abc as c
4import hashlib
5import typing as t
6from collections.abc import MutableMapping
7from datetime import datetime
8from datetime import timezone
10from itsdangerous import BadSignature
11from itsdangerous import URLSafeTimedSerializer
12from werkzeug.datastructures import CallbackDict
14from .json.tag import TaggedJSONSerializer
16if t.TYPE_CHECKING: # pragma: no cover
17 import typing_extensions as te
19 from .app import Flask
20 from .wrappers import Request
21 from .wrappers import Response
24class SessionMixin(MutableMapping[str, t.Any]):
25 """Expands a basic dictionary with session attributes."""
27 @property
28 def permanent(self) -> bool:
29 """This reflects the ``'_permanent'`` key in the dict."""
30 return self.get("_permanent", False)
32 @permanent.setter
33 def permanent(self, value: bool) -> None:
34 self["_permanent"] = bool(value)
36 #: Some implementations can detect whether a session is newly
37 #: created, but that is not guaranteed. Use with caution. The mixin
38 # default is hard-coded ``False``.
39 new = False
41 #: Some implementations can detect changes to the session and set
42 #: this when that happens. The mixin default is hard coded to
43 #: ``True``.
44 modified = True
46 #: Some implementations can detect when session data is read or
47 #: written and set this when that happens. The mixin default is hard
48 #: coded to ``True``.
49 accessed = True
52class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin):
53 """Base class for sessions based on signed cookies.
55 This session backend will set the :attr:`modified` and
56 :attr:`accessed` attributes. It cannot reliably track whether a
57 session is new (vs. empty), so :attr:`new` remains hard coded to
58 ``False``.
59 """
61 #: When data is changed, this is set to ``True``. Only the session
62 #: dictionary itself is tracked; if the session contains mutable
63 #: data (for example a nested dict) then this must be set to
64 #: ``True`` manually when modifying that data. The session cookie
65 #: will only be written to the response if this is ``True``.
66 modified = False
68 #: When data is read or written, this is set to ``True``. Used by
69 # :class:`.SecureCookieSessionInterface` to add a ``Vary: Cookie``
70 #: header, which allows caching proxies to cache different pages for
71 #: different users.
72 accessed = False
74 def __init__(
75 self,
76 initial: c.Mapping[str, t.Any] | c.Iterable[tuple[str, t.Any]] | None = None,
77 ) -> None:
78 def on_update(self: te.Self) -> None:
79 self.modified = True
80 self.accessed = True
82 super().__init__(initial, on_update)
84 def __getitem__(self, key: str) -> t.Any:
85 self.accessed = True
86 return super().__getitem__(key)
88 def get(self, key: str, default: t.Any = None) -> t.Any:
89 self.accessed = True
90 return super().get(key, default)
92 def setdefault(self, key: str, default: t.Any = None) -> t.Any:
93 self.accessed = True
94 return super().setdefault(key, default)
97class NullSession(SecureCookieSession):
98 """Class used to generate nicer error messages if sessions are not
99 available. Will still allow read-only access to the empty session
100 but fail on setting.
101 """
103 def _fail(self, *args: t.Any, **kwargs: t.Any) -> t.NoReturn:
104 raise RuntimeError(
105 "The session is unavailable because no secret "
106 "key was set. Set the secret_key on the "
107 "application to something unique and secret."
108 )
110 __setitem__ = __delitem__ = clear = pop = popitem = update = setdefault = _fail # type: ignore # noqa: B950
111 del _fail
114class SessionInterface:
115 """The basic interface you have to implement in order to replace the
116 default session interface which uses werkzeug's securecookie
117 implementation. The only methods you have to implement are
118 :meth:`open_session` and :meth:`save_session`, the others have
119 useful defaults which you don't need to change.
121 The session object returned by the :meth:`open_session` method has to
122 provide a dictionary like interface plus the properties and methods
123 from the :class:`SessionMixin`. We recommend just subclassing a dict
124 and adding that mixin::
126 class Session(dict, SessionMixin):
127 pass
129 If :meth:`open_session` returns ``None`` Flask will call into
130 :meth:`make_null_session` to create a session that acts as replacement
131 if the session support cannot work because some requirement is not
132 fulfilled. The default :class:`NullSession` class that is created
133 will complain that the secret key was not set.
135 To replace the session interface on an application all you have to do
136 is to assign :attr:`flask.Flask.session_interface`::
138 app = Flask(__name__)
139 app.session_interface = MySessionInterface()
141 Multiple requests with the same session may be sent and handled
142 concurrently. When implementing a new session interface, consider
143 whether reads or writes to the backing store must be synchronized.
144 There is no guarantee on the order in which the session for each
145 request is opened or saved, it will occur in the order that requests
146 begin and end processing.
148 .. versionadded:: 0.8
149 """
151 #: :meth:`make_null_session` will look here for the class that should
152 #: be created when a null session is requested. Likewise the
153 #: :meth:`is_null_session` method will perform a typecheck against
154 #: this type.
155 null_session_class = NullSession
157 #: A flag that indicates if the session interface is pickle based.
158 #: This can be used by Flask extensions to make a decision in regards
159 #: to how to deal with the session object.
160 #:
161 #: .. versionadded:: 0.10
162 pickle_based = False
164 def make_null_session(self, app: Flask) -> NullSession:
165 """Creates a null session which acts as a replacement object if the
166 real session support could not be loaded due to a configuration
167 error. This mainly aids the user experience because the job of the
168 null session is to still support lookup without complaining but
169 modifications are answered with a helpful error message of what
170 failed.
172 This creates an instance of :attr:`null_session_class` by default.
173 """
174 return self.null_session_class()
176 def is_null_session(self, obj: object) -> bool:
177 """Checks if a given object is a null session. Null sessions are
178 not asked to be saved.
180 This checks if the object is an instance of :attr:`null_session_class`
181 by default.
182 """
183 return isinstance(obj, self.null_session_class)
185 def get_cookie_name(self, app: Flask) -> str:
186 """The name of the session cookie. Uses``app.config["SESSION_COOKIE_NAME"]``."""
187 return app.config["SESSION_COOKIE_NAME"] # type: ignore[no-any-return]
189 def get_cookie_domain(self, app: Flask) -> str | None:
190 """The value of the ``Domain`` parameter on the session cookie. If not set,
191 browsers will only send the cookie to the exact domain it was set from.
192 Otherwise, they will send it to any subdomain of the given value as well.
194 Uses the :data:`SESSION_COOKIE_DOMAIN` config.
196 .. versionchanged:: 2.3
197 Not set by default, does not fall back to ``SERVER_NAME``.
198 """
199 return app.config["SESSION_COOKIE_DOMAIN"] # type: ignore[no-any-return]
201 def get_cookie_path(self, app: Flask) -> str:
202 """Returns the path for which the cookie should be valid. The
203 default implementation uses the value from the ``SESSION_COOKIE_PATH``
204 config var if it's set, and falls back to ``APPLICATION_ROOT`` or
205 uses ``/`` if it's ``None``.
206 """
207 return app.config["SESSION_COOKIE_PATH"] or app.config["APPLICATION_ROOT"] # type: ignore[no-any-return]
209 def get_cookie_httponly(self, app: Flask) -> bool:
210 """Returns True if the session cookie should be httponly. This
211 currently just returns the value of the ``SESSION_COOKIE_HTTPONLY``
212 config var.
213 """
214 return app.config["SESSION_COOKIE_HTTPONLY"] # type: ignore[no-any-return]
216 def get_cookie_secure(self, app: Flask) -> bool:
217 """Returns True if the cookie should be secure. This currently
218 just returns the value of the ``SESSION_COOKIE_SECURE`` setting.
219 """
220 return app.config["SESSION_COOKIE_SECURE"] # type: ignore[no-any-return]
222 def get_cookie_samesite(self, app: Flask) -> str | None:
223 """Return ``'Strict'`` or ``'Lax'`` if the cookie should use the
224 ``SameSite`` attribute. This currently just returns the value of
225 the :data:`SESSION_COOKIE_SAMESITE` setting.
226 """
227 return app.config["SESSION_COOKIE_SAMESITE"] # type: ignore[no-any-return]
229 def get_cookie_partitioned(self, app: Flask) -> bool:
230 """Returns True if the cookie should be partitioned. By default, uses
231 the value of :data:`SESSION_COOKIE_PARTITIONED`.
233 .. versionadded:: 3.1
234 """
235 return app.config["SESSION_COOKIE_PARTITIONED"] # type: ignore[no-any-return]
237 def get_expiration_time(self, app: Flask, session: SessionMixin) -> datetime | None:
238 """A helper method that returns an expiration date for the session
239 or ``None`` if the session is linked to the browser session. The
240 default implementation returns now + the permanent session
241 lifetime configured on the application.
242 """
243 if session.permanent:
244 return datetime.now(timezone.utc) + app.permanent_session_lifetime
245 return None
247 def should_set_cookie(self, app: Flask, session: SessionMixin) -> bool:
248 """Used by session backends to determine if a ``Set-Cookie`` header
249 should be set for this session cookie for this response. If the session
250 has been modified, the cookie is set. If the session is permanent and
251 the ``SESSION_REFRESH_EACH_REQUEST`` config is true, the cookie is
252 always set.
254 This check is usually skipped if the session was deleted.
256 .. versionadded:: 0.11
257 """
259 return session.modified or (
260 session.permanent and app.config["SESSION_REFRESH_EACH_REQUEST"]
261 )
263 def open_session(self, app: Flask, request: Request) -> SessionMixin | None:
264 """This is called at the beginning of each request, after
265 pushing the request context, before matching the URL.
267 This must return an object which implements a dictionary-like
268 interface as well as the :class:`SessionMixin` interface.
270 This will return ``None`` to indicate that loading failed in
271 some way that is not immediately an error. The request
272 context will fall back to using :meth:`make_null_session`
273 in this case.
274 """
275 raise NotImplementedError()
277 def save_session(
278 self, app: Flask, session: SessionMixin, response: Response
279 ) -> None:
280 """This is called at the end of each request, after generating
281 a response, before removing the request context. It is skipped
282 if :meth:`is_null_session` returns ``True``.
283 """
284 raise NotImplementedError()
287session_json_serializer = TaggedJSONSerializer()
290def _lazy_sha1(string: bytes = b"") -> t.Any:
291 """Don't access ``hashlib.sha1`` until runtime. FIPS builds may not include
292 SHA-1, in which case the import and use as a default would fail before the
293 developer can configure something else.
294 """
295 return hashlib.sha1(string)
298class SecureCookieSessionInterface(SessionInterface):
299 """The default session interface that stores sessions in signed cookies
300 through the :mod:`itsdangerous` module.
301 """
303 #: the salt that should be applied on top of the secret key for the
304 #: signing of cookie based sessions.
305 salt = "cookie-session"
306 #: the hash function to use for the signature. The default is sha1
307 digest_method = staticmethod(_lazy_sha1)
308 #: the name of the itsdangerous supported key derivation. The default
309 #: is hmac.
310 key_derivation = "hmac"
311 #: A python serializer for the payload. The default is a compact
312 #: JSON derived serializer with support for some extra Python types
313 #: such as datetime objects or tuples.
314 serializer = session_json_serializer
315 session_class = SecureCookieSession
317 def get_signing_serializer(self, app: Flask) -> URLSafeTimedSerializer | None:
318 if not app.secret_key:
319 return None
321 keys: list[str | bytes] = []
323 if fallbacks := app.config["SECRET_KEY_FALLBACKS"]:
324 keys.extend(fallbacks)
326 keys.append(app.secret_key) # itsdangerous expects current key at top
327 return URLSafeTimedSerializer(
328 keys, # type: ignore[arg-type]
329 salt=self.salt,
330 serializer=self.serializer,
331 signer_kwargs={
332 "key_derivation": self.key_derivation,
333 "digest_method": self.digest_method,
334 },
335 )
337 def open_session(self, app: Flask, request: Request) -> SecureCookieSession | None:
338 s = self.get_signing_serializer(app)
339 if s is None:
340 return None
341 val = request.cookies.get(self.get_cookie_name(app))
342 if not val:
343 return self.session_class()
344 max_age = int(app.permanent_session_lifetime.total_seconds())
345 try:
346 data = s.loads(val, max_age=max_age)
347 return self.session_class(data)
348 except BadSignature:
349 return self.session_class()
351 def save_session(
352 self, app: Flask, session: SessionMixin, response: Response
353 ) -> None:
354 name = self.get_cookie_name(app)
355 domain = self.get_cookie_domain(app)
356 path = self.get_cookie_path(app)
357 secure = self.get_cookie_secure(app)
358 partitioned = self.get_cookie_partitioned(app)
359 samesite = self.get_cookie_samesite(app)
360 httponly = self.get_cookie_httponly(app)
362 # Add a "Vary: Cookie" header if the session was accessed at all.
363 if session.accessed:
364 response.vary.add("Cookie")
366 # If the session is modified to be empty, remove the cookie.
367 # If the session is empty, return without setting the cookie.
368 if not session:
369 if session.modified:
370 response.delete_cookie(
371 name,
372 domain=domain,
373 path=path,
374 secure=secure,
375 partitioned=partitioned,
376 samesite=samesite,
377 httponly=httponly,
378 )
379 response.vary.add("Cookie")
381 return
383 if not self.should_set_cookie(app, session):
384 return
386 expires = self.get_expiration_time(app, session)
387 val = self.get_signing_serializer(app).dumps(dict(session)) # type: ignore[union-attr]
388 response.set_cookie(
389 name,
390 val,
391 expires=expires,
392 httponly=httponly,
393 domain=domain,
394 path=path,
395 secure=secure,
396 partitioned=partitioned,
397 samesite=samesite,
398 )
399 response.vary.add("Cookie")