Skip to content

fix: validate session token update events#2018

Merged
senamakel merged 3 commits into
tinyhumansai:mainfrom
LawyerLyu:fix/validate-session-token-event
May 18, 2026
Merged

fix: validate session token update events#2018
senamakel merged 3 commits into
tinyhumansai:mainfrom
LawyerLyu:fix/validate-session-token-event

Conversation

@LawyerLyu
Copy link
Copy Markdown
Contributor

@LawyerLyu LawyerLyu commented May 17, 2026

Summary

  • Validate core-state:session-token-updated payloads before accepting a session token.
  • Require event-provided tokens to be JWT-shaped, decodeable, and unexpired via the exp claim.
  • Add regression coverage for malformed event payloads and accepted unexpired JWT-shaped payloads.

Problem

Issue #1937 reports that the core-state:session-token-updated window event accepted arbitrary strings from event.detail.sessionToken. Any code able to dispatch that event could replace the frontend session token until the next authoritative refresh.

Solution

  • Add a small local validator in CoreStateProvider for this event-only path.
  • Decode the JWT payload using base64url handling and reject malformed JSON, missing exp, expired tokens, empty tokens, whitespace-wrapped tokens, and non-three-part values.
  • Keep the normal storeSessionToken path unchanged so existing Tauri/session storage flows continue to use their current contract.

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategy
  • Diff coverage ≥ 80% — CI Coverage Gate passed; focused Vitest coverage exercises the changed event path.
  • Coverage matrix updated — N/A: security hardening for an existing provider event path; no feature row added/removed/renamed.
  • All affected feature IDs from the matrix are listed in the PR description under ## Related — N/A: no matrix feature ID applies.
  • No new external network dependencies introduced (mock backend used per Testing Strategy)
  • Manual smoke checklist updated if this touches release-cut surfaces (docs/RELEASE-MANUAL-SMOKE.md) — N/A: no release manual smoke surface changed.
  • Linked issue closed via Closes #NNN in the ## Related section

Impact

This affects the React frontend CoreStateProvider event listener only. Malformed or expired session-token update events are ignored instead of being committed optimistically. Valid unexpired JWT-shaped events continue to trigger the existing optimistic session update and refresh flow.

No migration, performance, or dependency impact.

Related


AI Authored PR Metadata (required for Codex/Linear PRs)

Keep this section for AI-authored PRs. For human-only PRs, mark each field N/A.

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: fix/validate-session-token-event
  • Commit SHA: 1145563

Validation Run

  • pnpm --filter openhuman-app format:check — passed via the full pre-push hook after installing the local Rust toolchain.
  • pnpm typecheckcorepack pnpm --filter openhuman-app compile
  • Focused tests: corepack pnpm --filter openhuman-app exec vitest run --config test/vitest.config.ts src/providers/__tests__/CoreStateProvider.test.tsx
  • Rust fmt/check (if changed): pnpm rust:check passed; rustfmt check also passed through the full pre-push hook.
  • Tauri fmt/check (if changed): cargo check --manifest-path app/src-tauri/Cargo.toml passed via pnpm rust:check; no Tauri files changed.

Validation Blocked

  • command: N/A
  • error: N/A
  • impact: N/A — the earlier cargo: not found blocker was resolved by installing the local Rust toolchain; the full pre-push hook now passes.

Behavior Changes

  • Intended behavior change: ignore malformed or expired core-state:session-token-updated event tokens.
  • User-visible effect: none expected for valid session update flows; malicious or malformed injected events no longer replace the session token.

Parity Contract

  • Legacy behavior preserved: valid unexpired JWT-shaped deep-link/session-update events still update the session and trigger refresh.
  • Guard/fallback/dispatch parity checks: normal storeSessionToken path is unchanged; validation is scoped to the window event listener.

Duplicate / Superseded PR Handling

  • Duplicate PR(s): N/A
  • Canonical PR: this PR
  • Resolution (closed/superseded/updated): N/A

Summary by CodeRabbit

  • Bug Fixes

    • Session tokens are now sanity-checked (structure, decodability, and expiry) before use, preventing authentication refreshes from being triggered by malformed or expired tokens.
  • Tests

    • Added test coverage to verify proper handling of malformed, expired, and valid session tokens during token-update events.

Review Change Stack

@LawyerLyu LawyerLyu requested a review from a team May 17, 2026 17:56
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 17, 2026

📝 Walkthrough

Walkthrough

