fix auth lifetime
parent
f8f5bf80c1
commit
d55e8d1df3
|
@ -1,6 +1,5 @@
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from sqlalchemy.ext.asyncio import (AsyncEngine, AsyncSession,
|
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker
|
||||||
async_sessionmaker)
|
|
||||||
|
|
||||||
from api.application.abstractions.uow import UnitOfWork
|
from api.application.abstractions.uow import UnitOfWork
|
||||||
from api.application.protocols.date_time import DateTimeProvider
|
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.auth_user import LoginUser
|
||||||
from api.application.usecase.auth.create_user import CreateUser
|
from api.application.usecase.auth.create_user import CreateUser
|
||||||
from api.domain.user.repository import UserRepository
|
from api.domain.user.repository import UserRepository
|
||||||
from api.infrastructure.dependencies.adapters import (create_engine,
|
from api.infrastructure.auth.jwt_settings import JwtSettings
|
||||||
create_session_maker,
|
from api.infrastructure.dependencies.adapters import (
|
||||||
new_session,
|
create_engine,
|
||||||
new_unit_of_work)
|
create_session_maker,
|
||||||
from api.infrastructure.dependencies.configs import app_settings
|
new_session,
|
||||||
from api.infrastructure.dependencies.protocols import (get_date_time_provider,
|
new_unit_of_work,
|
||||||
get_jwt_token_processor,
|
)
|
||||||
get_password_hasher,
|
from api.infrastructure.dependencies.configs import (
|
||||||
get_user_login)
|
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.repositories import get_user_repository
|
||||||
from api.infrastructure.dependencies.usecases import provide_create_user
|
from api.infrastructure.dependencies.usecases import provide_create_user
|
||||||
|
from api.infrastructure.persistence.db_setings import DBSettings
|
||||||
from api.infrastructure.settings import Settings
|
from api.infrastructure.settings import Settings
|
||||||
|
|
||||||
|
|
||||||
def init_dependencies(app: FastAPI) -> None:
|
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[Settings] = app_settings
|
||||||
|
|
||||||
app.dependency_overrides[AsyncEngine] = create_engine
|
app.dependency_overrides[AsyncEngine] = create_engine
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
|
@ -5,8 +5,15 @@ from fastapi import FastAPI
|
||||||
from sqlalchemy.ext.asyncio import AsyncEngine
|
from sqlalchemy.ext.asyncio import AsyncEngine
|
||||||
|
|
||||||
from api.app_builder.dependencies import init_dependencies
|
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.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.persistence.models import Base
|
||||||
from api.infrastructure.settings import Settings
|
from api.infrastructure.settings import Settings
|
||||||
|
|
||||||
|
@ -16,9 +23,16 @@ from .routers import init_routers
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI) -> AsyncGenerator:
|
async def lifespan(app: FastAPI) -> AsyncGenerator:
|
||||||
print("init lifespan")
|
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[Settings] = app_settings
|
||||||
app.dependency_overrides[AsyncEngine] = create_engine
|
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:
|
async with engine.begin() as conn:
|
||||||
await conn.run_sync(Base.metadata.drop_all)
|
await conn.run_sync(Base.metadata.drop_all)
|
||||||
|
@ -32,5 +46,6 @@ def app_factory() -> FastAPI:
|
||||||
)
|
)
|
||||||
init_dependencies(app)
|
init_dependencies(app)
|
||||||
init_routers(app)
|
init_routers(app)
|
||||||
|
init_exc_handlers(app)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
|
@ -9,3 +9,6 @@ class JwtTokenProcessor(Protocol):
|
||||||
|
|
||||||
def validate_token(self, token: str) -> UserId | None:
|
def validate_token(self, token: str) -> UserId | None:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def refresh_token(self, token: str) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
|
@ -15,16 +15,12 @@ class LoginUser:
|
||||||
self.hasher = password_hasher
|
self.hasher = password_hasher
|
||||||
|
|
||||||
async def __call__(self, request: LoginRequest) -> AuthenticationResponse:
|
async def __call__(self, request: LoginRequest) -> AuthenticationResponse:
|
||||||
print("__call__ request", request)
|
|
||||||
user = await self.user_repository.get_user(filter={"email": request.email})
|
user = await self.user_repository.get_user(filter={"email": request.email})
|
||||||
print("__call__ user from repo", user)
|
|
||||||
error = UserInvalidCredentialsError("Email or password is incorrect")
|
error = UserInvalidCredentialsError("Email or password is incorrect")
|
||||||
if user is None:
|
if user is None:
|
||||||
print("user is none in LoginUser __call__")
|
|
||||||
raise error
|
raise error
|
||||||
|
|
||||||
if not self.hasher.verify_password(request.password, user.hashed_password):
|
if not self.hasher.verify_password(request.password, user.hashed_password):
|
||||||
print("wrong pass in LoginUser __call__")
|
|
||||||
raise error
|
raise error
|
||||||
|
|
||||||
return AuthenticationResponse(
|
return AuthenticationResponse(
|
||||||
|
|
|
@ -17,7 +17,7 @@ class JoseJwtTokenProcessor(JwtTokenProcessor):
|
||||||
|
|
||||||
def generate_token(self, user_id: UserId) -> str:
|
def generate_token(self, user_id: UserId) -> str:
|
||||||
issued_at = self.date_time_provider.get_current_time()
|
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 = {
|
claims = {
|
||||||
"iat": issued_at,
|
"iat": issued_at,
|
||||||
|
@ -30,8 +30,10 @@ class JoseJwtTokenProcessor(JwtTokenProcessor):
|
||||||
def validate_token(self, token: str) -> UserId | None:
|
def validate_token(self, token: str) -> UserId | None:
|
||||||
try:
|
try:
|
||||||
payload = decode(token, self.jwt_options.secret, [self.jwt_options.algorithm])
|
payload = decode(token, self.jwt_options.secret, [self.jwt_options.algorithm])
|
||||||
|
|
||||||
return UserId(UUID(payload["sub"]))
|
return UserId(UUID(payload["sub"]))
|
||||||
|
|
||||||
except (JWTError, ValueError, KeyError):
|
except (JWTError, ValueError, KeyError):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def refresh_token(self, token: str) -> str:
|
||||||
|
return ""
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import os
|
import os
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
import yaml # type: ignore
|
import yaml # type: ignore
|
||||||
|
from fastapi import Depends
|
||||||
|
|
||||||
from api.infrastructure.auth.jwt_settings import JwtSettings
|
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.persistence.db_setings import DBSettings
|
||||||
from api.infrastructure.settings import Settings
|
from api.infrastructure.settings import Settings
|
||||||
|
|
||||||
|
@ -14,23 +17,36 @@ def yaml_loader(file: str) -> dict[str, dict[str, str]]:
|
||||||
return yaml_data
|
return yaml_data
|
||||||
|
|
||||||
|
|
||||||
@lru_cache
|
def get_db_settings() -> DBSettings:
|
||||||
def app_settings() -> Settings:
|
|
||||||
config_data = yaml_loader(
|
config_data = yaml_loader(
|
||||||
file=os.getenv("CONFIG_PATH", "./config/api_config.yml"),
|
file=os.getenv("CONFIG_PATH", "./config/api_config.yml"),
|
||||||
)
|
)
|
||||||
|
return DBSettings(
|
||||||
return Settings(
|
pg_user=config_data["db"]["user"],
|
||||||
db=DBSettings(
|
pg_pass=config_data["db"]["password"],
|
||||||
pg_user=config_data["db"]["user"],
|
pg_host=config_data["db"]["host"],
|
||||||
pg_pass=config_data["db"]["password"],
|
pg_port=int(config_data["db"]["port"]),
|
||||||
pg_host=config_data["db"]["host"],
|
pg_db=config_data["db"]["database"],
|
||||||
pg_port=int(config_data["db"]["port"]),
|
)
|
||||||
pg_db=config_data["db"]["database"],
|
|
||||||
),
|
|
||||||
jwt=JwtSettings(
|
def get_jwt_settings() -> JwtSettings:
|
||||||
secret=config_data["jwt"]["secret_key"],
|
config_data = yaml_loader(
|
||||||
expires_in=int(config_data["jwt"]["expires_in"]),
|
file=os.getenv("CONFIG_PATH", "./config/api_config.yml"),
|
||||||
algorithm=config_data["jwt"]["algorithm"],
|
)
|
||||||
),
|
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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -26,9 +26,7 @@ def get_jwt_token_processor(
|
||||||
settings: Annotated[Settings, Depends(Stub(Settings))],
|
settings: Annotated[Settings, Depends(Stub(Settings))],
|
||||||
date_time_provider: Annotated[DateTimeProvider, Depends(Stub(DateTimeProvider))],
|
date_time_provider: Annotated[DateTimeProvider, Depends(Stub(DateTimeProvider))],
|
||||||
) -> JwtTokenProcessor:
|
) -> JwtTokenProcessor:
|
||||||
return JoseJwtTokenProcessor(
|
return JoseJwtTokenProcessor(jwt_options=settings.jwt, date_time_provider=date_time_provider)
|
||||||
jwt_options=settings.jwt, date_time_provider=date_time_provider
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_user_login(
|
def get_user_login(
|
||||||
|
|
|
@ -28,9 +28,12 @@ class SqlAlchemyUserRepository(UserRepository):
|
||||||
async def get_user(self, filter: dict) -> User | None:
|
async def get_user(self, filter: dict) -> User | None:
|
||||||
stmt = text("""SELECT * FROM users WHERE email = :val""")
|
stmt = text("""SELECT * FROM users WHERE email = :val""")
|
||||||
result = await self.session.execute(stmt, {"val": filter["email"]})
|
result = await self.session.execute(stmt, {"val": filter["email"]})
|
||||||
if not result:
|
|
||||||
|
result = result.mappings().one_or_none()
|
||||||
|
|
||||||
|
if result is None:
|
||||||
return None
|
return None
|
||||||
result = result.mappings().one()
|
|
||||||
return User(
|
return User(
|
||||||
id=UserId(result.id),
|
id=UserId(result.id),
|
||||||
name=UserFirstName(result.name),
|
name=UserFirstName(result.name),
|
||||||
|
|
|
@ -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
|
|
@ -39,3 +39,13 @@ async def login(
|
||||||
response.set_cookie(key="access_token", value=f"Bearer {token}", httponly=True)
|
response.set_cookie(key="access_token", value=f"Bearer {token}", httponly=True)
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@auth_router.post("/logout")
|
||||||
|
async def logout(
|
||||||
|
response: Response,
|
||||||
|
):
|
||||||
|
|
||||||
|
response.delete_cookie(key="access_token", httponly=True)
|
||||||
|
|
||||||
|
return {"result": "logout"}
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
from api.application.contracts.user import UserResponse
|
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 = 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]:
|
async def get_all_users() -> list[UserResponse]:
|
||||||
return []
|
return []
|
||||||
|
|
Loading…
Reference in New Issue