Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 8756d5c

Browse files
authored
Save login tokens in database (#13844)
* Save login tokens in database Signed-off-by: Quentin Gliech <[email protected]> * Add upgrade notes * Track login token reuse in a Prometheus metric Signed-off-by: Quentin Gliech <[email protected]>
1 parent d902181 commit 8756d5c

File tree

11 files changed

+337
-227
lines changed

11 files changed

+337
-227
lines changed

changelog.d/13844.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Save login tokens in database and prevent login token reuse.

docs/upgrade.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,15 @@ process, for example:
8888
dpkg -i matrix-synapse-py3_1.3.0+stretch1_amd64.deb
8989
```
9090
91+
# Upgrading to v1.71.0
92+
93+
## Removal of the `generate_short_term_login_token` module API method
94+
95+
As announced with the release of [Synapse 1.69.0](#deprecation-of-the-generate_short_term_login_token-module-api-method), the deprecated `generate_short_term_login_token` module method has been removed.
96+
97+
Modules relying on it can instead use the `create_login_token` method.
98+
99+
91100
# Upgrading to v1.69.0
92101
93102
## Changes to the receipts replication streams

synapse/handlers/auth.py

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import attr
3939
import bcrypt
4040
import unpaddedbase64
41+
from prometheus_client import Counter
4142

4243
from twisted.internet.defer import CancelledError
4344
from twisted.web.server import Request
@@ -48,6 +49,7 @@
4849
Codes,
4950
InteractiveAuthIncompleteError,
5051
LoginError,
52+
NotFoundError,
5153
StoreError,
5254
SynapseError,
5355
UserDeactivatedError,
@@ -63,10 +65,14 @@
6365
from synapse.http.site import SynapseRequest
6466
from synapse.logging.context import defer_to_thread
6567
from synapse.metrics.background_process_metrics import run_as_background_process
68+
from synapse.storage.databases.main.registration import (
69+
LoginTokenExpired,
70+
LoginTokenLookupResult,
71+
LoginTokenReused,
72+
)
6673
from synapse.types import JsonDict, Requester, UserID
6774
from synapse.util import stringutils as stringutils
6875
from synapse.util.async_helpers import delay_cancellation, maybe_awaitable
69-
from synapse.util.macaroons import LoginTokenAttributes
7076
from synapse.util.msisdn import phone_number_to_msisdn
7177
from synapse.util.stringutils import base62_encode
7278
from synapse.util.threepids import canonicalise_email
@@ -80,6 +86,12 @@
8086

8187
INVALID_USERNAME_OR_PASSWORD = "Invalid username or password"
8288

89+
invalid_login_token_counter = Counter(
90+
"synapse_user_login_invalid_login_tokens",
91+
"Counts the number of rejected m.login.token on /login",
92+
["reason"],
93+
)
94+
8395

8496
def convert_client_dict_legacy_fields_to_identifier(
8597
submission: JsonDict,
@@ -883,6 +895,25 @@ def _verify_refresh_token(self, token: str) -> bool:
883895

884896
return True
885897

898+
async def create_login_token_for_user_id(
899+
self,
900+
user_id: str,
901+
duration_ms: int = (2 * 60 * 1000),
902+
auth_provider_id: Optional[str] = None,
903+
auth_provider_session_id: Optional[str] = None,
904+
) -> str:
905+
login_token = self.generate_login_token()
906+
now = self._clock.time_msec()
907+
expiry_ts = now + duration_ms
908+
await self.store.add_login_token_to_user(
909+
user_id=user_id,
910+
token=login_token,
911+
expiry_ts=expiry_ts,
912+
auth_provider_id=auth_provider_id,
913+
auth_provider_session_id=auth_provider_session_id,
914+
)
915+
return login_token
916+
886917
async def create_refresh_token_for_user_id(
887918
self,
888919
user_id: str,
@@ -1401,6 +1432,18 @@ async def _check_local_password(self, user_id: str, password: str) -> Optional[s
14011432
return None
14021433
return user_id
14031434

1435+
def generate_login_token(self) -> str:
1436+
"""Generates an opaque string, for use as an short-term login token"""
1437+
1438+
# we use the following format for access tokens:
1439+
# syl_<random string>_<base62 crc check>
1440+
1441+
random_string = stringutils.random_string(20)
1442+
base = f"syl_{random_string}"
1443+
1444+
crc = base62_encode(crc32(base.encode("ascii")), minwidth=6)
1445+
return f"{base}_{crc}"
1446+
14041447
def generate_access_token(self, for_user: UserID) -> str:
14051448
"""Generates an opaque string, for use as an access token"""
14061449

@@ -1427,16 +1470,17 @@ def generate_refresh_token(self, for_user: UserID) -> str:
14271470
crc = base62_encode(crc32(base.encode("ascii")), minwidth=6)
14281471
return f"{base}_{crc}"
14291472

1430-
async def validate_short_term_login_token(
1431-
self, login_token: str
1432-
) -> LoginTokenAttributes:
1473+
async def consume_login_token(self, login_token: str) -> LoginTokenLookupResult:
14331474
try:
1434-
res = self.macaroon_gen.verify_short_term_login_token(login_token)
1435-
except Exception:
1436-
raise AuthError(403, "Invalid login token", errcode=Codes.FORBIDDEN)
1475+
return await self.store.consume_login_token(login_token)
1476+
except LoginTokenExpired:
1477+
invalid_login_token_counter.labels("expired").inc()
1478+
except LoginTokenReused:
1479+
invalid_login_token_counter.labels("reused").inc()
1480+
except NotFoundError:
1481+
invalid_login_token_counter.labels("not found").inc()
14371482

1438-
await self.auth_blocking.check_auth_blocking(res.user_id)
1439-
return res
1483+
raise AuthError(403, "Invalid login token", errcode=Codes.FORBIDDEN)
14401484

14411485
async def delete_access_token(self, access_token: str) -> None:
14421486
"""Invalidate a single access token
@@ -1711,7 +1755,7 @@ async def complete_sso_login(
17111755
)
17121756

17131757
# Create a login token
1714-
login_token = self.macaroon_gen.generate_short_term_login_token(
1758+
login_token = await self.create_login_token_for_user_id(
17151759
registered_user_id,
17161760
auth_provider_id=auth_provider_id,
17171761
auth_provider_session_id=auth_provider_session_id,

synapse/module_api/__init__.py

Lines changed: 1 addition & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -771,50 +771,11 @@ async def create_login_token(
771771
auth_provider_session_id: The session ID got during login from the SSO IdP,
772772
if any.
773773
"""
774-
# The deprecated `generate_short_term_login_token` method defaulted to an empty
775-
# string for the `auth_provider_id` because of how the underlying macaroon was
776-
# generated. This will change to a proper NULL-able field when the tokens get
777-
# moved to the database.
778-
return self._hs.get_macaroon_generator().generate_short_term_login_token(
774+
return await self._hs.get_auth_handler().create_login_token_for_user_id(
779775
user_id,
780-
auth_provider_id or "",
781-
auth_provider_session_id,
782776
duration_in_ms,
783-
)
784-
785-
def generate_short_term_login_token(
786-
self,
787-
user_id: str,
788-
duration_in_ms: int = (2 * 60 * 1000),
789-
auth_provider_id: str = "",
790-
auth_provider_session_id: Optional[str] = None,
791-
) -> str:
792-
"""Generate a login token suitable for m.login.token authentication
793-
794-
Added in Synapse v1.9.0.
795-
796-
This was deprecated in Synapse v1.69.0 in favor of create_login_token, and will
797-
be removed in Synapse 1.71.0.
798-
799-
Args:
800-
user_id: gives the ID of the user that the token is for
801-
802-
duration_in_ms: the time that the token will be valid for
803-
804-
auth_provider_id: the ID of the SSO IdP that the user used to authenticate
805-
to get this token, if any. This is encoded in the token so that
806-
/login can report stats on number of successful logins by IdP.
807-
"""
808-
logger.warn(
809-
"A module configured on this server uses ModuleApi.generate_short_term_login_token(), "
810-
"which is deprecated in favor of ModuleApi.create_login_token(), and will be removed in "
811-
"Synapse 1.71.0",
812-
)
813-
return self._hs.get_macaroon_generator().generate_short_term_login_token(
814-
user_id,
815777
auth_provider_id,
816778
auth_provider_session_id,
817-
duration_in_ms,
818779
)
819780

820781
@defer.inlineCallbacks

synapse/rest/client/login.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -436,8 +436,7 @@ async def _do_token_login(
436436
The body of the JSON response.
437437
"""
438438
token = login_submission["token"]
439-
auth_handler = self.auth_handler
440-
res = await auth_handler.validate_short_term_login_token(token)
439+
res = await self.auth_handler.consume_login_token(token)
441440

442441
return await self._complete_login(
443442
res.user_id,

synapse/rest/client/login_token_request.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ def __init__(self, hs: "HomeServer"):
5757
self.store = hs.get_datastores().main
5858
self.clock = hs.get_clock()
5959
self.server_name = hs.config.server.server_name
60-
self.macaroon_gen = hs.get_macaroon_generator()
6160
self.auth_handler = hs.get_auth_handler()
6261
self.token_timeout = hs.config.experimental.msc3882_token_timeout
6362
self.ui_auth = hs.config.experimental.msc3882_ui_auth
@@ -76,10 +75,10 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
7675
can_skip_ui_auth=False, # Don't allow skipping of UI auth
7776
)
7877

79-
login_token = self.macaroon_gen.generate_short_term_login_token(
78+
login_token = await self.auth_handler.create_login_token_for_user_id(
8079
user_id=requester.user.to_string(),
8180
auth_provider_id="org.matrix.msc3882.login_token_request",
82-
duration_in_ms=self.token_timeout,
81+
duration_ms=self.token_timeout,
8382
)
8483

8584
return (

0 commit comments

Comments
 (0)