Skip to content

Commit 8b42a12

Browse files
feat: set web/admin token lifetimes to 15m access / 30m refresh
1 parent 4506bf7 commit 8b42a12

3 files changed

Lines changed: 27 additions & 27 deletions

File tree

.env.default

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,11 @@ AUTHENTICATION__REFRESH_TOKEN__SECRET_KEY="secret2"
3636
AUTHENTICATION__MFA_TOKEN__SECRET_KEY="secret3"
3737
AUTHENTICATION__ACCESS_TOKEN__EXPIRATION=30
3838
AUTHENTICATION__REFRESH_TOKEN__EXPIRATION=540
39-
# Optional shorter token lifetimes (minutes) for web/admin clients. Unset = same as the
40-
# values above (feature off). Mobile and unknown/legacy clients always use the values above.
41-
#AUTHENTICATION__ACCESS_TOKEN__WEB_ADMIN_EXPIRATION=
42-
#AUTHENTICATION__REFRESH_TOKEN__WEB_ADMIN_EXPIRATION=
39+
# Shorter token lifetimes (minutes) for web/admin clients (default 15 / 30 in code).
40+
# Mobile and unknown/legacy clients always use the EXPIRATION values above. Override here
41+
# per environment if needed.
42+
#AUTHENTICATION__ACCESS_TOKEN__WEB_ADMIN_EXPIRATION=15
43+
#AUTHENTICATION__REFRESH_TOKEN__WEB_ADMIN_EXPIRATION=30
4344
AUTHENTICATION__ALGORITHM="HS256"
4445
AUTHENTICATION__TOKEN_TYPE="Bearer"
4546
AUTHENTICATION__PASSWORD_RECOVER__EXPIRATION=900

src/apps/authentication/tests/test_auth.py

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -415,21 +415,9 @@ def _decode_tokens(result: dict) -> tuple[dict, dict]:
415415
return access_payload, refresh_payload
416416

417417
@staticmethod
418-
def _assert_lifetimes_unchanged(
419-
access_payload: dict, refresh_payload: dict, before: datetime.datetime, after: datetime.datetime
420-
):
421-
access_delta = datetime.timedelta(minutes=settings.authentication.access_token.expiration)
422-
refresh_delta = datetime.timedelta(minutes=settings.authentication.refresh_token.expiration)
423-
assert (
424-
int((before + access_delta).timestamp())
425-
<= access_payload["exp"]
426-
<= int((after + access_delta).timestamp()) + 1
427-
)
428-
assert (
429-
int((before + refresh_delta).timestamp())
430-
<= refresh_payload["exp"]
431-
<= int((after + refresh_delta).timestamp()) + 1
432-
)
418+
def _assert_expires_in(exp: int, minutes: int, before: datetime.datetime, after: datetime.datetime):
419+
delta = datetime.timedelta(minutes=minutes)
420+
assert int((before + delta).timestamp()) <= exp <= int((after + delta).timestamp()) + 1
433421

434422
@pytest.mark.parametrize("content_source", ("web", "admin", "mobile"))
435423
async def test_login_embeds_client_claim(self, client: TestClient, user: User, content_source: str):
@@ -444,7 +432,16 @@ async def test_login_embeds_client_claim(self, client: TestClient, user: User, c
444432
access_payload, refresh_payload = self._decode_tokens(resp.json()["result"])
445433
assert access_payload["client"] == content_source
446434
assert refresh_payload["client"] == content_source
447-
self._assert_lifetimes_unchanged(access_payload, refresh_payload, before, after)
435+
# web/admin get the short lifetimes; mobile keeps the defaults.
436+
if content_source in ("web", "admin"):
437+
expected_access = settings.authentication.access_token.web_admin_expiration
438+
expected_refresh = settings.authentication.refresh_token.web_admin_expiration
439+
assert expected_access is not None and expected_refresh is not None
440+
else:
441+
expected_access = settings.authentication.access_token.expiration
442+
expected_refresh = settings.authentication.refresh_token.expiration
443+
self._assert_expires_in(access_payload["exp"], expected_access, before, after)
444+
self._assert_expires_in(refresh_payload["exp"], expected_refresh, before, after)
448445

449446
async def test_login_audit_event_records_client_source(self, client: TestClient, user: User, mocker: MockerFixture):
450447
audit_log = mocker.patch("apps.authentication.api.auth.log")
@@ -504,7 +501,9 @@ async def test_login_without_client_claim(self, client: TestClient, user: User,
504501
access_payload, refresh_payload = self._decode_tokens(resp.json()["result"])
505502
assert "client" not in access_payload
506503
assert "client" not in refresh_payload
507-
self._assert_lifetimes_unchanged(access_payload, refresh_payload, before, after)
504+
# No client claim -> default (mobile) lifetimes.
505+
self._assert_expires_in(access_payload["exp"], settings.authentication.access_token.expiration, before, after)
506+
self._assert_expires_in(refresh_payload["exp"], settings.authentication.refresh_token.expiration, before, after)
508507

509508

510509
class TestShortLivedWebAdminTokens(BaseTest):

src/config/authentication.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ class AccessTokenSettings(BaseModel):
77
secret_key: str
88
# Set in minutes
99
expiration: int = 30
10-
# Shorter lifetime (minutes) for web/admin clients. None = same as `expiration`
11-
# (feature off). See AuthenticationService.token_expiration_minutes.
12-
web_admin_expiration: int | None = None
10+
# Shorter lifetime (minutes) for web/admin clients. None = same as `expiration`.
11+
# See AuthenticationService.token_expiration_minutes.
12+
web_admin_expiration: int | None = 15
1313

1414
@field_validator("secret_key")
1515
@classmethod
@@ -23,9 +23,9 @@ class RefreshTokenSettings(BaseModel):
2323
secret_key: str
2424
# Set in minutes
2525
expiration: int = 540
26-
# Shorter lifetime (minutes) for web/admin clients. None = same as `expiration`
27-
# (feature off). See AuthenticationService.token_expiration_minutes.
28-
web_admin_expiration: int | None = None
26+
# Shorter lifetime (minutes) for web/admin clients. None = same as `expiration`.
27+
# See AuthenticationService.token_expiration_minutes.
28+
web_admin_expiration: int | None = 30
2929

3030
transition_key: str | None = None
3131
transition_expire_date: datetime.date | None = None

0 commit comments

Comments
 (0)