From a2f3f6180ef4a277c7808f2d35ea1fa6602fb4d8 Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Fri, 19 Jun 2026 17:53:53 +0200 Subject: [PATCH 01/10] fix(ffi): add methods to make QR login handlers exit cooperatively Signed-off-by: Johannes Marbach --- .../matrix-sdk-ffi/changelog.d/6677.fixed.md | 1 + bindings/matrix-sdk-ffi/src/qr_code.rs | 89 +++++++++++++++---- 2 files changed, 74 insertions(+), 16 deletions(-) create mode 100644 bindings/matrix-sdk-ffi/changelog.d/6677.fixed.md diff --git a/bindings/matrix-sdk-ffi/changelog.d/6677.fixed.md b/bindings/matrix-sdk-ffi/changelog.d/6677.fixed.md new file mode 100644 index 00000000000..3686e965983 --- /dev/null +++ b/bindings/matrix-sdk-ffi/changelog.d/6677.fixed.md @@ -0,0 +1 @@ +Add methods to make QR login handlers exit cooperatively. diff --git a/bindings/matrix-sdk-ffi/src/qr_code.rs b/bindings/matrix-sdk-ffi/src/qr_code.rs index 96ffc4cef10..c8485ffbe2d 100644 --- a/bindings/matrix-sdk-ffi/src/qr_code.rs +++ b/bindings/matrix-sdk-ffi/src/qr_code.rs @@ -23,6 +23,7 @@ use matrix_sdk::authentication::oauth::{ }; use matrix_sdk_base::crypto::types::qr_login::{self, QrCodeIntent}; use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm, stream::StreamExt}; +use tokio::sync::Notify; use crate::{ authentication::OAuthConfiguration, runtime::get_runtime_handle, task_handle::TaskHandle, @@ -33,11 +34,14 @@ use crate::{ pub struct LoginWithQrCodeHandler { oauth: OAuth, oauth_configuration: OAuthConfiguration, + /// Request a the handler to abort cooperatively. This will make the handler + /// tear down its running task and then emit the `Cancelled` update. + cancel: Notify, } impl LoginWithQrCodeHandler { pub(crate) fn new(oauth: OAuth, oauth_configuration: OAuthConfiguration) -> Self { - Self { oauth, oauth_configuration } + Self { oauth, oauth_configuration, cancel: Notify::new() } } } @@ -82,15 +86,25 @@ impl LoginWithQrCodeHandler { // We create this task, which will get cancelled once it's dropped, just in case // the progress stream doesn't end. - let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move { + let progress_task = TaskHandle::new(get_runtime_handle().spawn(async move { while let Some(state) = progress.next().await { progress_listener.on_update(state.into()); } })); - login.await?; - - Ok(()) + tokio::select! { + biased; + result = login => { + result?; + Ok(()) + } + _ = self.cancel.notified() => { + // Stop forwarding progress to the foreign callback before tearing + // down the handler. + drop(progress_task); + Err(HumanQrLoginError::Cancelled) + } + } } /// This method allows you to log in by generating a QR code. @@ -126,15 +140,31 @@ impl LoginWithQrCodeHandler { // We create this task, which will get cancelled once it's dropped, just in case // the progress stream doesn't end. - let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move { + let progress_task = TaskHandle::new(get_runtime_handle().spawn(async move { while let Some(state) = progress.next().await { progress_listener.on_update(state.into()); } })); - login.await?; + tokio::select! { + biased; + result = login => { + result?; + Ok(()) + } + _ = self.cancel.notified() => { + // Stop forwarding progress to the foreign callback before tearing + // down the handler. + drop(progress_task); + Err(HumanQrLoginError::Cancelled) + } + } + } - Ok(()) + /// Request a the handler to abort cooperatively. This will make the handler + /// tear down its running task and then return the `Cancelled` error. + pub fn abort(&self) { + self.cancel.notify_waiters(); } } @@ -142,11 +172,12 @@ impl LoginWithQrCodeHandler { #[derive(uniffi::Object)] pub struct GrantLoginWithQrCodeHandler { oauth: OAuth, + cancel: Notify, } impl GrantLoginWithQrCodeHandler { pub(crate) fn new(oauth: OAuth) -> Self { - Self { oauth } + Self { oauth, cancel: Notify::new() } } } @@ -182,15 +213,25 @@ impl GrantLoginWithQrCodeHandler { // We create this task, which will get cancelled once it's dropped, just in case // the progress stream doesn't end. - let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move { + let progress_task = TaskHandle::new(get_runtime_handle().spawn(async move { while let Some(state) = progress.next().await { progress_listener.on_update(state.into()); } })); - grant.await?; - - Ok(()) + tokio::select! { + biased; + result = grant => { + result?; + Ok(()) + } + _ = self.cancel.notified() => { + // Stop forwarding progress to the foreign callback before tearing + // down the handler. + drop(progress_task); + Err(HumanQrGrantLoginError::Cancelled) + } + } } /// This method allows you to grant login by generating a QR code. @@ -220,15 +261,31 @@ impl GrantLoginWithQrCodeHandler { // We create this task, which will get cancelled once it's dropped, just in case // the progress stream doesn't end. - let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move { + let progress_task = TaskHandle::new(get_runtime_handle().spawn(async move { while let Some(state) = progress.next().await { progress_listener.on_update(state.into()); } })); - grant.await?; + tokio::select! { + biased; + result = grant => { + result?; + Ok(()) + } + _ = self.cancel.notified() => { + // Stop forwarding progress to the foreign callback before tearing + // down the handler. + drop(progress_task); + Err(HumanQrGrantLoginError::Cancelled) + } + } + } - Ok(()) + /// Request a the handler to abort cooperatively. This will make the handler + /// tear down its running task and then return the `Cancelled` error. + pub fn abort(&self) { + self.cancel.notify_waiters(); } } From f8a853955e9dc2b6f15a765abe69620dd238c8cc Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Tue, 23 Jun 2026 20:34:20 +0200 Subject: [PATCH 02/10] fixup! fix(ffi): add methods to make QR login handlers exit cooperatively Fix doc comments Signed-off-by: Johannes Marbach --- bindings/matrix-sdk-ffi/src/qr_code.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/qr_code.rs b/bindings/matrix-sdk-ffi/src/qr_code.rs index c8485ffbe2d..c76fab873e0 100644 --- a/bindings/matrix-sdk-ffi/src/qr_code.rs +++ b/bindings/matrix-sdk-ffi/src/qr_code.rs @@ -34,8 +34,6 @@ use crate::{ pub struct LoginWithQrCodeHandler { oauth: OAuth, oauth_configuration: OAuthConfiguration, - /// Request a the handler to abort cooperatively. This will make the handler - /// tear down its running task and then emit the `Cancelled` update. cancel: Notify, } @@ -161,7 +159,7 @@ impl LoginWithQrCodeHandler { } } - /// Request a the handler to abort cooperatively. This will make the handler + /// Request the handler to abort cooperatively. This will make the handler /// tear down its running task and then return the `Cancelled` error. pub fn abort(&self) { self.cancel.notify_waiters(); @@ -282,7 +280,7 @@ impl GrantLoginWithQrCodeHandler { } } - /// Request a the handler to abort cooperatively. This will make the handler + /// Request the handler to abort cooperatively. This will make the handler /// tear down its running task and then return the `Cancelled` error. pub fn abort(&self) { self.cancel.notify_waiters(); From e9a461d78803379b745f5b0f6320587d9d989baa Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Tue, 23 Jun 2026 20:43:35 +0200 Subject: [PATCH 03/10] fixup! fix(ffi): add methods to make QR login handlers exit cooperatively Remove biasing Signed-off-by: Johannes Marbach --- bindings/matrix-sdk-ffi/src/qr_code.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/qr_code.rs b/bindings/matrix-sdk-ffi/src/qr_code.rs index c76fab873e0..3e855b62e58 100644 --- a/bindings/matrix-sdk-ffi/src/qr_code.rs +++ b/bindings/matrix-sdk-ffi/src/qr_code.rs @@ -91,7 +91,6 @@ impl LoginWithQrCodeHandler { })); tokio::select! { - biased; result = login => { result?; Ok(()) @@ -145,7 +144,6 @@ impl LoginWithQrCodeHandler { })); tokio::select! { - biased; result = login => { result?; Ok(()) @@ -218,7 +216,6 @@ impl GrantLoginWithQrCodeHandler { })); tokio::select! { - biased; result = grant => { result?; Ok(()) @@ -266,7 +263,6 @@ impl GrantLoginWithQrCodeHandler { })); tokio::select! { - biased; result = grant => { result?; Ok(()) From 551a2b6fdb31913e5eecffc980f05b390f5e7351 Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Thu, 25 Jun 2026 20:54:38 +0200 Subject: [PATCH 04/10] fixup! fix(ffi): add methods to make QR login handlers exit cooperatively Revert back to biased and document why its needed Signed-off-by: Johannes Marbach --- bindings/matrix-sdk-ffi/src/qr_code.rs | 40 +++++++++++++++----------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/qr_code.rs b/bindings/matrix-sdk-ffi/src/qr_code.rs index 3e855b62e58..e2c7df2c63e 100644 --- a/bindings/matrix-sdk-ffi/src/qr_code.rs +++ b/bindings/matrix-sdk-ffi/src/qr_code.rs @@ -91,16 +91,18 @@ impl LoginWithQrCodeHandler { })); tokio::select! { - result = login => { - result?; - Ok(()) - } + // Give priority to cancellation if both updates occur at the same time. + biased; _ = self.cancel.notified() => { // Stop forwarding progress to the foreign callback before tearing // down the handler. drop(progress_task); Err(HumanQrLoginError::Cancelled) } + result = login => { + result?; + Ok(()) + } } } @@ -144,16 +146,18 @@ impl LoginWithQrCodeHandler { })); tokio::select! { - result = login => { - result?; - Ok(()) - } + // Give priority to cancellation if both updates occur at the same time. + biased; _ = self.cancel.notified() => { // Stop forwarding progress to the foreign callback before tearing // down the handler. drop(progress_task); Err(HumanQrLoginError::Cancelled) } + result = login => { + result?; + Ok(()) + } } } @@ -216,16 +220,18 @@ impl GrantLoginWithQrCodeHandler { })); tokio::select! { - result = grant => { - result?; - Ok(()) - } + // Give priority to cancellation if both updates occur at the same time. + biased; _ = self.cancel.notified() => { // Stop forwarding progress to the foreign callback before tearing // down the handler. drop(progress_task); Err(HumanQrGrantLoginError::Cancelled) } + result = grant => { + result?; + Ok(()) + } } } @@ -263,16 +269,18 @@ impl GrantLoginWithQrCodeHandler { })); tokio::select! { - result = grant => { - result?; - Ok(()) - } + // Give priority to cancellation if both updates occur at the same time. + biased; _ = self.cancel.notified() => { // Stop forwarding progress to the foreign callback before tearing // down the handler. drop(progress_task); Err(HumanQrGrantLoginError::Cancelled) } + result = grant => { + result?; + Ok(()) + } } } From b731790f98fe488ad73165fc0ce4997481b45849 Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Thu, 2 Jul 2026 21:03:04 +0200 Subject: [PATCH 05/10] fixup! fix(ffi): add methods to make QR login handlers exit cooperatively Move logic into SDK Signed-off-by: Johannes Marbach --- Cargo.lock | 2 + Cargo.toml | 1 + bindings/matrix-sdk-ffi/Cargo.toml | 1 + bindings/matrix-sdk-ffi/src/qr_code.rs | 107 ++---- crates/matrix-sdk/Cargo.toml | 2 +- .../src/authentication/oauth/mod.rs | 59 ++- .../src/authentication/oauth/qrcode/grant.rs | 268 ++++++++------ .../src/authentication/oauth/qrcode/login.rs | 336 +++++++++++++----- .../src/authentication/oauth/qrcode/mod.rs | 8 + examples/qr-login/Cargo.toml | 1 + examples/qr-login/src/main.rs | 4 +- 11 files changed, 490 insertions(+), 299 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1835f6a7906..5b9a48be116 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1818,6 +1818,7 @@ dependencies = [ "futures-util", "matrix-sdk", "tokio", + "tokio-util", "tracing-subscriber", "url", ] @@ -3590,6 +3591,7 @@ dependencies = [ "tempfile", "thiserror 2.0.18", "tokio", + "tokio-util", "tracing", "tracing-appender", "tracing-core", diff --git a/Cargo.toml b/Cargo.toml index f5058576112..2c0f24ff389 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -108,6 +108,7 @@ tempfile = { version = "3.23.0", default-features = false } thiserror = { version = "2.0.17", default-features = false } tokio = { version = "1.48.0", default-features = false, features = ["sync"] } tokio-stream = { version = "0.1.17", default-features = false } +tokio-util = "0.7.17" tracing = { version = "0.1.41", default-features = false, features = ["std"] } tracing-appender = { version = "0.2.3", default-features = false } tracing-core = { version = "0.1.34", default-features = false } diff --git a/bindings/matrix-sdk-ffi/Cargo.toml b/bindings/matrix-sdk-ffi/Cargo.toml index 632ab723dc8..7babc583e69 100644 --- a/bindings/matrix-sdk-ffi/Cargo.toml +++ b/bindings/matrix-sdk-ffi/Cargo.toml @@ -102,6 +102,7 @@ sentry = { workspace = true, optional = true, features = [ ] } sentry-tracing = { workspace = true, optional = true } thiserror.workspace = true +tokio-util.workspace = true tracing.workspace = true tracing-appender.workspace = true tracing-core.workspace = true diff --git a/bindings/matrix-sdk-ffi/src/qr_code.rs b/bindings/matrix-sdk-ffi/src/qr_code.rs index e2c7df2c63e..0eae883484b 100644 --- a/bindings/matrix-sdk-ffi/src/qr_code.rs +++ b/bindings/matrix-sdk-ffi/src/qr_code.rs @@ -23,7 +23,7 @@ use matrix_sdk::authentication::oauth::{ }; use matrix_sdk_base::crypto::types::qr_login::{self, QrCodeIntent}; use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm, stream::StreamExt}; -use tokio::sync::Notify; +use tokio_util::sync::CancellationToken; use crate::{ authentication::OAuthConfiguration, runtime::get_runtime_handle, task_handle::TaskHandle, @@ -34,12 +34,12 @@ use crate::{ pub struct LoginWithQrCodeHandler { oauth: OAuth, oauth_configuration: OAuthConfiguration, - cancel: Notify, + cancel: CancellationToken, } impl LoginWithQrCodeHandler { pub(crate) fn new(oauth: OAuth, oauth_configuration: OAuthConfiguration) -> Self { - Self { oauth, oauth_configuration, cancel: Notify::new() } + Self { oauth, oauth_configuration, cancel: CancellationToken::new() } } } @@ -77,33 +77,24 @@ impl LoginWithQrCodeHandler { .registration_data() .map_err(|_| HumanQrLoginError::OAuthMetadataInvalid)?; - let login = - self.oauth.login_with_qr_code(Some(®istration_data)).scan(&qr_code_data.inner); + let login = self + .oauth + .login_with_qr_code(Some(®istration_data), self.cancel.clone()) + .scan(&qr_code_data.inner); let mut progress = login.subscribe_to_progress(); // We create this task, which will get cancelled once it's dropped, just in case // the progress stream doesn't end. - let progress_task = TaskHandle::new(get_runtime_handle().spawn(async move { + let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move { while let Some(state) = progress.next().await { progress_listener.on_update(state.into()); } })); - tokio::select! { - // Give priority to cancellation if both updates occur at the same time. - biased; - _ = self.cancel.notified() => { - // Stop forwarding progress to the foreign callback before tearing - // down the handler. - drop(progress_task); - Err(HumanQrLoginError::Cancelled) - } - result = login => { - result?; - Ok(()) - } - } + login.await?; + + Ok(()) } /// This method allows you to log in by generating a QR code. @@ -133,38 +124,28 @@ impl LoginWithQrCodeHandler { .registration_data() .map_err(|_| HumanQrLoginError::OAuthMetadataInvalid)?; - let login = self.oauth.login_with_qr_code(Some(®istration_data)).generate(); + let login = + self.oauth.login_with_qr_code(Some(®istration_data), self.cancel.clone()).generate(); let mut progress = login.subscribe_to_progress(); // We create this task, which will get cancelled once it's dropped, just in case // the progress stream doesn't end. - let progress_task = TaskHandle::new(get_runtime_handle().spawn(async move { + let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move { while let Some(state) = progress.next().await { progress_listener.on_update(state.into()); } })); - tokio::select! { - // Give priority to cancellation if both updates occur at the same time. - biased; - _ = self.cancel.notified() => { - // Stop forwarding progress to the foreign callback before tearing - // down the handler. - drop(progress_task); - Err(HumanQrLoginError::Cancelled) - } - result = login => { - result?; - Ok(()) - } - } + login.await?; + + Ok(()) } /// Request the handler to abort cooperatively. This will make the handler /// tear down its running task and then return the `Cancelled` error. pub fn abort(&self) { - self.cancel.notify_waiters(); + self.cancel.cancel(); } } @@ -172,12 +153,12 @@ impl LoginWithQrCodeHandler { #[derive(uniffi::Object)] pub struct GrantLoginWithQrCodeHandler { oauth: OAuth, - cancel: Notify, + cancel: CancellationToken, } impl GrantLoginWithQrCodeHandler { pub(crate) fn new(oauth: OAuth) -> Self { - Self { oauth, cancel: Notify::new() } + Self { oauth, cancel: CancellationToken::new() } } } @@ -207,32 +188,22 @@ impl GrantLoginWithQrCodeHandler { qr_code_data: &QrCodeData, progress_listener: Box, ) -> Result<(), HumanQrGrantLoginError> { - let grant = self.oauth.grant_login_with_qr_code().scan(&qr_code_data.inner); + let grant = + self.oauth.grant_login_with_qr_code(self.cancel.clone()).scan(&qr_code_data.inner); let mut progress = grant.subscribe_to_progress(); // We create this task, which will get cancelled once it's dropped, just in case // the progress stream doesn't end. - let progress_task = TaskHandle::new(get_runtime_handle().spawn(async move { + let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move { while let Some(state) = progress.next().await { progress_listener.on_update(state.into()); } })); - tokio::select! { - // Give priority to cancellation if both updates occur at the same time. - biased; - _ = self.cancel.notified() => { - // Stop forwarding progress to the foreign callback before tearing - // down the handler. - drop(progress_task); - Err(HumanQrGrantLoginError::Cancelled) - } - result = grant => { - result?; - Ok(()) - } - } + grant.await?; + + Ok(()) } /// This method allows you to grant login by generating a QR code. @@ -256,38 +227,27 @@ impl GrantLoginWithQrCodeHandler { self: Arc, progress_listener: Box, ) -> Result<(), HumanQrGrantLoginError> { - let grant = self.oauth.grant_login_with_qr_code().generate(); + let grant = self.oauth.grant_login_with_qr_code(self.cancel.clone()).generate(); let mut progress = grant.subscribe_to_progress(); // We create this task, which will get cancelled once it's dropped, just in case // the progress stream doesn't end. - let progress_task = TaskHandle::new(get_runtime_handle().spawn(async move { + let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move { while let Some(state) = progress.next().await { progress_listener.on_update(state.into()); } })); - tokio::select! { - // Give priority to cancellation if both updates occur at the same time. - biased; - _ = self.cancel.notified() => { - // Stop forwarding progress to the foreign callback before tearing - // down the handler. - drop(progress_task); - Err(HumanQrGrantLoginError::Cancelled) - } - result = grant => { - result?; - Ok(()) - } - } + grant.await?; + + Ok(()) } /// Request the handler to abort cooperatively. This will make the handler /// tear down its running task and then return the `Cancelled` error. pub fn abort(&self) { - self.cancel.notify_waiters(); + self.cancel.cancel(); } } @@ -445,6 +405,8 @@ impl From for HumanQrLoginError { | QRCodeLoginError::ServerReset(_) => HumanQrLoginError::Unknown, QRCodeLoginError::NotFound => HumanQrLoginError::NotFound, + + QRCodeLoginError::Cancelled => HumanQrLoginError::Cancelled, } } } @@ -548,6 +510,7 @@ impl From for HumanQrGrantLoginError { LoginFailureReason::UserCancelled => Self::Cancelled, _ => Self::Unknown(reason.to_string()), }, + QRCodeGrantLoginError::Cancelled => HumanQrGrantLoginError::Cancelled, } } } diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index 9ce54f397c2..73a7a773b49 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -145,7 +145,7 @@ sha2.workspace = true tempfile.workspace = true thiserror.workspace = true tokio-stream = { workspace = true, features = ["sync"] } -tokio-util = "0.7.17" +tokio-util.workspace = true tower = { version = "0.5.2", features = ["util"], optional = true } tracing = { workspace = true, features = ["attributes"] } uniffi = { workspace = true, optional = true } diff --git a/crates/matrix-sdk/src/authentication/oauth/mod.rs b/crates/matrix-sdk/src/authentication/oauth/mod.rs index fa23d684c08..9e678ca6aad 100644 --- a/crates/matrix-sdk/src/authentication/oauth/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/mod.rs @@ -194,6 +194,8 @@ use ruma::{ use serde::{Deserialize, Serialize}; use sha2::Digest as _; use tokio::sync::Mutex; +#[cfg(feature = "e2e-encryption")] +use tokio_util::sync::CancellationToken; use tracing::{debug, error, instrument, trace, warn}; use url::Url; @@ -378,18 +380,27 @@ impl OAuth { /// the server. If this is not provided, an error will occur unless /// [`OAuth::register_client()`] or [`OAuth::restore_registered_client()`] /// was called previously. + /// * `cancel` - A token for requesting the future to abort cooperatively. #[cfg(feature = "e2e-encryption")] pub fn login_with_qr_code<'a>( &'a self, registration_data: Option<&'a ClientRegistrationData>, + cancel: CancellationToken, ) -> LoginWithQrCodeBuilder<'a> { - LoginWithQrCodeBuilder { client: &self.client, registration_data } + LoginWithQrCodeBuilder::new(&self.client, registration_data, cancel) } /// Grant login to a new device using a QR code. + /// + /// # Arguments + /// + /// * `cancel` - A token for requesting the future to abort cooperatively. #[cfg(feature = "e2e-encryption")] - pub fn grant_login_with_qr_code<'a>(&'a self) -> GrantLoginWithQrCodeBuilder<'a> { - GrantLoginWithQrCodeBuilder::new(&self.client) + pub fn grant_login_with_qr_code<'a>( + &'a self, + cancel: CancellationToken, + ) -> GrantLoginWithQrCodeBuilder<'a> { + GrantLoginWithQrCodeBuilder::new(&self.client, cancel) } /// Restore or register the OAuth 2.0 client for the server with the given @@ -1357,10 +1368,22 @@ pub struct LoginWithQrCodeBuilder<'a> { /// The data to restore or register the client with the server. registration_data: Option<&'a ClientRegistrationData>, + + /// A token for requesting the future to abort cooperatively. + cancel: CancellationToken, } #[cfg(feature = "e2e-encryption")] impl<'a> LoginWithQrCodeBuilder<'a> { + /// Create a new builder. + fn new( + client: &'a Client, + registration_data: Option<&'a ClientRegistrationData>, + cancel: CancellationToken, + ) -> Self { + Self { client, registration_data, cancel } + } + /// This method allows you to log in with a scanned QR code. /// /// The existing device needs to display the QR code which this device can @@ -1416,7 +1439,8 @@ impl<'a> LoginWithQrCodeBuilder<'a> { /// /// // Subscribing to the progress is necessary since we need to input the check /// // code on the existing device. - /// let login = oauth.login_with_qr_code(Some(®istration_data)).scan(&qr_code_data); + /// let cancel = CancellationToken::new(); + /// let login = oauth.login_with_qr_code(Some(®istration_data), cancel).scan(&qr_code_data); /// let mut progress = login.subscribe_to_progress(); /// /// // Create a task which will show us the progress and tell us the check @@ -1445,7 +1469,7 @@ impl<'a> LoginWithQrCodeBuilder<'a> { /// # anyhow::Ok(()) }; /// ``` pub fn scan(self, data: &'a QrCodeData) -> LoginWithQrCode<'a> { - LoginWithQrCode::new(self.client, data, self.registration_data) + LoginWithQrCode::new(self.client, data, self.registration_data, self.cancel) } /// This method allows you to log in by generating a QR code. @@ -1490,7 +1514,8 @@ impl<'a> LoginWithQrCodeBuilder<'a> { /// /// // Subscribing to the progress is necessary since we need to display the /// // QR code and prompt for the check code. - /// let login = oauth.login_with_qr_code(Some(®istration_data)).generate(); + /// let cancel = CancellationToken::new(); + /// let login = oauth.login_with_qr_code(Some(®istration_data), cancel).generate(); /// let mut progress = login.subscribe_to_progress(); /// /// // Create a task which will show us the progress and allows us to display @@ -1526,7 +1551,7 @@ impl<'a> LoginWithQrCodeBuilder<'a> { /// # anyhow::Ok(()) }; /// ``` pub fn generate(self) -> LoginWithGeneratedQrCode<'a> { - LoginWithGeneratedQrCode::new(self.client, self.registration_data) + LoginWithGeneratedQrCode::new(self.client, self.registration_data, self.cancel) } } @@ -1539,13 +1564,15 @@ pub struct GrantLoginWithQrCodeBuilder<'a> { /// The duration to wait for the homeserver to create the new device after /// consenting the login before giving up. device_creation_timeout: Duration, + /// A token for requesting the future to abort cooperatively. + cancel: CancellationToken, } #[cfg(feature = "e2e-encryption")] impl<'a> GrantLoginWithQrCodeBuilder<'a> { /// Create a new builder with the default device creation timeout. - fn new(client: &'a Client) -> Self { - Self { client, device_creation_timeout: Duration::from_secs(10) } + fn new(client: &'a Client, cancel: CancellationToken) -> Self { + Self { client, device_creation_timeout: Duration::from_secs(10), cancel } } /// Set the device creation timeout. @@ -1607,7 +1634,8 @@ impl<'a> GrantLoginWithQrCodeBuilder<'a> { /// // Subscribing to the progress is necessary to capture /// // the checkcode in order to display it to the other device and to obtain the verification URL to /// // open it in a browser so the user can consent to the new login. - /// let mut grant = oauth.grant_login_with_qr_code().scan(&qr_code_data); + /// let cancel = CancellationToken::new() + /// let mut grant = oauth.grant_login_with_qr_code(cancel).scan(&qr_code_data); /// let mut progress = grant.subscribe_to_progress(); /// /// // Create a task which will show us the progress and allows us to receive @@ -1636,7 +1664,12 @@ impl<'a> GrantLoginWithQrCodeBuilder<'a> { /// # anyhow::Ok(()) }; /// ``` pub fn scan(self, data: &'a QrCodeData) -> GrantLoginWithScannedQrCode<'a> { - GrantLoginWithScannedQrCode::new(self.client, data, self.device_creation_timeout) + GrantLoginWithScannedQrCode::new( + self.client, + data, + self.device_creation_timeout, + self.cancel, + ) } /// This method allows you to grant login to a new device by generating a QR @@ -1677,7 +1710,7 @@ impl<'a> GrantLoginWithQrCodeBuilder<'a> { /// // Subscribing to the progress is necessary since we need to capture the /// // QR code, feed the checkcode back in and obtain the verification URL to /// // open it in a browser so the user can consent to the new login. - /// let mut grant = oauth.grant_login_with_qr_code().generate(); + /// let mut grant = oauth.grant_login_with_qr_code(CancellationToken::new()).generate(); /// let mut progress = grant.subscribe_to_progress(); /// /// // Create a task which will show us the progress and allows us to receive @@ -1713,7 +1746,7 @@ impl<'a> GrantLoginWithQrCodeBuilder<'a> { /// # anyhow::Ok(()) }; /// ``` pub fn generate(self) -> GrantLoginWithGeneratedQrCode<'a> { - GrantLoginWithGeneratedQrCode::new(self.client, self.device_creation_timeout) + GrantLoginWithGeneratedQrCode::new(self.client, self.device_creation_timeout, self.cancel) } } /// A full session for the OAuth 2.0 API. diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs index b2aced751a2..2e28784c760 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs @@ -25,6 +25,7 @@ use matrix_sdk_base::{ }; use oauth2::VerificationUriComplete; use ruma::time::Instant; +use tokio_util::sync::CancellationToken; use url::Url; #[cfg(doc)] use vodozemac::ecies::CheckCode; @@ -212,6 +213,7 @@ pub struct GrantLoginWithScannedQrCode<'a> { qr_code_data: &'a QrCodeData, device_creation_timeout: Duration, state: SharedObservable>, + cancel: CancellationToken, } impl<'a> GrantLoginWithScannedQrCode<'a> { @@ -219,12 +221,14 @@ impl<'a> GrantLoginWithScannedQrCode<'a> { client: &'a Client, qr_code_data: &'a QrCodeData, device_creation_timeout: Duration, + cancel: CancellationToken, ) -> GrantLoginWithScannedQrCode<'a> { GrantLoginWithScannedQrCode { client, qr_code_data, device_creation_timeout, state: Default::default(), + cancel, } } } @@ -248,54 +252,66 @@ impl<'a> IntoFuture for GrantLoginWithScannedQrCode<'a> { fn into_future(self) -> Self::IntoFuture { Box::pin(async move { - // Before we get here, the other device has created a new rendezvous session - // and presented a QR code which this device has scanned. - // -- MSC4108 Secure channel setup steps 1-3 - - // First things first, export the secrets bundle and establish the secure - // channel. Since we're the one that scanned the QR code, we're - // certain that the secure channel is secure, under the assumption - // that we didn't scan the wrong QR code. -- MSC4108 Secure channel - // setup steps 3-5 - let secrets_bundle = export_secrets_bundle(self.client).await?; - - let mut channel = EstablishedSecureChannel::from_qr_code( - self.client.inner.http_client.inner.clone(), - self.qr_code_data, - QrCodeIntent::Reciprocate, - ) - .await?; - - // The other side isn't yet sure that it's talking to the right device, show - // a check code so they can confirm. - // -- MSC4108 Secure channel setup step 6 - let check_code = channel.check_code().to_owned(); - self.state - .set(GrantLoginProgress::EstablishingSecureChannel(QrProgress { check_code })); - - // The user now enters the checkcode on the other device which verifies it - // and will only continue requesting the login if the code matches. - // -- MSC4108 Secure channel setup step 7 - - // Inform the other device about the available login protocols and the - // homeserver to use. - // -- MSC4108 OAuth 2.0 login step 1 - let message = QrAuthMessage::LoginProtocols { - protocols: vec![LoginProtocolType::DeviceAuthorizationGrant], - homeserver: self.client.homeserver(), + let cancel = self.cancel.clone(); + let grant = async move { + // Before we get here, the other device has created a new rendezvous session + // and presented a QR code which this device has scanned. + // -- MSC4108 Secure channel setup steps 1-3 + + // First things first, export the secrets bundle and establish the secure + // channel. Since we're the one that scanned the QR code, we're + // certain that the secure channel is secure, under the assumption + // that we didn't scan the wrong QR code. -- MSC4108 Secure channel + // setup steps 3-5 + let secrets_bundle = export_secrets_bundle(self.client).await?; + + let mut channel = EstablishedSecureChannel::from_qr_code( + self.client.inner.http_client.inner.clone(), + self.qr_code_data, + QrCodeIntent::Reciprocate, + ) + .await?; + + // The other side isn't yet sure that it's talking to the right device, show + // a check code so they can confirm. + // -- MSC4108 Secure channel setup step 6 + let check_code = channel.check_code().to_owned(); + self.state + .set(GrantLoginProgress::EstablishingSecureChannel(QrProgress { check_code })); + + // The user now enters the checkcode on the other device which verifies it + // and will only continue requesting the login if the code matches. + // -- MSC4108 Secure channel setup step 7 + + // Inform the other device about the available login protocols and the + // homeserver to use. + // -- MSC4108 OAuth 2.0 login step 1 + let message = QrAuthMessage::LoginProtocols { + protocols: vec![LoginProtocolType::DeviceAuthorizationGrant], + homeserver: self.client.homeserver(), + }; + channel.send_json(message).await?; + + // Proceed with granting the login. + // -- MSC4108 OAuth 2.0 login remaining steps + finish_login_grant( + self.client, + &mut channel, + self.device_creation_timeout, + &secrets_bundle, + &self.state, + ) + .await }; - channel.send_json(message).await?; - - // Proceed with granting the login. - // -- MSC4108 OAuth 2.0 login remaining steps - finish_login_grant( - self.client, - &mut channel, - self.device_creation_timeout, - &secrets_bundle, - &self.state, - ) - .await + + tokio::select! { + // Give priority to cancellation if both updates occur at the same time. + biased; + _ = cancel.cancelled() => { + Err(QRCodeGrantLoginError::Cancelled) + } + result = grant => result + } }) } } @@ -307,14 +323,21 @@ pub struct GrantLoginWithGeneratedQrCode<'a> { client: &'a Client, device_creation_timeout: Duration, state: SharedObservable>, + cancel: CancellationToken, } impl<'a> GrantLoginWithGeneratedQrCode<'a> { pub(crate) fn new( client: &'a Client, device_creation_timeout: Duration, + cancel: CancellationToken, ) -> GrantLoginWithGeneratedQrCode<'a> { - GrantLoginWithGeneratedQrCode { client, device_creation_timeout, state: Default::default() } + GrantLoginWithGeneratedQrCode { + client, + device_creation_timeout, + state: Default::default(), + cancel, + } } } @@ -338,55 +361,68 @@ impl<'a> IntoFuture for GrantLoginWithGeneratedQrCode<'a> { fn into_future(self) -> Self::IntoFuture { Box::pin(async move { - // Create a new ephemeral key pair and a rendezvous session to grant a - // login with. - // -- MSC4108 Secure channel setup steps 1 & 2 - let homeserver_url = self.client.homeserver(); - let http_client = self.client.inner.http_client.clone(); - let secrets_bundle = export_secrets_bundle(self.client).await?; - let channel = SecureChannel::reciprocate(http_client, &homeserver_url).await?; - - // Extract the QR code data and emit an update so that the caller can - // present the QR code for scanning by the new device. - // -- MSC4108 Secure channel setup step 3 - self.state.set(GrantLoginProgress::EstablishingSecureChannel( - GeneratedQrProgress::QrReady(channel.qr_code_data().clone()), - )); - - // Wait for the secure channel to connect. The other device now needs to scan - // the QR code and send us the LoginInitiateMessage which we respond to - // with the LoginOkMessage. -- MSC4108 step 4 & 5 - let channel = channel.connect().await?; - - // The other device now needs to verify our message, compute the checkcode and - // display it. We emit a progress update to let the caller prompt the - // user to enter the checkcode and feed it back to us. - // -- MSC4108 Secure channel setup step 6 - let (tx, rx) = tokio::sync::oneshot::channel(); - self.state.set(GrantLoginProgress::EstablishingSecureChannel( - GeneratedQrProgress::QrScanned(CheckCodeSender::new(tx)), - )); - let check_code = rx.await.map_err(|_| SecureChannelError::CannotReceiveCheckCode)?; - - // Use the checkcode to verify that the channel is actually secure. - // -- MSC4108 Secure channel setup step 7 - let mut channel = channel.confirm(check_code)?; - - // Since the QR code was generated on this existing device, the new device can - // derive the homeserver to use for logging in from the QR code and we - // don't need to send the m.login.protocols message. - // -- MSC4108 OAuth 2.0 login step 1 - - // Proceed with granting the login. - // -- MSC4108 OAuth 2.0 login remaining steps - finish_login_grant( - self.client, - &mut channel, - self.device_creation_timeout, - &secrets_bundle, - &self.state, - ) - .await + let cancel = self.cancel.clone(); + let grant = async move { + // Create a new ephemeral key pair and a rendezvous session to grant a + // login with. + // -- MSC4108 Secure channel setup steps 1 & 2 + let homeserver_url = self.client.homeserver(); + let http_client = self.client.inner.http_client.clone(); + let secrets_bundle = export_secrets_bundle(self.client).await?; + let channel = SecureChannel::reciprocate(http_client, &homeserver_url).await?; + + // Extract the QR code data and emit an update so that the caller can + // present the QR code for scanning by the new device. + // -- MSC4108 Secure channel setup step 3 + self.state.set(GrantLoginProgress::EstablishingSecureChannel( + GeneratedQrProgress::QrReady(channel.qr_code_data().clone()), + )); + + // Wait for the secure channel to connect. The other device now needs to scan + // the QR code and send us the LoginInitiateMessage which we respond to + // with the LoginOkMessage. -- MSC4108 step 4 & 5 + let channel = channel.connect().await?; + + // The other device now needs to verify our message, compute the checkcode and + // display it. We emit a progress update to let the caller prompt the + // user to enter the checkcode and feed it back to us. + // -- MSC4108 Secure channel setup step 6 + let (tx, rx) = tokio::sync::oneshot::channel(); + self.state.set(GrantLoginProgress::EstablishingSecureChannel( + GeneratedQrProgress::QrScanned(CheckCodeSender::new(tx)), + )); + let check_code = + rx.await.map_err(|_| SecureChannelError::CannotReceiveCheckCode)?; + + // Use the checkcode to verify that the channel is actually secure. + // -- MSC4108 Secure channel setup step 7 + let mut channel = channel.confirm(check_code)?; + + // Since the QR code was generated on this existing device, the new device can + // derive the homeserver to use for logging in from the QR code and we + // don't need to send the m.login.protocols message. + // -- MSC4108 OAuth 2.0 login step 1 + + // Proceed with granting the login. + // -- MSC4108 OAuth 2.0 login remaining steps + finish_login_grant( + self.client, + &mut channel, + self.device_creation_timeout, + &secrets_bundle, + &self.state, + ) + .await + }; + + tokio::select! { + // Give priority to cancellation if both updates occur at the same time. + biased; + _ = cancel.cancelled() => { + Err(QRCodeGrantLoginError::Cancelled) + } + result = grant => result + } }) } } @@ -822,7 +858,7 @@ mod test { // Prepare the login granting future. let oauth = alice.oauth(); let grant = oauth - .grant_login_with_qr_code() + .grant_login_with_qr_code(CancellationToken::new()) .device_creation_timeout(Duration::from_secs(2)) .generate(); let secrets_bundle = export_secrets_bundle(&alice) @@ -973,7 +1009,7 @@ mod test { // Prepare the login granting future using the QR code. let oauth = alice.oauth(); let grant = oauth - .grant_login_with_qr_code() + .grant_login_with_qr_code(CancellationToken::new()) .device_creation_timeout(Duration::from_secs(2)) .scan(&qr_code_data); let secrets_bundle = export_secrets_bundle(&alice) @@ -1102,7 +1138,7 @@ mod test { // Prepare the login granting future using the QR code. let oauth = alice.oauth(); let grant = oauth - .grant_login_with_qr_code() + .grant_login_with_qr_code(CancellationToken::new()) .device_creation_timeout(Duration::from_secs(2)) .scan(&qr_code_data); let secrets_bundle = export_secrets_bundle(&alice) @@ -1214,7 +1250,7 @@ mod test { // Prepare the login granting future. let oauth = alice.oauth(); let grant = oauth - .grant_login_with_qr_code() + .grant_login_with_qr_code(CancellationToken::new()) .device_creation_timeout(Duration::from_secs(2)) .generate(); let (qr_code_tx, qr_code_rx) = oneshot::channel(); @@ -1347,7 +1383,7 @@ mod test { // Prepare the login granting future using the QR code. let oauth = alice.oauth(); let grant = oauth - .grant_login_with_qr_code() + .grant_login_with_qr_code(CancellationToken::new()) .device_creation_timeout(Duration::from_secs(2)) .scan(&qr_code_data); let (checkcode_tx, checkcode_rx) = oneshot::channel(); @@ -1460,7 +1496,7 @@ mod test { // Prepare the login granting future. let oauth = alice.oauth(); let grant = oauth - .grant_login_with_qr_code() + .grant_login_with_qr_code(CancellationToken::new()) .device_creation_timeout(Duration::from_secs(2)) .generate(); let (qr_code_tx, qr_code_rx) = oneshot::channel(); @@ -1597,7 +1633,7 @@ mod test { // Prepare the login granting future using the QR code. let oauth = alice.oauth(); let grant = oauth - .grant_login_with_qr_code() + .grant_login_with_qr_code(CancellationToken::new()) .device_creation_timeout(Duration::from_secs(2)) .scan(&qr_code_data); let (checkcode_tx, checkcode_rx) = oneshot::channel(); @@ -1705,7 +1741,7 @@ mod test { // Prepare the login granting future. let oauth = alice.oauth(); let grant = oauth - .grant_login_with_qr_code() + .grant_login_with_qr_code(CancellationToken::new()) .device_creation_timeout(Duration::from_secs(2)) .generate(); let (qr_code_tx, qr_code_rx) = oneshot::channel(); @@ -1852,7 +1888,7 @@ mod test { // Prepare the login granting future using the QR code. let oauth = alice.oauth(); let grant = oauth - .grant_login_with_qr_code() + .grant_login_with_qr_code(CancellationToken::new()) .device_creation_timeout(Duration::from_secs(2)) .scan(&qr_code_data); let (checkcode_tx, checkcode_rx) = oneshot::channel(); @@ -1959,7 +1995,7 @@ mod test { // Prepare the login granting future. let oauth = alice.oauth(); let grant = oauth - .grant_login_with_qr_code() + .grant_login_with_qr_code(CancellationToken::new()) .device_creation_timeout(Duration::from_secs(2)) .generate(); @@ -2041,7 +2077,7 @@ mod test { // Prepare the login granting future using the QR code. let oauth = alice.oauth(); let grant = oauth - .grant_login_with_qr_code() + .grant_login_with_qr_code(CancellationToken::new()) .device_creation_timeout(Duration::from_secs(2)) .scan(&qr_code_data); @@ -2114,7 +2150,7 @@ mod test { // Prepare the login granting future. let oauth = alice.oauth(); let grant = oauth - .grant_login_with_qr_code() + .grant_login_with_qr_code(CancellationToken::new()) .device_creation_timeout(Duration::from_secs(2)) .generate(); let (qr_code_tx, qr_code_rx) = oneshot::channel(); @@ -2244,7 +2280,7 @@ mod test { // Prepare the login granting future using the QR code. let oauth = alice.oauth(); let grant = oauth - .grant_login_with_qr_code() + .grant_login_with_qr_code(CancellationToken::new()) .device_creation_timeout(Duration::from_secs(2)) .scan(&qr_code_data); let (checkcode_tx, checkcode_rx) = oneshot::channel(); @@ -2355,7 +2391,7 @@ mod test { // Prepare the login granting future. let oauth = alice.oauth(); let grant = oauth - .grant_login_with_qr_code() + .grant_login_with_qr_code(CancellationToken::new()) .device_creation_timeout(Duration::from_secs(2)) .generate(); let (qr_code_tx, qr_code_rx) = oneshot::channel(); @@ -2507,7 +2543,7 @@ mod test { // Prepare the login granting future using the QR code. let oauth = alice.oauth(); let grant = oauth - .grant_login_with_qr_code() + .grant_login_with_qr_code(CancellationToken::new()) .device_creation_timeout(Duration::from_secs(2)) .scan(&qr_code_data); let (checkcode_tx, checkcode_rx) = oneshot::channel(); @@ -2628,7 +2664,7 @@ mod test { // Prepare the login granting future. let oauth = alice.oauth(); let grant = oauth - .grant_login_with_qr_code() + .grant_login_with_qr_code(CancellationToken::new()) .device_creation_timeout(Duration::from_secs(2)) .generate(); let (qr_code_tx, qr_code_rx) = oneshot::channel(); @@ -2781,7 +2817,7 @@ mod test { // Prepare the login granting future using the QR code. let oauth = alice.oauth(); let grant = oauth - .grant_login_with_qr_code() + .grant_login_with_qr_code(CancellationToken::new()) .device_creation_timeout(Duration::from_secs(2)) .scan(&qr_code_data); let (checkcode_tx, checkcode_rx) = oneshot::channel(); @@ -2893,7 +2929,7 @@ mod test { // Prepare the login granting future. let oauth = alice.oauth(); let grant = oauth - .grant_login_with_qr_code() + .grant_login_with_qr_code(CancellationToken::new()) .device_creation_timeout(Duration::from_secs(2)) .generate(); let (qr_code_tx, qr_code_rx) = oneshot::channel(); @@ -3023,7 +3059,7 @@ mod test { // Prepare the login granting future using the QR code. let oauth = alice.oauth(); let grant = oauth - .grant_login_with_qr_code() + .grant_login_with_qr_code(CancellationToken::new()) .device_creation_timeout(Duration::from_secs(2)) .scan(&qr_code_data); let (checkcode_tx, checkcode_rx) = oneshot::channel(); diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs index a642aeddbfd..68ae695a9e7 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs @@ -26,6 +26,7 @@ use ruma::{ OwnedDeviceId, api::client::discovery::get_authorization_server_metadata::v1::AuthorizationServerMetadata, }; +use tokio_util::sync::CancellationToken; use tracing::trace; use vodozemac::Curve25519PublicKey; #[cfg(doc)] @@ -274,6 +275,7 @@ pub struct LoginWithQrCode<'a> { registration_data: Option<&'a ClientRegistrationData>, qr_code_data: &'a QrCodeData, state: SharedObservable>, + cancel: CancellationToken, } impl LoginWithQrCode<'_> { @@ -293,31 +295,43 @@ impl<'a> IntoFuture for LoginWithQrCode<'a> { fn into_future(self) -> Self::IntoFuture { Box::pin(async move { - // Before we get here, the other device has created a new rendezvous session - // and presented a QR code which this device has scanned. - // -- MSC4108 Secure channel setup steps 1-3 - - // First things first, establish the secure channel. Since we're the one that - // scanned the QR code, we're certain that the secure channel is - // secure, under the assumption that we didn't scan the wrong QR code. - // -- MSC4108 Secure channel setup steps 3-5 - let channel = self.establish_secure_channel().await?; - - trace!("Established the secure channel."); - - // The other side isn't yet sure that it's talking to the right device, show - // a check code so they can confirm. - // -- MSC4108 Secure channel setup step 6 - let check_code = channel.check_code().to_owned(); - self.state.set(LoginProgress::EstablishingSecureChannel(QrProgress { check_code })); - - // The user now enters the checkcode on the other device which verifies it - // and will only facilitate the login if the code matches. - // -- MSC4108 Secure channel setup step 7 - - // Now attempt to finish the login. - // -- MSC4108 OAuth 2.0 login all steps - finish_login(self.client, channel, self.registration_data, self.state).await + let cancel = self.cancel.clone(); + let login = async move { + // Before we get here, the other device has created a new rendezvous session + // and presented a QR code which this device has scanned. + // -- MSC4108 Secure channel setup steps 1-3 + + // First things first, establish the secure channel. Since we're the one that + // scanned the QR code, we're certain that the secure channel is + // secure, under the assumption that we didn't scan the wrong QR code. + // -- MSC4108 Secure channel setup steps 3-5 + let channel = self.establish_secure_channel().await?; + + trace!("Established the secure channel."); + + // The other side isn't yet sure that it's talking to the right device, show + // a check code so they can confirm. + // -- MSC4108 Secure channel setup step 6 + let check_code = channel.check_code().to_owned(); + self.state.set(LoginProgress::EstablishingSecureChannel(QrProgress { check_code })); + + // The user now enters the checkcode on the other device which verifies it + // and will only facilitate the login if the code matches. + // -- MSC4108 Secure channel setup step 7 + + // Now attempt to finish the login. + // -- MSC4108 OAuth 2.0 login all steps + finish_login(self.client, channel, self.registration_data, self.state).await + }; + + tokio::select! { + // Give priority to cancellation if both updates occur at the same time. + biased; + _ = cancel.cancelled() => { + Err(QRCodeLoginError::Cancelled) + } + result = login => result + } }) } } @@ -327,8 +341,15 @@ impl<'a> LoginWithQrCode<'a> { client: &'a Client, qr_code_data: &'a QrCodeData, registration_data: Option<&'a ClientRegistrationData>, + cancel: CancellationToken, ) -> LoginWithQrCode<'a> { - LoginWithQrCode { client, registration_data, qr_code_data, state: Default::default() } + LoginWithQrCode { + client, + registration_data, + qr_code_data, + state: Default::default(), + cancel, + } } async fn establish_secure_channel( @@ -354,6 +375,7 @@ pub struct LoginWithGeneratedQrCode<'a> { client: &'a Client, registration_data: Option<&'a ClientRegistrationData>, state: SharedObservable>, + cancel: CancellationToken, } impl LoginWithGeneratedQrCode<'_> { @@ -374,59 +396,71 @@ impl<'a> IntoFuture for LoginWithGeneratedQrCode<'a> { fn into_future(self) -> Self::IntoFuture { Box::pin(async move { - // Establish and verify the secure channel. - // -- MSC4108 Secure channel setup all steps - let mut channel = self.establish_secure_channel().await?; - - trace!("Established the secure channel."); - - // Wait for the other device to send us the m.login.protocols message - // so that we can discover the homeserver to use for logging in. - // -- MSC4108 OAuth 2.0 login step 1 - let message = channel.receive_json().await?; - - // Verify that the device authorization grant is supported and extract - // the homeserver URL. - let homeserver = match message { - QrAuthMessage::LoginProtocols { protocols, homeserver } => { - if !protocols.contains(&LoginProtocolType::DeviceAuthorizationGrant) { - channel - .send_json(QrAuthMessage::LoginFailure { + let cancel = self.cancel.clone(); + let login = async move { + // Establish and verify the secure channel. + // -- MSC4108 Secure channel setup all steps + let mut channel = self.establish_secure_channel().await?; + + trace!("Established the secure channel."); + + // Wait for the other device to send us the m.login.protocols message + // so that we can discover the homeserver to use for logging in. + // -- MSC4108 OAuth 2.0 login step 1 + let message = channel.receive_json().await?; + + // Verify that the device authorization grant is supported and extract + // the homeserver URL. + let homeserver = match message { + QrAuthMessage::LoginProtocols { protocols, homeserver } => { + if !protocols.contains(&LoginProtocolType::DeviceAuthorizationGrant) { + channel + .send_json(QrAuthMessage::LoginFailure { + reason: LoginFailureReason::UnsupportedProtocol, + homeserver: None, + }) + .await?; + + return Err(QRCodeLoginError::LoginFailure { reason: LoginFailureReason::UnsupportedProtocol, homeserver: None, - }) - .await?; + }); + } - return Err(QRCodeLoginError::LoginFailure { - reason: LoginFailureReason::UnsupportedProtocol, - homeserver: None, - }); + homeserver } + _ => { + send_unexpected_message_error(&mut channel).await?; - homeserver + return Err(QRCodeLoginError::UnexpectedMessage { + expected: "m.login.protocols", + received: message, + }); + } + }; + + // Change the login homeserver if it is different from the server hosting the + // secure channel. + if self.client.homeserver() != homeserver { + self.client + .switch_homeserver_and_re_resolve_well_known(homeserver) + .await + .map_err(QRCodeLoginError::ServerReset)?; } - _ => { - send_unexpected_message_error(&mut channel).await?; - return Err(QRCodeLoginError::UnexpectedMessage { - expected: "m.login.protocols", - received: message, - }); - } + // Proceed with logging in. + // -- MSC4108 OAuth 2.0 login remaining steps + finish_login(self.client, channel, self.registration_data, self.state).await }; - // Change the login homeserver if it is different from the server hosting the - // secure channel. - if self.client.homeserver() != homeserver { - self.client - .switch_homeserver_and_re_resolve_well_known(homeserver) - .await - .map_err(QRCodeLoginError::ServerReset)?; + tokio::select! { + // Give priority to cancellation if both updates occur at the same time. + biased; + _ = cancel.cancelled() => { + Err(QRCodeLoginError::Cancelled) + } + result = login => result } - - // Proceed with logging in. - // -- MSC4108 OAuth 2.0 login remaining steps - finish_login(self.client, channel, self.registration_data, self.state).await }) } } @@ -435,8 +469,9 @@ impl<'a> LoginWithGeneratedQrCode<'a> { pub(crate) fn new( client: &'a Client, registration_data: Option<&'a ClientRegistrationData>, + cancel: CancellationToken, ) -> Self { - Self { client, registration_data, state: Default::default() } + Self { client, registration_data, state: Default::default(), cancel } } async fn establish_secure_channel( @@ -517,6 +552,11 @@ mod test { LetSessionExpire, } + enum BobBehaviour { + HappyPath, + CancelWhileWaitingForToken, + } + /// The possible token responses. enum TokenResponse { Ok, @@ -633,7 +673,9 @@ mod test { let oauth = bob.oauth(); let registration_data = mock_client_metadata().into(); - let login_bob = oauth.login_with_qr_code(Some(®istration_data)).scan(&qr_code); + let login_bob = oauth + .login_with_qr_code(Some(®istration_data), CancellationToken::new()) + .scan(&qr_code); let mut updates = login_bob.subscribe_to_progress(); let updates_task = spawn(async move { @@ -802,7 +844,9 @@ mod test { let registration_data = mock_client_metadata().into(); let bob_oauth = bob.oauth(); - let bob_login = bob_oauth.login_with_qr_code(Some(®istration_data)).generate(); + let bob_login = bob_oauth + .login_with_qr_code(Some(®istration_data), CancellationToken::new()) + .generate(); let mut bob_updates = bob_login.subscribe_to_progress(); let updates_task = spawn(async move { @@ -919,7 +963,9 @@ mod test { let registration_data = mock_client_metadata().into(); let bob_oauth = bob.oauth(); - let bob_login = bob_oauth.login_with_qr_code(Some(®istration_data)).generate(); + let bob_login = bob_oauth + .login_with_qr_code(Some(®istration_data), CancellationToken::new()) + .generate(); let mut bob_updates = bob_login.subscribe_to_progress(); let updates_task = spawn(async move { @@ -978,6 +1024,7 @@ mod test { async fn test_failure( token_response: TokenResponse, alice_behavior: AliceBehaviour, + bob_behavior: BobBehaviour, ) -> Result<(), QRCodeLoginError> { let server = MatrixMockServer::new().await; let expiration = match alice_behavior { @@ -1049,7 +1096,9 @@ mod test { let oauth = bob.oauth(); let registration_data = mock_client_metadata().into(); - let login_bob = oauth.login_with_qr_code(Some(®istration_data)).scan(&qr_code); + let cancel = CancellationToken::new(); + let login_bob = + oauth.login_with_qr_code(Some(®istration_data), cancel.clone()).scan(&qr_code); let mut updates = login_bob.subscribe_to_progress(); let _updates_task = spawn(async move { @@ -1064,6 +1113,11 @@ mod test { .send(check_code) .expect("Bob should be able to send the check code to Alice"); } + LoginProgress::WaitingForToken { .. } => { + if let BobBehaviour::CancelWhileWaitingForToken = bob_behavior { + cancel.cancel(); + } + } LoginProgress::Done => break, _ => (), } @@ -1081,6 +1135,7 @@ mod test { async fn test_generated_failure( token_response: TokenResponse, alice_behavior: AliceBehaviour, + bob_behavior: BobBehaviour, ) -> Result<(), QRCodeLoginError> { let server = MatrixMockServer::new().await; let expiration = match alice_behavior { @@ -1157,7 +1212,9 @@ mod test { let registration_data = mock_client_metadata().into(); let bob_oauth = bob.oauth(); - let bob_login = bob_oauth.login_with_qr_code(Some(®istration_data)).generate(); + let cancel = CancellationToken::new(); + let bob_login = + bob_oauth.login_with_qr_code(Some(®istration_data), cancel.clone()).generate(); let mut bob_updates = bob_login.subscribe_to_progress(); let _updates_task = spawn(async move { @@ -1182,6 +1239,11 @@ mod test { .send(cctx) .expect("Bob should be able to send the qr code code to Alice"); } + LoginProgress::WaitingForToken { .. } => { + if let BobBehaviour::CancelWhileWaitingForToken = bob_behavior { + cancel.cancel(); + } + } LoginProgress::Done => break, _ => (), } @@ -1200,7 +1262,12 @@ mod test { #[async_test] async fn test_qr_login_refused_access_token() { - let result = test_failure(TokenResponse::AccessDenied, AliceBehaviour::HappyPath).await; + let result = test_failure( + TokenResponse::AccessDenied, + AliceBehaviour::HappyPath, + BobBehaviour::HappyPath, + ) + .await; assert_let!(Err(QRCodeLoginError::OAuth(e)) = result); assert_eq!( @@ -1212,8 +1279,12 @@ mod test { #[async_test] async fn test_generated_qr_login_refused_access_token() { - let result = - test_generated_failure(TokenResponse::AccessDenied, AliceBehaviour::HappyPath).await; + let result = test_generated_failure( + TokenResponse::AccessDenied, + AliceBehaviour::HappyPath, + BobBehaviour::HappyPath, + ) + .await; assert_let!(Err(QRCodeLoginError::OAuth(e)) = result); assert_eq!( @@ -1225,7 +1296,12 @@ mod test { #[async_test] async fn test_qr_login_expired_token() { - let result = test_failure(TokenResponse::ExpiredToken, AliceBehaviour::HappyPath).await; + let result = test_failure( + TokenResponse::ExpiredToken, + AliceBehaviour::HappyPath, + BobBehaviour::HappyPath, + ) + .await; assert_let!(Err(QRCodeLoginError::OAuth(e)) = result); assert_eq!( @@ -1237,8 +1313,12 @@ mod test { #[async_test] async fn test_generated_qr_login_expired_token() { - let result = - test_generated_failure(TokenResponse::ExpiredToken, AliceBehaviour::HappyPath).await; + let result = test_generated_failure( + TokenResponse::ExpiredToken, + AliceBehaviour::HappyPath, + BobBehaviour::HappyPath, + ) + .await; assert_let!(Err(QRCodeLoginError::OAuth(e)) = result); assert_eq!( @@ -1250,7 +1330,12 @@ mod test { #[async_test] async fn test_qr_login_declined_protocol() { - let result = test_failure(TokenResponse::Ok, AliceBehaviour::DeclinedProtocol).await; + let result = test_failure( + TokenResponse::Ok, + AliceBehaviour::DeclinedProtocol, + BobBehaviour::HappyPath, + ) + .await; assert_let!(Err(QRCodeLoginError::LoginFailure { reason, .. }) = result); assert_eq!( @@ -1262,8 +1347,12 @@ mod test { #[async_test] async fn test_generated_qr_login_declined_protocol() { - let result = - test_generated_failure(TokenResponse::Ok, AliceBehaviour::DeclinedProtocol).await; + let result = test_generated_failure( + TokenResponse::Ok, + AliceBehaviour::DeclinedProtocol, + BobBehaviour::HappyPath, + ) + .await; assert_let!(Err(QRCodeLoginError::LoginFailure { reason, .. }) = result); assert_eq!( @@ -1275,7 +1364,12 @@ mod test { #[async_test] async fn test_qr_login_unexpected_message() { - let result = test_failure(TokenResponse::Ok, AliceBehaviour::UnexpectedMessage).await; + let result = test_failure( + TokenResponse::Ok, + AliceBehaviour::UnexpectedMessage, + BobBehaviour::HappyPath, + ) + .await; assert_let!(Err(QRCodeLoginError::UnexpectedMessage { expected, .. }) = result); assert_eq!(expected, "m.login.protocol_accepted"); @@ -1283,8 +1377,12 @@ mod test { #[async_test] async fn test_generated_qr_login_unexpected_message() { - let result = - test_generated_failure(TokenResponse::Ok, AliceBehaviour::UnexpectedMessage).await; + let result = test_generated_failure( + TokenResponse::Ok, + AliceBehaviour::UnexpectedMessage, + BobBehaviour::HappyPath, + ) + .await; assert_let!(Err(QRCodeLoginError::UnexpectedMessage { expected, .. }) = result); assert_eq!(expected, "m.login.protocol_accepted"); @@ -1292,9 +1390,12 @@ mod test { #[async_test] async fn test_qr_login_unexpected_message_instead_of_secrets() { - let result = - test_failure(TokenResponse::Ok, AliceBehaviour::UnexpectedMessageInsteadOfSecrets) - .await; + let result = test_failure( + TokenResponse::Ok, + AliceBehaviour::UnexpectedMessageInsteadOfSecrets, + BobBehaviour::HappyPath, + ) + .await; assert_let!(Err(QRCodeLoginError::UnexpectedMessage { expected, .. }) = result); assert_eq!(expected, "m.login.secrets"); @@ -1305,6 +1406,7 @@ mod test { let result = test_generated_failure( TokenResponse::Ok, AliceBehaviour::UnexpectedMessageInsteadOfSecrets, + BobBehaviour::HappyPath, ) .await; @@ -1314,7 +1416,9 @@ mod test { #[async_test] async fn test_qr_login_refuse_secrets() { - let result = test_failure(TokenResponse::Ok, AliceBehaviour::RefuseSecrets).await; + let result = + test_failure(TokenResponse::Ok, AliceBehaviour::RefuseSecrets, BobBehaviour::HappyPath) + .await; assert_let!(Err(QRCodeLoginError::LoginFailure { reason, .. }) = result); assert_eq!(reason, LoginFailureReason::DeviceNotFound); @@ -1322,7 +1426,12 @@ mod test { #[async_test] async fn test_generated_qr_login_refuse_secrets() { - let result = test_generated_failure(TokenResponse::Ok, AliceBehaviour::RefuseSecrets).await; + let result = test_generated_failure( + TokenResponse::Ok, + AliceBehaviour::RefuseSecrets, + BobBehaviour::HappyPath, + ) + .await; assert_let!(Err(QRCodeLoginError::LoginFailure { reason, .. }) = result); assert_eq!(reason, LoginFailureReason::DeviceNotFound); @@ -1330,19 +1439,52 @@ mod test { #[async_test] async fn test_qr_login_session_expired() { - let result = test_failure(TokenResponse::Ok, AliceBehaviour::LetSessionExpire).await; + let result = test_failure( + TokenResponse::Ok, + AliceBehaviour::LetSessionExpire, + BobBehaviour::HappyPath, + ) + .await; assert_matches!(result, Err(QRCodeLoginError::NotFound)); } #[async_test] async fn test_generated_qr_login_session_expired() { - let result = - test_generated_failure(TokenResponse::Ok, AliceBehaviour::LetSessionExpire).await; + let result = test_generated_failure( + TokenResponse::Ok, + AliceBehaviour::LetSessionExpire, + BobBehaviour::HappyPath, + ) + .await; assert_matches!(result, Err(QRCodeLoginError::NotFound)); } + #[async_test] + async fn test_qr_login_cancelled() { + let result = test_failure( + TokenResponse::Ok, + AliceBehaviour::UnexpectedMessageInsteadOfSecrets, + BobBehaviour::CancelWhileWaitingForToken, + ) + .await; + + assert_matches!(result, Err(QRCodeLoginError::Cancelled)); + } + + #[async_test] + async fn test_generated_qr_login_cancelled() { + let result = test_generated_failure( + TokenResponse::Ok, + AliceBehaviour::UnexpectedMessageInsteadOfSecrets, + BobBehaviour::CancelWhileWaitingForToken, + ) + .await; + + assert_matches!(result, Err(QRCodeLoginError::Cancelled)); + } + #[async_test] async fn test_device_authorization_endpoint_missing() { let server = MatrixMockServer::new().await; @@ -1386,7 +1528,9 @@ mod test { let oauth = bob.oauth(); let registration_data = mock_client_metadata().into(); - let login_bob = oauth.login_with_qr_code(Some(®istration_data)).scan(&qr_code); + let login_bob = oauth + .login_with_qr_code(Some(®istration_data), CancellationToken::new()) + .scan(&qr_code); let mut updates = login_bob.subscribe_to_progress(); let _updates_task = spawn(async move { diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs index c1af524b1e9..fbfe5511dd9 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs @@ -120,6 +120,10 @@ pub enum QRCodeLoginError { /// reset the server URL. #[error(transparent)] ServerReset(crate::Error), + + /// The process was cancelled. + #[error("The process was cancelled")] + Cancelled, } impl From for QRCodeLoginError { @@ -187,6 +191,10 @@ pub enum QRCodeGrantLoginError { /// The reason, as signaled by the other device, for the login failure. reason: LoginFailureReason, }, + + /// The process was cancelled. + #[error("The process was cancelled")] + Cancelled, } impl From for QRCodeGrantLoginError { diff --git a/examples/qr-login/Cargo.toml b/examples/qr-login/Cargo.toml index ed93acaee28..5854d020d2d 100644 --- a/examples/qr-login/Cargo.toml +++ b/examples/qr-login/Cargo.toml @@ -18,6 +18,7 @@ anyhow.workspace = true clap = { workspace = true, features = ["derive"] } futures-util.workspace = true tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } +tokio-util.workspace = true tracing-subscriber.workspace = true url.workspace = true diff --git a/examples/qr-login/src/main.rs b/examples/qr-login/src/main.rs index 84a521c2c7e..fbb58f413ab 100644 --- a/examples/qr-login/src/main.rs +++ b/examples/qr-login/src/main.rs @@ -11,6 +11,7 @@ use matrix_sdk::{ }, ruma::serde::Raw, }; +use tokio_util::sync::CancellationToken; use url::Url; /// A command line example showcasing how to login using a QR code. @@ -117,7 +118,8 @@ async fn login(proxy: Option) -> Result<()> { let registration_data = client_metadata().into(); let oauth = client.oauth(); - let login_client = oauth.login_with_qr_code(Some(®istration_data)).scan(&data); + let cancel = CancellationToken::new(); + let login_client = oauth.login_with_qr_code(Some(®istration_data), cancel).scan(&data); let mut subscriber = login_client.subscribe_to_progress(); let task = tokio::spawn(async move { From 576870dfb48cc7a5fb6a9d08aa0b7a07a09f2235 Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Mon, 6 Jul 2026 19:18:19 +0200 Subject: [PATCH 06/10] fixup! fix(ffi): add methods to make QR login handlers exit cooperatively Add tests for the grant case Signed-off-by: Johannes Marbach --- .../src/authentication/oauth/qrcode/grant.rs | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs index 2e28784c760..cba1be3b345 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs @@ -1208,6 +1208,241 @@ mod test { bob_task.await.expect("Bob's task should finish"); } + #[async_test] + async fn test_grant_login_with_generated_qr_code_grant_cancelled() { + let server = MatrixMockServer::new().await; + let rendezvous_server = Arc::new( + MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX).await, + ); + debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url); + + server.mock_upload_keys().ok().expect(1).named("upload_keys").mount().await; + server + .mock_upload_cross_signing_keys() + .ok() + .expect(1) + .named("upload_xsigning_keys") + .mount() + .await; + server + .mock_upload_cross_signing_signatures() + .ok() + .expect(1) + .named("upload_xsigning_signatures") + .mount() + .await; + + // Create the existing client (Alice). + let user_id = owned_user_id!("@alice:example.org"); + let device_id = owned_device_id!("ALICE_DEVICE"); + let alice = server + .client_builder_for_crypto_end_to_end(&user_id, &device_id) + .logged_in_with_oauth() + .build() + .await; + alice + .encryption() + .bootstrap_cross_signing(None) + .await + .expect("Alice should be able to set up cross signing"); + + // Prepare the login granting future. + let oauth = alice.oauth(); + let cancel = CancellationToken::new(); + let grant = oauth + .grant_login_with_qr_code(cancel.clone()) + .device_creation_timeout(Duration::from_secs(2)) + .generate(); + let (qr_code_tx, qr_code_rx) = oneshot::channel(); + let (checkcode_tx, checkcode_rx) = oneshot::channel(); + + // Spawn the updates task. + let mut updates = grant.subscribe_to_progress(); + let mut state = grant.state.get(); + assert_matches!(state.clone(), GrantLoginProgress::Starting); + let updates_task = spawn(async move { + let mut qr_code_tx = Some(qr_code_tx); + let mut checkcode_rx = Some(checkcode_rx); + + while let Some(update) = updates.next().await { + match &update { + GrantLoginProgress::Starting => { + assert_matches!(state, GrantLoginProgress::Starting); + } + GrantLoginProgress::EstablishingSecureChannel( + GeneratedQrProgress::QrReady(qr_code_data), + ) => { + assert_matches!(state, GrantLoginProgress::Starting); + qr_code_tx + .take() + .expect("The QR code should only be forwarded once") + .send(qr_code_data.clone()) + .expect("Alice should be able to forward the QR code"); + } + GrantLoginProgress::EstablishingSecureChannel( + GeneratedQrProgress::QrScanned(checkcode_sender), + ) => { + assert_matches!( + state, + GrantLoginProgress::EstablishingSecureChannel( + GeneratedQrProgress::QrReady(_) + ) + ); + let checkcode = checkcode_rx + .take() + .expect("The checkcode should only be forwarded once") + .await + .expect("Alice should receive the checkcode"); + checkcode_sender + .send(checkcode) + .await + .expect("Alice should be able to forward the checkcode"); + cancel.cancel(); + break; + } + _ => { + panic!("Alice should abort the process"); + } + } + state = update; + } + }); + + // Let Bob request the login and run through the process. + let rendezvous_server_clone = rendezvous_server.clone(); + let bob_task = spawn(async move { + request_login_with_scanned_qr_code( + BobBehaviour::UnexpectedMessageInsteadOfLoginProtocol, + qr_code_rx, + checkcode_tx, + None, + &rendezvous_server_clone, + None, + None, + ) + .await; + }); + + // Wait for all tasks to finish / fail. + assert_matches!( + grant.await, + Err(QRCodeGrantLoginError::Cancelled), + "Alice should abort the login with expected error" + ); + updates_task.await.expect("Alice should run through all progress states"); + bob_task.await.expect("Bob's task should finish"); + } + + #[async_test] + async fn test_grant_login_with_scanned_qr_code_grant_cancelled() { + let server = MatrixMockServer::new().await; + let rendezvous_server = Arc::new( + MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX).await, + ); + debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url); + + server.mock_upload_keys().ok().expect(1).named("upload_keys").mount().await; + server + .mock_upload_cross_signing_keys() + .ok() + .expect(1) + .named("upload_xsigning_keys") + .mount() + .await; + server + .mock_upload_cross_signing_signatures() + .ok() + .expect(1) + .named("upload_xsigning_signatures") + .mount() + .await; + + // Create a secure channel on the new client (Bob) and extract the QR code. + let client = HttpClient::new(reqwest::Client::new(), Default::default()); + let channel = SecureChannel::login(client, &rendezvous_server.homeserver_url) + .await + .expect("Bob should be able to create a secure channel."); + let qr_code_data = channel.qr_code_data().clone(); + + // Create the existing client (Alice). + let user_id = owned_user_id!("@alice:example.org"); + let device_id = owned_device_id!("ALICE_DEVICE"); + let alice = server + .client_builder_for_crypto_end_to_end(&user_id, &device_id) + .logged_in_with_oauth() + .build() + .await; + alice + .encryption() + .bootstrap_cross_signing(None) + .await + .expect("Alice should be able to set up cross signing"); + + // Prepare the login granting future using the QR code. + let oauth = alice.oauth(); + let cancel = CancellationToken::new(); + let grant = oauth + .grant_login_with_qr_code(cancel.clone()) + .device_creation_timeout(Duration::from_secs(2)) + .scan(&qr_code_data); + let (checkcode_tx, checkcode_rx) = oneshot::channel(); + + // Spawn the updates task. + let mut updates = grant.subscribe_to_progress(); + let mut state = grant.state.get(); + assert_matches!(state.clone(), GrantLoginProgress::Starting); + let updates_task = spawn(async move { + let mut checkcode_tx = Some(checkcode_tx); + + while let Some(update) = updates.next().await { + match &update { + GrantLoginProgress::Starting => { + assert_matches!(state, GrantLoginProgress::Starting); + } + GrantLoginProgress::EstablishingSecureChannel(QrProgress { check_code }) => { + assert_matches!(state, GrantLoginProgress::Starting); + checkcode_tx + .take() + .expect("The checkcode should only be forwarded once") + .send(check_code.to_digit()) + .expect("Alice should be able to forward the checkcode"); + cancel.cancel(); + break; + } + _ => { + panic!("Alice should abort the process"); + } + } + state = update; + } + }); + + let rendezvous_server_clone = rendezvous_server.clone(); + // Let Bob request the login and run through the process. + let bob_task = spawn(async move { + request_login_with_generated_qr_code( + BobBehaviour::UnexpectedMessageInsteadOfLoginProtocol, + channel, + checkcode_rx, + None, + &rendezvous_server_clone, + alice.homeserver(), + None, + None, + ) + .await; + }); + + // Wait for all tasks to finish / fail. + assert_matches!( + grant.await, + Err(QRCodeGrantLoginError::Cancelled), + "Alice should abort the login with expected error" + ); + updates_task.await.expect("Alice should run through all progress states"); + bob_task.await.expect("Bob's task should finish"); + } + #[async_test] async fn test_grant_login_with_generated_qr_code_unexpected_message_instead_of_login_protocol() { From 35fedd40d0824da9bf961784c70821836e339ddc Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Mon, 6 Jul 2026 19:29:48 +0200 Subject: [PATCH 07/10] fixup! fix(ffi): add methods to make QR login handlers exit cooperatively Update changelogs Signed-off-by: Johannes Marbach --- bindings/matrix-sdk-ffi/changelog.d/6677.changed.md | 2 ++ bindings/matrix-sdk-ffi/changelog.d/6677.fixed.md | 1 - crates/matrix-sdk/changelog.d/6677.changed.md | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 bindings/matrix-sdk-ffi/changelog.d/6677.changed.md delete mode 100644 bindings/matrix-sdk-ffi/changelog.d/6677.fixed.md create mode 100644 crates/matrix-sdk/changelog.d/6677.changed.md diff --git a/bindings/matrix-sdk-ffi/changelog.d/6677.changed.md b/bindings/matrix-sdk-ffi/changelog.d/6677.changed.md new file mode 100644 index 00000000000..e0747f61492 --- /dev/null +++ b/bindings/matrix-sdk-ffi/changelog.d/6677.changed.md @@ -0,0 +1,2 @@ +Add `LoginWithQrCodeHandler::abort` and `GrantLoginWithQrCodeHandler::abort` for +requesting cooperative cancellation of the handler. diff --git a/bindings/matrix-sdk-ffi/changelog.d/6677.fixed.md b/bindings/matrix-sdk-ffi/changelog.d/6677.fixed.md deleted file mode 100644 index 3686e965983..00000000000 --- a/bindings/matrix-sdk-ffi/changelog.d/6677.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Add methods to make QR login handlers exit cooperatively. diff --git a/crates/matrix-sdk/changelog.d/6677.changed.md b/crates/matrix-sdk/changelog.d/6677.changed.md new file mode 100644 index 00000000000..4dc8cbd4fcc --- /dev/null +++ b/crates/matrix-sdk/changelog.d/6677.changed.md @@ -0,0 +1,3 @@ +[**breaking**] Add cancellation token argument to `OAuth::login_with_qr_code` and +`OAuth::grant_login_with_qr_code` for requesting cooperative cancellation of the +process. From 13fe76d253b78039e2545bf30c835d16a23d832e Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Mon, 6 Jul 2026 19:40:56 +0200 Subject: [PATCH 08/10] fixup! fix(ffi): add methods to make QR login handlers exit cooperatively Add missing use declarations Signed-off-by: Johannes Marbach --- crates/matrix-sdk/src/authentication/oauth/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/matrix-sdk/src/authentication/oauth/mod.rs b/crates/matrix-sdk/src/authentication/oauth/mod.rs index 9e678ca6aad..e374c63da0e 100644 --- a/crates/matrix-sdk/src/authentication/oauth/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/mod.rs @@ -1414,6 +1414,7 @@ impl<'a> LoginWithQrCodeBuilder<'a> { /// ruma::serde::Raw, /// Client, /// }; + /// use tokio_util::sync::CancellationToken; /// # fn client_metadata() -> Raw { unimplemented!() } /// # _ = async { /// # let bytes = unimplemented!(); @@ -1499,6 +1500,7 @@ impl<'a> LoginWithQrCodeBuilder<'a> { /// Client, /// }; /// use std::{error::Error, io::stdin}; + /// use tokio_util::sync::CancellationToken; /// # fn client_metadata() -> Raw { unimplemented!() } /// # _ = async { /// // Build the client as usual. From 3167bb4d582b4dd9a192e59022fcb289cb441da3 Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Mon, 6 Jul 2026 19:50:27 +0200 Subject: [PATCH 09/10] fixup! fix(ffi): add methods to make QR login handlers exit cooperatively Add more missing use declarations Signed-off-by: Johannes Marbach --- crates/matrix-sdk/src/authentication/oauth/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/matrix-sdk/src/authentication/oauth/mod.rs b/crates/matrix-sdk/src/authentication/oauth/mod.rs index e374c63da0e..9820e183547 100644 --- a/crates/matrix-sdk/src/authentication/oauth/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/mod.rs @@ -1618,6 +1618,7 @@ impl<'a> GrantLoginWithQrCodeBuilder<'a> { /// } /// }; /// use std::{error::Error, io::stdin}; + /// use tokio_util::sync::CancellationToken; /// # _ = async { /// # let bytes = unimplemented!(); /// // You'll need to use a different library to scan and extract the raw bytes from the QR @@ -1699,6 +1700,7 @@ impl<'a> GrantLoginWithQrCodeBuilder<'a> { /// } /// }; /// use std::{error::Error, io::stdin}; + /// use tokio_util::sync::CancellationToken; /// # _ = async { /// // Build the client as usual. /// let client = Client::builder() From 010794522b5c1e957ec102187f3df9f713c41ded Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Mon, 6 Jul 2026 19:57:03 +0200 Subject: [PATCH 10/10] fixup! fix(ffi): add methods to make QR login handlers exit cooperatively Add missing semicolon Signed-off-by: Johannes Marbach --- crates/matrix-sdk/src/authentication/oauth/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk/src/authentication/oauth/mod.rs b/crates/matrix-sdk/src/authentication/oauth/mod.rs index 9820e183547..661d3a97e74 100644 --- a/crates/matrix-sdk/src/authentication/oauth/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/mod.rs @@ -1637,7 +1637,7 @@ impl<'a> GrantLoginWithQrCodeBuilder<'a> { /// // Subscribing to the progress is necessary to capture /// // the checkcode in order to display it to the other device and to obtain the verification URL to /// // open it in a browser so the user can consent to the new login. - /// let cancel = CancellationToken::new() + /// let cancel = CancellationToken::new(); /// let mut grant = oauth.grant_login_with_qr_code(cancel).scan(&qr_code_data); /// let mut progress = grant.subscribe_to_progress(); ///