Skip to content

Commit

Permalink
Add an example for the QR code login
Browse files Browse the repository at this point in the history
  • Loading branch information
poljar committed May 28, 2024
1 parent e6dc24a commit 7dd08c3
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 1 deletion.
24 changes: 23 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions examples/qr-login/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "example-qr-login"
version = "0.1.0"
edition = "2021"
publish = false

[[bin]]
name = "example-qr-login"
test = false

[dependencies]
anyhow = "1"
tokio = { version = "1.24.2", features = ["macros", "rt-multi-thread"] }
clap = { version = "4.0.15", features = ["derive"] }
qrcode = { git = "https://github.com/kennytm/qrcode-rust/" }
futures-util = "0.3.24"
tracing-subscriber = "0.3.16"
url = "2.3.1"

[dependencies.matrix-sdk]
# when copy-pasting this, please use a git dependency or make sure that you
# have copied the example as it was at the time of the release you use.
path = "../../crates/matrix-sdk"
features = ["experimental-oidc"]
178 changes: 178 additions & 0 deletions examples/qr-login/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
use std::io::Write;

use anyhow::{bail, Context, Result};
use clap::Parser;
use futures_util::StreamExt;
use matrix_sdk::{
authentication::qrcode::{LoginProgress, QrCodeData, QrCodeModeData},
oidc::types::{
iana::oauth::OAuthClientAuthenticationMethod,
oidc::ApplicationType,
registration::{ClientMetadata, Localized, VerifiedClientMetadata},
requests::GrantType,
},
Client,
};
use url::Url;

/// A command line example showcasing how to login using a QR code.
///
/// Another device, which will display the QR code is needed to use this
/// example.
#[derive(Parser, Debug)]
struct Cli {
/// Set the proxy that should be used for the connection.
#[clap(short, long)]
proxy: Option<Url>,

/// Enable verbose logging output.
#[clap(short, long, action)]
verbose: bool,
}

/// Generate the OIDC client metadata.
///
/// For simplicity, we use most of the default values here, but usually this
/// should be adapted to the provider metadata to make interactions as secure as
/// possible, for example by using the most secure signing algorithms supported
/// by the provider.
fn client_metadata() -> VerifiedClientMetadata {
let client_uri = Url::parse("https://github.com/matrix-org/matrix-rust-sdk")
.expect("Couldn't parse client URI");

ClientMetadata {
// This is a native application (in contrast to a web application, that runs in a browser).
application_type: Some(ApplicationType::Native),
// Native clients should be able to register the loopback interface and then point to any
// port when needing a redirect URI. An alternative is to use a custom URI scheme registered
// with the OS.
redirect_uris: None,
// We are going to use the Authorization Code flow, and of course we want to be able to
// refresh our access token.
grant_types: Some(vec![GrantType::RefreshToken, GrantType::DeviceCode]),
// A native client shouldn't use authentication as the credentials could be intercepted.
// Other protections are in place for the different requests.
token_endpoint_auth_method: Some(OAuthClientAuthenticationMethod::None),
// The following fields should be displayed in the OIDC provider interface as part of the
// process to get the user's consent. It means that these should contain real data so the
// user can make sure that they allow the proper application.
// We are cheating here because this is an example.
client_name: Some(Localized::new("matrix-rust-sdk-qrlogin".to_owned(), [])),
contacts: Some(vec!["root@127.0.0.1".to_owned()]),
client_uri: Some(Localized::new(client_uri.clone(), [])),
policy_uri: Some(Localized::new(client_uri.clone(), [])),
tos_uri: Some(Localized::new(client_uri, [])),
..Default::default()
}
.validate()
.unwrap()
}

async fn print_devices(client: &Client) -> Result<()> {
let user_id = client.user_id().unwrap();
let own_device =
client.encryption().get_own_device().await?.expect("We should have our own device by now");

println!(
"Status of our own device {}",
if own_device.is_cross_signed_by_owner() { "✅" } else { "❌" }
);

println!("Devices of user {user_id}");

for device in client.encryption().get_user_devices(user_id).await?.devices() {
if device.device_id()
== client.device_id().expect("We should be logged in now and know our device id")
{
continue;
}

println!(
" {:<10} {:<30} {:<}",
device.device_id(),
device.display_name().unwrap_or("-"),
if device.is_verified() { "✅" } else { "❌" }
);
}

Ok(())
}

async fn login(proxy: Option<Url>) -> Result<()> {
println!("Please scan the QR code and convert the data to base64 before entering it here.");
println!("On Linux/Wayland, this can be achieved using the following command line:");
println!(
" $ grim -g \"$(slurp)\" - | zbarimg --oneshot -Sbinary PNG:- | base64 -w 0 | wl-copy"
);
println!("Paste the QR code data here: ");

let mut input = String::new();
std::io::stdin().read_line(&mut input).expect("error: unable to read user input");
let input = input.trim();

let data = QrCodeData::from_base64(input).context("Couldn't parse the base64 QR code data")?;

let QrCodeModeData::Reciprocate { homeserver_url } = &data.mode_data else {
bail!("The QR code is invalid, we did not receive a homeserver in the QR code.");
};
let mut client = Client::builder().server_name_or_homeserver_url(homeserver_url);

if let Some(proxy) = proxy {
client = client.proxy(proxy).disable_ssl_verification();
}

let client = client.build().await?;

let metadata = client_metadata();
let oidc = client.oidc();

let login_client = oidc.login_with_qr_code(&data, metadata);
let mut subscriber = login_client.subscribe_to_progress();

let task = tokio::spawn(async move {
while let Some(state) = subscriber.next().await {
match state {
LoginProgress::Starting => (),
LoginProgress::EstablishingSecureChannel { 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,
}
}

std::io::stdout().flush().expect("Unable to write to stdout");
});

let result = login_client.await;
task.abort();

result?;

let status = client.encryption().cross_signing_status().await.unwrap();
let user_id = client.user_id().unwrap();

println!(
"Successfully logged in as {user_id} using the qr code, cross-signing status: {status:?}"
);

print_devices(&client).await?;

Ok(())
}

#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();

if cli.verbose {
tracing_subscriber::fmt::init();
}

login(cli.proxy).await?;

Ok(())
}

0 comments on commit 7dd08c3

Please sign in to comment.