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

Use the v2 Identity Service API for lookups (MSC2134 + MSC2140) #5976

Merged
merged 40 commits into from
Sep 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
1954438
Use the v2 lookup API
anoadragon453 Aug 21, 2019
24ee3ae
lint
anoadragon453 Aug 21, 2019
902ef39
add changelog
anoadragon453 Aug 21, 2019
3a114fe
linter fight
anoadragon453 Aug 21, 2019
5426e13
Merge branch 'develop' into anoa/v2_lookup
anoadragon453 Aug 21, 2019
73fb6f3
Continue to support v1 lookup
anoadragon453 Aug 21, 2019
2472e2e
lint
anoadragon453 Aug 21, 2019
7bfccad
Address review comments
anoadragon453 Aug 27, 2019
75ef0f8
lint
anoadragon453 Aug 27, 2019
e68d648
small fixes and remove unnecessary Enum
anoadragon453 Aug 28, 2019
38dac27
Warn user when the id_server they chose does not support any of the h…
anoadragon453 Aug 28, 2019
8f1346d
Apply suggestions from code review
anoadragon453 Aug 28, 2019
4dc0849
lint
anoadragon453 Aug 28, 2019
849d8dc
Merge branch 'anoa/v2_lookup' of github.com:matrix-org/synapse into a…
anoadragon453 Aug 28, 2019
d9d156b
Merge branch 'develop' into anoa/v2_lookup
anoadragon453 Sep 3, 2019
42b11bd
use v2 identity service api endpoints for 3pid invites and lookup
anoadragon453 Sep 3, 2019
83021d9
Merge branch 'develop' of github.com:matrix-org/synapse into anoa/v2_…
anoadragon453 Sep 3, 2019
07154ea
Merge branch 'develop' of github.com:matrix-org/synapse into anoa/v2_…
anoadragon453 Sep 3, 2019
f4b7f7f
id_access_token support
anoadragon453 Sep 3, 2019
29c3489
Apply suggestions from code review
anoadragon453 Sep 4, 2019
ff5f6a0
Address review comments
anoadragon453 Sep 4, 2019
a5153af
Merge branch 'anoa/v2_lookup' of github.com:matrix-org/synapse into a…
anoadragon453 Sep 4, 2019
7f647bc
Revert moving lookup stuff to IdentityHandler
anoadragon453 Sep 4, 2019
f8bb859
Fix issues with moving stuff back to RoomMemberHandler
anoadragon453 Sep 4, 2019
1c59243
Factor our v2 invite things
anoadragon453 Sep 4, 2019
1b20928
lint
anoadragon453 Sep 4, 2019
db1d161
whoops
anoadragon453 Sep 4, 2019
9f92c3e
Change lookup_3pid back to a private method
anoadragon453 Sep 4, 2019
07169b1
Apply suggestions from code review
anoadragon453 Sep 5, 2019
5b852c2
Address review comments
anoadragon453 Sep 5, 2019
0d968c0
liiiiiiiiiiiint
anoadragon453 Sep 5, 2019
f18f3f1
address review comments
anoadragon453 Sep 9, 2019
18671b0
lint
anoadragon453 Sep 9, 2019
649dcbe
id_access_token -> access_token in query params
anoadragon453 Sep 10, 2019
b4520ea
Merge branch 'develop' of github.com:matrix-org/synapse into anoa/v2_…
anoadragon453 Sep 11, 2019
79f5c4f
Address review comments.
anoadragon453 Sep 11, 2019
cf8dbea
Merge branch 'develop' of github.com:matrix-org/synapse into anoa/v2_…
anoadragon453 Sep 11, 2019
7008c79
Send id access_token via Authorization headers, not JSON body
anoadragon453 Sep 11, 2019
ffb284e
Merge branch 'develop' of github.com:matrix-org/synapse into anoa/v2_…
anoadragon453 Sep 11, 2019
317dff6
Update changelog.d/5897.feature
anoadragon453 Sep 11, 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/5897.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Switch to using the v2 Identity Service `/lookup` API where available, with fallback to v1. (Implements [MSC2134](https://github.com/matrix-org/matrix-doc/pull/2134) plus id_access_token authentication for v2 Identity Service APIs from [MSC2140](https://github.com/matrix-org/matrix-doc/pull/2140)).
56 changes: 34 additions & 22 deletions synapse/handlers/identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,25 +74,6 @@ def _extract_items_from_creds_dict(self, creds):
id_access_token = creds.get("id_access_token")
return client_secret, id_server, id_access_token

def create_id_access_token_header(self, id_access_token):
"""Create an Authorization header for passing to SimpleHttpClient as the header value
of an HTTP request.

Args:
id_access_token (str): An identity server access token.

Returns:
list[str]: The ascii-encoded bearer token encased in a list.
"""
# Prefix with Bearer
bearer_token = "Bearer %s" % id_access_token

# Encode headers to standard ascii
bearer_token.encode("ascii")

# Return as a list as that's how SimpleHttpClient takes header values
return [bearer_token]

@defer.inlineCallbacks
def threepid_from_creds(self, id_server, creds):
"""
Expand Down Expand Up @@ -178,9 +159,7 @@ def bind_threepid(self, creds, mxid, use_v2=True):
bind_data = {"sid": sid, "client_secret": client_secret, "mxid": mxid}
if use_v2:
bind_url = "https://%s/_matrix/identity/v2/3pid/bind" % (id_server,)
headers["Authorization"] = self.create_id_access_token_header(
id_access_token
)
headers["Authorization"] = create_id_access_token_header(id_access_token)
else:
bind_url = "https://%s/_matrix/identity/api/v1/3pid/bind" % (id_server,)

Expand Down Expand Up @@ -478,3 +457,36 @@ def requestMsisdnToken(
except HttpResponseException as e:
logger.info("Proxied requestToken failed: %r", e)
raise e.to_synapse_error()


def create_id_access_token_header(id_access_token):
"""Create an Authorization header for passing to SimpleHttpClient as the header value
of an HTTP request.

Args:
id_access_token (str): An identity server access token.

Returns:
list[str]: The ascii-encoded bearer token encased in a list.
"""
# Prefix with Bearer
bearer_token = "Bearer %s" % id_access_token

# Encode headers to standard ascii
bearer_token.encode("ascii")

# Return as a list as that's how SimpleHttpClient takes header values
return [bearer_token]


class LookupAlgorithm:
"""
Supported hashing algorithms when performing a 3PID lookup.

SHA256 - Hashing an (address, medium, pepper) combo with sha256, then url-safe base64
encoding
NONE - Not performing any hashing. Simply sending an (address, medium) combo in plaintext
"""

SHA256 = "sha256"
NONE = "none"
4 changes: 3 additions & 1 deletion synapse/handlers/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,8 +579,8 @@ def create_room(self, requester, config, ratelimit=True, creator_join_profile=No

room_id = yield self._generate_room_id(creator_id=user_id, is_public=is_public)

directory_handler = self.hs.get_handlers().directory_handler
if room_alias:
directory_handler = self.hs.get_handlers().directory_handler
yield directory_handler.create_association(
requester=requester,
room_id=room_id,
Expand Down Expand Up @@ -665,6 +665,7 @@ def create_room(self, requester, config, ratelimit=True, creator_join_profile=No

for invite_3pid in invite_3pid_list:
id_server = invite_3pid["id_server"]
id_access_token = invite_3pid.get("id_access_token") # optional
address = invite_3pid["address"]
medium = invite_3pid["medium"]
yield self.hs.get_room_member_handler().do_3pid_invite(
Expand All @@ -675,6 +676,7 @@ def create_room(self, requester, config, ratelimit=True, creator_join_profile=No
id_server,
requester,
txn_id=None,
id_access_token=id_access_token,
)

result = {"room_id": room_id}
Expand Down
178 changes: 166 additions & 12 deletions synapse/handlers/room_member.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@
from synapse import types
from synapse.api.constants import EventTypes, Membership
from synapse.api.errors import AuthError, Codes, HttpResponseException, SynapseError
from synapse.handlers.identity import LookupAlgorithm, create_id_access_token_header
from synapse.types import RoomID, UserID
from synapse.util.async_helpers import Linearizer
from synapse.util.distributor import user_joined_room, user_left_room
from synapse.util.hash import sha256_and_url_safe_base64

from ._base import BaseHandler

Expand Down Expand Up @@ -626,7 +628,7 @@ def lookup_room_alias(self, room_alias):
servers.remove(room_alias.domain)
servers.insert(0, room_alias.domain)

return (RoomID.from_string(room_id), servers)
return RoomID.from_string(room_id), servers

@defer.inlineCallbacks
def _get_inviter(self, user_id, room_id):
Expand All @@ -638,7 +640,15 @@ def _get_inviter(self, user_id, room_id):

@defer.inlineCallbacks
def do_3pid_invite(
self, room_id, inviter, medium, address, id_server, requester, txn_id
self,
room_id,
inviter,
medium,
address,
id_server,
requester,
txn_id,
id_access_token=None,
):
if self.config.block_non_admin_invites:
is_requester_admin = yield self.auth.is_server_admin(requester.user)
Expand All @@ -661,7 +671,12 @@ def do_3pid_invite(
Codes.FORBIDDEN,
)

invitee = yield self._lookup_3pid(id_server, medium, address)
if not self._enable_lookup:
raise SynapseError(
403, "Looking up third-party identifiers is denied from this server"
)

invitee = yield self._lookup_3pid(id_server, medium, address, id_access_token)

if invitee:
yield self.update_membership(
Expand All @@ -673,9 +688,47 @@ def do_3pid_invite(
)

@defer.inlineCallbacks
def _lookup_3pid(self, id_server, medium, address):
def _lookup_3pid(self, id_server, medium, address, id_access_token=None):
"""Looks up a 3pid in the passed identity server.

Args:
id_server (str): The server name (including port, if required)
of the identity server to use.
medium (str): The type of the third party identifier (e.g. "email").
address (str): The third party identifier (e.g. "foo@example.com").
id_access_token (str|None): The access token to authenticate to the identity
server with

Returns:
str|None: the matrix ID of the 3pid, or None if it is not recognized.
"""
if id_access_token is not None:
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved
try:
results = yield self._lookup_3pid_v2(
id_server, id_access_token, medium, address
)
return results

except Exception as e:
# Catch HttpResponseExcept for a non-200 response code
# Check if this identity server does not know about v2 lookups
if isinstance(e, HttpResponseException) and e.code == 404:
# This is an old identity server that does not yet support v2 lookups
logger.warning(
"Attempted v2 lookup on v1 identity server %s. Falling "
"back to v1",
id_server,
)
else:
logger.warning("Error when looking up hashing details: %s", e)
return None

return (yield self._lookup_3pid_v1(id_server, medium, address))

@defer.inlineCallbacks
def _lookup_3pid_v1(self, id_server, medium, address):
"""Looks up a 3pid in the passed identity server using v1 lookup.

Args:
id_server (str): The server name (including port, if required)
of the identity server to use.
Expand All @@ -685,10 +738,6 @@ def _lookup_3pid(self, id_server, medium, address):
Returns:
str: the matrix ID of the 3pid, or None if it is not recognized.
"""
if not self._enable_lookup:
raise SynapseError(
403, "Looking up third-party identifiers is denied from this server"
)
try:
data = yield self.simple_http_client.get_json(
"%s%s/_matrix/identity/api/v1/lookup" % (id_server_scheme, id_server),
Expand All @@ -702,9 +751,116 @@ def _lookup_3pid(self, id_server, medium, address):
return data["mxid"]

except IOError as e:
logger.warn("Error from identity server lookup: %s" % (e,))
logger.warning("Error from v1 identity server lookup: %s" % (e,))

return None

@defer.inlineCallbacks
def _lookup_3pid_v2(self, id_server, id_access_token, medium, address):
"""Looks up a 3pid in the passed identity server using v2 lookup.

Args:
id_server (str): The server name (including port, if required)
of the identity server to use.
id_access_token (str): The access token to authenticate to the identity server with
richvdh marked this conversation as resolved.
Show resolved Hide resolved
medium (str): The type of the third party identifier (e.g. "email").
address (str): The third party identifier (e.g. "foo@example.com").

Returns:
Deferred[str|None]: the matrix ID of the 3pid, or None if it is not recognised.
"""
# Check what hashing details are supported by this identity server
hash_details = yield self.simple_http_client.get_json(
"%s%s/_matrix/identity/v2/hash_details" % (id_server_scheme, id_server),
{"access_token": id_access_token},
)

if not isinstance(hash_details, dict):
logger.warning(
"Got non-dict object when checking hash details of %s%s: %s",
id_server_scheme,
id_server,
hash_details,
)
raise SynapseError(
400,
"Non-dict object from %s%s during v2 hash_details request: %s"
% (id_server_scheme, id_server, hash_details),
)

# Extract information from hash_details
supported_lookup_algorithms = hash_details.get("algorithms")
lookup_pepper = hash_details.get("lookup_pepper")
if (
not supported_lookup_algorithms
or not isinstance(supported_lookup_algorithms, list)
or not lookup_pepper
or not isinstance(lookup_pepper, str)
):
raise SynapseError(
richvdh marked this conversation as resolved.
Show resolved Hide resolved
400,
"Invalid hash details received from identity server %s%s: %s"
% (id_server_scheme, id_server, hash_details),
)

# Check if any of the supported lookup algorithms are present
if LookupAlgorithm.SHA256 in supported_lookup_algorithms:
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved
# Perform a hashed lookup
lookup_algorithm = LookupAlgorithm.SHA256

# Hash address, medium and the pepper with sha256
to_hash = "%s %s %s" % (address, medium, lookup_pepper)
lookup_value = sha256_and_url_safe_base64(to_hash)

elif LookupAlgorithm.NONE in supported_lookup_algorithms:
# Perform a non-hashed lookup
lookup_algorithm = LookupAlgorithm.NONE

# Combine together plaintext address and medium
lookup_value = "%s %s" % (address, medium)

else:
logger.warning(
"None of the provided lookup algorithms of %s are supported: %s",
id_server,
supported_lookup_algorithms,
)
raise SynapseError(
400,
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved
"Provided identity server does not support any v2 lookup "
"algorithms that this homeserver supports.",
)

# Authenticate with identity server given the access token from the client
headers = {"Authorization": create_id_access_token_header(id_access_token)}

try:
lookup_results = yield self.simple_http_client.post_json_get_json(
"%s%s/_matrix/identity/v2/lookup" % (id_server_scheme, id_server),
{
"addresses": [lookup_value],
"algorithm": lookup_algorithm,
"pepper": lookup_pepper,
},
headers=headers,
)
except Exception as e:
logger.warning("Error when performing a v2 3pid lookup: %s", e)
raise SynapseError(
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved
500, "Unknown error occurred during identity server lookup"
)

# Check for a mapping from what we looked up to an MXID
if "mappings" not in lookup_results or not isinstance(
lookup_results["mappings"], dict
):
logger.warning("No results from 3pid lookup")
return None

# Return the MXID if it's available, or None otherwise
mxid = lookup_results["mappings"].get(lookup_value)
return mxid

@defer.inlineCallbacks
def _verify_any_signature(self, data, server_hostname):
if server_hostname not in data["signatures"]:
Expand Down Expand Up @@ -844,7 +1000,6 @@ def _ask_id_server_for_third_party_invite(
display_name (str): A user-friendly name to represent the invited
user.
"""

is_url = "%s%s/_matrix/identity/api/v1/store-invite" % (
id_server_scheme,
id_server,
Expand All @@ -862,7 +1017,6 @@ def _ask_id_server_for_third_party_invite(
"sender_display_name": inviter_display_name,
"sender_avatar_url": inviter_avatar_url,
}

try:
data = yield self.simple_http_client.post_json_get_json(
is_url, invite_config
Expand Down Expand Up @@ -1049,7 +1203,7 @@ def _remote_reject_invite(self, requester, remote_room_hosts, room_id, target):
# The 'except' clause is very broad, but we need to
# capture everything from DNS failures upwards
#
logger.warn("Failed to reject invite: %s", e)
logger.warning("Failed to reject invite: %s", e)

yield self.store.locally_reject_invite(target.to_string(), room_id)
return {}
Expand Down
1 change: 1 addition & 0 deletions synapse/rest/client/v1/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,7 @@ def on_POST(self, request, room_id, membership_action, txn_id=None):
content["id_server"],
requester,
txn_id,
content.get("id_access_token"),
)
return 200, {}

Expand Down
33 changes: 33 additions & 0 deletions synapse/util/hash.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# -*- 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.

import hashlib

import unpaddedbase64


def sha256_and_url_safe_base64(input_text):
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved
"""SHA256 hash an input string, encode the digest as url-safe base64, and
return

:param input_text: string to hash
:type input_text: str

:returns a sha256 hashed and url-safe base64 encoded digest
:rtype: str
"""
digest = hashlib.sha256(input_text.encode()).digest()
return unpaddedbase64.encode_base64(digest, urlsafe=True)