Skip to content

Enhancement: Multi-device login — 3 concurrent sessions with LRU eviction - #491

Open
baltinerdist wants to merge 13 commits into
amtgard:masterfrom
baltinerdist:feature/multi-device-login
Open

Enhancement: Multi-device login — 3 concurrent sessions with LRU eviction#491
baltinerdist wants to merge 13 commits into
amtgard:masterfrom
baltinerdist:feature/multi-device-login

Conversation

@baltinerdist

@baltinerdist baltinerdist commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Replaces ORK3's single-device login (one ork_mundane.token slot — a new login silently kicked the previous device) with a multi-session model backed by a new ork_session table: up to 3 concurrent sessions per user, with least-recently-active (LRU) eviction when a 4th device logs in.

All login entry points create sessions; the per-request check and the JSON-API authorizer validate tokens against ork_session on every request; logout tears down its own session. ork_mundane.token is retained as a dual-written vestigial pointer so the change is revertible in one file.

What changed

  • New ork_session table (db-migrations/2026-07-13-add-ork-session-table.sql) — one row per live token; unique token, indexed on mundane_id/expires.
  • All login paths (password, IDP/OAuth, self-registration) mint a session via Authorization::CreateSession() — cap 3, LRU eviction, and a verified INSERT (retries once, logs, and fails the login rather than returning a token that can't authenticate).
  • Per-request validation (class.Controller.php) and JSON-API auth (IsAuthorized_h) now validate against ork_session on every request (the prior PHP-session auth cache no longer short-circuits), so eviction/expiry takes effect immediately — including on AJAX endpoints.
  • Server-side session expiry is now enforced (72h, slid forward on activity, throttled to ≤1 write/60s; previously effectively cookie-only).
  • Logout deletes its ork_session row and clears the vestigial pointer to prevent replay.

Deploy

Run the migration once:

docker exec -i <db-container> mariadb -u root -p<pw> ork < db-migrations/2026-07-13-add-ork-session-table.sql

The migration backfills currently-active tokens (unexpired only), so existing logged-in users are not kicked on deploy. It is idempotent (INSERT IGNORE).

Testing

No unit-test harness exists in this repo; verified via curl + mariadb integration against the local Docker app:

  • 4-device login → exactly 3 sessions survive, oldest device kicked (session_replaced)
  • expired session rejected; valid AJAX still authorizes; logout frees a slot
  • a 6-lens review + an adversarial critic pass hardened the session helpers (always-revalidate, verified INSERT with retry/log, CSPRNG token rotation, DRY of the vestigial-token write) and caught/fixed a danger-audit actor-attribution regression

Coverage

All auth entry points are multi-device: password + IDP/OAuth + self-registration (fresh login), the per-request/JSON-API validation, logout, and the SOAP/mobile token-refresh path (Authorize_h token branch, which now validates against ork_session instead of the single ork_mundane.token). ork_mundane.token remains only as a vestigial rollback pointer.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DXQ2YePHm59VSzozsxbNXJ

baltinerdist and others added 13 commits July 13, 2026 11:23
…rotate/destroy + LRU evict)

Adds CreateSession/ValidateSessionByToken/RotateSession/DestroySession
plus private evictLruSessions and MAX_SESSIONS_PER_USER=3 to
Authorization, backing the new ork_session table (Task 1). No existing
call sites rewired yet (Task 3+).

Fixed a bug in the task brief's verbatim code: DataSet() returns a
YapoResultSet, so Next()/field reads must happen on that returned
object, not on $DB directly (confirmed via class.YapoResultSet.php
and existing call-site convention, e.g. class.StateOfAmtgard.php).

Committed via the sanctioned --no-verify procedure (user-approved
2026-06-03, see feedback_authorization_skip_commit.md): the repo's
pre-commit hook unconditionally strips class.Authorization.php from
every commit regardless of what's staged, so the local-only true||
login bypass was removed first, the file staged in full and verified
0 occurrences of 'true ||', then committed with --no-verify. The
bypass is restored immediately after this commit.
Replaces the raw ork_mundane.token comparison with
ValidateSessionByToken() so the stale-session check works against
the new multi-device ork_session table. Also drops a dead trailing
$DB->Clear() call left without a global $DB in scope after removing
the raw query, which was fatal-erroring every authenticated request.
Fresh login now calls CreateSession() instead of overwriting the
single ork_mundane.token column, so multiple devices can hold live
sessions concurrently (LRU-evicted beyond the 3-session cap). The
token-refresh branch rotates the existing session via RotateSession()
(falling back to CreateSession() if the old token was already
evicted/expired), and IsAuthorized_h() now validates against
ork_session via ValidateSessionByToken() instead of the mundane
token column. mundane.token/token_expires are kept in sync as a
vestigial "last token" pointer only.
Route the IDP/OAuth login flow through CreateSession (Task 2) in both
idpAuthorize() and createIdpLink(), so "Login with Amtgard" sessions get
the same 3-concurrent-device cap and LRU eviction as password login
(Task 3). ork_mundane.token remains a vestigial pointer only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXQ2YePHm59VSzozsxbNXJ
Today logout() only cleared the PHP session while the DB session row
lingered, allowing the token to be replayed. Now logout deletes the
current device's ork_session row before clearing session vars, via
Authorization::DestroySession, so the slot frees up immediately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXQ2YePHm59VSzozsxbNXJ
…cache removal; standardize self-reg token_expires to Y-m-d

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXQ2YePHm59VSzozsxbNXJ
…, CSPRNG rotate, DRY)

- IsAuthorized_h no longer short-circuits on the PHP-session cache, so an
  LRU-evicted/expired token is rejected on every request (incl. skip-listed
  AJAX controllers), not just on full page loads.
- CreateSession verifies the INSERT landed, retries once, logs + returns ''
  on hard failure; all login callers treat '' as a login failure.
- RotateSession now mints the new token from the CSPRNG (was derived from the
  old token).
- DRY the vestigial-token write into persistVestigialToken(); standardize
  token_expires to Y-m-d. TOKEN_LENGTH constant; evict/observability logtrace.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXQ2YePHm59VSzozsxbNXJ
…(multi-device)

The Authorize_h token-refresh branch keyed on the single ork_mundane.token
column, so only the most-recently-logged-in device could re-auth. It now
validates via ValidateSessionByToken against ork_session, so any of a user's
up-to-3 live device tokens re-authenticates — completing multi-device parity
for SOAP/mobile clients. Expiry is enforced per-session (ork_session.expires)
instead of the single token_expires column.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXQ2YePHm59VSzozsxbNXJ
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant