diff --git a/docs/source/Login.rst b/docs/source/Login.rst index c9a3d8c08..66721cde6 100644 --- a/docs/source/Login.rst +++ b/docs/source/Login.rst @@ -1,7 +1,22 @@ -Login API -======================== +Login +===== + +MXCuBE web sessions are meant to expire when there is no activity, +as opposed to a typical web session that expires when the browser is closed. + +So typically a MXCuBE web session closes 1 minute after the browser tab closes. +After that it is necessary to sign in again. + +For this purpose: + +* Flask config setting ``PERMANENT_SESSION_LIFETIME`` is set to 60 seconds. +* There is a ``/mxcube/api/v0.1/login/refresh_session`` endpoint + that the front end must call regularly (as long as the browser tab is open). + + +Login API +--------- .. autoflask:: mxcube3.routes.Login:mxcube :endpoints: login, signout, loginInfo, get_initial_state, proposal_samples - diff --git a/mxcube3/core/components/user/usermanager.py b/mxcube3/core/components/user/usermanager.py index e6a256794..1c6bf44d6 100644 --- a/mxcube3/core/components/user/usermanager.py +++ b/mxcube3/core/components/user/usermanager.py @@ -93,8 +93,7 @@ def update_active_users(self): and _u.last_request_timestamp and ( datetime.datetime.now() - _u.last_request_timestamp - ).total_seconds() - > 60 + ) > flask.current_app.permanent_session_lifetime ): logging.getLogger("HWR.MX3").info( f"Logged out inactive user {_u.username}" diff --git a/mxcube3/routes/login.py b/mxcube3/routes/login.py index b93c95a19..d9c0e557f 100644 --- a/mxcube3/routes/login.py +++ b/mxcube3/routes/login.py @@ -103,8 +103,10 @@ def send_feedback(): @bp.route("/refresh_session", methods=["GET"]) @server.restrict def refresh_session(): + # Since default value of `SESSION_REFRESH_EACH_REQUEST` config setting is `True` + # there is no need to do anything to refresh the session. logging.getLogger("MX3.HWR").debug("Session refresh") - server.flask.permanent_session_lifetime = timedelta(minutes=1) app.usermanager.update_active_users() return make_response("", 200) + return bp diff --git a/test/test_authn.py b/test/test_authn.py index 3552cbd9f..e05d42731 100644 --- a/test/test_authn.py +++ b/test/test_authn.py @@ -4,7 +4,9 @@ """Authentication tests.""" +import datetime import os +import time import pytest @@ -15,12 +17,15 @@ URL_SIGNIN = f"{URL_BASE}/" # Trailing slash is necessary URL_SIGNOUT = f"{URL_BASE}/signout" URL_INFO = f"{URL_BASE}/login_info" +URL_REFRESH = f"{URL_BASE}/refresh_session" CREDENTIALS_0 = {"proposal": "idtest0", "password": "sUpErSaFe"} # Password has to be `wrong` to simulate wrong password in `ISPyBClientMockup` CREDENTIALS_0_WRONG = {"proposal": "idtest0", "password": "wrong"} CREDENTIALS_1 = {"proposal": "idtest1", "password": "sUpErSaFe"} +SESSION_LIFETIME = 2.0 # seconds + USER_DB_PATH = "/tmp/mxcube-test-user.db" @@ -36,6 +41,9 @@ def server(): argv = [] server_, _ = mxcube3.build_server_and_config(test=True, argv=argv) server_.flask.config["TESTING"] = True + # For the tests we override the configured value of the session lifetime + # with a much smaller value, so that tests do not need to wait as long. + server_.flask.permanent_session_lifetime = SESSION_LIFETIME yield server_ @@ -87,7 +95,6 @@ def test_authn_info(client): The login info should have `loggedIn` false before authentication and true after successful authentication. """ - resp = client.get(URL_INFO) assert resp.status_code == 200 assert resp.json["loggedIn"] == False @@ -140,4 +147,41 @@ def test_authn_different_proposals(make_client): assert resp.json["msg"] == "Could not authenticate" +def test_authn_session_timeout(client): + """Test the session timeout + + The session can be refreshed, and can expire. + It should be possible to sign in again after a valid session expired. + """ + + # Sign in and --as a side effect-- create a session + client.post(URL_SIGNIN, json=CREDENTIALS_0) + resp = client.get(URL_INFO) + + # Let the session nearly expire + time.sleep(SESSION_LIFETIME * 0.9) + + # Refresh the session + resp = client.get(URL_REFRESH) + + # Let the session nearly expire again + time.sleep(SESSION_LIFETIME * 0.9) + + # Check that the session still has not expired + resp = client.get(URL_INFO) + assert resp.json["loggedIn"] == True, "Session did not refresh" + + # Let the session expire completely + time.sleep(SESSION_LIFETIME * 1.5) + + # Check that the session has expired + resp = client.get(URL_INFO) + assert resp.json["loggedIn"] == False, "Session did not expire" + + # Check that it is possible to sign in again + client.post(URL_SIGNIN, json=CREDENTIALS_0) + resp = client.get(URL_INFO) + assert resp.json["loggedIn"] == True, "We can not login again" + + # EOF