From 6054f9ce3bb81f97a235ac48812cde0a761cfe59 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Thu, 20 Nov 2025 19:53:32 +0000 Subject: [PATCH 1/3] Set expires when new login is created --- warehouse/accounts/services.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/warehouse/accounts/services.py b/warehouse/accounts/services.py index a10907b987f6..4190e079beab 100644 --- a/warehouse/accounts/services.py +++ b/warehouse/accounts/services.py @@ -766,6 +766,8 @@ def device_is_known(self, userid, request): user_id=userid, ip_address=request.remote_addr, status=UniqueLoginStatus.PENDING, + expires=datetime.datetime.now(datetime.UTC) + + datetime.timedelta(seconds=token_service.max_age), ) request.db.add(unique_login) request.db.flush() # To get the ID for the token From e56f8d6cfb8f1f4fa3529714767acd4a7c059fb8 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Thu, 20 Nov 2025 19:54:10 +0000 Subject: [PATCH 2/3] Data migration for existing logins --- ...update_expires_for_pending_user_unique_.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 warehouse/migrations/versions/a25f3d5186a9_update_expires_for_pending_user_unique_.py diff --git a/warehouse/migrations/versions/a25f3d5186a9_update_expires_for_pending_user_unique_.py b/warehouse/migrations/versions/a25f3d5186a9_update_expires_for_pending_user_unique_.py new file mode 100644 index 000000000000..91025be4b16a --- /dev/null +++ b/warehouse/migrations/versions/a25f3d5186a9_update_expires_for_pending_user_unique_.py @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: Apache-2.0 +""" +update expires for pending user_unique_logins + +Revision ID: a25f3d5186a9 +Revises: 537b63a29cea +Create Date: 2025-11-20 19:09:40.492013 +""" + + +from alembic import op + +revision = "a25f3d5186a9" +down_revision = "537b63a29cea" + + +def upgrade(): + op.execute( + """ + UPDATE user_unique_logins + SET expires = created + INTERVAL '6 hours' + WHERE status = 'pending' AND expires IS NULL + """ + ) + + +def downgrade(): + pass From 29a0c965880564de1a83c27637dc13680794f7eb Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Thu, 20 Nov 2025 19:54:30 +0000 Subject: [PATCH 3/3] Update tests --- tests/unit/accounts/test_services.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/unit/accounts/test_services.py b/tests/unit/accounts/test_services.py index 58ae56bbbb95..fa32cbe59499 100644 --- a/tests/unit/accounts/test_services.py +++ b/tests/unit/accounts/test_services.py @@ -2034,7 +2034,7 @@ def test_device_is_not_known(self, user_service, monkeypatch): user = UserFactory.create(with_verified_primary_email=True) send_email = pretend.call_recorder(lambda *a, **kw: None) monkeypatch.setattr(services, "send_unrecognized_login_email", send_email) - token_service = pretend.stub(dumps=lambda d: "fake_token") + token_service = pretend.stub(dumps=lambda d: "fake_token", max_age=60) user_service.request = pretend.stub( db=user_service.db, remote_addr=REMOTE_ADDR, @@ -2048,6 +2048,17 @@ def test_device_is_not_known(self, user_service, monkeypatch): ) assert not user_service.device_is_known(user.id, user_service.request) + + unique_login = ( + user_service.db.query(services.UserUniqueLogin) + .filter( + services.UserUniqueLogin.user_id == user.id, + services.UserUniqueLogin.ip_address == REMOTE_ADDR, + ) + .one() + ) + assert unique_login.expires is not None + assert send_email.calls == [ pretend.call( user_service.request, @@ -2065,7 +2076,7 @@ def test_device_is_pending_not_expired(self, user_service, monkeypatch): ) send_email = pretend.call_recorder(lambda *a, **kw: None) monkeypatch.setattr(services, "send_unrecognized_login_email", send_email) - token_service = pretend.stub(dumps=lambda d: "fake_token") + token_service = pretend.stub(dumps=lambda d: "fake_token", max_age=60) user_service.request = pretend.stub( db=user_service.db, remote_addr=REMOTE_ADDR, @@ -2123,7 +2134,7 @@ def test_device_is_not_known_bad_user_agent( user = UserFactory.create(with_verified_primary_email=True) send_email = pretend.call_recorder(lambda *a, **kw: None) monkeypatch.setattr(services, "send_unrecognized_login_email", send_email) - token_service = pretend.stub(dumps=lambda d: "fake_token") + token_service = pretend.stub(dumps=lambda d: "fake_token", max_age=60) headers = {} if ua_string: headers["User-Agent"] = ua_string