Add config option for always using "userinfo endpoint" for OIDC (#7658)

This allows for connecting to certain IdPs, e.g. GitLab.
This commit is contained in:
BBBSnowball 2020-10-01 19:54:35 +02:00 committed by GitHub
parent 0b68577ed6
commit 05ee048f2c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 65 additions and 15 deletions

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

@ -0,0 +1 @@
Add a configuration option for always using the "userinfo endpoint" for OpenID Connect. This fixes support for some identity providers, e.g. GitLab. Contributed by Benjamin Koch.

View File

@ -238,13 +238,36 @@ Synapse config:
```yaml ```yaml
oidc_config: oidc_config:
enabled: true enabled: true
issuer: "https://id.twitch.tv/oauth2/" issuer: "https://id.twitch.tv/oauth2/"
client_id: "your-client-id" # TO BE FILLED client_id: "your-client-id" # TO BE FILLED
client_secret: "your-client-secret" # TO BE FILLED client_secret: "your-client-secret" # TO BE FILLED
client_auth_method: "client_secret_post" client_auth_method: "client_secret_post"
user_mapping_provider: user_mapping_provider:
config: config:
localpart_template: '{{ user.preferred_username }}' localpart_template: "{{ user.preferred_username }}"
display_name_template: '{{ user.name }}' display_name_template: "{{ user.name }}"
```
### GitLab
1. Create a [new application](https://gitlab.com/profile/applications).
2. Add the `read_user` and `openid` scopes.
3. Add this Callback URL: `[synapse public baseurl]/_synapse/oidc/callback`
Synapse config:
```yaml
oidc_config:
enabled: true
issuer: "https://gitlab.com/"
client_id: "your-client-id" # TO BE FILLED
client_secret: "your-client-secret" # TO BE FILLED
client_auth_method: "client_secret_post"
scopes: ["openid", "read_user"]
user_profile_method: "userinfo_endpoint"
user_mapping_provider:
config:
localpart_template: '{{ user.nickname }}'
display_name_template: '{{ user.name }}'
``` ```

View File

@ -1714,6 +1714,14 @@ oidc_config:
# #
#skip_verification: true #skip_verification: true
# Whether to fetch the user profile from the userinfo endpoint. Valid
# values are: "auto" or "userinfo_endpoint".
#
# Defaults to "auto", which fetches the userinfo endpoint if "openid" is included
# in `scopes`. Uncomment the following to always fetch the userinfo endpoint.
#
#user_profile_method: "userinfo_endpoint"
# Uncomment to allow a user logging in via OIDC to match a pre-existing account instead # Uncomment to allow a user logging in via OIDC to match a pre-existing account instead
# of failing. This could be used if switching from password logins to OIDC. Defaults to false. # of failing. This could be used if switching from password logins to OIDC. Defaults to false.
# #

View File

@ -56,6 +56,7 @@ class OIDCConfig(Config):
self.oidc_userinfo_endpoint = oidc_config.get("userinfo_endpoint") self.oidc_userinfo_endpoint = oidc_config.get("userinfo_endpoint")
self.oidc_jwks_uri = oidc_config.get("jwks_uri") self.oidc_jwks_uri = oidc_config.get("jwks_uri")
self.oidc_skip_verification = oidc_config.get("skip_verification", False) self.oidc_skip_verification = oidc_config.get("skip_verification", False)
self.oidc_user_profile_method = oidc_config.get("user_profile_method", "auto")
self.oidc_allow_existing_users = oidc_config.get("allow_existing_users", False) self.oidc_allow_existing_users = oidc_config.get("allow_existing_users", False)
ump_config = oidc_config.get("user_mapping_provider", {}) ump_config = oidc_config.get("user_mapping_provider", {})
@ -159,6 +160,14 @@ class OIDCConfig(Config):
# #
#skip_verification: true #skip_verification: true
# Whether to fetch the user profile from the userinfo endpoint. Valid
# values are: "auto" or "userinfo_endpoint".
#
# Defaults to "auto", which fetches the userinfo endpoint if "openid" is included
# in `scopes`. Uncomment the following to always fetch the userinfo endpoint.
#
#user_profile_method: "userinfo_endpoint"
# Uncomment to allow a user logging in via OIDC to match a pre-existing account instead # Uncomment to allow a user logging in via OIDC to match a pre-existing account instead
# of failing. This could be used if switching from password logins to OIDC. Defaults to false. # of failing. This could be used if switching from password logins to OIDC. Defaults to false.
# #

View File

@ -96,6 +96,7 @@ class OidcHandler:
self.hs = hs self.hs = hs
self._callback_url = hs.config.oidc_callback_url # type: str self._callback_url = hs.config.oidc_callback_url # type: str
self._scopes = hs.config.oidc_scopes # type: List[str] self._scopes = hs.config.oidc_scopes # type: List[str]
self._user_profile_method = hs.config.oidc_user_profile_method # type: str
self._client_auth = ClientAuth( self._client_auth = ClientAuth(
hs.config.oidc_client_id, hs.config.oidc_client_id,
hs.config.oidc_client_secret, hs.config.oidc_client_secret,
@ -196,11 +197,11 @@ class OidcHandler:
% (m["response_types_supported"],) % (m["response_types_supported"],)
) )
# If the openid scope was not requested, we need a userinfo endpoint to fetch user infos # Ensure there's a userinfo endpoint to fetch from if it is required.
if self._uses_userinfo: if self._uses_userinfo:
if m.get("userinfo_endpoint") is None: if m.get("userinfo_endpoint") is None:
raise ValueError( raise ValueError(
'provider has no "userinfo_endpoint", even though it is required because the "openid" scope is not requested' 'provider has no "userinfo_endpoint", even though it is required'
) )
else: else:
# If we're not using userinfo, we need a valid jwks to validate the ID token # If we're not using userinfo, we need a valid jwks to validate the ID token
@ -220,8 +221,10 @@ class OidcHandler:
``access_token`` with the ``userinfo_endpoint``. ``access_token`` with the ``userinfo_endpoint``.
""" """
# Maybe that should be user-configurable and not inferred? return (
return "openid" not in self._scopes "openid" not in self._scopes
or self._user_profile_method == "userinfo_endpoint"
)
async def load_metadata(self) -> OpenIDProviderMetadata: async def load_metadata(self) -> OpenIDProviderMetadata:
"""Load and validate the provider metadata. """Load and validate the provider metadata.

View File

@ -286,9 +286,15 @@ class OidcHandlerTestCase(HomeserverTestCase):
h._validate_metadata, h._validate_metadata,
) )
# Tests for configs that the userinfo endpoint # Tests for configs that require the userinfo endpoint
self.assertFalse(h._uses_userinfo) self.assertFalse(h._uses_userinfo)
h._scopes = [] # do not request the openid scope self.assertEqual(h._user_profile_method, "auto")
h._user_profile_method = "userinfo_endpoint"
self.assertTrue(h._uses_userinfo)
# Revert the profile method and do not request the "openid" scope.
h._user_profile_method = "auto"
h._scopes = []
self.assertTrue(h._uses_userinfo) self.assertTrue(h._uses_userinfo)
self.assertRaisesRegex(ValueError, "userinfo_endpoint", h._validate_metadata) self.assertRaisesRegex(ValueError, "userinfo_endpoint", h._validate_metadata)