This PR adds client-side JWT plausibility checks to CoreStateProvider: payloads are base64url-decoded and must have three segments and a numeric, unexpired exp before the core-state:session-token-updated handler accepts the token and triggers a refresh. Tests cover malformed, expired, and valid tokens.

Changes

Session token validation on update

Layer / File(s) Summary
JWT validation helpers and event handler integration
app/src/providers/CoreStateProvider.tsx
New decodeJwtPayload and isPlausibleSessionToken helpers decode base64-url JWT payloads and verify token structure (three dot-separated segments) and expiry (exp numeric and in the future). The core-state:session-token-updated handler now rejects tokens that fail this validation.
Token validation test coverage
app/src/providers/__tests__/CoreStateProvider.test.tsx
Adds makeJwt test helper and three tests: malformed tokens are ignored, expired JWT-shaped tokens are ignored, and unexpired JWT-shaped tokens are accepted (updating displayed token and advancing mocked snapshot fetches).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • tinyhumansai/openhuman#1999: Modifies the same CoreStateProvider core-state:session-token-updated handler to validate incoming session tokens.

Suggested labels

working

Poem

🐇 I nibble bugs beneath the moonlit glade,
I check each token, every dot and shade,
Base64 whispers secrets in my paw,
Expiry checked — I stand up straight and law,
A tiny rabbit guards your auth parade.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: validate session token update events' clearly and concisely describes the main change: adding validation to the session token update event handler in CoreStateProvider.
Linked Issues check ✅ Passed All code requirements from issue #1937 are met: validation for three dot-separated parts, base64url decodeable payload, numeric exp claim in future time, and rejection of invalid tokens in the event path.
Out of Scope Changes check ✅ Passed All changes are directly scoped to validating the core-state:session-token-updated event handler as required; no unrelated modifications or scope creep detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
app/src/providers/__tests__/CoreStateProvider.test.tsx (1)

281-330: ⚡ Quick win

Add an explicit expired-token regression test.

This PR contract includes rejecting expired exp values, but current additions only lock malformed and unexpired paths.

