fix auth lifetime

main
Сергей Ванюшкин 2024-04-07 21:31:15 +00:00
parent f8f5bf80c1
commit d55e8d1df3
13 changed files with 212 additions and 42 deletions

View File

@ -1,6 +1,5 @@
from fastapi import FastAPI
from sqlalchemy.ext.asyncio import (AsyncEngine, AsyncSession,
async_sessionmaker)
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker
from api.application.abstractions.uow import UnitOfWork
from api.application.protocols.date_time import DateTimeProvider
@ -9,21 +8,33 @@ from api.application.protocols.password_hasher import PasswordHasher
from api.application.usecase.auth.auth_user import LoginUser
from api.application.usecase.auth.create_user import CreateUser
from api.domain.user.repository import UserRepository
from api.infrastructure.dependencies.adapters import (create_engine,
create_session_maker,
new_session,
new_unit_of_work)
from api.infrastructure.dependencies.configs import app_settings
from api.infrastructure.dependencies.protocols import (get_date_time_provider,
get_jwt_token_processor,
get_password_hasher,
get_user_login)
from api.infrastructure.auth.jwt_settings import JwtSettings
from api.infrastructure.dependencies.adapters import (
create_engine,
create_session_maker,
new_session,
new_unit_of_work,
)
from api.infrastructure.dependencies.configs import (
app_settings,
get_db_settings,
get_jwt_settings,
)
from api.infrastructure.dependencies.protocols import (
get_date_time_provider,
get_jwt_token_processor,
get_password_hasher,
get_user_login,
)
from api.infrastructure.dependencies.repositories import get_user_repository
from api.infrastructure.dependencies.usecases import provide_create_user
from api.infrastructure.persistence.db_setings import DBSettings
from api.infrastructure.settings import Settings
def init_dependencies(app: FastAPI) -> None:
app.dependency_overrides[DBSettings] = get_db_settings
app.dependency_overrides[JwtSettings] = get_jwt_settings
app.dependency_overrides[Settings] = app_settings
app.dependency_overrides[AsyncEngine] = create_engine

View File

@ -0,0 +1,51 @@
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from api.domain.error import DomainValidationError
from api.domain.user.error import (UserAlreadyExistsError,
UserInvalidCredentialsError,
UserIsNotAuthorizedError)
async def validation_error_exc_handler(
request: Request, exc: DomainValidationError
) -> JSONResponse:
return JSONResponse(status_code=400, content={"detail": exc.message})
async def user_authentication_error_exc_handler(
request: Request, exc: UserIsNotAuthorizedError
) -> JSONResponse:
return JSONResponse(
status_code=401,
content={"detail": exc.message},
headers={"WWW-Authenticate": "Bearer"},
)
async def user_already_exist_error_exc_handler(
request: Request, exc: UserAlreadyExistsError
) -> JSONResponse:
return JSONResponse(status_code=409, content={"detail": exc.message})
async def user_invalid_credentials_error_exc_handler(
request: Request, exc: UserInvalidCredentialsError
) -> JSONResponse:
return JSONResponse(status_code=401, content={"detail": exc.message})
def init_exc_handlers(app: FastAPI) -> None:
app.add_exception_handler(
DomainValidationError,
validation_error_exc_handler,
)
app.add_exception_handler(
UserIsNotAuthorizedError, user_authentication_error_exc_handler
)
app.add_exception_handler(
UserAlreadyExistsError, user_already_exist_error_exc_handler
)
app.add_exception_handler(
UserInvalidCredentialsError, user_invalid_credentials_error_exc_handler
)

View File

