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:
committed by
Damir Jelić
parent
da1369b9c2
commit
b4ebc8bc25
@@ -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},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user