Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
24867e8
fix(eval-overview): only show evaluator output metrics
mmabrouk Mar 3, 2026
35134d7
Merge branch 'main' into fix/evaluator-overview-output-metrics
mmabrouk Mar 5, 2026
b53cfc7
Merge branch 'main' into fix/evaluator-overview-output-metrics
mmabrouk Mar 5, 2026
7d4ca8c
[fix] Resolve `tunrstile` loopholes
jp-agenta Mar 10, 2026
12038b1
Initial fix
jp-agenta Mar 10, 2026
e984c3d
ruff format
jp-agenta Mar 10, 2026
9b62e52
fix double OTP check
jp-agenta Mar 10, 2026
a6c96ea
add comments
jp-agenta Mar 10, 2026
88108bf
fix api tests in oss
jp-agenta Mar 10, 2026
1fbbe8b
Merge branch 'main' into fix/turnstile-loopholes
jp-agenta Mar 10, 2026
6135d17
fix(sdk): invalidate secrets cache on invalid provider auth
mmabrouk Mar 11, 2026
c12c082
fix: cascade failure and cancellation states to downstream evaluators
ardaerzin Mar 11, 2026
26e1330
feat: invalidate vault secrets cache on API key failure
mmabrouk Mar 11, 2026
a86323d
Merge branch 'main' into fix/evaluator-overview-output-metrics
mmabrouk Mar 11, 2026
c07ee6a
Merge branch 'main' into fix/turnstile-loopholes
jp-agenta Mar 12, 2026
11c3a92
Merge branch 'main' into frontend-fix/age-3698-evaluators-keep-loadin…
mmabrouk Mar 12, 2026
b33a030
Clean up
jp-agenta Mar 12, 2026
72245f6
ruff format
jp-agenta Mar 12, 2026
fdd5ce4
Merge branch 'release/v0.94.1' into codex/implement-cache-invalidatio…
junaway Mar 12, 2026
6866352
fix: propagate root execution failure to downstream evaluator sessions
ardaerzin Mar 12, 2026
796778b
fix: only cascade failure/cancellation to downstream evaluators in fu…
ardaerzin Mar 12, 2026
5b0f2e9
v0.94.2
junaway Mar 12, 2026
c5d2f9c
Merge pull request #3947 from Agenta-AI/fix/turnstile-loopholes
bekossy Mar 12, 2026
d2b016a
Merge pull request #3960 from Agenta-AI/frontend-fix/age-3698-evaluat…
bekossy Mar 12, 2026
43bf65a
Merge pull request #3897 from Agenta-AI/fix/evaluator-overview-output…
bekossy Mar 12, 2026
1f01203
Merge pull request #3957 from Agenta-AI/codex/implement-cache-invalid…
bekossy Mar 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 187 additions & 13 deletions api/oss/src/core/auth/supertokens/overrides.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
)
from supertokens_python.recipe.passwordless.interfaces import (
RecipeInterface as PasswordlessRecipeInterface,
APIInterface as PasswordlessAPIInterface,
APIOptions as PasswordlessAPIOptions,
ConsumeCodeOkResult,
)
from supertokens_python.recipe.emailpassword.interfaces import (
Expand Down Expand Up @@ -56,7 +58,11 @@
from oss.src.core.auth.service import AuthService
from oss.src.dbs.postgres.users.dao import IdentitiesDAO
from oss.src.core.users.types import UserIdentityCreate
from oss.src.core.auth.turnstile import verify_turnstile_or_raise
from oss.src.core.auth.turnstile import (
get_client_ip,
has_turnstile_token,
verify_turnstile_or_raise,
)
from oss.src.services import db_manager

from oss.src.services.exceptions import UnauthorizedException
Expand Down Expand Up @@ -119,6 +125,35 @@ def _get_signup_cloud_region(cloud_url: str) -> str:
return "Other"


def _get_email_domain(email: Optional[str]) -> Optional[str]:
if not email or "@" not in email:
return None

domain = email.rsplit("@", 1)[-1].strip().lower()
return domain or None


def _log_auth_attempt(
*,
auth_method: str,
api_name: str,
api_options: Union[APIOptions, EmailPasswordAPIOptions, PasswordlessAPIOptions],
email: Optional[str] = None,
tenant_id: Optional[str] = None,
provider_id: Optional[str] = None,
) -> None:
log.info(
"[AUTH] Auth attempt auth_method=%s api_name=%s email_domain=%s client_ip=%s turnstile_token_present=%s tenant_id=%s provider_id=%s",
auth_method,
api_name,
_get_email_domain(email),
get_client_ip(api_options.request),
has_turnstile_token(api_options.request),
tenant_id,
provider_id,
)


def _merge_session_identities(
session: Optional[Any], method: Optional[str]
) -> List[str]:
Expand Down Expand Up @@ -363,11 +398,15 @@ async def verify_turnstile(
*,
api_options: EmailPasswordAPIOptions,
user_context: Dict[str, Any],
auth_flow: str,
) -> None:
if user_context.get("turnstile_verified"):
return

await verify_turnstile_or_raise(request=api_options.request)
await verify_turnstile_or_raise(
request=api_options.request,
auth_flow=auth_flow,
)
user_context["turnstile_verified"] = True

async def sign_in_post(
Expand All @@ -378,7 +417,23 @@ async def sign_in_post(
api_options: EmailPasswordAPIOptions,
user_context: Dict[str, Any],
):
await verify_turnstile(api_options=api_options, user_context=user_context)
email = (
form_fields[0].value
if form_fields and form_fields[0].id == "email"
else None
)
_log_auth_attempt(
auth_method="emailpassword",
api_name="sign_in_post",
api_options=api_options,
email=email,
tenant_id=tenant_id,
)
await verify_turnstile(
api_options=api_options,
user_context=user_context,
auth_flow="emailpassword_sign_in",
)

if form_fields[0].id == "email" and is_input_email(form_fields[0].value):
auth_info = await ensure_auth_info_not_blocked(
Expand Down Expand Up @@ -418,7 +473,23 @@ async def sign_up_post(
api_options: EmailPasswordAPIOptions,
user_context: Dict[str, Any],
):
await verify_turnstile(api_options=api_options, user_context=user_context)
email = (
form_fields[0].value
if form_fields and form_fields[0].id == "email"
else None
)
_log_auth_attempt(
auth_method="emailpassword",
api_name="sign_up_post",
api_options=api_options,
email=email,
tenant_id=tenant_id,
)
await verify_turnstile(
api_options=api_options,
user_context=user_context,
auth_flow="emailpassword_sign_up",
)

# FLOW 1: Sign in (redirect existing users with emailpassword credential)
auth_info = await ensure_auth_info_not_blocked(
Expand Down Expand Up @@ -469,22 +540,113 @@ async def sign_up_post(


def override_passwordless_apis(
original_implementation: PasswordlessRecipeInterface,
original_implementation: PasswordlessAPIInterface,
):
original_create_code_post = original_implementation.create_code_post
original_resend_code_post = original_implementation.resend_code_post
original_consume_code_post = original_implementation.consume_code_post

async def verify_turnstile(
*,
api_options: PasswordlessAPIOptions,
user_context: Dict[str, Any],
auth_flow: str,
) -> None:
if user_context.get("turnstile_verified"):
return

await verify_turnstile_or_raise(
request=api_options.request,
auth_flow=auth_flow,
)
user_context["turnstile_verified"] = True

async def create_code_post(
email: Union[str, None],
phone_number: Union[str, None],
session: Optional[SessionContainer],
should_try_linking_with_session_user: Union[bool, None],
tenant_id: str,
api_options: PasswordlessAPIOptions,
user_context: Dict[str, Any],
):
_log_auth_attempt(
auth_method="passwordless",
api_name="create_code_post",
api_options=api_options,
email=email,
tenant_id=tenant_id,
)
await verify_turnstile(
api_options=api_options,
user_context=user_context,
auth_flow="passwordless_create_code",
)

return await original_create_code_post(
email,
phone_number,
session,
should_try_linking_with_session_user,
tenant_id,
api_options,
user_context,
)

async def resend_code_post(
device_id: str,
pre_auth_session_id: str,
session: Optional[SessionContainer],
should_try_linking_with_session_user: Union[bool, None],
tenant_id: str,
api_options: PasswordlessAPIOptions,
user_context: Dict[str, Any],
):
_log_auth_attempt(
auth_method="passwordless",
api_name="resend_code_post",
api_options=api_options,
tenant_id=tenant_id,
)
# await verify_turnstile(
# api_options=api_options,
# user_context=user_context,
# auth_flow="passwordless_resend_code",
# )

return await original_resend_code_post(
device_id,
pre_auth_session_id,
session,
should_try_linking_with_session_user,
tenant_id,
api_options,
user_context,
)

async def consume_code_post(
pre_auth_session_id: str,
user_input_code: Union[str, None],
device_id: Union[str, None],
link_code: Union[str, None],
session: Optional[SessionContainer] = None,
should_try_linking_with_session_user: Optional[bool] = None,
tenant_id: str = "public",
api_options: Optional[APIOptions] = None,
user_context: Optional[Dict[str, Any]] = None,
session: Optional[SessionContainer],
should_try_linking_with_session_user: Optional[bool],
tenant_id: str,
api_options: PasswordlessAPIOptions,
user_context: Dict[str, Any],
):
# First we call the original implementation of consume_code_post.
_log_auth_attempt(
auth_method="passwordless",
api_name="consume_code_post",
api_options=api_options,
tenant_id=tenant_id,
)
# await verify_turnstile(
# api_options=api_options,
# user_context=user_context,
# auth_flow="passwordless_consume_code",
# )

response = await original_consume_code_post(
pre_auth_session_id,
user_input_code,
Expand All @@ -494,11 +656,13 @@ async def consume_code_post(
should_try_linking_with_session_user,
tenant_id,
api_options,
user_context or {},
user_context,
)

return response

original_implementation.create_code_post = create_code_post # type: ignore
original_implementation.resend_code_post = resend_code_post # type: ignore
original_implementation.consume_code_post = consume_code_post # type: ignore
return original_implementation

Expand All @@ -518,7 +682,17 @@ async def thirdparty_sign_in_up_post(
session: Optional[SessionContainer] = None,
should_try_linking_with_session_user: Optional[bool] = None,
):
await verify_turnstile_or_raise(request=api_options.request)
_log_auth_attempt(
auth_method="thirdparty",
api_name="sign_in_up_post",
api_options=api_options,
tenant_id=tenant_id,
provider_id=provider.id,
)
await verify_turnstile_or_raise(
request=api_options.request,
auth_flow="thirdparty_sign_in_up",
)

response = await original_sign_in_up(
provider,
Expand Down
Loading
Loading