Repository: freedomofpress/securedrop

Issue:
Failure to invalidate session when user resets their own password
## Description

When a user resets their own password, their session is not invalidated. 

## Steps to Reproduce

1. User logs in
2. User resets password

## Expected Behavior

User is logged out and is requested to use their new password to login

## Actual Behavior

User can continue to browse without having to enter their new password again

## Comments

Related tickets: #2300, #880


Discussion:

Resolving patch:
diff --git a/securedrop/journalist_app/account.py b/securedrop/journalist_app/account.py
--- a/securedrop/journalist_app/account.py
+++ b/securedrop/journalist_app/account.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 
 from flask import (Blueprint, render_template, request, g, redirect, url_for,
-                   flash)
+                   flash, session)
 from flask_babel import gettext
 
 from db import db_session
@@ -29,6 +29,9 @@ def new_password():
                          error_message):
             password = request.form.get('password')
             set_diceware_password(user, password)
+            session.pop('uid', None)
+            session.pop('expires', None)
+            return redirect(url_for('main.login'))
         return redirect(url_for('account.edit'))
 
     @view.route('/2fa', methods=('GET', 'POST'))


Test patch:
diff --git a/securedrop/tests/test_journalist.py b/securedrop/tests/test_journalist.py
--- a/securedrop/tests/test_journalist.py
+++ b/securedrop/tests/test_journalist.py
@@ -301,7 +301,7 @@ def test_admin_edits_user_password_error_response(self):
         assert ('There was an error, and the new password might not have '
                 'been saved correctly.') in resp.data.decode('utf-8')
 
-    def test_user_edits_password_success_reponse(self):
+    def test_user_edits_password_success_response(self):
         self._login_user()
         resp = self.client.post(
             url_for('account.new_password'),
@@ -314,6 +314,27 @@ def test_user_edits_password_success_reponse(self):
         assert "Password updated." in text
         assert VALID_PASSWORD_2 in text
 
+    def test_user_edits_password_expires_session(self):
+        with self.client as client:
+            # do a real login to get a real session
+            # (none of the mocking `g` hacks)
+            resp = client.post(url_for('main.login'),
+                               data=dict(username=self.user.username,
+                                         password=self.user_pw,
+                                         token='mocked'))
+            self.assertRedirects(resp, url_for('main.index'))
+            assert 'uid' in session
+
+            resp = client.post(
+                url_for('account.new_password'),
+                data=dict(current_password=self.user_pw,
+                          token='mocked',
+                          password=VALID_PASSWORD_2))
+
+            self.assertRedirects(resp, url_for('main.login'))
+            # verify the session was expired after the password was changed
+            assert 'uid' not in session
+
     def test_user_edits_password_error_reponse(self):
         self._login_user()
 
@@ -737,14 +758,6 @@ def test_incorrect_current_password_change(self):
         text = resp.data.decode('utf-8')
         self.assertIn('Incorrect password or two-factor code', text)
 
-    def test_invalid_user_password_change(self):
-        self._login_user()
-        res = self.client.post(url_for('account.new_password'),
-                               data=dict(password='badpw',
-                                         token='mocked',
-                                         current_password=self.user_pw))
-        self.assertRedirects(res, url_for('account.edit'))
-
     def test_too_long_user_password_change(self):
         self._login_user()
 
@@ -1077,17 +1090,18 @@ def test_journalist_session_expiration(self):
 
         try:
             with self.client as client:
-                # do a real login to get a real session
-                # (none of the mocking `g` hacks)
-                resp = self.client.post(url_for('main.login'),
-                                        data=dict(username=self.user.username,
-                                                  password=VALID_PASSWORD,
-                                                  token='mocked'))
-                assert resp.status_code == 200
-
                 # set the expiration to ensure we trigger an expiration
                 config.SESSION_EXPIRATION_MINUTES = -1
 
+                # do a real login to get a real session
+                # (none of the mocking `g` hacks)
+                resp = client.post(url_for('main.login'),
+                                   data=dict(username=self.user.username,
+                                             password=self.user_pw,
+                                             token='mocked'))
+                self.assertRedirects(resp, url_for('main.index'))
+                assert 'uid' in session
+
                 resp = client.get(url_for('account.edit'),
                                   follow_redirects=True)