authentik.core.sessions

authentik sessions engine

  1"""authentik sessions engine"""
  2
  3import pickle  # nosec
  4
  5from django.contrib.auth import BACKEND_SESSION_KEY, HASH_SESSION_KEY, SESSION_KEY
  6from django.contrib.sessions.backends.db import SessionStore as SessionBase
  7from django.core.exceptions import SuspiciousOperation
  8from django.utils import timezone
  9from django.utils.functional import cached_property
 10from structlog.stdlib import get_logger
 11
 12from authentik.root.middleware import ClientIPMiddleware
 13
 14LOGGER = get_logger()
 15
 16
 17class SessionStore(SessionBase):
 18    def __init__(self, session_key=None, last_ip=None, last_user_agent=""):
 19        super().__init__(session_key)
 20        self._create_kwargs = {
 21            "last_ip": last_ip or ClientIPMiddleware.default_ip,
 22            "last_user_agent": last_user_agent,
 23        }
 24
 25    @classmethod
 26    def get_model_class(cls):
 27        from authentik.core.models import Session
 28
 29        return Session
 30
 31    @cached_property
 32    def model_fields(self):
 33        return [k.value for k in self.model.Keys]
 34
 35    def _get_session_from_db(self):
 36        try:
 37            return self.model.objects.select_related(
 38                "authenticatedsession",
 39                "authenticatedsession__user",
 40            ).get(
 41                session_key=self.session_key,
 42                expires__gt=timezone.now(),
 43            )
 44        except (self.model.DoesNotExist, SuspiciousOperation) as exc:
 45            if isinstance(exc, SuspiciousOperation):
 46                LOGGER.warning(str(exc))
 47            self._session_key = None
 48
 49    async def _aget_session_from_db(self):
 50        try:
 51            return await self.model.objects.select_related(
 52                "authenticatedsession",
 53                "authenticatedsession__user",
 54            ).aget(
 55                session_key=self.session_key,
 56                expires__gt=timezone.now(),
 57            )
 58        except (self.model.DoesNotExist, SuspiciousOperation) as exc:
 59            if isinstance(exc, SuspiciousOperation):
 60                LOGGER.warning(str(exc))
 61            self._session_key = None
 62
 63    def encode(self, session_dict):
 64        return pickle.dumps(session_dict, protocol=pickle.HIGHEST_PROTOCOL)
 65
 66    def decode(self, session_data):
 67        try:
 68            return pickle.loads(session_data)  # nosec
 69        except pickle.PickleError, AttributeError, TypeError:
 70            # PickleError, ValueError - unpickling exceptions
 71            # AttributeError - can happen when Django model fields (e.g., FileField) are unpickled
 72            #                  and their descriptors fail to initialize (e.g., missing storage)
 73            # TypeError - can happen with incompatible pickled objects
 74            # If any of these happen, just return an empty dictionary (an empty session)
 75            LOGGER.warning("Failed to decode session data", exc_info=True)
 76            pass
 77        return {}
 78
 79    def load(self):
 80        s = self._get_session_from_db()
 81        if s:
 82            return {
 83                "authenticatedsession": getattr(s, "authenticatedsession", None),
 84                **{k: getattr(s, k) for k in self.model_fields},
 85                **self.decode(s.session_data),
 86            }
 87        else:
 88            return {}
 89
 90    async def aload(self):
 91        s = await self._aget_session_from_db()
 92        if s:
 93            return {
 94                "authenticatedsession": getattr(s, "authenticatedsession", None),
 95                **{k: getattr(s, k) for k in self.model_fields},
 96                **self.decode(s.session_data),
 97            }
 98        else:
 99            return {}
