Files
at-container-registry/pkg/appview/db/session_orphan_test.go
T
Evan JarrettandClaude Opus 5 8cd59a61f1 appview: stop a UI session outliving the OAuth session behind it
Only one oauth_sessions row is kept per account, so signing in again — on a
second device, or simply a second time — replaces it and leaves every earlier
ui_sessions row pointing at an oauth_session_id that no longer exists. Get
checked only expiry, so those still read back as usable.

Found on a live appview: four ui_sessions rows, three orphaned, and requesting
/settings/user with an orphaned cookie returned 200 with the account's handle
rendered throughout, where an anonymous request gets a 302. The browser looks
signed in while the credential behind it is gone, so every PDS-backed action
fails against a UI insisting the session is fine. It now fails closed and sends
the user back through login.

Get also never checked ownership. oauth_sessions is unique on
(account_did, session_id), so the existence check is scoped by both; matching
session_id alone would let one account's live OAuth session validate another
account's dangling reference. That has its own test.

An empty oauth_session_id stays valid, since Create makes sessions that never
had one, and a test pins that so the check cannot start rejecting them.

TestSessionStore_CreateWithOAuth referenced an OAuth session it never inserted,
which is an orphan by definition, so it now creates the row. Its intent was
that CreateWithOAuth persists the ID; it relied on the orphan behaviour only
incidentally. Its not-found branch used t.Error and then dereferenced the nil
session, so that is now t.Fatal.

Pre-existing at efabb677 rather than introduced by this range.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 16:34:25 -05:00

105 lines
3.7 KiB
Go

package db
import (
"testing"
"time"
)
// A ui_sessions row outlives the OAuth session it points at. Only one
// oauth_sessions row is kept per account, so signing in again — on a second
// device, or just a second time — replaces it and leaves every earlier
// ui_sessions row referencing an oauth_session_id that no longer exists.
//
// Observed on a live appview: four ui_sessions rows, three of them orphaned,
// and requesting /settings/user with an orphaned cookie returned 200 with the
// account's handle rendered throughout, rather than the 302 an anonymous
// request gets. The browser looks signed in while the credential behind it is
// gone, so anything needing the PDS token fails against a UI insisting the
// session is fine.
func insertOAuthSession(t *testing.T, s *SessionStore, did, sessionID string) {
t.Helper()
_, err := s.db.Exec(`
INSERT INTO oauth_sessions (session_key, account_did, session_id, session_data)
VALUES (?, ?, ?, ?)
`, did+":"+sessionID, did, sessionID, `{"account_did":"`+did+`"}`)
if err != nil {
t.Fatalf("insert oauth session: %v", err)
}
}
// The bug: an orphaned reference must not read back as a usable session.
func TestSessionStore_Get_RejectsOrphanedOAuthSession(t *testing.T) {
store := setupSessionTestDB(t)
const did = "did:plc:orphan"
createSessionTestUser(t, store, did, "orphan.test")
id, err := store.CreateWithOAuth(did, "orphan.test", "https://pds.example.com",
"oauth-session-that-is-gone", time.Hour)
if err != nil {
t.Fatalf("create session: %v", err)
}
if _, ok := store.Get(id); ok {
t.Fatal("Get returned a session whose OAuth session no longer exists; " +
"the browser would render as signed in with no usable credential")
}
}
// A session created without an OAuth session at all is legitimate — Create
// passes an empty oauthSessionID — and must keep working.
func TestSessionStore_Get_AcceptsSessionWithoutOAuthSession(t *testing.T) {
store := setupSessionTestDB(t)
const did = "did:plc:nooauth"
createSessionTestUser(t, store, did, "nooauth.test")
id, err := store.Create(did, "nooauth.test", "https://pds.example.com", time.Hour)
if err != nil {
t.Fatalf("create session: %v", err)
}
if _, ok := store.Get(id); !ok {
t.Fatal("Get rejected a session that never had an OAuth session; " +
"the orphan check must not catch these")
}
}
// The ordinary case stays working.
func TestSessionStore_Get_AcceptsLiveOAuthSession(t *testing.T) {
store := setupSessionTestDB(t)
const did = "did:plc:live"
createSessionTestUser(t, store, did, "live.test")
insertOAuthSession(t, store, did, "live-oauth-session")
id, err := store.CreateWithOAuth(did, "live.test", "https://pds.example.com",
"live-oauth-session", time.Hour)
if err != nil {
t.Fatalf("create session: %v", err)
}
if _, ok := store.Get(id); !ok {
t.Fatal("Get rejected a session backed by a live OAuth session")
}
}
// The reference is only meaningful for the account that owns it: oauth_sessions
// is unique on (account_did, session_id), so matching on session_id alone would
// let one account's live session validate another's dangling reference.
func TestSessionStore_Get_RejectsOAuthSessionBelongingToAnotherAccount(t *testing.T) {
store := setupSessionTestDB(t)
const owner, other = "did:plc:owner", "did:plc:other"
createSessionTestUser(t, store, owner, "owner.test")
createSessionTestUser(t, store, other, "other.test")
insertOAuthSession(t, store, other, "shared-session-id")
id, err := store.CreateWithOAuth(owner, "owner.test", "https://pds.example.com",
"shared-session-id", time.Hour)
if err != nil {
t.Fatalf("create session: %v", err)
}
if _, ok := store.Get(id); ok {
t.Fatal("Get accepted a session validated by another account's OAuth session")
}
}