Skip to content

Commit 5a122d6

Browse files
feat: per-client token expiration settings and resolver
1 parent 62aa943 commit 5a122d6

3 files changed

Lines changed: 56 additions & 0 deletions

File tree

src/apps/authentication/services/security.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,35 @@
1818
from apps.users.domain import User
1919
from apps.users.password_validation import PasswordValidator
2020
from config import settings
21+
from infrastructure.http.domain import MindloggerContentSource
2122

2223
__all__ = ["AuthenticationService"]
2324

25+
_WEB_ADMIN_CLIENTS = (MindloggerContentSource.web, MindloggerContentSource.admin)
26+
2427

2528
class AuthenticationService:
2629
def __init__(self, session) -> None:
2730
self.session = session
2831

32+
@staticmethod
33+
def token_expiration_minutes(
34+
client: MindloggerContentSource | None,
35+
default_minutes: int,
36+
web_admin_minutes: int | None,
37+
) -> int:
38+
"""Resolve a token lifetime for a client.
39+
40+
Web/admin clients get ``web_admin_minutes`` when it is configured;
41+
everything else (mobile, or an unknown/legacy client with no `client`
42+
claim) keeps ``default_minutes``. An unset ``web_admin_minutes`` means
43+
the per-client shortening is off, so all clients fall back to the
44+
default — this is what keeps the feature a no-op until ops opts in.
45+
"""
46+
if client in _WEB_ADMIN_CLIENTS and web_admin_minutes is not None:
47+
return web_admin_minutes
48+
return default_minutes
49+
2950
@staticmethod
3051
def create_access_token(data: dict) -> str:
3152
to_encode = data.copy()
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import pytest
2+
3+
from apps.authentication.services.security import AuthenticationService
4+
from infrastructure.http.domain import MindloggerContentSource
5+
6+
DEFAULT = 30
7+
WEB_ADMIN = 15
8+
9+
10+
@pytest.mark.parametrize(
11+
"client",
12+
(MindloggerContentSource.web, MindloggerContentSource.admin),
13+
)
14+
def test_web_admin_use_short_lifetime_when_configured(client: MindloggerContentSource):
15+
assert AuthenticationService.token_expiration_minutes(client, DEFAULT, WEB_ADMIN) == WEB_ADMIN
16+
17+
18+
@pytest.mark.parametrize(
19+
"client",
20+
(MindloggerContentSource.web, MindloggerContentSource.admin),
21+
)
22+
def test_web_admin_fall_back_to_default_when_unset(client: MindloggerContentSource):
23+
assert AuthenticationService.token_expiration_minutes(client, DEFAULT, None) == DEFAULT
24+
25+
26+
@pytest.mark.parametrize("web_admin_minutes", (WEB_ADMIN, None))
27+
@pytest.mark.parametrize("client", (MindloggerContentSource.mobile, None))
28+
def test_mobile_and_unknown_always_use_default(client, web_admin_minutes):
29+
assert AuthenticationService.token_expiration_minutes(client, DEFAULT, web_admin_minutes) == DEFAULT

src/config/authentication.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +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
1013

1114
@field_validator("secret_key")
1215
@classmethod
@@ -20,6 +23,9 @@ class RefreshTokenSettings(BaseModel):
2023
secret_key: str
2124
# Set in minutes
2225
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
2329

2430
transition_key: str | None = None
2531
transition_expire_date: datetime.date | None = None

0 commit comments

Comments
 (0)