@@ -505,3 +505,89 @@ async def test_login_without_client_claim(self, client: TestClient, user: User,
505505 assert "client" not in access_payload
506506 assert "client" not in refresh_payload
507507 self ._assert_lifetimes_unchanged (access_payload , refresh_payload , before , after )
508+
509+
510+ class TestShortLivedWebAdminTokens (BaseTest ):
511+ """Web/admin clients get shorter token lifetimes when configured; others are unchanged."""
512+
513+ get_token_url = auth_router .url_path_for ("get_token" )
514+ refresh_access_token_url = auth_router .url_path_for ("refresh_access_token" )
515+
516+ SHORT_ACCESS = 15
517+ SHORT_REFRESH = 120
518+
519+ @pytest .fixture
520+ def short_lifetimes (self , mocker : MockerFixture ):
521+ mocker .patch .object (settings .authentication .access_token , "web_admin_expiration" , self .SHORT_ACCESS )
522+ mocker .patch .object (settings .authentication .refresh_token , "web_admin_expiration" , self .SHORT_REFRESH )
523+
524+ @staticmethod
525+ def _decode (token : str , secret : str ) -> dict :
526+ return jwt .decode (token , secret , algorithms = [settings .authentication .algorithm ])
527+
528+ @staticmethod
529+ def _assert_expires_in (exp : int , minutes : int , before : datetime .datetime , after : datetime .datetime ):
530+ delta = datetime .timedelta (minutes = minutes )
531+ assert int ((before + delta ).timestamp ()) <= exp <= int ((after + delta ).timestamp ()) + 1
532+
533+ @pytest .mark .parametrize ("content_source" , ("web" , "admin" ))
534+ async def test_web_admin_get_short_lifetimes (
535+ self , client : TestClient , user : User , short_lifetimes , content_source : str
536+ ):
537+ before = datetime .datetime .now (datetime .timezone .utc )
538+ resp = await client .post (
539+ self .get_token_url ,
540+ data = {"email" : user .email_encrypted , "password" : TEST_PASSWORD },
541+ headers = {"Mindlogger-Content-Source" : content_source },
542+ )
543+ after = datetime .datetime .now (datetime .timezone .utc )
544+ assert resp .status_code == http .HTTPStatus .OK
545+ token = resp .json ()["result" ]["token" ]
546+ access = self ._decode (token ["accessToken" ], settings .authentication .access_token .secret_key )
547+ refresh = self ._decode (token ["refreshToken" ], settings .authentication .refresh_token .secret_key )
548+ self ._assert_expires_in (access ["exp" ], self .SHORT_ACCESS , before , after )
549+ self ._assert_expires_in (refresh ["exp" ], self .SHORT_REFRESH , before , after )
550+
551+ @pytest .mark .parametrize (
552+ "headers" ,
553+ ({"Mindlogger-Content-Source" : "mobile" }, None ),
554+ ids = ("mobile" , "no-header" ),
555+ )
556+ async def test_mobile_and_unknown_keep_default_lifetimes (
557+ self , client : TestClient , user : User , short_lifetimes , headers : dict | None
558+ ):
559+ before = datetime .datetime .now (datetime .timezone .utc )
560+ resp = await client .post (
561+ self .get_token_url ,
562+ data = {"email" : user .email_encrypted , "password" : TEST_PASSWORD },
563+ headers = headers ,
564+ )
565+ after = datetime .datetime .now (datetime .timezone .utc )
566+ assert resp .status_code == http .HTTPStatus .OK
567+ token = resp .json ()["result" ]["token" ]
568+ access = self ._decode (token ["accessToken" ], settings .authentication .access_token .secret_key )
569+ refresh = self ._decode (token ["refreshToken" ], settings .authentication .refresh_token .secret_key )
570+ self ._assert_expires_in (access ["exp" ], settings .authentication .access_token .expiration , before , after )
571+ self ._assert_expires_in (refresh ["exp" ], settings .authentication .refresh_token .expiration , before , after )
572+
573+ async def test_refresh_preserves_short_access_lifetime (
574+ self , client : TestClient , user : User , short_lifetimes , mocker : MockerFixture
575+ ):
576+ mocker .patch ("apps.authentication.api.auth.log" )
577+ login = await client .post (
578+ self .get_token_url ,
579+ data = {"email" : user .email_encrypted , "password" : TEST_PASSWORD },
580+ headers = {"Mindlogger-Content-Source" : "admin" },
581+ )
582+ refresh_token = login .json ()["result" ]["token" ]["refreshToken" ]
583+
584+ before = datetime .datetime .now (datetime .timezone .utc )
585+ resp = await client .post (
586+ self .refresh_access_token_url ,
587+ data = {"refresh_token" : refresh_token },
588+ headers = {"Mindlogger-Content-Source" : "admin" },
589+ )
590+ after = datetime .datetime .now (datetime .timezone .utc )
591+ assert resp .status_code == http .HTTPStatus .OK
592+ access = self ._decode (resp .json ()["result" ]["accessToken" ], settings .authentication .access_token .secret_key )
593+ self ._assert_expires_in (access ["exp" ], self .SHORT_ACCESS , before , after )
0 commit comments