diff --git a/Cargo.toml b/Cargo.toml index 1abff107..318c89ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -368,6 +368,7 @@ features = [ "unstable-msc2666", "unstable-msc2867", "unstable-msc2870", + "unstable-msc2965", "unstable-msc3026", "unstable-msc3061", "unstable-msc3245", diff --git a/conduwuit-example.toml b/conduwuit-example.toml index 6934e67c..69eaebc1 100644 --- a/conduwuit-example.toml +++ b/conduwuit-example.toml @@ -1624,6 +1624,20 @@ # #dual_protocol = false +[global.auth] + +# Use this homeserver as the OIDC authentication reference. +# Note that the legacy Matrix authentication still will work. +# Unset by default. +# +#enable_oidc_login = + +# The URL where the user is able to access the account management +# capabilities of the homeserver. Only used if `enable_oidc_login` is set. +# Unset by default. +# +#enable_oidc_account_management = + [global.well_known] # The server URL that the client well-known file will serve. This should diff --git a/src/api/client/mod.rs b/src/api/client/mod.rs index be54e65f..6e1869be 100644 --- a/src/api/client/mod.rs +++ b/src/api/client/mod.rs @@ -2,6 +2,7 @@ pub(super) mod account; pub(super) mod account_data; pub(super) mod alias; pub(super) mod appservice; +pub(super) mod oidc; pub(super) mod backup; pub(super) mod capabilities; pub(super) mod context; @@ -44,6 +45,7 @@ pub(super) use account::*; pub(super) use account_data::*; pub(super) use alias::*; pub(super) use appservice::*; +pub(super) use oidc::*; pub(super) use backup::*; pub(super) use capabilities::*; pub(super) use context::*; diff --git a/src/api/client/oidc/discovery.rs b/src/api/client/oidc/discovery.rs new file mode 100644 index 00000000..2d9835b4 --- /dev/null +++ b/src/api/client/oidc/discovery.rs @@ -0,0 +1,89 @@ +/// Manual implementation of [MSC2965]'s OIDC server discovery. +/// +/// [MSC2965]: https://github.com/matrix-org/matrix-spec-proposals/pull/2965 +use axum::extract::State; +use conduwuit::Result; +use ruma::serde::Raw; +use ruma::api::client::{ + error::{ + Error as ClientError, + ErrorKind as ClientErrorKind, + ErrorBody as ClientErrorBody, + }, + discovery::get_authorization_server_metadata::{ + self, + msc2965::{ + AccountManagementAction, + AuthorizationServerMetadata, + CodeChallengeMethod, + GrantType, + Prompt, + ResponseMode, + ResponseType, + Response, + }, + }, +}; +use crate::{conduwuit::Error, Ruma, RumaResponse}; + +/// # `GET /_matrix/client/unstable/org.matrix.msc2965/auth_metadata` +/// +/// If `globals.auth.enable_oidc_login` is set, advertise this homeserver's OAuth2 endpoints. +/// Otherwise, MSC2965 requires that the homeserver responds with 404/M_UNRECOGNIZED. +pub(crate) async fn get_auth_metadata( + State(services): State, + _body: Ruma, +) -> Result> { + let unrecognized_error = Err(Error::Ruma( + ClientError::new( + http::StatusCode::NOT_FOUND, + ClientErrorBody::Standard { + kind: ClientErrorKind::Unrecognized, + message: "This homeserver doesn't do OIDC authentication.".to_string() + } + ) + )); + let Some(ref auth) = services.server.config.auth else { + return unrecognized_error; + }; + if ! auth.enable_oidc_login { + return unrecognized_error; + }; + // Advertise this homeserver's access URL as the issuer URL. + // Unwrap all Url::parse() calls because the issuer URL is validated at startup. + let issuer = services.server.config.well_known.client.as_ref().unwrap(); + let account_management_uri = auth + .enable_oidc_account_management + .then_some(issuer.join("/_matrix/client/unstable/org.matrix.msc2964/account").unwrap()); + + let metadata = AuthorizationServerMetadata { + issuer: issuer.clone(), + authorization_endpoint: + issuer.join("/_matrix/client/unstable/org.matrix.msc2964/authorize").unwrap(), + device_authorization_endpoint: + Some(issuer.join("/_matrix/client/unstable/org.matrix.msc2964/device").unwrap()), + token_endpoint: + issuer.join("/_matrix/client/unstable/org.matrix.msc2964/token").unwrap(), + registration_endpoint: + Some(issuer.join("/_matrix/client/unstable/org.matrix.msc2964/device/register").unwrap()), + revocation_endpoint: + issuer.join("_matrix/client/unstable/org.matrix.msc2964/revoke").unwrap(), + response_types_supported: [ResponseType::Code].into(), + grant_types_supported: [GrantType::AuthorizationCode, GrantType::RefreshToken].into(), + response_modes_supported: [ResponseMode::Fragment, ResponseMode::Query].into(), + code_challenge_methods_supported: [CodeChallengeMethod::S256].into(), + account_management_uri, + account_management_actions_supported: [ + AccountManagementAction::Profile, + AccountManagementAction::SessionView, + AccountManagementAction::SessionEnd, + ].into(), + prompt_values_supported: match services.server.config.allow_registration { + | true => vec![Prompt::Create], + | false => vec![] + } + }; + let metadata = Raw::new(&metadata).expect("authorization server metadata should serialize"); + + Ok(RumaResponse(Response::new(metadata))) +} diff --git a/src/api/client/oidc/mod.rs b/src/api/client/oidc/mod.rs new file mode 100644 index 00000000..c0cab565 --- /dev/null +++ b/src/api/client/oidc/mod.rs @@ -0,0 +1,3 @@ +mod discovery; + +pub(crate) use self::discovery::get_auth_metadata; diff --git a/src/api/client/well_known.rs b/src/api/client/well_known.rs index 35b7fc1e..698eb470 100644 --- a/src/api/client/well_known.rs +++ b/src/api/client/well_known.rs @@ -2,7 +2,12 @@ use axum::{Json, extract::State, response::IntoResponse}; use conduwuit::{Error, Result}; use ruma::api::client::{ discovery::{ - discover_homeserver::{self, HomeserverInfo, SlidingSyncProxyInfo}, + discover_homeserver::{ + self, + HomeserverInfo, + SlidingSyncProxyInfo, + AuthenticationServerInfo, + }, discover_support::{self, Contact}, }, error::ErrorKind, @@ -25,8 +30,18 @@ pub(crate) async fn well_known_client( Ok(discover_homeserver::Response { homeserver: HomeserverInfo { base_url: client_url.clone() }, identity_server: None, - sliding_sync_proxy: Some(SlidingSyncProxyInfo { url: client_url }), + sliding_sync_proxy: Some(SlidingSyncProxyInfo { url: client_url.clone() }), tile_server: None, + authentication: services.config.auth.as_ref().and_then(|auth| + auth.enable_oidc_login.then_some( + AuthenticationServerInfo::new( + client_url.clone(), + auth.enable_oidc_account_management.then_some( + format!("{client_url}/account") + ) + ) + ) + ) }) } diff --git a/src/api/router.rs b/src/api/router.rs index 5416e9e9..e7393837 100644 --- a/src/api/router.rs +++ b/src/api/router.rs @@ -117,6 +117,9 @@ pub fn build(router: Router, server: &Server) -> Router { .ruma_route(&client::get_protocols_route) .route("/_matrix/client/unstable/thirdparty/protocols", get(client::get_protocols_route_unstable)) + // MSC2965 is still not stabilized. See https://github.com/sandhose/matrix-spec-proposals/blob/msc/sandhose/oidc-discovery/proposals/2965-auth-metadata.md#unstable-prefix + .route("/_matrix/client/unstable/org.matrix.msc2965/auth_metadata", + get(client::get_auth_metadata)) .ruma_route(&client::send_message_event_route) .ruma_route(&client::send_state_event_for_key_route) .ruma_route(&client::get_state_events_route) diff --git a/src/core/config/check.rs b/src/core/config/check.rs index ded9533d..4d4d4a5f 100644 --- a/src/core/config/check.rs +++ b/src/core/config/check.rs @@ -271,6 +271,17 @@ pub fn check(config: &Config) -> Result { )); } + if let Some(auth) = &config.auth { + if auth.enable_oidc_login { + if config.well_known.client.is_none() { + return Err!(Config( + "auth.enable_oidc_login", + "Oidc authentication is enabled but the well-known client is not set." + )) + } + } + } + Ok(()) } diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index 66ed0b2e..351f3165 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -103,7 +103,9 @@ pub struct Config { #[serde(default)] pub tls: TlsConfig, - /// The UNIX socket continuwuity will listen on. + pub auth: Option, + + /// The UNIX socket conduwuit will listen on. /// /// continuwuity cannot listen on both an IP address and a UNIX socket. If /// listening on a UNIX socket, you MUST remove/comment the `address` key. @@ -1880,6 +1882,20 @@ pub struct TlsConfig { pub dual_protocol: bool, } +#[derive(Clone, Debug, Deserialize, Default)] +#[config_example_generator(filename = "conduwuit-example.toml", section = "global.auth")] +pub struct AuthConfig { + /// Use this homeserver as the OIDC authentication reference. + /// Note that the legacy Matrix authentication still will work. + /// Unset by default. + pub enable_oidc_login: bool, + + /// The URL where the user is able to access the account management + /// capabilities of the homeserver. Only used if `enable_oidc_login` is set. + /// Unset by default. + pub enable_oidc_account_management: bool, +} + #[allow(rustdoc::broken_intra_doc_links, rustdoc::bare_urls)] #[derive(Clone, Debug, Deserialize, Default)] #[config_example_generator(filename = "conduwuit-example.toml", section = "global.well_known")]