Repository: pypi/warehouse

Issue:
Attempting to view account settings for an account with 2FA enabled asks to reauthenticate even immediately after login
**Describe the bug**
After logging in, and immediately visiting the account settings page only a few seconds later, a "Confirm password to continue" page is shown.

My account has U2F 2FA enabled, in case that ends up being relevant.

**Expected behavior**

Immediately after login (and having provided both a password and the U2F), access should likely be enabled across any part of the site, including sensitive areas, until some timeout has elapsed.

**To Reproduce**

In a private window (or other place where you have not logged in), visit the home page, click log in, fill out your credentials, authenticate via U2F if applicable, then click "Account Settings" in the menu bar after logging in.

Quick video attached:


![login](https://user-images.githubusercontent.com/329822/90337291-84e39680-dfaf-11ea-93fb-602163e41af8.gif)


**My Platform**
FF80, macOS, U2F via Yubikey

**Additional context**

#8191 seems likely related (though don't know that the behavior is new after it).



Discussion:
Thanks for the report, I've noticed this as well. It looks like the initial login is not setting the "time to reauthenticate" in the session. 
![output](https://user-images.githubusercontent.com/35963992/91219677-a4f62100-e738-11ea-9aae-b247a700c415.gif)
I couldn't create this issue again
Perhaps indeed 2FA/U2F is required to observe the behavior then?
Yes, it definitely is. The `record_auth_timestamp` function is only called when the user doesn't have 2FA set: https://github.com/pypa/warehouse/blob/ab78391c8e69173aa878c08f88259756de1db7b4/warehouse/accounts/views.py#L200

This needs to happen here as well:

https://github.com/pypa/warehouse/blob/ab78391c8e69173aa878c08f88259756de1db7b4/warehouse/accounts/views.py#L249-L261

Really, since `_login_user` is used in both, it should happen here instead:

https://github.com/pypa/warehouse/blob/ab78391c8e69173aa878c08f88259756de1db7b4/warehouse/accounts/views.py#L777-L788

Resolving patch:
diff --git a/warehouse/accounts/views.py b/warehouse/accounts/views.py
--- a/warehouse/accounts/views.py
+++ b/warehouse/accounts/views.py
@@ -197,7 +197,6 @@ def login(request, redirect_field_name=REDIRECT_FIELD_NAME, _form_class=LoginFor
                     .lower(),
                 )
 
-                request.session.record_auth_timestamp()
             return resp
 
     return {
@@ -257,7 +256,6 @@ def two_factor_and_totp_validate(request, _form_class=TOTPAuthenticationForm):
                 .hexdigest()
                 .lower(),
             )
-
             return resp
         else:
             form.totp_value.data = ""
@@ -784,7 +782,7 @@ def _login_user(request, userid, two_factor_method=None):
         ip_address=request.remote_addr,
         additional={"two_factor_method": two_factor_method},
     )
-
+    request.session.record_auth_timestamp()
     return headers
 
 


Test patch:
diff --git a/tests/unit/accounts/test_views.py b/tests/unit/accounts/test_views.py
--- a/tests/unit/accounts/test_views.py
+++ b/tests/unit/accounts/test_views.py
@@ -573,6 +573,9 @@ def test_totp_auth(self, monkeypatch, pyramid_request, redirect_url):
         pyramid_request.set_property(
             lambda r: str(uuid.uuid4()), name="unauthenticated_userid"
         )
+        pyramid_request.session.record_auth_timestamp = pretend.call_recorder(
+            lambda *args: None
+        )
 
         form_obj = pretend.stub(
             validate=pretend.call_recorder(lambda: True),
@@ -606,6 +609,7 @@ def test_totp_auth(self, monkeypatch, pyramid_request, redirect_url):
                 additional={"two_factor_method": "totp"},
             )
         ]
+        assert pyramid_request.session.record_auth_timestamp.calls == [pretend.call()]
 
     def test_totp_auth_already_authed(self):
         request = pretend.stub(
@@ -984,6 +988,9 @@ def test_recovery_code_auth(self, monkeypatch, pyramid_request, redirect_url):
         pyramid_request.set_property(
             lambda r: str(uuid.uuid4()), name="unauthenticated_userid"
         )
+        pyramid_request.session.record_auth_timestamp = pretend.call_recorder(
+            lambda *args: None
+        )
 
         form_obj = pretend.stub(
             validate=pretend.call_recorder(lambda: True),
@@ -1026,6 +1033,7 @@ def test_recovery_code_auth(self, monkeypatch, pyramid_request, redirect_url):
                 queue="success",
             )
         ]
+        assert pyramid_request.session.record_auth_timestamp.calls == [pretend.call()]
 
     def test_recovery_code_form_invalid(self):
         token_data = {"userid": 1}
@@ -1202,6 +1210,9 @@ def test_register_redirect(self, db_request, monkeypatch):
         create_user = pretend.call_recorder(lambda *args, **kwargs: user)
         add_email = pretend.call_recorder(lambda *args, **kwargs: email)
         record_event = pretend.call_recorder(lambda *a, **kw: None)
+        db_request.session.record_auth_timestamp = pretend.call_recorder(
+            lambda *args: None
+        )
         db_request.find_service = pretend.call_recorder(
             lambda *args, **kwargs: pretend.stub(
                 csp_policy={},
@@ -1227,6 +1238,7 @@ def test_register_redirect(self, db_request, monkeypatch):
                 "full_name": "full_name",
             }
         )
+
         send_email = pretend.call_recorder(lambda *a: None)
         monkeypatch.setattr(views, "send_email_verification_email", send_email)