Skip to content

feat: per-device remember-me tokens - #527

Merged
markshust merged 1 commit into
developfrom
feature/per-device-remember-tokens
Oct 6, 2026
Merged

markshust merged 1 commit into
developfrom
feature/per-device-remember-tokens

Conversation

@markshust

Copy link
Copy Markdown
Collaborator

Summary

  • Before this change each user had one remember token (remember_token + remember_token_expires_at). A remember-me login on device B rotated it, which logged device A out. RememberTokenStorageInterface existed, but nothing implemented it, so auth:clear-tokens had no storage to call.
  • RememberTokenStorageInterface now stores one token per device: store, findBySelector, rotateValidator (compare-and-swap on the stored hash), deleteBySelector, clearExpiredTokens and clearAllTokens. Tokens are passed around as a new RememberTokenRecord value object. RememberTokenManager gains generateSelector().
  • When a storage is bound, SessionGuard sets a selector:validator cookie and looks the token up by guard + selector. It checks the validator with hash_equals on the SHA-256 hash and rotates only the validator, keeping the fix!: expire remember-me tokens server-side and rotate session and csrf token on login and logout #503 fixed expiry. Expired rows and rows for deleted users are removed along with the cookie. A wrong validator (tampered cookie) is rejected and the row is kept. logout() deletes only the current device's row, and only if that row belongs to the logged-in user. The browser's User-Agent is saved with each row.
  • When no storage is bound, SessionGuard keeps using the existing single-column path. An old id|token cookie still works after storage is installed: on its next use the column is cleared and that device gets its own row, so the upgrade does not log remembered users out.
  • marko/admin-auth binds RememberTokenRepository over a new remember_tokens table, created by db:migrate from the RememberToken entity. The binding covers every session guard, and the guard column keeps each guard's tokens separate. auth:clear-tokens now purges expired rows through it. AdminUserProvider needed no changes, because device tokens resolve users via retrieveById().
  • AuthManager is now built by a factory in module.php ($container->has() makes the storage optional), and CurrentRequest is passed to session guards. The two fixture copies of the authentication module.php (testing http-app, roadrunner app) are updated to match.
  • Docs: new "Per-Device Tokens" and "Clearing Expired Tokens" sections and API reference entries in authentication.md. In admin-auth.md: the remember_tokens table, a "Remember-Me Tokens" section with upgrade steps, and the auth:clear-tokens command.

Upgrade notes

  • Apps with marko/admin-auth must run marko db:migrate to create remember_tokens before deploying. Remember-me logins write to it.
  • RememberTokenStorageInterface gained methods. Nothing implemented it before, so only out-of-tree implementations are affected.
  • The single-column path is kept, so this is not feat!. Apps without a storage binding behave exactly as before.

Test plan

  • SessionGuardDeviceRememberTest:
    • two devices stay logged in independently across repeated remember-me logins
    • a remember-me login on one device does not log another out
    • logout on one device leaves the other remembered
    • a tampered validator is rejected and the row kept
    • a replayed (already rotated) validator is rejected
    • an expired token is rejected and its row and cookie deleted
    • of two requests racing on the same cookie, exactly one logs in
    • tokens are scoped per guard
    • a planted cookie cannot revoke another user's device on logout
    • User-Agent is recorded
    • a legacy id|token cookie is migrated onto a device row
  • RememberTokenRepositoryTest (SQLite, real SQL):
    • store/find, guard scoping, CAS rotation, per-device delete
    • expired-row purge (including through auth:clear-tokens), clearAllTokens
    • end-to-end SessionGuard over the table: two devices, logout of one, tampered validator, expiry
  • ClearTokensCommandTest uses an in-memory storage and checks that expired rows are purged and live rows kept
  • RememberMeIntegrationTest: the booted container gives guards the bound storage, or null when none is bound
  • admin-auth MySQL/PgSQL FreshMigrateTest now expects 6 schema migrations, including remember_tokens. Not run locally (no integration services), so CI Integration will confirm.
  • composer test, composer cs:check, composer phpstan green locally

Fixes #513

🤖 Generated with Claude Code

https://claude.ai/code/session_01KyjR4cJTfro54iFZ9CWkv1

A single remember_token column per user meant a remember-me login on one
device rotated the token and logged every other device out.

RememberTokenStorageInterface is now a real per-device store (store,
findBySelector, compare-and-swap rotateValidator, deleteBySelector, plus
the existing clear methods). When it is bound, SessionGuard issues a
"selector:validator" cookie per device, looks tokens up by guard and
selector, compares the validator hash with hash_equals, rotates only the
validator (keeping the #503 fixed expiry), deletes only the current
device's row on logout, and records the User-Agent. Without a binding it
falls back to the single-column path, and an old "id|token" cookie is
moved onto its own device row on first use.

marko/admin-auth binds RememberTokenRepository over a new remember_tokens
table (RememberToken entity, created by db:migrate), so auth:clear-tokens
now has storage to purge expired rows from. AuthManager is built by a
module factory so the storage is optional.

Fixes #513

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KyjR4cJTfro54iFZ9CWkv1
@github-actions github-actions Bot added the enhancement New feature or request label Oct 6, 2026
@markshust
markshust merged commit dcb9dd3 into develop Oct 6, 2026
5 checks passed
@markshust
markshust deleted the feature/per-device-remember-tokens branch October 6, 2026 15:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Per-device remember-me tokens so one device's login doesn't invalidate another

1 participant