diff --git a/.coverage b/.coverage index d27ba23..4f48a44 100644 Binary files a/.coverage and b/.coverage differ diff --git a/src/intervalsicu_mcp_ui/app.py b/src/intervalsicu_mcp_ui/app.py index 2302cbd..0e9c0f6 100644 --- a/src/intervalsicu_mcp_ui/app.py +++ b/src/intervalsicu_mcp_ui/app.py @@ -36,11 +36,17 @@ def create_app(config: Config | None = None) -> FastAPI: oauth = OAuth() oauth.register( - name="authentik", + name="oidc", server_metadata_url=f"{cfg.oidc_issuer}.well-known/openid-configuration", client_id=cfg.oidc_client_id, - client_secret=cfg.oidc_client_secret, - client_kwargs={"scope": cfg.oidc_scopes}, + # Public client: no secret, PKCE instead (Better Auth hashes confidential + # secrets, so the portal is registered as a public + PKCE client). + client_secret=cfg.oidc_client_secret or None, + client_kwargs={ + "scope": cfg.oidc_scopes, + "code_challenge_method": "S256", + "token_endpoint_auth_method": "none", + }, ) app = FastAPI(title="Intervals.icu MCP portal", root_path=cfg.root_path) @@ -69,25 +75,25 @@ def create_app(config: Config | None = None) -> FastAPI: @app.get("/auth/login") async def auth_login(request: Request): # pragma: no cover - OIDC redirect (integration) - return await oauth.authentik.authorize_redirect(request, cfg.oidc_redirect_url) + return await oauth.oidc.authorize_redirect(request, cfg.oidc_redirect_url) @app.get("/auth/callback") async def auth_callback(request: Request): # pragma: no cover - OIDC callback (integration) - token = await oauth.authentik.authorize_access_token(request) + token = await oauth.oidc.authorize_access_token(request) claims = dict(token.get("userinfo") or {}) sub = claims.get("sub") if not sub: return redirect("/login") email = claims.get("email", "") name = claims.get("name") - groups = claims.get("groups") or [] async with db.sessionmaker()() as session: await db.upsert_login(session, sub, email, name) request.session["user"] = { "sub": sub, "email": email, "name": name, - "is_admin": cfg.admin_group in groups, + # Admin is an email allowlist now (Better Auth has no group claims). + "is_admin": bool(email) and email.lower() in cfg.admin_emails, } return redirect("/account") diff --git a/src/intervalsicu_mcp_ui/config.py b/src/intervalsicu_mcp_ui/config.py index dd699c8..593d092 100644 --- a/src/intervalsicu_mcp_ui/config.py +++ b/src/intervalsicu_mcp_ui/config.py @@ -9,12 +9,12 @@ from dataclasses import dataclass @dataclass(frozen=True) class Config: database_url: str - oidc_issuer: str # Authentik application base, e.g. https://auth.farh.net/application/o// + oidc_issuer: str # OIDC provider base, e.g. https://intervalsicu.farhoodlabs.com/api/auth/ oidc_client_id: str - oidc_client_secret: str - oidc_redirect_url: str # https:///auth/callback + oidc_client_secret: str # empty for a public + PKCE client + oidc_redirect_url: str # https:///portal/auth/callback session_secret: str - admin_group: str + admin_emails: frozenset[str] # lower-cased allowlist; admin has no group claim intervals_api_base: str oidc_scopes: str root_path: str # public path prefix when served behind a gateway (e.g. "/portal") @@ -26,12 +26,14 @@ def load_config() -> Config: database_url=os.environ["DATABASE_URL"], oidc_issuer=os.environ["OIDC_ISSUER"].rstrip("/") + "/", oidc_client_id=os.environ["OIDC_CLIENT_ID"], - oidc_client_secret=os.environ["OIDC_CLIENT_SECRET"], + oidc_client_secret=os.environ.get("OIDC_CLIENT_SECRET", ""), oidc_redirect_url=os.environ["OIDC_REDIRECT_URL"], session_secret=os.environ["SESSION_SECRET"], - admin_group=os.environ.get("ADMIN_GROUP", "intervalsicu-mcp-admins"), + admin_emails=frozenset( + e.strip().lower() for e in os.environ.get("ADMIN_EMAILS", "").split(",") if e.strip() + ), intervals_api_base=os.environ.get("INTERVALS_API_BASE_URL", "https://intervals.icu/api/v1"), - oidc_scopes=os.environ.get("OIDC_SCOPES", "openid email profile groups"), + oidc_scopes=os.environ.get("OIDC_SCOPES", "openid email profile"), root_path=os.environ.get("ROOT_PATH", "").rstrip("/"), mcp_url=os.environ.get("MCP_URL", "https://intervalsicu.farhoodlabs.com/mcp"), ) diff --git a/tests/test_app.py b/tests/test_app.py index 776e76e..3d54087 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -38,9 +38,9 @@ def app_url(tmp_path, monkeypatch): oidc_client_secret="sec", oidc_redirect_url="https://ui.example/auth/callback", session_secret="x" * 32, - admin_group="intervalsicu-mcp-admins", + admin_emails=frozenset({"admin@x.com"}), intervals_api_base="https://intervals.icu/api/v1", - oidc_scopes="openid email profile groups", + oidc_scopes="openid email profile", root_path="", mcp_url="https://intervalsicu.farhoodlabs.com/mcp", )