mirror of
https://git.anonymousland.org/anonymousland/synapse.git
synced 2025-05-04 14:24:55 -04:00
Save login tokens in database (#13844)
* Save login tokens in database Signed-off-by: Quentin Gliech <quenting@element.io> * Add upgrade notes * Track login token reuse in a Prometheus metric Signed-off-by: Quentin Gliech <quenting@element.io>
This commit is contained in:
parent
d902181de9
commit
8756d5c87e
11 changed files with 338 additions and 228 deletions
|
@ -38,6 +38,7 @@ from typing import (
|
|||
import attr
|
||||
import bcrypt
|
||||
import unpaddedbase64
|
||||
from prometheus_client import Counter
|
||||
|
||||
from twisted.internet.defer import CancelledError
|
||||
from twisted.web.server import Request
|
||||
|
@ -48,6 +49,7 @@ from synapse.api.errors import (
|
|||
Codes,
|
||||
InteractiveAuthIncompleteError,
|
||||
LoginError,
|
||||
NotFoundError,
|
||||
StoreError,
|
||||
SynapseError,
|
||||
UserDeactivatedError,
|
||||
|
@ -63,10 +65,14 @@ from synapse.http.server import finish_request, respond_with_html
|
|||
from synapse.http.site import SynapseRequest
|
||||
from synapse.logging.context import defer_to_thread
|
||||
from synapse.metrics.background_process_metrics import run_as_background_process
|
||||
from synapse.storage.databases.main.registration import (
|
||||
LoginTokenExpired,
|
||||
LoginTokenLookupResult,
|
||||
LoginTokenReused,
|
||||
)
|
||||
from synapse.types import JsonDict, Requester, UserID
|
||||
from synapse.util import stringutils as stringutils
|
||||
from synapse.util.async_helpers import delay_cancellation, maybe_awaitable
|
||||
from synapse.util.macaroons import LoginTokenAttributes
|
||||
from synapse.util.msisdn import phone_number_to_msisdn
|
||||
from synapse.util.stringutils import base62_encode
|
||||
from synapse.util.threepids import canonicalise_email
|
||||
|
@ -80,6 +86,12 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
INVALID_USERNAME_OR_PASSWORD = "Invalid username or password"
|
||||
|
||||
invalid_login_token_counter = Counter(
|
||||
"synapse_user_login_invalid_login_tokens",
|
||||
"Counts the number of rejected m.login.token on /login",
|
||||
["reason"],
|
||||
)
|
||||
|
||||
|
||||
def convert_client_dict_legacy_fields_to_identifier(
|
||||
submission: JsonDict,
|
||||
|
@ -883,6 +895,25 @@ class AuthHandler:
|
|||
|
||||
return True
|
||||
|
||||
async def create_login_token_for_user_id(
|
||||
self,
|
||||
user_id: str,
|
||||
duration_ms: int = (2 * 60 * 1000),
|
||||
auth_provider_id: Optional[str] = None,
|
||||
auth_provider_session_id: Optional[str] = None,
|
||||
) -> str:
|
||||
login_token = self.generate_login_token()
|
||||
now = self._clock.time_msec()
|
||||
expiry_ts = now + duration_ms
|
||||
await self.store.add_login_token_to_user(
|
||||
user_id=user_id,
|
||||
token=login_token,
|
||||
expiry_ts=expiry_ts,
|
||||
auth_provider_id=auth_provider_id,
|
||||
auth_provider_session_id=auth_provider_session_id,
|
||||
)
|
||||
return login_token
|
||||
|
||||
async def create_refresh_token_for_user_id(
|
||||
self,
|
||||
user_id: str,
|
||||
|
@ -1401,6 +1432,18 @@ class AuthHandler:
|
|||
return None
|
||||
return user_id
|
||||
|
||||
def generate_login_token(self) -> str:
|
||||
"""Generates an opaque string, for use as an short-term login token"""
|
||||
|
||||
# we use the following format for access tokens:
|
||||
# syl_<random string>_<base62 crc check>
|
||||
|
||||
random_string = stringutils.random_string(20)
|
||||
base = f"syl_{random_string}"
|
||||
|
||||
crc = base62_encode(crc32(base.encode("ascii")), minwidth=6)
|
||||
return f"{base}_{crc}"
|
||||
|
||||
def generate_access_token(self, for_user: UserID) -> str:
|
||||
"""Generates an opaque string, for use as an access token"""
|
||||
|
||||
|
@ -1427,16 +1470,17 @@ class AuthHandler:
|
|||
crc = base62_encode(crc32(base.encode("ascii")), minwidth=6)
|
||||
return f"{base}_{crc}"
|
||||
|
||||
async def validate_short_term_login_token(
|
||||
self, login_token: str
|
||||
) -> LoginTokenAttributes:
|
||||
async def consume_login_token(self, login_token: str) -> LoginTokenLookupResult:
|
||||
try:
|
||||
res = self.macaroon_gen.verify_short_term_login_token(login_token)
|
||||
except Exception:
|
||||
raise AuthError(403, "Invalid login token", errcode=Codes.FORBIDDEN)
|
||||
return await self.store.consume_login_token(login_token)
|
||||
except LoginTokenExpired:
|
||||
invalid_login_token_counter.labels("expired").inc()
|
||||
except LoginTokenReused:
|
||||
invalid_login_token_counter.labels("reused").inc()
|
||||
except NotFoundError:
|
||||
invalid_login_token_counter.labels("not found").inc()
|
||||
|
||||
await self.auth_blocking.check_auth_blocking(res.user_id)
|
||||
return res
|
||||
raise AuthError(403, "Invalid login token", errcode=Codes.FORBIDDEN)
|
||||
|
||||
async def delete_access_token(self, access_token: str) -> None:
|
||||
"""Invalidate a single access token
|
||||
|
@ -1711,7 +1755,7 @@ class AuthHandler:
|
|||
)
|
||||
|
||||
# Create a login token
|
||||
login_token = self.macaroon_gen.generate_short_term_login_token(
|
||||
login_token = await self.create_login_token_for_user_id(
|
||||
registered_user_id,
|
||||
auth_provider_id=auth_provider_id,
|
||||
auth_provider_session_id=auth_provider_session_id,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue