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 <noreply@anthropic.com>

* fix: keep current user route available in stack auth

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Abhishek 2026-07-06 22:18:04 +05:30 committed by GitHub
parent 45afc47473
commit 4fb3193eb5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 91 additions and 3 deletions

View file

@ -5,7 +5,11 @@ from api.db import db_client
from api.db.models import UserModel from api.db.models import UserModel
from api.enums import OrganizationConfigurationKey, PostHogEvent from api.enums import OrganizationConfigurationKey, PostHogEvent
from api.schemas.auth import AuthResponse, LoginRequest, SignupRequest, UserResponse 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 ( from api.services.configuration.ai_model_configuration import (
convert_legacy_ai_model_configuration_to_v2, 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): async def signup(request: SignupRequest):
# Check if email is already taken # Check if email is already taken
existing_user = await db_client.get_user_by_email(request.email) 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): async def login(request: LoginRequest):
# Look up user by email # Look up user by email
user = await db_client.get_user_by_email(request.email) user = await db_client.get_user_by_email(request.email)

View file

@ -20,6 +20,19 @@ from api.services.posthog_client import (
) )
from api.utils.auth import decode_jwt_token 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_GROUP_TYPE = "organization"
POSTHOG_ORGANIZATION_USES_MPS_BILLING_V2_PROPERTY = "uses_mps_billing_v2" POSTHOG_ORGANIZATION_USES_MPS_BILLING_V2_PROPERTY = "uses_mps_billing_v2"

View file

@ -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",
}