From 54b347b8554827d2692e044b8f9d290d67c6621e Mon Sep 17 00:00:00 2001 From: Jason Volk Date: Wed, 10 Sep 2025 10:08:11 +0000 Subject: [PATCH] Abstract and dedup the general UIAA pattern into api::router. Signed-off-by: Jason Volk --- src/api/client/account.rs | 115 +++--------------------------------- src/api/client/device.rs | 81 +++---------------------- src/api/router.rs | 4 +- src/api/router/auth.rs | 2 + src/api/router/auth/uiaa.rs | 81 +++++++++++++++++++++++++ 5 files changed, 103 insertions(+), 180 deletions(-) create mode 100644 src/api/router/auth/uiaa.rs diff --git a/src/api/client/account.rs b/src/api/client/account.rs index 05e096ca..90b020f4 100644 --- a/src/api/client/account.rs +++ b/src/api/client/account.rs @@ -1,18 +1,13 @@ use axum::extract::State; use axum_client_ip::InsecureClientIp; use futures::{FutureExt, StreamExt}; -use ruma::api::client::{ - account::{ - ThirdPartyIdRemovalStatus, change_password, deactivate, get_3pids, - request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn, - whoami, - }, - uiaa::{AuthData, AuthFlow, AuthType, Jwt, UiaaInfo}, +use ruma::api::client::account::{ + ThirdPartyIdRemovalStatus, change_password, deactivate, get_3pids, + request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn, whoami, }; -use tuwunel_core::{Err, Error, Result, err, info, utils, utils::ReadyExt}; +use tuwunel_core::{Err, Result, info, utils::ReadyExt}; -use super::{SESSION_ID_LENGTH, session::jwt::validate_user}; -use crate::Ruma; +use crate::{Ruma, router::auth_uiaa}; /// # `POST /_matrix/client/r0/account/password` /// @@ -37,45 +32,7 @@ pub(crate) async fn change_password_route( InsecureClientIp(client): InsecureClientIp, body: Ruma, ) -> Result { - // Authentication for this endpoint was made optional, but we need - // authentication currently - let sender_user = body - .sender_user - .as_ref() - .ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?; - - let mut uiaainfo = UiaaInfo { - flows: vec![AuthFlow { stages: vec![AuthType::Password] }], - ..Default::default() - }; - - match &body.auth { - | Some(auth) => { - let (worked, uiaainfo) = services - .uiaa - .try_auth(sender_user, body.sender_device(), auth, &uiaainfo) - .await?; - - if !worked { - return Err(Error::Uiaa(uiaainfo)); - } - - // Success! - }, - | _ => match body.json_body { - | Some(ref json) => { - uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH)); - services - .uiaa - .create(sender_user, body.sender_device(), &uiaainfo, json); - - return Err(Error::Uiaa(uiaainfo)); - }, - | _ => { - return Err!(Request(NotJson("JSON body is not valid"))); - }, - }, - } + let ref sender_user = auth_uiaa(&services, &body).await?; services .users @@ -87,7 +44,7 @@ pub(crate) async fn change_password_route( services .users .all_device_ids(sender_user) - .ready_filter(|id| *id != body.sender_device()) + .ready_filter(|&id| Some(id) != body.sender_device.as_deref()) .for_each(|id| services.users.remove_device(sender_user, id)) .await; } @@ -140,69 +97,15 @@ pub(crate) async fn deactivate_route( InsecureClientIp(client): InsecureClientIp, body: Ruma, ) -> Result { - let pass_flow = AuthFlow { stages: vec![AuthType::Password] }; - let jwt_flow = AuthFlow { stages: vec![AuthType::Jwt] }; - let mut uiaainfo = UiaaInfo { - flows: [pass_flow, jwt_flow].into(), - ..Default::default() - }; - - let sender_user = match &body.auth { - | Some(AuthData::Jwt(Jwt { token, .. })) => { - let sender_user = validate_user(&services, token)?; - if !services.users.exists(&sender_user).await { - return Err!(Request(NotFound("User {sender_user} is not registered."))); - } - - // Success! - sender_user - }, - | Some(auth) => { - let sender_user = body - .sender_user - .as_deref() - .ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?; - - let (worked, uiaainfo) = services - .uiaa - .try_auth(sender_user, body.sender_device(), auth, &uiaainfo) - .await?; - - if !worked { - return Err(Error::Uiaa(uiaainfo)); - } - - // Success! - sender_user.to_owned() - }, - | _ => match body.json_body { - | Some(ref json) => { - let sender_user = body - .sender_user - .as_ref() - .ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?; - - uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH)); - services - .uiaa - .create(sender_user, body.sender_device(), &uiaainfo, json); - - return Err(Error::Uiaa(uiaainfo)); - }, - | _ => { - return Err!(Request(NotJson("JSON body is not valid"))); - }, - }, - }; + let ref sender_user = auth_uiaa(&services, &body).await?; services .deactivate - .full_deactivate(&sender_user) + .full_deactivate(sender_user) .boxed() .await?; info!("User {sender_user} deactivated their account."); - if services.server.config.admin_room_notices { services .admin diff --git a/src/api/client/device.rs b/src/api/client/device.rs index 443597a8..581870f5 100644 --- a/src/api/client/device.rs +++ b/src/api/client/device.rs @@ -3,16 +3,13 @@ use axum_client_ip::InsecureClientIp; use futures::StreamExt; use ruma::{ MilliSecondsSinceUnixEpoch, OwnedDeviceId, - api::client::{ - device::{self, delete_device, delete_devices, get_device, get_devices, update_device}, - error::ErrorKind, - uiaa::{AuthFlow, AuthType, UiaaInfo}, + api::client::device::{ + self, delete_device, delete_devices, get_device, get_devices, update_device, }, }; -use tuwunel_core::{Err, Error, Result, debug, err, utils}; +use tuwunel_core::{Err, Result, debug, err, utils}; -use super::SESSION_ID_LENGTH; -use crate::{Ruma, client::DEVICE_ID_LENGTH}; +use crate::{Ruma, client::DEVICE_ID_LENGTH, router::auth_uiaa}; /// # `GET /_matrix/client/r0/devices` /// @@ -126,10 +123,10 @@ pub(crate) async fn delete_device_route( State(services): State, body: Ruma, ) -> Result { - let (sender_user, sender_device) = body.sender(); let appservice = body.appservice_info.as_ref(); if appservice.is_some_and(|appservice| appservice.registration.device_management) { + let sender_user = body.sender_user(); debug!( "Skipping UIAA for {sender_user} as this is from an appservice and MSC4190 is \ enabled" @@ -142,38 +139,7 @@ pub(crate) async fn delete_device_route( return Ok(delete_device::v3::Response {}); } - // UIAA - let mut uiaainfo = UiaaInfo { - flows: vec![AuthFlow { stages: vec![AuthType::Password] }], - ..Default::default() - }; - - match &body.auth { - | Some(auth) => { - let (worked, uiaainfo) = services - .uiaa - .try_auth(sender_user, sender_device, auth, &uiaainfo) - .await?; - - if !worked { - return Err!(Uiaa(uiaainfo)); - } - // Success! - }, - | _ => match body.json_body { - | Some(ref json) => { - uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH)); - services - .uiaa - .create(sender_user, sender_device, &uiaainfo, json); - - return Err!(Uiaa(uiaainfo)); - }, - | _ => { - return Err!(Request(NotJson("Not json."))); - }, - }, - } + let ref sender_user = auth_uiaa(&services, &body).await?; services .users @@ -200,10 +166,10 @@ pub(crate) async fn delete_devices_route( State(services): State, body: Ruma, ) -> Result { - let (sender_user, sender_device) = body.sender(); let appservice = body.appservice_info.as_ref(); if appservice.is_some_and(|appservice| appservice.registration.device_management) { + let sender_user = body.sender_user(); debug!( "Skipping UIAA for {sender_user} as this is from an appservice and MSC4190 is \ enabled" @@ -218,38 +184,7 @@ pub(crate) async fn delete_devices_route( return Ok(delete_devices::v3::Response {}); } - // UIAA - let mut uiaainfo = UiaaInfo { - flows: vec![AuthFlow { stages: vec![AuthType::Password] }], - ..Default::default() - }; - - match &body.auth { - | Some(auth) => { - let (worked, uiaainfo) = services - .uiaa - .try_auth(sender_user, sender_device, auth, &uiaainfo) - .await?; - - if !worked { - return Err(Error::Uiaa(uiaainfo)); - } - // Success! - }, - | _ => match body.json_body { - | Some(ref json) => { - uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH)); - services - .uiaa - .create(sender_user, sender_device, &uiaainfo, json); - - return Err(Error::Uiaa(uiaainfo)); - }, - | _ => { - return Err(Error::BadRequest(ErrorKind::NotJson, "Not json.")); - }, - }, - } + let ref sender_user = auth_uiaa(&services, &body).await?; for device_id in &body.devices { services diff --git a/src/api/router.rs b/src/api/router.rs index 79dd4d00..ec88ba7a 100644 --- a/src/api/router.rs +++ b/src/api/router.rs @@ -16,7 +16,9 @@ use http::{Uri, uri}; use tuwunel_core::{Server, err}; use self::handler::RouterExt; -pub(super) use self::{args::Args as Ruma, response::RumaResponse, state::State}; +pub(super) use self::{ + args::Args as Ruma, auth::auth_uiaa, response::RumaResponse, state::State, +}; use crate::{client, server}; pub fn build(router: Router, server: &Server) -> Router { diff --git a/src/api/router/auth.rs b/src/api/router/auth.rs index 564e2966..b88f78b7 100644 --- a/src/api/router/auth.rs +++ b/src/api/router/auth.rs @@ -1,5 +1,6 @@ mod appservice; mod server; +mod uiaa; use std::{fmt::Debug, time::SystemTime}; @@ -35,6 +36,7 @@ use ruma::{ use tuwunel_core::{Err, Error, Result, is_less_than, utils::result::LogDebugErr}; use tuwunel_service::{Services, appservice::RegistrationInfo}; +pub(crate) use self::uiaa::auth_uiaa; use self::{appservice::auth_appservice, server::auth_server}; use super::request::Request; diff --git a/src/api/router/auth/uiaa.rs b/src/api/router/auth/uiaa.rs new file mode 100644 index 00000000..25a72669 --- /dev/null +++ b/src/api/router/auth/uiaa.rs @@ -0,0 +1,81 @@ +use ruma::{ + CanonicalJsonValue, OwnedUserId, + api::{ + IncomingRequest, + client::uiaa::{AuthData, AuthFlow, AuthType, Jwt, UiaaInfo}, + }, +}; +use tuwunel_core::{Err, Error, Result, err, utils}; +use tuwunel_service::{Services, uiaa::SESSION_ID_LENGTH}; + +use crate::{Ruma, client::jwt}; + +pub(crate) async fn auth_uiaa(services: &Services, body: &Ruma) -> Result +where + T: IncomingRequest + Send + Sync, +{ + let flows = [ + AuthFlow::new([AuthType::Password].into()), + AuthFlow::new([AuthType::Jwt].into()), + ]; + + let mut uiaainfo = UiaaInfo { + flows: flows.into(), + ..Default::default() + }; + + match body + .json_body + .as_ref() + .and_then(CanonicalJsonValue::as_object) + .and_then(|body| body.get("auth")) + .cloned() + .map(CanonicalJsonValue::into) + .map(serde_json::from_value) + .transpose()? + { + | Some(AuthData::Jwt(Jwt { ref token, .. })) => { + let sender_user = jwt::validate_user(services, token)?; + if !services.users.exists(&sender_user).await { + return Err!(Request(NotFound("User {sender_user} is not registered."))); + } + + // Success! + Ok(sender_user) + }, + | Some(ref auth) => { + let sender_user = body + .sender_user + .as_deref() + .ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?; + + let (worked, uiaainfo) = services + .uiaa + .try_auth(sender_user, body.sender_device(), auth, &uiaainfo) + .await?; + + if !worked { + return Err(Error::Uiaa(uiaainfo)); + } + + // Success! + Ok(sender_user.to_owned()) + }, + | _ => match body.json_body { + | Some(ref json) => { + let sender_user = body + .sender_user + .as_ref() + .ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?; + + uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH)); + services + .uiaa + .create(sender_user, body.sender_device(), &uiaainfo, json); + + Err(Error::Uiaa(uiaainfo)) + }, + | _ => Err!(Request(NotJson("JSON body is not valid"))), + }, + } +}