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

Implement OpenID Connect-based login #7256

Merged
merged 45 commits into from
May 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
968791b
Implement OpenID Connect-based login
sandhose Apr 9, 2020
ff202e5
OIDC login: allow specifying custom endpoints
sandhose Apr 16, 2020
380260e
OIDC login: include redirect_uri in grant & authorization steps
sandhose Apr 16, 2020
582e19b
OIDC login: make user properties configurable
sandhose Apr 16, 2020
7dccd63
OIDC login: add changelog entry
sandhose Apr 16, 2020
e8f893a
OIDC login: disable by default
sandhose Apr 16, 2020
82a8efb
OIDC login: add some tests
sandhose Apr 17, 2020
cc7250f
OIDC login: add some typings
sandhose Apr 17, 2020
359a55c
OIDC login: allow skipping verification
sandhose Apr 18, 2020
8a78906
OIDC login: fix syntax
sandhose Apr 18, 2020
0f3f3db
OIDC login: allow fetching userinfo
sandhose Apr 18, 2020
42988e0
OIDC login: re-generate sample config
sandhose Apr 19, 2020
b0c0f52
Send `Accept: application/json` in SimpleHttpClient JSON methods
sandhose Apr 19, 2020
61ba148
OIDC login: support alternative client auth methods
sandhose Apr 19, 2020
a0836db
OIDC login: reload jwks if jwt decoding fails
sandhose Apr 19, 2020
7ff9a5d
OIDC login: fix test on python 3.5
sandhose Apr 19, 2020
0809627
OIDC login: fix test on python 3.5 (attempt #2)
sandhose Apr 20, 2020
b9f18c1
OIDC login: user-friendly errors
sandhose Apr 21, 2020
3052ea8
OIDC login: add docs to test with some providers
sandhose Apr 21, 2020
5583c47
OIDC login: fix auth method default config
sandhose Apr 21, 2020
2a5f66c
OIDC login: regenerate sample config
sandhose Apr 21, 2020
6d77dcb
OIDC login: fix lint
sandhose Apr 21, 2020
a77ab05
OIDC login: test oauth2 callback
sandhose Apr 21, 2020
d662043
OIDC login: fix test on python 3.5
sandhose Apr 21, 2020
5668fc5
OIDC login: add a lot of docstrings
sandhose Apr 24, 2020
4f8ffaa
OIDC login: custom modules for user mappings
sandhose May 3, 2020
782bf3a
OIDC login: remove type hints from docstrings
sandhose May 3, 2020
e9b0138
OIDC login: fix various things
sandhose May 3, 2020
244e7c5
OIDC login: use the SSO template dir for OIDC
sandhose May 3, 2020
1ef3b20
OIDC login: fix syntax on python 3.5
sandhose May 3, 2020
c60fa61
OIDC login: simplify the code exchange request
sandhose May 3, 2020
6e2a0db
OIDC login: fix code exchange request on py3.5
sandhose May 3, 2020
e76c50a
OIDC login: fix sample config in docs
sandhose May 3, 2020
543e046
OIDC login: proper error handling in code exchange
sandhose May 5, 2020
ea5d71d
OIDC login: more tests
sandhose May 5, 2020
2181fac
Apply suggestions from code review
sandhose May 5, 2020
02f8dc0
OIDC login: fix sample config templates
sandhose May 5, 2020
a6b6bd9
OIDC login: log throughout the process
sandhose May 5, 2020
be7b732
OIDC login: check the validity of the auth method
sandhose May 5, 2020
c358f72
OIDC login: fix dev docs & add Twitch example
sandhose May 5, 2020
9bd40d1
OIDC login: make the user attribute mapping async
sandhose May 7, 2020
eace065
OIDC login: check for None values in metadata
sandhose May 7, 2020
b3e7b6c
OIDC login: refactor macaroon generation/verification
sandhose May 7, 2020
cfa177c
OIDC login: add docstrings to tests
sandhose May 7, 2020
0f5b4fd
OIDC login: various fixes from PR review
sandhose May 7, 2020
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/7256.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add OpenID Connect login/registration support. Contributed by Quentin Gliech, on behalf of [les Connecteurs](https://connecteu.rs).
175 changes: 175 additions & 0 deletions docs/dev/oidc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# 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 }}'
```
95 changes: 95 additions & 0 deletions docs/sample_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1458,6 +1458,94 @@ saml2_config:
#template_dir: "res/templates"


# Enable OpenID Connect for registration and login. Uses authlib.
#
oidc_config:
# enable OpenID Connect. Defaults to false.
#
#enabled: true

# use the OIDC discovery mechanism to discover endpoints. Defaults to true.
#
#discover: true

# the OIDC issuer. Used to validate tokens and discover the providers endpoints. Required.
#
#issuer: "https://accounts.example.com/"

# oauth2 client id to use. Required.
#
#client_id: "provided-by-your-issuer"

# oauth2 client secret to use. Required.
#
#client_secret: "provided-by-your-issuer"

# auth method to use when exchanging the token.
# Valid values are "client_secret_basic" (default), "client_secret_post" and "none".
#
#client_auth_method: "client_auth_basic"

# list of scopes to ask. This should include the "openid" scope. Defaults to ["openid"].
#
#scopes: ["openid"]

# 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 asked.
#
#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"

# 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: false


# 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'.
#
#module: mapping_provider.OidcMappingProvider

# Custom configuration values for the module. Below options are intended
# for the built-in provider, they should be changed if using a custom
# module. This section will be passed as a Python dictionary to the
# module's `parse_config` method.
#
# Below is the config of the default mapping provider, based on Jinja2
# templates. Those templates are used to render user attributes, where the
# userinfo object is available through the `user` variable.
#
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
#
localpart_template: "{{ user.preferred_username }}"

# Jinja2 template for the display name to set on first login. Optional.
#
#display_name_template: "{{ user.given_name }} {{ user.last_name }}"



# Enable CAS for registration and login.
#
Expand Down Expand Up @@ -1542,6 +1630,13 @@ sso:
#
# This template has no additional variables.
#
# * HTML page to display to users if something goes wrong during the
# OpenID Connect authentication process: 'sso_error.html'.
#
# When rendering, this template is given two variables:
# * error: the technical name of the error
# * error_description: a human-readable message for the error
#
# You can see the default templates at:
# https://github.com/matrix-org/synapse/tree/master/synapse/res/templates
#
Expand Down
3 changes: 3 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,6 @@ ignore_missing_imports = True

[mypy-jwt.*]
ignore_missing_imports = True

[mypy-authlib.*]
ignore_missing_imports = True
12 changes: 12 additions & 0 deletions synapse/app/homeserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,11 @@ def _configure_named_resource(self, name, compress=False):
}
)

if self.get_config().oidc_enabled:
from synapse.rest.oidc import OIDCResource

resources["/_synapse/oidc"] = OIDCResource(self)

if self.get_config().saml2_enabled:
from synapse.rest.saml2 import SAML2Resource

Expand Down Expand Up @@ -422,6 +427,13 @@ def start():
# Check if it needs to be reprovisioned every day.
hs.get_clock().looping_call(reprovision_acme, 24 * 60 * 60 * 1000)

# Load the OIDC provider metadatas, if OIDC is enabled.
if hs.config.oidc_enabled:
sandhose marked this conversation as resolved.
Show resolved Hide resolved
oidc = hs.get_oidc_handler()
# Loading the provider metadata also ensures the provider config is valid.
yield defer.ensureDeferred(oidc.load_metadata())
yield defer.ensureDeferred(oidc.load_jwks())

_base.start(hs, config.listeners)

hs.get_datastore().db.updates.start_doing_background_updates()
Expand Down
2 changes: 2 additions & 0 deletions synapse/config/_base.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ from synapse.config import (
key,
logger,
metrics,
oidc_config,
password,
password_auth_providers,
push,
Expand Down Expand Up @@ -59,6 +60,7 @@ class RootConfig:
saml2: saml2_config.SAML2Config
cas: cas.CasConfig
sso: sso.SSOConfig
oidc: oidc_config.OIDCConfig
jwt: jwt_config.JWTConfig
password: password.PasswordConfig
email: emailconfig.EmailConfig
Expand Down
2 changes: 2 additions & 0 deletions synapse/config/homeserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from .key import KeyConfig
from .logger import LoggingConfig
from .metrics import MetricsConfig
from .oidc_config import OIDCConfig
from .password import PasswordConfig
from .password_auth_providers import PasswordAuthProviderConfig
from .push import PushConfig
Expand Down Expand Up @@ -66,6 +67,7 @@ class HomeServerConfig(RootConfig):
AppServiceConfig,
KeyConfig,
SAML2Config,
OIDCConfig,
CasConfig,
SSOConfig,
JWTConfig,
Expand Down
Loading