@ -5,8 +5,15 @@ from fastapi import FastAPI
from sqlalchemy.ext.asyncio import AsyncEngine
from api.app_builder.dependencies import init_dependencies
from api.app_builder.error_handlers import init_exc_handlers
from api.infrastructure.auth.jwt_settings import JwtSettings
from api.infrastructure.dependencies.adapters import create_engine
from api.infrastructure.dependencies.configs import app_settings
from api.infrastructure.dependencies.configs import (
app_settings,
get_db_settings,
get_jwt_settings,
)
from api.infrastructure.persistence.db_setings import DBSettings
from api.infrastructure.persistence.models import Base
from api.infrastructure.settings import Settings
@ -16,9 +23,16 @@ from .routers import init_routers
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator:
print("init lifespan")
app.dependency_overrides[DBSettings] = get_db_settings
app.dependency_overrides[JwtSettings] = get_jwt_settings
app.dependency_overrides[Settings] = app_settings
app.dependency_overrides[AsyncEngine] = create_engine
engine = app.dependency_overrides[AsyncEngine](app.dependency_overrides[Settings]())
engine = app.dependency_overrides[AsyncEngine](
app.dependency_overrides[Settings](
app.dependency_overrides[DBSettings](),
app.dependency_overrides[JwtSettings](),
),
)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
@ -32,5 +46,6 @@ def app_factory() -> FastAPI:
)
init_dependencies(app)
init_routers(app)
init_exc_handlers(app)
return app

View File

@ -9,3 +9,6 @@ class JwtTokenProcessor(Protocol):
def validate_token(self, token: str) -> UserId | None:
raise NotImplementedError
def refresh_token(self, token: str) -> str:
raise NotImplementedError

View File