🧪 Suggested test addition
+  it('ignores expired JWT-shaped session-token-updated events (`#1937`)', async () => {
+    const expiredToken = makeJwt({ exp: Math.floor(Date.now() / 1000) - 1 });
+    fetchSnapshot.mockResolvedValue(makeSnapshot({ userId: null, sessionToken: null }));
+    listTeams.mockResolvedValue([]);
+
+    render(
+      <CoreStateProvider>
+        <Consumer />
+      </CoreStateProvider>
+    );
+
+    await waitFor(() => expect(screen.getByTestId('ready').textContent).toBe('ready'));
+
+    await act(async () => {
+      window.dispatchEvent(
+        new CustomEvent('core-state:session-token-updated', {
+          detail: { sessionToken: expiredToken },
+        })
+      );
+    });
+
+    expect(screen.getByTestId('token').textContent).toBe('none');
+    expect(fetchSnapshot).toHaveBeenCalledTimes(1);
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/providers/__tests__/CoreStateProvider.test.tsx` around lines 281 -
330, Add a new test in CoreStateProvider.test.tsx that verifies expired
JWT-shaped session-token-updated events are rejected: use makeJwt to create a
token with exp set to Math.floor(Date.now()/1000) - 60 (or another past
timestamp), stub fetchSnapshot to return initial snapshot (sessionToken null)
and to remain unchanged after the event, and ensure dispatching the
CustomEvent('core-state:session-token-updated', { detail: { sessionToken:
expiredToken } }) does not set the token in the rendered Consumer and
fetchSnapshot is not called again; place the test alongside the existing
malformed and unexpired tests that use CoreStateProvider, Consumer,
fetchSnapshot, listTeams and makeSnapshot for consistency.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@app/src/providers/__tests__/CoreStateProvider.test.tsx`:
- Around line 281-330: Add a new test in CoreStateProvider.test.tsx that
verifies expired JWT-shaped session-token-updated events are rejected: use
makeJwt to create a token with exp set to Math.floor(Date.now()/1000) - 60 (or
another past timestamp), stub fetchSnapshot to return initial snapshot
(sessionToken null) and to remain unchanged after the event, and ensure
dispatching the CustomEvent('core-state:session-token-updated', { detail: {
sessionToken: expiredToken } }) does not set the token in the rendered Consumer
and fetchSnapshot is not called again; place the test alongside the existing
malformed and unexpired tests that use CoreStateProvider, Consumer,
fetchSnapshot, listTeams and makeSnapshot for consistency.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 22fb4803-3724-4da7-8f0a-4bede38926a9

📥 Commits

Reviewing files that changed from the base of the PR and between f9de38d and 1145563.

📒 Files selected for processing (2)
  • app/src/providers/CoreStateProvider.tsx
  • app/src/providers/__tests__/CoreStateProvider.test.tsx

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 17, 2026
@coderabbitai coderabbitai Bot added the working A PR that is being worked on by the team. label May 18, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
app/src/providers/__tests__/CoreStateProvider.test.tsx (2)

331-357: ⚡ Quick win

Stabilize expiry tests by controlling time.

Using Date.now() directly makes these tests wall-clock dependent. Prefer fixed system time (vi.useFakeTimers() + vi.setSystemTime(...)) or much larger offsets to avoid time-sensitive flakes.

As per coding guidelines: “Keep tests deterministic: avoid real network calls, time-sensitive flakes, or hidden global state.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/providers/__tests__/CoreStateProvider.test.tsx` around lines 331 -
357, The tests "ignores expired JWT-shaped session-token-updated events (`#1937`)"
and "accepts unexpired JWT-shaped session-token-updated events (`#1937`)" are
fragile because they call makeJwt with Date.now(); stabilize them by using
vi.useFakeTimers() and vi.setSystemTime(...) to fix the current time (or
alternatively construct exp values using a very large offset) before calling
makeJwt, and then restore timers with vi.useRealTimers() after each test; update
the setup for these tests (referencing makeJwt and the two it(...) blocks) so
the expiry calculations are deterministic.

356-380: ⚡ Quick win

Assert the refresh side effect on accepted token events.

This test validates token acceptance, but it doesn’t explicitly verify the refresh flow was triggered. Add a call-count assertion so regressions don’t silently pass if only optimistic state update remains.

Suggested assertion
   await act(async () => {
     window.dispatchEvent(
       new CustomEvent('core-state:session-token-updated', { detail: { sessionToken: token } })
     );
   });

   expect(screen.getByTestId('token').textContent).toBe(token);
+  expect(fetchSnapshot).toHaveBeenCalledTimes(2);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/providers/__tests__/CoreStateProvider.test.tsx` around lines 356 -
380, The test "accepts unexpired JWT-shaped session-token-updated events
(`#1937`)" accepts the token but doesn't assert the refresh side-effect; after
dispatching the 'core-state:session-token-updated' CustomEvent (detail: {
sessionToken: token }) add an assertion that the snapshot refresh mock
(fetchSnapshot) was invoked (e.g., expect(fetchSnapshot).toHaveBeenCalled() or
toHaveBeenCalledTimes with the expected count) to ensure the refresh flow in
CoreStateProvider was triggered by the event.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@app/src/providers/__tests__/CoreStateProvider.test.tsx`:
- Around line 331-357: The tests "ignores expired JWT-shaped
session-token-updated events (`#1937`)" and "accepts unexpired JWT-shaped
session-token-updated events (`#1937`)" are fragile because they call makeJwt with
Date.now(); stabilize them by using vi.useFakeTimers() and vi.setSystemTime(...)
to fix the current time (or alternatively construct exp values using a very
large offset) before calling makeJwt, and then restore timers with
vi.useRealTimers() after each test; update the setup for these tests
(referencing makeJwt and the two it(...) blocks) so the expiry calculations are
deterministic.
- Around line 356-380: The test "accepts unexpired JWT-shaped
session-token-updated events (`#1937`)" accepts the token but doesn't assert the
refresh side-effect; after dispatching the 'core-state:session-token-updated'
CustomEvent (detail: { sessionToken: token }) add an assertion that the snapshot
refresh mock (fetchSnapshot) was invoked (e.g.,
expect(fetchSnapshot).toHaveBeenCalled() or toHaveBeenCalledTimes with the
expected count) to ensure the refresh flow in CoreStateProvider was triggered by
the event.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8327c325-1d7d-457b-875f-692a5f166ae2

📥 Commits

Reviewing files that changed from the base of the PR and between 1145563 and 3c8f328.

📒 Files selected for processing (2)
  • app/src/providers/CoreStateProvider.tsx
  • app/src/providers/__tests__/CoreStateProvider.test.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/providers/CoreStateProvider.tsx

@senamakel senamakel merged commit db087a7 into tinyhumansai:main May 18, 2026
24 of 27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

working A PR that is being worked on by the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security: Unvalidated session-token-updated window event in CoreStateProvider

2 participants