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

This is a redo of https://github.com/matrix-org/synapse/pull/5897 but with `id_access_token` accepted.

Implements [MSC2134](https://github.com/matrix-org/matrix-doc/pull/2134) plus Identity Service v2 authentication ala [MSC2140](https://github.com/matrix-org/matrix-doc/pull/2140).

Identity lookup-related functions were also moved from `RoomMemberHandler` to `IdentityHandler`.
This commit is contained in:
Andrew Morgan 2019-09-11 16:02:42 +01:00 committed by GitHub
parent cbcbfe64a2
commit 9fc71dc5ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 238 additions and 35 deletions

1
changelog.d/5897.feature Normal file
View File

@ -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)).

View File

@ -74,25 +74,6 @@ class IdentityHandler(BaseHandler):
id_access_token = creds.get("id_access_token") id_access_token = creds.get("id_access_token")
return client_secret, id_server, 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 @defer.inlineCallbacks
def threepid_from_creds(self, id_server, creds): def threepid_from_creds(self, id_server, creds):
""" """
@ -178,9 +159,7 @@ class IdentityHandler(BaseHandler):
bind_data = {"sid": sid, "client_secret": client_secret, "mxid": mxid} bind_data = {"sid": sid, "client_secret": client_secret, "mxid": mxid}
if use_v2: if use_v2:
bind_url = "https://%s/_matrix/identity/v2/3pid/bind" % (id_server,) bind_url = "https://%s/_matrix/identity/v2/3pid/bind" % (id_server,)
headers["Authorization"] = self.create_id_access_token_header( headers["Authorization"] = create_id_access_token_header(id_access_token)
id_access_token
)
else: else:
bind_url = "https://%s/_matrix/identity/api/v1/3pid/bind" % (id_server,) bind_url = "https://%s/_matrix/identity/api/v1/3pid/bind" % (id_server,)
@ -478,3 +457,36 @@ class IdentityHandler(BaseHandler):
except HttpResponseException as e: except HttpResponseException as e:
logger.info("Proxied requestToken failed: %r", e) logger.info("Proxied requestToken failed: %r", e)
raise e.to_synapse_error() 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"

View File

@ -579,8 +579,8 @@ class RoomCreationHandler(BaseHandler):
room_id = yield self._generate_room_id(creator_id=user_id, is_public=is_public) 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: if room_alias:
directory_handler = self.hs.get_handlers().directory_handler
yield directory_handler.create_association( yield directory_handler.create_association(
requester=requester, requester=requester,
room_id=room_id, room_id=room_id,
@ -665,6 +665,7 @@ class RoomCreationHandler(BaseHandler):
for invite_3pid in invite_3pid_list: for invite_3pid in invite_3pid_list:
id_server = invite_3pid["id_server"] id_server = invite_3pid["id_server"]
id_access_token = invite_3pid.get("id_access_token") # optional
address = invite_3pid["address"] address = invite_3pid["address"]
medium = invite_3pid["medium"] medium = invite_3pid["medium"]
yield self.hs.get_room_member_handler().do_3pid_invite( yield self.hs.get_room_member_handler().do_3pid_invite(
@ -675,6 +676,7 @@ class RoomCreationHandler(BaseHandler):
id_server, id_server,
requester, requester,
txn_id=None, txn_id=None,
id_access_token=id_access_token,
) )
result = {"room_id": room_id} result = {"room_id": room_id}

View File

@ -29,9 +29,11 @@ from twisted.internet import defer
from synapse import types from synapse import types
from synapse.api.constants import EventTypes, Membership from synapse.api.constants import EventTypes, Membership
from synapse.api.errors import AuthError, Codes, HttpResponseException, SynapseError 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.types import RoomID, UserID
from synapse.util.async_helpers import Linearizer from synapse.util.async_helpers import Linearizer
from synapse.util.distributor import user_joined_room, user_left_room 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 from ._base import BaseHandler
@ -626,7 +628,7 @@ class RoomMemberHandler(object):
servers.remove(room_alias.domain) servers.remove(room_alias.domain)
servers.insert(0, 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 @defer.inlineCallbacks
def _get_inviter(self, user_id, room_id): def _get_inviter(self, user_id, room_id):
@ -638,7 +640,15 @@ class RoomMemberHandler(object):
@defer.inlineCallbacks @defer.inlineCallbacks
def do_3pid_invite( 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: if self.config.block_non_admin_invites:
is_requester_admin = yield self.auth.is_server_admin(requester.user) is_requester_admin = yield self.auth.is_server_admin(requester.user)
@ -661,7 +671,12 @@ class RoomMemberHandler(object):
Codes.FORBIDDEN, 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: if invitee:
yield self.update_membership( yield self.update_membership(
@ -673,9 +688,47 @@ class RoomMemberHandler(object):
) )
@defer.inlineCallbacks @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. """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:
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: Args:
id_server (str): The server name (including port, if required) id_server (str): The server name (including port, if required)
of the identity server to use. of the identity server to use.
@ -685,10 +738,6 @@ class RoomMemberHandler(object):
Returns: Returns:
str: the matrix ID of the 3pid, or None if it is not recognized. 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: try:
data = yield self.simple_http_client.get_json( data = yield self.simple_http_client.get_json(
"%s%s/_matrix/identity/api/v1/lookup" % (id_server_scheme, id_server), "%s%s/_matrix/identity/api/v1/lookup" % (id_server_scheme, id_server),
@ -702,9 +751,116 @@ class RoomMemberHandler(object):
return data["mxid"] return data["mxid"]
except IOError as e: 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
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(
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:
# 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,
"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(
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 None
# Return the MXID if it's available, or None otherwise
mxid = lookup_results["mappings"].get(lookup_value)
return mxid
@defer.inlineCallbacks @defer.inlineCallbacks
def _verify_any_signature(self, data, server_hostname): def _verify_any_signature(self, data, server_hostname):
if server_hostname not in data["signatures"]: if server_hostname not in data["signatures"]:
@ -844,7 +1000,6 @@ class RoomMemberHandler(object):
display_name (str): A user-friendly name to represent the invited display_name (str): A user-friendly name to represent the invited
user. user.
""" """
is_url = "%s%s/_matrix/identity/api/v1/store-invite" % ( is_url = "%s%s/_matrix/identity/api/v1/store-invite" % (
id_server_scheme, id_server_scheme,
id_server, id_server,
@ -862,7 +1017,6 @@ class RoomMemberHandler(object):
"sender_display_name": inviter_display_name, "sender_display_name": inviter_display_name,
"sender_avatar_url": inviter_avatar_url, "sender_avatar_url": inviter_avatar_url,
} }
try: try:
data = yield self.simple_http_client.post_json_get_json( data = yield self.simple_http_client.post_json_get_json(
is_url, invite_config is_url, invite_config
@ -1049,7 +1203,7 @@ class RoomMemberMasterHandler(RoomMemberHandler):
# The 'except' clause is very broad, but we need to # The 'except' clause is very broad, but we need to
# capture everything from DNS failures upwards # 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) yield self.store.locally_reject_invite(target.to_string(), room_id)
return {} return {}

View File

@ -701,6 +701,7 @@ class RoomMembershipRestServlet(TransactionRestServlet):
content["id_server"], content["id_server"],
requester, requester,
txn_id, txn_id,
content.get("id_access_token"),
) )
return 200, {} return 200, {}

33
synapse/util/hash.py Normal file
View File

@ -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):
"""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)