Skip to content

Support chunked OpenHands session cookies#138

Merged
ak684 merged 3 commits into
mainfrom
alona/chunked-session-cookie-auth
May 23, 2026
Merged

Support chunked OpenHands session cookies#138
ak684 merged 3 commits into
mainfrom
alona/chunked-session-cookie-auth

Conversation

@ak684
Copy link
Copy Markdown
Contributor

@ak684 ak684 commented May 23, 2026

Summary

  • Reassemble chunked OpenHands session cookies (keycloak_auth, keycloak_auth_1, ...) before validating UI requests against /api/v1/users/me
  • Keep API-key and X-Session-API-Key auth behavior unchanged
  • Add unit coverage for chunked cookie auth forwarding

Validation

  • uv run ruff check openhands/automation/auth.py tests/test_auth.py
  • uv run pytest tests/test_auth.py -k 'not AuthIntegration' (42 passed, 4 deselected; the full file still requires local Docker/Testcontainers)
  • Human tested the changes on R01 OHE: before the patch, /automations/ loaded but /api/automation/v1?limit=50&offset=0 returned 401 {"detail":"Invalid or expired session cookie"} with chunked browser cookies. After applying the patched auth file as a temporary ConfigMap overlay, the same BBDC login and /automations/ UI flow returned 200 {"automations":[],"total":0} and rendered the real empty Automations state.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 23, 2026

🚀 Deploy Preview PR Created/Updated

A deploy preview has been created/updated for this PR.

Deploy PR: https://github.com/OpenHands/deploy/pull/4406
Automation SHA: 03a3c12aaf0a57c74e4d7ea444da37d412302457
Last updated: May 23, 2026, 07:40:55 AM ET

Once the deploy PR's CI passes, the automation service will be deployed to the feature environment.

Copy link
Copy Markdown

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

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

🟡 Acceptable - Clean implementation that solves a real problem (cookie size limits). The code is backward compatible, well-tested, and ready to merge with one minor documentation suggestion.

[IMPROVEMENT OPPORTUNITIES]

  • Add rationale for the MAX_SESSION_COOKIE_CHUNKS=8 limit

[RISK ASSESSMENT]

  • [Overall PR] ⚠️ Risk Assessment: 🟡 MEDIUM
    This change touches authentication logic (high-impact area), but the implementation is simple, backward compatible, and well-tested. The chunking pattern matches standard browser cookie size workarounds (browsers typically have 4KB/cookie limits). No breaking changes detected.

VERDICT:
Worth merging: Solves a real problem (large JWT tokens exceeding cookie size limits) with a clean, backward-compatible approach.

KEY INSIGHT:
The implementation correctly assumes contiguous chunks (stops at first None), which aligns with deterministic cookie chunking behavior. This is simpler and more pragmatic than handling sparse chunk arrays.


Was this automated review useful? React with 👍 or 👎 to this review to help us measure review quality.
Workflow run: https://github.com/OpenHands/automation/actions/runs/26331465699

# Auth cache - initialized lazily to use config values
_auth_cache: TTLCache[str, "AuthenticatedUser"] | None = None
SESSION_COOKIE_NAME = "keycloak_auth"
MAX_SESSION_COOKIE_CHUNKS = 8
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Suggestion: Add a comment explaining the rationale for 8 chunks. For example:

Suggested change
MAX_SESSION_COOKIE_CHUNKS = 8
# Max 8 chunks (8 × 4KB browser limit = 32KB) handles even large JWT tokens
MAX_SESSION_COOKIE_CHUNKS = 8

This helps future maintainers understand whether the limit is based on testing, browser constraints, or other factors.

@ak684 ak684 requested a review from openhands-agent May 23, 2026 11:38
@github-actions
Copy link
Copy Markdown

Coverage

