Skip to content

Commit 7dd08c3

Browse files
committed
Add an example for the QR code login
1 parent e6dc24a commit 7dd08c3

File tree

3 files changed

+225
-1
lines changed

3 files changed

+225
-1
lines changed

Cargo.lock

Lines changed: 23 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/qr-login/Cargo.toml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[package]
2+
name = "example-qr-login"
3+
version = "0.1.0"
4+
edition = "2021"
5+
publish = false
6+
7+
[[bin]]
8+
name = "example-qr-login"
9+
test = false
10+
11+
[dependencies]
12+
anyhow = "1"
13+
tokio = { version = "1.24.2", features = ["macros", "rt-multi-thread"] }
14+
clap = { version = "4.0.15", features = ["derive"] }
15+
qrcode = { git = "https://github.com/kennytm/qrcode-rust/" }
16+
futures-util = "0.3.24"
17+
tracing-subscriber = "0.3.16"
18+
url = "2.3.1"
19+
20+
[dependencies.matrix-sdk]
21+
# when copy-pasting this, please use a git dependency or make sure that you
22+
# have copied the example as it was at the time of the release you use.
23+
path = "../../crates/matrix-sdk"
24+
features = ["experimental-oidc"]

examples/qr-login/src/main.rs

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

0 commit comments

Comments
 (0)