Skip to content

Commit 0fbc3cd

Browse files
Thomas Kpenouclaude
authored andcommitted
feat: GitHub Actions CI + secure/httponly cookies (PR bugy#812)
## GitHub Actions CI (.github/workflows/ci.yml) - Replace dead Travis CI with GitHub Actions workflow - Matrix: Python 3.10, 3.11, 3.12 (fail-fast: false) - Separate job for frontend tests (Node 20, npm ci) - Uses pip cache and npm cache for faster runs ## Secure & HttpOnly cookies (upstream PR bugy#812 by @cpadlab) - Add cookie_secure config option (default: true) in ServerConfig - Set httponly=True on all session cookies (username, token, token_details, client_id_token) - Respect cookie_secure flag on all set_secure_cookie calls - Add xsrf_cookie_kwargs: httponly=True, samesite=Lax, secure=cookie_secure - Fix test mocks: add application.server_config, accept **kwargs in set_secure_cookie (test_utils, ip_idenfication_test, test_auth_abstract_oauth, server_test) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 65a7675 commit 0fbc3cd

10 files changed

Lines changed: 90 additions & 7 deletions

File tree

.github/workflows/ci.yml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [ master, stable ]
6+
pull_request:
7+
branches: [ master, stable ]
8+
9+
jobs:
10+
python-tests:
11+
name: Python ${{ matrix.python-version }}
12+
runs-on: ubuntu-latest
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
python-version: ["3.10", "3.11", "3.12"]
17+
18+
steps:
19+
- uses: actions/checkout@v4
20+
21+
- name: Set up Python ${{ matrix.python-version }}
22+
uses: actions/setup-python@v5
23+
with:
24+
python-version: ${{ matrix.python-version }}
25+
cache: pip
26+
27+
- name: Install dependencies
28+
run: |
29+
pip install -r requirements.txt
30+
pip install ldap3 bcrypt parameterized pytest
31+
32+
- name: Run unit tests
33+
working-directory: src
34+
run: pytest tests/ -q --tb=short
35+
36+
frontend-tests:
37+
name: Frontend (Node 20)
38+
runs-on: ubuntu-latest
39+
40+
steps:
41+
- uses: actions/checkout@v4
42+
43+
- name: Set up Node.js
44+
uses: actions/setup-node@v4
45+
with:
46+
node-version: "20"
47+
cache: npm
48+
cache-dependency-path: web-src/package-lock.json
49+
50+
- name: Install dependencies
51+
working-directory: web-src
52+
run: npm ci
53+
54+
- name: Run unit tests
55+
working-directory: web-src
56+
env:
57+
NODE_OPTIONS: --openssl-legacy-provider
58+
run: npm run test:unit-ci

src/auth/identification.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ def _read_client_token(self, request_handler):
116116
def _write_client_token(self, client_id, request_handler):
117117
expiry_time = date_utils.get_current_millis() + days_to_ms(self.EXPIRES_DAYS)
118118
new_token = client_id + '&' + str(expiry_time)
119-
request_handler.set_secure_cookie(self.COOKIE_KEY, new_token, expires_days=self.EXPIRES_DAYS)
119+
server_config = request_handler.application.server_config
120+
request_handler.set_secure_cookie(self.COOKIE_KEY, new_token, expires_days=self.EXPIRES_DAYS, secure=server_config.cookie_secure, httponly=True)
120121

121122
def _can_write(self, request_handler):
122123
return can_write_secure_cookie(request_handler)

src/auth/oauth_token_manager.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ def update_tokens(self, token_response: OAuthTokenResponse, username, request_ha
2424
if not self._enabled:
2525
return
2626

27-
request_handler.set_secure_cookie('token', token_response.access_token)
27+
server_config = request_handler.application.server_config
28+
request_handler.set_secure_cookie('token', token_response.access_token, httponly=True, secure=server_config.cookie_secure)
2829

2930
if token_response.should_refresh():
3031
refresh_token = token_response.refresh_token
@@ -33,7 +34,7 @@ def update_tokens(self, token_response: OAuthTokenResponse, username, request_ha
3334
self._refresh_tokens[username] = refresh_token
3435
self._schedule_token_refresh(username, refresh_token, token_response.resolve_next_refresh_datetime())
3536

36-
request_handler.set_secure_cookie('token_details', token_response.serialize_details())
37+
request_handler.set_secure_cookie('token_details', token_response.serialize_details(), httponly=True, secure=server_config.cookie_secure)
3738

3839
def can_restore_state(self, request_handler):
3940
if not self._enabled:

src/auth/tornado_auth.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ def authenticate(self, request_handler):
8888

8989
LOGGER.info('Authenticated user ' + username)
9090

91-
request_handler.set_secure_cookie('username', username, expires_days=self.authenticator.auth_expiration_days)
91+
server_config = request_handler.application.server_config
92+
request_handler.set_secure_cookie('username', username, expires_days=self.authenticator.auth_expiration_days, httponly=True, secure=server_config.cookie_secure)
9293

9394
path = tornado.escape.url_unescape(request_handler.get_argument('next', '/'))
9495

src/model/server_conf.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ def __init__(self) -> None:
4343
self.user_header_name = None
4444
self.secret_storage_file = None
4545
self.xsrf_protection = None
46+
self.cookie_secure = True
4647
# noinspection PyTypeChecker
4748
self.env_vars: EnvVariables = None
4849

@@ -201,6 +202,7 @@ def from_json(conf_path, temp_folder):
201202

202203
security = model_helper.read_dict(json_object, 'security')
203204

205+
config.cookie_secure = model_helper.read_bool_from_config('cookie_secure', security, default=True)
204206
config.allowed_users = _prepare_allowed_users(allowed_users, admin_users, user_groups)
205207
config.alerts_config = json_object.get('alerts')
206208
config.callbacks_config = json_object.get('callbacks')

src/tests/auth/test_auth_abstract_oauth.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,13 +157,18 @@ def mock_request_handler(code):
157157

158158
handler_mock.get_secure_cookie = lambda cookie: secure_cookies.get(cookie)
159159

160-
def set_secure_cookie(cookie, value):
160+
def set_secure_cookie(cookie, value, **kwargs):
161161
secure_cookies[cookie] = value.encode('utf-8')
162162

163163
def clear_secure_cookie(cookie):
164164
if cookie in secure_cookies:
165165
del secure_cookies[cookie]
166166

167+
server_config = mock_object()
168+
server_config.cookie_secure = False
169+
170+
handler_mock.application = mock_object()
171+
handler_mock.application.server_config = server_config
167172
handler_mock.set_secure_cookie = set_secure_cookie
168173
handler_mock.clear_cookie = clear_secure_cookie
169174

src/tests/ip_idenfication_test.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ def mock_request_handler(ip=None, x_forwarded_for=None, x_real_ip=None, saved_to
1515
handler_mock.application = mock_object()
1616
handler_mock.application.auth = TornadoAuth(None)
1717
handler_mock.application.identification = IpBasedIdentification(TrustedIpValidator(['127.0.0.1']), user_header_name)
18+
handler_mock.application.server_config = mock_object()
19+
handler_mock.application.server_config.cookie_secure = False
1820

1921
handler_mock.request = mock_object()
2022
handler_mock.request.headers = {}
@@ -38,7 +40,7 @@ def get_secure_cookie(name):
3840
return values.encode('utf8')
3941
return None
4042

41-
def set_secure_cookie(key, value, expires_days=30):
43+
def set_secure_cookie(key, value, expires_days=30, **kwargs):
4244
cookies[key] = value
4345

4446
def clear_cookie(key):

src/tests/test_utils.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,7 @@ def get_argument(arg_name, default=None):
490490
return default
491491
return arguments.get(arg_name)
492492

493-
def set_secure_cookie(cookie_name, value):
493+
def set_secure_cookie(cookie_name, value, **kwargs):
494494
cookies[cookie_name] = f'!SECURE!{value}!!!'
495495

496496
def clear_cookie(cookie_name):
@@ -506,10 +506,17 @@ def get_secure_cookie(cookie_name):
506506

507507
return value[8:-3].encode('utf8')
508508

509+
server_config = mock_object()
510+
server_config.cookie_secure = False
511+
512+
application = mock_object()
513+
application.server_config = server_config
514+
509515
request_handler.get_argument = get_argument
510516
request_handler.set_secure_cookie = set_secure_cookie
511517
request_handler.get_secure_cookie = get_secure_cookie
512518
request_handler.clear_cookie = clear_cookie
519+
request_handler.application = application
513520

514521
request_handler.request = mock_object()
515522
request_handler.request.method = method

src/tests/web/server_test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ def start_server(self, port, address, *, xsrf_protection=XSRF_PROTECTION_TOKEN):
278278
config.port = port
279279
config.address = address
280280
config.xsrf_protection = xsrf_protection
281+
config.cookie_secure = False
281282
config.max_request_size_mb = 1
282283

283284
authorizer = Authorizer(ANY_USER, ['admin_user'], [], ['admin_user'], EmptyGroupProvider())

src/web/server.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,11 @@ def init(server_config: ServerConfig,
864864
'websocket_ping_timeout': 300,
865865
'compress_response': True,
866866
'xsrf_cookies': server_config.xsrf_protection != XSRF_PROTECTION_DISABLED,
867+
'xsrf_cookie_kwargs': {
868+
'httponly': True,
869+
'secure': server_config.cookie_secure,
870+
'samesite': 'Lax'
871+
},
867872
}
868873

869874
application = tornado.web.Application(handlers, **settings)

0 commit comments

Comments
 (0)