Regression when login after deactivation (no more `M_USER_DEACTIVATED` code)
This issue has been migrated from #15747.
Android CI is failing when running deactivate account test
With previous synapse version, the test was getting a M_USER_DEACTIVATED error, and now (since 1.85.0?) the error is:
{"errcode":"M_FORBIDDEN","error":"Invalid username or password"}
Is it expected?
This is still the case with Synapse 1.129.0+lts.6 and 1.142.0. Reported by a TIM customer using our Helm charts and validated by me with Podman and Postgres 17. Using Synapse images from registry.element.io and GitHub.
The Spec version 1.11 and latest at time of writing, 1.16 both indicate that logging in can fail with a 403 response and one of M_FORBIDDEN or M_USER_DEACTIVATED error codes. However, 403 - M_FORBIDDEN is returned for both versions I tested with.
$ curl --request GET \
--url http://synapse.matrix.local/_matrix/federation/v1/version
{"server":{"name":"Synapse","version":"1.129.0+lts.6"}}%
$ curl --request POST \
--url http://synapse.matrix.local/_matrix/client/r0/login \
--header 'content-type: application/json' \
--data '{
"type": "m.login.password",
"identifier": {
"type": "m.id.user",
"user": "test1"
},
"password": "123",
"initial_device_display_name": "test1"
}'
{"user_id":"@test1:matrix.local","access_token":"syt_dGVzdDE_OKvtIowFFTLKBuocyyCW_3jJRLN","home_server":"matrix.local","device_id":"CRIKOFBDUV"}%
$ curl --request POST \
--url http://synapse.matrix.local/_synapse/admin/v1/deactivate/@test1:matrix.local \
--header 'Authorization: Bearer syt_YWRtaW4_ZMlVsOlHwargSrJYGEdb_3mnyuj' \
--header 'content-type: application/json' \
--data '{
"erase": true
}'
{"id_server_unbind_result":"success"}%
$ curl --request POST \
--url http://synapse.matrix.local/_matrix/client/r0/login \
--header 'content-type: application/json' \
--data '{
"type": "m.login.password",
"identifier": {
"type": "m.id.user",
"user": "test1"
},
"password": "123",
"initial_device_display_name": "test1"
}'
{"errcode":"M_FORBIDDEN","error":"Invalid username or password"}%
$ curl --request GET \
--url http://synapse.matrix.local/_matrix/federation/v1/version
{"server":{"name":"Synapse","version":"1.142.0"}}%
$ curl --request POST \
--url http://synapse.matrix.local/_matrix/client/r0/login \
--header 'content-type: application/json' \
--data '{
"type": "m.login.password",
"identifier": {
"type": "m.id.user",
"user": "test1"
},
"password": "123",
"initial_device_display_name": "test1"
}'
{"user_id":"@test1:matrix.local","access_token":"syt_dGVzdDE_kzmvSNNAEyNkOzFntNhJ_4MYUaD","home_server":"matrix.local","device_id":"KIEOGKEFJE"}%
$ curl --request POST \
--url http://synapse.matrix.local/_synapse/admin/v1/deactivate/@test1:matrix.local \
--header 'Authorization: Bearer syt_YWRtaW4_ZMlVsOlHwargSrJYGEdb_3mnyuj' \
--header 'content-type: application/json' \
--data '{
"erase": true
}'
{"id_server_unbind_result":"success"}%
$ curl --request POST \
--url http://synapse.matrix.local/_matrix/client/r0/login \
--header 'content-type: application/json' \
--data '{
"type": "m.login.password",
"identifier": {
"type": "m.id.user",
"user": "test1"
},
"password": "123",
"initial_device_display_name": "test1"
}'
{"errcode":"M_FORBIDDEN","error":"Invalid username or password"}%
2025-11-11 16:11:07,284 - synapse.rest.client.login - 347 - INFO - POST-29 - Got login request with identifier: {'type': 'm.id.user', 'user': 'test1'}, medium: None, address: None, user: None
2025-11-11 16:11:07,285 - synapse.handlers.auth - 1435 - WARNING - POST-29 - Failed password login for user @test1:matrix.local
2025-11-11 16:11:07,285 - synapse.http.server - 132 - INFO - POST-29 - <XForwardedForRequest at 0x7fbaa5909a90 method='POST' uri='/_matrix/client/r0/login' clientproto='HTTP/1.1' site='8448'> SynapseError: 403 - Invalid username or password
2025-11-11 16:11:07,286 - synapse.access.http.8448 - 521 - INFO - POST-29 - 10.89.0.10 - 8448 - {None} Processed request: 0.002sec/0.000sec (0.000sec, 0.000sec) (0.000sec/0.001sec/1) 64B 403 "POST /_matrix/client/r0/login HTTP/1.1" "curl/8.14.1" [0 dbevts]
So I suspect here we need to adjust the spec if it expects a M_USER_DEACTIVATED. Ultimately I see two reasons why we prefer returning a M_FORBIDDEN:
- We don't want to allow enumeration of usernames, even deactivated ones on a server.
- We don't want to retain the password hashes to prevent the above ^.
I think perhaps the one case where we might still want a M_USER_DEACTIVATED is on delegated auth success, but the account has been removed from Synapse.
We now wipe password hashes upon deactivation, which means we can no longer validate the user’s password upon login, meaning they get a M_FORBIDDEN.
The below code is thus unreachable, and should be updated/removed:
https://github.com/element-hq/synapse/blob/408a05ebbc70f94cb31e29ebac2d24df8363dfd6/synapse/rest/client/login.py#L425-L427
(Maybe the code above still has its place, for SSO users or if the password hash does still exist for some reason [e.g. for users before we started wiping] and it does validate)
@reivilibre Good point. We should at least leave a comment explaining that due to user's passwords being wiped, it's expected that most users will not receive this error, and this is purely left as a guard.
We could also wipe all historical, deactivated local user passwords to align the behaviour for deactivated users, if ever desired.