feat: implement long-term user login with refresh tokens
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m23s
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m23s
- Introduced a dual-token system for user authentication: a short-lived access token and a long-lived rotating refresh token. - Created a new RefreshToken model to manage refresh tokens securely. - Updated auth_api.py to handle login, refresh, and logout processes with the new token system. - Enhanced security measures including token rotation and theft detection. - Updated frontend to handle token refresh on 401 errors and adjusted SSE authentication. - Removed CORS middleware as it's unnecessary behind the nginx proxy. - Added tests to ensure functionality and security of the new token system.
This commit is contained in:
@@ -14,13 +14,13 @@ from models.user import User
|
||||
from db.db import users_db
|
||||
from config.deletion_config import MIN_THRESHOLD_HOURS, MAX_THRESHOLD_HOURS
|
||||
from tinydb import Query
|
||||
from tests.conftest import TEST_SECRET_KEY
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
"""Create test client."""
|
||||
app.config['TESTING'] = True
|
||||
app.config['SECRET_KEY'] = 'supersecretkey'
|
||||
with app.test_client() as client:
|
||||
yield client
|
||||
|
||||
@@ -45,7 +45,7 @@ def admin_user():
|
||||
users_db.insert(user.to_dict())
|
||||
|
||||
# Create JWT token
|
||||
token = jwt.encode({'user_id': 'admin_user'}, 'supersecretkey', algorithm='HS256')
|
||||
token = jwt.encode({'user_id': 'admin_user'}, TEST_SECRET_KEY, algorithm='HS256')
|
||||
return token
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ class TestGetDeletionQueue:
|
||||
|
||||
def test_get_deletion_queue_success(self, client, admin_user, setup_deletion_queue):
|
||||
"""Test getting deletion queue returns correct users."""
|
||||
client.set_cookie('token', admin_user)
|
||||
client.set_cookie('access_token', admin_user)
|
||||
response = client.get('/admin/deletion-queue')
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -147,7 +147,7 @@ class TestGetDeletionQueue:
|
||||
|
||||
def test_get_deletion_queue_invalid_token(self, client, setup_deletion_queue):
|
||||
"""Test that invalid token is rejected."""
|
||||
client.set_cookie('token', 'invalid_token')
|
||||
client.set_cookie('access_token', 'invalid_token')
|
||||
response = client.get('/admin/deletion-queue')
|
||||
|
||||
assert response.status_code == 401
|
||||
@@ -161,11 +161,11 @@ class TestGetDeletionQueue:
|
||||
# Create expired token
|
||||
expired_token = jwt.encode(
|
||||
{'user_id': 'admin_user', 'exp': datetime.now() - timedelta(hours=1)},
|
||||
'supersecretkey',
|
||||
TEST_SECRET_KEY,
|
||||
algorithm='HS256'
|
||||
)
|
||||
|
||||
client.set_cookie('token', expired_token)
|
||||
client.set_cookie('access_token', expired_token)
|
||||
response = client.get('/admin/deletion-queue')
|
||||
|
||||
assert response.status_code == 401
|
||||
@@ -192,7 +192,7 @@ class TestGetDeletionQueue:
|
||||
)
|
||||
users_db.insert(admin.to_dict())
|
||||
|
||||
client.set_cookie('token', admin_user)
|
||||
client.set_cookie('access_token', admin_user)
|
||||
response = client.get('/admin/deletion-queue')
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -206,7 +206,7 @@ class TestGetDeletionThreshold:
|
||||
|
||||
def test_get_threshold_success(self, client, admin_user):
|
||||
"""Test getting current threshold configuration."""
|
||||
client.set_cookie('token', admin_user)
|
||||
client.set_cookie('access_token', admin_user)
|
||||
response = client.get('/admin/deletion-threshold')
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -232,7 +232,7 @@ class TestUpdateDeletionThreshold:
|
||||
|
||||
def test_update_threshold_success(self, client, admin_user):
|
||||
"""Test updating threshold with valid value."""
|
||||
client.set_cookie('token', admin_user)
|
||||
client.set_cookie('access_token', admin_user)
|
||||
response = client.put(
|
||||
'/admin/deletion-threshold',
|
||||
json={'threshold_hours': 168}
|
||||
@@ -245,7 +245,7 @@ class TestUpdateDeletionThreshold:
|
||||
|
||||
def test_update_threshold_validates_minimum(self, client, admin_user):
|
||||
"""Test that threshold below minimum is rejected."""
|
||||
client.set_cookie('token', admin_user)
|
||||
client.set_cookie('access_token', admin_user)
|
||||
response = client.put(
|
||||
'/admin/deletion-threshold',
|
||||
json={'threshold_hours': 23}
|
||||
@@ -258,7 +258,7 @@ class TestUpdateDeletionThreshold:
|
||||
|
||||
def test_update_threshold_validates_maximum(self, client, admin_user):
|
||||
"""Test that threshold above maximum is rejected."""
|
||||
client.set_cookie('token', admin_user)
|
||||
client.set_cookie('access_token', admin_user)
|
||||
response = client.put(
|
||||
'/admin/deletion-threshold',
|
||||
json={'threshold_hours': 721}
|
||||
@@ -271,7 +271,7 @@ class TestUpdateDeletionThreshold:
|
||||
|
||||
def test_update_threshold_missing_value(self, client, admin_user):
|
||||
"""Test that missing threshold value is rejected."""
|
||||
client.set_cookie('token', admin_user)
|
||||
client.set_cookie('access_token', admin_user)
|
||||
response = client.put(
|
||||
'/admin/deletion-threshold',
|
||||
json={}
|
||||
@@ -284,7 +284,7 @@ class TestUpdateDeletionThreshold:
|
||||
|
||||
def test_update_threshold_invalid_type(self, client, admin_user):
|
||||
"""Test that non-integer threshold is rejected."""
|
||||
client.set_cookie('token', admin_user)
|
||||
client.set_cookie('access_token', admin_user)
|
||||
response = client.put(
|
||||
'/admin/deletion-threshold',
|
||||
json={'threshold_hours': 'invalid'}
|
||||
@@ -310,7 +310,7 @@ class TestTriggerDeletionQueue:
|
||||
|
||||
def test_trigger_deletion_success(self, client, admin_user, setup_deletion_queue):
|
||||
"""Test manually triggering deletion queue."""
|
||||
client.set_cookie('token', admin_user)
|
||||
client.set_cookie('access_token', admin_user)
|
||||
response = client.post('/admin/deletion-queue/trigger')
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -348,7 +348,7 @@ class TestTriggerDeletionQueue:
|
||||
)
|
||||
users_db.insert(admin.to_dict())
|
||||
|
||||
client.set_cookie('token', admin_user)
|
||||
client.set_cookie('access_token', admin_user)
|
||||
response = client.post('/admin/deletion-queue/trigger')
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -381,9 +381,9 @@ class TestAdminRoleValidation:
|
||||
users_db.insert(user.to_dict())
|
||||
|
||||
# Create token for non-admin
|
||||
token = jwt.encode({'user_id': 'regular_user'}, 'supersecretkey', algorithm='HS256')
|
||||
token = jwt.encode({'user_id': 'regular_user'}, TEST_SECRET_KEY, algorithm='HS256')
|
||||
|
||||
client.set_cookie('token', token)
|
||||
client.set_cookie('access_token', token)
|
||||
response = client.get('/admin/deletion-queue')
|
||||
|
||||
# Should return 403 Forbidden
|
||||
@@ -414,9 +414,9 @@ class TestAdminRoleValidation:
|
||||
users_db.insert(admin.to_dict())
|
||||
|
||||
# Create token for admin
|
||||
token = jwt.encode({'user_id': 'admin_user'}, 'supersecretkey', algorithm='HS256')
|
||||
token = jwt.encode({'user_id': 'admin_user'}, TEST_SECRET_KEY, algorithm='HS256')
|
||||
|
||||
client.set_cookie('token', token)
|
||||
client.set_cookie('access_token', token)
|
||||
response = client.get('/admin/deletion-queue')
|
||||
|
||||
# Should succeed
|
||||
@@ -439,9 +439,9 @@ class TestAdminRoleValidation:
|
||||
)
|
||||
users_db.insert(user.to_dict())
|
||||
|
||||
token = jwt.encode({'user_id': 'regular_user'}, 'supersecretkey', algorithm='HS256')
|
||||
token = jwt.encode({'user_id': 'regular_user'}, TEST_SECRET_KEY, algorithm='HS256')
|
||||
|
||||
client.set_cookie('token', token)
|
||||
client.set_cookie('access_token', token)
|
||||
response = client.put('/admin/deletion-threshold', json={'threshold_hours': 168})
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
Reference in New Issue
Block a user