100
101    def create_model_instance(self, data):
102        args = {
103            "session_key": self._get_or_create_session_key(),
104            "expires": self.get_expiry_date(),
105            "session_data": {},
106            **self._create_kwargs,
107        }
108        for k, v in data.items():
109            # Don't save:
110            # - unused auth data
111            # - related models
112            if k in [SESSION_KEY, BACKEND_SESSION_KEY, HASH_SESSION_KEY, "authenticatedsession"]:
113                pass
114            elif k in self.model_fields:
115                args[k] = v
116            else:
117                args["session_data"][k] = v
118        args["session_data"] = self.encode(args["session_data"])
119        return self.model(**args)
120
121    async def acreate_model_instance(self, data):
122        args = {
123            "session_key": await self._aget_or_create_session_key(),
124            "expires": await self.aget_expiry_date(),
125            "session_data": {},
126            **self._create_kwargs,
127        }
128        for k, v in data.items():
129            # Don't save:
130            # - unused auth data
131            # - related models
132            if k in [SESSION_KEY, BACKEND_SESSION_KEY, HASH_SESSION_KEY, "authenticatedsession"]:
133                pass
134            elif k in self.model_fields:
135                args[k] = v
136            else:
137                args["session_data"][k] = v
138        args["session_data"] = self.encode(args["session_data"])
139        return self.model(**args)
140
141    @classmethod
142    def clear_expired(cls):
143        cls.get_model_class().objects.filter(expires__lt=timezone.now()).delete()
144
145    @classmethod
146    async def aclear_expired(cls):
147        await cls.get_model_class().objects.filter(expires__lt=timezone.now()).adelete()
148
149    def cycle_key(self):
150        data = self._session
151        key = self.session_key
152        self.create()
153        self._session_cache = data
154        if key:
155            self.delete(key)
156        if (authenticated_session := data.get("authenticatedsession")) is not None:
157            authenticated_session.session_id = self.session_key
158            authenticated_session.save(force_insert=True)
LOGGER = <BoundLoggerLazyProxy(logger=None, wrapper_class=None, processors=None, context_class=None, initial_values={}, logger_factory_args=())>
class SessionStore(django.contrib.sessions.backends.db.SessionStore):
 18class SessionStore(SessionBase):
 19    def __init__(self, session_key=None, last_ip=None, last_user_agent=""):
 20        super().__init__(session_key)
 21        self._create_kwargs = {
 22            "last_ip": last_ip or ClientIPMiddleware.default_ip,
 23            "last_user_agent": last_user_agent,
 24        }
 25
 26    @classmethod
 27    def get_model_class(cls):
 28        from authentik.core.models import Session
 29
 30        return Session
 31
 32    @cached_property
 33    def model_fields(self):
 34        return [k.value for k in self.model.Keys]
 35
 36    def _get_session_from_db(self):
 37        try:
 38            return self.model.objects.select_related(
 39                "authenticatedsession",
 40                "authenticatedsession__user",
 41            ).get(
 42                session_key=self.session_key,
 43                expires__gt=timezone.now(),
 44            )
 45        except (self.model.DoesNotExist, SuspiciousOperation) as exc:
 46            if isinstance(exc, SuspiciousOperation):
 47                LOGGER.warning(str(exc))
 48            self._session_key = None
 49
 50    async def _aget_session_from_db(self):
 51        try:
 52            return await self.model.objects.select_related(
 53                "authenticatedsession",
 54                "authenticatedsession__user",
 55            ).aget(
 56                session_key=self.session_key,
 57                expires__gt=timezone.now(),
 58            )
 59        except (self.model.DoesNotExist, SuspiciousOperation) as exc:
 60            if isinstance(exc, SuspiciousOperation):
 61                LOGGER.warning(str(exc))
 62            self._session_key = None
 63
 64    def encode(self, session_dict):
 65        return pickle.dumps(session_dict, protocol=pickle.HIGHEST_PROTOCOL)
 66
 67    def decode(self, session_data):
 68        try:
 69            return pickle.loads(session_data)  # nosec
 70        except pickle.PickleError, AttributeError, TypeError:
 71            # PickleError, ValueError - unpickling exceptions
 72            # AttributeError - can happen when Django model fields (e.g., FileField) are unpickled
 73            #                  and their descriptors fail to initialize (e.g., missing storage)
 74            # TypeError - can happen with incompatible pickled objects
 75            # If any of these happen, just return an empty dictionary (an empty session)
 76            LOGGER.warning("Failed to decode session data", exc_info=True)
 77            pass
 78        return {}
 79
 80    def load(self):
 81        s = self._get_session_from_db()
 82        if s:
 83            return {
 84                "authenticatedsession": getattr(s, "authenticatedsession", None),
 85                **{k: getattr(s, k) for k in self.model_fields},
 86                **self.decode(s.session_data),
 87            }
 88        else:
 89            return {}
 90
 91    async def aload(self):
 92        s = await self._aget_session_from_db()
 93        if s:
 94            return {
 95                "authenticatedsession": getattr(s, "authenticatedsession", None),
 96                **{k: getattr(s, k) for k in self.model_fields},
 97                **self.decode(s.session_data),
 98            }
 99        else:
100            return {}
101
102    def create_model_instance(self, data):
103        args = {
104            "session_key": self._get_or_create_session_key(),
105            "expires": self.get_expiry_date(),
106            "session_data": {},
107            **self._create_kwargs,
108        }
109        for k, v in data.items():
110            # Don't save:
111            # - unused auth data
112            # - related models
113            if k in [SESSION_KEY, BACKEND_SESSION_KEY, HASH_SESSION_KEY, "authenticatedsession"]:
114                pass
115            elif k in self.model_fields:
116                args[k] = v
117            else:
118                args["session_data"][k] = v
119        args["session_data"] = self.encode(args["session_data"])
120        return self.model(**args)
121
122    async def acreate_model_instance(self, data):
123        args = {
124            "session_key": await self._aget_or_create_session_key(),
125            "expires": await self.aget_expiry_date(),
126            "session_data": {},
127            **self._create_kwargs,
128        }
129        for k, v in data.items():
130            # Don't save:
131            # - unused auth data
132            # - related models
133            if k in [SESSION_KEY, BACKEND_SESSION_KEY, HASH_SESSION_KEY, "authenticatedsession"]:
134                pass
135            elif k in self.model_fields:
136                args[k] = v
137            else:
138                args["session_data"][k] = v
139        args["session_data"] = self.encode(args["session_data"])
140        return self.model(**args)
141
142    @classmethod
143    def clear_expired(cls):
144        cls.get_model_class().objects.filter(expires__lt=timezone.now()).delete()
145
146    @classmethod
147    async def aclear_expired(cls):
148        await cls.get_model_class().objects.filter(expires__lt=timezone.now()).adelete()
149
150    def cycle_key(self):
151        data = self._session
152        key = self.session_key
153        self.create()
154        self._session_cache = data
155        if key:
156            self.delete(key)
157        if (authenticated_session := data.get("authenticatedsession")) is not None:
158            authenticated_session.session_id = self.session_key
159            authenticated_session.save(force_insert=True)

