feat(oauth): add flow for granting login by scanning a QR code

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
This commit is contained in:
Johannes Marbach
2025-10-30 16:06:30 +01:00
committed by Damir Jelić
parent da1369b9c2
commit b4ebc8bc25
5 changed files with 921 additions and 14 deletions
+3
View File
@@ -8,6 +8,9 @@ All notable changes to this project will be documented in this file.
### Features
- Extend `authentication::oauth::OAuth::grant_login_with_qr_code` to support granting
login by scanning a QR code on the existing device.
([#5818](https://github.com/matrix-org/matrix-rust-sdk/pull/5818))
- Add a new `RequestConfig::skip_auth()` option. This is useful to ensure that
certain request won't ever include an authorization header.
([#5822](https://github.com/matrix-org/matrix-rust-sdk/pull/5822))
@@ -216,7 +216,10 @@ mod tests;
#[cfg(feature = "e2e-encryption")]
use self::cross_process::{CrossProcessRefreshLockGuard, CrossProcessRefreshManager};
#[cfg(feature = "e2e-encryption")]
use self::qrcode::{GrantLoginWithGeneratedQrCode, LoginWithGeneratedQrCode, LoginWithQrCode};
use self::qrcode::{
GrantLoginWithGeneratedQrCode, GrantLoginWithScannedQrCode, LoginWithGeneratedQrCode,
LoginWithQrCode,
};
pub use self::{
account_management_url::{AccountManagementActionFull, AccountManagementUrlBuilder},
auth_code_builder::{OAuthAuthCodeUrlBuilder, OAuthAuthorizationData},
@@ -1537,7 +1540,7 @@ pub struct GrantLoginWithQrCodeBuilder<'a> {
#[cfg(feature = "e2e-encryption")]
impl<'a> GrantLoginWithQrCodeBuilder<'a> {
/// Create a new builder with the default device creation timeout.
pub fn new(client: &'a Client) -> Self {
fn new(client: &'a Client) -> Self {
Self { client, device_creation_timeout: Duration::from_secs(10) }
}
@@ -1552,6 +1555,86 @@ impl<'a> GrantLoginWithQrCodeBuilder<'a> {
self
}
/// This method allows you to grant login to a new device by scanning a
/// QR code generated by the new device.
///
/// The new device needs to display the QR code which this device can
/// scan and call this method to grant the login.
///
/// A successful login grant using this method will automatically mark the
/// new device as verified and transfer all end-to-end encryption
/// related secrets, like the private cross-signing keys and the backup
/// key from this device device to the new device.
///
/// For the reverse flow where this device generates the QR code
/// for the new device to scan, use
/// [`GrantLoginWithQrCodeBuilder::generate`].
///
/// # Arguments
///
/// * `data` - The data scanned from a QR code.
///
/// # Example
///
/// ```no_run
/// use anyhow::bail;
/// use futures_util::StreamExt;
/// use matrix_sdk::{
/// Client, authentication::oauth::{
/// qrcode::{GrantLoginProgress, QrCodeData, QrCodeModeData, QrProgress},
/// }
/// };
/// use std::{error::Error, io::stdin};
/// # _ = 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)?;
///
/// // 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();
///
/// // 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 mut progress = grant.subscribe_to_progress();
///
/// // Create a task which will show us the progress and allows us to receive
/// // and feed back data.
/// let task = tokio::spawn(async move {
/// while let Some(state) = progress.next().await {
/// match state {
/// GrantLoginProgress::Starting | GrantLoginProgress::SyncingSecrets => (),
/// GrantLoginProgress::EstablishingSecureChannel(QrProgress { check_code }) => {
/// println!("Please enter the checkcode on your other device: {:?}", check_code);
/// }
/// GrantLoginProgress::WaitingForAuth { verification_uri } => {
/// println!("Please open {verification_uri} to confirm the new login")
/// },
/// GrantLoginProgress::Done => break,
/// }
/// }
/// Ok::<(), Box<dyn Error + Send + Sync>>(())
/// });
///
/// // Now run the future to grant the login.
/// grant.await?;
/// task.abort();
///
/// println!("Successfully granted login");
/// # anyhow::Ok(()) };
/// ```
pub fn scan(self, data: &'a QrCodeData) -> GrantLoginWithScannedQrCode<'a> {
GrantLoginWithScannedQrCode::new(self.client, data, self.device_creation_timeout)
}
/// This method allows you to grant login to a new device by generating a QR
/// code on this device to be scanned by the new device.
///
@@ -16,9 +16,17 @@ use std::time::{Duration, Instant};
use eyeball::SharedObservable;
use futures_core::Stream;
use matrix_sdk_base::{boxed_into_future, crypto::types::SecretsBundle};
use matrix_sdk_base::{
boxed_into_future,
crypto::types::{
SecretsBundle,
qr_login::{QrCodeData, QrCodeMode},
},
};
use oauth2::VerificationUriComplete;
use url::Url;
#[cfg(doc)]
use vodozemac::ecies::CheckCode;
use super::{
LoginProtocolType, QrAuthMessage,
@@ -28,7 +36,7 @@ use crate::{
Client,
authentication::oauth::qrcode::{
CheckCodeSender, GeneratedQrProgress, LoginFailureReason, QRCodeGrantLoginError,
SecureChannelError,
QrProgress, SecureChannelError,
},
};
@@ -166,9 +174,9 @@ pub enum GrantLoginProgress<Q> {
#[default]
Starting,
/// The secure channel is being established by exchanging the QR code
/// and/or checkcode.
/// and/or [`CheckCode`].
EstablishingSecureChannel(Q),
/// The secure channel has been confirmed using the checkcode and this
/// The secure channel has been confirmed using the [`CheckCode`] and this
/// device is waiting for the authorization to complete.
WaitingForAuth {
/// A URI to open in a (secure) system browser to verify the new login.
@@ -181,6 +189,99 @@ pub enum GrantLoginProgress<Q> {
Done,
}
/// Named future for granting login by scanning a QR code on this, existing,
/// device that was generated by the other, new, device.
#[derive(Debug)]
pub struct GrantLoginWithScannedQrCode<'a> {
client: &'a Client,
qr_code_data: &'a QrCodeData,
device_creation_timeout: Duration,
state: SharedObservable<GrantLoginProgress<QrProgress>>,
}
impl<'a> GrantLoginWithScannedQrCode<'a> {
pub(crate) fn new(
client: &'a Client,
qr_code_data: &'a QrCodeData,
device_creation_timeout: Duration,
) -> GrantLoginWithScannedQrCode<'a> {
GrantLoginWithScannedQrCode {
client,
qr_code_data,
device_creation_timeout,
state: Default::default(),
}
}
}
impl GrantLoginWithScannedQrCode<'_> {
/// Subscribe to the progress of QR code login.
///
/// It's necessary to subscribe to this to capture the [`CheckCode`] in
/// order to display it to the other device and to obtain the
/// verification URL for consenting to the login.
pub fn subscribe_to_progress(
&self,
) -> impl Stream<Item = GrantLoginProgress<QrProgress>> + use<> {
self.state.subscribe()
}
}
impl<'a> IntoFuture for GrantLoginWithScannedQrCode<'a> {
type Output = Result<(), QRCodeGrantLoginError>;
boxed_into_future!(extra_bounds: '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 mut channel = EstablishedSecureChannel::from_qr_code(
self.client.inner.http_client.inner.clone(),
self.qr_code_data,
QrCodeMode::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,
&export_secrets_bundle(self.client).await?,
&self.state,
)
.await
})
}
}
/// Named future for granting login by generating a QR code on this, existing,
/// device to be scanned by the other, new, device.
#[derive(Debug)]
@@ -203,7 +304,7 @@ impl GrantLoginWithGeneratedQrCode<'_> {
/// Subscribe to the progress of QR code login.
///
/// It's necessary to subscribe to this to capture the QR code in order to
/// display it to the other device, to feed the checkcode entered by the
/// display it to the other device, to feed the [`CheckCode`] entered by the
/// user back in and to obtain the verification URL for consenting to
/// the login.
pub fn subscribe_to_progress(
@@ -275,10 +376,7 @@ impl<'a> IntoFuture for GrantLoginWithGeneratedQrCode<'a> {
mod test {
use assert_matches2::{assert_let, assert_matches};
use futures_util::{StreamExt, join};
use matrix_sdk_base::crypto::types::{
SecretsBundle,
qr_login::{QrCodeData, QrCodeMode},
};
use matrix_sdk_base::crypto::types::SecretsBundle;
use matrix_sdk_common::executor::spawn;
use matrix_sdk_test::async_test;
use oauth2::{EndUserVerificationUrl, VerificationUriComplete};
@@ -294,6 +392,7 @@ mod test {
messages::{AuthorizationGrant, LoginProtocolType},
secure_channel::{EstablishedSecureChannel, test::MockedRendezvousServer},
},
http_client::HttpClient,
test_utils::mocks::MatrixMockServer,
};
@@ -430,6 +529,137 @@ mod test {
);
}
async fn request_login_with_generated_qr_code(
behaviour: BobBehaviour,
channel: SecureChannel,
check_code_rx: oneshot::Receiver<u8>,
server: MatrixMockServer,
homeserver: Url,
device_authorization_grant: Option<AuthorizationGrant>,
secrets_bundle: Option<SecretsBundle>,
) {
// Wait for Alice to scan the qr code and connect the secure channel.
let channel =
channel.connect().await.expect("Bob should be able to connect the secure channel");
// Wait for Alice to send us the checkcode and use it to verify the channel.
let check_code = check_code_rx.await.expect("Bob should receive the checkcode");
let mut bob = channel
.confirm(check_code)
.expect("Bob should be able to confirm the channel is secure");
// Receive the LoginProtocols message.
let message = bob
.receive_json()
.await
.expect("Bob should receive the LoginProtocolAccepted message from Alice");
assert_let!(
QrAuthMessage::LoginProtocols { protocols, homeserver: alice_homeserver } = message
);
assert_eq!(protocols, vec![LoginProtocolType::DeviceAuthorizationGrant]);
assert_eq!(alice_homeserver, homeserver);
match behaviour {
BobBehaviour::UnexpectedMessageInsteadOfLoginProtocol => {
// Send an unexpected message and exit.
let message = QrAuthMessage::LoginSuccess;
bob.send_json(message).await.unwrap();
return;
}
BobBehaviour::DeviceAlreadyExists => {
// Mock the endpoint for querying devices so that Alice thinks the device
// already exists.
server.mock_get_device().ok().expect(1..).named("get_device").mount().await;
// Now send the LoginProtocol message.
let message = QrAuthMessage::LoginProtocol {
protocol: LoginProtocolType::DeviceAuthorizationGrant,
device_authorization_grant: device_authorization_grant
.expect("Bob needs the device authorization grant"),
device_id: Curve25519PublicKey::from_base64(
"wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4",
)
.unwrap(),
};
bob.send_json(message).await.unwrap();
// Alice should fail the login with the appropriate reason.
let message = bob
.receive_json()
.await
.expect("Bob should receive the LoginFailure message from Alice");
assert_let!(QrAuthMessage::LoginFailure { reason, .. } = message);
assert_matches!(reason, LoginFailureReason::DeviceAlreadyExists);
return; // Exit.
}
_ => {
// Send the LoginProtocol message.
let message = QrAuthMessage::LoginProtocol {
protocol: LoginProtocolType::DeviceAuthorizationGrant,
device_authorization_grant: device_authorization_grant
.expect("Bob needs the device authorization grant"),
device_id: Curve25519PublicKey::from_base64(
"wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4",
)
.unwrap(),
};
bob.send_json(message).await.unwrap();
}
}
// Receive the LoginProtocolAccepted message.
let message = bob
.receive_json()
.await
.expect("Bob should receive the LoginProtocolAccepted message from Alice");
assert_let!(QrAuthMessage::LoginProtocolAccepted = message);
match behaviour {
BobBehaviour::DeviceNotCreated => {
// Don't mock the endpoint for querying devices so that Alice cannot verify that
// we have logged in.
// Send the LoginSuccess message to claim that we have logged in.
let message = QrAuthMessage::LoginSuccess;
bob.send_json(message).await.unwrap();
// Alice should eventually give up querying our device and fail the login with
// the appropriate reason.
let message = bob
.receive_json()
.await
.expect("Bob should receive the LoginFailure message from Alice");
assert_let!(QrAuthMessage::LoginFailure { reason, .. } = message);
assert_matches!(reason, LoginFailureReason::DeviceNotFound);
return; // Exit.
}
_ => {
// Mock the endpoint for querying devices so that Alice thinks we have logged
// in.
server.mock_get_device().ok().expect(1..).named("get_device").mount().await;
// Send the LoginSuccess message.
let message = QrAuthMessage::LoginSuccess;
bob.send_json(message).await.unwrap();
}
}
// Receive the LoginSecrets message.
let message = bob
.receive_json()
.await
.expect("Bob should receive the LoginSecrets message from Alice");
assert_let!(QrAuthMessage::LoginSecrets(bundle) = message);
// Verify that we received the correct secrets.
assert_eq!(
serde_json::to_value(&secrets_bundle).unwrap(),
serde_json::to_value(&bundle).unwrap()
);
}
#[async_test]
async fn test_grant_login_with_generated_qr_code() {
let server = MatrixMockServer::new().await;
@@ -574,6 +804,262 @@ mod test {
);
}
#[async_test]
async fn test_grant_login_with_scanned_qr_code() {
let server = MatrixMockServer::new().await;
let rendezvous_server = MockedRendezvousServer::new(server.server(), "abcdEFG12345").await;
debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url);
let device_authorization_grant = AuthorizationGrant {
verification_uri_complete: Some(VerificationUriComplete::new(
"https://id.matrix.org/device/abcde".to_owned(),
)),
verification_uri: EndUserVerificationUrl::new(
"https://id.matrix.org/device/abcde?code=ABCDE".to_owned(),
)
.unwrap(),
};
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 grant = oauth
.grant_login_with_qr_code()
.device_creation_timeout(Duration::from_secs(2))
.scan(&qr_code_data);
let secrets_bundle = export_secrets_bundle(&alice)
.await
.expect("Alice should be able to export the secrets bundle");
let (checkcode_tx, checkcode_rx) = oneshot::channel();
// Spawn the updates task.
let mut updates = grant.subscribe_to_progress();
let mut state = grant.state.get();
let verification_uri_complete =
device_authorization_grant.clone().verification_uri_complete.unwrap().into_secret();
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");
}
GrantLoginProgress::WaitingForAuth { verification_uri } => {
assert_matches!(
state,
GrantLoginProgress::EstablishingSecureChannel(QrProgress { .. })
);
assert_eq!(verification_uri.as_str(), verification_uri_complete);
}
GrantLoginProgress::SyncingSecrets => {
assert_matches!(state, GrantLoginProgress::WaitingForAuth { .. });
}
GrantLoginProgress::Done => {
assert_matches!(state, GrantLoginProgress::SyncingSecrets);
break;
}
}
state = update;
}
});
// Let Bob request the login and run through the process.
let bob_task = spawn(async move {
request_login_with_generated_qr_code(
BobBehaviour::HappyPath,
channel,
checkcode_rx,
server,
alice.homeserver(),
Some(device_authorization_grant),
Some(secrets_bundle),
)
.await;
});
// Wait for all tasks to finish.
join!(
async { updates_task.await.expect("Alice should run through all progress states") },
async { grant.await.expect("Alice should be able to grant the login") },
async { bob_task.await.expect("Bob's task should finish") }
);
}
#[async_test]
async fn test_grant_login_with_scanned_qr_code_with_homeserver_swap() {
let server = MatrixMockServer::new().await;
let rendezvous_server = MockedRendezvousServer::new(server.server(), "abcdEFG12345").await;
debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url);
let device_authorization_grant = AuthorizationGrant {
verification_uri_complete: Some(VerificationUriComplete::new(
"https://id.matrix.org/device/abcde".to_owned(),
)),
verification_uri: EndUserVerificationUrl::new(
"https://id.matrix.org/device/abcde?code=ABCDE".to_owned(),
)
.unwrap(),
};
let login_server = MatrixMockServer::new().await;
login_server.mock_upload_keys().ok().expect(1).named("upload_keys").mount().await;
login_server
.mock_upload_cross_signing_keys()
.ok()
.expect(1)
.named("upload_xsigning_keys")
.mount()
.await;
login_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 = login_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 grant = oauth
.grant_login_with_qr_code()
.device_creation_timeout(Duration::from_secs(2))
.scan(&qr_code_data);
let secrets_bundle = export_secrets_bundle(&alice)
.await
.expect("Alice should be able to export the secrets bundle");
let (checkcode_tx, checkcode_rx) = oneshot::channel();
// Spawn the updates task.
let mut updates = grant.subscribe_to_progress();
let mut state = grant.state.get();
let verification_uri_complete =
device_authorization_grant.clone().verification_uri_complete.unwrap().into_secret();
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");
}
GrantLoginProgress::WaitingForAuth { verification_uri } => {
assert_matches!(
state,
GrantLoginProgress::EstablishingSecureChannel(QrProgress { .. })
);
assert_eq!(verification_uri.as_str(), verification_uri_complete);
}
GrantLoginProgress::SyncingSecrets => {
assert_matches!(state, GrantLoginProgress::WaitingForAuth { .. });
}
GrantLoginProgress::Done => {
assert_matches!(state, GrantLoginProgress::SyncingSecrets);
break;
}
}
state = update;
}
});
// Let Bob request the login and run through the process.
let bob_task = spawn(async move {
request_login_with_generated_qr_code(
BobBehaviour::HappyPath,
channel,
checkcode_rx,
login_server,
alice.homeserver(),
Some(device_authorization_grant),
Some(secrets_bundle),
)
.await;
});
// Wait for all tasks to finish.
join!(
async { updates_task.await.expect("Alice should run through all progress states") },
async { grant.await.expect("Alice should be able to grant the login") },
async { 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()
{
@@ -692,6 +1178,108 @@ mod test {
);
}
#[async_test]
async fn test_grant_login_with_scanned_qr_code_unexpected_message_instead_of_login_protocol() {
let server = MatrixMockServer::new().await;
let rendezvous_server = MockedRendezvousServer::new(server.server(), "abcdEFG12345").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 grant = oauth
.grant_login_with_qr_code()
.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");
break;
}
_ => {
panic!("Alice should abort the process");
}
}
state = update;
}
});
// 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,
server,
alice.homeserver(),
None,
None,
)
.await;
});
// Wait for all tasks to finish / fail.
join!(
async { updates_task.await.expect("Alice should run through all progress states") },
async { grant.await.expect_err("Alice should abort the login") },
async { bob_task.await.expect("Bob's task should finish") }
);
}
#[async_test]
async fn test_grant_login_with_generated_qr_code_device_already_exists() {
let server = MatrixMockServer::new().await;
@@ -818,6 +1406,117 @@ mod test {
);
}
#[async_test]
async fn test_grant_login_with_scanned_qr_code_device_already_exists() {
let server = MatrixMockServer::new().await;
let rendezvous_server = MockedRendezvousServer::new(server.server(), "abcdEFG12345").await;
debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url);
let device_authorization_grant = AuthorizationGrant {
verification_uri_complete: Some(VerificationUriComplete::new(
"https://id.matrix.org/device/abcde".to_owned(),
)),
verification_uri: EndUserVerificationUrl::new(
"https://id.matrix.org/device/abcde?code=ABCDE".to_owned(),
)
.unwrap(),
};
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 grant = oauth
.grant_login_with_qr_code()
.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");
}
_ => {
panic!("Alice should abort the process");
}
}
state = update;
}
});
// Let Bob request the login and run through the process.
let bob_task = spawn(async move {
request_login_with_generated_qr_code(
BobBehaviour::DeviceAlreadyExists,
channel,
checkcode_rx,
server,
alice.homeserver(),
Some(device_authorization_grant),
None,
)
.await;
});
// Wait for all tasks to finish.
join!(
async { updates_task.await.expect("Alice should run through all progress states") },
async { grant.await.expect_err("Alice should abort the login") },
async { bob_task.await.expect("Bob's task should finish") }
);
}
#[async_test]
async fn test_grant_login_with_generated_qr_code_device_not_created() {
let server = MatrixMockServer::new().await;
@@ -954,4 +1653,124 @@ mod test {
async { bob_task.await.expect("Bob's task should finish") }
);
}
#[async_test]
async fn test_grant_login_with_scanned_qr_code_device_not_created() {
let server = MatrixMockServer::new().await;
let rendezvous_server = MockedRendezvousServer::new(server.server(), "abcdEFG12345").await;
debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url);
let device_authorization_grant = AuthorizationGrant {
verification_uri_complete: Some(VerificationUriComplete::new(
"https://id.matrix.org/device/abcde".to_owned(),
)),
verification_uri: EndUserVerificationUrl::new(
"https://id.matrix.org/device/abcde?code=ABCDE".to_owned(),
)
.unwrap(),
};
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 grant = oauth
.grant_login_with_qr_code()
.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();
let verification_uri_complete =
device_authorization_grant.clone().verification_uri_complete.unwrap().into_secret();
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");
}
GrantLoginProgress::WaitingForAuth { verification_uri } => {
assert_matches!(
state,
GrantLoginProgress::EstablishingSecureChannel(QrProgress { .. })
);
assert_eq!(verification_uri.as_str(), verification_uri_complete);
}
_ => {
panic!("Alice should abort the process");
}
}
state = update;
}
});
// Let Bob request the login and run through the process.
let bob_task = spawn(async move {
request_login_with_generated_qr_code(
BobBehaviour::DeviceNotCreated,
channel,
checkcode_rx,
server,
alice.homeserver(),
Some(device_authorization_grant),
None,
)
.await;
});
// Wait for all tasks to finish.
join!(
async { updates_task.await.expect("Alice should run through all progress states") },
async { grant.await.expect_err("Alice should abort the login") },
async { bob_task.await.expect("Bob's task should finish") }
);
}
}
@@ -28,6 +28,8 @@ use ruma::{
};
use tracing::trace;
use vodozemac::Curve25519PublicKey;
#[cfg(doc)]
use vodozemac::ecies::CheckCode;
use super::{
DeviceAuthorizationOAuthError, QRCodeLoginError, SecureChannelError,
@@ -247,7 +249,7 @@ pub enum LoginProgress<Q> {
#[default]
Starting,
/// We have established the secure channel, but need to exchange the
/// checkcode so the channel can be verified to indeed be secure.
/// [`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
@@ -278,7 +280,7 @@ impl LoginWithQrCode<'_> {
/// Subscribe to the progress of QR code login.
///
/// It's usually necessary to subscribe to this to let the existing device
/// know about the checkcode which is used to verify that the two
/// know about the [`CheckCode`] which is used to verify that the two
/// devices are communicating in a secure manner.
pub fn subscribe_to_progress(&self) -> impl Stream<Item = LoginProgress<QrProgress>> + use<> {
self.state.subscribe()
@@ -46,7 +46,7 @@ mod rendezvous_channel;
mod secure_channel;
pub use self::{
grant::{GrantLoginProgress, GrantLoginWithGeneratedQrCode},
grant::{GrantLoginProgress, GrantLoginWithGeneratedQrCode, GrantLoginWithScannedQrCode},
login::{LoginProgress, LoginWithGeneratedQrCode, LoginWithQrCode},
messages::{LoginFailureReason, LoginProtocolType, QrAuthMessage},
};