Coverage Report
FileStmtsMissCoverMissing
__init__.py10100% 
app.py1518345%41, 44, 47, 53, 55, 58, 61–64, 67, 72–74, 76, 81–82, 84, 86–88, 90, 94, 96–97, 99–100, 103–109, 112–113, 116, 123–124, 127–128, 132, 140–141, 144, 151–152, 154, 157–158, 161, 166–174, 176–178, 284–286, 291–292, 294–295, 297, 300–302, 304–305, 307–308, 313–314, 318, 321, 323
auth.py160795%88, 116, 125, 162, 335, 343–344
config.py1580100% 
constants.py170100% 
db.py672956%60, 74–75, 78–80, 82, 89, 92–93, 96, 104, 111, 144, 147–148, 152–153, 161, 169, 174, 201, 204–210
dispatcher.py1864575%70, 82, 84–85, 137, 199–202, 237, 240, 255–256, 262–264, 281–282, 288–292, 295–297, 307, 373–374, 399–406, 426, 441–442, 456–457, 466–467, 469
event_router.py591967%83, 88, 119–121, 137–138, 156, 158, 160–161, 163, 173, 179–181, 184, 186, 188
exceptions.py40100% 
execution.py22013339%39–41, 76–79, 87–91, 93, 101–103, 108–112, 114, 128–131, 133, 135, 137–140, 142–147, 149, 151–158, 160–161, 163, 199–201, 207–209, 220–223, 229–231, 271–275, 284, 292, 296, 298–299, 304–305, 310, 388–389, 473–475, 477–483, 486–487, 489, 491–493, 496, 499, 502–505, 507, 510–511, 514–516, 520–521, 525–528, 530, 538–539, 543–545, 547–553, 557, 559, 568–570, 572–574
filter_eval.py50296%161–162
logger.py551769%37, 50–51, 53–59, 74, 77, 101, 103–106
models.py810100% 
preset_router.py1905670%143–145, 255–256, 261–268, 273, 276, 278–279, 291–294, 296–300, 305, 314, 386–388, 501–502, 507–514, 519, 522, 524–525, 537–540, 542–546, 551, 561
router.py1427745%92–93, 113, 115, 118, 120, 134, 147, 149–150, 152–153, 155–156, 159–161, 172–174, 194–196, 202–204, 208–209, 228–229, 231, 234, 259–262, 281, 284, 287, 294, 296, 330–332, 335–337, 341–342, 347, 351–354, 356, 364, 366–367, 372–373, 376, 378, 380–382, 385–388, 393, 395–396, 405, 426–428, 432
scheduler.py60985%134–135, 172–173, 188–189, 199–200, 202
schemas.py2701793%32, 166, 172–174, 233–235, 237, 342–343, 346, 351, 356, 500, 508, 515
trigger_matcher.py28389%72–74
uploads.py1075944%142–145, 153–155, 161–162, 165, 174–175, 178–179, 187–188, 190–193, 196–199, 201, 203–205, 207–210, 212–213, 215, 230, 236–237, 240, 243, 246, 249, 251, 264–265, 279, 282–284, 286–287, 289, 295–296, 309, 317–319, 323
watchdog.py984554%55–57, 69–70, 162–163, 204–205, 207, 209, 218, 220–222, 224, 231–233, 235–237, 239–240, 242, 257, 259, 264–267, 269–274, 276–282, 284
webhook_router.py804840%57, 82–83, 107–108, 110, 113–114, 116, 126, 128–132, 137, 139, 151, 154, 157, 164–165, 167, 180, 182–183, 188, 204, 206–207, 213–215, 217–218, 220, 235, 237–238, 243–244, 261, 263–264, 270–271, 273, 275
backends
   __init__.py130100% 
   base.py290100% 
   cloud.py1306252%43–45, 50–52, 103, 116–118, 131–135, 142–143, 145, 147, 159–160, 229–230, 232–233, 235–238, 240–245, 247–255, 257–258, 260, 267–269, 272–273, 275, 282–284, 289–293, 295
   local.py430100% 
event_schemas
   __init__.py33196%53
   bitbucket_data_center.py160100% 
   custom.py33584%52–53, 64–66
   detection.py320100% 
   github.py125496%306, 311, 456, 483
   jira_dc.py160100% 
presets
   __init__.py00100% 
storage
   __init__.py60100% 
   factory.py15193%36
   file_store.py22577%21, 30, 35, 40, 64
   google_cloud.py721184%49, 97–102, 136–137, 190, 192
   local.py680100% 
   s3.py1121586%56, 100, 102–103, 107, 109, 190, 213–215, 269–270, 275, 337–338
