Repository navigation
feat: per-device remember-me tokens - #527
Merged
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
remember_token+remember_token_expires_at). A remember-me login on device B rotated it, which logged device A out.RememberTokenStorageInterfaceexisted, but nothing implemented it, soauth:clear-tokenshad no storage to call.RememberTokenStorageInterfacenow stores one token per device:store,findBySelector,rotateValidator(compare-and-swap on the stored hash),deleteBySelector,clearExpiredTokensandclearAllTokens. Tokens are passed around as a newRememberTokenRecordvalue object.RememberTokenManagergainsgenerateSelector().SessionGuardsets aselector:validatorcookie and looks the token up by guard + selector. It checks the validator withhash_equalson 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.SessionGuardkeeps using the existing single-column path. An oldid|tokencookie 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-authbindsRememberTokenRepositoryover a newremember_tokenstable, created bydb:migratefrom theRememberTokenentity. The binding covers every session guard, and theguardcolumn keeps each guard's tokens separate.auth:clear-tokensnow purges expired rows through it.AdminUserProviderneeded no changes, because device tokens resolve users viaretrieveById().AuthManageris now built by a factory inmodule.php($container->has()makes the storage optional), andCurrentRequestis passed to session guards. The two fixture copies of the authenticationmodule.php(testing http-app, roadrunner app) are updated to match.authentication.md. Inadmin-auth.md: theremember_tokenstable, a "Remember-Me Tokens" section with upgrade steps, and theauth:clear-tokenscommand.Upgrade notes
marko/admin-authmust runmarko db:migrateto createremember_tokensbefore deploying. Remember-me logins write to it.RememberTokenStorageInterfacegained methods. Nothing implemented it before, so only out-of-tree implementations are affected.feat!. Apps without a storage binding behave exactly as before.Test plan
SessionGuardDeviceRememberTest:id|tokencookie is migrated onto a device rowRememberTokenRepositoryTest(SQLite, real SQL):auth:clear-tokens),clearAllTokensSessionGuardover the table: two devices, logout of one, tampered validator, expiryClearTokensCommandTestuses an in-memory storage and checks that expired rows are purged and live rows keptRememberMeIntegrationTest: the booted container gives guards the bound storage, or null when none is boundFreshMigrateTestnow expects 6 schema migrations, includingremember_tokens. Not run locally (no integration services), so CIIntegrationwill confirm.composer test,composer cs:check,composer phpstangreen locallyFixes #513
🤖 Generated with Claude Code
https://claude.ai/code/session_01KyjR4cJTfro54iFZ9CWkv1