From 6cb59d081a8af261bd3fca3bfd1271e4af5cd5ce Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Wed, 24 Sep 2025 12:59:53 +0200 Subject: [PATCH 1/6] Add generated QR code login flow Signed-off-by: Johannes Marbach --- bindings/matrix-sdk-ffi/src/qr_code.rs | 5 +- crates/matrix-sdk/CHANGELOG.md | 3 + .../src/authentication/oauth/mod.rs | 27 +- .../src/authentication/oauth/qrcode/login.rs | 518 +++++++++++++++++- .../src/authentication/oauth/qrcode/mod.rs | 10 +- .../oauth/qrcode/rendezvous_channel.rs | 1 - .../oauth/qrcode/secure_channel.rs | 41 +- 7 files changed, 577 insertions(+), 28 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/qr_code.rs b/bindings/matrix-sdk-ffi/src/qr_code.rs index 4a2e9666863..f2e0c13d0b2 100644 --- a/bindings/matrix-sdk-ffi/src/qr_code.rs +++ b/bindings/matrix-sdk-ffi/src/qr_code.rs @@ -104,7 +104,10 @@ impl From for HumanQrLoginError { | SecureChannelError::RendezvousChannel(_) => HumanQrLoginError::Unknown, SecureChannelError::SecureChannelMessage { .. } | SecureChannelError::Ecies(_) - | SecureChannelError::InvalidCheckCode => HumanQrLoginError::ConnectionInsecure, + | SecureChannelError::InvalidCheckCode + | SecureChannelError::CannotReceiveCheckCode => { + HumanQrLoginError::ConnectionInsecure + } SecureChannelError::InvalidIntent => HumanQrLoginError::OtherDeviceNotSignedIn, }, diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index b46512f5938..504cdada676 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -79,6 +79,9 @@ All notable changes to this project will be documented in this file. begins ([#5678](https://github.com/matrix-org/matrix-rust-sdk/pull/5678). - Make `PaginationTokens` `pub`, as well as its `previous` and `next` tokens so they can be assigned from other files ([#5678](https://github.com/matrix-org/matrix-rust-sdk/pull/5678). +- Add `OAuth::login_with_generated_qr_code` for generating a QR code on a new device + and logging it in with the help of an existing device scanning the code. + ([#5711](https://github.com/matrix-org/matrix-rust-sdk/pull/5711)) ### Refactor diff --git a/crates/matrix-sdk/src/authentication/oauth/mod.rs b/crates/matrix-sdk/src/authentication/oauth/mod.rs index 402de16a07f..a06128877aa 100644 --- a/crates/matrix-sdk/src/authentication/oauth/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/mod.rs @@ -214,7 +214,7 @@ mod tests; #[cfg(feature = "e2e-encryption")] use self::cross_process::{CrossProcessRefreshLockGuard, CrossProcessRefreshManager}; #[cfg(feature = "e2e-encryption")] -use self::qrcode::LoginWithQrCode; +use self::qrcode::{LoginWithGeneratedQrCode, LoginWithQrCode}; pub use self::{ account_management_url::{AccountManagementActionFull, AccountManagementUrlBuilder}, auth_code_builder::{OAuthAuthCodeUrlBuilder, OAuthAuthorizationData}, @@ -459,6 +459,31 @@ impl OAuth { LoginWithQrCode::new(&self.client, data, registration_data) } + /// Log in using a generated QR code. + /// + /// This method allows you to log in with a QR code, this device + /// needs to display the QR code by calling this method so the existing + /// device can scan it and grant the log in. + /// + /// A successful login using this method will automatically mark the device + /// as verified and transfer all end-to-end encryption related secrets, like + /// the private cross-signing keys and the backup key from the existing + /// device to the new device. + /// + /// # Arguments + /// + /// * `registration_data` - The data to restore or register the client with + /// the server. If this is not provided, an error will occur unless + /// [`OAuth::register_client()`] or [`OAuth::restore_registered_client()`] + /// was called previously. + #[cfg(feature = "e2e-encryption")] + pub fn login_with_generated_qr_code<'a>( + &'a self, + registration_data: Option<&'a ClientRegistrationData>, + ) -> LoginWithGeneratedQrCode<'a> { + LoginWithGeneratedQrCode::new(&self.client, registration_data) + } + /// Restore or register the OAuth 2.0 client for the server with the given /// metadata, with the given optional [`ClientRegistrationData`]. /// diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs index 9725a1be694..2383b4c8945 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs @@ -12,13 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::future::IntoFuture; +use std::{future::IntoFuture, sync::Arc}; use eyeball::SharedObservable; use futures_core::Stream; use matrix_sdk_base::{ SessionMeta, boxed_into_future, - crypto::types::qr_login::{QrCodeData, QrCodeMode}, + crypto::types::qr_login::{QrCodeData, QrCodeMode, QrCodeModeData}, store::RoomLoadSettings, }; use oauth2::{DeviceCodeErrorResponseType, StandardDeviceAuthorizationResponse}; @@ -26,17 +26,18 @@ use ruma::{ OwnedDeviceId, api::client::discovery::get_authorization_server_metadata::v1::AuthorizationServerMetadata, }; +use tokio::sync::Mutex; use tracing::trace; use vodozemac::{Curve25519PublicKey, ecies::CheckCode}; use super::{ DeviceAuthorizationOAuthError, QRCodeLoginError, SecureChannelError, messages::{LoginFailureReason, QrAuthMessage}, - secure_channel::EstablishedSecureChannel, + secure_channel::{EstablishedSecureChannel, SecureChannel}, }; use crate::{ Client, - authentication::oauth::{ClientRegistrationData, OAuth, OAuthError}, + authentication::oauth::{ClientRegistrationData, OAuth, OAuthError, qrcode::LoginProtocolType}, }; async fn send_unexpected_message_error( @@ -243,9 +244,8 @@ pub enum LoginProgress { /// We're just starting up, this is the default and initial state. #[default] Starting, - /// We have established the secure channel, but we need to let the other - /// side know about the [`CheckCode`] so they can verify that the secure - /// channel is indeed secure. + /// We have established the secure channel, but need to exchange the + /// [`CheckCode`] so the channel can be verified to indeed be secure. EstablishingSecureChannel(Q), /// We're waiting for the OAuth 2.0 authorization server to give us the /// access token. This will only happen if the other device allows the @@ -264,12 +264,68 @@ pub enum LoginProgress { /// Metadata to be used with [`LoginProgress::EstablishingSecureChannel`] when /// this device is the one scanning the QR code. +/// +/// We have established the secure channel, but we need to let the other +/// side know about the [`CheckCode`] so they can verify that the secure +/// channel is indeed secure. #[derive(Clone, Debug)] pub struct QrProgress { /// The check code we need to, out of band, send to the other device. pub check_code: CheckCode, } +/// Metadata to be used with [`LoginProgress::EstablishingSecureChannel`] when +/// this device is the one generating the QR code. +/// +/// We have established the secure channel, but we need to let the +/// other side know about the [`QrCodeData`] so they can send us the +/// [`CheckCode`] and verify that the secure channel is indeed secure. +#[derive(Clone, Debug)] +pub enum GeneratedQrProgress { + /// The QR code data that must be sent to the existing device. + QrReady(QrCodeData), + /// Used to send the [`CheckCode`] to the login task once we receive + /// it from the existing device. + QrScanned(CheckCodeSender), +} + +/// Used to send the [`CheckCode`] to the new device that generated the +/// QR code. +#[derive(Clone, Debug)] +pub struct CheckCodeSender { + inner: Arc>>>, +} + +impl CheckCodeSender { + fn new(tx: tokio::sync::oneshot::Sender) -> Self { + Self { inner: Arc::new(Mutex::new(Some(tx))) } + } + + /// Send the [`CheckCode`]. + /// + /// Calling this method more than once will result in an error. + pub async fn send(&self, check_code: u8) -> Result<(), CheckCodeSenderError> { + match self.inner.lock().await.take() { + Some(tx) => tx.send(check_code).map_err(|_| CheckCodeSenderError::CannotSend), + None => Err(CheckCodeSenderError::AlreadySent), + } + } +} + +/// Possible errors when calling [`CheckCodeSender::send`]. +#[derive(Debug, thiserror::Error)] +pub enum CheckCodeSenderError { + /// The check code has already been sent. + #[error("check code already sent.")] + AlreadySent, + /// The check code cannot be sent. + /// + /// This probably means that the new device log-in process + /// crashed somehow. + #[error("check code cannot be sent.")] + CannotSend, +} + /// Named future for the [`OAuth::login_with_qr_code()`] method. #[derive(Debug)] pub struct LoginWithQrCode<'a> { @@ -339,6 +395,111 @@ impl<'a> LoginWithQrCode<'a> { } } +/// Named future for the [`OAuth::login_with_generated_qr_code()`] method. +#[derive(Debug)] +pub struct LoginWithGeneratedQrCode<'a> { + client: &'a Client, + registration_data: Option<&'a ClientRegistrationData>, + state: SharedObservable>, +} + +impl LoginWithGeneratedQrCode<'_> { + /// Subscribe to the progress of QR code login. + /// + /// It's necessary to subscribe to this to show the QR code to the existing + /// device so it can send the check code back to this device. + pub fn subscribe_to_progress( + &self, + ) -> impl Stream> + use<> { + self.state.subscribe() + } +} + +impl<'a> IntoFuture for LoginWithGeneratedQrCode<'a> { + type Output = Result<(), QRCodeLoginError>; + boxed_into_future!(extra_bounds: 'a); + + fn into_future(self) -> Self::IntoFuture { + Box::pin(async move { + let mut channel = self.establish_secure_channel().await?; + + trace!("Established the secure channel."); + + // Receive m.login.protocols message. + let message = channel.receive_json().await?; + + match message { + QrAuthMessage::LoginProtocols { protocols, .. } => { + 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, + }); + } + } + _ => { + send_unexpected_message_error(&mut channel).await?; + + return Err(QRCodeLoginError::UnexpectedMessage { + expected: "m.login.protocols", + received: message, + }); + } + } + + finish_login(self.client, channel, self.registration_data, self.state).await + }) + } +} + +impl<'a> LoginWithGeneratedQrCode<'a> { + pub(crate) fn new( + client: &'a Client, + registration_data: Option<&'a ClientRegistrationData>, + ) -> Self { + Self { client, registration_data, state: Default::default() } + } + + async fn establish_secure_channel( + &self, + ) -> Result { + let http_client = self.client.inner.http_client.clone(); + + let secure_channel = SecureChannel::login(http_client, &self.client.homeserver()).await?; + + assert_eq!(QrCodeModeData::Login, secure_channel.qr_code_data().mode_data); + + let qr_code_data = secure_channel.qr_code_data().clone(); + + trace!("Generated QR code."); + self.state.set(LoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrReady( + qr_code_data, + ))); + + let (tx, rx) = tokio::sync::oneshot::channel(); + + let channel = secure_channel.connect().await?; + + trace!("Waiting for checkcode."); + self.state.set(LoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrScanned( + CheckCodeSender::new(tx), + ))); + + let check_code = rx.await.map_err(|_| SecureChannelError::CannotReceiveCheckCode)?; + + trace!("Received check code."); + + channel.confirm(check_code) + } +} + #[cfg(all(test, not(target_family = "wasm")))] mod test { use assert_matches2::{assert_let, assert_matches}; @@ -519,6 +680,190 @@ mod test { assert!(own_identity.is_verified()); } + async fn grant_login_with_generated_qr( + alice: &Client, + qr_receiver: tokio::sync::oneshot::Receiver, + cctx_receiver: tokio::sync::oneshot::Receiver, + behavior: AliceBehaviour, + ) { + let qr_code_data = qr_receiver.await.expect("Alice should receive the QR code"); + + let mut channel = EstablishedSecureChannel::from_qr_code( + alice.inner.http_client.inner.clone(), + &qr_code_data, + QrCodeMode::Reciprocate, + ) + .await + .expect("Alice should be able to establish the secure channel"); + + 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. + let check_code = channel.check_code().to_digit(); + + let check_code_sender = + cctx_receiver.await.expect("Alice should receive the CheckCodeSender"); + + check_code_sender + .send(check_code) + .await + .expect("Alice should be able to send the check code to Bob"); + + // Alice sends m.login.protocols message + let message = QrAuthMessage::LoginProtocols { + protocols: vec![LoginProtocolType::DeviceAuthorizationGrant], + homeserver: alice.homeserver(), + }; + channel + .send_json(message) + .await + .expect("Alice should be able to send the `m.login.protocols` message to Bob"); + + // Alice receives m.login.protocol message + let message: QrAuthMessage = channel + .receive_json() + .await + .expect("Alice should be able to receive the `m.login.protocol` message from Bob"); + assert_let!(QrAuthMessage::LoginProtocol { protocol, .. } = message); + assert_eq!(protocol, LoginProtocolType::DeviceAuthorizationGrant); + + // Alice sends m.login.protocol_accepted message + let message = match behavior { + AliceBehaviour::DeclinedProtocol => QrAuthMessage::LoginFailure { + reason: LoginFailureReason::UnsupportedProtocol, + homeserver: None, + }, + AliceBehaviour::UnexpectedMessage => QrAuthMessage::LoginDeclined, + _ => QrAuthMessage::LoginProtocolAccepted, + }; + channel + .send_json(message) + .await + .expect("Alice should be able to send the `m.login.protocol_accepted` message to Bob"); + + let message: QrAuthMessage = channel + .receive_json() + .await + .expect("Alice should be able to receive the `m.login.success` message from Bob"); + assert_let!(QrAuthMessage::LoginSuccess = message); + + // Alice sends m.login.secrets message + let message = match behavior { + AliceBehaviour::UnexpectedMessageInsteadOfSecrets => QrAuthMessage::LoginDeclined, + AliceBehaviour::RefuseSecrets => QrAuthMessage::LoginFailure { + reason: LoginFailureReason::DeviceNotFound, + homeserver: None, + }, + _ => QrAuthMessage::LoginSecrets(secrets_bundle()), + }; + channel + .send_json(message) + .await + .expect("Alice should be able to send the `m.login.secrets` message to Bob"); + } + + #[async_test] + async fn test_generated_qr_login() { + let server = MatrixMockServer::new().await; + let rendezvous_server = MockedRendezvousServer::new(server.server(), "abcdEFG12345").await; + let (qr_sender, qr_receiver) = tokio::sync::oneshot::channel(); + let (cctx_sender, cctx_receiver) = tokio::sync::oneshot::channel(); + + let oauth_server = server.oauth(); + oauth_server.mock_server_metadata().ok().expect(1).named("server_metadata").mount().await; + oauth_server.mock_registration().ok().expect(1).named("registration").mount().await; + oauth_server + .mock_device_authorization() + .ok() + .expect(1) + .named("device_authorization") + .mount() + .await; + oauth_server.mock_token().ok().expect(1).named("token").mount().await; + + server.mock_versions().ok().expect(1..).named("versions").mount().await; + server.mock_who_am_i().ok().expect(1).named("whoami").mount().await; + server.mock_upload_keys().ok().expect(1).named("upload_keys").mount().await; + server.mock_query_keys().ok().expect(1).named("query_keys").mount().await; + + let homeserver_url = rendezvous_server.homeserver_url.clone(); + + // Create Alice, the existing client, as a logged-in client. They will scan the + // QR code generated by Bob. + let alice = server.client_builder().logged_in_with_oauth().build().await; + assert!(alice.session_meta().is_some(), "Alice should be logged in"); + + // Create Bob, the new client. They will generate the QR code. + let bob = Client::builder() + .server_name_or_homeserver_url(&homeserver_url) + .request_config(RequestConfig::new().disable_retry()) + .build() + .await + .expect("Should be able to create a client for Bob"); + + let secure_channel = SecureChannel::login(bob.inner.http_client.clone(), &homeserver_url) + .await + .expect("Bob should be able to create a secure channel"); + + assert_eq!(QrCodeModeData::Login, secure_channel.qr_code_data().mode_data); + + let registration_data = mock_client_metadata().into(); + let bob_oauth = bob.oauth(); + let bob_login = bob_oauth.login_with_generated_qr_code(Some(®istration_data)); + let mut bob_updates = bob_login.subscribe_to_progress(); + + let updates_task = spawn(async move { + let mut qr_sender = Some(qr_sender); + let mut cctx_sender = Some(cctx_sender); + + while let Some(update) = bob_updates.next().await { + match update { + LoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrReady(qr)) => { + qr_sender + .take() + .expect("The establishing secure channel update with a qr code should be received only once") + .send(qr) + .expect("Bob should be able to send the qr code code to Alice"); + } + LoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrScanned( + cctx, + )) => { + cctx_sender + .take() + .expect("The establishing secure channel update with a CheckCodeSender should be received only once") + .send(cctx) + .expect("Bob should be able to send the qr code code to Alice"); + } + LoginProgress::Done => break, + _ => (), + } + } + }); + + let alice_task = spawn(async move { + grant_login_with_generated_qr( + &alice, + qr_receiver, + cctx_receiver, + AliceBehaviour::HappyPath, + ) + .await + }); + + join!( + async { bob_login.await.expect("Bob should be able to login") }, + async { alice_task.await.expect("Alice should have completed it's task successfully") }, + async { updates_task.await.unwrap() } + ); + + assert!(bob.encryption().cross_signing_status().await.unwrap().is_complete()); + let own_identity = + bob.encryption().get_user_identity(bob.user_id().unwrap()).await.unwrap().unwrap(); + + assert!(own_identity.is_verified()); + } + async fn test_failure( token_response: TokenResponse, alice_behavior: AliceBehaviour, @@ -591,6 +936,97 @@ mod test { login_bob.await } + async fn test_generated_failure( + token_response: TokenResponse, + alice_behavior: AliceBehaviour, + ) -> Result<(), QRCodeLoginError> { + let server = MatrixMockServer::new().await; + let rendezvous_server = MockedRendezvousServer::new(server.server(), "abcdEFG12345").await; + let (qr_sender, qr_receiver) = tokio::sync::oneshot::channel(); + let (cctx_sender, cctx_receiver) = tokio::sync::oneshot::channel(); + + let oauth_server = server.oauth(); + oauth_server.mock_server_metadata().ok().expect(1).named("server_metadata").mount().await; + oauth_server.mock_registration().ok().expect(1).named("registration").mount().await; + oauth_server + .mock_device_authorization() + .ok() + .expect(1) + .named("device_authorization") + .mount() + .await; + + let token_mock = oauth_server.mock_token(); + let token_mock = match token_response { + TokenResponse::Ok => token_mock.ok(), + TokenResponse::AccessDenied => token_mock.access_denied(), + TokenResponse::ExpiredToken => token_mock.expired_token(), + }; + token_mock.named("token").mount().await; + + server.mock_versions().ok().named("versions").mount().await; + server.mock_who_am_i().ok().named("whoami").mount().await; + + let homeserver_url = rendezvous_server.homeserver_url.clone(); + + // Create Alice, the existing client, as a logged-in client. They will scan the + // QR code generated by Bob. + let alice = server.client_builder().logged_in_with_oauth().build().await; + assert!(alice.session_meta().is_some(), "Alice should be logged in"); + + // Create Bob, the new client. They will generate the QR code. + let bob = Client::builder() + .server_name_or_homeserver_url(&homeserver_url) + .request_config(RequestConfig::new().disable_retry()) + .build() + .await + .expect("Should be able to create a client for Bob"); + + let secure_channel = SecureChannel::login(bob.inner.http_client.clone(), &homeserver_url) + .await + .expect("Bob should be able to create a secure channel"); + + assert_eq!(QrCodeModeData::Login, secure_channel.qr_code_data().mode_data); + + let registration_data = mock_client_metadata().into(); + let bob_oauth = bob.oauth(); + let bob_login = bob_oauth.login_with_generated_qr_code(Some(®istration_data)); + let mut bob_updates = bob_login.subscribe_to_progress(); + + let _updates_task = spawn(async move { + let mut qr_sender = Some(qr_sender); + let mut cctx_sender = Some(cctx_sender); + + while let Some(update) = bob_updates.next().await { + match update { + LoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrReady(qr)) => { + qr_sender + .take() + .expect("The establishing secure channel update with a qr code should be received only once") + .send(qr) + .expect("Bob should be able to send the qr code code to Alice"); + } + LoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrScanned( + cctx, + )) => { + cctx_sender + .take() + .expect("The establishing secure channel update with a CheckCodeSender should be received only once") + .send(cctx) + .expect("Bob should be able to send the qr code code to Alice"); + } + LoginProgress::Done => break, + _ => (), + } + } + }); + + let _alice_task = spawn(async move { + grant_login_with_generated_qr(&alice, qr_receiver, cctx_receiver, alice_behavior).await + }); + bob_login.await + } + #[async_test] async fn test_qr_login_refused_access_token() { let result = test_failure(TokenResponse::AccessDenied, AliceBehaviour::HappyPath).await; @@ -603,6 +1039,19 @@ mod test { ); } + #[async_test] + async fn test_generated_qr_login_refused_access_token() { + let result = + test_generated_failure(TokenResponse::AccessDenied, AliceBehaviour::HappyPath).await; + + assert_let!(Err(QRCodeLoginError::OAuth(e)) = result); + assert_eq!( + e.as_request_token_error(), + Some(&DeviceCodeErrorResponseType::AccessDenied), + "The server should have told us that access has been denied." + ); + } + #[async_test] async fn test_qr_login_expired_token() { let result = test_failure(TokenResponse::ExpiredToken, AliceBehaviour::HappyPath).await; @@ -615,6 +1064,19 @@ mod test { ); } + #[async_test] + async fn test_generated_qr_login_expired_token() { + let result = + test_generated_failure(TokenResponse::ExpiredToken, AliceBehaviour::HappyPath).await; + + assert_let!(Err(QRCodeLoginError::OAuth(e)) = result); + assert_eq!( + e.as_request_token_error(), + Some(&DeviceCodeErrorResponseType::ExpiredToken), + "The server should have told us that access has been denied." + ); + } + #[async_test] async fn test_qr_login_declined_protocol() { let result = test_failure(TokenResponse::Ok, AliceBehaviour::DeclinedProtocol).await; @@ -627,6 +1089,19 @@ mod test { ); } + #[async_test] + async fn test_generated_qr_login_declined_protocol() { + let result = + test_generated_failure(TokenResponse::Ok, AliceBehaviour::DeclinedProtocol).await; + + assert_let!(Err(QRCodeLoginError::LoginFailure { reason, .. }) = result); + assert_eq!( + reason, + LoginFailureReason::UnsupportedProtocol, + "Alice should have told us that the protocol is unsupported." + ); + } + #[async_test] async fn test_qr_login_unexpected_message() { let result = test_failure(TokenResponse::Ok, AliceBehaviour::UnexpectedMessage).await; @@ -635,6 +1110,15 @@ mod test { assert_eq!(expected, "m.login.protocol_accepted"); } + #[async_test] + async fn test_generated_qr_login_unexpected_message() { + let result = + test_generated_failure(TokenResponse::Ok, AliceBehaviour::UnexpectedMessage).await; + + assert_let!(Err(QRCodeLoginError::UnexpectedMessage { expected, .. }) = result); + assert_eq!(expected, "m.login.protocol_accepted"); + } + #[async_test] async fn test_qr_login_unexpected_message_instead_of_secrets() { let result = @@ -645,6 +1129,18 @@ mod test { assert_eq!(expected, "m.login.secrets"); } + #[async_test] + async fn test_generated_qr_login_unexpected_message_instead_of_secrets() { + let result = test_generated_failure( + TokenResponse::Ok, + AliceBehaviour::UnexpectedMessageInsteadOfSecrets, + ) + .await; + + assert_let!(Err(QRCodeLoginError::UnexpectedMessage { expected, .. }) = result); + assert_eq!(expected, "m.login.secrets"); + } + #[async_test] async fn test_qr_login_refuse_secrets() { let result = test_failure(TokenResponse::Ok, AliceBehaviour::RefuseSecrets).await; @@ -653,6 +1149,14 @@ mod test { assert_eq!(reason, LoginFailureReason::DeviceNotFound); } + #[async_test] + async fn test_generated_qr_login_refuse_secrets() { + let result = test_generated_failure(TokenResponse::Ok, AliceBehaviour::RefuseSecrets).await; + + assert_let!(Err(QRCodeLoginError::LoginFailure { reason, .. }) = result); + assert_eq!(reason, LoginFailureReason::DeviceNotFound); + } + #[async_test] async fn test_device_authorization_endpoint_missing() { let server = MatrixMockServer::new().await; diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs index c318dc33ddf..1a1888f6670 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs @@ -41,7 +41,7 @@ mod rendezvous_channel; mod secure_channel; pub use self::{ - login::{LoginProgress, LoginWithQrCode, QrProgress}, + login::{LoginProgress, LoginWithGeneratedQrCode, LoginWithQrCode, QrProgress}, messages::{LoginFailureReason, LoginProtocolType, QrAuthMessage}, }; use super::CrossProcessRefreshLockError; @@ -192,4 +192,12 @@ pub enum SecureChannelError { the two devices have the same login intent" )] InvalidIntent, + + /// The secure channel could not have been established, the check code + /// cannot be received. + #[error( + "The secure channel could not have been established, \ + the check code cannot be received" + )] + CannotReceiveCheckCode, } diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel.rs index f5a30f292b7..14c5ec4cf00 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel.rs @@ -109,7 +109,6 @@ impl RendezvousChannel { /// By outbound we mean that we're going to tell the Matrix server to create /// a new rendezvous session. We're going to send an initial empty message /// through the channel. - #[cfg(test)] pub(super) async fn create_outbound( client: HttpClient, rendezvous_server: &Url, diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel.rs index 5244eca6517..c89bec2a8c1 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel.rs @@ -12,16 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -#[cfg(test)] -use matrix_sdk_base::crypto::types::qr_login::QrCodeModeData; -use matrix_sdk_base::crypto::types::qr_login::{QrCodeData, QrCodeMode}; +use matrix_sdk_base::crypto::types::qr_login::{QrCodeData, QrCodeMode, QrCodeModeData}; use serde::{Serialize, de::DeserializeOwned}; use tracing::{instrument, trace}; -#[cfg(test)] use url::Url; -use vodozemac::ecies::{CheckCode, Ecies, EstablishedEcies, Message, OutboundCreationResult}; -#[cfg(test)] -use vodozemac::ecies::{InboundCreationResult, InitialMessage}; +use vodozemac::ecies::{ + CheckCode, Ecies, EstablishedEcies, InboundCreationResult, InitialMessage, Message, + OutboundCreationResult, +}; use super::{ SecureChannelError as Error, @@ -32,21 +30,32 @@ use crate::{config::RequestConfig, http_client::HttpClient}; const LOGIN_INITIATE_MESSAGE: &str = "MATRIX_QR_CODE_LOGIN_INITIATE"; const LOGIN_OK_MESSAGE: &str = "MATRIX_QR_CODE_LOGIN_OK"; -#[cfg(test)] pub(super) struct SecureChannel { channel: RendezvousChannel, qr_code_data: QrCodeData, ecies: Ecies, } -// This is only used in tests because we're only supporting the new device part -// of the QR login flow. It will be needed once we support reciprocating of the -// login. -// -// It's still very much useful to have this, as we're testing the whole flow by -// mocking the reciprocation. -#[cfg(test)] impl SecureChannel { + /// Create a new secure channel to request a login with. + pub(super) async fn login( + http_client: HttpClient, + homeserver_url: &Url, + ) -> Result { + let channel = RendezvousChannel::create_outbound(http_client, homeserver_url).await?; + let rendezvous_url = channel.rendezvous_url().to_owned(); + let mode_data = QrCodeModeData::Login; + + let ecies = Ecies::new(); + let public_key = ecies.public_key(); + + let qr_code_data = QrCodeData { public_key, rendezvous_url, mode_data }; + + Ok(Self { channel, qr_code_data, ecies }) + } + + /// Create a new login to reciprocate an existing login with. + #[cfg(test)] pub(super) async fn new(http_client: HttpClient, homeserver_url: &Url) -> Result { let channel = RendezvousChannel::create_outbound(http_client, homeserver_url).await?; let rendezvous_url = channel.rendezvous_url().to_owned(); @@ -99,12 +108,10 @@ impl SecureChannel { /// An SecureChannel that is yet to be confirmed as with the [`CheckCode`]. /// Same deal as for the [`SecureChannel`], not used for now. -#[cfg(test)] pub(super) struct AlmostEstablishedSecureChannel { secure_channel: EstablishedSecureChannel, } -#[cfg(test)] impl AlmostEstablishedSecureChannel { /// Confirm that the secure channel is indeed secure. /// From 2de41589a8bf364cc08a6a752aca793acf29051d Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Thu, 9 Oct 2025 16:04:33 +0200 Subject: [PATCH 2/6] fixup! Add generated QR code login flow Reuse SecureChannel::login in SecureChannel::new Signed-off-by: Johannes Marbach --- .../authentication/oauth/qrcode/secure_channel.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel.rs index c89bec2a8c1..d351dee0bc0 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel.rs @@ -57,18 +57,12 @@ impl SecureChannel { /// Create a new login to reciprocate an existing login with. #[cfg(test)] pub(super) async fn new(http_client: HttpClient, homeserver_url: &Url) -> Result { - let channel = RendezvousChannel::create_outbound(http_client, homeserver_url).await?; - let rendezvous_url = channel.rendezvous_url().to_owned(); + let mut channel = SecureChannel::login(http_client, homeserver_url).await?; // We're a bit abusing the QR code data here, since we're passing the homeserver // URL, but for our tests this is fine. - let mode_data = QrCodeModeData::Reciprocate { server_name: homeserver_url.to_string() }; - - let ecies = Ecies::new(); - let public_key = ecies.public_key(); - - let qr_code_data = QrCodeData { public_key, rendezvous_url, mode_data }; - - Ok(Self { channel, qr_code_data, ecies }) + channel.qr_code_data.mode_data = + QrCodeModeData::Reciprocate { server_name: homeserver_url.to_string() }; + Ok(channel) } pub(super) fn qr_code_data(&self) -> &QrCodeData { From 1183efad52fda9e67040c76f08b21741d494af5a Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Thu, 9 Oct 2025 16:11:07 +0200 Subject: [PATCH 3/6] fixup! Add generated QR code login flow Remove assert Signed-off-by: Johannes Marbach --- crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs index 2383b4c8945..ee7a02c7f47 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs @@ -18,7 +18,7 @@ use eyeball::SharedObservable; use futures_core::Stream; use matrix_sdk_base::{ SessionMeta, boxed_into_future, - crypto::types::qr_login::{QrCodeData, QrCodeMode, QrCodeModeData}, + crypto::types::qr_login::{QrCodeData, QrCodeMode}, store::RoomLoadSettings, }; use oauth2::{DeviceCodeErrorResponseType, StandardDeviceAuthorizationResponse}; @@ -474,8 +474,6 @@ impl<'a> LoginWithGeneratedQrCode<'a> { let secure_channel = SecureChannel::login(http_client, &self.client.homeserver()).await?; - assert_eq!(QrCodeModeData::Login, secure_channel.qr_code_data().mode_data); - let qr_code_data = secure_channel.qr_code_data().clone(); trace!("Generated QR code."); From 9c4e005683a05cbe6077bbc362de81fd1c911a35 Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Thu, 9 Oct 2025 16:13:50 +0200 Subject: [PATCH 4/6] fixup! Add generated QR code login flow Remove misleading comment Signed-off-by: Johannes Marbach --- crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs index ee7a02c7f47..375ee1cc748 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs @@ -319,9 +319,6 @@ pub enum CheckCodeSenderError { #[error("check code already sent.")] AlreadySent, /// The check code cannot be sent. - /// - /// This probably means that the new device log-in process - /// crashed somehow. #[error("check code cannot be sent.")] CannotSend, } From e5e59e49eea6a64e0ee1fa7a0d15e326b02a4adc Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Thu, 9 Oct 2025 16:21:46 +0200 Subject: [PATCH 5/6] fixup! Add generated QR code login flow Clarify that the check code has to be supplied in digits representation Signed-off-by: Johannes Marbach --- crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs index 375ee1cc748..41efc98a4f8 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs @@ -304,6 +304,10 @@ impl CheckCodeSender { /// Send the [`CheckCode`]. /// /// Calling this method more than once will result in an error. + /// + /// # Arguments + /// + /// * `check_code` - The check code in digits representation. pub async fn send(&self, check_code: u8) -> Result<(), CheckCodeSenderError> { match self.inner.lock().await.take() { Some(tx) => tx.send(check_code).map_err(|_| CheckCodeSenderError::CannotSend), From 5e385199f8c971af8dfb81e911bcaa50c7c568f9 Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Fri, 10 Oct 2025 14:56:46 +0200 Subject: [PATCH 6/6] fixup! Add generated QR code login flow Use builder to avoid introducing another method and add example in doc comment Signed-off-by: Johannes Marbach --- bindings/matrix-sdk-ffi/src/client.rs | 2 +- .../src/authentication/oauth/mod.rs | 281 +++++++++++------- .../src/authentication/oauth/qrcode/login.rs | 10 +- .../src/authentication/oauth/qrcode/mod.rs | 4 +- examples/qr-login/src/main.rs | 2 +- 5 files changed, 185 insertions(+), 114 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index c06be0bda51..171ecaf3644 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -565,7 +565,7 @@ impl Client { .map_err(|_| HumanQrLoginError::OidcMetadataInvalid)?; let oauth = self.inner.oauth(); - let login = oauth.login_with_qr_code(&qr_code_data.inner, Some(®istration_data)); + let login = oauth.login_with_qr_code(Some(®istration_data)).scan(&qr_code_data.inner); let mut progress = login.subscribe_to_progress(); diff --git a/crates/matrix-sdk/src/authentication/oauth/mod.rs b/crates/matrix-sdk/src/authentication/oauth/mod.rs index a06128877aa..3980945ad1e 100644 --- a/crates/matrix-sdk/src/authentication/oauth/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/mod.rs @@ -366,122 +366,18 @@ impl OAuth { /// Log in using a QR code. /// - /// This method allows you to log in with a QR code, the existing device - /// needs to display the QR code which this device can scan and call - /// this method to log in. - /// - /// A successful login using this method will automatically mark the device - /// as verified and transfer all end-to-end encryption related secrets, like - /// the private cross-signing keys and the backup key from the existing - /// device to the new device. - /// /// # Arguments /// - /// * `data` - The data scanned from a QR code. - /// /// * `registration_data` - The data to restore or register the client with /// the server. If this is not provided, an error will occur unless /// [`OAuth::register_client()`] or [`OAuth::restore_registered_client()`] /// was called previously. - /// - /// # Example - /// - /// ```no_run - /// use anyhow::bail; - /// use futures_util::StreamExt; - /// use matrix_sdk::{ - /// authentication::oauth::{ - /// registration::ClientMetadata, - /// qrcode::{LoginProgress, QrCodeData, QrCodeModeData}, - /// }, - /// ruma::serde::Raw, - /// Client, - /// }; - /// # fn client_metadata() -> Raw { unimplemented!() } - /// # _ = async { - /// # let bytes = unimplemented!(); - /// // You'll need to use a different library to scan and extract the raw bytes from the QR - /// // code. - /// let qr_code_data = QrCodeData::from_bytes(bytes)?; - /// - /// // Fetch the homeserver out of the parsed QR code data. - /// let QrCodeModeData::Reciprocate{ server_name } = qr_code_data.mode_data else { - /// bail!("The QR code is invalid, we did not receive a homeserver in the QR code."); - /// }; - /// - /// // Build the client as usual. - /// let client = Client::builder() - /// .server_name_or_homeserver_url(server_name) - /// .handle_refresh_tokens() - /// .build() - /// .await?; - /// - /// let oauth = client.oauth(); - /// let client_metadata: Raw = client_metadata(); - /// let registration_data = client_metadata.into(); - /// - /// // 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(&qr_code_data, Some(®istration_data)); - /// let mut progress = login.subscribe_to_progress(); - /// - /// // Create a task which will show us the progress and tell us the check - /// // code to input in the existing device. - /// let task = tokio::spawn(async move { - /// while let Some(state) = progress.next().await { - /// match state { - /// LoginProgress::Starting | LoginProgress::SyncingSecrets => (), - /// LoginProgress::EstablishingSecureChannel(progress) => { - /// let code = progress.check_code.to_digit(); - /// println!("Please enter the following code into the other device {code:02}"); - /// }, - /// LoginProgress::WaitingForToken { user_code } => { - /// println!("Please use your other device to confirm the log in {user_code}") - /// }, - /// LoginProgress::Done => break, - /// } - /// } - /// }); - /// - /// // Now run the future to complete the login. - /// login.await?; - /// task.abort(); - /// - /// println!("Successfully logged in: {:?} {:?}", client.user_id(), client.device_id()); - /// # anyhow::Ok(()) }; - /// ``` #[cfg(feature = "e2e-encryption")] pub fn login_with_qr_code<'a>( - &'a self, - data: &'a QrCodeData, - registration_data: Option<&'a ClientRegistrationData>, - ) -> LoginWithQrCode<'a> { - LoginWithQrCode::new(&self.client, data, registration_data) - } - - /// Log in using a generated QR code. - /// - /// This method allows you to log in with a QR code, this device - /// needs to display the QR code by calling this method so the existing - /// device can scan it and grant the log in. - /// - /// A successful login using this method will automatically mark the device - /// as verified and transfer all end-to-end encryption related secrets, like - /// the private cross-signing keys and the backup key from the existing - /// device to the new device. - /// - /// # Arguments - /// - /// * `registration_data` - The data to restore or register the client with - /// the server. If this is not provided, an error will occur unless - /// [`OAuth::register_client()`] or [`OAuth::restore_registered_client()`] - /// was called previously. - #[cfg(feature = "e2e-encryption")] - pub fn login_with_generated_qr_code<'a>( &'a self, registration_data: Option<&'a ClientRegistrationData>, - ) -> LoginWithGeneratedQrCode<'a> { - LoginWithGeneratedQrCode::new(&self.client, registration_data) + ) -> LoginWithQrCodeBuilder<'a> { + LoginWithQrCodeBuilder { client: &self.client, registration_data } } /// Restore or register the OAuth 2.0 client for the server with the given @@ -1428,6 +1324,179 @@ impl OAuth { } } +/// Builder for QR login futures. +#[derive(Debug)] +pub struct LoginWithQrCodeBuilder<'a> { + /// The underlying Matrix API client. + client: &'a Client, + + /// The data to restore or register the client with the server. + registration_data: Option<&'a ClientRegistrationData>, +} + +impl<'a> LoginWithQrCodeBuilder<'a> { + /// 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 + /// scan and call this method to log in. + /// + /// A successful login using this method will automatically mark the device + /// as verified and transfer all end-to-end encryption related secrets, like + /// the private cross-signing keys and the backup key from the existing + /// device to the new device. + /// + /// # Arguments + /// + /// * `data` - The data scanned from a QR code. + /// + /// # Example + /// + /// ```no_run + /// use anyhow::bail; + /// use futures_util::StreamExt; + /// use matrix_sdk::{ + /// authentication::oauth::{ + /// registration::ClientMetadata, + /// qrcode::{LoginProgress, QrCodeData, QrCodeModeData}, + /// }, + /// ruma::serde::Raw, + /// Client, + /// }; + /// # fn client_metadata() -> Raw { unimplemented!() } + /// # _ = async { + /// # let bytes = unimplemented!(); + /// // You'll need to use a different library to scan and extract the raw bytes from the QR + /// // code. + /// let qr_code_data = QrCodeData::from_bytes(bytes)?; + /// + /// // Fetch the homeserver out of the parsed QR code data. + /// let QrCodeModeData::Reciprocate{ server_name } = qr_code_data.mode_data else { + /// bail!("The QR code is invalid, we did not receive a homeserver in the QR code."); + /// }; + /// + /// // Build the client as usual. + /// let client = Client::builder() + /// .server_name_or_homeserver_url(server_name) + /// .handle_refresh_tokens() + /// .build() + /// .await?; + /// + /// let oauth = client.oauth(); + /// let client_metadata: Raw = client_metadata(); + /// let registration_data = client_metadata.into(); + /// + /// // 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 mut progress = login.subscribe_to_progress(); + /// + /// // Create a task which will show us the progress and tell us the check + /// // code to input in the existing device. + /// let task = tokio::spawn(async move { + /// while let Some(state) = progress.next().await { + /// match state { + /// LoginProgress::Starting | LoginProgress::SyncingSecrets => (), + /// LoginProgress::EstablishingSecureChannel(QrProgress { check_code }) => { + /// let code = check_code.to_digit(); + /// println!("Please enter the following code into the other device {code:02}"); + /// }, + /// LoginProgress::WaitingForToken { user_code } => { + /// println!("Please use your other device to confirm the log in {user_code}") + /// }, + /// LoginProgress::Done => break, + /// } + /// } + /// }); + /// + /// // Now run the future to complete the login. + /// login.await?; + /// task.abort(); + /// + /// println!("Successfully logged in: {:?} {:?}", client.user_id(), client.device_id()); + /// # anyhow::Ok(()) }; + /// ``` + pub fn scan(self, data: &'a QrCodeData) -> LoginWithQrCode<'a> { + LoginWithQrCode::new(self.client, data, self.registration_data) + } + + /// This method allows you to log in by generating a QR code. + /// + /// This device needs to call this method to generate and display the + /// QR code which the existing device can scan and grant the log in. + /// + /// A successful login using this method will automatically mark the device + /// as verified and transfer all end-to-end encryption related secrets, like + /// the private cross-signing keys and the backup key from the existing + /// device to the new device. + /// + /// # Example + /// + /// ```no_run + /// use anyhow::bail; + /// use futures_util::StreamExt; + /// use matrix_sdk::{ + /// authentication::oauth::{ + /// registration::ClientMetadata, + /// qrcode::{GeneratedQrProgress, LoginProgress, QrCodeData, QrCodeModeData}, + /// }, + /// ruma::serde::Raw, + /// Client, + /// }; + /// use std::io::stdin; + /// # fn client_metadata() -> Raw { unimplemented!() } + /// # _ = async { + /// // Build the client as usual. + /// let client = Client::builder() + /// .server_name_or_homeserver_url("matrix.org") + /// .handle_refresh_tokens() + /// .build() + /// .await?; + /// + /// let oauth = client.oauth(); + /// let client_metadata: Raw = client_metadata(); + /// let registration_data = client_metadata.into(); + /// + /// // 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 mut progress = login.subscribe_to_progress(); + /// + /// // Create a task which will show us the progress and allows us to display + /// // the QR code and prompt for the check code. + /// let task = tokio::spawn(async move { + /// while let Some(state) = progress.next().await { + /// match state { + /// LoginProgress::Starting | LoginProgress::SyncingSecrets => (), + /// LoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrReady(qr)) => { + /// println!("Please use your other device to scan the QR code {:?}", qr) + /// } + /// LoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrScanned(cctx)) => { + /// println!("Please enter the code displayed on your other device"); + /// let mut s = String::new(); + /// stdin().read_line(&mut s).unwrap(); + /// let check_code = s.trim().parse::().unwrap(); + /// cctx.send(check_code).await.unwrap() + /// } + /// LoginProgress::WaitingForToken { user_code } => { + /// println!("Please use your other device to confirm the log in {user_code}") + /// }, + /// LoginProgress::Done => break, + /// } + /// } + /// }); + /// + /// // Now run the future to complete the login. + /// login.await?; + /// task.abort(); + /// + /// println!("Successfully logged in: {:?} {:?}", client.user_id(), client.device_id()); + /// # anyhow::Ok(()) }; + /// ``` + pub fn generate(self) -> LoginWithGeneratedQrCode<'a> { + LoginWithGeneratedQrCode::new(self.client, self.registration_data) + } +} + /// A full session for the OAuth 2.0 API. #[derive(Debug, Clone)] pub struct OAuthSession { diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs index 41efc98a4f8..eb73d023c15 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs @@ -639,7 +639,7 @@ mod test { let oauth = bob.oauth(); let registration_data = mock_client_metadata().into(); - let login_bob = oauth.login_with_qr_code(&qr_code, Some(®istration_data)); + let login_bob = oauth.login_with_qr_code(Some(®istration_data)).scan(&qr_code); let mut updates = login_bob.subscribe_to_progress(); let updates_task = spawn(async move { @@ -809,7 +809,7 @@ mod test { let registration_data = mock_client_metadata().into(); let bob_oauth = bob.oauth(); - let bob_login = bob_oauth.login_with_generated_qr_code(Some(®istration_data)); + let bob_login = bob_oauth.login_with_qr_code(Some(®istration_data)).generate(); let mut bob_updates = bob_login.subscribe_to_progress(); let updates_task = spawn(async move { @@ -911,7 +911,7 @@ mod test { let oauth = bob.oauth(); let registration_data = mock_client_metadata().into(); - let login_bob = oauth.login_with_qr_code(&qr_code, Some(®istration_data)); + let login_bob = oauth.login_with_qr_code(Some(®istration_data)).scan(&qr_code); let mut updates = login_bob.subscribe_to_progress(); let _updates_task = spawn(async move { @@ -989,7 +989,7 @@ mod test { let registration_data = mock_client_metadata().into(); let bob_oauth = bob.oauth(); - let bob_login = bob_oauth.login_with_generated_qr_code(Some(®istration_data)); + let bob_login = bob_oauth.login_with_qr_code(Some(®istration_data)).generate(); let mut bob_updates = bob_login.subscribe_to_progress(); let _updates_task = spawn(async move { @@ -1193,7 +1193,7 @@ mod test { let oauth = bob.oauth(); let registration_data = mock_client_metadata().into(); - let login_bob = oauth.login_with_qr_code(&qr_code, Some(®istration_data)); + let login_bob = oauth.login_with_qr_code(Some(®istration_data)).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 1a1888f6670..9fc38bc16fa 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs @@ -41,7 +41,9 @@ mod rendezvous_channel; mod secure_channel; pub use self::{ - login::{LoginProgress, LoginWithGeneratedQrCode, LoginWithQrCode, QrProgress}, + login::{ + GeneratedQrProgress, LoginProgress, LoginWithGeneratedQrCode, LoginWithQrCode, QrProgress, + }, messages::{LoginFailureReason, LoginProtocolType, QrAuthMessage}, }; use super::CrossProcessRefreshLockError; diff --git a/examples/qr-login/src/main.rs b/examples/qr-login/src/main.rs index 627385d7984..e0e9ec896a3 100644 --- a/examples/qr-login/src/main.rs +++ b/examples/qr-login/src/main.rs @@ -115,7 +115,7 @@ async fn login(proxy: Option) -> Result<()> { let registration_data = client_metadata().into(); let oauth = client.oauth(); - let login_client = oauth.login_with_qr_code(&data, Some(®istration_data)); + let login_client = oauth.login_with_qr_code(Some(®istration_data)).scan(&data); let mut subscriber = login_client.subscribe_to_progress(); let task = tokio::spawn(async move {