Skip to content

feat: bind free sessions to accounts to prevent multi-account abuse - #1171

Open
AbhijitK20 wants to merge 3 commits into
CodebuffAI:mainfrom
AbhijitK20:feat/session-account-binding
Open

feat: bind free sessions to accounts to prevent multi-account abuse#1171
AbhijitK20 wants to merge 3 commits into
CodebuffAI:mainfrom
AbhijitK20:feat/session-account-binding

Conversation

@AbhijitK20

Copy link
Copy Markdown

Summary

Prevents users from switching accounts mid-session to abuse free-tier limits. When a freebuff session becomes active, it is now bound to the authenticating user's ID. Multiple guard rails enforce this:

Changes

File Change
cli/src/state/freebuff-session-store.ts Added sessionBoundUserId state + setter
cli/src/hooks/use-freebuff-session.ts Set binding on active, clear on ended/none/superseded, startup guard for mismatched users
cli/src/commands/command-registry.ts /logout blocked during active session (requires --end-session or --force)
cli/src/hooks/use-auth-state.ts Login rejected if new user differs from session-bound user
cli/src/data/slash-commands.ts Updated /logout help text to document --force

How it works

  1. Session binds on activation — When the poll loop receives an active status, sessionBoundUserId is set to the current user's ID from credentials.json.

  2. /logout blocked — If a session is bound, /logout shows a warning and suggests /end-session first. Use /logout --force to override.

  3. Login rejected on mismatch — If someone logs in as a different user while a session is bound to another account, the login is rejected and credentials are cleared.

  4. Startup guard — On mount, if sessionBoundUserId exists but doesn't match the current credentials (e.g., credentials were swapped externally), the session is ended immediately.

  5. Binding cleared on session end — When the session transitions to ended, none, or superseded, the binding is cleared.

Abuse scenario prevented

Before: User A logs in → gets free session → /logout → User B logs in → another free session
After:  User A logs in → gets free session → /logout blocked → must /end-session first

Notes

  • Server already enforces one free session per account — this adds client-side enforcement so the same machine can't cycle through accounts
  • No functional change for legitimate single-account users
  • The --force flag on /logout provides an escape hatch for edge cases

Dataflow Dev added 2 commits September 1, 2026 04:37
Prevent users from switching accounts mid-session to abuse free-tier
limits. When a freebuff session becomes active, it is now bound to the
authenticating user's ID. Multiple guard rails enforce this:

- /logout blocked during active session (use /end-session or --force)
- Login rejected if new user differs from session-bound user
- Startup guard ends session if credentials changed externally
- Session binding cleared on ended/none/superseded transitions

The server already enforces one free session per account; this adds
client-side enforcement so the same machine can't cycle through
multiple accounts to farm free sessions.
…d tests

- Login modal now shows clear error message when rejected due to
  account mismatch instead of silently reverting to the login screen
- /logout --force now releases the server-side session slot and clears
  the binding before clearing credentials
- Added ACCOUNT_SWITCH_BLOCKED analytics event to track abuse attempts
- Added unit tests for session binding store and helper functions

Addresses review feedback on PR CodebuffAI#1171 for tighter abuse prevention.
@AbhijitK20
AbhijitK20 force-pushed the feat/session-account-binding branch from d8e4630 to 792fcc1 Compare August 31, 2026 23:12
@codebuff-team

Copy link
Copy Markdown
Contributor

Good instinct — preventing account-cycling on shared free-tier slots is worth doing — but the implementation doesn't actually hold up against the scenario it's designed to stop.

sessionBoundUserId in freebuff-session-store.ts is plain in-memory zustand state with no persistence (no persist middleware, no write to disk/credentials file). A user abusing free sessions doesn't need /logout --force — they can just Ctrl-C the CLI and start a new process. On restart, sessionBoundUserId is null again, the startup guard in use-freebuff-session.ts (line ~426) has nothing to compare against, and once the poll loop sees status: 'active' it re-binds to whatever user is currently in credentials.json — including the new account. So the described abuse scenario (logout → login as B → new free session) is unaffected by this PR whenever the user simply restarts the process instead of using /logout, which is the more natural way to switch accounts anyway.

That makes the added friction (blocking /logout, rejecting login on mismatch, the --force escape hatch, the new analytics event) mostly cosmetic — it inconveniences the honest path while doing nothing against the actual abuse path. The PR body's claim that this is meaningful "client-side enforcement" on top of server-side limits doesn't hold unless the binding survives process restarts (e.g. persisted alongside credentials.json or tied to the server-issued session/instance id itself, checked server-side).

Secondary point: login-modal.tsx's onLoginSuccess signature changes from void to string | null — worth double-checking there are no other call sites assuming the old contract.

Tests only cover the zustand store getters/setters, not the actual restart-bypass scenario, which is the crux of whether this defends anything.

Worth revisiting with persisted or server-verified binding rather than closing outright, since the direction is sound.

@codebuff-team codebuff-team added bot:triaged Classified by the community triage bot pr:needs-work Right idea, not mergeable as written labels Sep 1, 2026
Addresses bot feedback on PR CodebuffAI#1171: the in-memory session binding was
bypassed by simply restarting the CLI process. Now the binding is
persisted to ~/.config/manicode/session-binding.json alongside the
session lifecycle:

- Persisted when session becomes active
- Loaded on startup and restored to in-memory state
- Cleared when session ends or on force-logout
- Checked on startup against current credentials

This closes the restart-bypass abuse path: a user who Ctrl-C's and
restarts with different credentials will hit the startup guard, which
reads the persisted binding and ends the session if the user changed.
@AbhijitK20

Copy link
Copy Markdown
Author

Thanks for the thorough review — the restart-bypass hole was a real catch. Here's what's been addressed:

1. Persistence (the main gap)

Added cli/src/utils/session-binding.ts — the binding is now persisted to ~/.config/manicode/session-binding.json:

  • Written when session becomes active
  • Loaded on startup and restored to in-memory state
  • Cleared on session end or /logout --force
  • Checked on startup against current credentials

The restart-bypass path is now closed:

Before: Ctrl-C → restart → binding is null → switch accounts → new session
After:  Ctrl-C → restart → readSessionBinding() returns old user → startup guard ends session

2. onLoginSuccess type change

Verified — only one call site (app.tsx:235LoginModal). The LoginModalProps interface was updated to match. The plain login flow (freebuff login command) runs outside the TUI and doesn't use this callback.

3. Tests

Added cli/src/utils/__tests__/session-binding.test.ts — 8 tests covering read/write/clear/malformed edge cases against the actual file I/O.

Also added cli/src/state/__tests__/freebuff-session-store.test.ts for the store binding lifecycle.

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

Labels

bot:triaged Classified by the community triage bot pr:needs-work Right idea, not mergeable as written

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants