2019-09-25 12:33:03 +02:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
|
|
#
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
# you may not use this file except in compliance with the License.
|
|
|
|
# You may obtain a copy of the License at
|
|
|
|
#
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
#
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
# See the License for the specific language governing permissions and
|
|
|
|
# limitations under the License.
|
2020-07-15 19:40:54 +02:00
|
|
|
|
2019-09-25 12:33:03 +02:00
|
|
|
import logging
|
2020-07-23 21:45:39 +02:00
|
|
|
from typing import Any
|
2019-09-25 12:33:03 +02:00
|
|
|
|
|
|
|
from canonicaljson import json
|
|
|
|
|
|
|
|
from twisted.web.client import PartialDownloadError
|
|
|
|
|
|
|
|
from synapse.api.constants import LoginType
|
|
|
|
from synapse.api.errors import Codes, LoginError, SynapseError
|
|
|
|
from synapse.config.emailconfig import ThreepidBehaviour
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
class UserInteractiveAuthChecker:
|
|
|
|
"""Abstract base class for an interactive auth checker"""
|
|
|
|
|
|
|
|
def __init__(self, hs):
|
|
|
|
pass
|
|
|
|
|
2020-07-23 21:45:39 +02:00
|
|
|
def is_enabled(self) -> bool:
|
2019-09-25 13:10:26 +02:00
|
|
|
"""Check if the configuration of the homeserver allows this checker to work
|
|
|
|
|
|
|
|
Returns:
|
2020-07-23 21:45:39 +02:00
|
|
|
True if this login type is enabled.
|
2019-09-25 13:10:26 +02:00
|
|
|
"""
|
|
|
|
|
2020-07-23 21:45:39 +02:00
|
|
|
async def check_auth(self, authdict: dict, clientip: str) -> Any:
|
2019-09-25 12:33:03 +02:00
|
|
|
"""Given the authentication dict from the client, attempt to check this step
|
|
|
|
|
|
|
|
Args:
|
2020-07-23 21:45:39 +02:00
|
|
|
authdict: authentication dictionary from the client
|
|
|
|
clientip: The IP address of the client.
|
2019-09-25 12:33:03 +02:00
|
|
|
|
|
|
|
Raises:
|
|
|
|
SynapseError if authentication failed
|
|
|
|
|
|
|
|
Returns:
|
2020-07-23 21:45:39 +02:00
|
|
|
The result of authentication (to pass back to the client?)
|
2019-09-25 12:33:03 +02:00
|
|
|
"""
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
|
|
|
|
|
|
class DummyAuthChecker(UserInteractiveAuthChecker):
|
|
|
|
AUTH_TYPE = LoginType.DUMMY
|
|
|
|
|
2019-09-25 13:10:26 +02:00
|
|
|
def is_enabled(self):
|
|
|
|
return True
|
|
|
|
|
2020-07-23 21:45:39 +02:00
|
|
|
async def check_auth(self, authdict, clientip):
|
|
|
|
return True
|
2019-09-25 12:33:03 +02:00
|
|
|
|
|
|
|
|
|
|
|
class TermsAuthChecker(UserInteractiveAuthChecker):
|
|
|
|
AUTH_TYPE = LoginType.TERMS
|
|
|
|
|
2019-09-25 13:10:26 +02:00
|
|
|
def is_enabled(self):
|
|
|
|
return True
|
|
|
|
|
2020-07-23 21:45:39 +02:00
|
|
|
async def check_auth(self, authdict, clientip):
|
|
|
|
return True
|
2019-09-25 12:33:03 +02:00
|
|
|
|
|
|
|
|
|
|
|
class RecaptchaAuthChecker(UserInteractiveAuthChecker):
|
|
|
|
AUTH_TYPE = LoginType.RECAPTCHA
|
|
|
|
|
|
|
|
def __init__(self, hs):
|
|
|
|
super().__init__(hs)
|
2019-09-25 13:10:26 +02:00
|
|
|
self._enabled = bool(hs.config.recaptcha_private_key)
|
2019-11-01 15:07:44 +01:00
|
|
|
self._http_client = hs.get_proxied_http_client()
|
2019-09-25 12:33:03 +02:00
|
|
|
self._url = hs.config.recaptcha_siteverify_api
|
|
|
|
self._secret = hs.config.recaptcha_private_key
|
|
|
|
|
2019-09-25 13:10:26 +02:00
|
|
|
def is_enabled(self):
|
|
|
|
return self._enabled
|
|
|
|
|
2020-07-23 21:45:39 +02:00
|
|
|
async def check_auth(self, authdict, clientip):
|
2019-09-25 12:33:03 +02:00
|
|
|
try:
|
|
|
|
user_response = authdict["response"]
|
|
|
|
except KeyError:
|
|
|
|
# Client tried to provide captcha but didn't give the parameter:
|
|
|
|
# bad request.
|
|
|
|
raise LoginError(
|
|
|
|
400, "Captcha response is required", errcode=Codes.CAPTCHA_NEEDED
|
|
|
|
)
|
|
|
|
|
|
|
|
logger.info(
|
|
|
|
"Submitting recaptcha response %s with remoteip %s", user_response, clientip
|
|
|
|
)
|
|
|
|
|
|
|
|
# TODO: get this from the homeserver rather than creating a new one for
|
|
|
|
# each request
|
|
|
|
try:
|
2020-07-23 21:45:39 +02:00
|
|
|
resp_body = await self._http_client.post_urlencoded_get_json(
|
2019-09-25 12:33:03 +02:00
|
|
|
self._url,
|
|
|
|
args={
|
|
|
|
"secret": self._secret,
|
|
|
|
"response": user_response,
|
|
|
|
"remoteip": clientip,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
except PartialDownloadError as pde:
|
|
|
|
# Twisted is silly
|
|
|
|
data = pde.response
|
2020-07-15 19:40:54 +02:00
|
|
|
resp_body = json.loads(data.decode("utf-8"))
|
2019-09-25 12:33:03 +02:00
|
|
|
|
|
|
|
if "success" in resp_body:
|
|
|
|
# Note that we do NOT check the hostname here: we explicitly
|
|
|
|
# intend the CAPTCHA to be presented by whatever client the
|
|
|
|
# user is using, we just care that they have completed a CAPTCHA.
|
|
|
|
logger.info(
|
|
|
|
"%s reCAPTCHA from hostname %s",
|
|
|
|
"Successful" if resp_body["success"] else "Failed",
|
|
|
|
resp_body.get("hostname"),
|
|
|
|
)
|
|
|
|
if resp_body["success"]:
|
|
|
|
return True
|
|
|
|
raise LoginError(401, "", errcode=Codes.UNAUTHORIZED)
|
|
|
|
|
|
|
|
|
|
|
|
class _BaseThreepidAuthChecker:
|
|
|
|
def __init__(self, hs):
|
|
|
|
self.hs = hs
|
|
|
|
self.store = hs.get_datastore()
|
|
|
|
|
2020-05-26 19:46:22 +02:00
|
|
|
async def _check_threepid(self, medium, authdict):
|
2019-09-25 12:33:03 +02:00
|
|
|
if "threepid_creds" not in authdict:
|
|
|
|
raise LoginError(400, "Missing threepid_creds", Codes.MISSING_PARAM)
|
|
|
|
|
|
|
|
threepid_creds = authdict["threepid_creds"]
|
|
|
|
|
|
|
|
identity_handler = self.hs.get_handlers().identity_handler
|
|
|
|
|
|
|
|
logger.info("Getting validated threepid. threepidcreds: %r", (threepid_creds,))
|
2019-09-25 13:29:35 +02:00
|
|
|
|
|
|
|
# msisdns are currently always ThreepidBehaviour.REMOTE
|
|
|
|
if medium == "msisdn":
|
|
|
|
if not self.hs.config.account_threepid_delegate_msisdn:
|
|
|
|
raise SynapseError(
|
|
|
|
400, "Phone number verification is not enabled on this homeserver"
|
|
|
|
)
|
2020-05-26 19:46:22 +02:00
|
|
|
threepid = await identity_handler.threepid_from_creds(
|
2019-09-25 13:29:35 +02:00
|
|
|
self.hs.config.account_threepid_delegate_msisdn, threepid_creds
|
|
|
|
)
|
|
|
|
elif medium == "email":
|
|
|
|
if self.hs.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE:
|
|
|
|
assert self.hs.config.account_threepid_delegate_email
|
2020-05-26 19:46:22 +02:00
|
|
|
threepid = await identity_handler.threepid_from_creds(
|
2019-09-25 12:33:03 +02:00
|
|
|
self.hs.config.account_threepid_delegate_email, threepid_creds
|
|
|
|
)
|
2019-09-25 13:29:35 +02:00
|
|
|
elif self.hs.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL:
|
|
|
|
threepid = None
|
2020-05-26 19:46:22 +02:00
|
|
|
row = await self.store.get_threepid_validation_session(
|
2019-09-25 13:29:35 +02:00
|
|
|
medium,
|
|
|
|
threepid_creds["client_secret"],
|
|
|
|
sid=threepid_creds["sid"],
|
|
|
|
validated=True,
|
2019-09-25 12:33:03 +02:00
|
|
|
)
|
|
|
|
|
2019-09-25 13:29:35 +02:00
|
|
|
if row:
|
|
|
|
threepid = {
|
|
|
|
"medium": row["medium"],
|
|
|
|
"address": row["address"],
|
|
|
|
"validated_at": row["validated_at"],
|
|
|
|
}
|
2019-09-25 12:33:03 +02:00
|
|
|
|
2019-09-25 13:29:35 +02:00
|
|
|
# Valid threepid returned, delete from the db
|
2020-05-26 19:46:22 +02:00
|
|
|
await self.store.delete_threepid_session(threepid_creds["sid"])
|
2019-09-25 13:29:35 +02:00
|
|
|
else:
|
|
|
|
raise SynapseError(
|
|
|
|
400, "Email address verification is not enabled on this homeserver"
|
|
|
|
)
|
2019-09-25 12:33:03 +02:00
|
|
|
else:
|
2019-09-25 13:29:35 +02:00
|
|
|
# this can't happen!
|
|
|
|
raise AssertionError("Unrecognized threepid medium: %s" % (medium,))
|
2019-09-25 12:33:03 +02:00
|
|
|
|
|
|
|
if not threepid:
|
|
|
|
raise LoginError(401, "", errcode=Codes.UNAUTHORIZED)
|
|
|
|
|
|
|
|
if threepid["medium"] != medium:
|
|
|
|
raise LoginError(
|
|
|
|
401,
|
|
|
|
"Expecting threepid of type '%s', got '%s'"
|
|
|
|
% (medium, threepid["medium"]),
|
|
|
|
errcode=Codes.UNAUTHORIZED,
|
|
|
|
)
|
|
|
|
|
|
|
|
threepid["threepid_creds"] = authdict["threepid_creds"]
|
|
|
|
|
|
|
|
return threepid
|
|
|
|
|
|
|
|
|
|
|
|
class EmailIdentityAuthChecker(UserInteractiveAuthChecker, _BaseThreepidAuthChecker):
|
|
|
|
AUTH_TYPE = LoginType.EMAIL_IDENTITY
|
|
|
|
|
|
|
|
def __init__(self, hs):
|
|
|
|
UserInteractiveAuthChecker.__init__(self, hs)
|
|
|
|
_BaseThreepidAuthChecker.__init__(self, hs)
|
|
|
|
|
2019-09-25 13:10:26 +02:00
|
|
|
def is_enabled(self):
|
|
|
|
return self.hs.config.threepid_behaviour_email in (
|
|
|
|
ThreepidBehaviour.REMOTE,
|
|
|
|
ThreepidBehaviour.LOCAL,
|
|
|
|
)
|
|
|
|
|
2020-07-23 21:45:39 +02:00
|
|
|
async def check_auth(self, authdict, clientip):
|
|
|
|
return await self._check_threepid("email", authdict)
|
2019-09-25 12:33:03 +02:00
|
|
|
|
|
|
|
|
|
|
|
class MsisdnAuthChecker(UserInteractiveAuthChecker, _BaseThreepidAuthChecker):
|
|
|
|
AUTH_TYPE = LoginType.MSISDN
|
|
|
|
|
|
|
|
def __init__(self, hs):
|
|
|
|
UserInteractiveAuthChecker.__init__(self, hs)
|
|
|
|
_BaseThreepidAuthChecker.__init__(self, hs)
|
|
|
|
|
2019-09-25 13:10:26 +02:00
|
|
|
def is_enabled(self):
|
|
|
|
return bool(self.hs.config.account_threepid_delegate_msisdn)
|
|
|
|
|
2020-07-23 21:45:39 +02:00
|
|
|
async def check_auth(self, authdict, clientip):
|
|
|
|
return await self._check_threepid("msisdn", authdict)
|
2019-09-25 12:33:03 +02:00
|
|
|
|
|
|
|
|
|
|
|
INTERACTIVE_AUTH_CHECKERS = [
|
|
|
|
DummyAuthChecker,
|
|
|
|
TermsAuthChecker,
|
|
|
|
RecaptchaAuthChecker,
|
|
|
|
EmailIdentityAuthChecker,
|
|
|
|
MsisdnAuthChecker,
|
|
|
|
]
|
|
|
|
"""A list of UserInteractiveAuthChecker classes"""
|