From 4fb3193eb5be77900986e45fb78a2728da33bd8a Mon Sep 17 00:00:00 2001 From: Abhishek Date: Mon, 6 Jul 2026 22:18:04 +0530 Subject: [PATCH] fix: gate OSS email/password auth endpoints outside local auth mode (#500) * fix: gate OSS email/password auth endpoints outside local auth mode The /auth signup/login/me routes were mounted unconditionally, so the SaaS deployment accepted unauthenticated signups that created oss_* provider-id users (and auto-provisioned MPS service keys) bypassing Stack Auth entirely. Gate them with a router-level dependency that 404s when AUTH_PROVIDER is not "local", rather than conditionally mounting the router, so the OpenAPI spec and the clients generated from it stay identical across deployment modes. Co-Authored-By: Claude Fable 5 * fix: keep current user route available in stack auth --------- Co-authored-by: Claude Fable 5 --- api/routes/auth.py | 18 ++++++++-- api/services/auth/depends.py | 13 ++++++++ api/tests/test_auth_routes.py | 63 +++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 api/tests/test_auth_routes.py diff --git a/api/routes/auth.py b/api/routes/auth.py index 6083b875..c67978e0 100644 --- a/api/routes/auth.py +++ b/api/routes/auth.py @@ -5,7 +5,11 @@ from api.db import db_client from api.db.models import UserModel from api.enums import OrganizationConfigurationKey, PostHogEvent from api.schemas.auth import AuthResponse, LoginRequest, SignupRequest, UserResponse -from api.services.auth.depends import create_user_configuration_with_mps_key, get_user +from api.services.auth.depends import ( + create_user_configuration_with_mps_key, + get_user, + require_local_auth, +) from api.services.configuration.ai_model_configuration import ( convert_legacy_ai_model_configuration_to_v2, ) @@ -18,7 +22,11 @@ router = APIRouter( ) -@router.post("/signup", response_model=AuthResponse) +@router.post( + "/signup", + response_model=AuthResponse, + dependencies=[Depends(require_local_auth)], +) async def signup(request: SignupRequest): # Check if email is already taken existing_user = await db_client.get_user_by_email(request.email) @@ -85,7 +93,11 @@ async def signup(request: SignupRequest): ) -@router.post("/login", response_model=AuthResponse) +@router.post( + "/login", + response_model=AuthResponse, + dependencies=[Depends(require_local_auth)], +) async def login(request: LoginRequest): # Look up user by email user = await db_client.get_user_by_email(request.email) diff --git a/api/services/auth/depends.py b/api/services/auth/depends.py index 4225b6f0..488d45a4 100644 --- a/api/services/auth/depends.py +++ b/api/services/auth/depends.py @@ -20,6 +20,19 @@ from api.services.posthog_client import ( ) from api.utils.auth import decode_jwt_token + +async def require_local_auth() -> None: + """Reject email/password auth requests outside OSS (local) deployments. + + The auth router stays mounted in every mode so the OpenAPI spec — and the + clients generated from it — don't vary with AUTH_PROVIDER; the gate has to + happen at request time. Without it, the SaaS deployment accepts + unauthenticated signups that mint oss_* users bypassing Stack Auth. + """ + if AUTH_PROVIDER != "local": + raise HTTPException(status_code=404, detail="Not found") + + POSTHOG_ORGANIZATION_GROUP_TYPE = "organization" POSTHOG_ORGANIZATION_USES_MPS_BILLING_V2_PROPERTY = "uses_mps_billing_v2" diff --git a/api/tests/test_auth_routes.py b/api/tests/test_auth_routes.py new file mode 100644 index 00000000..143266b1 --- /dev/null +++ b/api/tests/test_auth_routes.py @@ -0,0 +1,63 @@ +from types import SimpleNamespace + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from api.routes.auth import router +from api.services.auth import depends as auth_depends +from api.services.auth.depends import get_user + + +def _make_test_app() -> FastAPI: + app = FastAPI() + app.include_router(router) + return app + + +def test_stack_mode_hides_email_password_auth_routes(monkeypatch): + monkeypatch.setattr(auth_depends, "AUTH_PROVIDER", "stack") + client = TestClient(_make_test_app()) + + signup_response = client.post( + "/auth/signup", + json={ + "email": "user@example.com", + "password": "password123", + "name": "User", + }, + ) + login_response = client.post( + "/auth/login", + json={ + "email": "user@example.com", + "password": "password123", + }, + ) + + assert signup_response.status_code == 404 + assert signup_response.json() == {"detail": "Not found"} + assert login_response.status_code == 404 + assert login_response.json() == {"detail": "Not found"} + + +def test_stack_mode_keeps_current_user_route_available(monkeypatch): + monkeypatch.setattr(auth_depends, "AUTH_PROVIDER", "stack") + app = _make_test_app() + app.dependency_overrides[get_user] = lambda: SimpleNamespace( + id=7, + email="user@example.com", + selected_organization_id=42, + provider_id="stack-user-1", + ) + client = TestClient(app) + + response = client.get("/auth/me") + + assert response.status_code == 200 + assert response.json() == { + "id": 7, + "email": "user@example.com", + "name": None, + "organization_id": 42, + "provider_id": "stack-user-1", + }