Cleanups to the OpenID Connect integration (#7628)

docs, default configs, comments. Nothing very significant.
This commit is contained in:
Richard van der Hoff 2020-06-03 21:13:17 +01:00 committed by GitHub
parent e91abfd291
commit 11de843626
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 421 additions and 317 deletions

1
changelog.d/7628.misc Normal file
View file

@ -0,0 +1 @@
Minor cleanups to OpenID Connect integration.

View file

@ -1,175 +0,0 @@
# How to test OpenID Connect
Any OpenID Connect Provider (OP) should work with Synapse, as long as it supports the authorization code flow.
There are a few options for that:
- start a local OP. Synapse has been tested with [Hydra][hydra] and [Dex][dex-idp].
Note that for an OP to work, it should be served under a secure (HTTPS) origin.
A certificate signed with a self-signed, locally trusted CA should work. In that case, start Synapse with a `SSL_CERT_FILE` environment variable set to the path of the CA.
- use a publicly available OP. Synapse has been tested with [Google][google-idp].
- setup a SaaS OP, like [Auth0][auth0] and [Okta][okta]. Auth0 has a free tier which has been tested with Synapse.
[google-idp]: https://developers.google.com/identity/protocols/OpenIDConnect#authenticatingtheuser
[auth0]: https://auth0.com/
[okta]: https://www.okta.com/
[dex-idp]: https://github.com/dexidp/dex
[hydra]: https://www.ory.sh/docs/hydra/
## Sample configs
Here are a few configs for providers that should work with Synapse.
### [Dex][dex-idp]
[Dex][dex-idp] is a simple, open-source, certified OpenID Connect Provider.
Although it is designed to help building a full-blown provider, with some external database, it can be configured with static passwords in a config file.
Follow the [Getting Started guide](https://github.com/dexidp/dex/blob/master/Documentation/getting-started.md) to install Dex.
Edit `examples/config-dev.yaml` config file from the Dex repo to add a client:
```yaml
staticClients:
- id: synapse
secret: secret
redirectURIs:
- '[synapse base url]/_synapse/oidc/callback'
name: 'Synapse'
```
Run with `dex serve examples/config-dex.yaml`
Synapse config:
```yaml
oidc_config:
enabled: true
skip_verification: true # This is needed as Dex is served on an insecure endpoint
issuer: "http://127.0.0.1:5556/dex"
discover: true
client_id: "synapse"
client_secret: "secret"
scopes:
- openid
- profile
user_mapping_provider:
config:
localpart_template: '{{ user.name }}'
display_name_template: '{{ user.name|capitalize }}'
```
### [Auth0][auth0]
1. Create a regular web application for Synapse
2. Set the Allowed Callback URLs to `[synapse base url]/_synapse/oidc/callback`
3. Add a rule to add the `preferred_username` claim.
<details>
<summary>Code sample</summary>
```js
function addPersistenceAttribute(user, context, callback) {
user.user_metadata = user.user_metadata || {};
user.user_metadata.preferred_username = user.user_metadata.preferred_username || user.user_id;
context.idToken.preferred_username = user.user_metadata.preferred_username;
auth0.users.updateUserMetadata(user.user_id, user.user_metadata)
.then(function(){
callback(null, user, context);
})
.catch(function(err){
callback(err);
});
}
```
</details>
```yaml
oidc_config:
enabled: true
issuer: "https://your-tier.eu.auth0.com/" # TO BE FILLED
discover: true
client_id: "your-client-id" # TO BE FILLED
client_secret: "your-client-secret" # TO BE FILLED
scopes:
- openid
- profile
user_mapping_provider:
config:
localpart_template: '{{ user.preferred_username }}'
display_name_template: '{{ user.name }}'
```
### GitHub
GitHub is a bit special as it is not an OpenID Connect compliant provider, but just a regular OAuth2 provider.
The `/user` API endpoint can be used to retrieve informations from the user.
As the OIDC login mechanism needs an attribute to uniquely identify users and that endpoint does not return a `sub` property, an alternative `subject_claim` has to be set.
1. Create a new OAuth application: https://github.com/settings/applications/new
2. Set the callback URL to `[synapse base url]/_synapse/oidc/callback`
```yaml
oidc_config:
enabled: true
issuer: "https://github.com/"
discover: false
client_id: "your-client-id" # TO BE FILLED
client_secret: "your-client-secret" # TO BE FILLED
authorization_endpoint: "https://github.com/login/oauth/authorize"
token_endpoint: "https://github.com/login/oauth/access_token"
userinfo_endpoint: "https://api.github.com/user"
scopes:
- read:user
user_mapping_provider:
config:
subject_claim: 'id'
localpart_template: '{{ user.login }}'
display_name_template: '{{ user.name }}'
```
### Google
1. Setup a project in the Google API Console
2. Obtain the OAuth 2.0 credentials (see <https://developers.google.com/identity/protocols/oauth2/openid-connect>)
3. Add this Authorized redirect URI: `[synapse base url]/_synapse/oidc/callback`
```yaml
oidc_config:
enabled: true
issuer: "https://accounts.google.com/"
discover: true
client_id: "your-client-id" # TO BE FILLED
client_secret: "your-client-secret" # TO BE FILLED
scopes:
- openid
- profile
user_mapping_provider:
config:
localpart_template: '{{ user.given_name|lower }}'
display_name_template: '{{ user.name }}'
```
### Twitch
1. Setup a developer account on [Twitch](https://dev.twitch.tv/)
2. Obtain the OAuth 2.0 credentials by [creating an app](https://dev.twitch.tv/console/apps/)
3. Add this OAuth Redirect URL: `[synapse base url]/_synapse/oidc/callback`
```yaml
oidc_config:
enabled: true
issuer: "https://id.twitch.tv/oauth2/"
discover: true
client_id: "your-client-id" # TO BE FILLED
client_secret: "your-client-secret" # TO BE FILLED
client_auth_method: "client_secret_post"
scopes:
- openid
user_mapping_provider:
config:
localpart_template: '{{ user.preferred_username }}'
display_name_template: '{{ user.name }}'
```

206
docs/openid.md Normal file
View file

@ -0,0 +1,206 @@
# Configuring Synapse to authenticate against an OpenID Connect provider
Synapse can be configured to use an OpenID Connect Provider (OP) for
authentication, instead of its own local password database.
Any OP should work with Synapse, as long as it supports the authorization code
flow. There are a few options for that:
- start a local OP. Synapse has been tested with [Hydra][hydra] and
[Dex][dex-idp]. Note that for an OP to work, it should be served under a
secure (HTTPS) origin. A certificate signed with a self-signed, locally
trusted CA should work. In that case, start Synapse with a `SSL_CERT_FILE`
environment variable set to the path of the CA.
- set up a SaaS OP, like [Google][google-idp], [Auth0][auth0] or
[Okta][okta]. Synapse has been tested with Auth0 and Google.
It may also be possible to use other OAuth2 providers which provide the
[authorization code grant type](https://tools.ietf.org/html/rfc6749#section-4.1),
such as [Github][github-idp].
[google-idp]: https://developers.google.com/identity/protocols/oauth2/openid-connect
[auth0]: https://auth0.com/
[okta]: https://www.okta.com/
[dex-idp]: https://github.com/dexidp/dex
[hydra]: https://www.ory.sh/docs/hydra/
[github-idp]: https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps
## Preparing Synapse
The OpenID integration in Synapse uses the
[`authlib`](https://pypi.org/project/Authlib/) library, which must be installed
as follows:
* The relevant libraries are included in the Docker images and Debian packages
provided by `matrix.org` so no further action is needed.
* If you installed Synapse into a virtualenv, run `/path/to/env/bin/pip
install synapse[oidc]` to install the necessary dependencies.
* For other installation mechanisms, see the documentation provided by the
maintainer.
To enable the OpenID integration, you should then add an `oidc_config` section
to your configuration file (or uncomment the `enabled: true` line in the
existing section). See [sample_config.yaml](./sample_config.yaml) for some
sample settings, as well as the text below for example configurations for
specific providers.
## Sample configs
Here are a few configs for providers that should work with Synapse.
### [Dex][dex-idp]
[Dex][dex-idp] is a simple, open-source, certified OpenID Connect Provider.
Although it is designed to help building a full-blown provider with an
external database, it can be configured with static passwords in a config file.
Follow the [Getting Started
guide](https://github.com/dexidp/dex/blob/master/Documentation/getting-started.md)
to install Dex.
Edit `examples/config-dev.yaml` config file from the Dex repo to add a client:
```yaml
staticClients:
- id: synapse
secret: secret
redirectURIs:
- '[synapse public baseurl]/_synapse/oidc/callback'
name: 'Synapse'
```
Run with `dex serve examples/config-dex.yaml`.
Synapse config:
```yaml
oidc_config:
enabled: true
skip_verification: true # This is needed as Dex is served on an insecure endpoint
issuer: "http://127.0.0.1:5556/dex"
client_id: "synapse"
client_secret: "secret"
scopes: ["openid", "profile"]
user_mapping_provider:
config:
localpart_template: "{{ user.name }}"
display_name_template: "{{ user.name|capitalize }}"
```
### [Auth0][auth0]
1. Create a regular web application for Synapse
2. Set the Allowed Callback URLs to `[synapse public baseurl]/_synapse/oidc/callback`
3. Add a rule to add the `preferred_username` claim.
<details>
<summary>Code sample</summary>
```js
function addPersistenceAttribute(user, context, callback) {
user.user_metadata = user.user_metadata || {};
user.user_metadata.preferred_username = user.user_metadata.preferred_username || user.user_id;
context.idToken.preferred_username = user.user_metadata.preferred_username;
auth0.users.updateUserMetadata(user.user_id, user.user_metadata)
.then(function(){
callback(null, user, context);
})
.catch(function(err){
callback(err);
});
}
```
</details>
Synapse config:
```yaml
oidc_config:
enabled: true
issuer: "https://your-tier.eu.auth0.com/" # TO BE FILLED
client_id: "your-client-id" # TO BE FILLED
client_secret: "your-client-secret" # TO BE FILLED
scopes: ["openid", "profile"]
user_mapping_provider:
config:
localpart_template: "{{ user.preferred_username }}"
display_name_template: "{{ user.name }}"
```
### GitHub
GitHub is a bit special as it is not an OpenID Connect compliant provider, but
just a regular OAuth2 provider.
The [`/user` API endpoint](https://developer.github.com/v3/users/#get-the-authenticated-user)
can be used to retrieve information on the authenticated user. As the Synaspse
login mechanism needs an attribute to uniquely identify users, and that endpoint
does not return a `sub` property, an alternative `subject_claim` has to be set.
1. Create a new OAuth application: https://github.com/settings/applications/new.
2. Set the callback URL to `[synapse public baseurl]/_synapse/oidc/callback`.
Synapse config:
```yaml
oidc_config:
enabled: true
discover: false
issuer: "https://github.com/"
client_id: "your-client-id" # TO BE FILLED
client_secret: "your-client-secret" # TO BE FILLED
authorization_endpoint: "https://github.com/login/oauth/authorize"
token_endpoint: "https://github.com/login/oauth/access_token"
userinfo_endpoint: "https://api.github.com/user"
scopes: ["read:user"]
user_mapping_provider:
config:
subject_claim: "id"
localpart_template: "{{ user.login }}"
display_name_template: "{{ user.name }}"
```
### [Google][google-idp]
1. Set up a project in the Google API Console (see
https://developers.google.com/identity/protocols/oauth2/openid-connect#appsetup).
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:
```yaml
oidc_config:
enabled: true
issuer: "https://accounts.google.com/"
client_id: "your-client-id" # TO BE FILLED
client_secret: "your-client-secret" # TO BE FILLED
scopes: ["openid", "profile"]
user_mapping_provider:
config:
localpart_template: "{{ user.given_name|lower }}"
display_name_template: "{{ user.name }}"
```
4. Back in the Google console, add this Authorized redirect URI: `[synapse
public baseurl]/_synapse/oidc/callback`.
### Twitch
1. Setup a developer account on [Twitch](https://dev.twitch.tv/)
2. Obtain the OAuth 2.0 credentials by [creating an app](https://dev.twitch.tv/console/apps/)
3. Add this OAuth Redirect URL: `[synapse public baseurl]/_synapse/oidc/callback`
Synapse config:
```yaml
oidc_config:
enabled: true
issuer: "https://id.twitch.tv/oauth2/"
client_id: "your-client-id" # TO BE FILLED
client_secret: "your-client-secret" # TO BE FILLED
client_auth_method: "client_secret_post"
user_mapping_provider:
config:
localpart_template: '{{ user.preferred_username }}'
display_name_template: '{{ user.name }}'
```

View file

@ -1379,6 +1379,8 @@ trusted_key_servers:
#key_server_signing_keys_path: "key_server_signing_keys.key" #key_server_signing_keys_path: "key_server_signing_keys.key"
## Single sign-on integration ##
# Enable SAML2 for registration and login. Uses pysaml2. # Enable SAML2 for registration and login. Uses pysaml2.
# #
# At least one of `sp_config` or `config_path` must be set in this section to # At least one of `sp_config` or `config_path` must be set in this section to
@ -1526,37 +1528,52 @@ saml2_config:
#template_dir: "res/templates" #template_dir: "res/templates"
# Enable OpenID Connect for registration and login. Uses authlib. # OpenID Connect integration. The following settings can be used to make Synapse
# use an OpenID Connect Provider for authentication, instead of its internal
# password database.
#
# See https://github.com/matrix-org/synapse/blob/master/openid.md.
# #
oidc_config: oidc_config:
# enable OpenID Connect. Defaults to false. # Uncomment the following to enable authorization against an OpenID Connect
# server. Defaults to false.
# #
#enabled: true #enabled: true
# use the OIDC discovery mechanism to discover endpoints. Defaults to true. # Uncomment the following to disable use of the OIDC discovery mechanism to
# discover endpoints. Defaults to true.
# #
#discover: true #discover: false
# the OIDC issuer. Used to validate tokens and discover the providers endpoints. Required. # the OIDC issuer. Used to validate tokens and (if discovery is enabled) to
# discover the provider's endpoints.
#
# Required if 'enabled' is true.
# #
#issuer: "https://accounts.example.com/" #issuer: "https://accounts.example.com/"
# oauth2 client id to use. Required. # oauth2 client id to use.
#
# Required if 'enabled' is true.
# #
#client_id: "provided-by-your-issuer" #client_id: "provided-by-your-issuer"
# oauth2 client secret to use. Required. # oauth2 client secret to use.
#
# Required if 'enabled' is true.
# #
#client_secret: "provided-by-your-issuer" #client_secret: "provided-by-your-issuer"
# auth method to use when exchanging the token. # auth method to use when exchanging the token.
# Valid values are "client_secret_basic" (default), "client_secret_post" and "none". # Valid values are 'client_secret_basic' (default), 'client_secret_post' and
# 'none'.
# #
#client_auth_method: "client_secret_basic" #client_auth_method: client_secret_post
# list of scopes to ask. This should include the "openid" scope. Defaults to ["openid"]. # list of scopes to request. This should normally include the "openid" scope.
# Defaults to ["openid"].
# #
#scopes: ["openid"] #scopes: ["openid", "profile"]
# the oauth2 authorization endpoint. Required if provider discovery is disabled. # the oauth2 authorization endpoint. Required if provider discovery is disabled.
# #
@ -1566,20 +1583,23 @@ oidc_config:
# #
#token_endpoint: "https://accounts.example.com/oauth2/token" #token_endpoint: "https://accounts.example.com/oauth2/token"
# the OIDC userinfo endpoint. Required if discovery is disabled and the "openid" scope is not asked. # the OIDC userinfo endpoint. Required if discovery is disabled and the
# "openid" scope is not requested.
# #
#userinfo_endpoint: "https://accounts.example.com/userinfo" #userinfo_endpoint: "https://accounts.example.com/userinfo"
# URI where to fetch the JWKS. Required if discovery is disabled and the "openid" scope is used. # 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" #jwks_uri: "https://accounts.example.com/.well-known/jwks.json"
# skip metadata verification. Defaults to false. # Uncomment to skip metadata verification. Defaults to false.
# Use this if you are connecting to a provider that is not OpenID Connect compliant. #
# Use this if you are connecting to a provider that is not OpenID Connect
# compliant.
# Avoid this in production. # Avoid this in production.
# #
#skip_verification: false #skip_verification: true
# An external module can be provided here as a custom solution to mapping # An external module can be provided here as a custom solution to mapping
# attributes returned from a OIDC provider onto a matrix user. # attributes returned from a OIDC provider onto a matrix user.
@ -1588,16 +1608,17 @@ oidc_config:
# The custom module's class. Uncomment to use a custom module. # The custom module's class. Uncomment to use a custom module.
# Default is 'synapse.handlers.oidc_handler.JinjaOidcMappingProvider'. # 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 #module: mapping_provider.OidcMappingProvider
# Custom configuration values for the module. Below options are intended # Custom configuration values for the module. This section will be passed as
# for the built-in provider, they should be changed if using a custom # a Python dictionary to the user mapping provider module's `parse_config`
# module. This section will be passed as a Python dictionary to the # method.
# module's `parse_config` method.
# #
# Below is the config of the default mapping provider, based on Jinja2 # The examples below are intended for the default provider: they should be
# templates. Those templates are used to render user attributes, where the # changed if using a custom provider.
# userinfo object is available through the `user` variable.
# #
config: config:
# name of the claim containing a unique identifier for the user. # name of the claim containing a unique identifier for the user.
@ -1605,11 +1626,19 @@ oidc_config:
# #
#subject_claim: "sub" #subject_claim: "sub"
# Jinja2 template for the localpart of the MXID # 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
#
# This must be configured if using the default mapping provider.
# #
localpart_template: "{{ user.preferred_username }}" localpart_template: "{{ user.preferred_username }}"
# Jinja2 template for the display name to set on first login. Optional. # 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 }}" #display_name_template: "{{ user.given_name }} {{ user.last_name }}"
@ -1626,7 +1655,8 @@ oidc_config:
# # name: value # # name: value
# Additional settings to use with single-sign on systems such as SAML2 and CAS. # Additional settings to use with single-sign on systems such as OpenID Connect,
# SAML2 and CAS.
# #
sso: sso:
# A list of client URLs which are whitelisted so that the user does not # A list of client URLs which are whitelisted so that the user does not

View file

@ -55,7 +55,6 @@ class OIDCConfig(Config):
self.oidc_token_endpoint = oidc_config.get("token_endpoint") self.oidc_token_endpoint = oidc_config.get("token_endpoint")
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_subject_claim = oidc_config.get("subject_claim", "sub")
self.oidc_skip_verification = oidc_config.get("skip_verification", False) self.oidc_skip_verification = oidc_config.get("skip_verification", False)
ump_config = oidc_config.get("user_mapping_provider", {}) ump_config = oidc_config.get("user_mapping_provider", {})
@ -86,37 +85,52 @@ class OIDCConfig(Config):
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 for registration and login. Uses authlib. # OpenID Connect integration. The following settings can be used to make Synapse
# use an OpenID Connect Provider for authentication, instead of its internal
# password database.
#
# See https://github.com/matrix-org/synapse/blob/master/openid.md.
# #
oidc_config: oidc_config:
# enable OpenID Connect. Defaults to false. # Uncomment the following to enable authorization against an OpenID Connect
# server. Defaults to false.
# #
#enabled: true #enabled: true
# use the OIDC discovery mechanism to discover endpoints. Defaults to true. # Uncomment the following to disable use of the OIDC discovery mechanism to
# discover endpoints. Defaults to true.
# #
#discover: true #discover: false
# the OIDC issuer. Used to validate tokens and discover the providers endpoints. Required. # the OIDC issuer. Used to validate tokens and (if discovery is enabled) to
# discover the provider's endpoints.
#
# Required if 'enabled' is true.
# #
#issuer: "https://accounts.example.com/" #issuer: "https://accounts.example.com/"
# oauth2 client id to use. Required. # oauth2 client id to use.
#
# Required if 'enabled' is true.
# #
#client_id: "provided-by-your-issuer" #client_id: "provided-by-your-issuer"
# oauth2 client secret to use. Required. # oauth2 client secret to use.
#
# Required if 'enabled' is true.
# #
#client_secret: "provided-by-your-issuer" #client_secret: "provided-by-your-issuer"
# auth method to use when exchanging the token. # auth method to use when exchanging the token.
# Valid values are "client_secret_basic" (default), "client_secret_post" and "none". # Valid values are 'client_secret_basic' (default), 'client_secret_post' and
# 'none'.
# #
#client_auth_method: "client_secret_basic" #client_auth_method: client_secret_post
# list of scopes to ask. This should include the "openid" scope. Defaults to ["openid"]. # list of scopes to request. This should normally include the "openid" scope.
# Defaults to ["openid"].
# #
#scopes: ["openid"] #scopes: ["openid", "profile"]
# the oauth2 authorization endpoint. Required if provider discovery is disabled. # the oauth2 authorization endpoint. Required if provider discovery is disabled.
# #
@ -126,20 +140,23 @@ class OIDCConfig(Config):
# #
#token_endpoint: "https://accounts.example.com/oauth2/token" #token_endpoint: "https://accounts.example.com/oauth2/token"
# the OIDC userinfo endpoint. Required if discovery is disabled and the "openid" scope is not asked. # the OIDC userinfo endpoint. Required if discovery is disabled and the
# "openid" scope is not requested.
# #
#userinfo_endpoint: "https://accounts.example.com/userinfo" #userinfo_endpoint: "https://accounts.example.com/userinfo"
# URI where to fetch the JWKS. Required if discovery is disabled and the "openid" scope is used. # 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" #jwks_uri: "https://accounts.example.com/.well-known/jwks.json"
# skip metadata verification. Defaults to false. # Uncomment to skip metadata verification. Defaults to false.
# Use this if you are connecting to a provider that is not OpenID Connect compliant. #
# Use this if you are connecting to a provider that is not OpenID Connect
# compliant.
# Avoid this in production. # Avoid this in production.
# #
#skip_verification: false #skip_verification: true
# An external module can be provided here as a custom solution to mapping # An external module can be provided here as a custom solution to mapping
# attributes returned from a OIDC provider onto a matrix user. # attributes returned from a OIDC provider onto a matrix user.
@ -148,16 +165,17 @@ class OIDCConfig(Config):
# The custom module's class. Uncomment to use a custom module. # The custom module's class. Uncomment to use a custom module.
# Default is {mapping_provider!r}. # 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 #module: mapping_provider.OidcMappingProvider
# Custom configuration values for the module. Below options are intended # Custom configuration values for the module. This section will be passed as
# for the built-in provider, they should be changed if using a custom # a Python dictionary to the user mapping provider module's `parse_config`
# module. This section will be passed as a Python dictionary to the # method.
# module's `parse_config` method.
# #
# Below is the config of the default mapping provider, based on Jinja2 # The examples below are intended for the default provider: they should be
# templates. Those templates are used to render user attributes, where the # changed if using a custom provider.
# userinfo object is available through the `user` variable.
# #
config: config:
# name of the claim containing a unique identifier for the user. # name of the claim containing a unique identifier for the user.
@ -165,11 +183,19 @@ class OIDCConfig(Config):
# #
#subject_claim: "sub" #subject_claim: "sub"
# Jinja2 template for the localpart of the MXID # 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
#
# This must be configured if using the default mapping provider.
# #
localpart_template: "{{{{ user.preferred_username }}}}" localpart_template: "{{{{ user.preferred_username }}}}"
# Jinja2 template for the display name to set on first login. Optional. # 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 }}}}" #display_name_template: "{{{{ user.given_name }}}} {{{{ user.last_name }}}}"
""".format( """.format(

View file

@ -218,6 +218,8 @@ class SAML2Config(Config):
def generate_config_section(self, config_dir_path, server_name, **kwargs): def generate_config_section(self, config_dir_path, server_name, **kwargs):
return """\ return """\
## Single sign-on integration ##
# Enable SAML2 for registration and login. Uses pysaml2. # Enable SAML2 for registration and login. Uses pysaml2.
# #
# At least one of `sp_config` or `config_path` must be set in this section to # At least one of `sp_config` or `config_path` must be set in this section to

View file

@ -61,7 +61,8 @@ class SSOConfig(Config):
def generate_config_section(self, **kwargs): def generate_config_section(self, **kwargs):
return """\ return """\
# Additional settings to use with single-sign on systems such as SAML2 and CAS. # Additional settings to use with single-sign on systems such as OpenID Connect,
# SAML2 and CAS.
# #
sso: sso:
# A list of client URLs which are whitelisted so that the user does not # A list of client URLs which are whitelisted so that the user does not

View file

@ -37,6 +37,7 @@ from twisted.web.client import readBody
from synapse.config import ConfigError from synapse.config import ConfigError
from synapse.http.server import finish_request from synapse.http.server import finish_request
from synapse.http.site import SynapseRequest from synapse.http.site import SynapseRequest
from synapse.logging.context import make_deferred_yieldable
from synapse.push.mailer import load_jinja2_templates from synapse.push.mailer import load_jinja2_templates
from synapse.server import HomeServer from synapse.server import HomeServer
from synapse.types import UserID, map_username_to_mxid_localpart from synapse.types import UserID, map_username_to_mxid_localpart
@ -99,7 +100,6 @@ class OidcHandler:
hs.config.oidc_client_auth_method, hs.config.oidc_client_auth_method,
) # type: ClientAuth ) # type: ClientAuth
self._client_auth_method = hs.config.oidc_client_auth_method # type: str self._client_auth_method = hs.config.oidc_client_auth_method # type: str
self._subject_claim = hs.config.oidc_subject_claim
self._provider_metadata = OpenIDProviderMetadata( self._provider_metadata = OpenIDProviderMetadata(
issuer=hs.config.oidc_issuer, issuer=hs.config.oidc_issuer,
authorization_endpoint=hs.config.oidc_authorization_endpoint, authorization_endpoint=hs.config.oidc_authorization_endpoint,
@ -310,6 +310,10 @@ class OidcHandler:
received in the callback to exchange it for a token. The call uses the received in the callback to exchange it for a token. The call uses the
``ClientAuth`` to authenticate with the client with its ID and secret. ``ClientAuth`` to authenticate with the client with its ID and secret.
See:
https://tools.ietf.org/html/rfc6749#section-3.2
https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
Args: Args:
code: The authorization code we got from the callback. code: The authorization code we got from the callback.
@ -362,7 +366,7 @@ class OidcHandler:
code=response.code, phrase=response.phrase.decode("utf-8") code=response.code, phrase=response.phrase.decode("utf-8")
) )
resp_body = await readBody(response) resp_body = await make_deferred_yieldable(readBody(response))
if response.code >= 500: if response.code >= 500:
# In case of a server error, we should first try to decode the body # In case of a server error, we should first try to decode the body
@ -484,6 +488,7 @@ class OidcHandler:
claims_params=claims_params, claims_params=claims_params,
) )
except ValueError: except ValueError:
logger.info("Reloading JWKS after decode error")
jwk_set = await self.load_jwks(force=True) # try reloading the jwks jwk_set = await self.load_jwks(force=True) # try reloading the jwks
claims = jwt.decode( claims = jwt.decode(
token["id_token"], token["id_token"],
@ -592,6 +597,9 @@ class OidcHandler:
# The provider might redirect with an error. # The provider might redirect with an error.
# In that case, just display it as-is. # In that case, just display it as-is.
if b"error" in request.args: if b"error" in request.args:
# error response from the auth server. see:
# https://tools.ietf.org/html/rfc6749#section-4.1.2.1
# https://openid.net/specs/openid-connect-core-1_0.html#AuthError
error = request.args[b"error"][0].decode() error = request.args[b"error"][0].decode()
description = request.args.get(b"error_description", [b""])[0].decode() description = request.args.get(b"error_description", [b""])[0].decode()
@ -605,8 +613,11 @@ class OidcHandler:
self._render_error(request, error, description) self._render_error(request, error, description)
return return
# otherwise, it is presumably a successful response. see:
# https://tools.ietf.org/html/rfc6749#section-4.1.2
# Fetch the session cookie # Fetch the session cookie
session = request.getCookie(SESSION_COOKIE_NAME) session = request.getCookie(SESSION_COOKIE_NAME) # type: Optional[bytes]
if session is None: if session is None:
logger.info("No session cookie found") logger.info("No session cookie found")
self._render_error(request, "missing_session", "No session cookie found") self._render_error(request, "missing_session", "No session cookie found")
@ -654,7 +665,7 @@ class OidcHandler:
self._render_error(request, "invalid_request", "Code parameter is missing") self._render_error(request, "invalid_request", "Code parameter is missing")
return return
logger.info("Exchanging code") logger.debug("Exchanging code")
code = request.args[b"code"][0].decode() code = request.args[b"code"][0].decode()
try: try:
token = await self._exchange_code(code) token = await self._exchange_code(code)
@ -663,10 +674,12 @@ class OidcHandler:
self._render_error(request, e.error, e.error_description) self._render_error(request, e.error, e.error_description)
return return
logger.debug("Successfully obtained OAuth2 access token")
# Now that we have a token, get the userinfo, either by decoding the # Now that we have a token, get the userinfo, either by decoding the
# `id_token` or by fetching the `userinfo_endpoint`. # `id_token` or by fetching the `userinfo_endpoint`.
if self._uses_userinfo: if self._uses_userinfo:
logger.info("Fetching userinfo") logger.debug("Fetching userinfo")
try: try:
userinfo = await self._fetch_userinfo(token) userinfo = await self._fetch_userinfo(token)
except Exception as e: except Exception as e:
@ -674,7 +687,7 @@ class OidcHandler:
self._render_error(request, "fetch_error", str(e)) self._render_error(request, "fetch_error", str(e))
return return
else: else:
logger.info("Extracting userinfo from id_token") logger.debug("Extracting userinfo from id_token")
try: try:
userinfo = await self._parse_id_token(token, nonce=nonce) userinfo = await self._parse_id_token(token, nonce=nonce)
except Exception as e: except Exception as e:
@ -750,7 +763,7 @@ class OidcHandler:
return macaroon.serialize() return macaroon.serialize()
def _verify_oidc_session_token( def _verify_oidc_session_token(
self, session: str, state: str self, session: bytes, state: str
) -> Tuple[str, str, Optional[str]]: ) -> Tuple[str, str, Optional[str]]:
"""Verifies and extract an OIDC session token. """Verifies and extract an OIDC session token.