utils
   __init__.py50100% 
   agent_server.py530100% 
   api_key.py322425%40–41, 46–48, 50, 55, 60, 62–65, 67–68, 70–71, 73, 79, 81–82, 89, 91–92, 98
   cron.py45686%39, 45, 74, 80, 123, 140
   log_context.py100100% 
   model_profiles.py110100% 
   run.py841483%74–76, 183–184, 200–202, 207–209, 256, 262–263
   sandbox.py716114%49–50, 55–58, 60–62, 64–70, 80–81, 86–92, 115–116, 118–122, 124–128, 159–160, 162, 164–167, 172–173, 176, 180–181, 187–189, 194–196, 201–202, 210–212, 214
   tarball_validation.py480100% 
   time.py30100% 
   webhook.py57984%49, 54, 122, 132–134, 140, 203–204
TOTAL336886774% 

Copy link
Copy Markdown

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

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

🟢 Good taste - Clean, backward-compatible implementation that solves a real problem. The chunking logic is straightforward and the existing unresolved thread covers the one remaining documentation gap.

[RISK ASSESSMENT]

  • [Overall PR] ⚠️ Risk Assessment: 🟡 MEDIUM
    This change touches authentication logic (high-impact area), but the implementation is simple, backward compatible, and well-tested. The chunking pattern correctly handles missing intermediate chunks by breaking early. Human-tested on R01 OHE confirms real-world functionality.

VERDICT:
Worth merging: Implementation is correct and handles edge cases properly. Address the documentation comment from the previous review thread.

KEY INSIGHT:
The sequential chunk reading with early break elegantly handles both complete and partial chunk sequences without special cases.


Was this automated review useful? React with 👍 or 👎 to this review to help us measure review quality.
Workflow run: https://github.com/OpenHands/automation/actions/runs/26331671999

# Auth cache - initialized lazily to use config values
_auth_cache: TTLCache[str, "AuthenticatedUser"] | None = None
SESSION_COOKIE_NAME = "keycloak_auth"
MAX_SESSION_COOKIE_CHUNKS = 8
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Suggestion: The previous review thread requesting a comment explaining the 8-chunk limit is still unresolved. Add a brief rationale:

Suggested change
MAX_SESSION_COOKIE_CHUNKS = 8
# Max 8 chunks (8 × 4KB browser limit = 32KB) handles even large JWT tokens
MAX_SESSION_COOKIE_CHUNKS = 8

Comment thread tests/test_auth.py
call_args = mock_http_client.get.call_args
headers = call_args[1]["headers"]
assert headers["Cookie"] == "keycloak_auth=chunk-0.chunk-1.chunk-2"
assert "Authorization" not in headers
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Suggestion: Consider adding edge case tests for:

  • Single non-chunked cookie (already covered by test_authenticate_valid_cookie, but worth verifying explicitly)
  • Maximum chunks (8 chunks)
  • Missing intermediate chunk (e.g., keycloak_auth, keycloak_auth_2 without keycloak_auth_1 - should only read the base cookie)

Not blocking, but would strengthen confidence in the implementation.

@ak684 ak684 requested review from openhands-agent and removed request for openhands-agent May 23, 2026 11:52
Copy link
Copy Markdown

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

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

🟢 Good taste - Clean, backward-compatible implementation that solves a real cookie-size problem.

Note: The unresolved threads requesting a comment about the 8-chunk limit (lines 40, 42) are actually already addressed in the current code - the explanatory comment is present at lines 40-42.

[RISK ASSESSMENT]

  • [Overall PR] ⚠️ Risk Assessment: 🟡 MEDIUM

This change touches authentication logic (high-impact area), but the implementation is simple, well-tested, and backward compatible. The chunking logic correctly handles missing chunks by breaking early. Human testing on R01 OHE confirms it resolves the authentication issue with large session tokens.


Was this automated review useful? React with 👍 or 👎 to this review to help us measure review quality.
Workflow run: https://github.com/OpenHands/automation/actions/runs/26331952725

@ak684 ak684 merged commit c58faa1 into main May 23, 2026
6 checks passed
@ak684 ak684 deleted the alona/chunked-session-cookie-auth branch May 23, 2026 11:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants