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

Implement MSC2290 #6043

Merged
merged 46 commits into from
Sep 23, 2019
Merged
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
d00435b
Allow HS to send emails when adding an email to the HS
anoadragon453 Sep 16, 2019
c04b2c0
Correct some small issues and fix bug
anoadragon453 Sep 17, 2019
292c007
Address review comments
anoadragon453 Sep 18, 2019
da11cc6
Remove blacklist
anoadragon453 Sep 18, 2019
f5e4c1b
Fix add_threepid template default values in emailconfig
anoadragon453 Sep 18, 2019
bb13515
Re-blacklist tests
anoadragon453 Sep 18, 2019
9718ad6
Factor out removing id_server from msisdn
anoadragon453 Sep 18, 2019
b7cd985
Ensure REMOTE vs LOCAL ThreepidBehaviour is handled
anoadragon453 Sep 19, 2019
1f17307
Move jinja failure template loading into servlet constructor
anoadragon453 Sep 19, 2019
d7ff1cd
Set email
anoadragon453 Sep 19, 2019
7a678a6
Make sure templates only get loaded when necessary
anoadragon453 Sep 19, 2019
102b608
Pull if validation_session out of helper method
anoadragon453 Sep 20, 2019
37281e3
this confused me for so long
anoadragon453 Sep 20, 2019
29fc7bb
Ensure we catch HttpResponseException when calling to id servers
anoadragon453 Sep 20, 2019
f4e93ae
Unpack response from identity server to check for errors
anoadragon453 Sep 20, 2019
b840aff
Factor out password_reset trailing slash change
anoadragon453 Sep 20, 2019
3713c2c
Address review comments
anoadragon453 Sep 20, 2019
8dcb79c
validation_session cannot be None
anoadragon453 Sep 20, 2019
3336902
Just added the endpoints, pulling in infra
anoadragon453 Sep 16, 2019
a7cd54e
Fill out ThreepidAddRestServlet
anoadragon453 Sep 17, 2019
088d6e4
Finish the bind endpoint servlet and remove _extract_items_from_creds…
anoadragon453 Sep 17, 2019
f3fbe5f
Add changelog
anoadragon453 Sep 17, 2019
8463b7d
Make user account deactivation remove bound 3pids not on the user acc…
anoadragon453 Sep 17, 2019
21ea59b
Just added the endpoints, pulling in infra
anoadragon453 Sep 16, 2019
b0a2c2e
Fill out ThreepidAddRestServlet
anoadragon453 Sep 17, 2019
113ebcf
Finish the bind endpoint servlet and remove _extract_items_from_creds…
anoadragon453 Sep 17, 2019
3a0b7f2
Remove id_server from POST /account/3pid/msisdn/requestToken
anoadragon453 Sep 18, 2019
b13db4a
Make sure these new endpoints aren't also on r0
anoadragon453 Sep 19, 2019
4206cfc
Fix wrong config option and delete double servlets
anoadragon453 Sep 20, 2019
8683acc
Correct pulling variables out of validation_session
anoadragon453 Sep 20, 2019
9066368
Make sure to yield
anoadragon453 Sep 20, 2019
c637f74
english
anoadragon453 Sep 20, 2019
d89152d
/account/3pid/add is email only for now
anoadragon453 Sep 20, 2019
4275980
Temporarily c/p /account/3pid to /account/3pid/add
anoadragon453 Sep 20, 2019
c1a676e
Address review comments
anoadragon453 Sep 20, 2019
8fd3581
Consolidate threepid adding functionality in /account/3pid, account/3…
anoadragon453 Sep 20, 2019
66944d7
lint
anoadragon453 Sep 20, 2019
3daf0de
typo
anoadragon453 Sep 20, 2019
e4bb5ab
submit_token got its trailing slash again
anoadragon453 Sep 20, 2019
abc6f20
remove cyclic dep
anoadragon453 Sep 20, 2019
ff9eca5
Forgot my darn yields
anoadragon453 Sep 20, 2019
b61b73a
Re-add error checking on threepid_from_creds
anoadragon453 Sep 20, 2019
4c784e9
address my own review comments
richvdh Sep 23, 2019
93de39b
Update synapse/handlers/identity.py
richvdh Sep 23, 2019
e5f4041
Merge branch 'develop' into anoa/msc2290
richvdh Sep 23, 2019
6b2f8d1
fix bad merge
richvdh Sep 23, 2019
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
1 change: 1 addition & 0 deletions changelog.d/6043.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implement new Client Server API endpoints `/account/3pid/add` and `/account/3pid/bind` as per [MSC2290](https://github.com/matrix-org/matrix-doc/pull/2290).
4 changes: 3 additions & 1 deletion synapse/handlers/deactivate_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ def deactivate_account(self, user_id, erase_data, id_server=None):
# unbinding
identity_server_supports_unbinding = True

threepids = yield self.store.user_get_threepids(user_id)
# Retrieve the 3PIDs this user has bound to an identity server
threepids = yield self.store.user_get_bound_threepids(user_id)

for threepid in threepids:
try:
result = yield self._identity_handler.try_unbind_threepid(
Expand Down
115 changes: 65 additions & 50 deletions synapse/handlers/identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
HttpResponseException,
SynapseError,
)
from synapse.config.emailconfig import ThreepidBehaviour
from synapse.util.stringutils import random_string

from ._base import BaseHandler
Expand All @@ -43,36 +44,7 @@ def __init__(self, hs):
self.http_client = hs.get_simple_http_client()
self.federation_http_client = hs.get_http_client()
self.hs = hs

def _extract_items_from_creds_dict(self, creds):
"""
Retrieve entries from a "credentials" dictionary

Args:
creds (dict[str, str]): Dictionary of credentials that contain the following keys:
* client_secret|clientSecret: A unique secret str provided by the client
* id_server|idServer: the domain of the identity server to query
* id_access_token: The access token to authenticate to the identity
server with.

Returns:
tuple(str, str, str|None): A tuple containing the client_secret, the id_server,
and the id_access_token value if available.
"""
client_secret = creds.get("client_secret") or creds.get("clientSecret")
if not client_secret:
raise SynapseError(
400, "No client_secret in creds", errcode=Codes.MISSING_PARAM
)

id_server = creds.get("id_server") or creds.get("idServer")
if not id_server:
raise SynapseError(
400, "No id_server in creds", errcode=Codes.MISSING_PARAM
)

id_access_token = creds.get("id_access_token")
return client_secret, id_server, id_access_token
self.identity_handler = hs.get_handlers().identity_handler

@defer.inlineCallbacks
def threepid_from_creds(self, id_server, creds):
Expand Down Expand Up @@ -112,32 +84,29 @@ def threepid_from_creds(self, id_server, creds):
return data if "medium" in data else None

@defer.inlineCallbacks
def bind_threepid(self, creds, mxid, use_v2=True):
def bind_threepid(
self, client_secret, sid, mxid, id_server, id_access_token=None, use_v2=True
):
"""Bind a 3PID to an identity server

Args:
creds (dict[str, str]): Dictionary of credentials that contain the following keys:
* client_secret|clientSecret: A unique secret str provided by the client
* id_server|idServer: the domain of the identity server to query
* id_access_token: The access token to authenticate to the identity
server with. Required if use_v2 is true
client_secret (str): A unique secret provided by the client

sid (str): The ID of the validation session

mxid (str): The MXID to bind the 3PID to
use_v2 (bool): Whether to use v2 Identity Service API endpoints

id_server (str): The domain of the identity server to query

id_access_token (str): The access token to authenticate to the identity
server with, if necessary. Required if use_v2 is true

use_v2 (bool): Whether to use v2 Identity Service API endpoints. Defaults to True

Returns:
Deferred[dict]: The response from the identity server
"""
logger.debug("binding threepid %r to %s", creds, mxid)

client_secret, id_server, id_access_token = self._extract_items_from_creds_dict(
creds
)

sid = creds.get("sid")
if not sid:
raise SynapseError(
400, "No sid in three_pid_creds", errcode=Codes.MISSING_PARAM
)
logger.debug("Proxying threepid bind request for %s to %s", mxid, id_server)

# If an id_access_token is not supplied, force usage of v1
if id_access_token is None:
Expand All @@ -156,7 +125,6 @@ def bind_threepid(self, creds, mxid, use_v2=True):
data = yield self.http_client.post_json_get_json(
bind_url, bind_data, headers=headers
)
logger.debug("bound threepid %r to %s", creds, mxid)

# Remember where we bound the threepid
yield self.store.add_user_bound_threepid(
Expand All @@ -176,7 +144,10 @@ def bind_threepid(self, creds, mxid, use_v2=True):
return data

logger.info("Got 404 when POSTing JSON %s, falling back to v1 URL", bind_url)
return (yield self.bind_threepid(creds, mxid, use_v2=False))
res = yield self.bind_threepid(
client_secret, sid, mxid, id_server, id_access_token, use_v2=False
)
return res

@defer.inlineCallbacks
def try_unbind_threepid(self, mxid, threepid):
Expand Down Expand Up @@ -447,6 +418,50 @@ def requestMsisdnToken(
logger.info("Proxied requestToken failed: %r", e)
raise e.to_synapse_error()

@defer.inlineCallbacks
def validate_threepid_session(self, client_secret, sid):
"""Validates a threepid session with only the client secret and session ID
Tries validating against any configured account_threepid_delegates as well as locally.

Args:
client_secret (str): A secret provided by the client

sid (str): The ID of the session

Returns:
Dict[str, str|int] if validation was successful, otherwise None
"""
# We don't actually know which medium this 3PID is. Thus we first assume it's email,
# and if validation fails we try msisdn
validation_session = None

# XXX: We shouldn't need to keep wrapping and unwrapping this value
threepid_creds = {"client_secret": client_secret, "sid": sid}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll address this in a future PR


# Try to validate as email
if self.hs.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE:
# Ask our delegated email identity server
validation_session = yield self.identity_handler.threepid_from_creds(
self.hs.config.account_threepid_delegate_email, threepid_creds
)
elif self.hs.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL:
# Get a validated session matching these details
validation_session = yield self.store.get_threepid_validation_session(
"email", client_secret, sid=sid, validated=True
)

if validation_session:
return validation_session

# Try to validate as msisdn
if self.hs.config.account_threepid_delegate_msisdn:
# Ask our delegated msisdn identity server
validation_session = yield self.identity_handler.threepid_from_creds(
self.hs.config.account_threepid_delegate_msisdn, threepid_creds
)

return validation_session


def create_id_access_token_header(id_access_token):
"""Create an Authorization header for passing to SimpleHttpClient as the header value
Expand Down
161 changes: 89 additions & 72 deletions synapse/rest/client/v2_alpha/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,7 @@
from twisted.internet import defer

from synapse.api.constants import LoginType
from synapse.api.errors import (
Codes,
HttpResponseException,
SynapseError,
ThreepidValidationError,
)
from synapse.api.errors import Codes, SynapseError, ThreepidValidationError
from synapse.config.emailconfig import ThreepidBehaviour
from synapse.http.server import finish_request
from synapse.http.servlet import (
Expand Down Expand Up @@ -485,10 +480,8 @@ def __init__(self, hs):
def on_POST(self, request):
body = parse_json_object_from_request(request)
assert_params_in_dict(
body,
["id_server", "client_secret", "country", "phone_number", "send_attempt"],
body, ["client_secret", "country", "phone_number", "send_attempt"]
)
id_server = "https://" + body["id_server"] # Assume https
client_secret = body["client_secret"]
country = body["country"]
phone_number = body["phone_number"]
Expand All @@ -509,8 +502,23 @@ def on_POST(self, request):
if existing_user_id is not None:
raise SynapseError(400, "MSISDN is already in use", Codes.THREEPID_IN_USE)

if not self.hs.config.account_threepid_delegate_msisdn:
logger.warn(
"No upstream msisdn account_threepid_delegate configured on the server to "
"handle this request"
)
raise SynapseError(
400,
"Adding phone numbers to user account is not supported by this homeserver",
)

ret = yield self.identity_handler.requestMsisdnToken(
id_server, country, phone_number, client_secret, send_attempt, next_link
self.hs.config.account_threepid_delegate_msisdn,
country,
phone_number,
client_secret,
send_attempt,
next_link,
)

return 200, ret
Expand Down Expand Up @@ -627,81 +635,88 @@ def on_POST(self, request):
client_secret = threepid_creds["client_secret"]
sid = threepid_creds["sid"]

# We don't actually know which medium this 3PID is. Thus we first assume it's email,
# and if validation fails we try msisdn
validation_session = None

# Try to validate as email
if self.hs.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE:
# Ask our delegated email identity server
try:
validation_session = yield self.identity_handler.threepid_from_creds(
self.hs.config.account_threepid_delegate_email, threepid_creds
)
except HttpResponseException:
logger.debug(
"%s reported non-validated threepid: %s",
self.hs.config.account_threepid_delegate_email,
threepid_creds,
)
elif self.hs.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL:
# Get a validated session matching these details
validation_session = yield self.datastore.get_threepid_validation_session(
"email", client_secret, sid=sid, validated=True
)

# Old versions of Sydent return a 200 http code even on a failed validation check.
# Thus, in addition to the HttpResponseException check above (which checks for
# non-200 errors), we need to make sure validation_session isn't actually an error,
# identified by containing an "error" key
# See https://github.com/matrix-org/sydent/issues/215 for details
if validation_session and "error" not in validation_session:
yield self._add_threepid_to_account(user_id, validation_session)
validation_session = self.identity_handler.validate_threepid_session(
client_secret, sid
)
if validation_session:
yield self.auth_handler.add_threepid(
user_id,
validation_session["medium"],
validation_session["address"],
validation_session["validated_at"],
)
return 200, {}

# Try to validate as msisdn
if self.hs.config.account_threepid_delegate_msisdn:
# Ask our delegated msisdn identity server
try:
validation_session = yield self.identity_handler.threepid_from_creds(
self.hs.config.account_threepid_delegate_msisdn, threepid_creds
)
except HttpResponseException:
logger.debug(
"%s reported non-validated threepid: %s",
self.hs.config.account_threepid_delegate_email,
threepid_creds,
)
raise SynapseError(
400, "No validated 3pid session found", Codes.THREEPID_AUTH_FAILED
)


# Check that validation_session isn't actually an error due to old Sydent instances
# See explanatory comment above
if validation_session and "error" not in validation_session:
yield self._add_threepid_to_account(user_id, validation_session)
return 200, {}
class ThreepidAddRestServlet(RestServlet):
PATTERNS = client_patterns("/account/3pid/add$", releases=(), unstable=True)

def __init__(self, hs):
super(ThreepidAddRestServlet, self).__init__()
self.hs = hs
self.identity_handler = hs.get_handlers().identity_handler
self.auth = hs.get_auth()
self.auth_handler = hs.get_auth_handler()

@defer.inlineCallbacks
def on_POST(self, request):
requester = yield self.auth.get_user_by_req(request)
user_id = requester.user.to_string()
body = parse_json_object_from_request(request)

assert_params_in_dict(body, ["client_secret", "sid"])
client_secret = body["client_secret"]
sid = body["sid"]

validation_session = self.identity_handler.validate_threepid_session(
client_secret, sid
)
if validation_session:
yield self.auth_handler.add_threepid(
user_id,
validation_session["medium"],
validation_session["address"],
validation_session["validated_at"],
)
return 200, {}

raise SynapseError(
400, "No validated 3pid session found", Codes.THREEPID_AUTH_FAILED
)


class ThreepidBindRestServlet(RestServlet):
PATTERNS = client_patterns("/account/3pid/bind$", releases=(), unstable=True)

def __init__(self, hs):
super(ThreepidBindRestServlet, self).__init__()
self.hs = hs
self.identity_handler = hs.get_handlers().identity_handler
self.auth = hs.get_auth()

@defer.inlineCallbacks
def _add_threepid_to_account(self, user_id, validation_session):
"""Add a threepid wrapped in a validation_session dict to an account
def on_POST(self, request):
body = parse_json_object_from_request(request)

Args:
user_id (str): The mxid of the user to add this 3PID to
assert_params_in_dict(body, ["id_server", "sid", "client_secret"])
id_server = body["id_server"]
sid = body["sid"]
client_secret = body["client_secret"]
id_access_token = body.get("id_access_token") # optional

validation_session (dict): A dict containing the following:
* medium - medium of the threepid
* address - address of the threepid
* validated_at - timestamp of when the validation occurred
"""
yield self.auth_handler.add_threepid(
user_id,
validation_session["medium"],
validation_session["address"],
validation_session["validated_at"],
requester = yield self.auth.get_user_by_req(request)
user_id = requester.user.to_string()

yield self.identity_handler.bind_threepid(
client_secret, sid, user_id, id_server, id_access_token
)

return 200, {}


class ThreepidUnbindRestServlet(RestServlet):
PATTERNS = client_patterns("/account/3pid/unbind$", releases=(), unstable=True)
Expand Down Expand Up @@ -794,6 +809,8 @@ def register_servlets(hs, http_server):
MsisdnThreepidRequestTokenRestServlet(hs).register(http_server)
AddThreepidSubmitTokenServlet(hs).register(http_server)
ThreepidRestServlet(hs).register(http_server)
ThreepidAddRestServlet(hs).register(http_server)
ThreepidBindRestServlet(hs).register(http_server)
ThreepidUnbindRestServlet(hs).register(http_server)
ThreepidDeleteRestServlet(hs).register(http_server)
WhoamiRestServlet(hs).register(http_server)
Loading