Refactor config to be an experimental feature
Also enforce you can't combine it with incompatible config options
This commit is contained in:
parent
03920bdd4e
commit
249f4a338d
|
@ -65,7 +65,7 @@ class PrivateKeyJWTWithKid(PrivateKeyJWT):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class OAuthDelegatedAuth(BaseAuth):
|
class MSC3861DelegatedAuth(BaseAuth):
|
||||||
AUTH_METHODS = {
|
AUTH_METHODS = {
|
||||||
"client_secret_post": encode_client_secret_post,
|
"client_secret_post": encode_client_secret_post,
|
||||||
"client_secret_basic": encode_client_secret_basic,
|
"client_secret_basic": encode_client_secret_basic,
|
||||||
|
@ -78,35 +78,38 @@ class OAuthDelegatedAuth(BaseAuth):
|
||||||
def __init__(self, hs: "HomeServer"):
|
def __init__(self, hs: "HomeServer"):
|
||||||
super().__init__(hs)
|
super().__init__(hs)
|
||||||
|
|
||||||
self._config = hs.config.auth
|
self._config = hs.config.experimental.msc3861
|
||||||
assert self._config.oauth_delegation_enabled, "OAuth delegation is not enabled"
|
auth_method = MSC3861DelegatedAuth.AUTH_METHODS.get(
|
||||||
assert self._config.oauth_delegation_issuer, "No issuer provided"
|
self._config.client_auth_method.value, None
|
||||||
assert self._config.oauth_delegation_client_id, "No client_id provided"
|
)
|
||||||
assert self._config.oauth_delegation_client_secret, "No client_secret provided"
|
# Those assertions are already checked when parsing the config
|
||||||
assert (
|
assert self._config.enabled, "OAuth delegation is not enabled"
|
||||||
self._config.oauth_delegation_client_auth_method
|
assert self._config.issuer, "No issuer provided"
|
||||||
in OAuthDelegatedAuth.AUTH_METHODS
|
assert self._config.client_id, "No client_id provided"
|
||||||
), "Invalid client_auth_method"
|
assert auth_method is not None, "Invalid client_auth_method provided"
|
||||||
|
|
||||||
self._http_client = hs.get_proxied_http_client()
|
self._http_client = hs.get_proxied_http_client()
|
||||||
self._hostname = hs.hostname
|
self._hostname = hs.hostname
|
||||||
|
|
||||||
self._issuer_metadata = RetryOnExceptionCachedCall(self._load_metadata)
|
self._issuer_metadata = RetryOnExceptionCachedCall(self._load_metadata)
|
||||||
secret = self._config.oauth_delegation_client_secret
|
|
||||||
self._client_auth = ClientAuth(
|
if isinstance(auth_method, PrivateKeyJWTWithKid):
|
||||||
self._config.oauth_delegation_client_id,
|
# Use the JWK as the client secret when using the private_key_jwt method
|
||||||
secret,
|
assert self._config.jwk, "No JWK provided"
|
||||||
OAuthDelegatedAuth.AUTH_METHODS[
|
self._client_auth = ClientAuth(
|
||||||
self._config.oauth_delegation_client_auth_method
|
self._config.client_id, self._config.jwk, auth_method
|
||||||
],
|
)
|
||||||
)
|
else:
|
||||||
|
# Else use the client secret
|
||||||
|
assert self._config.client_secret, "No client_secret provided"
|
||||||
|
self._client_auth = ClientAuth(
|
||||||
|
self._config.client_id, self._config.client_secret, auth_method
|
||||||
|
)
|
||||||
|
|
||||||
async def _load_metadata(self) -> OpenIDProviderMetadata:
|
async def _load_metadata(self) -> OpenIDProviderMetadata:
|
||||||
if self._config.oauth_delegation_issuer_metadata is not None:
|
if self._config.issuer_metadata is not None:
|
||||||
return OpenIDProviderMetadata(
|
return OpenIDProviderMetadata(**self._config.issuer_metadata)
|
||||||
**self._config.oauth_delegation_issuer_metadata
|
url = get_well_known_url(self._config.issuer, external=True)
|
||||||
)
|
|
||||||
url = get_well_known_url(self._config.oauth_delegation_issuer, external=True)
|
|
||||||
response = await self._http_client.get_json(url)
|
response = await self._http_client.get_json(url)
|
||||||
metadata = OpenIDProviderMetadata(**response)
|
metadata = OpenIDProviderMetadata(**response)
|
||||||
# metadata.validate_introspection_endpoint()
|
# metadata.validate_introspection_endpoint()
|
||||||
|
@ -203,7 +206,7 @@ class OAuthDelegatedAuth(BaseAuth):
|
||||||
)
|
)
|
||||||
|
|
||||||
user_id_str = await self.store.get_user_by_external_id(
|
user_id_str = await self.store.get_user_by_external_id(
|
||||||
OAuthDelegatedAuth.EXTERNAL_ID_PROVIDER, sub
|
MSC3861DelegatedAuth.EXTERNAL_ID_PROVIDER, sub
|
||||||
)
|
)
|
||||||
if user_id_str is None:
|
if user_id_str is None:
|
||||||
# If we could not find a user via the external_id, it either does not exist,
|
# If we could not find a user via the external_id, it either does not exist,
|
||||||
|
@ -236,7 +239,7 @@ class OAuthDelegatedAuth(BaseAuth):
|
||||||
|
|
||||||
# And record the sub as external_id
|
# And record the sub as external_id
|
||||||
await self.store.record_user_external_id(
|
await self.store.record_user_external_id(
|
||||||
OAuthDelegatedAuth.EXTERNAL_ID_PROVIDER, sub, user_id.to_string()
|
MSC3861DelegatedAuth.EXTERNAL_ID_PROVIDER, sub, user_id.to_string()
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
user_id = UserID.from_string(user_id_str)
|
user_id = UserID.from_string(user_id_str)
|
|
@ -14,11 +14,9 @@
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from authlib.jose.rfc7517 import JsonWebKey
|
|
||||||
|
|
||||||
from synapse.types import JsonDict
|
from synapse.types import JsonDict
|
||||||
|
|
||||||
from ._base import Config, ConfigError
|
from ._base import Config
|
||||||
|
|
||||||
|
|
||||||
class AuthConfig(Config):
|
class AuthConfig(Config):
|
||||||
|
@ -31,7 +29,14 @@ class AuthConfig(Config):
|
||||||
if password_config is None:
|
if password_config is None:
|
||||||
password_config = {}
|
password_config = {}
|
||||||
|
|
||||||
passwords_enabled = password_config.get("enabled", True)
|
# The default value of password_config.enabled is True, unless msc3861 is enabled.
|
||||||
|
msc3861_enabled = (
|
||||||
|
config.get("experimental_features", {})
|
||||||
|
.get("msc3861", {})
|
||||||
|
.get("enabled", False)
|
||||||
|
)
|
||||||
|
passwords_enabled = password_config.get("enabled", not msc3861_enabled)
|
||||||
|
|
||||||
# 'only_for_reauth' allows users who have previously set a password to use it,
|
# 'only_for_reauth' allows users who have previously set a password to use it,
|
||||||
# even though passwords would otherwise be disabled.
|
# even though passwords would otherwise be disabled.
|
||||||
passwords_for_reauth_only = passwords_enabled == "only_for_reauth"
|
passwords_for_reauth_only = passwords_enabled == "only_for_reauth"
|
||||||
|
@ -55,29 +60,3 @@ class AuthConfig(Config):
|
||||||
self.ui_auth_session_timeout = self.parse_duration(
|
self.ui_auth_session_timeout = self.parse_duration(
|
||||||
ui_auth.get("session_timeout", 0)
|
ui_auth.get("session_timeout", 0)
|
||||||
)
|
)
|
||||||
|
|
||||||
oauth_delegation = config.get("oauth_delegation", {})
|
|
||||||
self.oauth_delegation_enabled = oauth_delegation.get("enabled", False)
|
|
||||||
self.oauth_delegation_issuer = oauth_delegation.get("issuer", "")
|
|
||||||
self.oauth_delegation_issuer_metadata = oauth_delegation.get("issuer_metadata")
|
|
||||||
self.oauth_delegation_account = oauth_delegation.get("account", "")
|
|
||||||
self.oauth_delegation_client_id = oauth_delegation.get("client_id", "")
|
|
||||||
self.oauth_delegation_client_secret = oauth_delegation.get("client_secret", "")
|
|
||||||
self.oauth_delegation_client_auth_method = oauth_delegation.get(
|
|
||||||
"client_auth_method", "client_secret_post"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.password_enabled = password_config.get(
|
|
||||||
"enabled", not self.oauth_delegation_enabled
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.oauth_delegation_client_auth_method == "private_key_jwt":
|
|
||||||
self.oauth_delegation_client_secret = JsonWebKey.import_key(
|
|
||||||
self.oauth_delegation_client_secret
|
|
||||||
)
|
|
||||||
|
|
||||||
# If we are delegating via OAuth then password cannot be supported as well
|
|
||||||
if self.oauth_delegation_enabled and self.password_enabled:
|
|
||||||
raise ConfigError(
|
|
||||||
"Password auth cannot be enabled when OAuth delegation is enabled"
|
|
||||||
)
|
|
||||||
|
|
|
@ -12,15 +12,196 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from typing import Any, Optional
|
import enum
|
||||||
|
from typing import TYPE_CHECKING, Any, Optional
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
|
import attr.validators
|
||||||
|
|
||||||
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersions
|
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersions
|
||||||
from synapse.config import ConfigError
|
from synapse.config import ConfigError
|
||||||
from synapse.config._base import Config
|
from synapse.config._base import Config, RootConfig
|
||||||
from synapse.types import JsonDict
|
from synapse.types import JsonDict
|
||||||
|
|
||||||
|
# Determine whether authlib is installed.
|
||||||
|
try:
|
||||||
|
import authlib # noqa: F401
|
||||||
|
|
||||||
|
HAS_AUTHLIB = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_AUTHLIB = False
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
# Only import this if we're type checking, as it might not be installed at runtime.
|
||||||
|
from authlib.jose.rfc7517 import JsonWebKey
|
||||||
|
|
||||||
|
|
||||||
|
class ClientAuthMethod(enum.Enum):
|
||||||
|
"""List of supported client auth methods."""
|
||||||
|
|
||||||
|
CLIENT_SECRET_POST = "client_secret_post"
|
||||||
|
CLIENT_SECRET_BASIC = "client_secret_basic"
|
||||||
|
CLIENT_SECRET_JWT = "client_secret_jwt"
|
||||||
|
PRIVATE_KEY_JWT = "private_key_jwt"
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_jwks(jwks: Optional[JsonDict]) -> Optional["JsonWebKey"]:
|
||||||
|
"""A helper function to parse a JWK dict into a JsonWebKey."""
|
||||||
|
|
||||||
|
if jwks is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
from authlib.jose.rfc7517 import JsonWebKey
|
||||||
|
|
||||||
|
return JsonWebKey.import_key(jwks)
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(slots=True, frozen=True)
|
||||||
|
class MSC3861:
|
||||||
|
"""Configuration for MSC3861: Matrix architecture change to delegate authentication via OIDC"""
|
||||||
|
|
||||||
|
enabled: bool = attr.ib(default=False, validator=attr.validators.instance_of(bool))
|
||||||
|
"""Whether to enable MSC3861 auth delegation."""
|
||||||
|
|
||||||
|
@enabled.validator
|
||||||
|
def _check_enabled(self, attribute: attr.Attribute, value: bool) -> None:
|
||||||
|
# Only allow enabling MSC3861 if authlib is installed
|
||||||
|
if value and not HAS_AUTHLIB:
|
||||||
|
raise ConfigError(
|
||||||
|
"MSC3861 is enabled but authlib is not installed. "
|
||||||
|
"Please install authlib to use MSC3861."
|
||||||
|
)
|
||||||
|
|
||||||
|
issuer: str = attr.ib(default="", validator=attr.validators.instance_of(str))
|
||||||
|
"""The URL of the OIDC Provider."""
|
||||||
|
|
||||||
|
issuer_metadata: Optional[JsonDict] = attr.ib(default=None)
|
||||||
|
"""The issuer metadata to use, otherwise discovered from /.well-known/openid-configuration as per MSC2965."""
|
||||||
|
|
||||||
|
client_id: str = attr.ib(
|
||||||
|
default="",
|
||||||
|
validator=attr.validators.instance_of(str),
|
||||||
|
)
|
||||||
|
"""The client ID to use when calling the introspection endpoint."""
|
||||||
|
|
||||||
|
client_auth_method: ClientAuthMethod = attr.ib(
|
||||||
|
default=ClientAuthMethod.CLIENT_SECRET_POST, converter=ClientAuthMethod
|
||||||
|
)
|
||||||
|
"""The auth method used when calling the introspection endpoint."""
|
||||||
|
|
||||||
|
client_secret: Optional[str] = attr.ib(
|
||||||
|
default=None,
|
||||||
|
validator=attr.validators.optional(attr.validators.instance_of(str)),
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
The client secret to use when calling the introspection endpoint,
|
||||||
|
when using any of the client_secret_* client auth methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
jwk: Optional["JsonWebKey"] = attr.ib(default=None, converter=_parse_jwks)
|
||||||
|
"""
|
||||||
|
The JWKS to use when calling the introspection endpoint,
|
||||||
|
when using the private_key_jwt client auth method.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@client_auth_method.validator
|
||||||
|
def _check_client_auth_method(
|
||||||
|
self, attribute: attr.Attribute, value: ClientAuthMethod
|
||||||
|
) -> None:
|
||||||
|
# Check that the right client credentials are provided for the client auth method.
|
||||||
|
if not self.enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
if value == ClientAuthMethod.PRIVATE_KEY_JWT and self.jwk is None:
|
||||||
|
raise ConfigError(
|
||||||
|
"A JWKS must be provided when using the private_key_jwt client auth method"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
value
|
||||||
|
in (
|
||||||
|
ClientAuthMethod.CLIENT_SECRET_POST,
|
||||||
|
ClientAuthMethod.CLIENT_SECRET_BASIC,
|
||||||
|
ClientAuthMethod.CLIENT_SECRET_JWT,
|
||||||
|
)
|
||||||
|
and self.client_secret is None
|
||||||
|
):
|
||||||
|
raise ConfigError(
|
||||||
|
f"A client secret must be provided when using the {value} client auth method"
|
||||||
|
)
|
||||||
|
|
||||||
|
account_management_url: Optional[str] = attr.ib(
|
||||||
|
default=None,
|
||||||
|
validator=attr.validators.optional(attr.validators.instance_of(str)),
|
||||||
|
)
|
||||||
|
"""The URL of the My Account page on the OIDC Provider as per MSC2965."""
|
||||||
|
|
||||||
|
def check_config_conflicts(self, root: RootConfig) -> None:
|
||||||
|
"""Checks for any configuration conflicts with other parts of Synapse.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ConfigError: If there are any configuration conflicts.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self.enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
if (
|
||||||
|
root.auth.password_enabled_for_reauth
|
||||||
|
or root.auth.password_enabled_for_login
|
||||||
|
):
|
||||||
|
raise ConfigError(
|
||||||
|
"Password auth cannot be enabled when OAuth delegation is enabled"
|
||||||
|
)
|
||||||
|
|
||||||
|
if root.registration.enable_registration:
|
||||||
|
raise ConfigError(
|
||||||
|
"Registration cannot be enabled when OAuth delegation is enabled"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
root.oidc.oidc_enabled
|
||||||
|
or root.saml2.saml2_enabled
|
||||||
|
or root.cas.cas_enabled
|
||||||
|
or root.jwt.jwt_enabled
|
||||||
|
):
|
||||||
|
raise ConfigError("SSO cannot be enabled when OAuth delegation is enabled")
|
||||||
|
|
||||||
|
if bool(root.authproviders.password_providers):
|
||||||
|
raise ConfigError(
|
||||||
|
"Password auth providers cannot be enabled when OAuth delegation is enabled"
|
||||||
|
)
|
||||||
|
|
||||||
|
if root.captcha.enable_registration_captcha:
|
||||||
|
raise ConfigError(
|
||||||
|
"CAPTCHA cannot be enabled when OAuth delegation is enabled"
|
||||||
|
)
|
||||||
|
|
||||||
|
if root.experimental.msc3882_enabled:
|
||||||
|
raise ConfigError(
|
||||||
|
"MSC3882 cannot be enabled when OAuth delegation is enabled"
|
||||||
|
)
|
||||||
|
|
||||||
|
if root.registration.refresh_token_lifetime:
|
||||||
|
raise ConfigError(
|
||||||
|
"refresh_token_lifetime cannot be set when OAuth delegation is enabled"
|
||||||
|
)
|
||||||
|
|
||||||
|
if root.registration.nonrefreshable_access_token_lifetime:
|
||||||
|
raise ConfigError(
|
||||||
|
"nonrefreshable_access_token_lifetime cannot be set when OAuth delegation is enabled"
|
||||||
|
)
|
||||||
|
|
||||||
|
if root.registration.session_lifetime:
|
||||||
|
raise ConfigError(
|
||||||
|
"session_lifetime cannot be set when OAuth delegation is enabled"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not root.experimental.msc3970_enabled:
|
||||||
|
raise ConfigError(
|
||||||
|
"experimental_features.msc3970_enabled must be 'true' when OAuth delegation is enabled"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@attr.s(auto_attribs=True, frozen=True, slots=True)
|
@attr.s(auto_attribs=True, frozen=True, slots=True)
|
||||||
class MSC3866Config:
|
class MSC3866Config:
|
||||||
|
@ -182,8 +363,14 @@ class ExperimentalConfig(Config):
|
||||||
"msc3981_recurse_relations", False
|
"msc3981_recurse_relations", False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# MSC3861: Matrix architecture change to delegate authentication via OIDC
|
||||||
|
self.msc3861 = MSC3861(**experimental.get("msc3861", {}))
|
||||||
|
|
||||||
# MSC3970: Scope transaction IDs to devices
|
# MSC3970: Scope transaction IDs to devices
|
||||||
self.msc3970_enabled = experimental.get("msc3970_enabled", False)
|
self.msc3970_enabled = experimental.get("msc3970_enabled", self.msc3861.enabled)
|
||||||
|
|
||||||
|
# Check that none of the other config options conflict with MSC3861 when enabled
|
||||||
|
self.msc3861.check_config_conflicts(self.root)
|
||||||
|
|
||||||
# MSC4009: E.164 Matrix IDs
|
# MSC4009: E.164 Matrix IDs
|
||||||
self.msc4009_e164_mxids = experimental.get("msc4009_e164_mxids", False)
|
self.msc4009_e164_mxids = experimental.get("msc4009_e164_mxids", False)
|
||||||
|
|
|
@ -274,7 +274,7 @@ class AuthHandler:
|
||||||
# response.
|
# response.
|
||||||
self._extra_attributes: Dict[str, SsoLoginExtraAttributes] = {}
|
self._extra_attributes: Dict[str, SsoLoginExtraAttributes] = {}
|
||||||
|
|
||||||
self.oauth_delegation_enabled = hs.config.auth.oauth_delegation_enabled
|
self.msc3861_oauth_delegation_enabled = hs.config.experimental.msc3861.enabled
|
||||||
|
|
||||||
async def validate_user_via_ui_auth(
|
async def validate_user_via_ui_auth(
|
||||||
self,
|
self,
|
||||||
|
@ -325,7 +325,7 @@ class AuthHandler:
|
||||||
LimitExceededError if the ratelimiter's failed request count for this
|
LimitExceededError if the ratelimiter's failed request count for this
|
||||||
user is too high to proceed
|
user is too high to proceed
|
||||||
"""
|
"""
|
||||||
if self.oauth_delegation_enabled:
|
if self.msc3861_oauth_delegation_enabled:
|
||||||
raise SynapseError(
|
raise SynapseError(
|
||||||
HTTPStatus.INTERNAL_SERVER_ERROR, "UIA shouldn't be used with MSC3861"
|
HTTPStatus.INTERNAL_SERVER_ERROR, "UIA shouldn't be used with MSC3861"
|
||||||
)
|
)
|
||||||
|
|
|
@ -38,6 +38,7 @@ from twisted.web.resource import Resource
|
||||||
|
|
||||||
from synapse.api import errors
|
from synapse.api import errors
|
||||||
from synapse.api.errors import SynapseError
|
from synapse.api.errors import SynapseError
|
||||||
|
from synapse.config import ConfigError
|
||||||
from synapse.events import EventBase
|
from synapse.events import EventBase
|
||||||
from synapse.events.presence_router import (
|
from synapse.events.presence_router import (
|
||||||
GET_INTERESTED_USERS_CALLBACK,
|
GET_INTERESTED_USERS_CALLBACK,
|
||||||
|
@ -252,6 +253,7 @@ class ModuleApi:
|
||||||
self._device_handler = hs.get_device_handler()
|
self._device_handler = hs.get_device_handler()
|
||||||
self.custom_template_dir = hs.config.server.custom_template_directory
|
self.custom_template_dir = hs.config.server.custom_template_directory
|
||||||
self._callbacks = hs.get_module_api_callbacks()
|
self._callbacks = hs.get_module_api_callbacks()
|
||||||
|
self.msc3861_oauth_delegation_enabled = hs.config.experimental.msc3861.enabled
|
||||||
|
|
||||||
try:
|
try:
|
||||||
app_name = self._hs.config.email.email_app_name
|
app_name = self._hs.config.email.email_app_name
|
||||||
|
@ -419,6 +421,11 @@ class ModuleApi:
|
||||||
|
|
||||||
Added in Synapse v1.46.0.
|
Added in Synapse v1.46.0.
|
||||||
"""
|
"""
|
||||||
|
if self.msc3861_oauth_delegation_enabled:
|
||||||
|
raise ConfigError(
|
||||||
|
"Cannot use password auth provider callbacks when OAuth delegation is enabled"
|
||||||
|
)
|
||||||
|
|
||||||
return self._password_auth_provider.register_password_auth_provider_callbacks(
|
return self._password_auth_provider.register_password_auth_provider_callbacks(
|
||||||
check_3pid_auth=check_3pid_auth,
|
check_3pid_auth=check_3pid_auth,
|
||||||
on_logged_out=on_logged_out,
|
on_logged_out=on_logged_out,
|
||||||
|
|
|
@ -601,7 +601,7 @@ class ThreepidRestServlet(RestServlet):
|
||||||
# ThreePidBindRestServelet.PostBody with an `alias_generator` to handle
|
# ThreePidBindRestServelet.PostBody with an `alias_generator` to handle
|
||||||
# `threePidCreds` versus `three_pid_creds`.
|
# `threePidCreds` versus `three_pid_creds`.
|
||||||
async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
|
async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
|
||||||
if self.hs.config.auth.oauth_delegation_enabled:
|
if self.hs.config.experimental.msc3861.enabled:
|
||||||
raise NotFoundError(errcode=Codes.UNRECOGNIZED)
|
raise NotFoundError(errcode=Codes.UNRECOGNIZED)
|
||||||
|
|
||||||
if not self.hs.config.registration.enable_3pid_changes:
|
if not self.hs.config.registration.enable_3pid_changes:
|
||||||
|
@ -894,7 +894,7 @@ class AccountStatusRestServlet(RestServlet):
|
||||||
|
|
||||||
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
||||||
if hs.config.worker.worker_app is None:
|
if hs.config.worker.worker_app is None:
|
||||||
if not hs.config.auth.oauth_delegation_enabled:
|
if not hs.config.experimental.msc3861.enabled:
|
||||||
EmailPasswordRequestTokenRestServlet(hs).register(http_server)
|
EmailPasswordRequestTokenRestServlet(hs).register(http_server)
|
||||||
DeactivateAccountRestServlet(hs).register(http_server)
|
DeactivateAccountRestServlet(hs).register(http_server)
|
||||||
PasswordRestServlet(hs).register(http_server)
|
PasswordRestServlet(hs).register(http_server)
|
||||||
|
@ -906,7 +906,7 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
||||||
if hs.config.worker.worker_app is None:
|
if hs.config.worker.worker_app is None:
|
||||||
ThreepidBindRestServlet(hs).register(http_server)
|
ThreepidBindRestServlet(hs).register(http_server)
|
||||||
ThreepidUnbindRestServlet(hs).register(http_server)
|
ThreepidUnbindRestServlet(hs).register(http_server)
|
||||||
if not hs.config.auth.oauth_delegation_enabled:
|
if not hs.config.experimental.msc3861.enabled:
|
||||||
ThreepidAddRestServlet(hs).register(http_server)
|
ThreepidAddRestServlet(hs).register(http_server)
|
||||||
ThreepidDeleteRestServlet(hs).register(http_server)
|
ThreepidDeleteRestServlet(hs).register(http_server)
|
||||||
WhoamiRestServlet(hs).register(http_server)
|
WhoamiRestServlet(hs).register(http_server)
|
||||||
|
|
|
@ -135,7 +135,7 @@ class DeviceRestServlet(RestServlet):
|
||||||
self.device_handler = handler
|
self.device_handler = handler
|
||||||
self.auth_handler = hs.get_auth_handler()
|
self.auth_handler = hs.get_auth_handler()
|
||||||
self._msc3852_enabled = hs.config.experimental.msc3852_enabled
|
self._msc3852_enabled = hs.config.experimental.msc3852_enabled
|
||||||
self.oauth_delegation_enabled = hs.config.auth.oauth_delegation_enabled
|
self._msc3861_oauth_delegation_enabled = hs.config.experimental.msc3861.enabled
|
||||||
|
|
||||||
async def on_GET(
|
async def on_GET(
|
||||||
self, request: SynapseRequest, device_id: str
|
self, request: SynapseRequest, device_id: str
|
||||||
|
@ -167,7 +167,7 @@ class DeviceRestServlet(RestServlet):
|
||||||
async def on_DELETE(
|
async def on_DELETE(
|
||||||
self, request: SynapseRequest, device_id: str
|
self, request: SynapseRequest, device_id: str
|
||||||
) -> Tuple[int, JsonDict]:
|
) -> Tuple[int, JsonDict]:
|
||||||
if self.oauth_delegation_enabled:
|
if self._msc3861_oauth_delegation_enabled:
|
||||||
raise UnrecognizedRequestError(code=404)
|
raise UnrecognizedRequestError(code=404)
|
||||||
|
|
||||||
requester = await self.auth.get_user_by_req(request)
|
requester = await self.auth.get_user_by_req(request)
|
||||||
|
@ -350,7 +350,7 @@ class ClaimDehydratedDeviceServlet(RestServlet):
|
||||||
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
||||||
if (
|
if (
|
||||||
hs.config.worker.worker_app is None
|
hs.config.worker.worker_app is None
|
||||||
and not hs.config.auth.oauth_delegation_enabled
|
and not hs.config.experimental.msc3861.enabled
|
||||||
):
|
):
|
||||||
DeleteDevicesRestServlet(hs).register(http_server)
|
DeleteDevicesRestServlet(hs).register(http_server)
|
||||||
DevicesRestServlet(hs).register(http_server)
|
DevicesRestServlet(hs).register(http_server)
|
||||||
|
|
|
@ -386,7 +386,7 @@ class SigningKeyUploadServlet(RestServlet):
|
||||||
# time. Because there is no UIA in MSC3861, for now we throw an error if the
|
# time. Because there is no UIA in MSC3861, for now we throw an error if the
|
||||||
# user tries to reset the device signing key when MSC3861 is enabled, but allow
|
# user tries to reset the device signing key when MSC3861 is enabled, but allow
|
||||||
# first-time setup.
|
# first-time setup.
|
||||||
if self.hs.config.auth.oauth_delegation_enabled:
|
if self.hs.config.experimental.msc3861.enabled:
|
||||||
# There is no way to reset the device signing key with MSC3861
|
# There is no way to reset the device signing key with MSC3861
|
||||||
if is_cross_signing_setup:
|
if is_cross_signing_setup:
|
||||||
raise SynapseError(
|
raise SynapseError(
|
||||||
|
|
|
@ -633,7 +633,7 @@ class CasTicketServlet(RestServlet):
|
||||||
|
|
||||||
|
|
||||||
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
||||||
if hs.config.auth.oauth_delegation_enabled:
|
if hs.config.experimental.msc3861.enabled:
|
||||||
return
|
return
|
||||||
|
|
||||||
LoginRestServlet(hs).register(http_server)
|
LoginRestServlet(hs).register(http_server)
|
||||||
|
|
|
@ -80,7 +80,7 @@ class LogoutAllRestServlet(RestServlet):
|
||||||
|
|
||||||
|
|
||||||
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
||||||
if hs.config.auth.oauth_delegation_enabled:
|
if hs.config.experimental.msc3861.enabled:
|
||||||
return
|
return
|
||||||
|
|
||||||
LogoutRestServlet(hs).register(http_server)
|
LogoutRestServlet(hs).register(http_server)
|
||||||
|
|
|
@ -955,7 +955,7 @@ def _calculate_registration_flows(
|
||||||
|
|
||||||
|
|
||||||
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
||||||
if hs.config.auth.oauth_delegation_enabled:
|
if hs.config.experimental.msc3861.enabled:
|
||||||
return
|
return
|
||||||
|
|
||||||
if hs.config.worker.worker_app is None:
|
if hs.config.worker.worker_app is None:
|
||||||
|
|
|
@ -47,7 +47,7 @@ def build_synapse_client_resource_tree(hs: "HomeServer") -> Mapping[str, Resourc
|
||||||
}
|
}
|
||||||
|
|
||||||
# Expose the JWKS endpoint if OAuth2 delegation is enabled
|
# Expose the JWKS endpoint if OAuth2 delegation is enabled
|
||||||
if hs.config.auth.oauth_delegation_enabled:
|
if hs.config.experimental.msc3861.enabled:
|
||||||
from synapse.rest.synapse.client.jwks import JwksResource
|
from synapse.rest.synapse.client.jwks import JwksResource
|
||||||
|
|
||||||
resources["/_synapse/jwks"] = JwksResource(hs)
|
resources["/_synapse/jwks"] = JwksResource(hs)
|
||||||
|
|
|
@ -26,8 +26,6 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class JwksResource(DirectServeJsonResource):
|
class JwksResource(DirectServeJsonResource):
|
||||||
def __init__(self, hs: "HomeServer"):
|
def __init__(self, hs: "HomeServer"):
|
||||||
from authlib.jose.rfc7517 import Key
|
|
||||||
|
|
||||||
super().__init__(extract_context=True)
|
super().__init__(extract_context=True)
|
||||||
|
|
||||||
# Parameters that are allowed to be exposed in the public key.
|
# Parameters that are allowed to be exposed in the public key.
|
||||||
|
@ -53,10 +51,10 @@ class JwksResource(DirectServeJsonResource):
|
||||||
"ext",
|
"ext",
|
||||||
}
|
}
|
||||||
|
|
||||||
secret = hs.config.auth.oauth_delegation_client_secret
|
key = hs.config.experimental.msc3861.jwk
|
||||||
|
|
||||||
if isinstance(secret, Key):
|
if key is not None:
|
||||||
private_key = secret.as_dict()
|
private_key = key.as_dict()
|
||||||
public_key = {
|
public_key = {
|
||||||
k: v for k, v in private_key.items() if k in public_parameters
|
k: v for k, v in private_key.items() if k in public_parameters
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,14 +44,15 @@ class WellKnownBuilder:
|
||||||
"base_url": self._config.registration.default_identity_server
|
"base_url": self._config.registration.default_identity_server
|
||||||
}
|
}
|
||||||
|
|
||||||
if self._config.auth.oauth_delegation_enabled:
|
# We use the MSC3861 values as they are used by multiple MSCs
|
||||||
|
if self._config.experimental.msc3861.enabled:
|
||||||
result["org.matrix.msc2965.authentication"] = {
|
result["org.matrix.msc2965.authentication"] = {
|
||||||
"issuer": self._config.auth.oauth_delegation_issuer
|
"issuer": self._config.experimental.msc3861.issuer
|
||||||
}
|
}
|
||||||
if self._config.auth.oauth_delegation_account != "":
|
if self._config.experimental.msc3861.account_management_url is not None:
|
||||||
result["org.matrix.msc2965.authentication"][
|
result["org.matrix.msc2965.authentication"][
|
||||||
"account"
|
"account"
|
||||||
] = self._config.auth.oauth_delegation_account
|
] = self._config.experimental.msc3861.account_management_url
|
||||||
|
|
||||||
if self._config.server.extra_well_known_client_content:
|
if self._config.server.extra_well_known_client_content:
|
||||||
for (
|
for (
|
||||||
|
|
|
@ -428,10 +428,10 @@ class HomeServer(metaclass=abc.ABCMeta):
|
||||||
|
|
||||||
@cache_in_self
|
@cache_in_self
|
||||||
def get_auth(self) -> Auth:
|
def get_auth(self) -> Auth:
|
||||||
if self.config.auth.oauth_delegation_enabled:
|
if self.config.experimental.msc3861.enabled:
|
||||||
from synapse.api.auth.oauth_delegated import OAuthDelegatedAuth
|
from synapse.api.auth.msc3861_delegated import MSC3861DelegatedAuth
|
||||||
|
|
||||||
return OAuthDelegatedAuth(self)
|
return MSC3861DelegatedAuth(self)
|
||||||
return InternalAuth(self)
|
return InternalAuth(self)
|
||||||
|
|
||||||
@cache_in_self
|
@cache_in_self
|
||||||
|
|
|
@ -0,0 +1,202 @@
|
||||||
|
# Copyright 2023 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.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from typing import Any, Dict
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
from synapse.config import ConfigError
|
||||||
|
from synapse.module_api import ModuleApi
|
||||||
|
from synapse.types import JsonDict
|
||||||
|
|
||||||
|
from tests.server import get_clock
|
||||||
|
from tests.unittest import HomeserverTestCase, override_config, skip_unless
|
||||||
|
|
||||||
|
try:
|
||||||
|
import authlib # noqa: F401
|
||||||
|
|
||||||
|
HAS_AUTHLIB = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_AUTHLIB = False
|
||||||
|
|
||||||
|
|
||||||
|
# These are a few constants that are used as config parameters in the tests.
|
||||||
|
SERVER_NAME = "test"
|
||||||
|
ISSUER = "https://issuer/"
|
||||||
|
CLIENT_ID = "test-client-id"
|
||||||
|
CLIENT_SECRET = "test-client-secret"
|
||||||
|
BASE_URL = "https://synapse/"
|
||||||
|
|
||||||
|
|
||||||
|
class CustomAuthModule:
|
||||||
|
"""A module which registers a password auth provider."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_config(config: JsonDict) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __init__(self, config: None, api: ModuleApi):
|
||||||
|
api.register_password_auth_provider_callbacks(
|
||||||
|
auth_checkers={("m.login.password", ("password",)): Mock()},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@skip_unless(HAS_AUTHLIB, "requires authlib")
|
||||||
|
class MSC3861OAuthDelegation(HomeserverTestCase):
|
||||||
|
"""Test that the Homeserver fails to initialize if the config is invalid."""
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.reactor, self.clock = get_clock()
|
||||||
|
self._hs_args = {"clock": self.clock, "reactor": self.reactor}
|
||||||
|
|
||||||
|
def default_config(self) -> Dict[str, Any]:
|
||||||
|
config = super().default_config()
|
||||||
|
config["public_baseurl"] = BASE_URL
|
||||||
|
if "experimental_features" not in config:
|
||||||
|
config["experimental_features"] = {}
|
||||||
|
config["experimental_features"]["msc3861"] = {
|
||||||
|
"enabled": True,
|
||||||
|
"issuer": ISSUER,
|
||||||
|
"client_id": CLIENT_ID,
|
||||||
|
"client_auth_method": "client_secret_post",
|
||||||
|
"client_secret": CLIENT_SECRET,
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
|
||||||
|
def test_registration_cannot_be_enabled(self) -> None:
|
||||||
|
with self.assertRaises(ConfigError):
|
||||||
|
self.setup_test_homeserver()
|
||||||
|
|
||||||
|
@override_config(
|
||||||
|
{
|
||||||
|
"enable_registration": False,
|
||||||
|
"password_config": {
|
||||||
|
"enabled": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def test_password_config_cannot_be_enabled(self) -> None:
|
||||||
|
with self.assertRaises(ConfigError):
|
||||||
|
self.setup_test_homeserver()
|
||||||
|
|
||||||
|
@override_config(
|
||||||
|
{
|
||||||
|
"enable_registration": False,
|
||||||
|
"oidc_providers": [
|
||||||
|
{
|
||||||
|
"idp_id": "microsoft",
|
||||||
|
"idp_name": "Microsoft",
|
||||||
|
"issuer": "https://login.microsoftonline.com/<tenant id>/v2.0",
|
||||||
|
"client_id": "<client id>",
|
||||||
|
"client_secret": "<client secret>",
|
||||||
|
"scopes": ["openid", "profile"],
|
||||||
|
"authorization_endpoint": "https://login.microsoftonline.com/<tenant id>/oauth2/v2.0/authorize",
|
||||||
|
"token_endpoint": "https://login.microsoftonline.com/<tenant id>/oauth2/v2.0/token",
|
||||||
|
"userinfo_endpoint": "https://graph.microsoft.com/oidc/userinfo",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def test_oidc_sso_cannot_be_enabled(self) -> None:
|
||||||
|
with self.assertRaises(ConfigError):
|
||||||
|
self.setup_test_homeserver()
|
||||||
|
|
||||||
|
@override_config(
|
||||||
|
{
|
||||||
|
"enable_registration": False,
|
||||||
|
"cas_config": {
|
||||||
|
"enabled": True,
|
||||||
|
"server_url": "https://cas-server.com",
|
||||||
|
"displayname_attribute": "name",
|
||||||
|
"required_attributes": {"userGroup": "staff", "department": "None"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def test_cas_sso_cannot_be_enabled(self) -> None:
|
||||||
|
with self.assertRaises(ConfigError):
|
||||||
|
self.setup_test_homeserver()
|
||||||
|
|
||||||
|
@override_config(
|
||||||
|
{
|
||||||
|
"enable_registration": False,
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"module": f"{__name__}.{CustomAuthModule.__qualname__}",
|
||||||
|
"config": {},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def test_auth_providers_cannot_be_enabled(self) -> None:
|
||||||
|
with self.assertRaises(ConfigError):
|
||||||
|
self.setup_test_homeserver()
|
||||||
|
|
||||||
|
@override_config(
|
||||||
|
{
|
||||||
|
"enable_registration": False,
|
||||||
|
"jwt_config": {
|
||||||
|
"enabled": True,
|
||||||
|
"secret": "my-secret-token",
|
||||||
|
"algorithm": "HS256",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def test_jwt_auth_cannot_be_enabled(self) -> None:
|
||||||
|
with self.assertRaises(ConfigError):
|
||||||
|
self.setup_test_homeserver()
|
||||||
|
|
||||||
|
@override_config(
|
||||||
|
{
|
||||||
|
"enable_registration": False,
|
||||||
|
"experimental_features": {
|
||||||
|
"msc3882_enabled": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def test_msc3882_auth_cannot_be_enabled(self) -> None:
|
||||||
|
with self.assertRaises(ConfigError):
|
||||||
|
self.setup_test_homeserver()
|
||||||
|
|
||||||
|
@override_config(
|
||||||
|
{
|
||||||
|
"enable_registration": False,
|
||||||
|
"recaptcha_public_key": "test",
|
||||||
|
"recaptcha_private_key": "test",
|
||||||
|
"enable_registration_captcha": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def test_captcha_cannot_be_enabled(self) -> None:
|
||||||
|
with self.assertRaises(ConfigError):
|
||||||
|
self.setup_test_homeserver()
|
||||||
|
|
||||||
|
@override_config(
|
||||||
|
{
|
||||||
|
"enable_registration": False,
|
||||||
|
"refresh_token_lifetime": "24h",
|
||||||
|
"refreshable_access_token_lifetime": "10m",
|
||||||
|
"nonrefreshable_access_token_lifetime": "24h",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def test_refreshable_tokens_cannot_be_enabled(self) -> None:
|
||||||
|
with self.assertRaises(ConfigError):
|
||||||
|
self.setup_test_homeserver()
|
||||||
|
|
||||||
|
@override_config(
|
||||||
|
{
|
||||||
|
"enable_registration": False,
|
||||||
|
"session_lifetime": "24h",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def test_session_lifetime_cannot_be_set(self) -> None:
|
||||||
|
with self.assertRaises(ConfigError):
|
||||||
|
self.setup_test_homeserver()
|
|
@ -109,12 +109,15 @@ class MSC3861OAuthDelegation(HomeserverTestCase):
|
||||||
def default_config(self) -> Dict[str, Any]:
|
def default_config(self) -> Dict[str, Any]:
|
||||||
config = super().default_config()
|
config = super().default_config()
|
||||||
config["public_baseurl"] = BASE_URL
|
config["public_baseurl"] = BASE_URL
|
||||||
config["oauth_delegation"] = {
|
config["disable_registration"] = True
|
||||||
"enabled": True,
|
config["experimental_features"] = {
|
||||||
"issuer": ISSUER,
|
"msc3861": {
|
||||||
"client_id": CLIENT_ID,
|
"enabled": True,
|
||||||
"client_auth_method": "client_secret_post",
|
"issuer": ISSUER,
|
||||||
"client_secret": CLIENT_SECRET,
|
"client_id": CLIENT_ID,
|
||||||
|
"client_auth_method": "client_secret_post",
|
||||||
|
"client_secret": CLIENT_SECRET,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
|
@ -108,14 +108,17 @@ class WellKnownTests(unittest.HomeserverTestCase):
|
||||||
@unittest.override_config(
|
@unittest.override_config(
|
||||||
{
|
{
|
||||||
"public_baseurl": "https://homeserver", # this is only required so that client well known is served
|
"public_baseurl": "https://homeserver", # this is only required so that client well known is served
|
||||||
"oauth_delegation": {
|
"experimental_features": {
|
||||||
"enabled": True,
|
"msc3861": {
|
||||||
"issuer": "https://issuer",
|
"enabled": True,
|
||||||
"account": "https://my-account.issuer",
|
"issuer": "https://issuer",
|
||||||
"client_id": "id",
|
"account_management_url": "https://my-account.issuer",
|
||||||
"client_auth_method": "client_secret_post",
|
"client_id": "id",
|
||||||
"client_secret": "secret",
|
"client_auth_method": "client_secret_post",
|
||||||
|
"client_secret": "secret",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
"disable_registration": True,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
def test_client_well_known_msc3861_oauth_delegation(self) -> None:
|
def test_client_well_known_msc3861_oauth_delegation(self) -> None:
|
||||||
|
|
Loading…
Reference in New Issue