This repository was archived by the owner on Apr 26, 2024. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Support trying multiple localparts for OpenID Connect. #8801
Merged
Merged
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
24265fa
Support trying multiple localparts for OpenID Connect.
clokep f4bdc2f
Add an additional test.
clokep f419ff5
Abstract the shared user mapping code from SAML and OIDC.
clokep 7e6a549
Do not pass through parameters to the mapper.
clokep c34e973
Do not retry Matrix IDs which will keep failing.
clokep 1f64e02
Support matching an existing username for OIDC.
clokep eda3e50
Clarify some of the type hints.
clokep c0ae560
Merge remote-tracking branch 'origin/develop' into clokep/oidc-localp…
clokep d3c4e04
Add a newsfragment.
clokep 0ded8b1
Fix some comments.
clokep c29d8de
Remove a reference to SAML in the abstract code.
clokep 512529f
Update the changelog based on feedback.
clokep 9c5d0f6
Expand comments and add a return type.
clokep 1934c10
Append the number of failures by default when mapping OIDC IDs.
clokep 89f1869
Do not use a deprecated function.
clokep 6d11ff8
Add an additional comment.
clokep 18fcaee
Clarify another comment.
clokep File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Add support for re-trying generation of a localpart for OpenID Connect mapping providers. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ | |
| # 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 inspect | ||
| import logging | ||
| from typing import TYPE_CHECKING, Dict, Generic, List, Optional, Tuple, TypeVar | ||
| from urllib.parse import urlencode | ||
|
|
@@ -35,15 +36,10 @@ | |
|
|
||
| from synapse.config import ConfigError | ||
| from synapse.handlers._base import BaseHandler | ||
| from synapse.handlers.sso import MappingException | ||
| from synapse.handlers.sso import MappingException, UserAttributes | ||
| from synapse.http.site import SynapseRequest | ||
| from synapse.logging.context import make_deferred_yieldable | ||
| from synapse.types import ( | ||
| JsonDict, | ||
| UserID, | ||
| contains_invalid_mxid_characters, | ||
| map_username_to_mxid_localpart, | ||
| ) | ||
| from synapse.types import JsonDict, map_username_to_mxid_localpart | ||
| from synapse.util import json_decoder | ||
|
|
||
| if TYPE_CHECKING: | ||
|
|
@@ -869,73 +865,51 @@ async def _map_userinfo_to_user( | |
| # to be strings. | ||
| remote_user_id = str(remote_user_id) | ||
|
|
||
| # first of all, check if we already have a mapping for this user | ||
| previously_registered_user_id = await self._sso_handler.get_sso_user_by_remote_user_id( | ||
| self._auth_provider_id, remote_user_id, | ||
| # Older mapping providers don't accept the `failures` argument, so we | ||
| # try and detect support. | ||
| mapper_signature = inspect.signature( | ||
| self._user_mapping_provider.map_user_attributes | ||
| ) | ||
| if previously_registered_user_id: | ||
| return previously_registered_user_id | ||
| supports_failures = "failures" in mapper_signature.parameters | ||
|
|
||
| # Otherwise, generate a new user. | ||
| try: | ||
| attributes = await self._user_mapping_provider.map_user_attributes( | ||
| userinfo, token | ||
| ) | ||
| except Exception as e: | ||
| raise MappingException( | ||
| "Could not extract user attributes from OIDC response: " + str(e) | ||
| ) | ||
| async def oidc_response_to_user_attributes(failures: int) -> UserAttributes: | ||
| """ | ||
| Call the mapping provider to map the OIDC userinfo and token to user attributes. | ||
|
|
||
| logger.debug( | ||
| "Retrieved user attributes from user mapping provider: %r", attributes | ||
| ) | ||
| This is backwards compatibility for abstraction for the SSO handler. | ||
| """ | ||
| if supports_failures: | ||
| attributes = await self._user_mapping_provider.map_user_attributes( | ||
| userinfo, token, failures | ||
| ) | ||
| else: | ||
| # If the mapping provider does not support processing failures, | ||
| # do not continually generate the same Matrix ID since it will | ||
| # continue to already be in use. Note that the error raised is | ||
| # arbitrary and will get turned into a MappingException. | ||
| if failures: | ||
| raise RuntimeError( | ||
| "Mapping provider does not support de-duplicating Matrix IDs" | ||
| ) | ||
|
|
||
| localpart = attributes["localpart"] | ||
| if not localpart: | ||
| raise MappingException( | ||
| "Error parsing OIDC response: OIDC mapping provider plugin " | ||
| "did not return a localpart value" | ||
| ) | ||
| attributes = await self._user_mapping_provider.map_user_attributes( # type: ignore | ||
| userinfo, token | ||
| ) | ||
|
|
||
| user_id = UserID(localpart, self.server_name).to_string() | ||
| users = await self.store.get_users_by_id_case_insensitive(user_id) | ||
| if users: | ||
| if self._allow_existing_users: | ||
| if len(users) == 1: | ||
| registered_user_id = next(iter(users)) | ||
| elif user_id in users: | ||
| registered_user_id = user_id | ||
| else: | ||
| raise MappingException( | ||
| "Attempted to login as '{}' but it matches more than one user inexactly: {}".format( | ||
| user_id, list(users.keys()) | ||
| ) | ||
| ) | ||
| else: | ||
| # This mxid is taken | ||
| raise MappingException("mxid '{}' is already taken".format(user_id)) | ||
| else: | ||
| # Since the localpart is provided via a potentially untrusted module, | ||
| # ensure the MXID is valid before registering. | ||
| if contains_invalid_mxid_characters(localpart): | ||
| raise MappingException("localpart is invalid: %s" % (localpart,)) | ||
|
|
||
| # It's the first time this user is logging in and the mapped mxid was | ||
| # not taken, register the user | ||
| registered_user_id = await self._registration_handler.register_user( | ||
| localpart=localpart, | ||
| default_display_name=attributes["display_name"], | ||
| user_agent_ips=[(user_agent, ip_address)], | ||
| ) | ||
| return UserAttributes(**attributes) | ||
|
|
||
| await self.store.record_user_external_id( | ||
| self._auth_provider_id, remote_user_id, registered_user_id, | ||
| return await self._sso_handler.get_mxid_from_sso( | ||
| self._auth_provider_id, | ||
| remote_user_id, | ||
| user_agent, | ||
| ip_address, | ||
| oidc_response_to_user_attributes, | ||
| self._allow_existing_users, | ||
| ) | ||
| return registered_user_id | ||
|
|
||
|
|
||
| UserAttribute = TypedDict( | ||
| "UserAttribute", {"localpart": str, "display_name": Optional[str]} | ||
| UserAttributeDict = TypedDict( | ||
| "UserAttributeDict", {"localpart": str, "display_name": Optional[str]} | ||
| ) | ||
| C = TypeVar("C") | ||
|
|
||
|
|
@@ -978,13 +952,15 @@ def get_remote_user_id(self, userinfo: UserInfo) -> str: | |
| raise NotImplementedError() | ||
|
|
||
| async def map_user_attributes( | ||
| self, userinfo: UserInfo, token: Token | ||
| ) -> UserAttribute: | ||
| self, userinfo: UserInfo, token: Token, failures: int | ||
| ) -> UserAttributeDict: | ||
| """Map a `UserInfo` object into user attributes. | ||
|
|
||
| Args: | ||
| userinfo: An object representing the user given by the OIDC provider | ||
| token: A dict with the tokens returned by the provider | ||
| failures: How many times a call to this function with this | ||
| UserInfo has resulted in a failure. | ||
|
|
||
| Returns: | ||
| A dict containing the ``localpart`` and (optionally) the ``display_name`` | ||
|
|
@@ -1084,13 +1060,17 @@ def get_remote_user_id(self, userinfo: UserInfo) -> str: | |
| return userinfo[self._config.subject_claim] | ||
|
|
||
| async def map_user_attributes( | ||
| self, userinfo: UserInfo, token: Token | ||
| ) -> UserAttribute: | ||
| self, userinfo: UserInfo, token: Token, failures: int | ||
| ) -> UserAttributeDict: | ||
| localpart = self._config.localpart_template.render(user=userinfo).strip() | ||
|
|
||
| # Ensure only valid characters are included in the MXID. | ||
| localpart = map_username_to_mxid_localpart(localpart) | ||
|
|
||
| # Append suffix integer if last call to this function failed to produce | ||
| # a usable mxid. | ||
| localpart += str(failures) if failures else "" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am quite hesitant about this as a) it'll explode if we have more users with same localpart than the number of retries, and b) there isn't a way of writing this particular logic correctly with this API? I know that this is what we do for other SSO, but it feels like a footgun that will one day hit us (especially if we decided to reduce the number of retries to a sensible number). I don't suggest we fix this here, but might be worth at least making an issue for it.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Filed #8813 |
||
|
|
||
| display_name = None # type: Optional[str] | ||
| if self._config.display_name_template is not None: | ||
| display_name = self._config.display_name_template.render( | ||
|
|
@@ -1100,7 +1080,7 @@ async def map_user_attributes( | |
| if display_name == "": | ||
| display_name = None | ||
|
|
||
| return UserAttribute(localpart=localpart, display_name=display_name) | ||
| return UserAttributeDict(localpart=localpart, display_name=display_name) | ||
|
|
||
| async def get_extra_attributes(self, userinfo: UserInfo, token: Token) -> JsonDict: | ||
| extras = {} # type: Dict[str, str] | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.