example_qr_login/
main.rs

1use std::io::Write;
2
3use anyhow::{Context, Result, bail};
4use clap::Parser;
5use futures_util::StreamExt;
6use matrix_sdk::{
7    Client,
8    authentication::oauth::{
9        qrcode::{LoginProgress, Msc4108IntentData, QrCodeData, QrCodeIntentData, QrProgress},
10        registration::{ApplicationType, ClientMetadata, Localized, OAuthGrantType},
11    },
12    ruma::serde::Raw,
13};
14use url::Url;
15
16/// A command line example showcasing how to login using a QR code.
17///
18/// Another device, which will display the QR code is needed to use this
19/// example.
20#[derive(Parser, Debug)]
21struct Cli {
22    /// Set the proxy that should be used for the connection.
23    #[clap(short, long)]
24    proxy: Option<Url>,
25
26    /// Enable verbose logging output.
27    #[clap(short, long, action)]
28    verbose: bool,
29}
30
31/// Generate the OAuth 2.0 client metadata.
32fn client_metadata() -> Raw<ClientMetadata> {
33    let client_uri = Localized::new(
34        Url::parse("https://github.com/matrix-org/matrix-rust-sdk")
35            .expect("Couldn't parse client URI"),
36        None,
37    );
38
39    let metadata = ClientMetadata {
40        // The following fields should be displayed in the OAuth 2.0 authorization server's web UI
41        // as part of the process to get the user's consent. It means that these should
42        // contain real data so the user can make sure that they allow the proper
43        // application. We are cheating here because this is an example.
44        client_name: Some(Localized::new("matrix-rust-sdk-qrlogin".to_owned(), [])),
45        policy_uri: Some(client_uri.clone()),
46        tos_uri: Some(client_uri.clone()),
47        ..ClientMetadata::new(
48            // This is a native application (in contrast to a web application, that runs in a
49            // browser).
50            ApplicationType::Native,
51            // We are going to use the Device Authorization flow.
52            vec![OAuthGrantType::DeviceCode],
53            client_uri,
54        )
55    };
56
57    Raw::new(&metadata).expect("Couldn't serialize client metadata")
58}
59
60async fn print_devices(client: &Client) -> Result<()> {
61    let user_id = client.user_id().unwrap();
62    let own_device =
63        client.encryption().get_own_device().await?.expect("We should have our own device by now");
64
65    println!(
66        "Status of our own device {}",
67        if own_device.is_cross_signed_by_owner() { "✅" } else { "❌" }
68    );
69
70    println!("Devices of user {user_id}");
71
72    for device in client.encryption().get_user_devices(user_id).await?.devices() {
73        if device.device_id()
74            == client.device_id().expect("We should be logged in now and know our device id")
75        {
76            continue;
77        }
78
79        println!(
80            "   {:<10} {:<30} {:<}",
81            device.device_id(),
82            device.display_name().unwrap_or("-"),
83            if device.is_verified() { "✅" } else { "❌" }
84        );
85    }
86
87    Ok(())
88}
89
90async fn login(proxy: Option<Url>) -> Result<()> {
91    println!("Please scan the QR code and convert the data to base64 before entering it here.");
92    println!("On Linux/Wayland, this can be achieved using the following command line:");
93    println!(
94        "    $ grim -g \"$(slurp)\" - | zbarimg --oneshot -Sbinary PNG:- | base64 -w 0 | wl-copy"
95    );
96    println!("Paste the QR code data here: ");
97
98    let mut input = String::new();
99    std::io::stdin().read_line(&mut input).expect("error: unable to read user input");
100    let input = input.trim();
101
102    let data = QrCodeData::from_base64(input).context("Couldn't parse the base64 QR code data")?;
103
104    let QrCodeIntentData::Msc4108 { data: Msc4108IntentData::Reciprocate { server_name }, .. } =
105        &data.intent_data()
106    else {
107        bail!("The QR code is invalid, we did not receive a homeserver in the QR code.");
108    };
109    let mut client = Client::builder().server_name_or_homeserver_url(server_name);
110
111    if let Some(proxy) = proxy {
112        client = client.proxy(proxy).disable_ssl_verification();
113    }
114
115    let client = client.build().await?;
116
117    let registration_data = client_metadata().into();
118    let oauth = client.oauth();
119
120    let login_client = oauth.login_with_qr_code(Some(&registration_data)).scan(&data);
121    let mut subscriber = login_client.subscribe_to_progress();
122
123    let task = tokio::spawn(async move {
124        while let Some(state) = subscriber.next().await {
125            match state {
126                LoginProgress::Starting | LoginProgress::SyncingSecrets => (),
127                LoginProgress::EstablishingSecureChannel(QrProgress { check_code }) => {
128                    let code = check_code.to_digit();
129                    println!("Please enter the following code into the other device {code:02}");
130                }
131                LoginProgress::WaitingForToken { user_code } => {
132                    println!("Please use your other device to confirm the log in {user_code}")
133                }
134                LoginProgress::Done => break,
135            }
136        }
137
138        std::io::stdout().flush().expect("Unable to write to stdout");
139    });
140
141    let result = login_client.await;
142    task.abort();
143
144    result?;
145
146    let status = client.encryption().cross_signing_status().await.unwrap();
147    let user_id = client.user_id().unwrap();
148
149    println!(
150        "Successfully logged in as {user_id} using the qr code, cross-signing status: {status:?}"
151    );
152
153    print_devices(&client).await?;
154
155    Ok(())
156}
157
158#[tokio::main]
159async fn main() -> Result<()> {
160    let cli = Cli::parse();
161
162    if cli.verbose {
163        tracing_subscriber::fmt::init();
164    }
165
166    login(cli.proxy).await?;
167
168    Ok(())
169}