serve: add JWT consumer shim for bincio-auth integration

When --jwt-secret / BINCIO_AUTH_JWT_SECRET is set, auth is validated
locally by decoding the bincio-auth-issued JWT — no DB session lookup.
Falls back to existing DB-based session lookup when the flag is absent,
so standalone deployments keep working without any config change.

Changes:
- deps.py: add jwt_secret global, _decode_jwt helper, wire into
  _current_user and _require_auth
- cli.py: add --jwt-secret option; log active auth mode on startup
- pyproject.toml: add PyJWT>=2.8 to serve and dev extras
This commit is contained in:
Davide Scaini
2026-06-02 14:54:43 +02:00
parent 0d6bf57932
commit 2af29a460b
3 changed files with 48 additions and 20 deletions
+30 -11
View File
@@ -12,20 +12,16 @@ import re
import threading
import time
from pathlib import Path
from typing import Optional
import jwt as _jwt
from fastapi import Cookie, HTTPException, Request, Response
from bincio.edit.ops import VALID_ACTIVITY_ID as _VALID_ACTIVITY_ID
from bincio.serve.db import (
User,
authenticate,
create_session,
delete_session,
get_session,
get_user,
open_db,
)
from bincio.edit.ops import VALID_ACTIVITY_ID as _VALID_ACTIVITY_ID
from bincio.shared.images import ALLOWED_IMAGE_TYPES as _ALLOWED_IMAGE_TYPES # noqa: F401
from bincio.shared.images import MAX_IMAGE_BYTES as _MAX_IMAGE_BYTES # noqa: F401
from bincio.shared.images import unique_image_name as _unique_image_name # noqa: F401
@@ -40,6 +36,7 @@ strava_client_secret: str = ""
public_url: str = ""
dem_url: str = "https://api.open-elevation.com"
sync_secret: str = ""
jwt_secret: str = "" # when set, validates JWTs from bincio-auth instead of DB session lookup
_db = None
_strava_sync_running = False
_strava_sync_lock = threading.Lock()
@@ -118,20 +115,42 @@ def _check_rate_limit(
# ── Auth dependency functions ─────────────────────────────────────────────────
def _current_user(bincio_session: Optional[str] = Cookie(default=None)) -> Optional[User]:
def _decode_jwt(token: str) -> User | None:
"""Decode a bincio-auth JWT and return a User. Returns None on any failure."""
try:
payload = _jwt.decode(token, jwt_secret, algorithms=["HS256"])
except _jwt.PyJWTError:
return None
handle = payload.get("sub")
if not handle:
return None
return User(
handle=handle,
display_name=payload.get("display_name", ""),
is_admin=bool(payload.get("is_admin", False)),
wiki_access=bool(payload.get("wiki_access", True)),
activity_access=bool(payload.get("activity_access", False)),
suspended=False,
created_at=0,
)
def _current_user(bincio_session: str | None = Cookie(default=None)) -> User | None:
if not bincio_session:
return None
if jwt_secret:
return _decode_jwt(bincio_session)
return get_session(_get_db(), bincio_session)
def _require_user(bincio_session: Optional[str] = Cookie(default=None)) -> User:
def _require_user(bincio_session: str | None = Cookie(default=None)) -> User:
user = _current_user(bincio_session)
if not user:
raise HTTPException(401, "Not authenticated")
return user
def _require_admin(bincio_session: Optional[str] = Cookie(default=None)) -> User:
def _require_admin(bincio_session: str | None = Cookie(default=None)) -> User:
user = _require_user(bincio_session)
if not user.is_admin:
raise HTTPException(403, "Admin required")
@@ -140,7 +159,7 @@ def _require_admin(bincio_session: Optional[str] = Cookie(default=None)) -> User
def _require_auth(
request: Request,
bincio_session: Optional[str] = Cookie(default=None),
bincio_session: str | None = Cookie(default=None),
) -> User:
"""Accept session cookie (web) OR Authorization: Bearer token (mobile)."""
token = bincio_session
@@ -150,7 +169,7 @@ def _require_auth(
token = auth[7:]
if not token:
raise HTTPException(401, "Not authenticated")
user = get_session(_get_db(), token)
user = _decode_jwt(token) if jwt_secret else get_session(_get_db(), token)
if not user:
raise HTTPException(401, "Invalid or expired session")
return user