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

Store an IdP ID in the OIDC session #9109

Merged
merged 6 commits into from
Jan 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/9109.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for multiple SSO Identity Providers.
26 changes: 23 additions & 3 deletions synapse/config/oidc_config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2020 Quentin Gliech
# Copyright 2020 The Matrix.org Foundation C.I.C.
# Copyright 2020-2021 The Matrix.org Foundation C.I.C.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -14,6 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import string
from typing import Optional, Type

import attr
Expand All @@ -38,7 +39,7 @@ def read_config(self, config, **kwargs):

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")
validate_config(OIDC_PROVIDER_CONFIG_SCHEMA, oidc_config, ("oidc_config",))
self.oidc_provider = _parse_oidc_config_dict(oidc_config)

if not self.oidc_provider:
Expand Down Expand Up @@ -205,6 +206,8 @@ def generate_config_section(self, config_dir_path, server_name, **kwargs):
"type": "object",
"required": ["issuer", "client_id", "client_secret"],
"properties": {
"idp_id": {"type": "string", "minLength": 1, "maxLength": 128},
"idp_name": {"type": "string"},
"discover": {"type": "boolean"},
"issuer": {"type": "string"},
"client_id": {"type": "string"},
Expand Down Expand Up @@ -277,7 +280,17 @@ def _parse_oidc_config_dict(oidc_config: JsonDict) -> "OidcProviderConfig":
"methods: %s" % (", ".join(missing_methods),)
)

# MSC2858 will appy certain limits in what can be used as an IdP id, so let's
# enforce those limits now.
idp_id = oidc_config.get("idp_id", "oidc")
valid_idp_chars = set(string.ascii_letters + string.digits + "-._~")

if any(c not in valid_idp_chars for c in idp_id):
raise ConfigError('idp_id may only contain A-Z, a-z, 0-9, "-", ".", "_", "~"')

return OidcProviderConfig(
idp_id=idp_id,
idp_name=oidc_config.get("idp_name", "OIDC"),
discover=oidc_config.get("discover", True),
issuer=oidc_config["issuer"],
client_id=oidc_config["client_id"],
Expand All @@ -296,8 +309,15 @@ def _parse_oidc_config_dict(oidc_config: JsonDict) -> "OidcProviderConfig":
)


@attr.s
@attr.s(slots=True, frozen=True)
class OidcProviderConfig:
# a unique identifier for this identity provider. Used in the 'user_external_ids'
# table, as well as the query/path parameter used in the login protocol.
idp_id = attr.ib(type=str)

# user-facing name for this identity provider.
idp_name = attr.ib(type=str)

# whether the OIDC discovery mechanism is used to discover endpoints
discover = attr.ib(type=bool)

Expand Down
22 changes: 16 additions & 6 deletions synapse/handlers/oidc_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ async def handle_oidc_callback(self, request: SynapseRequest) -> None:
session_data = self._token_generator.verify_oidc_session_token(
session, state
)
except MacaroonDeserializationException as e:
except (MacaroonDeserializationException, ValueError) as e:
logger.exception("Invalid session")
self._sso_handler.render_error(request, "invalid_session", str(e))
return
Expand Down Expand Up @@ -253,10 +253,10 @@ def __init__(
self._server_name = hs.config.server_name # type: str

# identifier for the external_ids table
self.idp_id = "oidc"
self.idp_id = provider.idp_id

# user-facing name of this auth provider
self.idp_name = "OIDC"
self.idp_name = provider.idp_name

self._sso_handler = hs.get_sso_handler()

Expand Down Expand Up @@ -656,6 +656,7 @@ async def handle_redirect_request(
cookie = self._token_generator.generate_oidc_session_token(
state=state,
session_data=OidcSessionData(
idp_id=self.idp_id,
nonce=nonce,
client_redirect_url=client_redirect_url.decode(),
ui_auth_session_id=ui_auth_session_id,
Expand Down Expand Up @@ -924,6 +925,7 @@ def generate_oidc_session_token(
macaroon.add_first_party_caveat("gen = 1")
macaroon.add_first_party_caveat("type = session")
macaroon.add_first_party_caveat("state = %s" % (state,))
macaroon.add_first_party_caveat("idp_id = %s" % (session_data.idp_id,))
macaroon.add_first_party_caveat("nonce = %s" % (session_data.nonce,))
macaroon.add_first_party_caveat(
"client_redirect_url = %s" % (session_data.client_redirect_url,)
Expand Down Expand Up @@ -952,6 +954,9 @@ def verify_oidc_session_token(

Returns:
The data extracted from the session cookie

Raises:
ValueError if an expected caveat is missing from the macaroon.
"""
macaroon = pymacaroons.Macaroon.deserialize(session)

Expand All @@ -960,6 +965,7 @@ def verify_oidc_session_token(
v.satisfy_exact("type = session")
v.satisfy_exact("state = %s" % (state,))
v.satisfy_general(lambda c: c.startswith("nonce = "))
v.satisfy_general(lambda c: c.startswith("idp_id = "))
v.satisfy_general(lambda c: c.startswith("client_redirect_url = "))
# Sometimes there's a UI auth session ID, it seems to be OK to attempt
# to always satisfy this.
Expand All @@ -968,9 +974,9 @@ def verify_oidc_session_token(

v.verify(macaroon, self._macaroon_secret_key)

# Extract the `nonce`, `client_redirect_url`, and maybe the
# `ui_auth_session_id` from the token.
# Extract the session data from the token.
nonce = self._get_value_from_macaroon(macaroon, "nonce")
idp_id = self._get_value_from_macaroon(macaroon, "idp_id")
client_redirect_url = self._get_value_from_macaroon(
macaroon, "client_redirect_url"
)
Expand All @@ -983,6 +989,7 @@ def verify_oidc_session_token(

return OidcSessionData(
nonce=nonce,
idp_id=idp_id,
client_redirect_url=client_redirect_url,
ui_auth_session_id=ui_auth_session_id,
)
Expand All @@ -998,7 +1005,7 @@ def _get_value_from_macaroon(self, macaroon: pymacaroons.Macaroon, key: str) ->
The extracted value

Raises:
Exception: if the caveat was not in the macaroon
ValueError: if the caveat was not in the macaroon
"""
prefix = key + " = "
for caveat in macaroon.caveats:
Expand All @@ -1019,6 +1026,9 @@ def _verify_expiry(self, caveat: str) -> bool:
class OidcSessionData:
"""The attributes which are stored in a OIDC session cookie"""

# the Identity Provider being used
idp_id = attr.ib(type=str)

# The `nonce` parameter passed to the OIDC provider.
nonce = attr.ib(type=str)

Expand Down
3 changes: 2 additions & 1 deletion tests/handlers/test_oidc.py
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,7 @@ def _generate_oidc_session_token(
return self.handler._token_generator.generate_oidc_session_token(
state=state,
session_data=OidcSessionData(
idp_id="oidc",
nonce=nonce,
client_redirect_url=client_redirect_url,
ui_auth_session_id=ui_auth_session_id,
Expand Down Expand Up @@ -990,7 +991,7 @@ async def _make_callback_with_userinfo(
session = handler._token_generator.generate_oidc_session_token(
state=state,
session_data=OidcSessionData(
nonce="nonce", client_redirect_url=client_redirect_url,
idp_id="oidc", nonce="nonce", client_redirect_url=client_redirect_url,
),
)
request = _build_callback_request("code", state, session)
Expand Down