Skip to content

Commit 29706f9

Browse files
clutesteruruwhy
andauthored
Feature/persistent sessions (#3264)
* feat: add configurable session persistence across server reboots * fix: restore accidentally deleted configuration fields in default.yml * update unit tests, remove unused imports from auth_svc, update gitignore --------- Co-authored-by: uruwhy <58484522+uruwhy@users.noreply.github.com>
1 parent a29906d commit 29706f9

File tree

4 files changed

+40
-6
lines changed

4 files changed

+40
-6
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ conf/*.yml
2020
!conf/default.yml
2121
data/object_store
2222
data/fact_store
23+
data/cookie_storage
2324
data/results/*
2425
!data/results/.gitkeep
2526
data/payloads/*

app/service/auth_svc.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import base64
21
from collections import namedtuple
32
from importlib import import_module
3+
import os
44

55
from aiohttp import web, web_request
66
from aiohttp.web_exceptions import HTTPUnauthorized, HTTPForbidden
@@ -10,7 +10,6 @@
1010
from aiohttp_security.abc import AbstractAuthorizationPolicy
1111
from aiohttp_session import setup as setup_session
1212
from aiohttp_session.cookie_storage import EncryptedCookieStorage
13-
from cryptography import fernet
1413

1514
from app.service.interfaces.i_auth_svc import AuthServiceInterface
1615
from app.service.interfaces.i_login_handler import LoginHandlerInterface
@@ -73,9 +72,35 @@ async def apply(self, app, users):
7372
for username, password in user.items():
7473
await self.create_user(username, password, group)
7574
app.user_map = self.user_map
76-
fernet_key = fernet.Fernet.generate_key()
77-
secret_key = base64.urlsafe_b64decode(fernet_key)
78-
storage = EncryptedCookieStorage(secret_key, cookie_name=COOKIE_SESSION)
75+
cookie_file = 'cookie_storage'
76+
expiration_days = self.get_config('session_expiration_days')
77+
file_svc = self.get_service('file_svc')
78+
cookie_path = os.path.join('data', cookie_file)
79+
80+
# Safely calculate max_age in seconds, allowing for fractional days
81+
try:
82+
max_age = int(float(expiration_days) * 86400) if expiration_days else None
83+
except (ValueError, TypeError):
84+
max_age = None
85+
try:
86+
if os.path.exists(os.path.join('data', cookie_file)):
87+
secret_key = file_svc._read(cookie_path)
88+
self.log.debug('Loaded persistent session key from data/cookie_storage')
89+
else:
90+
# Generate a new random 32-byte key for AES encryption if no valid key is found in the config or data folder
91+
secret_key = os.urandom(32)
92+
file_svc._save(cookie_path, secret_key, encrypt=True)
93+
self.log.debug('Generated and saved new persistent session key.')
94+
except Exception as e:
95+
# Fallback if file operations fail
96+
self.log.warning('Could not manage persistent key file, falling back to ephemeral: %s', e)
97+
secret_key = os.urandom(32)
98+
if len(secret_key) != 32:
99+
secret_key = os.urandom(32)
100+
self.log.warning('Loaded session key is not 32 bytes long. Generating new key.')
101+
102+
# Pass max_age to the storage initializer
103+
storage = EncryptedCookieStorage(secret_key, cookie_name=COOKIE_SESSION, max_age=max_age)
79104
setup_session(app, storage)
80105
policy = SessionIdentityPolicy()
81106
setup_security(app, policy, DictionaryAuthorizationPolicy(self.user_map))

conf/default.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ app.contact.websocket: 0.0.0.0:7012
2727
auth.login.handler.module: default
2828
crypt_salt: REPLACE_WITH_RANDOM_VALUE
2929
encryption_key: ADMIN123
30+
session_expiration_days: 7
3031
exfil_dir: /tmp/caldera
3132
host: 0.0.0.0
3233
objects.planners.default: atomic

tests/api/v2/test_security.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import pytest
2+
import os
23
from aiohttp import web
34

45
from app.api.v2 import security
@@ -8,13 +9,17 @@
89

910
@pytest.fixture
1011
def base_world():
12+
cookie_path = os.path.join('data', 'cookie_storage')
1113
BaseWorld.clear_config()
14+
if os.path.exists(cookie_path):
15+
os.remove(cookie_path)
1216

1317
BaseWorld.apply_config(
1418
name='main',
1519
config={
1620
CONFIG_API_KEY_RED: 'abc123',
17-
21+
'crypt_salt': 'REPLACE_WITH_RANDOM_VALUE',
22+
'encryption_key': 'ADMIN123',
1823
'users': {
1924
'red': {'reduser': 'redpass'},
2025
'blue': {'blueuser': 'bluepass'}
@@ -25,6 +30,8 @@ def base_world():
2530

2631
yield BaseWorld
2732
BaseWorld.clear_config()
33+
if os.path.exists(cookie_path):
34+
os.remove(cookie_path)
2835

2936

3037
@pytest.fixture

0 commit comments

Comments
 (0)