Skip to content

Commit e68e6a4

Browse files
authored
Merge pull request #1194 from ssl-hep/874-appauth-false-settings-doesnt-work-anymore-with-bearer_token-env-variable-configured-in-enviroment
add graceful exits to token refresh
2 parents 7acb93e + d635ef5 commit e68e6a4

File tree

3 files changed

+106
-3
lines changed

3 files changed

+106
-3
lines changed

servicex_app/servicex_app/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,8 @@ def approve_user_command(sub):
196196
Bootstrap5(app)
197197
CORS(app)
198198

199+
app.config["JWT_DECODE_ALGORITHMS"] = ["HS256", "RS256"]
199200
JWTManager(app)
200-
201201
# setup logging
202202

203203
logstash_host = os.environ.get("LOGSTASH_HOST")

servicex_app/servicex_app/resources/users/token_refresh.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,30 @@
2626
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
2727
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2828

29+
from flask import current_app
2930
from flask_jwt_extended import create_access_token, decode_token, get_jwt, jwt_required
3031
from flask_restful import Resource
3132
from servicex_app.models import UserModel
3233

3334

3435
class TokenRefresh(Resource):
35-
@jwt_required(refresh=True)
36+
@jwt_required(refresh=True, optional=True)
3637
def post(self):
38+
if not current_app.config.get("ENABLE_AUTH"):
39+
return {
40+
"message": "Authentication is disabled on this instance",
41+
"access_token": "authentication_disabled",
42+
"auth_disabled": True,
43+
}, 200
44+
3745
claims = get_jwt()
46+
if not claims:
47+
return {"message": "Missing refresh token"}, 401
48+
3849
user = UserModel.find_by_email(claims["sub"])
3950
decoded = decode_token(user.refresh_token)
4051
if not claims["jti"] == decoded["jti"]:
4152
return {"message": "Invalid or outdated refresh token"}, 401
4253
current_user = user.email
4354
access_token = create_access_token(identity=current_user)
44-
return {"access_token": access_token}
55+
return {"access_token": access_token, "auth_disabled": False}, 200
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
from flask.wrappers import Response
2+
from flask_jwt_extended import create_refresh_token
3+
4+
from servicex_app_test.resource_test_base import ResourceTestBase
5+
from servicex_app.models import UserModel
6+
7+
8+
class TestTokenRefresh(ResourceTestBase):
9+
"""Test suite for token refresh endpoint and JWT configuration."""
10+
11+
def test_jwt_decode_algorithms_configured(self, client):
12+
"""Test that JWT_DECODE_ALGORITHMS is properly configured with HS256 and RS256."""
13+
assert "JWT_DECODE_ALGORITHMS" in client.application.config
14+
algorithms = client.application.config["JWT_DECODE_ALGORITHMS"]
15+
16+
assert "HS256" in algorithms, "HS256 algorithm should be configured"
17+
assert "RS256" in algorithms, "RS256 algorithm should be configured"
18+
19+
def test_token_refresh_with_auth_disabled(self, client):
20+
"""Test that token refresh endpoint returns graceful message when auth is disabled."""
21+
response: Response = client.post("/token/refresh")
22+
23+
assert response.status_code == 200
24+
assert "access_token" in response.json
25+
assert response.json["access_token"] == "authentication_disabled"
26+
assert "auth_disabled" in response.json
27+
assert response.json["auth_disabled"] is True
28+
29+
def test_token_refresh_with_auth_enabled_requires_token(self):
30+
"""Test that token refresh requires a valid token when auth is enabled."""
31+
client = self._test_client(extra_config={"ENABLE_AUTH": True})
32+
33+
with client.application.app_context():
34+
response: Response = client.post("/token/refresh")
35+
assert response.status_code == 401
36+
37+
def test_token_refresh_with_auth_enabled_and_valid_token(self, mocker):
38+
"""Test token refresh works with valid refresh token when auth is enabled."""
39+
client = self._test_client(extra_config={"ENABLE_AUTH": True})
40+
41+
with client.application.app_context():
42+
test_user = UserModel()
43+
test_user.email = "[email protected]"
44+
test_user.sub = "test-sub-123"
45+
test_user.name = "Test User"
46+
test_user.institution = "Test Institution"
47+
48+
refresh_token = create_refresh_token(identity=test_user.email)
49+
test_user.refresh_token = refresh_token
50+
51+
mocker.patch(
52+
"servicex_app.resources.users.token_refresh.UserModel.find_by_email",
53+
return_value=test_user,
54+
)
55+
56+
headers = {"Authorization": f"Bearer {refresh_token}"}
57+
response: Response = client.post("/token/refresh", headers=headers)
58+
59+
assert response.status_code == 200
60+
assert "access_token" in response.json
61+
assert response.json["access_token"] != "authentication_disabled"
62+
assert "auth_disabled" in response.json
63+
assert response.json["auth_disabled"] is False
64+
65+
def test_token_refresh_with_mismatched_jti(self, mocker):
66+
"""Test that token refresh fails when JTI doesn't match stored token."""
67+
client = self._test_client(extra_config={"ENABLE_AUTH": True})
68+
69+
with client.application.app_context():
70+
test_user = UserModel()
71+
test_user.email = "[email protected]"
72+
test_user.sub = "test-sub-123"
73+
test_user.name = "Test User"
74+
test_user.institution = "Test Institution"
75+
76+
old_refresh_token = create_refresh_token(identity=test_user.email)
77+
new_refresh_token = create_refresh_token(identity=test_user.email)
78+
79+
test_user.refresh_token = new_refresh_token
80+
81+
mocker.patch(
82+
"servicex_app.resources.users.token_refresh.UserModel.find_by_email",
83+
return_value=test_user,
84+
)
85+
86+
headers = {"Authorization": f"Bearer {old_refresh_token}"}
87+
response: Response = client.post("/token/refresh", headers=headers)
88+
89+
assert response.status_code == 401
90+
assert "Invalid or outdated refresh token" in response.json.get(
91+
"message", ""
92+
)

0 commit comments

Comments
 (0)