diff --git a/conduwuit-example.toml b/conduwuit-example.toml index a1cad85e0..273c2f777 100644 --- a/conduwuit-example.toml +++ b/conduwuit-example.toml @@ -1999,3 +1999,16 @@ # provide an email address. # #require_email_for_token_registration = false + +#[global.oauth] + +# The compatibility mode to use for OAuth. +# +# - "disabled": OAuth will be unavailable. Users will only be able to log +# in using legacy authentication. +# - "hybrid": OAuth and legacy authentication will both be available. Some +# clients may only use one or the other. +# - "exclusive": Only OAuth will be available. Clients which require +# legacy authentication will be unable to log in. +# +#compatibility_mode = "hybrid" diff --git a/src/api/client/oauth/mod.rs b/src/api/client/oauth/mod.rs index 3bf6e92d2..865332950 100644 --- a/src/api/client/oauth/mod.rs +++ b/src/api/client/oauth/mod.rs @@ -1,9 +1,12 @@ use axum::{ Json, Router, - extract::State, + extract::{Request, State}, + middleware::{self, Next}, + response::{IntoResponse, Response}, routing::method_routing::{get, post}, }; use const_str::concat; +use http::StatusCode; use serde_json::json; pub(crate) use server_metadata::*; @@ -19,14 +22,28 @@ const TOKEN_PATH: &str = "grant/token"; const ACCOUNT_MANAGEMENT_PATH: &str = concat!(conduwuit_core::ROUTE_PREFIX, "/account/deeplink"); -pub(crate) fn router() -> Router { - Router::new().nest(BASE_PATH, oauth_router()) - // TODO(unspecced): used by old versions of the matrix-js-sdk - .route("/.well-known/openid-configuration", get( - async |State(services): State| { - Json(authorization_server_metadata(&services).await) - } - )) +pub(crate) fn router(state: crate::State) -> Router { + Router::new() + .nest(BASE_PATH, oauth_router()) + .route( + "/.well-known/openid-configuration", + get( + // TODO(unspecced): used by old versions of the matrix-js-sdk + async |State(services): State| { + Json(authorization_server_metadata(&services).await) + }, + ), + ) + .layer(middleware::from_fn_with_state( + state, + async |State(state): State, request: Request, next: Next| -> Response { + if state.config.oauth.compatibility_mode.oauth_available() { + next.run(request).await + } else { + (StatusCode::NOT_FOUND, "OAuth is unavailable on this server").into_response() + } + }, + )) } fn oauth_router() -> Router { diff --git a/src/api/client/oauth/server_metadata.rs b/src/api/client/oauth/server_metadata.rs index ed310dc31..60d287960 100644 --- a/src/api/client/oauth/server_metadata.rs +++ b/src/api/client/oauth/server_metadata.rs @@ -1,5 +1,5 @@ use axum::extract::State; -use conduwuit::Result; +use conduwuit::{Err, Result}; use ruma::{ api::client::discovery::get_authorization_server_metadata::{ self, v1::AccountManagementAction, @@ -21,6 +21,10 @@ pub(crate) async fn get_authorization_server_metadata_route( State(services): State, _body: Ruma, ) -> Result { + if !services.config.oauth.compatibility_mode.oauth_available() { + return Err!(Request(Unrecognized("OAuth is unavailable on this server"))); + } + let metadata = Raw::new(&authorization_server_metadata(&services).await).unwrap(); Ok(get_authorization_server_metadata::v1::Response::new(metadata.cast_unchecked())) diff --git a/src/api/client/session.rs b/src/api/client/session.rs index 846a38132..b07913de9 100644 --- a/src/api/client/session.rs +++ b/src/api/client/session.rs @@ -43,6 +43,12 @@ pub(crate) async fn get_login_types_route( ClientIp(client): ClientIp, _body: Ruma, ) -> Result { + if !services.config.oauth.compatibility_mode.uiaa_available() { + return Err!(Request(Unrecognized( + "User-interactive authentication is not available on this server." + ))); + } + Ok(get_login_types::v3::Response::new(vec![ get_login_types::v3::LoginType::Password(PasswordLoginType::default()), get_login_types::v3::LoginType::ApplicationService(ApplicationServiceLoginType::default()), @@ -118,10 +124,15 @@ pub(crate) async fn login_route( ClientIp(client): ClientIp, body: Ruma, ) -> Result { + if !services.config.oauth.compatibility_mode.uiaa_available() { + return Err!(Request(Unrecognized( + "User-interactive authentication is not available on this server." + ))); + } + let emergency_mode_enabled = services.config.emergency_password.is_some(); // Validate login method - // TODO: Other login methods let user_id = match &body.login_info { #[allow(deprecated)] | login::v3::LoginInfo::Password(login::v3::Password { diff --git a/src/api/router.rs b/src/api/router.rs index 348be3994..23ba8dd8f 100644 --- a/src/api/router.rs +++ b/src/api/router.rs @@ -10,7 +10,7 @@ response::{IntoResponse, Redirect}, routing::{any, get, post}, }; -use conduwuit::{Server, err}; +use conduwuit::err; pub(super) use conduwuit_service::state::State; use http::{Uri, uri}; @@ -18,8 +18,8 @@ pub(super) use self::{args::Args as Ruma, response::RumaResponse}; use crate::{admin, client, server}; -pub fn build(router: Router, server: &Server) -> Router { - let config = &server.config; +pub fn build(router: Router, state: State) -> Router { + let config = &state.server.config; let mut router = router .ruma_route(&client::appservice_ping) .ruma_route(&client::get_supported_versions_route) @@ -186,7 +186,7 @@ pub fn build(router: Router, server: &Server) -> Router { .ruma_route(&client::get_rtc_transports) .ruma_route(&client::room_initial_sync_route) .ruma_route(&client::get_authorization_server_metadata_route) - .merge(client::oauth::router()) + .merge(client::oauth::router(state)) .route("/_conduwuit/server_version", get(client::conduwuit_server_version)) .route("/_continuwuity/server_version", get(client::conduwuit_server_version)) .ruma_route(&admin::rooms::ban::ban_room) diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index eb28d4dda..f2324ea17 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -673,6 +673,10 @@ pub struct Config { #[serde(default)] pub registration_terms: HashMap>, + /// display: nested + #[serde(default)] + pub oauth: OauthConfig, + /// Controls whether encrypted rooms and events are allowed. #[serde(default = "true_fn")] pub allow_encryption: bool, @@ -2406,6 +2410,43 @@ pub struct TermsDocument { pub url: String, } +#[derive(Clone, Debug, Default, Deserialize)] +#[config_example_generator( + filename = "conduwuit-example.toml", + section = "global.oauth", + optional = "true" +)] +pub struct OauthConfig { + /// The compatibility mode to use for OAuth. + /// + /// - "disabled": OAuth will be unavailable. Users will only be able to log + /// in using legacy authentication. + /// - "hybrid": OAuth and legacy authentication will both be available. Some + /// clients may only use one or the other. + /// - "exclusive": Only OAuth will be available. Clients which require + /// legacy authentication will be unable to log in. + /// + /// default: "hybrid" + pub compatibility_mode: OAuthMode, +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum OAuthMode { + Disabled, + #[default] + Hybrid, + Exclusive, +} + +impl OAuthMode { + #[must_use] + pub fn uiaa_available(&self) -> bool { matches!(self, Self::Disabled | Self::Hybrid) } + + #[must_use] + pub fn oauth_available(&self) -> bool { matches!(self, Self::Hybrid | Self::Exclusive) } +} + const DEPRECATED_KEYS: &[&str] = &[ "cache_capacity", "conduit_cache_capacity_modifier", diff --git a/src/core/error/response.rs b/src/core/error/response.rs index 563a57ac0..e4a0ce59f 100644 --- a/src/core/error/response.rs +++ b/src/core/error/response.rs @@ -73,11 +73,8 @@ pub(super) fn bad_request_code(kind: &ErrorKind) -> StatusCode { // 413 | TooLarge => StatusCode::PAYLOAD_TOO_LARGE, - // 405 - | Unrecognized => StatusCode::METHOD_NOT_ALLOWED, - // 404 - | NotFound => StatusCode::NOT_FOUND, + | Unrecognized | NotFound => StatusCode::NOT_FOUND, // 403 | GuestAccessForbidden diff --git a/src/router/request.rs b/src/router/request.rs index 0d1b7946c..2f7151b1c 100644 --- a/src/router/request.rs +++ b/src/router/request.rs @@ -112,7 +112,9 @@ fn handle_result(method: &Method, uri: &Uri, result: Response) -> Result) -> (Router, Guard) { let router = Router::::new(); let (state, guard) = state::create(services.clone()); - let router = conduwuit_api::router::build(router, &services.server) + let router = conduwuit_api::router::build(router, state) .merge(conduwuit_web::build(services)) .fallback(not_found) .with_state(state); diff --git a/src/service/oauth/mod.rs b/src/service/oauth/mod.rs index 28cdfea77..d495e03ad 100644 --- a/src/service/oauth/mod.rs +++ b/src/service/oauth/mod.rs @@ -16,7 +16,7 @@ use url::Url; use crate::{ - Dep, config, + Dep, oauth::{ client_metadata::{ApplicationType, ClientMetadata, ResponseType}, grant::{ @@ -44,7 +44,6 @@ struct Data { } struct Services { - config: Dep, users: Dep, } @@ -111,7 +110,6 @@ impl crate::Service for Service { fn build(args: crate::Args<'_>) -> Result> { Ok(Arc::new(Self { services: Services { - config: args.depend::("config"), users: args.depend::("users"), }, db: Data { diff --git a/src/service/uiaa/mod.rs b/src/service/uiaa/mod.rs index b8641aa8a..8ee69797a 100644 --- a/src/service/uiaa/mod.rs +++ b/src/service/uiaa/mod.rs @@ -304,6 +304,20 @@ async fn create_session( UiaaSessionMetadata::Legacy { identity: Identity::default() } }; + // Legacy sessions aren't available if OAuth is required + if matches!(&session_metadata, UiaaSessionMetadata::Legacy { .. }) + && !self + .services + .config + .oauth + .compatibility_mode + .uiaa_available() + { + return Err!(Request(Unrecognized( + "User-interactive authentication is unavailable on this server" + ))); + } + uiaa_sessions.insert(session_id, UiaaSession { session_metadata, info: info.clone() }); Ok(info) diff --git a/src/web/pages/account/device.rs b/src/web/pages/account/device.rs index 112b6917c..49f271786 100644 --- a/src/web/pages/account/device.rs +++ b/src/web/pages/account/device.rs @@ -94,7 +94,7 @@ async fn route_remove_device( Expect(Path(query)): Expect>, PostForm(form): PostForm<()>, ) -> Result { - let user_id = user.expect(LoginTarget::RemoveDevice(query.clone()))?; + let user_id = user.expect_recent(LoginTarget::RemoveDevice(query.clone()))?; let Ok(device) = services .users