Repository: freedomofpress/securedrop

Issue:
session expiry message shows up when not logged in
## Description

This is a low-priority issue but filing for awareness: right now our session expiration applies to all routes on the application, including when users aren't logged in. [Here](https://github.com/freedomofpress/securedrop/blob/develop/securedrop/source_app/__init__.py#L143-L151) is the relevant code.

### Steps to Reproduce

1. Go to SecureDrop source interface homepage. Do not log in.
2. Wait for 120 minutes (this is `SESSION_EXPIRATION_MINUTES`).
3. Refresh the page.

### Expected Behavior

Nothing happens, page reloads. 

### Actual Behavior

You'll get a flashed message indicating you have been logged out. This is odd for the user since they're not logged in. 

### Comments

A proposal is to only use the session expiration logic when `logged_in` is in the session object. 


Discussion:
Hi @redshiftzero, I'd love to take this on as my first contribution to SecureDrop. I'll follow your proposal under the Comments heading as a guideline. Other than that, is there anything else I need to know before jumping in? 
@sheonhan feel free to submit a PR (with tests), we can discuss if anything else is required at that. Thank you for showing interest.

Resolving patch:
diff --git a/securedrop/source_app/__init__.py b/securedrop/source_app/__init__.py
--- a/securedrop/source_app/__init__.py
+++ b/securedrop/source_app/__init__.py
@@ -23,7 +23,7 @@
 from sdconfig import SDConfig
 from source_app import main, info, api
 from source_app.decorators import ignore_static
-from source_app.utils import logged_in
+from source_app.utils import logged_in, was_in_generate_flow
 from store import Storage
 
 
@@ -109,7 +109,7 @@ def check_tor2web() -> None:
                 'provide anonymity. '
                 '<a href="{url}">Why is this dangerous?</a>')
                 .format(url=url_for('info.tor2web_warning'))),
-                  "banner-warning")
+                "banner-warning")
 
     @app.before_request
     @ignore_static
@@ -124,11 +124,24 @@ def setup_g() -> Optional[werkzeug.Response]:
         if 'expires' in session and datetime.utcnow() >= session['expires']:
             msg = render_template('session_timeout.html')
 
+            # Show expiration message only if the user was
+            # either in the codename generation flow or logged in
+            show_expiration_message = any([
+                session.get('show_expiration_message'),
+                logged_in(),
+                was_in_generate_flow(),
+            ])
+
             # clear the session after we render the message so it's localized
             session.clear()
 
+            # Persist this properety across sessions to distinguish users whose sessions expired
+            # from users who never logged in or generated a codename
+            session['show_expiration_message'] = show_expiration_message
+
             # Redirect to index with flashed message
-            flash(Markup(msg), "important")
+            if session['show_expiration_message']:
+                flash(Markup(msg), "important")
             return redirect(url_for('main.index'))
 
         session['expires'] = datetime.utcnow() + \
diff --git a/securedrop/source_app/main.py b/securedrop/source_app/main.py
--- a/securedrop/source_app/main.py
+++ b/securedrop/source_app/main.py
@@ -257,7 +257,7 @@ def submit() -> werkzeug.Response:
                 current_app.logger.info("generating key, entropy: {}".format(
                     entropy_avail))
             else:
-                current_app.logger.warn(
+                current_app.logger.warning(
                         "skipping key generation. entropy: {}".format(
                                 entropy_avail))
 
diff --git a/securedrop/source_app/utils.py b/securedrop/source_app/utils.py
--- a/securedrop/source_app/utils.py
+++ b/securedrop/source_app/utils.py
@@ -21,6 +21,10 @@
     from typing import Optional  # noqa: F401
 
 
+def was_in_generate_flow() -> bool:
+    return 'codenames' in session
+
+
 def logged_in() -> bool:
     return 'logged_in' in session
 


Test patch:
diff --git a/securedrop/tests/test_source.py b/securedrop/tests/test_source.py
--- a/securedrop/tests/test_source.py
+++ b/securedrop/tests/test_source.py
@@ -727,6 +727,7 @@ def test_source_session_expiration(config, source_app):
         # which is always present and 'csrf_token' which leaks no info)
         session.pop('expires', None)
         session.pop('csrf_token', None)
+        session.pop('show_expiration_message', None)
         assert not session
 
         text = resp.data.decode('utf-8')
@@ -752,12 +753,31 @@ def test_source_session_expiration_create(config, source_app):
         # which is always present and 'csrf_token' which leaks no info)
         session.pop('expires', None)
         session.pop('csrf_token', None)
+        session.pop('show_expiration_message', None)
         assert not session
 
         text = resp.data.decode('utf-8')
         assert 'You were logged out due to inactivity' in text
 
 
+def test_source_no_session_expiration_message_when_not_logged_in(config, source_app):
+    """If sources never logged in, no message should be displayed
+    after SESSION_EXPIRATION_MINUTES."""
+
+    with source_app.test_client() as app:
+        seconds_session_expire = 1
+        config.SESSION_EXPIRATION_MINUTES = seconds_session_expire / 60.
+
+        resp = app.get(url_for('main.index'))
+        assert resp.status_code == 200
+
+        time.sleep(seconds_session_expire + 1)
+
+        refreshed_resp = app.get(url_for('main.index'), follow_redirects=True)
+        text = refreshed_resp.data.decode('utf-8')
+        assert 'You were logged out due to inactivity' not in text
+
+
 def test_csrf_error_page(config, source_app):
     source_app.config['WTF_CSRF_ENABLED'] = True
     with source_app.test_client() as app: