diff --git a/changelog.d/12952.feature b/changelog.d/12952.feature
new file mode 100644
index 0000000000..7329bcc3d4
--- /dev/null
+++ b/changelog.d/12952.feature
@@ -0,0 +1 @@
+Allow updating a user's password using the admin API without logging out their devices. Contributed by @jcgruenhage.
diff --git a/docs/admin_api/user_admin_api.md b/docs/admin_api/user_admin_api.md
index c8794299e7..62f89e8cba 100644
--- a/docs/admin_api/user_admin_api.md
+++ b/docs/admin_api/user_admin_api.md
@@ -115,7 +115,9 @@ URL parameters:
Body parameters:
- `password` - string, optional. If provided, the user's password is updated and all
- devices are logged out.
+ devices are logged out, unless `logout_devices` is set to `false`.
+- `logout_devices` - bool, optional, defaults to `true`. If set to false, devices aren't
+ logged out even when `password` is provided.
- `displayname` - string, optional, defaults to the value of `user_id`.
- `threepids` - array, optional, allows setting the third-party IDs (email, msisdn)
- `medium` - string. Kind of third-party ID, either `email` or `msisdn`.
diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py
index 8e29ada8a0..f0614a2897 100644
--- a/synapse/rest/admin/users.py
+++ b/synapse/rest/admin/users.py
@@ -226,6 +226,13 @@ class UserRestServletV2(RestServlet):
if not isinstance(password, str) or len(password) > 512:
raise SynapseError(HTTPStatus.BAD_REQUEST, "Invalid password")
+ logout_devices = body.get("logout_devices", True)
+ if not isinstance(logout_devices, bool):
+ raise SynapseError(
+ HTTPStatus.BAD_REQUEST,
+ "'logout_devices' parameter is not of type boolean",
+ )
+
deactivate = body.get("deactivated", False)
if not isinstance(deactivate, bool):
raise SynapseError(
@@ -305,7 +312,6 @@ class UserRestServletV2(RestServlet):
await self.store.set_server_admin(target_user, set_admin_to)
if password is not None:
- logout_devices = True
new_password_hash = await self.auth_handler.hash(password)
await self.set_password_handler.set_password(
|