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.
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.
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.
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.