|
1 | 1 | import uuid |
| 2 | +from datetime import datetime, timedelta, timezone |
2 | 3 |
|
3 | 4 | import pytest |
4 | 5 | from pytest import FixtureRequest |
|
7 | 8 |
|
8 | 9 | from apps.authentication.domain.login import UserLoginRequest |
9 | 10 | from apps.authentication.domain.token import InternalToken |
10 | | -from apps.authentication.domain.token.internal import TokenPurpose |
| 11 | +from apps.authentication.domain.token.internal import TokenPayload, TokenPurpose |
11 | 12 | from apps.authentication.errors import BadCredentials, InvalidCredentials |
12 | 13 | from apps.authentication.services import AuthenticationService |
13 | 14 | from apps.authentication.services.core import TokensService |
14 | 15 | from apps.users.cruds.user import UsersCRUD |
15 | 16 | from apps.users.domain import User |
16 | 17 | from apps.users.errors import UserIsDeletedError, UserNotFound |
| 18 | +from config import settings |
| 19 | +from infrastructure.http.domain import MindloggerContentSource |
17 | 20 |
|
18 | 21 | TEST_PASSWORD = "Test12345!" |
19 | 22 | RJTI = str(uuid.uuid4()) |
@@ -154,3 +157,47 @@ async def test_token_revoke__ttl_less_than_one( |
154 | 157 | await token_blacklist_service.revoke(access_token_internal, TokenPurpose.ACCESS) |
155 | 158 | is_revoked = await token_blacklist_service.is_revoked(access_token_internal) |
156 | 159 | assert not is_revoked |
| 160 | + |
| 161 | + |
| 162 | +class TestRefreshTokenExpDerivation: |
| 163 | + """`_get_refresh_token_by_access` must derive the paired refresh exp using the |
| 164 | + same per-client lifetimes the tokens were minted with (keyed on the `client` claim).""" |
| 165 | + |
| 166 | + @staticmethod |
| 167 | + def _access_token(access_exp: datetime, client: MindloggerContentSource | None) -> InternalToken: |
| 168 | + return InternalToken( |
| 169 | + payload=TokenPayload( |
| 170 | + sub=uuid.uuid4(), |
| 171 | + exp=int(access_exp.timestamp()), |
| 172 | + jti=str(uuid.uuid4()), |
| 173 | + rjti=RJTI, |
| 174 | + client=client, |
| 175 | + ) |
| 176 | + ) |
| 177 | + |
| 178 | + def test_web_admin_uses_short_deltas(self, auth_service: AuthenticationService, mocker: MockerFixture): |
| 179 | + mocker.patch.object(settings.authentication.access_token, "web_admin_expiration", 15) |
| 180 | + mocker.patch.object(settings.authentication.refresh_token, "web_admin_expiration", 120) |
| 181 | + access_exp = datetime.now(timezone.utc) + timedelta(minutes=15) |
| 182 | + derived = auth_service._get_refresh_token_by_access( |
| 183 | + self._access_token(access_exp, MindloggerContentSource.admin) |
| 184 | + ) |
| 185 | + assert derived is not None |
| 186 | + expected = int((access_exp - timedelta(minutes=15) + timedelta(minutes=120)).timestamp()) |
| 187 | + assert derived.payload.exp == expected |
| 188 | + assert derived.payload.jti == RJTI |
| 189 | + |
| 190 | + def test_legacy_client_none_uses_defaults(self, auth_service: AuthenticationService, mocker: MockerFixture): |
| 191 | + mocker.patch.object(settings.authentication.access_token, "web_admin_expiration", 15) |
| 192 | + mocker.patch.object(settings.authentication.refresh_token, "web_admin_expiration", 120) |
| 193 | + access_exp = datetime.now(timezone.utc) + timedelta(minutes=settings.authentication.access_token.expiration) |
| 194 | + derived = auth_service._get_refresh_token_by_access(self._access_token(access_exp, None)) |
| 195 | + assert derived is not None |
| 196 | + expected = int( |
| 197 | + ( |
| 198 | + access_exp |
| 199 | + - timedelta(minutes=settings.authentication.access_token.expiration) |
| 200 | + + timedelta(minutes=settings.authentication.refresh_token.expiration) |
| 201 | + ).timestamp() |
| 202 | + ) |
| 203 | + assert derived.payload.exp == expected |
0 commit comments