Land support for multiple OIDC providers (#9110)

This is the final step for supporting multiple OIDC providers concurrently.

First of all, we reorganise the config so that you can specify a list of OIDC providers, instead of a single one. Before:

    oidc_config:
       enabled: true
       issuer: "https://oidc_provider"
       # etc

After:

    oidc_providers:
     - idp_id: prov1
       issuer: "https://oidc_provider"

     - idp_id: prov2
       issuer: "https://another_oidc_provider"

The old format is still grandfathered in.

With that done, it's then simply a matter of having OidcHandler instantiate a new OidcProvider for each configured provider.
This commit is contained in:
Richard van der Hoff 2021-01-15 16:55:29 +00:00 committed by GitHub
parent 3e4cdfe5d9
commit 9de6b94117
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 447 additions and 373 deletions

1
changelog.d/9110.feature Normal file
View file

@ -0,0 +1 @@
Add support for multiple SSO Identity Providers.

View file

@ -42,11 +42,10 @@ as follows:
* For other installation mechanisms, see the documentation provided by the * For other installation mechanisms, see the documentation provided by the
maintainer. maintainer.
To enable the OpenID integration, you should then add an `oidc_config` section To enable the OpenID integration, you should then add a section to the `oidc_providers`
to your configuration file (or uncomment the `enabled: true` line in the setting in your configuration file (or uncomment one of the existing examples).
existing section). See [sample_config.yaml](./sample_config.yaml) for some See [sample_config.yaml](./sample_config.yaml) for some sample settings, as well as
sample settings, as well as the text below for example configurations for the text below for example configurations for specific providers.
specific providers.
## Sample configs ## Sample configs
@ -62,20 +61,21 @@ Directory (tenant) ID as it will be used in the Azure links.
Edit your Synapse config file and change the `oidc_config` section: Edit your Synapse config file and change the `oidc_config` section:
```yaml ```yaml
oidc_config: oidc_providers:
enabled: true - idp_id: microsoft
issuer: "https://login.microsoftonline.com/<tenant id>/v2.0" idp_name: Microsoft
client_id: "<client id>" issuer: "https://login.microsoftonline.com/<tenant id>/v2.0"
client_secret: "<client secret>" client_id: "<client id>"
scopes: ["openid", "profile"] client_secret: "<client secret>"
authorization_endpoint: "https://login.microsoftonline.com/<tenant id>/oauth2/v2.0/authorize" scopes: ["openid", "profile"]
token_endpoint: "https://login.microsoftonline.com/<tenant id>/oauth2/v2.0/token" authorization_endpoint: "https://login.microsoftonline.com/<tenant id>/oauth2/v2.0/authorize"
userinfo_endpoint: "https://graph.microsoft.com/oidc/userinfo" token_endpoint: "https://login.microsoftonline.com/<tenant id>/oauth2/v2.0/token"
userinfo_endpoint: "https://graph.microsoft.com/oidc/userinfo"
user_mapping_provider: user_mapping_provider:
config: config:
localpart_template: "{{ user.preferred_username.split('@')[0] }}" localpart_template: "{{ user.preferred_username.split('@')[0] }}"
display_name_template: "{{ user.name }}" display_name_template: "{{ user.name }}"
``` ```
### [Dex][dex-idp] ### [Dex][dex-idp]
@ -103,17 +103,18 @@ Run with `dex serve examples/config-dev.yaml`.
Synapse config: Synapse config:
```yaml ```yaml
oidc_config: oidc_providers:
enabled: true - idp_id: dex
skip_verification: true # This is needed as Dex is served on an insecure endpoint idp_name: "My Dex server"
issuer: "http://127.0.0.1:5556/dex" skip_verification: true # This is needed as Dex is served on an insecure endpoint
client_id: "synapse" issuer: "http://127.0.0.1:5556/dex"
client_secret: "secret" client_id: "synapse"
scopes: ["openid", "profile"] client_secret: "secret"
user_mapping_provider: scopes: ["openid", "profile"]
config: user_mapping_provider:
localpart_template: "{{ user.name }}" config:
display_name_template: "{{ user.name|capitalize }}" localpart_template: "{{ user.name }}"
display_name_template: "{{ user.name|capitalize }}"
``` ```
### [Keycloak][keycloak-idp] ### [Keycloak][keycloak-idp]
@ -152,16 +153,17 @@ Follow the [Getting Started Guide](https://www.keycloak.org/getting-started) to
8. Copy Secret 8. Copy Secret
```yaml ```yaml
oidc_config: oidc_providers:
enabled: true - idp_id: keycloak
issuer: "https://127.0.0.1:8443/auth/realms/{realm_name}" idp_name: "My KeyCloak server"
client_id: "synapse" issuer: "https://127.0.0.1:8443/auth/realms/{realm_name}"
client_secret: "copy secret generated from above" client_id: "synapse"
scopes: ["openid", "profile"] client_secret: "copy secret generated from above"
user_mapping_provider: scopes: ["openid", "profile"]
config: user_mapping_provider:
localpart_template: "{{ user.preferred_username }}" config:
display_name_template: "{{ user.name }}" localpart_template: "{{ user.preferred_username }}"
display_name_template: "{{ user.name }}"
``` ```
### [Auth0][auth0] ### [Auth0][auth0]
@ -191,16 +193,17 @@ oidc_config:
Synapse config: Synapse config:
```yaml ```yaml
oidc_config: oidc_providers:
enabled: true - idp_id: auth0
issuer: "https://your-tier.eu.auth0.com/" # TO BE FILLED idp_name: Auth0
client_id: "your-client-id" # TO BE FILLED issuer: "https://your-tier.eu.auth0.com/" # TO BE FILLED
client_secret: "your-client-secret" # TO BE FILLED client_id: "your-client-id" # TO BE FILLED
scopes: ["openid", "profile"] client_secret: "your-client-secret" # TO BE FILLED
user_mapping_provider: scopes: ["openid", "profile"]
config: user_mapping_provider:
localpart_template: "{{ user.preferred_username }}" config:
display_name_template: "{{ user.name }}" localpart_template: "{{ user.preferred_username }}"
display_name_template: "{{ user.name }}"
``` ```
### GitHub ### GitHub
@ -219,21 +222,22 @@ does not return a `sub` property, an alternative `subject_claim` has to be set.
Synapse config: Synapse config:
```yaml ```yaml
oidc_config: oidc_providers:
enabled: true - idp_id: github
discover: false idp_name: Github
issuer: "https://github.com/" discover: false
client_id: "your-client-id" # TO BE FILLED issuer: "https://github.com/"
client_secret: "your-client-secret" # TO BE FILLED client_id: "your-client-id" # TO BE FILLED
authorization_endpoint: "https://github.com/login/oauth/authorize" client_secret: "your-client-secret" # TO BE FILLED
token_endpoint: "https://github.com/login/oauth/access_token" authorization_endpoint: "https://github.com/login/oauth/authorize"
userinfo_endpoint: "https://api.github.com/user" token_endpoint: "https://github.com/login/oauth/access_token"
scopes: ["read:user"] userinfo_endpoint: "https://api.github.com/user"
user_mapping_provider: scopes: ["read:user"]
config: user_mapping_provider:
subject_claim: "id" config:
localpart_template: "{{ user.login }}" subject_claim: "id"
display_name_template: "{{ user.name }}" localpart_template: "{{ user.login }}"
display_name_template: "{{ user.name }}"
``` ```
### [Google][google-idp] ### [Google][google-idp]
@ -243,16 +247,17 @@ oidc_config:
2. add an "OAuth Client ID" for a Web Application under "Credentials". 2. add an "OAuth Client ID" for a Web Application under "Credentials".
3. Copy the Client ID and Client Secret, and add the following to your synapse config: 3. Copy the Client ID and Client Secret, and add the following to your synapse config:
```yaml ```yaml
oidc_config: oidc_providers:
enabled: true - idp_id: google
issuer: "https://accounts.google.com/" idp_name: Google
client_id: "your-client-id" # TO BE FILLED issuer: "https://accounts.google.com/"
client_secret: "your-client-secret" # TO BE FILLED client_id: "your-client-id" # TO BE FILLED
scopes: ["openid", "profile"] client_secret: "your-client-secret" # TO BE FILLED
user_mapping_provider: scopes: ["openid", "profile"]
config: user_mapping_provider:
localpart_template: "{{ user.given_name|lower }}" config:
display_name_template: "{{ user.name }}" localpart_template: "{{ user.given_name|lower }}"
display_name_template: "{{ user.name }}"
``` ```
4. Back in the Google console, add this Authorized redirect URI: `[synapse 4. Back in the Google console, add this Authorized redirect URI: `[synapse
public baseurl]/_synapse/oidc/callback`. public baseurl]/_synapse/oidc/callback`.
@ -266,16 +271,17 @@ oidc_config:
Synapse config: Synapse config:
```yaml ```yaml
oidc_config: oidc_providers:
enabled: true - idp_id: twitch
issuer: "https://id.twitch.tv/oauth2/" idp_name: Twitch
client_id: "your-client-id" # TO BE FILLED issuer: "https://id.twitch.tv/oauth2/"
client_secret: "your-client-secret" # TO BE FILLED client_id: "your-client-id" # TO BE FILLED
client_auth_method: "client_secret_post" client_secret: "your-client-secret" # TO BE FILLED
user_mapping_provider: client_auth_method: "client_secret_post"
config: user_mapping_provider:
localpart_template: "{{ user.preferred_username }}" config:
display_name_template: "{{ user.name }}" localpart_template: "{{ user.preferred_username }}"
display_name_template: "{{ user.name }}"
``` ```
### GitLab ### GitLab
@ -287,16 +293,17 @@ oidc_config:
Synapse config: Synapse config:
```yaml ```yaml
oidc_config: oidc_providers:
enabled: true - idp_id: gitlab
issuer: "https://gitlab.com/" idp_name: Gitlab
client_id: "your-client-id" # TO BE FILLED issuer: "https://gitlab.com/"
client_secret: "your-client-secret" # TO BE FILLED client_id: "your-client-id" # TO BE FILLED
client_auth_method: "client_secret_post" client_secret: "your-client-secret" # TO BE FILLED
scopes: ["openid", "read_user"] client_auth_method: "client_secret_post"
user_profile_method: "userinfo_endpoint" scopes: ["openid", "read_user"]
user_mapping_provider: user_profile_method: "userinfo_endpoint"
config: user_mapping_provider:
localpart_template: '{{ user.nickname }}' config:
display_name_template: '{{ user.name }}' localpart_template: '{{ user.nickname }}'
display_name_template: '{{ user.name }}'
``` ```

View file

@ -1709,141 +1709,149 @@ saml2_config:
#idp_entityid: 'https://our_idp/entityid' #idp_entityid: 'https://our_idp/entityid'
# Enable OpenID Connect (OIDC) / OAuth 2.0 for registration and login. # List of OpenID Connect (OIDC) / OAuth 2.0 identity providers, for registration
# and login.
#
# Options for each entry include:
#
# idp_id: a unique identifier for this identity provider. Used internally
# by Synapse; should be a single word such as 'github'.
#
# Note that, if this is changed, users authenticating via that provider
# will no longer be recognised as the same user!
#
# idp_name: A user-facing name for this identity provider, which is used to
# offer the user a choice of login mechanisms.
#
# discover: set to 'false' to disable the use of the OIDC discovery mechanism
# to discover endpoints. Defaults to true.
#
# issuer: Required. The OIDC issuer. Used to validate tokens and (if discovery
# is enabled) to discover the provider's endpoints.
#
# client_id: Required. oauth2 client id to use.
#
# client_secret: Required. oauth2 client secret to use.
#
# client_auth_method: auth method to use when exchanging the token. Valid
# values are 'client_secret_basic' (default), 'client_secret_post' and
# 'none'.
#
# scopes: list of scopes to request. This should normally include the "openid"
# scope. Defaults to ["openid"].
#
# authorization_endpoint: the oauth2 authorization endpoint. Required if
# provider discovery is disabled.
#
# token_endpoint: the oauth2 token endpoint. Required if provider discovery is
# disabled.
#
# userinfo_endpoint: the OIDC userinfo endpoint. Required if discovery is
# disabled and the 'openid' scope is not requested.
#
# jwks_uri: URI where to fetch the JWKS. Required if discovery is disabled and
# the 'openid' scope is used.
#
# skip_verification: set to 'true' to skip metadata verification. Use this if
# you are connecting to a provider that is not OpenID Connect compliant.
# Defaults to false. Avoid this in production.
#
# user_profile_method: 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'. Set to 'userinfo_endpoint' to always fetch the
# userinfo endpoint.
#
# allow_existing_users: set to 'true' 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.
#
# user_mapping_provider: Configuration for how attributes returned from a OIDC
# provider are mapped onto a matrix user. This setting has the following
# sub-properties:
#
# module: The class name of a custom mapping module. Default is
# 'synapse.handlers.oidc_handler.JinjaOidcMappingProvider'.
# See https://github.com/matrix-org/synapse/blob/master/docs/sso_mapping_providers.md#openid-mapping-providers
# for information on implementing a custom mapping provider.
#
# config: Configuration for the mapping provider module. This section will
# be passed as a Python dictionary to the user mapping provider
# module's `parse_config` method.
#
# For the default provider, the following settings are available:
#
# sub: name of the claim containing a unique identifier for the
# user. Defaults to 'sub', which OpenID Connect compliant
# providers should provide.
#
# localpart_template: Jinja2 template for the localpart of the MXID.
# If this is not set, the user will be prompted to choose their
# own username.
#
# display_name_template: Jinja2 template for the display name to set
# on first login. If unset, no displayname will be set.
#
# extra_attributes: a map of Jinja2 templates for extra attributes
# to send back to the client during login.
# Note that these are non-standard and clients will ignore them
# without modifications.
#
# When rendering, the Jinja2 templates are given a 'user' variable,
# which is set to the claims returned by the UserInfo Endpoint and/or
# in the ID Token.
# #
# See https://github.com/matrix-org/synapse/blob/master/docs/openid.md # See https://github.com/matrix-org/synapse/blob/master/docs/openid.md
# for some example configurations. # for information on how to configure these options.
# #
oidc_config: # For backwards compatibility, it is also possible to configure a single OIDC
# Uncomment the following to enable authorization against an OpenID Connect # provider via an 'oidc_config' setting. This is now deprecated and admins are
# server. Defaults to false. # advised to migrate to the 'oidc_providers' format.
#
oidc_providers:
# Generic example
# #
#enabled: true #- idp_id: my_idp
# idp_name: "My OpenID provider"
# discover: false
# issuer: "https://accounts.example.com/"
# client_id: "provided-by-your-issuer"
# client_secret: "provided-by-your-issuer"
# client_auth_method: client_secret_post
# scopes: ["openid", "profile"]
# authorization_endpoint: "https://accounts.example.com/oauth2/auth"
# token_endpoint: "https://accounts.example.com/oauth2/token"
# userinfo_endpoint: "https://accounts.example.com/userinfo"
# jwks_uri: "https://accounts.example.com/.well-known/jwks.json"
# skip_verification: true
# Uncomment the following to disable use of the OIDC discovery mechanism to # For use with Keycloak
# discover endpoints. Defaults to true.
# #
#discover: false #- idp_id: keycloak
# idp_name: Keycloak
# issuer: "https://127.0.0.1:8443/auth/realms/my_realm_name"
# client_id: "synapse"
# client_secret: "copy secret generated in Keycloak UI"
# scopes: ["openid", "profile"]
# the OIDC issuer. Used to validate tokens and (if discovery is enabled) to # For use with Github
# discover the provider's endpoints.
# #
# Required if 'enabled' is true. #- idp_id: google
# # idp_name: Google
#issuer: "https://accounts.example.com/" # discover: false
# issuer: "https://github.com/"
# oauth2 client id to use. # client_id: "your-client-id" # TO BE FILLED
# # client_secret: "your-client-secret" # TO BE FILLED
# Required if 'enabled' is true. # authorization_endpoint: "https://github.com/login/oauth/authorize"
# # token_endpoint: "https://github.com/login/oauth/access_token"
#client_id: "provided-by-your-issuer" # userinfo_endpoint: "https://api.github.com/user"
# scopes: ["read:user"]
# oauth2 client secret to use. # user_mapping_provider:
# # config:
# Required if 'enabled' is true. # subject_claim: "id"
# # localpart_template: "{ user.login }"
#client_secret: "provided-by-your-issuer" # display_name_template: "{ user.name }"
# auth method to use when exchanging the token.
# Valid values are 'client_secret_basic' (default), 'client_secret_post' and
# 'none'.
#
#client_auth_method: client_secret_post
# list of scopes to request. This should normally include the "openid" scope.
# Defaults to ["openid"].
#
#scopes: ["openid", "profile"]
# the oauth2 authorization endpoint. Required if provider discovery is disabled.
#
#authorization_endpoint: "https://accounts.example.com/oauth2/auth"
# the oauth2 token endpoint. Required if provider discovery is disabled.
#
#token_endpoint: "https://accounts.example.com/oauth2/token"
# the OIDC userinfo endpoint. Required if discovery is disabled and the
# "openid" scope is not requested.
#
#userinfo_endpoint: "https://accounts.example.com/userinfo"
# URI where to fetch the JWKS. Required if discovery is disabled and the
# "openid" scope is used.
#
#jwks_uri: "https://accounts.example.com/.well-known/jwks.json"
# Uncomment to skip metadata verification. Defaults to false.
#
# Use this if you are connecting to a provider that is not OpenID Connect
# compliant.
# Avoid this in production.
#
#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
# of failing. This could be used if switching from password logins to OIDC. Defaults to false.
#
#allow_existing_users: true
# An external module can be provided here as a custom solution to mapping
# attributes returned from a OIDC provider onto a matrix user.
#
user_mapping_provider:
# The custom module's class. Uncomment to use a custom module.
# Default is 'synapse.handlers.oidc_handler.JinjaOidcMappingProvider'.
#
# See https://github.com/matrix-org/synapse/blob/master/docs/sso_mapping_providers.md#openid-mapping-providers
# for information on implementing a custom mapping provider.
#
#module: mapping_provider.OidcMappingProvider
# Custom configuration values for the module. This section will be passed as
# a Python dictionary to the user mapping provider module's `parse_config`
# method.
#
# The examples below are intended for the default provider: they should be
# changed if using a custom provider.
#
config:
# name of the claim containing a unique identifier for the user.
# Defaults to `sub`, which OpenID Connect compliant providers should provide.
#
#subject_claim: "sub"
# Jinja2 template for the localpart of the MXID.
#
# When rendering, this template is given the following variables:
# * user: The claims returned by the UserInfo Endpoint and/or in the ID
# Token
#
# If this is not set, the user will be prompted to choose their
# own username.
#
#localpart_template: "{{ user.preferred_username }}"
# Jinja2 template for the display name to set on first login.
#
# If unset, no displayname will be set.
#
#display_name_template: "{{ user.given_name }} {{ user.last_name }}"
# Jinja2 templates for extra attributes to send back to the client during
# login.
#
# Note that these are non-standard and clients will ignore them without modifications.
#
#extra_attributes:
#birthdate: "{{ user.birthdate }}"
# Enable Central Authentication Service (CAS) for registration and login. # Enable Central Authentication Service (CAS) for registration and login.

View file

@ -40,7 +40,7 @@ class CasConfig(Config):
self.cas_required_attributes = {} self.cas_required_attributes = {}
def generate_config_section(self, config_dir_path, server_name, **kwargs): def generate_config_section(self, config_dir_path, server_name, **kwargs):
return """ return """\
# Enable Central Authentication Service (CAS) for registration and login. # Enable Central Authentication Service (CAS) for registration and login.
# #
cas_config: cas_config:

View file

@ -15,7 +15,7 @@
# limitations under the License. # limitations under the License.
import string import string
from typing import Optional, Type from typing import Iterable, Optional, Type
import attr import attr
@ -33,16 +33,8 @@ class OIDCConfig(Config):
section = "oidc" section = "oidc"
def read_config(self, config, **kwargs): def read_config(self, config, **kwargs):
validate_config(MAIN_CONFIG_SCHEMA, config, ()) self.oidc_providers = tuple(_parse_oidc_provider_configs(config))
if not self.oidc_providers:
self.oidc_provider = None # type: Optional[OidcProviderConfig]
oidc_config = config.get("oidc_config")
if oidc_config and oidc_config.get("enabled", False):
validate_config(OIDC_PROVIDER_CONFIG_SCHEMA, oidc_config, ("oidc_config",))
self.oidc_provider = _parse_oidc_config_dict(oidc_config)
if not self.oidc_provider:
return return
try: try:
@ -58,144 +50,153 @@ class OIDCConfig(Config):
@property @property
def oidc_enabled(self) -> bool: def oidc_enabled(self) -> bool:
# OIDC is enabled if we have a provider # OIDC is enabled if we have a provider
return bool(self.oidc_provider) return bool(self.oidc_providers)
def generate_config_section(self, config_dir_path, server_name, **kwargs): def generate_config_section(self, config_dir_path, server_name, **kwargs):
return """\ return """\
# Enable OpenID Connect (OIDC) / OAuth 2.0 for registration and login. # List of OpenID Connect (OIDC) / OAuth 2.0 identity providers, for registration
# and login.
#
# Options for each entry include:
#
# idp_id: a unique identifier for this identity provider. Used internally
# by Synapse; should be a single word such as 'github'.
#
# Note that, if this is changed, users authenticating via that provider
# will no longer be recognised as the same user!
#
# idp_name: A user-facing name for this identity provider, which is used to
# offer the user a choice of login mechanisms.
#
# discover: set to 'false' to disable the use of the OIDC discovery mechanism
# to discover endpoints. Defaults to true.
#
# issuer: Required. The OIDC issuer. Used to validate tokens and (if discovery
# is enabled) to discover the provider's endpoints.
#
# client_id: Required. oauth2 client id to use.
#
# client_secret: Required. oauth2 client secret to use.
#
# client_auth_method: auth method to use when exchanging the token. Valid
# values are 'client_secret_basic' (default), 'client_secret_post' and
# 'none'.
#
# scopes: list of scopes to request. This should normally include the "openid"
# scope. Defaults to ["openid"].
#
# authorization_endpoint: the oauth2 authorization endpoint. Required if
# provider discovery is disabled.
#
# token_endpoint: the oauth2 token endpoint. Required if provider discovery is
# disabled.
#
# userinfo_endpoint: the OIDC userinfo endpoint. Required if discovery is
# disabled and the 'openid' scope is not requested.
#
# jwks_uri: URI where to fetch the JWKS. Required if discovery is disabled and
# the 'openid' scope is used.
#
# skip_verification: set to 'true' to skip metadata verification. Use this if
# you are connecting to a provider that is not OpenID Connect compliant.
# Defaults to false. Avoid this in production.
#
# user_profile_method: 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'. Set to 'userinfo_endpoint' to always fetch the
# userinfo endpoint.
#
# allow_existing_users: set to 'true' 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.
#
# user_mapping_provider: Configuration for how attributes returned from a OIDC
# provider are mapped onto a matrix user. This setting has the following
# sub-properties:
#
# module: The class name of a custom mapping module. Default is
# {mapping_provider!r}.
# See https://github.com/matrix-org/synapse/blob/master/docs/sso_mapping_providers.md#openid-mapping-providers
# for information on implementing a custom mapping provider.
#
# config: Configuration for the mapping provider module. This section will
# be passed as a Python dictionary to the user mapping provider
# module's `parse_config` method.
#
# For the default provider, the following settings are available:
#
# sub: name of the claim containing a unique identifier for the
# user. Defaults to 'sub', which OpenID Connect compliant
# providers should provide.
#
# localpart_template: Jinja2 template for the localpart of the MXID.
# If this is not set, the user will be prompted to choose their
# own username.
#
# display_name_template: Jinja2 template for the display name to set
# on first login. If unset, no displayname will be set.
#
# extra_attributes: a map of Jinja2 templates for extra attributes
# to send back to the client during login.
# Note that these are non-standard and clients will ignore them
# without modifications.
#
# When rendering, the Jinja2 templates are given a 'user' variable,
# which is set to the claims returned by the UserInfo Endpoint and/or
# in the ID Token.
# #
# See https://github.com/matrix-org/synapse/blob/master/docs/openid.md # See https://github.com/matrix-org/synapse/blob/master/docs/openid.md
# for some example configurations. # for information on how to configure these options.
# #
oidc_config: # For backwards compatibility, it is also possible to configure a single OIDC
# Uncomment the following to enable authorization against an OpenID Connect # provider via an 'oidc_config' setting. This is now deprecated and admins are
# server. Defaults to false. # advised to migrate to the 'oidc_providers' format.
#
oidc_providers:
# Generic example
# #
#enabled: true #- idp_id: my_idp
# idp_name: "My OpenID provider"
# discover: false
# issuer: "https://accounts.example.com/"
# client_id: "provided-by-your-issuer"
# client_secret: "provided-by-your-issuer"
# client_auth_method: client_secret_post
# scopes: ["openid", "profile"]
# authorization_endpoint: "https://accounts.example.com/oauth2/auth"
# token_endpoint: "https://accounts.example.com/oauth2/token"
# userinfo_endpoint: "https://accounts.example.com/userinfo"
# jwks_uri: "https://accounts.example.com/.well-known/jwks.json"
# skip_verification: true
# Uncomment the following to disable use of the OIDC discovery mechanism to # For use with Keycloak
# discover endpoints. Defaults to true.
# #
#discover: false #- idp_id: keycloak
# idp_name: Keycloak
# issuer: "https://127.0.0.1:8443/auth/realms/my_realm_name"
# client_id: "synapse"
# client_secret: "copy secret generated in Keycloak UI"
# scopes: ["openid", "profile"]
# the OIDC issuer. Used to validate tokens and (if discovery is enabled) to # For use with Github
# discover the provider's endpoints.
# #
# Required if 'enabled' is true. #- idp_id: google
# # idp_name: Google
#issuer: "https://accounts.example.com/" # discover: false
# issuer: "https://github.com/"
# oauth2 client id to use. # client_id: "your-client-id" # TO BE FILLED
# # client_secret: "your-client-secret" # TO BE FILLED
# Required if 'enabled' is true. # authorization_endpoint: "https://github.com/login/oauth/authorize"
# # token_endpoint: "https://github.com/login/oauth/access_token"
#client_id: "provided-by-your-issuer" # userinfo_endpoint: "https://api.github.com/user"
# scopes: ["read:user"]
# oauth2 client secret to use. # user_mapping_provider:
# # config:
# Required if 'enabled' is true. # subject_claim: "id"
# # localpart_template: "{{ user.login }}"
#client_secret: "provided-by-your-issuer" # display_name_template: "{{ user.name }}"
# auth method to use when exchanging the token.
# Valid values are 'client_secret_basic' (default), 'client_secret_post' and
# 'none'.
#
#client_auth_method: client_secret_post
# list of scopes to request. This should normally include the "openid" scope.
# Defaults to ["openid"].
#
#scopes: ["openid", "profile"]
# the oauth2 authorization endpoint. Required if provider discovery is disabled.
#
#authorization_endpoint: "https://accounts.example.com/oauth2/auth"
# the oauth2 token endpoint. Required if provider discovery is disabled.
#
#token_endpoint: "https://accounts.example.com/oauth2/token"
# the OIDC userinfo endpoint. Required if discovery is disabled and the
# "openid" scope is not requested.
#
#userinfo_endpoint: "https://accounts.example.com/userinfo"
# URI where to fetch the JWKS. Required if discovery is disabled and the
# "openid" scope is used.
#
#jwks_uri: "https://accounts.example.com/.well-known/jwks.json"
# Uncomment to skip metadata verification. Defaults to false.
#
# Use this if you are connecting to a provider that is not OpenID Connect
# compliant.
# Avoid this in production.
#
#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
# of failing. This could be used if switching from password logins to OIDC. Defaults to false.
#
#allow_existing_users: true
# An external module can be provided here as a custom solution to mapping
# attributes returned from a OIDC provider onto a matrix user.
#
user_mapping_provider:
# The custom module's class. Uncomment to use a custom module.
# Default is {mapping_provider!r}.
#
# See https://github.com/matrix-org/synapse/blob/master/docs/sso_mapping_providers.md#openid-mapping-providers
# for information on implementing a custom mapping provider.
#
#module: mapping_provider.OidcMappingProvider
# Custom configuration values for the module. This section will be passed as
# a Python dictionary to the user mapping provider module's `parse_config`
# method.
#
# The examples below are intended for the default provider: they should be
# changed if using a custom provider.
#
config:
# name of the claim containing a unique identifier for the user.
# Defaults to `sub`, which OpenID Connect compliant providers should provide.
#
#subject_claim: "sub"
# Jinja2 template for the localpart of the MXID.
#
# When rendering, this template is given the following variables:
# * user: The claims returned by the UserInfo Endpoint and/or in the ID
# Token
#
# If this is not set, the user will be prompted to choose their
# own username.
#
#localpart_template: "{{{{ user.preferred_username }}}}"
# Jinja2 template for the display name to set on first login.
#
# If unset, no displayname will be set.
#
#display_name_template: "{{{{ user.given_name }}}} {{{{ user.last_name }}}}"
# Jinja2 templates for extra attributes to send back to the client during
# login.
#
# Note that these are non-standard and clients will ignore them without modifications.
#
#extra_attributes:
#birthdate: "{{{{ user.birthdate }}}}"
""".format( """.format(
mapping_provider=DEFAULT_USER_MAPPING_PROVIDER mapping_provider=DEFAULT_USER_MAPPING_PROVIDER
) )
@ -234,7 +235,22 @@ OIDC_PROVIDER_CONFIG_SCHEMA = {
}, },
} }
# the `oidc_config` setting can either be None (as it is in the default # the same as OIDC_PROVIDER_CONFIG_SCHEMA, but with compulsory idp_id and idp_name
OIDC_PROVIDER_CONFIG_WITH_ID_SCHEMA = {
"allOf": [OIDC_PROVIDER_CONFIG_SCHEMA, {"required": ["idp_id", "idp_name"]}]
}
# the `oidc_providers` list can either be None (as it is in the default config), or
# a list of provider configs, each of which requires an explicit ID and name.
OIDC_PROVIDER_LIST_SCHEMA = {
"oneOf": [
{"type": "null"},
{"type": "array", "items": OIDC_PROVIDER_CONFIG_WITH_ID_SCHEMA},
]
}
# the `oidc_config` setting can either be None (which it used to be in the default
# config), or an object. If an object, it is ignored unless it has an "enabled: True" # config), or an object. If an object, it is ignored unless it has an "enabled: True"
# property. # property.
# #
@ -243,12 +259,41 @@ OIDC_PROVIDER_CONFIG_SCHEMA = {
# additional checks in the code. # additional checks in the code.
OIDC_CONFIG_SCHEMA = {"oneOf": [{"type": "null"}, {"type": "object"}]} OIDC_CONFIG_SCHEMA = {"oneOf": [{"type": "null"}, {"type": "object"}]}
# the top-level schema can contain an "oidc_config" and/or an "oidc_providers".
MAIN_CONFIG_SCHEMA = { MAIN_CONFIG_SCHEMA = {
"type": "object", "type": "object",
"properties": {"oidc_config": OIDC_CONFIG_SCHEMA}, "properties": {
"oidc_config": OIDC_CONFIG_SCHEMA,
"oidc_providers": OIDC_PROVIDER_LIST_SCHEMA,
},
} }
def _parse_oidc_provider_configs(config: JsonDict) -> Iterable["OidcProviderConfig"]:
"""extract and parse the OIDC provider configs from the config dict
The configuration may contain either a single `oidc_config` object with an
`enabled: True` property, or a list of provider configurations under
`oidc_providers`, *or both*.
Returns a generator which yields the OidcProviderConfig objects
"""
validate_config(MAIN_CONFIG_SCHEMA, config, ())
for p in config.get("oidc_providers") or []:
yield _parse_oidc_config_dict(p)
# for backwards-compatibility, it is also possible to provide a single "oidc_config"
# object with an "enabled: True" property.
oidc_config = config.get("oidc_config")
if oidc_config and oidc_config.get("enabled", False):
# MAIN_CONFIG_SCHEMA checks that `oidc_config` is an object, but not that
# it matches OIDC_PROVIDER_CONFIG_SCHEMA (see the comments on OIDC_CONFIG_SCHEMA
# above), so now we need to validate it.
validate_config(OIDC_PROVIDER_CONFIG_SCHEMA, oidc_config, ("oidc_config",))
yield _parse_oidc_config_dict(oidc_config)
def _parse_oidc_config_dict(oidc_config: JsonDict) -> "OidcProviderConfig": def _parse_oidc_config_dict(oidc_config: JsonDict) -> "OidcProviderConfig":
"""Take the configuration dict and parse it into an OidcProviderConfig """Take the configuration dict and parse it into an OidcProviderConfig

View file

@ -78,21 +78,28 @@ class OidcHandler:
def __init__(self, hs: "HomeServer"): def __init__(self, hs: "HomeServer"):
self._sso_handler = hs.get_sso_handler() self._sso_handler = hs.get_sso_handler()
provider_conf = hs.config.oidc.oidc_provider provider_confs = hs.config.oidc.oidc_providers
# we should not have been instantiated if there is no configured provider. # we should not have been instantiated if there is no configured provider.
assert provider_conf is not None assert provider_confs
self._token_generator = OidcSessionTokenGenerator(hs) self._token_generator = OidcSessionTokenGenerator(hs)
self._providers = {
self._provider = OidcProvider(hs, self._token_generator, provider_conf) p.idp_id: OidcProvider(hs, self._token_generator, p) for p in provider_confs
}
async def load_metadata(self) -> None: async def load_metadata(self) -> None:
"""Validate the config and load the metadata from the remote endpoint. """Validate the config and load the metadata from the remote endpoint.
Called at startup to ensure we have everything we need. Called at startup to ensure we have everything we need.
""" """
await self._provider.load_metadata() for idp_id, p in self._providers.items():
await self._provider.load_jwks() try:
await p.load_metadata()
await p.load_jwks()
except Exception as e:
raise Exception(
"Error while initialising OIDC provider %r" % (idp_id,)
) from e
async def handle_oidc_callback(self, request: SynapseRequest) -> None: async def handle_oidc_callback(self, request: SynapseRequest) -> None:
"""Handle an incoming request to /_synapse/oidc/callback """Handle an incoming request to /_synapse/oidc/callback
@ -184,6 +191,12 @@ class OidcHandler:
self._sso_handler.render_error(request, "mismatching_session", str(e)) self._sso_handler.render_error(request, "mismatching_session", str(e))
return return
oidc_provider = self._providers.get(session_data.idp_id)
if not oidc_provider:
logger.error("OIDC session uses unknown IdP %r", oidc_provider)
self._sso_handler.render_error(request, "unknown_idp", "Unknown IdP")
return
if b"code" not in request.args: if b"code" not in request.args:
logger.info("Code parameter is missing") logger.info("Code parameter is missing")
self._sso_handler.render_error( self._sso_handler.render_error(
@ -193,7 +206,7 @@ class OidcHandler:
code = request.args[b"code"][0].decode() code = request.args[b"code"][0].decode()
await self._provider.handle_oidc_callback(request, session_data, code) await oidc_provider.handle_oidc_callback(request, session_data, code)
class OidcError(Exception): class OidcError(Exception):

View file

@ -145,7 +145,7 @@ class OidcHandlerTestCase(HomeserverTestCase):
hs = self.setup_test_homeserver(proxied_http_client=self.http_client) hs = self.setup_test_homeserver(proxied_http_client=self.http_client)
self.handler = hs.get_oidc_handler() self.handler = hs.get_oidc_handler()
self.provider = self.handler._provider self.provider = self.handler._providers["oidc"]
sso_handler = hs.get_sso_handler() sso_handler = hs.get_sso_handler()
# Mock the render error method. # Mock the render error method.
self.render_error = Mock(return_value=None) self.render_error = Mock(return_value=None)
@ -866,7 +866,7 @@ async def _make_callback_with_userinfo(
from synapse.handlers.oidc_handler import OidcSessionData from synapse.handlers.oidc_handler import OidcSessionData
handler = hs.get_oidc_handler() handler = hs.get_oidc_handler()
provider = handler._provider provider = handler._providers["oidc"]
provider._exchange_code = simple_async_mock(return_value={}) provider._exchange_code = simple_async_mock(return_value={})
provider._parse_id_token = simple_async_mock(return_value=userinfo) provider._parse_id_token = simple_async_mock(return_value=userinfo)
provider._fetch_userinfo = simple_async_mock(return_value=userinfo) provider._fetch_userinfo = simple_async_mock(return_value=userinfo)