Repository navigation
Conversation
A login token (the JWT set as the login cookie) is only checked against the user's stored login token id, and logout never changed that value, so a copy of the cookie taken before logout kept working until it expired. Logout now gives the user a new login token id before the session is invalidated, so every login token issued to that user earlier is rejected. The value is written with a single-column update and the user is evicted from the cache, so other nodes in a cluster read the new value and a concurrent save of other user fields is not overwritten. A failure is logged and does not stop the logout. Logging out therefore ends token-based access for that user on every device; API tokens are not affected.
…nges Changing a password did not change the user's login token id, so login tokens issued with the old password kept working until they expired. Setting a new password now also gives the user a new login token id, as part of the same save, on these paths: - UserAPI.save with validatePassword, used by the legacy user edit screen and the current-user edit (PUT /api/v1/users/current). - The admin user edit endpoint (PUT /api/v1/users) when the request carries a password. - UserAPI.updatePassword, used when a password reset is completed. A save that does not change the password leaves the login token id alone, so profile edits and the save done on each login do not end the user's sessions.
…hout a session Logout now picks the user whose login token id it changes as follows. While an administrator is logged in as another user, the session's user is the impersonated one, but the login cookie the browser holds belongs to the administrator. Logout now changes the administrator's login token id and leaves the impersonated user's alone. Ending Login As (returning to the administrator) still changes nothing. When the request has no session user, for example /dotAdmin/logout after the HTTP session has expired, logout now reads the login token sent in the login cookie or as a Bearer header and changes that user's login token id. Only a token that passes the usual validation (signature, expiry and current token id) is used, so a token that does not validate changes nothing.
|
Claude finished @swicken's task in 52s —— View job SDK Compatibility Analysis
Verdict: No SDK breaking change. All 12 changed files are backend login/session-token code and its integration test:
None of these touch the categories in the reference doc:
A confirming grep across the full diff for graphql/nav/UVE/postMessage/SdkVersion/sdk-compat/page-api/navigation-api paths returned zero matches. No comment or label added, per instructions (only required when a breaking change is found). |
…inTokens Every change to a user's login token id (user_.skinid) now goes through UserAPI.invalidateLoginTokens and UserFactory.updateRememberMeToken: logout, the three password-change paths, the first value set on login, and the two revoke endpoints in ApiTokenResource. The password paths call it after the save, in the same transaction, and invalidateLoginTokens returns the new value so callers can keep the User they hold current. The general user update in UserFactoryImpl.save no longer writes skinid, so saving a User loaded before a logout or password change cannot write the old value back. The insert of a new user still writes it. updateRememberMeToken normalizes the user id once and uses it for both the update and the cache eviction, and a blank id now throws DotDataException instead of a NullPointerException. The Javadoc explains why a random UUID works: a login JWT is only accepted while its jti matches the stored value.
…cy updates updateRememberMeToken evicted the user from the cache while the surrounding transaction was still open. Until the commit, a request on any node could read the committed old login token id and cache it again, and nothing evicted that entry afterwards. It now also evicts the user from a commit listener; with no transaction open the listener runs right away. The legacy Liferay update (UserPersistence.update) wrote skinid from the in-memory User when no tracked field had changed, so a User loaded before a logout could write the replaced value back. That branch now keeps the stored value. Changes to tracked fields still rotate it to a fresh UUID.
|
Thanks for the review. 3b3725b covers the points:
|
dotCMS-Machine-User
left a comment
There was a problem hiding this comment.
✅ dotbot review: all reviewer models (meta/muse-spark-1.3, ~z-ai/glm-latest) agree — patch is correct.
approved automatically by dotbot
# Conflicts: # dotcms-integration/src/test/java/com/dotcms/MainSuite2a.java
|
dotbot code review:
Logout, password-change, revoke, and first-login paths rotate the stored login token id via single-column update with cache eviction, and general saves no longer restore stale values. No introduced P0/P1 defect with concrete in-repo evidence was found. Tip: comment with "/dotbot address comments" to attempt automated fixes for unresolved review threads. reviewed by dotbot · meta/muse-spark-1.3 · medium |
|
dotbot code review:
All token-id rotation paths funnel through UserAPI.invalidateLoginTokens → UserFactory.updateRememberMeToken, which writes only the skinid column, normalizes the id once for both the SQL parameter and cache eviction, and evicts both immediately and after transaction commit. The general UserFactoryImpl update and the legacy UserPersistence update no longer write this column, so a stale User save cannot resurrect an old token id. Logout resolves the user before session invalidation (Login As principal, session user, then a validated rme/Bearer login JWT, excluding API tokens), and the password paths rotate only when a password is present; the previously flagged cache-key normalization issue is fixed in the current code. No P0/P1 defect introduced by this patch was identified. Tip: comment with "/dotbot address comments" to attempt automated fixes for unresolved review threads. reviewed by dotbot · ~z-ai/glm-latest · medium |
dotCMS-Machine-User
left a comment
There was a problem hiding this comment.
✅ dotbot review: all reviewer models (meta/muse-spark-1.3, ~z-ai/glm-latest) agree — patch is correct.
approved automatically by dotbot
…ending to the end of MainSuite2a
Proposed Changes
A back-end login issues a login token (the
rmecookie, which is also accepted as a Bearer token). The token is valid as long as its id matches the user's storedrememberMeToken. Until now, logging out or changing the password did not change that value, so a login token issued before the logout or password change kept working until it expired. This PR gives the user a newrememberMeTokenon logout and on every password change, so every login token issued before then stops working.Fixes dotCMS/private-issues#960
Logout (7ef7118)
UserFactory.updateRememberMeToken(userId, value)updates the one column (update user_ set skinid = ? where userid = ?) and then evicts the user fromUserCache. Writing a single column means the change cannot overwrite a concurrent change to other user fields, which a fullUserAPI.savecould.UserAPI.invalidateLoginTokens(userId)(@WrapInTransaction) wraps it, andLazyUserAPIWrapperdelegates it.LoginServiceImplcalls it first indoActionLogoutand in thedoLogoutoverride. That covers REST/api/v1/logout, the UI's/dotAdmin/logout, OAuth logout, invalid-token cleanup,CookiesFilterand SAML logout. Any failure is logged at warn, and the logout carries on.Password change (24605d0)
UserAPIImpl.save(..., validatePassword=true, ...)andUserAPIImpl.updatePasswordset a newrememberMeTokenon the user object they already save. So there is no extra write, and nothing for a later save to overwrite. That covers the legacy user editor and current-user edit (UserAPI.savewithvalidatePassword=true) and reset-password completion (ResetPasswordResourcetoUserManagerImpl.resetPasswordtoupdatePassword).UserResource.updateUser(PUT /api/v1/users) does the same when the form carries a password.Login As and logout without a session (97f41ac)
rmecookie, then to a Bearer value. It validates each one withApiTokenAPI.fromJwt(signature, expiry, currentrememberMeToken, active user) and accepts only a login token. A token that fails validation, or an API token, changes nothing. This covers a logout from a browser whose session has already timed out.Behavior change
Logout now ends token-based access for that user everywhere, like "log out of all devices". Other browsers' live HTTP sessions keep working until they time out, but can no longer be revived from their old cookie. Scripts that use a login token as a Bearer token stop working when that user logs out in the UI or changes their password. API tokens are not affected, and remain the supported way to authenticate scripts. When the OAuth callback fails in a browser that already had a session, the cleanup now ends that user's other login tokens too.
Rollback is safe: there is no schema change, and an older build simply keeps validating against whatever value is stored.
Checklist
Tests
LoginTokenInvalidationTest(new, registered inMainSuite2a), 9 tests, 0 failures:updatePassword,savewith a new password, orPUT /api/v1/userswith a password is rejected afterwards. A fresh login token works.savewithout a password change keeps the token.rmecookie and no session changes the value. A cookie with an altered signature or an old token id changes nothing.LoginServiceAPI.doLogout(the SAML path) changes the value.On the code before this PR, the logout,
updatePassword, save-with-new-password, Login As,PUT /api/v1/usersand cookie-only tests fail on their "must be rejected" assertions.MainSuite2ais the shortest MainSuite: its median job time over four recent merged PRs is 22.7 min, against 39.5 to 45.5 min for the others.These were also checked by hand against 26.10.23-02 with the changed classes swapped in. After
GET /api/v1/logout, the oldrmecookie, the full pre-logout cookie set, and the token as Bearer all got 401; before the change they got 200. With two browsers sharing a login token, a UI logout in one makes that token stop working in both, while the other browser's live session continues.Additional Info
UserDataGen.removethrows an NPE ("replacementUserRole is null") in the test's cleanup on a fresh test database, so the users the test creates are not deleted. This happens on main too. It is logged and does not affect the tests.skinidfrom the general user update SQL inUserFactoryImpl.save, and send every change of this value throughupdateRememberMeToken. Then a full save of a stale user object on another node cannot write an old value back.