Skip to content

fix(auth): gate cache deny on non-null user_id to prevent cross-user pollution#5389

Open
truecallerabreham wants to merge 2 commits into
Agenta-AI:mainfrom
truecallerabreham:fix/cache-deny-null-user-id
Open

fix(auth): gate cache deny on non-null user_id to prevent cross-user pollution#5389
truecallerabreham wants to merge 2 commits into
Agenta-AI:mainfrom
truecallerabreham:fix/cache-deny-null-user-id

Conversation

@truecallerabreham

@truecallerabreham truecallerabreham commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Summary

When user_id is None (anonymous/unauthenticated requests), the outer exception handlers in �erify_bearer_token call set_cache with a deny value using a generic cache key (empty user segment). Since all anonymous users share the same cache key, one user's auth failure can cause all subsequent anonymous requests to also fail auth for up to 5 minutes.

This fix adds a user_id is not None guard before writing deny entries to the cache, so anonymous failures are not cached globally.

Changes

  • �pi/oss/src/middlewares/auth.py: Added if user_id is not None: guards around set_cache calls in both the UnauthorizedException handler (line 770) and the generic Exception handler (line 788). The _deny logging call is preserved unconditionally so failures are still recorded.
  • �pi/oss/tests/pytest/unit/middlewares/test_cache_deny.py: Added regression tests verifying that deny entries are only cached when user_id is known, and not cached for anonymous users.

Demo

Before fix - set_cache called unconditionally with user_id=None:

