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:
@@ -2,10 +2,11 @@ import pytest
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from flask import Flask
|
||||
from api.auth_api import auth_api
|
||||
from db.db import users_db
|
||||
from db.db import users_db, refresh_tokens_db
|
||||
from tinydb import Query
|
||||
from models.user import User
|
||||
from datetime import datetime
|
||||
from tests.conftest import TEST_SECRET_KEY, TEST_REFRESH_TOKEN_EXPIRY_DAYS
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
@@ -13,7 +14,8 @@ def client():
|
||||
app = Flask(__name__)
|
||||
app.register_blueprint(auth_api, url_prefix='/auth')
|
||||
app.config['TESTING'] = True
|
||||
app.config['SECRET_KEY'] = 'supersecretkey'
|
||||
app.config['SECRET_KEY'] = TEST_SECRET_KEY
|
||||
app.config['REFRESH_TOKEN_EXPIRY_DAYS'] = TEST_REFRESH_TOKEN_EXPIRY_DAYS
|
||||
app.config['FRONTEND_URL'] = 'http://localhost:5173'
|
||||
with app.test_client() as client:
|
||||
yield client
|
||||
@@ -54,7 +56,10 @@ def test_login_with_correct_password(client):
|
||||
data = {'email': 'test@example.com', 'password': 'password123'}
|
||||
response = client.post('/auth/login', json=data)
|
||||
assert response.status_code == 200
|
||||
assert 'token' in response.headers.get('Set-Cookie', '')
|
||||
cookies = response.headers.getlist('Set-Cookie')
|
||||
cookie_str = ' '.join(cookies)
|
||||
assert 'access_token=' in cookie_str
|
||||
assert 'refresh_token=' in cookie_str
|
||||
|
||||
def test_login_with_incorrect_password(client):
|
||||
"""Test login fails with incorrect password."""
|
||||
@@ -116,18 +121,30 @@ def test_reset_password_invalidates_existing_jwt(client):
|
||||
|
||||
login_response = client.post('/auth/login', json={'email': 'test@example.com', 'password': 'oldpassword123'})
|
||||
assert login_response.status_code == 200
|
||||
login_cookie = login_response.headers.get('Set-Cookie', '')
|
||||
assert 'token=' in login_cookie
|
||||
old_token = login_cookie.split('token=', 1)[1].split(';', 1)[0]
|
||||
login_cookies = login_response.headers.getlist('Set-Cookie')
|
||||
login_cookie_str = ' '.join(login_cookies)
|
||||
assert 'access_token=' in login_cookie_str
|
||||
# Extract the old access token
|
||||
old_token = None
|
||||
for c in login_cookies:
|
||||
if c.startswith('access_token='):
|
||||
old_token = c.split('access_token=', 1)[1].split(';', 1)[0]
|
||||
break
|
||||
assert old_token
|
||||
|
||||
reset_response = client.post('/auth/reset-password', json={'token': 'validtoken2', 'password': 'newpassword123'})
|
||||
assert reset_response.status_code == 200
|
||||
reset_cookie = reset_response.headers.get('Set-Cookie', '')
|
||||
assert 'token=' in reset_cookie
|
||||
reset_cookies = reset_response.headers.getlist('Set-Cookie')
|
||||
reset_cookie_str = ' '.join(reset_cookies)
|
||||
assert 'access_token=' in reset_cookie_str
|
||||
|
||||
# Verify all refresh tokens for this user are deleted
|
||||
user_dict = users_db.get(Query().email == 'test@example.com')
|
||||
user_tokens = refresh_tokens_db.search(Query().user_id == user_dict['id'])
|
||||
assert len(user_tokens) == 0
|
||||
|
||||
# Set the old token as a cookie and test that it's now invalid
|
||||
client.set_cookie('token', old_token)
|
||||
client.set_cookie('access_token', old_token)
|
||||
me_response = client.get('/auth/me')
|
||||
assert me_response.status_code == 401
|
||||
assert me_response.json['code'] == 'INVALID_TOKEN'
|
||||
|
||||
Reference in New Issue
Block a user