Implement database session store.

SessionStore(session_key=None, last_ip=None, last_user_agent='')
19    def __init__(self, session_key=None, last_ip=None, last_user_agent=""):
20        super().__init__(session_key)
21        self._create_kwargs = {
22            "last_ip": last_ip or ClientIPMiddleware.default_ip,
23            "last_user_agent": last_user_agent,
24        }
@classmethod
def get_model_class(cls):
26    @classmethod
27    def get_model_class(cls):
28        from authentik.core.models import Session
29
30        return Session
def model_fields(unknown):

The type of the None singleton.

def encode(self, session_dict):
64    def encode(self, session_dict):
65        return pickle.dumps(session_dict, protocol=pickle.HIGHEST_PROTOCOL)

Return the given session dictionary serialized and encoded as a string.

def decode(self, session_data):
67    def decode(self, session_data):
68        try:
69            return pickle.loads(session_data)  # nosec
70        except pickle.PickleError, AttributeError, TypeError:
71            # PickleError, ValueError - unpickling exceptions
72            # AttributeError - can happen when Django model fields (e.g., FileField) are unpickled
73            #                  and their descriptors fail to initialize (e.g., missing storage)
74            # TypeError - can happen with incompatible pickled objects
75            # If any of these happen, just return an empty dictionary (an empty session)
76            LOGGER.warning("Failed to decode session data", exc_info=True)
77            pass
78        return {}
def load(self):
80    def load(self):
81        s = self._get_session_from_db()
82        if s:
83            return {
84                "authenticatedsession": getattr(s, "authenticatedsession", None),
85                **{k: getattr(s, k) for k in self.model_fields},
86                **self.decode(s.session_data),
87            }
88        else:
89            return {}

Load the session data and return a dictionary.

async def aload(self):
 91    async def aload(self):
 92        s = await self._aget_session_from_db()
 93        if s:
 94            return {
 95                "authenticatedsession": getattr(s, "authenticatedsession", None),
 96                **{k: getattr(s, k) for k in self.model_fields},
 97                **self.decode(s.session_data),
 98            }
 99        else:
100            return {}
def create_model_instance(self, data):
102    def create_model_instance(self, data):
103        args = {
104            "session_key": self._get_or_create_session_key(),
105            "expires": self.get_expiry_date(),
106            "session_data": {},
107            **self._create_kwargs,
108        }
109        for k, v in data.items():
110            # Don't save:
111            # - unused auth data
112            # - related models
113            if k in [SESSION_KEY, BACKEND_SESSION_KEY, HASH_SESSION_KEY, "authenticatedsession"]:
114                pass
115            elif k in self.model_fields:
116                args[k] = v
117            else:
118                args["session_data"][k] = v
119        args["session_data"] = self.encode(args["session_data"])
120        return self.model(**args)

Return a new instance of the session model object, which represents the current session state. Intended to be used for saving the session data to the database.

async def acreate_model_instance(self, data):
122    async def acreate_model_instance(self, data):
123        args = {
124            "session_key": await self._aget_or_create_session_key(),
125            "expires": await self.aget_expiry_date(),
126            "session_data": {},
127            **self._create_kwargs,
128        }
129        for k, v in data.items():
130            # Don't save:
131            # - unused auth data
132            # - related models
133            if k in [SESSION_KEY, BACKEND_SESSION_KEY, HASH_SESSION_KEY, "authenticatedsession"]:
134                pass
135            elif k in self.model_fields:
136                args[k] = v
137            else:
138                args["session_data"][k] = v
139        args["session_data"] = self.encode(args["session_data"])
140        return self.model(**args)

See create_model_instance().

@classmethod
def clear_expired(cls):
142    @classmethod
143    def clear_expired(cls):
144        cls.get_model_class().objects.filter(expires__lt=timezone.now()).delete()

Remove expired sessions from the session store.

If this operation isn't possible on a given backend, it should raise NotImplementedError. If it isn't necessary, because the backend has a built-in expiration mechanism, it should be a no-op.

@classmethod
async def aclear_expired(cls):
146    @classmethod
147    async def aclear_expired(cls):
148        await cls.get_model_class().objects.filter(expires__lt=timezone.now()).adelete()
def cycle_key(self):
150    def cycle_key(self):
151        data = self._session
152        key = self.session_key
153        self.create()
154        self._session_cache = data
155        if key:
156            self.delete(key)
157        if (authenticated_session := data.get("authenticatedsession")) is not None:
158            authenticated_session.session_id = self.session_key
159            authenticated_session.save(force_insert=True)

Create a new session key, while retaining the current session data.