python except UnauthorizedException as exc: _deny("catch_unauthorized", "UnauthorizedException propagated", exc) await set_cache( project_id=query_project_id, user_id=user_id, # None for anonymous users! namespace="verify_bearer_token", key=cache_key, value={"deny": True}, ) raise exc

After fix - set_cache gated on user_id is not None:

python except UnauthorizedException as exc: _deny("catch_unauthorized", "UnauthorizedException propagated", exc) if user_id is not None: # Guard added await set_cache( project_id=query_project_id, user_id=user_id, namespace="verify_bearer_token", key=cache_key, value={"deny": True}, ) raise exc

Code diff showing the fix

Impact

  • High availability fix: Prevents a single anonymous auth failure from blocking all anonymous users for the cache TTL (5 minutes)
  • No behavioral change for authenticated users: When user_id is set, the cache deny behavior is unchanged
  • Backend-only change: No UI or frontend impact

Testing

  • est_cache_deny_not_written_when_user_id_is_none - verifies set_cache is NOT called with deny when user_id is None
  • est_cache_deny_written_when_user_id_is_set - verifies set_cache IS called with deny when user_id is set

Related

…pollution

When user_id is None (anonymous/unauthenticated requests), the outer
exception handlers in auth_user call set_cache with a deny value using
a generic cache key (empty user segment). Since all anonymous users
share the same cache key, one user's auth failure can cause all
subsequent anonymous requests to also fail auth for up to 5 minutes.

This fix adds a user_id is not None guard before writing deny entries
to the cache, so anonymous failures are not cached globally.

Includes regression tests verifying that deny entries are only cached
when user_id is known.

Closes Agenta-AI#5387
@dosubot dosubot Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Jul 18, 2026
@vercel

vercel Bot commented Jul 18, 2026

Copy link
Copy Markdown

@truecallerabreham is attempting to deploy a commit to the agenta projects Team on Vercel.

A member of the Team first needs to authorize it.

@dosubot dosubot Bot added the Backend label Jul 18, 2026
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@dosubot dosubot Bot added the Bug Report Something isn't working label Jul 18, 2026
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0a24efe7-c2f2-4388-9868-96b1ab525b45

📥 Commits

Reviewing files that changed from the base of the PR and between c6015a1 and 3837646.

📒 Files selected for processing (1)
  • api/oss/tests/pytest/unit/middlewares/test_cache_deny.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/oss/tests/pytest/unit/middlewares/test_cache_deny.py

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Prevented failed authentication attempts without a valid user ID from creating shared deny-cache entries.
    • Kept caching authentication failures when a specific user ID is available.
  • Tests
    • Added new tests to validate cache behavior for both anonymous (no user ID) and user-specific authentication failures.

Walkthrough

The auth middleware now skips deny-cache writes when no user ID is available. Unit tests cover anonymous sessions and confirm identified sessions still receive deny entries.

Changes

Auth cache scoping

Layer / File(s) Summary
Gate deny-cache writes
api/oss/src/middlewares/auth.py
verify_bearer_token writes deny entries only when user_id is not None in both exception paths.
Validate deny-cache behavior
api/oss/tests/pytest/unit/middlewares/test_cache_deny.py
Tests confirm deny entries are skipped for anonymous sessions and written for identified sessions.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • Agenta-AI/agenta#5388: Applies the same non-null user_id gating to deny-cache writes and adds corresponding tests.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: gating deny-cache writes on non-null user_id to avoid cross-user pollution.
Description check ✅ Passed The description is directly related and accurately describes the auth cache-deny fix and regression tests.
Linked Issues check ✅ Passed The code and tests satisfy #5387 by only caching deny entries when user_id is known and preserving behavior for authenticated users.
Out of Scope Changes check ✅ Passed The changes stay within the auth middleware fix and its regression tests, with no unrelated scope introduced.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
api/oss/tests/pytest/unit/middlewares/test_cache_deny.py (1)

29-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove unused variable assignment.

Static analysis indicates that mock_set_cache is assigned but never used (Flake8 F841).

♻️ Proposed refactor
     with patch("oss.src.middlewares.auth.get_session", return_value=mock_session), \
-         patch("oss.src.middlewares.auth.set_cache", side_effect=capture_set_cache) as mock_set_cache, \
+         patch("oss.src.middlewares.auth.set_cache", side_effect=capture_set_cache), \
          patch("oss.src.middlewares.auth.get_cache", return_value=None), \
          pytest.raises(Exception):

Source: Linters/SAST tools


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e5aa8be8-d861-4c42-982c-ca92fcd02538

📥 Commits

Reviewing files that changed from the base of the PR and between 3b0c0cb and c6015a1.

📒 Files selected for processing (2)
  • api/oss/src/middlewares/auth.py
  • api/oss/tests/pytest/unit/middlewares/test_cache_deny.py

Comment thread api/oss/tests/pytest/unit/middlewares/test_cache_deny.py Outdated
Comment on lines +71 to +76
with patch("oss.src.middlewares.auth.get_session", return_value=mock_session), \
patch("oss.src.middlewares.auth.set_cache", side_effect=capture_set_cache), \
patch("oss.src.middlewares.auth.get_cache", return_value=None), \
patch("oss.src.middlewares.auth.get_supertokens_user_by_id", side_effect=Exception("timeout")), \
pytest.raises(Exception):
await auth_user(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fix test logic: GatewayTimeoutException bypasses the catch-all handler.

The test patches get_supertokens_user_by_id to raise an Exception. However, inside the middleware, this exception is caught and re-raised as a GatewayTimeoutException (line 451). This specific exception is explicitly handled earlier in the exception block (line 765) and re-raised directly, completely bypassing the catch-all Exception handler where the new user_id is not None check and caching logic reside.

As a result, set_cache will never be called, and assert len(deny_calls) >= 1 will fail.

To correctly test the catch-all exception handler caching when user_id is populated, you can mock get_cache to return a user payload (which sets user_id) and then raise an exception on a downstream cache read to trigger the correct handler.

💚 Proposed fix to trigger the catch-all handler
+    async def mock_get_cache(*args, **kwargs):
+        if kwargs.get("namespace") == "get_supertokens_user_by_id":
+            # Return a valid user so `user_id` gets populated
+            return {"user_id": "user-123", "user_email": "test@test.com"}
+        # Raise an exception on downstream read to trigger the catch-all handler
+        raise Exception("mocked downstream error")
+
     with patch("oss.src.middlewares.auth.get_session", return_value=mock_session), \
          patch("oss.src.middlewares.auth.set_cache", side_effect=capture_set_cache), \
-         patch("oss.src.middlewares.auth.get_cache", return_value=None), \
-         patch("oss.src.middlewares.auth.get_supertokens_user_by_id", side_effect=Exception("timeout")), \
+         patch("oss.src.middlewares.auth.get_cache", side_effect=mock_get_cache), \
          pytest.raises(Exception):
         await auth_user(
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
with patch("oss.src.middlewares.auth.get_session", return_value=mock_session), \
patch("oss.src.middlewares.auth.set_cache", side_effect=capture_set_cache), \
patch("oss.src.middlewares.auth.get_cache", return_value=None), \
patch("oss.src.middlewares.auth.get_supertokens_user_by_id", side_effect=Exception("timeout")), \
pytest.raises(Exception):
await auth_user(
async def mock_get_cache(*args, **kwargs):
if kwargs.get("namespace") == "get_supertokens_user_by_id":
# Return a valid user so `user_id` gets populated
return {"user_id": "user-123", "user_email": "test@test.com"}
# Raise an exception on downstream read to trigger the catch-all handler
raise Exception("mocked downstream error")
with patch("oss.src.middlewares.auth.get_session", return_value=mock_session), \
patch("oss.src.middlewares.auth.set_cache", side_effect=capture_set_cache), \
patch("oss.src.middlewares.auth.get_cache", side_effect=mock_get_cache), \
pytest.raises(Exception):
await auth_user(

- Import verify_bearer_token instead of nonexistent auth_user
- Fix test_cache_deny_written_when_user_id_is_set: mock
  get_supertokens_user_by_id to return None (user not found)
  instead of raising Exception (which triggers GatewayTimeoutException
  and bypasses the catch-all handler)
- Remove unused mock_set_cache variable assignment
@truecallerabreham

Copy link
Copy Markdown
Contributor Author

Auth.py diff: See files changed for the full diff of the cache deny fix. The fix adds if user_id is not None: guards around set_cache calls in both exception handlers.

@truecallerabreham truecallerabreham left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Demo Screenshot\n\nAuth cache deny fix\n\nThe diff shows the if user_id is not None: guard added around both set_cache deny calls.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Backend Bug Report Something isn't working size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(auth): gate cache deny on non-null user_id to prevent cross-user pollution

2 participants