impl MSC2965: self-advertise as OIDC authentication provider

MSC2965 proposes to let the homeserver advertise its current OIDC authentication
issuer. These changes let conduwuit advertise itself as the issuer when
[global.auth.enable_oidc_login] is set. It also advertises its account management
endpoint if [global.auth.enable_oidc_account_management] is set.

None of these endpoints are implemented. This commit only implements the bare
advertisement, as requested by the MSC.
This commit is contained in:
lafleur 2025-04-01 09:29:25 +02:00 committed by Jade Ellis
parent dcbc4b54c5
commit f8c7b2ae3a
No known key found for this signature in database
GPG key ID: 8705A2A3EBF77BD2
9 changed files with 157 additions and 3 deletions

View file

@ -368,6 +368,7 @@ features = [
"unstable-msc2666",
"unstable-msc2867",
"unstable-msc2870",
"unstable-msc2965",
"unstable-msc3026",
"unstable-msc3061",
"unstable-msc3245",

View file

@ -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

View file

@ -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::*;

View file

@ -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<crate::State>,
_body: Ruma<get_authorization_server_metadata::msc2965::Request>,
) -> Result<RumaResponse<Response>> {
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)))
}

View file

@ -0,0 +1,3 @@
mod discovery;
pub(crate) use self::discovery::get_auth_metadata;

View file

@ -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")
)
)
)
)
})
}

View file

@ -117,6 +117,9 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
.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)

View file

@ -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(())
}

View file

@ -103,7 +103,9 @@ pub struct Config {
#[serde(default)]
pub tls: TlsConfig,
/// The UNIX socket continuwuity will listen on.
pub auth: Option<AuthConfig>,
/// 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")]