@ -15,16 +15,12 @@ class LoginUser:
self.hasher = password_hasher
async def __call__(self, request: LoginRequest) -> AuthenticationResponse:
print("__call__ request", request)
user = await self.user_repository.get_user(filter={"email": request.email})
print("__call__ user from repo", user)
error = UserInvalidCredentialsError("Email or password is incorrect")
if user is None:
print("user is none in LoginUser __call__")
raise error
if not self.hasher.verify_password(request.password, user.hashed_password):
print("wrong pass in LoginUser __call__")
raise error
return AuthenticationResponse(

View File

@ -17,7 +17,7 @@ class JoseJwtTokenProcessor(JwtTokenProcessor):
def generate_token(self, user_id: UserId) -> str:
issued_at = self.date_time_provider.get_current_time()
expiration_time = issued_at + timedelta(hours=self.jwt_options.expires_in)
expiration_time = issued_at + timedelta(minutes=self.jwt_options.expires_in)
claims = {
"iat": issued_at,
@ -30,8 +30,10 @@ class JoseJwtTokenProcessor(JwtTokenProcessor):
def validate_token(self, token: str) -> UserId | None:
try:
payload = decode(token, self.jwt_options.secret, [self.jwt_options.algorithm])
return UserId(UUID(payload["sub"]))
except (JWTError, ValueError, KeyError):
return None
def refresh_token(self, token: str) -> str:
return ""

View File

@ -1,9 +1,12 @@
import os
from functools import lru_cache
from typing import Annotated
import yaml # type: ignore
from fastapi import Depends
from api.infrastructure.auth.jwt_settings import JwtSettings
from api.infrastructure.dependencies.stub import Stub
from api.infrastructure.persistence.db_setings import DBSettings
from api.infrastructure.settings import Settings
@ -14,23 +17,36 @@ def yaml_loader(file: str) -> dict[str, dict[str, str]]:
return yaml_data
@lru_cache
def app_settings() -> Settings:
def get_db_settings() -> DBSettings:
config_data = yaml_loader(
file=os.getenv("CONFIG_PATH", "./config/api_config.yml"),
)
return Settings(
db=DBSettings(
pg_user=config_data["db"]["user"],
pg_pass=config_data["db"]["password"],
pg_host=config_data["db"]["host"],
pg_port=int(config_data["db"]["port"]),
pg_db=config_data["db"]["database"],
),
jwt=JwtSettings(
secret=config_data["jwt"]["secret_key"],
expires_in=int(config_data["jwt"]["expires_in"]),
algorithm=config_data["jwt"]["algorithm"],
),
return DBSettings(
pg_user=config_data["db"]["user"],
pg_pass=config_data["db"]["password"],
pg_host=config_data["db"]["host"],
pg_port=int(config_data["db"]["port"]),
pg_db=config_data["db"]["database"],
)
def get_jwt_settings() -> JwtSettings:
config_data = yaml_loader(
file=os.getenv("CONFIG_PATH", "./config/api_config.yml"),
)
return JwtSettings(
secret=config_data["jwt"]["secret_key"],
expires_in=int(config_data["jwt"]["expires_in"]),
algorithm=config_data["jwt"]["algorithm"],
)
@lru_cache
def app_settings(
db_conf: Annotated[DBSettings, Depends(Stub(DBSettings))],
jwt_conf: Annotated[JwtSettings, Depends(Stub(JwtSettings))],
) -> Settings:
return Settings(
db=db_conf,
jwt=jwt_conf,
)

View File

@ -26,9 +26,7 @@ def get_jwt_token_processor(
settings: Annotated[Settings, Depends(Stub(Settings))],
date_time_provider: Annotated[DateTimeProvider, Depends(Stub(DateTimeProvider))],
) -> JwtTokenProcessor:
return JoseJwtTokenProcessor(
jwt_options=settings.jwt, date_time_provider=date_time_provider
)
return JoseJwtTokenProcessor(jwt_options=settings.jwt, date_time_provider=date_time_provider)
def get_user_login(

View File

@ -28,9 +28,12 @@ class SqlAlchemyUserRepository(UserRepository):
async def get_user(self, filter: dict) -> User | None:
stmt = text("""SELECT * FROM users WHERE email = :val""")
result = await self.session.execute(stmt, {"val": filter["email"]})
if not result:
result = result.mappings().one_or_none()
if result is None:
return None
result = result.mappings().one()
return User(
id=UserId(result.id),
name=UserFirstName(result.name),

View File

View File

@ -0,0 +1,60 @@
from typing import Annotated
from fastapi import Depends, HTTPException, Request, Response, status
from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel
from fastapi.security import OAuth2
from fastapi.security.utils import get_authorization_scheme_param
from api.application.protocols.jwt import JwtTokenProcessor
from api.domain.user.error import UserIsNotAuthorizedError
from api.infrastructure.dependencies.stub import Stub
class OAuth2PasswordBearerWithCookie(OAuth2):
def __init__(
self,
tokenUrl: str,
scheme_name: str | None = None,
scopes: dict[str, str] | None = None,
auto_error: bool = True,
):
if not scopes:
scopes = {}
flows = OAuthFlowsModel(password={"tokenUrl": tokenUrl, "scopes": scopes})
super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error)
async def __call__(self, request: Request) -> str | None:
authorization: str | None = request.cookies.get("access_token")
scheme, param = get_authorization_scheme_param(authorization)
if authorization is None or scheme.lower() != "bearer":
if self.auto_error:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
else:
return None
print(param)
return param
oauth2_scheme = OAuth2PasswordBearerWithCookie("/auth/login")
async def auth_required(
request: Request,
token: Annotated[
str,
Depends(oauth2_scheme),
],
jwt_processor: Annotated[JwtTokenProcessor, Depends(Stub(JwtTokenProcessor))],
) -> None:
if token is None:
raise UserIsNotAuthorizedError("Invalid authorization credentials")
if jwt_processor.validate_token(token=token) is None:
raise UserIsNotAuthorizedError("authorization credentials is old")
request.scope["auth"] = token

View File

@ -39,3 +39,13 @@ async def login(
response.set_cookie(key="access_token", value=f"Bearer {token}", httponly=True)
return user
@auth_router.post("/logout")
async def logout(
response: Response,
):
response.delete_cookie(key="access_token", httponly=True)
return {"result": "logout"}

View File

@ -1,10 +1,15 @@
from fastapi import APIRouter
from fastapi import APIRouter, Depends
from api.application.contracts.user import UserResponse
from api.presentation.auth.fasapi_auth import auth_required
user_router = APIRouter(prefix="/users", tags=["Users"])
@user_router.get("/")
@user_router.get(
"/",
response_model=list[UserResponse],
dependencies=[Depends(auth_required)],
)
async def get_all_users() -> list[UserResponse]:
return []