Cybersecurity-Projects/PROJECTS/bug-bounty-platform/backend/app/auth/service.py

206 lines
5.5 KiB
Python

"""
ⒸAngelaMos | 2025
service.py
"""
import uuid6
from sqlalchemy.ext.asyncio import (
AsyncSession,
)
from core.exceptions import (
InvalidCredentials,
TokenError,
TokenRevokedError,
)
from core.security import (
hash_token,
create_access_token,
create_refresh_token,
verify_password_with_timing_safety,
)
from user.User import User
from user.repository import UserRepository
from .repository import RefreshTokenRepository
from .schemas import (
TokenResponse,
TokenWithUserResponse,
)
from user.schemas import UserResponse
class AuthService:
"""
Business logic for authentication operations
"""
def __init__(self, session: AsyncSession) -> None:
self.session = session
async def authenticate(
self,
email: str,
password: str,
device_id: str | None = None,
device_name: str | None = None,
ip_address: str | None = None,
) -> tuple[str,
str,
User]:
"""
Authenticate user and create tokens
"""
user = await UserRepository.get_by_email(self.session, email)
hashed_password = user.hashed_password if user else None
is_valid, new_hash = await verify_password_with_timing_safety(
password, hashed_password
)
if not is_valid or user is None:
raise InvalidCredentials()
if not user.is_active:
raise InvalidCredentials()
if new_hash:
await UserRepository.update_password(self.session, user, new_hash)
access_token = create_access_token(user.id, user.token_version)
family_id = uuid6.uuid7()
raw_refresh, token_hash, expires_at = create_refresh_token(user.id, family_id)
await RefreshTokenRepository.create_token(
self.session,
user_id = user.id,
token_hash = token_hash,
family_id = family_id,
expires_at = expires_at,
device_id = device_id,
device_name = device_name,
ip_address = ip_address,
)
return access_token, raw_refresh, user
async def login(
self,
email: str,
password: str,
device_id: str | None = None,
device_name: str | None = None,
ip_address: str | None = None,
) -> tuple[TokenWithUserResponse,
str]:
"""
Login and return tokens with user data
"""
access_token, refresh_token, user = await self.authenticate(
email,
password,
device_id,
device_name,
ip_address,
)
response = TokenWithUserResponse(
access_token = access_token,
user = UserResponse.model_validate(user),
)
return response, refresh_token
async def refresh_tokens(
self,
refresh_token: str,
device_id: str | None = None,
device_name: str | None = None,
ip_address: str | None = None,
) -> TokenResponse:
"""
Refresh access token using refresh token
Implements token rotation with replay attack detection
"""
token_hash = hash_token(refresh_token)
stored_token = await RefreshTokenRepository.get_by_hash(
self.session,
token_hash
)
if stored_token is None:
raise TokenError(message = "Invalid refresh token")
if stored_token.is_revoked:
await RefreshTokenRepository.revoke_family(
self.session,
stored_token.family_id
)
raise TokenRevokedError()
if stored_token.is_expired:
raise TokenError(message = "Refresh token expired")
user = await UserRepository.get_by_id(
self.session,
stored_token.user_id
)
if user is None or not user.is_active:
raise TokenError(message = "User not found or inactive")
await RefreshTokenRepository.revoke_token(self.session, stored_token)
access_token = create_access_token(user.id, user.token_version)
_, new_hash, expires_at = create_refresh_token(
user.id, stored_token.family_id
)
await RefreshTokenRepository.create_token(
self.session,
user_id = user.id,
token_hash = new_hash,
family_id = stored_token.family_id,
expires_at = expires_at,
device_id = device_id,
device_name = device_name,
ip_address = ip_address,
)
return TokenResponse(access_token = access_token)
async def logout(
self,
refresh_token: str,
) -> None:
"""
Logout by revoking refresh token
Silently succeeds if token is already revoked or doesn't exist
"""
token_hash = hash_token(refresh_token)
stored_token = await RefreshTokenRepository.get_by_hash(
self.session,
token_hash
)
if stored_token and not stored_token.is_revoked:
await RefreshTokenRepository.revoke_token(
self.session,
stored_token
)
async def logout_all(
self,
user: User,
) -> int:
"""
Logout from all devices
Returns count of revoked sessions
"""
await UserRepository.increment_token_version(self.session, user)
return await RefreshTokenRepository.revoke_all_user_tokens(
self.session,
user.id
)