From 342343ff7adfe368c94a79e6ea739f3ea61972b3 Mon Sep 17 00:00:00 2001 From: Matt Shirley Date: Wed, 29 Oct 2025 16:35:05 -0500 Subject: [PATCH 1/8] add tests --- servicex_app/servicex_app/__init__.py | 2 +- .../resources/users/token_refresh.py | 9 +- .../resources/users/test_token_refresh.py | 88 +++++++++++++++++++ 3 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 servicex_app/servicex_app_test/resources/users/test_token_refresh.py diff --git a/servicex_app/servicex_app/__init__.py b/servicex_app/servicex_app/__init__.py index 8a5557e57..3c29810a4 100644 --- a/servicex_app/servicex_app/__init__.py +++ b/servicex_app/servicex_app/__init__.py @@ -196,8 +196,8 @@ def approve_user_command(sub): Bootstrap5(app) CORS(app) + app.config["JWT_DECODE_ALGORITHMS"] = ["HS256", "RS256"] JWTManager(app) - # setup logging logstash_host = os.environ.get("LOGSTASH_HOST") diff --git a/servicex_app/servicex_app/resources/users/token_refresh.py b/servicex_app/servicex_app/resources/users/token_refresh.py index c035c56ff..b80e3811b 100644 --- a/servicex_app/servicex_app/resources/users/token_refresh.py +++ b/servicex_app/servicex_app/resources/users/token_refresh.py @@ -26,15 +26,22 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +from flask import current_app from flask_jwt_extended import create_access_token, decode_token, get_jwt, jwt_required from flask_restful import Resource from servicex_app.models import UserModel class TokenRefresh(Resource): - @jwt_required(refresh=True) + @jwt_required(refresh=True, optional=True) def post(self): + if not current_app.config.get("ENABLE_AUTH"): + return {"message": "Authentication is disabled on this instance"}, 200 + claims = get_jwt() + if not claims: + return {"message": "Missing refresh token"}, 401 + user = UserModel.find_by_email(claims["sub"]) decoded = decode_token(user.refresh_token) if not claims["jti"] == decoded["jti"]: diff --git a/servicex_app/servicex_app_test/resources/users/test_token_refresh.py b/servicex_app/servicex_app_test/resources/users/test_token_refresh.py new file mode 100644 index 000000000..2b6193b7e --- /dev/null +++ b/servicex_app/servicex_app_test/resources/users/test_token_refresh.py @@ -0,0 +1,88 @@ +import jwt +from datetime import datetime, timedelta +from flask.wrappers import Response +from flask_jwt_extended import create_refresh_token + +from servicex_app_test.resource_test_base import ResourceTestBase +from servicex_app.models import UserModel + + +class TestTokenRefresh(ResourceTestBase): + """Test suite for token refresh endpoint and JWT configuration.""" + + def test_jwt_decode_algorithms_configured(self, client): + """Test that JWT_DECODE_ALGORITHMS is properly configured with HS256 and RS256.""" + assert "JWT_DECODE_ALGORITHMS" in client.application.config + algorithms = client.application.config["JWT_DECODE_ALGORITHMS"] + + assert "HS256" in algorithms, "HS256 algorithm should be configured" + assert "RS256" in algorithms, "RS256 algorithm should be configured" + + def test_token_refresh_with_auth_disabled(self, client): + """Test that token refresh endpoint returns graceful message when auth is disabled.""" + response: Response = client.post("/token/refresh") + + assert response.status_code == 200 + assert response.json == { + "message": "Authentication is disabled on this instance" + } + + def test_token_refresh_with_auth_enabled_requires_token(self): + """Test that token refresh requires a valid token when auth is enabled.""" + client = self._test_client(extra_config={"ENABLE_AUTH": True}) + + with client.application.app_context(): + response: Response = client.post("/token/refresh") + assert response.status_code == 401 + + def test_token_refresh_with_auth_enabled_and_valid_token(self, mocker): + """Test token refresh works with valid refresh token when auth is enabled.""" + client = self._test_client(extra_config={"ENABLE_AUTH": True}) + + with client.application.app_context(): + test_user = UserModel() + test_user.email = "testuser@example.com" + test_user.sub = "test-sub-123" + test_user.name = "Test User" + test_user.institution = "Test Institution" + + refresh_token = create_refresh_token(identity=test_user.email) + test_user.refresh_token = refresh_token + + mocker.patch( + "servicex_app.resources.users.token_refresh.UserModel.find_by_email", + return_value=test_user + ) + + headers = {"Authorization": f"Bearer {refresh_token}"} + response: Response = client.post("/token/refresh", headers=headers) + + assert response.status_code == 200 + assert "access_token" in response.json + + def test_token_refresh_with_mismatched_jti(self, mocker): + """Test that token refresh fails when JTI doesn't match stored token.""" + client = self._test_client(extra_config={"ENABLE_AUTH": True}) + + with client.application.app_context(): + test_user = UserModel() + test_user.email = "testuser@example.com" + test_user.sub = "test-sub-123" + test_user.name = "Test User" + test_user.institution = "Test Institution" + + old_refresh_token = create_refresh_token(identity=test_user.email) + new_refresh_token = create_refresh_token(identity=test_user.email) + + test_user.refresh_token = new_refresh_token + + mocker.patch( + "servicex_app.resources.users.token_refresh.UserModel.find_by_email", + return_value=test_user + ) + + headers = {"Authorization": f"Bearer {old_refresh_token}"} + response: Response = client.post("/token/refresh", headers=headers) + + assert response.status_code == 401 + assert "Invalid or outdated refresh token" in response.json.get("message", "") From 6ab3b8add16792a1f893ff5626dfb98c4a8ff6aa Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 21:43:45 +0000 Subject: [PATCH 2/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../resources/users/test_token_refresh.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/servicex_app/servicex_app_test/resources/users/test_token_refresh.py b/servicex_app/servicex_app_test/resources/users/test_token_refresh.py index 2b6193b7e..57ef2bea7 100644 --- a/servicex_app/servicex_app_test/resources/users/test_token_refresh.py +++ b/servicex_app/servicex_app_test/resources/users/test_token_refresh.py @@ -51,7 +51,7 @@ def test_token_refresh_with_auth_enabled_and_valid_token(self, mocker): mocker.patch( "servicex_app.resources.users.token_refresh.UserModel.find_by_email", - return_value=test_user + return_value=test_user, ) headers = {"Authorization": f"Bearer {refresh_token}"} @@ -78,11 +78,13 @@ def test_token_refresh_with_mismatched_jti(self, mocker): mocker.patch( "servicex_app.resources.users.token_refresh.UserModel.find_by_email", - return_value=test_user + return_value=test_user, ) headers = {"Authorization": f"Bearer {old_refresh_token}"} response: Response = client.post("/token/refresh", headers=headers) assert response.status_code == 401 - assert "Invalid or outdated refresh token" in response.json.get("message", "") + assert "Invalid or outdated refresh token" in response.json.get( + "message", "" + ) From a31999f6f74e4eb071083fdcd1c829b6ec2f35bd Mon Sep 17 00:00:00 2001 From: Matt Shirley Date: Wed, 29 Oct 2025 16:44:13 -0500 Subject: [PATCH 3/8] resolve flake8 --- .../servicex_app_test/resources/users/test_token_refresh.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/servicex_app/servicex_app_test/resources/users/test_token_refresh.py b/servicex_app/servicex_app_test/resources/users/test_token_refresh.py index 2b6193b7e..62b12a9d2 100644 --- a/servicex_app/servicex_app_test/resources/users/test_token_refresh.py +++ b/servicex_app/servicex_app_test/resources/users/test_token_refresh.py @@ -1,5 +1,3 @@ -import jwt -from datetime import datetime, timedelta from flask.wrappers import Response from flask_jwt_extended import create_refresh_token From 3563133101d25a4a19cbd5a57a62d2e59ee9ed86 Mon Sep 17 00:00:00 2001 From: Matt Shirley Date: Wed, 29 Oct 2025 17:00:38 -0500 Subject: [PATCH 4/8] maintain support with old clients --- .../servicex_app/resources/users/token_refresh.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/servicex_app/servicex_app/resources/users/token_refresh.py b/servicex_app/servicex_app/resources/users/token_refresh.py index b80e3811b..c169f988f 100644 --- a/servicex_app/servicex_app/resources/users/token_refresh.py +++ b/servicex_app/servicex_app/resources/users/token_refresh.py @@ -36,7 +36,11 @@ class TokenRefresh(Resource): @jwt_required(refresh=True, optional=True) def post(self): if not current_app.config.get("ENABLE_AUTH"): - return {"message": "Authentication is disabled on this instance"}, 200 + return { + "message": "Authentication is disabled on this instance", + "access_token": "authentication_disabled", + "auth_disabled": True + }, 200 claims = get_jwt() if not claims: @@ -48,4 +52,4 @@ def post(self): return {"message": "Invalid or outdated refresh token"}, 401 current_user = user.email access_token = create_access_token(identity=current_user) - return {"access_token": access_token} + return {"access_token": access_token}, 200 From 71ddfef5d74060da2d5ab18ed5bf19e232128226 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 22:01:02 +0000 Subject: [PATCH 5/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- servicex_app/servicex_app/resources/users/token_refresh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/servicex_app/servicex_app/resources/users/token_refresh.py b/servicex_app/servicex_app/resources/users/token_refresh.py index c169f988f..28a6dc2e4 100644 --- a/servicex_app/servicex_app/resources/users/token_refresh.py +++ b/servicex_app/servicex_app/resources/users/token_refresh.py @@ -39,7 +39,7 @@ def post(self): return { "message": "Authentication is disabled on this instance", "access_token": "authentication_disabled", - "auth_disabled": True + "auth_disabled": True, }, 200 claims = get_jwt() From fe09530128ed7c9fa2ace13d7713c103cefe6f17 Mon Sep 17 00:00:00 2001 From: Matt Shirley Date: Wed, 29 Oct 2025 17:06:03 -0500 Subject: [PATCH 6/8] add auth_disabled: False when auth enabled --- servicex_app/servicex_app/resources/users/token_refresh.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/servicex_app/servicex_app/resources/users/token_refresh.py b/servicex_app/servicex_app/resources/users/token_refresh.py index c169f988f..15d0ddc2d 100644 --- a/servicex_app/servicex_app/resources/users/token_refresh.py +++ b/servicex_app/servicex_app/resources/users/token_refresh.py @@ -52,4 +52,7 @@ def post(self): return {"message": "Invalid or outdated refresh token"}, 401 current_user = user.email access_token = create_access_token(identity=current_user) - return {"access_token": access_token}, 200 + return { + "access_token": access_token, + "auth_disabled": False + }, 200 From ddc94774178a5011602dc5113acadfe09e59eb82 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 22:06:36 +0000 Subject: [PATCH 7/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- servicex_app/servicex_app/resources/users/token_refresh.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/servicex_app/servicex_app/resources/users/token_refresh.py b/servicex_app/servicex_app/resources/users/token_refresh.py index 1aba629d9..df7b0f631 100644 --- a/servicex_app/servicex_app/resources/users/token_refresh.py +++ b/servicex_app/servicex_app/resources/users/token_refresh.py @@ -52,7 +52,4 @@ def post(self): return {"message": "Invalid or outdated refresh token"}, 401 current_user = user.email access_token = create_access_token(identity=current_user) - return { - "access_token": access_token, - "auth_disabled": False - }, 200 + return {"access_token": access_token, "auth_disabled": False}, 200 From cfa1d08447af2abbd19f423838b3e89d1dd7d684 Mon Sep 17 00:00:00 2001 From: Matt Shirley Date: Wed, 29 Oct 2025 17:19:13 -0500 Subject: [PATCH 8/8] resolve tests --- .../resources/users/test_token_refresh.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/servicex_app/servicex_app_test/resources/users/test_token_refresh.py b/servicex_app/servicex_app_test/resources/users/test_token_refresh.py index e0752536d..85367772f 100644 --- a/servicex_app/servicex_app_test/resources/users/test_token_refresh.py +++ b/servicex_app/servicex_app_test/resources/users/test_token_refresh.py @@ -21,9 +21,10 @@ def test_token_refresh_with_auth_disabled(self, client): response: Response = client.post("/token/refresh") assert response.status_code == 200 - assert response.json == { - "message": "Authentication is disabled on this instance" - } + assert "access_token" in response.json + assert response.json["access_token"] == "authentication_disabled" + assert "auth_disabled" in response.json + assert response.json["auth_disabled"] is True def test_token_refresh_with_auth_enabled_requires_token(self): """Test that token refresh requires a valid token when auth is enabled.""" @@ -57,6 +58,9 @@ def test_token_refresh_with_auth_enabled_and_valid_token(self, mocker): assert response.status_code == 200 assert "access_token" in response.json + assert response.json["access_token"] != "authentication_disabled" + assert "auth_disabled" in response.json + assert response.json["auth_disabled"] is False def test_token_refresh_with_mismatched_jti(self, mocker): """Test that token refresh fails when JTI doesn't match stored token."""