Skip to content

fix(credentials): keep sessions and the API key on per-user storage - #8428

Merged
M3gA-Mind merged 2 commits into
tinyhumansai:mainfrom
M3gA-Mind:fix/signout-keeps-api-key-per-user-storage
Oct 27, 2026
Merged

M3gA-Mind merged 2 commits into
tinyhumansai:mainfrom
M3gA-Mind:fix/signout-keeps-api-key-per-user-storage

Conversation

@M3gA-Mind

@M3gA-Mind M3gA-Mind commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • On the default storage, where each profile has its own users/<id>/storage.db, a sign-in stored the session in the pre-login database. Right after signing in, auth.get_session_token read null and auth.get_state reported signed out.
  • Because of that, a session-only sign-out lost the user's API key (fix(credentials): keep the API key on sign-out when profiles live in storage #8366): auth.get_state reported signed out, including after a restart. This is release-blocking.
  • Both have one cause: credential writes happened against the wrong per-user database around a storage rebind. This PR fixes it and adds regression tests on the real per-user layout.

Problem

Found by the storage scenarios harness (#8426, scenarios A and J) and confirmed on disk.

  • The auth profiles live in the installed storage backend (profiles/persistence.rs → storage::secrets::current()), and the default backend belongs to the profile. Every credential change rebinds the process to the next profile's database (ops/user_scope.rs rebind_after_credential_change → storage::rebind).
  • Session in the wrong database. set_credential (security/credentials/ops/credential.rs) called store_provider_token(app-session) before rebind_after_credential_change. So the session was written to users/local/storage.db while the user's own users/<id>/storage.db stayed empty.
    • On disk after a sign-in, users/local/storage.db held the secret <user>:auth:app-session:default. The user's database held only their provider credential.
  • API key lost on sign-out. clear_session_credential removes app-session from the user's database, where it isn't, so it returns removed = false. In clear_credential, the carry-forward of the API key (refactor(auth): the core takes a credential; hosts own login and the backend session #6318) only runs if removed_session, so it was skipped. Meanwhile the teardown rebinds to users/local, where the key isn't.
    • Observed over JSON-RPC: auth_clear_credential {kind: "session"} → {removed: false, removedSession: false, removedApiKey: false}, then auth_get_state → isAuthenticated: false, also after a restart.
  • The shares_store special case added for fix(credentials): keep the API key on sign-out when profiles live in storage #8366 assumed one record for every profile. With per-user databases, even a carried key would have left a stale copy in the user's database.
  • The existing storage tests install one backend without a URL, which is never rebound (storage::rebind). So none of them could see a record written to the wrong database.

Solution

  • Sign-in: set_credential rebinds first, then stores the session, so the session lands in the user's own database. A failed rebind stores nothing, so a retry takes the install path again rather than the refresh fast-path that skips the bind. That's the invariant the old rollback protected.
  • Sign-out: clear_credential removes the API key from its source while that database is still installed, runs the teardown, then carries the key into the signed-out profile.
    • If the teardown or the carry fails, the key is put back (restore_api_key, logged), so a failed sign-out does not lose it.
    • The carry no longer depends on removed_session. The teardown rebinds to the signed-out workspace either way.
    • The shared-store special case is removed: with a shared store or per-directory files, the remove-then-carry rewrites the same record.

Semantics, matching #6318 and v0.64.15

  • A session-only sign-out keeps the API key in effect: it moves to the device's signed-out profile (users/local), and survives a restart.
  • The key is removed from the user's own database, so the next login to that account does not resurrect a stale copy. Only the session authenticates there.
  • Multi-user: a different user signing in uses their own database. They do not get the first user's key, and are authenticated by their own session.
  • A full sign-out (no kind) still removes the key everywhere, unchanged.

Tests

New ops_credential_per_user_storage_tests.rs (storage-sqlite, so part of the product test set). It installs the profile's default backend from its URL, as the desktop host does, so every sign-in and sign-out really rebinds between two databases:

  • a_session_is_stored_in_the_users_own_database: the token reads back exactly, and auth.get_state is a session for that user.
  • a_session_sign_out_keeps_the_api_key_on_per_user_storage: removedSession: true; the key is in effect from the signed-out profile, also after the store is reopened from its URL; and signing back in finds no stale key in the user's database.
  • another_user_signing_in_does_not_get_the_first_users_api_key: the negative multi-user check.

Red/green on cuttlefish: nextest, -p openhuman --lib, core product features, filter credential_per_user_storage_tests, 5 runs per side.

test fix 73204a2 main credential.rs (412075c)
a_session_is_stored_in_the_users_own_database 5/5 pass 0/5: get_session_token is None (:120)
a_session_sign_out_keeps_the_api_key_on_per_user_storage 5/5 pass 0/5: removedSession is false (:162)
another_user_signing_in_does_not_get_the_first_users_api_key 5/5 pass 0/5: fails at its control (:221), because on main the key is not in effect while signed out at all
  • On the fix, the security::credentials:: tests: 269/269 pass. That includes the existing storage-backend tests clearing_the_session_keeps_the_api_key_on_a_storage_backend and clearing_everything_removes_the_api_key_on_a_storage_backend.
  • End to end over JSON-RPC (feat(scripts): storage scenarios against the real core #8426's storage scenarios, --driver sqlite --only A,J, core built at 73204a2): J is 6/6, with removedSession: true and the key in effect after the sign-out and after a restart. A-secret-roundtrip passes, and the session reads back exactly.
  • 711383d, this head, adds only the cargo fmt order of the test module lines on top of 73204a2.

Submission Checklist

Impact

  • Desktop users on the default storage stay signed in after signing in, and keep their API key after a session-only sign-out.
  • No schema or migration change. An install that signed in on an earlier build has its session in users/local and reads as signed out: one sign-in stores it in the user's database. The stray app-session record left in users/local is not cleaned up here. Its token secret is keyed to the other user, so it reads back with no token and reports unauthenticated (seen in the scenario run).

Related

Summary by CodeRabbit

  • Bug Fixes
    • Improved credential updates so a new credential is stored only after the application’s settings have been successfully refreshed.
    • Signing out now preserves the API key in the signed-out profile, while keeping it out of profiles used for later sign-ins.
    • Improved recovery when signing out fails, helping restore the API key to its original storage.

On the default storage the backend belongs to the profile
(users/<id>/storage.db), and every credential change rebinds the process
to the next profile's database. Two paths wrote to the wrong one:

- set_credential stored the app-session profile before the rebind, so the
  session landed in the pre-login database (users/local) and read back as
  absent from the user's own: get_session_token returned null and
  auth.get_state reported signed out right after a sign-in.
- A session-only sign-out then found no session in the user's database
  (removedSession false), skipped the carry-forward, and rebound to
  users/local, where the API key was not: the key stopped working (tinyhumansai#8366),
  also after a restart.

Rebind before storing the session. On a sign-out, take the API key out of
its source while that database is still bound, then carry it into the
signed-out profile after the teardown, putting it back if the teardown or
the carry fails. This replaces the shared-store special case, which
assumed one record for every profile. The key stays with the device's
signed-out state, as tinyhumansai#6318 kept it; another account signing in does not
get it.

Regression tests on the default per-user layout (the existing storage
tests install one backend with no URL, which is never rebound).
@tinysweeper

tinysweeper Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Tiny Sweeper review

Reviewed head: 711383dd83c9aba41687cd8d9b3396333adc8999

Findings

No active actionable findings.

Advisory notes

  • medium: Cover the carry-failure restore path or drop it (crates/openhuman-core/src/security/credentials/ops/credential.rs:454)
    Problem: The new `restore_api_key` helper is the entire justification for removing the key *before* teardown: if the move fails, the key must be put back where the process can still read it
    Suggested fix:
    No concrete fix was supplied.

Verdict: Ready for maintainer review · Priority: none
Coverage: Reviewed 3 of 3 files.

Diagnostics and supporting details

Full summary

Pull request purpose (title): fix(credentials): keep sessions and the API key on per-user storage. Fixes credential writes that landed in the wrong per-user storage database on the default SQLite backend. In `set_credential`, the storage rebind now runs before the session write (with the old rollback removed) so the session lands in the user's own `users/<id>/storage.db`; a failed rebind stores nothing, preserving the retry invariant. In `clear_credential`, the source API key is cleared while the source store is still installed, the teardown rebinds, the key is carried into the signed-out profile, and a new `restore_api_key` helper puts the key back if teardown or carry fails; the carry no longer depends on `removed_session` and the `shares_store` special case is dropped. A new storage-sqlite-gated test module covers the sign-in read-back, session-only sign-out persistence across a store reopen, and the multi-user isolation path. The tests lane notes the new failure-recovery branches are not exercised by any test.

Review snapshot

Change surface Files Review signal Count
Production 1 Active findings 0
Tests 2 Noted findings 1
Documentation 0 Resolved findings 0
Configuration 0 Pending checks/questions 0

Completeness: Complete
Test assessment: Test coverage is assessed from changed tests and lane evidence; execution is not claimed without trusted check data.

What changed

Credential operations on per-user storage are reordered: `set_credential` rebinds storage to the target profile before storing the app session, and `clear_credential` clears the source API key before teardown, carries it into the signed-out profile after teardown regardless of whether a session profile was removed, and restores the key on teardown/carry failure via a new `restore_api_key` helper. The prior `shares_store` special case and the set_credential rollback are removed. Tests: a new `ops_credential_per_user_storage_tests.rs` module (wired behind the `storage-sqlite` feature in `ops_tests.rs`) installs the default backend from its URL so rebinds really switch databases, exercising sign-in storage placement, session-only sign-out key survival across a reopen, and second-user isolation.

Features

  • Modified — Rebind storage before storing the app session in set_credential: With per-user databases, the session is written to the signing-in user's own `storage.db` instead of the pre-login database, so the token reads back after sign-in. A failed rebind returns before any session write, so a retry retakes the install path rather than the refresh fast-path that skips the bind, which supersedes the removed rollback. (crates/openhuman-core/src/security/credentials/ops/credential.rs pub async fn set_credential()
  • Modified — Clear-then-carry API key ordering in clear_credential: The source API key is removed while its store is still installed, and after teardown it is carried into the signed-out workspace regardless of whether a session profile was found, so a session-only sign-out keeps the key in effect and leaves no stale copy in the user's database to resurrect on next login. (crates/openhuman-core/src/security/credentials/ops/credential.rs pub async fn clear_credential()
  • Added — restore_api_key failure recovery on sign-out: If teardown or the carry fails after the source key was removed, the key is written back so a failed sign-out does not lose it; restoration failure is logged as an error indicating the key must be re-entered. (crates/openhuman-core/src/security/credentials/ops/credential.rs pub async fn clear_credential()
  • Removed — Drop shares_store special case and set_credential rollback: Removes the shared-store branch that would otherwise be ambiguous under per-user databases; remove-then-carry now rewrites the same record for a shared store or per-directory files. (crates/openhuman-core/src/security/credentials/ops/credential.rs pub async fn clear_credential(, crates/openhuman-core/src/security/credentials/ops/credential.rs pub async fn set_credential()

Tests

  • regression — Signs in on the default SQLite backend and asserts the session token reads back from the user's own database and `auth_get_state` reports a session for that user.: Covers the sign-in half of the fix on the real per-user layout; the tests lane confirms this path is exercised. (crates/openhuman-core/src/security/credentials/ops_tests.rs fn store_live_session(user_id: &str) -> Config {)
  • regression — Performs a session-only sign-out, asserts `removedSession: true` and `removedApiKey: false`, that the key is in effect from the signed-out profile including after the store is reopened from its URL, and that signing back in finds no stale key in the user's database.: Covers the sign-out carry and reopen behavior; the tests lane notes the failure-recovery branches added by the reorder are not exercised. (crates/openhuman-core/src/security/credentials/ops_tests.rs fn store_live_session(user_id: &str) -> Config {)
  • regression — After a first user signs out leaving the key in the signed-out profile, a second user signs in and is asserted to receive no API key and to authenticate via their own session.: Covers the multi-user isolation negative case as described. (crates/openhuman-core/src/security/credentials/ops_tests.rs fn store_live_session(user_id: &str) -> Config {)

Before merge

None.

Agent review details

critique

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: Reviewed 3 files; 4 findings. _The code index is behind this pull request (indexed at `710726b959a1`), so retrieved context may be out of date._

security

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: Reviewed 3 files; 0 findings. _The code index is behind this pull request (indexed at `710726b959a1`), so retrieved context may be out of date._

tests

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Positive: The tests lane confirms the reordered sign-in and sign-out paths, including the sign-out, reopen, and second-user paths, have covering tests on the default SQLite backend, and flags that the new failure-recovery branches are never exercised.
  • Lane summary: The change moves the storage rebind in `set_credential` before the session write and drops the rollback, and in `clear_credential` clears the source API key while the source store is still installed, carries it forward after teardown, and restores it on failure. The diff adds a dedicated per-user-storage test module that exercises the new order on the default SQLite backend, including the sign-out, reopen, and second-user paths, so the behavioural changes have covering tests. One gap: the new failure-recovery branches, which are the reason the move is safe to reorder, are never exercised. _The code index is behind this pull request (indexed at `710726b959a1`), so retrieved context may be out of date._

commits

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Positive: The commits lane reported no sensitive content in committed files.
  • Lane summary: No sensitive content in committed files. _The code index is behind this pull request (indexed at `710726b959a1`), so retrieved context may be out of date._

description

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Positive: The description lane reports the description accurately explains both wrong-database root causes and matches the reordering, the removal of the shares_store special case, the new restore_api_key rollback, and the three new tests with storage-sqlite gating.
  • Lane summary: The description is accurate and complete for this change: it explains the wrong-database root cause for both the sign-in and the session-only sign-out, matches the reordering in `set_credential` and `clear_credential`, mentions the removal of the `shares_store` special case and the new `restore_api_key` rollback, and lists the three new tests with the storage-sqlite gating. The structure of the tests follows the repository's sibling-file rule. I found nothing in the diff that the description misstates or omits. _The code index is behind this pull request (indexed at `710726b959a1`), so retrieved context may be out of date._
Evidence and run details
  • Models: ladder/vectors-oai3, deepseek-v4.1-flash
  • Spend: $0.000248
  • Tokens: 647510 input · 10283 output · 145920 cached · 623 embedding
  • Updated: 2026-10-27T07:40:24Z
Head State Pass summary
711383dd83c9 ready for maintainer review 0 active finding(s), 0 resolved finding(s) (at 2026-10-27T07:40:24Z)

tinysweeper 0.1.2

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review 🔄 Running since 2026-10-27T07:32:29.726536Z 711383d PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 95ea749b-5f8c-4280-8711-8728fec7c605
Commits

Reviewing files that changed from the base of the PR and between 412075c and 711383d.

Files selected for processing: 3
  • crates/openhuman-core/src/security/credentials/ops/credential.rs
  • crates/openhuman-core/src/security/credentials/ops_credential_per_user_storage_tests.rs
  • crates/openhuman-core/src/security/credentials/ops_tests.rs

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 4 remain after this review.

Walkthrough

Credential installation now rebinds process globals before storing credentials. Credential clearing updates API-key storage around session teardown and reloads the signed-out configuration. SQLite tests cover per-user session storage and API-key behavior across sign-out and subsequent sign-ins.

Changes: Credential lifecycle

Layer / File(s) Summary
Credential installation and clearing
crates/openhuman-core/src/security/credentials/ops/credential.rs
set_credential rebinds process globals before storing credentials. clear_credential clears the source API key before teardown, reloads the signed-out configuration, and attempts key restoration if teardown or carrying fails.
Per-user storage tests
crates/openhuman-core/src/security/credentials/ops_credential_per_user_storage_tests.rs, crates/openhuman-core/src/security/credentials/ops_tests.rs
SQLite-feature tests cover per-user session storage and API-key behavior after sign-out, storage reopening, and sign-in as the same or a different user.

Priority: Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Possibly related PRs

  • tinyhumansai/openhuman#8366: Changes the session-only sign-out path for preserving an API key, which overlaps with this PR’s changes to clear_credential.

Suggested reviewers: senamakel, tinysweeper

Merge Risk: Low · up to 71138

Credential installation is generally mergeable, but a persistence failure can leave the wrong user workspace active until the scope is corrected.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Documentation-only edits and deletions within surv… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed 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 clearly and concisely describes the main change: preserving sessions and API keys in per-user storage.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 12 functions across 3 files.

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

A rabbit checks the keys at night,
And watches profiles settle right.
The session rests, the keys remain,
Then tests sign in and out again.
Small paws applaud the SQLite trail.

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

@tinysweeper tinysweeper 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.

tinysweeper found nothing blocking. Approving.

Diagnostics

Total spend: $0.0002

@tinysweeper tinysweeper Bot added the priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect. label Oct 27, 2026
@M3gA-Mind
M3gA-Mind merged commit ec81327 into tinyhumansai:main Oct 27, 2026
29 of 35 checks passed
M3gA-Mind added a commit to M3gA-Mind/openhuman that referenced this pull request Oct 27, 2026
Follow-up to tinyhumansai#8428. clear_credential takes the API key out of its source
before the session teardown, and puts it back if the teardown or the carry
into the signed-out workspace fails. A reload_config_or failure between the
teardown and the carry returned with the key removed. All three steps now
restore it the same way (inspect_err).

clear_credential delegates to clear_credential_with, which takes the
post-teardown reload as a closure (production passes reload_config_or), so
tests can fail it: a failed reload and a failed carry each return the error
with the key back at its source. restore_api_key is also tested directly.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant