Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions synapse/handlers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -1485,6 +1485,42 @@ async def consume_login_token(self, login_token: str) -> LoginTokenLookupResult:

raise AuthError(403, "Invalid login token", errcode=Codes.FORBIDDEN)

async def expire_access_token(self, access_token: str) -> None:
"""Invalidate a single access token and associated refresh tokens, putting the user in the soft logout state.

Args:
access_token: access token to be deleted

"""
token = await self.store.get_user_by_access_token(access_token)
if not token:
# At this point, the token should already have been fetched once by
# the caller, so this should not happen, unless of a race condition
# between two delete requests
raise SynapseError(HTTPStatus.UNAUTHORIZED, "Unrecognised access token")

await self.store.expire_access_token(access_token)

if token.device_id is not None:
await self.store.expire_refresh_tokens_for_device(
user_id=token.user_id, device_id=token.device_id
)

# see if any modules want to know about this
await self.password_auth_provider.on_logged_out(
user_id=token.user_id,
device_id=token.device_id,
access_token=access_token,
)

# delete pushers associated with this access token
# XXX(quenting): This is only needed until the 'set_device_id_for_pushers'
# background update completes.
if token.token_id is not None:
await self.hs.get_pusherpool().remove_pushers_by_access_tokens(
token.user_id, (token.token_id,)
)

async def delete_access_token(self, access_token: str) -> None:
"""Invalidate a single access token

Expand Down
10 changes: 8 additions & 2 deletions synapse/rest/client/logout.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

from synapse.handlers.device import DeviceHandler
from synapse.http.server import HttpServer
from synapse.http.servlet import RestServlet
from synapse.http.servlet import RestServlet, parse_json_object_from_request
from synapse.http.site import SynapseRequest
from synapse.rest.client._base import client_patterns
from synapse.types import JsonDict
Expand All @@ -51,7 +51,13 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
request, allow_expired=True, allow_locked=True
)

if requester.device_id is None:
body = parse_json_object_from_request(request, allow_empty_body=True)

if body.get("com.famedly.soft_logout", False):
access_token = self.auth.get_access_token_from_request(request)
# For soft_logout, just expire the tokens, don't delete them.
await self._auth_handler.expire_access_token(access_token)
elif requester.device_id is None:
# The access token wasn't associated with a device.
# Just delete the access token
access_token = self.auth.get_access_token_from_request(request)
Expand Down
43 changes: 43 additions & 0 deletions synapse/storage/databases/main/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -1437,6 +1437,30 @@ async def update_access_token_last_validated(self, token_id: int) -> None:
desc="update_access_token_last_validated",
)

async def expire_access_token(self, access_token: str) -> None:
"""Updates the valid_until_ms for the access token to be expired.

Args:
token_id: The ID of the access token to update.
Raises:
StoreError if there was a problem updating this.
"""
now = self._clock.time_msec() - 100

def f(txn: LoggingTransaction) -> None:
self.db_pool.simple_update_one_txn(
txn,
"access_tokens",
{"token": access_token},
{"valid_until_ms": now},
)

self._invalidate_cache_and_stream(
txn, self.get_user_by_access_token, (access_token,)
)

await self.db_pool.runInteraction("expire_access_token", f)

async def registration_token_is_valid(self, token: str) -> bool:
"""Checks if a token can be used to authenticate a registration.

Expand Down Expand Up @@ -1917,6 +1941,25 @@ def _replace_refresh_token_txn(txn: LoggingTransaction) -> None:
"replace_refresh_token", _replace_refresh_token_txn
)

async def expire_refresh_tokens_for_device(
self, user_id: str, device_id: str
) -> None:
"""
Expires all refresh tokens for a specific user and device.

Args:
user_id: the ID of the user owning the refresh token
device_id: the id of the device owning the refresh token
"""

now = self._clock.time_msec() - 100
await self.db_pool.simple_update(
"refresh_tokens",
{"user_id": user_id, "device_id": device_id},
{"expiry_ts": now},
"expire_refresh_tokens",
)

async def add_login_token_to_user(
self,
user_id: str,
Expand Down
83 changes: 83 additions & 0 deletions tests/rest/client/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -1064,6 +1064,89 @@ def use_custom_refresh_token(refresh_token: str) -> FakeChannel:
refresh_response.code, HTTPStatus.FORBIDDEN, refresh_response.result
)

def test_explicit_soft_logout(self) -> None:
"""
Doing an explicit soft logout should invalidate all tokens.
"""

body = {
"type": "m.login.password",
"user": "test",
"password": self.user_pass,
"refresh_token": True,
}
login_response = self.make_request(
"POST",
"/_matrix/client/v3/login",
body,
)
self.assertEqual(login_response.code, HTTPStatus.OK, login_response.result)
refresh_token = login_response.json_body["refresh_token"]
access_token = login_response.json_body["access_token"]
device_id = login_response.json_body["device_id"]

# access token should be valid
whoami_response = self.make_request(
"GET",
"/_matrix/client/v3/account/whoami",
access_token=access_token,
)
self.assertEqual(whoami_response.code, HTTPStatus.OK, whoami_response.result)

# Do a soft logout
logout_response = self.make_request(
"POST",
"/_matrix/client/v3/logout",
{
"com.famedly.soft_logout": True,
},
access_token=access_token,
)
self.assertEqual(logout_response.code, HTTPStatus.OK, logout_response.result)

# Assert that we can't refresh the token anymore
refresh_response = self.use_refresh_token(refresh_token)
self.assertEqual(
refresh_response.code, HTTPStatus.FORBIDDEN, refresh_response.result
)

# access token should be invalid
whoami_response = self.make_request(
"GET",
"/_matrix/client/v3/account/whoami",
access_token=access_token,
)
self.assertEqual(
whoami_response.code, HTTPStatus.UNAUTHORIZED, whoami_response.result
)
self.assertEqual(
whoami_response.json_body["soft_logout"], True, whoami_response.result
)

# Log back into the existing session
body = {
"type": "m.login.password",
"user": "test",
"password": self.user_pass,
"refresh_token": True,
"device_id": device_id,
}
login_response = self.make_request(
"POST",
"/_matrix/client/v3/login",
body,
)
self.assertEqual(login_response.code, HTTPStatus.OK, login_response.result)
access_token = login_response.json_body["access_token"]

# access token should be valid again
whoami_response = self.make_request(
"GET",
"/_matrix/client/v3/account/whoami",
access_token=access_token,
)
self.assertEqual(whoami_response.code, HTTPStatus.OK, whoami_response.result)

@override_config(
{
"refreshable_access_token_lifetime": "2m",
Expand Down
Loading