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            pass
 76        return {}
 77
 78    def load(self):
 79        s = self._get_session_from_db()
 80        if s:
 81            return {
 82                "authenticatedsession": getattr(s, "authenticatedsession", None),
 83                **{k: getattr(s, k) for k in self.model_fields},
 84                **self.decode(s.session_data),
 85            }
 86        else:
 87            return {}
 88
 89    async def aload(self):
 90        s = await self._aget_session_from_db()
 91        if s:
 92            return {
 93                "authenticatedsession": getattr(s, "authenticatedsession", None),
 94                **{k: getattr(s, k) for k in self.model_fields},
 95                **self.decode(s.session_data),
 96            }
 97        else:
 98            return {}
 99
100    def create_model_instance(self, data):
101        args = {
102            "session_key": self._get_or_create_session_key(),
103            "expires": self.get_expiry_date(),
104            "session_data": {},
105            **self._create_kwargs,
106        }
107        for k, v in data.items():
108            # Don't save:
109            # - unused auth data
110            # - related models
111            if k in [SESSION_KEY, BACKEND_SESSION_KEY, HASH_SESSION_KEY, "authenticatedsession"]:
112                pass
113            elif k in self.model_fields:
114                args[k] = v
115            else:
116                args["session_data"][k] = v
117        args["session_data"] = self.encode(args["session_data"])
118        return self.model(**args)
119
120    async def acreate_model_instance(self, data):
121        args = {
122            "session_key": await self._aget_or_create_session_key(),
123            "expires": await self.aget_expiry_date(),
124            "session_data": {},
125            **self._create_kwargs,
126        }
127        for k, v in data.items():
128            # Don't save:
129            # - unused auth data
130            # - related models
131            if k in [SESSION_KEY, BACKEND_SESSION_KEY, HASH_SESSION_KEY, "authenticatedsession"]:
132                pass
133            elif k in self.model_fields:
134                args[k] = v
135            else:
136                args["session_data"][k] = v
137        args["session_data"] = self.encode(args["session_data"])
138        return self.model(**args)
139
140    @classmethod
141    def clear_expired(cls):
142        cls.get_model_class().objects.filter(expires__lt=timezone.now()).delete()
143
144    @classmethod
145    async def aclear_expired(cls):
146        await cls.get_model_class().objects.filter(expires__lt=timezone.now()).adelete()
147
148    def cycle_key(self):
149        data = self._session
150        key = self.session_key
151        self.create()
152        self._session_cache = data
153        if key:
154            self.delete(key)
155        if (authenticated_session := data.get("authenticatedsession")) is not None:
156            authenticated_session.session_id = self.session_key
157            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            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)

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            pass
77        return {}
def load(self):
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 {}

Load the session data and return a dictionary.

async def aload(self):
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 {}
def create_model_instance(self, data):
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)

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):
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)

See create_model_instance().

@classmethod
def clear_expired(cls):
141    @classmethod
142    def clear_expired(cls):
143        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):
145    @classmethod
146    async def aclear_expired(cls):
147        await cls.get_model_class().objects.filter(expires__lt=timezone.now()).adelete()
def cycle_key(self):
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)

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