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

Push login completion down into SsoHandler #8941

Merged
merged 2 commits into from
Dec 16, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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/8941.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for allowing users to pick their own user ID during a single-sign-on login.
62 changes: 27 additions & 35 deletions synapse/handlers/oidc_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,6 @@ def __init__(self, hs: "HomeServer"):
self._allow_existing_users = hs.config.oidc_allow_existing_users # type: bool

self._http_client = hs.get_proxied_http_client()
self._auth_handler = hs.get_auth_handler()
self._registration_handler = hs.get_registration_handler()
self._server_name = hs.config.server_name # type: str
self._macaroon_secret_key = hs.config.macaroon_secret_key

Expand Down Expand Up @@ -689,33 +687,14 @@ async def handle_oidc_callback(self, request: SynapseRequest) -> None:

# otherwise, it's a login

# Pull out the user-agent and IP from the request.
user_agent = request.get_user_agent("")
ip_address = self.hs.get_ip_from_request(request)

# Call the mapper to register/login the user
try:
user_id = await self._map_userinfo_to_user(
userinfo, token, user_agent, ip_address
await self._complete_oidc_login(
userinfo, token, request, client_redirect_url
)
except MappingException as e:
logger.exception("Could not map user")
self._sso_handler.render_error(request, "mapping_error", str(e))
return

# Mapping providers might not have get_extra_attributes: only call this
# method if it exists.
extra_attributes = None
get_extra_attributes = getattr(
self._user_mapping_provider, "get_extra_attributes", None
)
if get_extra_attributes:
extra_attributes = await get_extra_attributes(userinfo, token)

# and finally complete the login
await self._auth_handler.complete_sso_login(
user_id, request, client_redirect_url, extra_attributes
)

def _generate_oidc_session_token(
self,
Expand Down Expand Up @@ -838,10 +817,14 @@ def _verify_expiry(self, caveat: str) -> bool:
now = self.clock.time_msec()
return now < expiry

async def _map_userinfo_to_user(
self, userinfo: UserInfo, token: Token, user_agent: str, ip_address: str
) -> str:
"""Maps a UserInfo object to a mxid.
async def _complete_oidc_login(
self,
userinfo: UserInfo,
token: Token,
request: SynapseRequest,
client_redirect_url: str,
):
richvdh marked this conversation as resolved.
Show resolved Hide resolved
"""Given a UserInfo response, complete the login flow

UserInfo should have a claim that uniquely identifies users. This claim
is usually `sub`, but can be configured with `oidc_config.subject_claim`.
Expand All @@ -853,17 +836,16 @@ async def _map_userinfo_to_user(
If a user already exists with the mxid we've mapped and allow_existing_users
is disabled, raise an exception.

Otherwise, render a redirect back to the client_redirect_url with a loginToken.

Args:
userinfo: an object representing the user
token: a dict with the tokens obtained from the provider
user_agent: The user agent of the client making the request.
ip_address: The IP address of the client making the request.
request: The request to respond to
client_redirect_url: The redirect URL passed in by the client.

Raises:
MappingException: if there was an error while mapping some properties

Returns:
The mxid of the user
"""
try:
remote_user_id = self._remote_id_from_userinfo(userinfo)
Expand Down Expand Up @@ -931,13 +913,23 @@ async def grandfather_existing_users() -> Optional[str]:

return None

return await self._sso_handler.get_mxid_from_sso(
# Mapping providers might not have get_extra_attributes: only call this
# method if it exists.
extra_attributes = None
get_extra_attributes = getattr(
self._user_mapping_provider, "get_extra_attributes", None
)
if get_extra_attributes:
extra_attributes = await get_extra_attributes(userinfo, token)

await self._sso_handler.complete_sso_login_request(
self._auth_provider_id,
remote_user_id,
user_agent,
ip_address,
request,
client_redirect_url,
oidc_response_to_user_attributes,
grandfather_existing_users,
extra_attributes,
)

def _remote_id_from_userinfo(self, userinfo: UserInfo) -> str:
Expand Down
38 changes: 13 additions & 25 deletions synapse/handlers/saml_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,6 @@ def __init__(self, hs: "HomeServer"):
super().__init__(hs)
self._saml_client = Saml2Client(hs.config.saml2_sp_config)
self._saml_idp_entityid = hs.config.saml2_idp_entityid
self._auth_handler = hs.get_auth_handler()
self._registration_handler = hs.get_registration_handler()

self._saml2_session_lifetime = hs.config.saml2_session_lifetime
self._grandfathered_mxid_source_attribute = (
Expand Down Expand Up @@ -229,40 +227,29 @@ async def _handle_authn_response(
)
return

# Pull out the user-agent and IP from the request.
user_agent = request.get_user_agent("")
ip_address = self.hs.get_ip_from_request(request)

# Call the mapper to register/login the user
try:
user_id = await self._map_saml_response_to_user(
saml2_auth, relay_state, user_agent, ip_address
)
await self._complete_saml_login(saml2_auth, request, relay_state)
except MappingException as e:
logger.exception("Could not map user")
self._sso_handler.render_error(request, "mapping_error", str(e))
return

await self._auth_handler.complete_sso_login(user_id, request, relay_state)

async def _map_saml_response_to_user(
async def _complete_saml_login(
self,
saml2_auth: saml2.response.AuthnResponse,
request: SynapseRequest,
client_redirect_url: str,
user_agent: str,
ip_address: str,
) -> str:
) -> None:
"""
Given a SAML response, retrieve the user ID for it and possibly register the user.
Given a SAML response, complete the login flow

Retrieves the remote user ID, registers the user if necessary, and serves
a redirect back to the client with a login-token.

Args:
saml2_auth: The parsed SAML2 response.
request: The request to respond to
client_redirect_url: The redirect URL passed in by the client.
user_agent: The user agent of the client making the request.
ip_address: The IP address of the client making the request.

Returns:
The user ID associated with this response.

Raises:
MappingException if there was a problem mapping the response to a user.
Expand Down Expand Up @@ -318,13 +305,14 @@ async def grandfather_existing_users() -> Optional[str]:

return None

return await self._sso_handler.get_mxid_from_sso(
await self._sso_handler.complete_sso_login_request(
self._auth_provider_id,
remote_user_id,
user_agent,
ip_address,
request,
client_redirect_url,
saml_response_to_remapped_user_attributes,
grandfather_existing_users,
None,
)

def _remote_id_from_saml_response(
Expand Down
58 changes: 36 additions & 22 deletions synapse/handlers/sso.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@

from synapse.api.errors import RedirectException
from synapse.http.server import respond_with_html
from synapse.types import UserID, contains_invalid_mxid_characters
from synapse.http.site import SynapseRequest
from synapse.types import JsonDict, UserID, contains_invalid_mxid_characters
from synapse.util.async_helpers import Linearizer

if TYPE_CHECKING:
Expand Down Expand Up @@ -119,15 +120,16 @@ async def get_sso_user_by_remote_user_id(
# No match.
return None

async def get_mxid_from_sso(
async def complete_sso_login_request(
self,
auth_provider_id: str,
remote_user_id: str,
user_agent: str,
ip_address: str,
request: SynapseRequest,
client_redirect_url: str,
sso_to_matrix_id_mapper: Callable[[int], Awaitable[UserAttributes]],
grandfather_existing_users: Optional[Callable[[], Awaitable[Optional[str]]]],
) -> str:
extra_login_attributes: Optional[JsonDict],
richvdh marked this conversation as resolved.
Show resolved Hide resolved
) -> None:
"""
Given an SSO ID, retrieve the user ID for it and possibly register the user.

Expand All @@ -146,12 +148,18 @@ async def get_mxid_from_sso(
given user-agent and IP address and the SSO ID is linked to this matrix
ID for subsequent calls.

Finally, we generate a redirect to the supplied redirect uri, with a login token

Args:
auth_provider_id: A unique identifier for this SSO provider, e.g.
"oidc" or "saml".

remote_user_id: The unique identifier from the SSO provider.
user_agent: The user agent of the client making the request.
ip_address: The IP address of the client making the request.

request: The request to respond to

client_redirect_url: The redirect URL passed in by the client.

sso_to_matrix_id_mapper: A callable to generate the user attributes.
The only parameter is an integer which represents the amount of
times the returned mxid localpart mapping has failed.
Expand All @@ -163,12 +171,13 @@ async def get_mxid_from_sso(
to the user.
RedirectException to redirect to an additional page (e.g.
to prompt the user for more information).

grandfather_existing_users: A callable which can return an previously
existing matrix ID. The SSO ID is then linked to the returned
matrix ID.

Returns:
The user ID associated with the SSO response.
extra_login_attributes: An optional dictionary of extra
attributes to be provided to the client in the login response.

Raises:
MappingException if there was a problem mapping the response to a user.
Expand All @@ -181,28 +190,33 @@ async def get_mxid_from_sso(
# interstitial pages.
with await self._mapping_lock.queue(auth_provider_id):
# first of all, check if we already have a mapping for this user
previously_registered_user_id = await self.get_sso_user_by_remote_user_id(
user_id = await self.get_sso_user_by_remote_user_id(
auth_provider_id, remote_user_id,
)
if previously_registered_user_id:
return previously_registered_user_id

# Check for grandfathering of users.
if grandfather_existing_users:
previously_registered_user_id = await grandfather_existing_users()
if previously_registered_user_id:
if not user_id and grandfather_existing_users:
user_id = await grandfather_existing_users()
if user_id:
# Future logins should also match this user ID.
await self._store.record_user_external_id(
auth_provider_id, remote_user_id, previously_registered_user_id
auth_provider_id, remote_user_id, user_id
)
return previously_registered_user_id

# Otherwise, generate a new user.
attributes = await self._call_attribute_mapper(sso_to_matrix_id_mapper)
user_id = await self._register_mapped_user(
attributes, auth_provider_id, remote_user_id, user_agent, ip_address,
)
return user_id
if not user_id:
attributes = await self._call_attribute_mapper(sso_to_matrix_id_mapper)
user_id = await self._register_mapped_user(
attributes,
auth_provider_id,
remote_user_id,
request.get_user_agent(""),
request.getClientIP(),
)

await self._auth_handler.complete_sso_login(
user_id, request, client_redirect_url, extra_login_attributes
)

async def _call_attribute_mapper(
self, sso_to_matrix_id_mapper: Callable[[int], Awaitable[UserAttributes]],
Expand Down
8 changes: 4 additions & 4 deletions tests/handlers/test_saml.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ def test_map_saml_response_to_user(self):

# check that the auth handler got called as expected
auth_handler.complete_sso_login.assert_called_once_with(
"@test_user:test", request, "redirect_uri"
"@test_user:test", request, "redirect_uri", None
)

@override_config({"saml2_config": {"grandfathered_mxid_source_attribute": "mxid"}})
Expand All @@ -157,7 +157,7 @@ def test_map_saml_response_to_existing_user(self):

# check that the auth handler got called as expected
auth_handler.complete_sso_login.assert_called_once_with(
"@test_user:test", request, ""
"@test_user:test", request, "", None
)

# Subsequent calls should map to the same mxid.
Expand All @@ -166,7 +166,7 @@ def test_map_saml_response_to_existing_user(self):
self.handler._handle_authn_response(request, saml_response, "")
)
auth_handler.complete_sso_login.assert_called_once_with(
"@test_user:test", request, ""
"@test_user:test", request, "", None
)

def test_map_saml_response_to_invalid_localpart(self):
Expand Down Expand Up @@ -214,7 +214,7 @@ def test_map_saml_response_to_user_retries(self):

# test_user is already taken, so test_user1 gets registered instead.
auth_handler.complete_sso_login.assert_called_once_with(
"@test_user1:test", request, ""
"@test_user1:test", request, "", None
)
auth_handler.complete_sso_login.reset_mock()

Expand Down