Enhance OAuth security and state validation

- Implemented stricter OAuth security measures, including CSRF protection via state parameter validation and redirect_uri checks.
- Updated OAuth models to support timezone-aware datetime comparisons, replacing deprecated `utcnow`.
- Enhanced logging for malformed Basic auth headers during token, introspect, and revoke requests.
- Added allowlist validation for OAuth provider domains to prevent open redirect attacks.
- Improved nonce validation for OpenID Connect tokens, ensuring token integrity during Google provider flows.
- Updated E2E and unit tests to cover new security features and expanded OAuth state handling scenarios.
This commit is contained in:
Felipe Cardoso
2025-11-25 23:50:43 +01:00
parent 7716468d72
commit 400d6f6f75
14 changed files with 246 additions and 57 deletions

View File

@@ -214,9 +214,6 @@ async def e2e_superuser(e2e_client):
"""
from uuid import uuid4
from app.crud.user import user as user_crud
from app.schemas.users import UserCreate
email = f"admin-{uuid4().hex[:8]}@example.com"
password = "SuperAdmin123!"

View File

@@ -21,7 +21,7 @@ pytestmark = [
]
async def register_user(client, email: str, password: str = "SecurePassword123!"):
async def register_user(client, email: str, password: str = "SecurePassword123!"): # noqa: S107
"""Helper to register a user."""
resp = await client.post(
"/api/v1/auth/register",
@@ -35,7 +35,7 @@ async def register_user(client, email: str, password: str = "SecurePassword123!"
return resp.json()
async def login_user(client, email: str, password: str = "SecurePassword123!"):
async def login_user(client, email: str, password: str = "SecurePassword123!"): # noqa: S107
"""Helper to login a user."""
resp = await client.post(
"/api/v1/auth/login",

View File

@@ -22,7 +22,7 @@ pytestmark = [
]
async def register_and_login(client, email: str, password: str = "SecurePassword123!"):
async def register_and_login(client, email: str, password: str = "SecurePassword123!"): # noqa: S107
"""Helper to register a user and get tokens."""
# Register
await client.post(

View File

@@ -451,6 +451,7 @@ class TestHandleCallbackComplete:
state="valid_state_login",
provider="google",
code_verifier="test_verifier",
redirect_uri="http://localhost:3000/callback",
expires_at=datetime.now(UTC) + timedelta(minutes=10),
)
await oauth_state.create_state(session, obj_in=state_data)
@@ -533,6 +534,7 @@ class TestHandleCallbackComplete:
state_data = OAuthStateCreate(
state="valid_state_inactive",
provider="google",
redirect_uri="http://localhost:3000/callback",
expires_at=datetime.now(UTC) + timedelta(minutes=10),
)
await oauth_state.create_state(session, obj_in=state_data)
@@ -583,6 +585,7 @@ class TestHandleCallbackComplete:
state="valid_state_linking",
provider="github",
user_id=async_test_user.id, # User is logged in
redirect_uri="http://localhost:3000/callback",
expires_at=datetime.now(UTC) + timedelta(minutes=10),
)
await oauth_state.create_state(session, obj_in=state_data)
@@ -648,6 +651,7 @@ class TestHandleCallbackComplete:
state="valid_state_bad_user",
provider="google",
user_id=uuid4(), # Non-existent user
redirect_uri="http://localhost:3000/callback",
expires_at=datetime.now(UTC) + timedelta(minutes=10),
)
await oauth_state.create_state(session, obj_in=state_data)
@@ -707,6 +711,7 @@ class TestHandleCallbackComplete:
state="valid_state_already_linked",
provider="google",
user_id=async_test_user.id,
redirect_uri="http://localhost:3000/callback",
expires_at=datetime.now(UTC) + timedelta(minutes=10),
)
await oauth_state.create_state(session, obj_in=state_data)
@@ -769,6 +774,7 @@ class TestHandleCallbackComplete:
state_data = OAuthStateCreate(
state="valid_state_autolink",
provider="google",
redirect_uri="http://localhost:3000/callback",
expires_at=datetime.now(UTC) + timedelta(minutes=10),
)
await oauth_state.create_state(session, obj_in=state_data)
@@ -832,6 +838,7 @@ class TestHandleCallbackComplete:
state_data = OAuthStateCreate(
state="valid_state_new_user",
provider="google",
redirect_uri="http://localhost:3000/callback",
expires_at=datetime.now(UTC) + timedelta(minutes=10),
)
await oauth_state.create_state(session, obj_in=state_data)
@@ -904,6 +911,7 @@ class TestHandleCallbackComplete:
state_data = OAuthStateCreate(
state="valid_state_no_email",
provider="github",
redirect_uri="http://localhost:3000/callback",
expires_at=datetime.now(UTC) + timedelta(minutes=10),
)
await oauth_state.create_state(session, obj_in=state_data)
@@ -961,6 +969,7 @@ class TestHandleCallbackComplete:
state_data = OAuthStateCreate(
state="valid_state_token_fail",
provider="google",
redirect_uri="http://localhost:3000/callback",
expires_at=datetime.now(UTC) + timedelta(minutes=10),
)
await oauth_state.create_state(session, obj_in=state_data)
@@ -1004,6 +1013,7 @@ class TestHandleCallbackComplete:
state_data = OAuthStateCreate(
state="valid_state_userinfo_fail",
provider="google",
redirect_uri="http://localhost:3000/callback",
expires_at=datetime.now(UTC) + timedelta(minutes=10),
)
await oauth_state.create_state(session, obj_in=state_data)
@@ -1047,6 +1057,7 @@ class TestHandleCallbackComplete:
state_data = OAuthStateCreate(
state="valid_state_no_token",
provider="google",
redirect_uri="http://localhost:3000/callback",
expires_at=datetime.now(UTC) + timedelta(minutes=10),
)
await oauth_state.create_state(session, obj_in=state_data)
@@ -1090,6 +1101,7 @@ class TestHandleCallbackComplete:
state_data = OAuthStateCreate(
state="valid_state_no_user_id",
provider="google",
redirect_uri="http://localhost:3000/callback",
expires_at=datetime.now(UTC) + timedelta(minutes=10),
)
await oauth_state.create_state(session, obj_in=state_data)