Skip to content

Commit 8b62501

Browse files
committed
Some changes to authentication.
- Do not use session cookies for AWS/CWS, but regular cookies - Allow authentication in CWS without IP autologin or cookies, but with a `X-CMS-Authorization` header.
1 parent d6849ff commit 8b62501

File tree

5 files changed

+37
-14
lines changed

5 files changed

+37
-14
lines changed

cms/server/admin/authentication.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,8 @@ def my_start_response(status, headers, exc_info=None):
144144
"""
145145
response = Response(status=status, headers=headers)
146146
self._cookie.save_cookie(
147-
response, AWSAuthMiddleware.COOKIE, httponly=True)
147+
response, AWSAuthMiddleware.COOKIE, httponly=True,
148+
max_age=config.admin_cookie_duration)
148149
return start_response(
149150
status, response.headers.to_wsgi_list(), exc_info)
150151

cms/server/contest/authentication.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ def authenticate_request(
164164
contest: Contest,
165165
timestamp: datetime,
166166
cookie: bytes | None,
167+
authorization_header: bytes | None,
167168
ip_address: AnyIPAddress,
168169
) -> tuple[Participation | None, bytes | None]:
169170
"""Authenticate a user returning to the site, with a cookie.
@@ -180,6 +181,9 @@ def authenticate_request(
180181
- if IP autologin is enabled, we look for a participation whose IP
181182
address matches the remote IP address; if a match is found, the
182183
user is authenticated as that participation;
184+
- if username/password authentication is enabled, and a
185+
"X-CMS-Authorization" header is present and valid, the
186+
corresponding participation is returned.
183187
- if username/password authentication is enabled, and the cookie
184188
is valid, the corresponding participation is returned, together
185189
with a refreshed cookie.
@@ -219,8 +223,8 @@ def authenticate_request(
219223

220224
if participation is None \
221225
and contest.allow_password_authentication:
222-
participation, cookie = _authenticate_request_from_cookie(
223-
sql_session, contest, timestamp, cookie)
226+
participation, cookie = _authenticate_request_from_cookie_or_authorization_header(
227+
sql_session, contest, timestamp, authorization_header if authorization_header is not None else cookie)
224228

225229
if participation is None:
226230
return None, None
@@ -305,7 +309,7 @@ def _authenticate_request_by_ip_address(
305309
return participation
306310

307311

308-
def _authenticate_request_from_cookie(
312+
def _authenticate_request_from_cookie_or_authorization_header(
309313
sql_session: Session, contest: Contest, timestamp: datetime, cookie: bytes | None
310314
) -> tuple[Participation | None, bytes | None]:
311315
"""Return the current participation based on the cookie.
@@ -316,8 +320,8 @@ def _authenticate_request_from_cookie(
316320
execute queries.
317321
contest: the contest the user is trying to access.
318322
timestamp: the date and the time of the request.
319-
cookie: the cookie the user's browser provided in the
320-
request (if any).
323+
cookie: the contents of the cookie (or authorization header)
324+
provided in the request (if any).
321325
322326
return: the participation
323327
extracted from the cookie and the cookie to set/refresh, or

cms/server/contest/handlers/contest.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
import tornado.web as tornado_web
5050

5151
from cms import config, TOKEN_MODE_MIXED
52-
from cms.db import Contest, Submission, Task, UserTest
52+
from cms.db import Contest, Submission, Task, UserTest, contest
5353
from cms.locale import filter_language_codes
5454
from cms.server import FileHandlerMixin
5555
from cms.server.contest.authentication import authenticate_request
@@ -73,6 +73,7 @@ class ContestHandler(BaseHandler):
7373
child of this class.
7474
7575
"""
76+
7677
def __init__(self, *args, **kwargs):
7778
super().__init__(*args, **kwargs)
7879
self.contest_url: Url = None
@@ -139,6 +140,9 @@ def get_current_user(self) -> Participation | None:
139140
- if IP autologin is enabled, the remote IP address is matched
140141
with the participation IP address; if a match is found, that
141142
participation is returned; in case of errors, None is returned;
143+
- if username/password authentication is enabled, and a
144+
"X-CMS-Authorization" header is present and valid, the
145+
corresponding participation is returned.
142146
- if username/password authentication is enabled, and the cookie
143147
is valid, the corresponding participation is returned, and the
144148
cookie is refreshed.
@@ -164,12 +168,16 @@ def get_current_user(self) -> Participation | None:
164168
return None
165169

166170
participation, cookie = authenticate_request(
167-
self.sql_session, self.contest, self.timestamp, cookie, ip_address)
171+
self.sql_session, self.contest,
172+
self.timestamp, cookie,
173+
self.request.headers.get("X-CMS-Authorization", None),
174+
ip_address)
168175

169176
if cookie is None:
170177
self.clear_cookie(cookie_name)
171178
elif self.refresh_cookie:
172-
self.set_secure_cookie(cookie_name, cookie, expires_days=None)
179+
self.set_secure_cookie(
180+
cookie_name, cookie, expires_days=None, max_age=config.cookie_duration)
173181

174182
return participation
175183

cms/server/contest/handlers/main.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,8 @@ def _get_user(self) -> User:
183183

184184
# Find user if it exists
185185
user: User | None = (
186-
self.sql_session.query(User).filter(User.username == username).first()
186+
self.sql_session.query(User).filter(
187+
User.username == username).first()
187188
)
188189
if user is None:
189190
raise tornado_web.HTTPError(404)
@@ -200,7 +201,8 @@ def _get_team(self) -> Team | None:
200201
try:
201202
team_code: str = self.get_argument("team")
202203
team: Team | None = (
203-
self.sql_session.query(Team).filter(Team.code == team_code).one()
204+
self.sql_session.query(Team).filter(
205+
Team.code == team_code).one()
204206
)
205207
except (tornado_web.MissingArgumentError, NoResultFound):
206208
raise tornado_web.HTTPError(400)
@@ -246,7 +248,8 @@ def post(self):
246248
if cookie is None:
247249
self.clear_cookie(cookie_name)
248250
else:
249-
self.set_secure_cookie(cookie_name, cookie, expires_days=None)
251+
self.set_secure_cookie(
252+
cookie_name, cookie, expires_days=None, max_age=config.cookie_duration)
250253

251254
if participation is None:
252255
self.redirect(error_page)
@@ -295,7 +298,8 @@ class NotificationsHandler(ContestHandler):
295298
def get(self):
296299
participation: Participation = self.current_user
297300

298-
last_notification: str | None = self.get_argument("last_notification", None)
301+
last_notification: str | None = self.get_argument(
302+
"last_notification", None)
299303
if last_notification is not None:
300304
last_notification = make_datetime(float(last_notification))
301305

@@ -380,7 +384,7 @@ def get(self):
380384
language_docs = []
381385
if config.docs_path is not None:
382386
for language in languages:
383-
ext = language.source_extensions[0][1:] # remove dot
387+
ext = language.source_extensions[0][1:] # remove dot
384388
path = os.path.join(config.docs_path, ext)
385389
if os.path.exists(path):
386390
language_docs.append((language.name, ext))

cmstestsuite/unit_tests/server/contest/authentication_test.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ def attempt_authentication(self, **kwargs):
175175
self.session, self.contest,
176176
kwargs.get("timestamp", self.timestamp),
177177
kwargs.get("cookie", self.cookie),
178+
kwargs.get("authorization", None),
178179
ipaddress.ip_address(kwargs.get("ip_address", "10.0.0.1")))
179180

180181
def assertSuccess(self, **kwargs):
@@ -327,6 +328,11 @@ def test_invalid_cookie(self):
327328
self.assertFailure(cookie=None)
328329
self.assertFailure(cookie="not a valid cookie")
329330

331+
def test_authorization_header(self):
332+
self.contest.ip_autologin = False
333+
self.contest.allow_password_authentication = True
334+
self.assertSuccess(cookie=None, authorization=self.cookie)
335+
330336
def test_no_user(self):
331337
self.session.delete(self.user)
332338
self.assertFailure()

0 commit comments

Comments
 (0)