Phase 4: Authentication System (T039-T048)

Implemented complete JWT-based authentication system with RBAC:

**Tests (TDD Approach):**
- Created contract tests for /api/v1/auth/login endpoint
- Created contract tests for /api/v1/auth/logout endpoint
- Created unit tests for AuthService (login, logout, validate_token, password hashing)
- Created pytest configuration and fixtures (test DB, test users, tokens)

**Schemas:**
- LoginRequest: username/password validation
- TokenResponse: access_token, refresh_token, user info
- LogoutResponse: logout confirmation
- RefreshTokenRequest: token refresh payload
- UserInfo: user data (excludes password_hash)

**Services:**
- AuthService: login(), logout(), validate_token(), hash_password(), verify_password()
- Integrated bcrypt password hashing
- JWT token generation (access + refresh tokens)
- Token blacklisting in Redis
- Audit logging for all auth operations

**Middleware:**
- Authentication middleware with JWT validation
- Role-based access control (RBAC) helpers
- require_role() dependency factory
- Convenience dependencies: require_viewer(), require_operator(), require_administrator()
- Client IP and User-Agent extraction

**Router:**
- POST /api/v1/auth/login - Authenticate and get tokens
- POST /api/v1/auth/logout - Blacklist token
- POST /api/v1/auth/refresh - Refresh access token
- GET /api/v1/auth/me - Get current user info

**Integration:**
- Registered auth router in main.py
- Updated startup event to initialize Redis and SDK Bridge clients
- Updated shutdown event to cleanup connections properly
- Fixed error translation utilities
- Added asyncpg dependency for PostgreSQL async driver

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Geutebruck API Developer
2025-12-09 09:04:16 +01:00
parent a4bde18d0f
commit fbebe10711
15 changed files with 1651 additions and 13 deletions

View File

@@ -0,0 +1,172 @@
"""
Contract tests for authentication API endpoints
These tests define the expected behavior - they will FAIL until implementation is complete
"""
import pytest
from httpx import AsyncClient
from fastapi import status
from main import app
@pytest.mark.asyncio
class TestAuthLogin:
"""Contract tests for POST /api/v1/auth/login"""
async def test_login_success(self, async_client: AsyncClient):
"""Test successful login with valid credentials"""
response = await async_client.post(
"/api/v1/auth/login",
json={
"username": "admin",
"password": "admin123"
}
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# Verify response structure
assert "access_token" in data
assert "refresh_token" in data
assert "token_type" in data
assert "expires_in" in data
assert "user" in data
# Verify token type
assert data["token_type"] == "bearer"
# Verify user info
assert data["user"]["username"] == "admin"
assert data["user"]["role"] == "administrator"
assert "password_hash" not in data["user"] # Never expose password hash
async def test_login_invalid_username(self, async_client: AsyncClient):
"""Test login with non-existent username"""
response = await async_client.post(
"/api/v1/auth/login",
json={
"username": "nonexistent",
"password": "somepassword"
}
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
data = response.json()
assert "error" in data
assert data["error"] == "Unauthorized"
async def test_login_invalid_password(self, async_client: AsyncClient):
"""Test login with incorrect password"""
response = await async_client.post(
"/api/v1/auth/login",
json={
"username": "admin",
"password": "wrongpassword"
}
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
data = response.json()
assert "error" in data
async def test_login_missing_username(self, async_client: AsyncClient):
"""Test login with missing username field"""
response = await async_client.post(
"/api/v1/auth/login",
json={
"password": "admin123"
}
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
async def test_login_missing_password(self, async_client: AsyncClient):
"""Test login with missing password field"""
response = await async_client.post(
"/api/v1/auth/login",
json={
"username": "admin"
}
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
async def test_login_empty_username(self, async_client: AsyncClient):
"""Test login with empty username"""
response = await async_client.post(
"/api/v1/auth/login",
json={
"username": "",
"password": "admin123"
}
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
async def test_login_empty_password(self, async_client: AsyncClient):
"""Test login with empty password"""
response = await async_client.post(
"/api/v1/auth/login",
json={
"username": "admin",
"password": ""
}
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
@pytest.mark.asyncio
class TestAuthLogout:
"""Contract tests for POST /api/v1/auth/logout"""
async def test_logout_success(self, async_client: AsyncClient, auth_token: str):
"""Test successful logout with valid token"""
response = await async_client.post(
"/api/v1/auth/logout",
headers={"Authorization": f"Bearer {auth_token}"}
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["message"] == "Successfully logged out"
async def test_logout_no_token(self, async_client: AsyncClient):
"""Test logout without authentication token"""
response = await async_client.post("/api/v1/auth/logout")
assert response.status_code == status.HTTP_401_UNAUTHORIZED
async def test_logout_invalid_token(self, async_client: AsyncClient):
"""Test logout with invalid token"""
response = await async_client.post(
"/api/v1/auth/logout",
headers={"Authorization": "Bearer invalid_token_here"}
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
async def test_logout_expired_token(self, async_client: AsyncClient, expired_token: str):
"""Test logout with expired token"""
response = await async_client.post(
"/api/v1/auth/logout",
headers={"Authorization": f"Bearer {expired_token}"}
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.asyncio
class TestAuthProtectedEndpoint:
"""Test authentication middleware on protected endpoints"""
async def test_protected_endpoint_with_valid_token(self, async_client: AsyncClient, auth_token: str):
"""Test accessing protected endpoint with valid token"""
# This will be used to test any protected endpoint once we have them
# For now, we'll test with a mock protected endpoint
pass
async def test_protected_endpoint_without_token(self, async_client: AsyncClient):
"""Test accessing protected endpoint without token"""
# Will be implemented when we have actual protected endpoints
pass