fix(auth): the session row must carry the id the token claims

The sid revocation check added in e2589d4 rejected EVERY authenticated request
with 401. issue_session generated session_id = Uuid::new_v4(), minted the access
token with it as sid, then called create_session WITHOUT passing it, so Postgres
assigned the row a different primary key. is_session_live(sid) therefore never
matched and the liveness check failed closed on every call.

Both halves were individually correct, which is why unit tests did not catch it:
303 backend tests passed throughout. The e2e suite caught it, as a wallet that
authenticated successfully and then showed 0 for every asset.

NewSession now carries the id explicitly and create_session binds it, at all
three call sites (extension/website login and the refresh rotation, which shares
the helper).

Verified: fiat-balance e2e goes from all-zero to $108.21.
This commit is contained in:
jwinterm
2026-08-02 21:32:58 -04:00
parent f48373282f
commit c64cd9be8e
4 changed files with 13 additions and 2 deletions
+1
View File
@@ -224,6 +224,7 @@ async fn issue_session(
state
.db
.create_session(NewSession {
id: session_id,
user_id,
refresh_token_hash,
platform: platform.to_string(),
+1
View File
@@ -302,6 +302,7 @@ pub async fn website_verify(
state
.db
.create_session(NewSession {
id: session_id,
user_id: user.id,
refresh_token_hash,
platform: Platform::Web.to_string(),
+6 -2
View File
@@ -22,11 +22,15 @@ impl Database {
#[instrument(skip(self, input), fields(user_id = %input.user_id, platform = %input.platform))]
pub async fn create_session(&self, input: NewSession) -> Result<Session, AppError> {
let sql = format!(
// `id` is bound explicitly: the access token carries it as `sid`,
// so the row and the token must agree or revocation checks reject
// every request.
"INSERT INTO sessions \
(user_id, refresh_token_hash, platform, device_info, expires_at) \
VALUES ($1, $2, $3, $4, $5) RETURNING {SESSION_COLS}"
(id, user_id, refresh_token_hash, platform, device_info, expires_at) \
VALUES ($1, $2, $3, $4, $5, $6) RETURNING {SESSION_COLS}"
);
let session = sqlx::query_as::<_, Session>(&sql)
.bind(input.id)
.bind(input.user_id)
.bind(&input.refresh_token_hash)
.bind(&input.platform)
+5
View File
@@ -208,6 +208,11 @@ pub struct NewUser {
#[derive(Debug, Clone)]
pub struct NewSession {
/// The session's primary key. Supplied by the caller (NOT defaulted by the
/// DB) because the access token embeds it as `sid` for revocation: if the
/// row got a different id than the token, every authenticated request would
/// fail the liveness check.
pub id: Uuid,
pub user_id: Uuid,
pub refresh_token_hash: String,
pub platform: String,