fix(auth): protect setup and expire sessions
This commit is contained in:
@@ -2,7 +2,7 @@ import asyncio
|
||||
import base64
|
||||
import hashlib
|
||||
from collections.abc import AsyncIterator
|
||||
from datetime import UTC, datetime
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any, cast
|
||||
|
||||
@@ -57,6 +57,7 @@ def problem_response(request: Request, status: int, code: str, detail: str) -> J
|
||||
class SetupInput(BaseModel):
|
||||
username: str = Field(min_length=1, max_length=255)
|
||||
password: str = Field(min_length=12, max_length=1024)
|
||||
bootstrap_secret: str | None = Field(default=None, max_length=1024)
|
||||
|
||||
|
||||
class LoginInput(BaseModel):
|
||||
@@ -127,6 +128,7 @@ def create_app(settings: Settings) -> FastAPI:
|
||||
app.state.sessions = async_sessionmaker(app.state.engine, expire_on_commit=False)
|
||||
app.state.cipher = EnvelopeCipher.from_file(settings.master_key_file)
|
||||
app.state.setup_lock = asyncio.Lock()
|
||||
app.state.revoked_sessions = set()
|
||||
|
||||
@app.middleware("http")
|
||||
async def request_id_middleware(request: Request, call_next: Any) -> Response:
|
||||
@@ -167,7 +169,7 @@ def create_app(settings: Settings) -> FastAPI:
|
||||
return user, set(token.scopes), False
|
||||
encoded = request.cookies.get("backup_tool_session")
|
||||
data = verify_session(encoded, settings.master_key_file) if encoded else None
|
||||
if data is None:
|
||||
if data is None or data["sid"] in app.state.revoked_sessions:
|
||||
raise Problem(401, "authentication_required", "Authentication is required.")
|
||||
user = await db.get(User, data["sub"])
|
||||
if user is None or user.state != "active":
|
||||
@@ -213,16 +215,29 @@ def create_app(settings: Settings) -> FastAPI:
|
||||
|
||||
def set_session(response: Response, user_id: str) -> None:
|
||||
csrf = new_csrf_token()
|
||||
ttl = settings.session_ttl_seconds
|
||||
response.set_cookie(
|
||||
"backup_tool_session",
|
||||
sign_session(user_id, csrf, settings.master_key_file),
|
||||
sign_session(
|
||||
user_id,
|
||||
csrf,
|
||||
settings.master_key_file,
|
||||
expires_at=datetime.now(UTC) + timedelta(seconds=ttl),
|
||||
),
|
||||
httponly=True,
|
||||
secure=True,
|
||||
samesite="strict",
|
||||
path="/",
|
||||
max_age=ttl,
|
||||
)
|
||||
response.set_cookie(
|
||||
"backup_tool_csrf", csrf, httponly=False, secure=True, samesite="strict", path="/"
|
||||
"backup_tool_csrf",
|
||||
csrf,
|
||||
httponly=False,
|
||||
secure=True,
|
||||
samesite="strict",
|
||||
path="/",
|
||||
max_age=ttl,
|
||||
)
|
||||
|
||||
@app.get("/livez")
|
||||
@@ -247,6 +262,11 @@ def create_app(settings: Settings) -> FastAPI:
|
||||
db: Annotated[AsyncSession, Depends(session)],
|
||||
) -> dict[str, str]:
|
||||
async with app.state.setup_lock:
|
||||
if (
|
||||
settings.bootstrap_secret is not None
|
||||
and input_.bootstrap_secret != settings.bootstrap_secret
|
||||
):
|
||||
raise Problem(403, "bootstrap_required", "Bootstrap credentials are required.")
|
||||
if await db.scalar(select(User.id).limit(1)) is not None:
|
||||
raise Problem(409, "setup_complete", "Initial administrator already exists.")
|
||||
user = User(username=input_.username, password_hash=hash_password(input_.password))
|
||||
@@ -286,8 +306,14 @@ def create_app(settings: Settings) -> FastAPI:
|
||||
|
||||
@app.post("/api/v2/auth/logout", status_code=204)
|
||||
async def logout(
|
||||
response: Response, _: Annotated[tuple[User, set[str], bool], Depends(require)]
|
||||
request: Request,
|
||||
response: Response,
|
||||
_: Annotated[tuple[User, set[str], bool], Depends(require)],
|
||||
) -> None:
|
||||
encoded = request.cookies.get("backup_tool_session")
|
||||
data = verify_session(encoded, settings.master_key_file) if encoded else None
|
||||
if data is not None:
|
||||
app.state.revoked_sessions.add(data["sid"])
|
||||
response.delete_cookie("backup_tool_session", path="/")
|
||||
response.delete_cookie("backup_tool_csrf", path="/")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user