implement fj auth login

This commit is contained in:
Cyborus 2024-06-01 11:10:26 -04:00
parent c47a24ad22
commit 3ff6a86e8e
No known key found for this signature in database
4 changed files with 396 additions and 16 deletions

237
Cargo.lock generated
View file

@ -65,6 +65,23 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "async-trait"
version = "0.1.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]] [[package]]
name = "auth-git2" name = "auth-git2"
version = "0.5.4" version = "0.5.4"
@ -121,6 +138,15 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.16.0" version = "3.16.0"
@ -212,6 +238,25 @@ version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]]
name = "cpufeatures"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]] [[package]]
name = "deranged" name = "deranged"
version = "0.3.11" version = "0.3.11"
@ -222,6 +267,16 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]] [[package]]
name = "directories" name = "directories"
version = "5.0.1" version = "5.0.1"
@ -298,15 +353,20 @@ name = "fj"
version = "0.0.4" version = "0.0.4"
dependencies = [ dependencies = [
"auth-git2", "auth-git2",
"base64ct",
"clap", "clap",
"directories", "directories",
"eyre", "eyre",
"forgejo-api", "forgejo-api",
"futures", "futures",
"git2", "git2",
"hyper 1.3.1",
"hyper-util",
"open", "open",
"rand",
"serde", "serde",
"serde_json", "serde_json",
"sha256",
"soft_assert", "soft_assert",
"time", "time",
"tokio", "tokio",
@ -452,6 +512,16 @@ dependencies = [
"slab", "slab",
] ]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.14" version = "0.2.14"
@ -495,7 +565,26 @@ dependencies = [
"futures-core", "futures-core",
"futures-sink", "futures-sink",
"futures-util", "futures-util",
"http", "http 0.2.12",
"indexmap",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "h2"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab"
dependencies = [
"atomic-waker",
"bytes",
"fnv",
"futures-core",
"futures-sink",
"http 1.1.0",
"indexmap", "indexmap",
"slab", "slab",
"tokio", "tokio",
@ -521,6 +610,12 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]] [[package]]
name = "http" name = "http"
version = "0.2.12" version = "0.2.12"
@ -532,6 +627,17 @@ dependencies = [
"itoa", "itoa",
] ]
[[package]]
name = "http"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]] [[package]]
name = "http-body" name = "http-body"
version = "0.4.6" version = "0.4.6"
@ -539,10 +645,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
dependencies = [ dependencies = [
"bytes", "bytes",
"http", "http 0.2.12",
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "http-body"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
dependencies = [
"bytes",
"http 1.1.0",
]
[[package]] [[package]]
name = "httparse" name = "httparse"
version = "1.8.0" version = "1.8.0"
@ -565,9 +681,9 @@ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
"futures-util", "futures-util",
"h2", "h2 0.3.26",
"http", "http 0.2.12",
"http-body", "http-body 0.4.6",
"httparse", "httparse",
"httpdate", "httpdate",
"itoa", "itoa",
@ -579,6 +695,26 @@ dependencies = [
"want", "want",
] ]
[[package]]
name = "hyper"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"h2 0.4.5",
"http 1.1.0",
"http-body 1.0.0",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"smallvec",
"tokio",
]
[[package]] [[package]]
name = "hyper-tls" name = "hyper-tls"
version = "0.5.0" version = "0.5.0"
@ -586,12 +722,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [ dependencies = [
"bytes", "bytes",
"hyper", "hyper 0.14.28",
"native-tls", "native-tls",
"tokio", "tokio",
"tokio-native-tls", "tokio-native-tls",
] ]
[[package]]
name = "hyper-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56"
dependencies = [
"bytes",
"futures-util",
"http 1.1.0",
"http-body 1.0.0",
"hyper 1.3.1",
"pin-project-lite",
"tokio",
]
[[package]] [[package]]
name = "idna" name = "idna"
version = "0.5.0" version = "0.5.0"
@ -962,6 +1113,12 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.81" version = "1.0.81"
@ -980,6 +1137,36 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.1" version = "0.5.1"
@ -1011,10 +1198,10 @@ dependencies = [
"encoding_rs", "encoding_rs",
"futures-core", "futures-core",
"futures-util", "futures-util",
"h2", "h2 0.3.26",
"http", "http 0.2.12",
"http-body", "http-body 0.4.6",
"hyper", "hyper 0.14.28",
"hyper-tls", "hyper-tls",
"ipnet", "ipnet",
"js-sys", "js-sys",
@ -1156,6 +1343,30 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "sha2"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sha256"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18278f6a914fa3070aa316493f7d2ddfb9ac86ebc06fa3b83bffda487e9065b0"
dependencies = [
"async-trait",
"bytes",
"hex",
"sha2",
"tokio",
]
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.2" version = "1.4.2"
@ -1413,6 +1624,12 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "typenum"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]] [[package]]
name = "unicase" name = "unicase"
version = "2.7.0" version = "2.7.0"

View file

@ -7,15 +7,20 @@ edition = "2021"
[dependencies] [dependencies]
auth-git2 = "0.5.3" auth-git2 = "0.5.3"
base64ct = { version = "1.6.0", features = ["std"] }
clap = { version = "4.3.11", features = ["derive"] } clap = { version = "4.3.11", features = ["derive"] }
directories = "5.0.1" directories = "5.0.1"
eyre = "0.6.8" eyre = "0.6.8"
forgejo-api = "0.3.0" forgejo-api = "0.3.0"
futures = "0.3.28" futures = "0.3.28"
git2 = "0.17.2" git2 = "0.17.2"
hyper = "1.3.1"
hyper-util = { version = "0.1.5", features = ["tokio", "server", "http1", "http2"] }
open = "5.0.0" open = "5.0.0"
rand = "0.8.5"
serde = { version = "1.0.170", features = ["derive"] } serde = { version = "1.0.170", features = ["derive"] }
serde_json = "1.0.100" serde_json = "1.0.100"
sha256 = "1.5.0"
soft_assert = "0.1.1" soft_assert = "0.1.1"
time = { version = "0.3.30", features = ["formatting", "macros"] } time = { version = "0.3.30", features = ["formatting", "macros"] }
tokio = { version = "1.29.1", features = ["full"] } tokio = { version = "1.29.1", features = ["full"] }

View file

@ -1,4 +1,5 @@
use clap::Subcommand; use clap::Subcommand;
use eyre::OptionExt;
#[derive(Subcommand, Clone, Debug)] #[derive(Subcommand, Clone, Debug)]
pub enum AuthCommand { pub enum AuthCommand {
@ -18,12 +19,28 @@ pub enum AuthCommand {
} }
impl AuthCommand { impl AuthCommand {
pub async fn run(self, keys: &mut crate::KeyInfo) -> eyre::Result<()> { pub async fn run(self, keys: &mut crate::KeyInfo, host_name: Option<&str>) -> eyre::Result<()> {
match self { match self {
AuthCommand::Login => { AuthCommand::Login => {
todo!(); let repo_info = crate::repo::RepoInfo::get_current(host_name, None, None)?;
// let user = readline("username: ").await?; let host_url = repo_info.host_url();
// let pass = readline("password: ").await?; let client_info = get_client_info_for(host_url);
if let Some((client_id, _)) = client_info {
oauth_login(keys, host_url, client_id).await?;
} else {
let host_domain = host_url.host_str().ok_or_eyre("invalid host")?;
let host_path = host_url.path();
let mut applications_url = host_url.clone();
applications_url
.path_segments_mut()
.map_err(|_| eyre::eyre!("invalid url"))?
.extend(["user", "settings", "applications"]);
println!("{host_domain}{host_path} doesn't support easy login");
println!();
println!("Please visit {applications_url}");
println!("to create a token, and use it to log in with `fj auth add-token`");
}
} }
AuthCommand::Logout { host } => { AuthCommand::Logout { host } => {
let info_opt = keys.hosts.remove(&host); let info_opt = keys.hosts.remove(&host);
@ -64,10 +81,151 @@ impl AuthCommand {
} }
pub fn get_client_info_for(url: &url::Url) -> Option<(&'static str, &'static str)> { pub fn get_client_info_for(url: &url::Url) -> Option<(&'static str, &'static str)> {
let host = url.host_str()?;
let client_info = match (url.host_str()?, url.path()) { let client_info = match (url.host_str()?, url.path()) {
("codeberg.org", "/") => option_env!("CLIENT_INFO_CODEBERG"), ("codeberg.org", "/") => option_env!("CLIENT_INFO_CODEBERG"),
_ => None, _ => None,
}; };
client_info.and_then(|info| info.split_once(":")) client_info.and_then(|info| info.split_once(":"))
} }
async fn oauth_login(
keys: &mut crate::KeyInfo,
host: &url::Url,
client_id: &'static str,
) -> eyre::Result<()> {
use base64ct::Encoding;
use rand::{distributions::Alphanumeric, prelude::*};
let mut rng = thread_rng();
let state = (0..32)
.map(|_| rng.sample(Alphanumeric) as char)
.collect::<String>();
let code_verifier = (0..43)
.map(|_| rng.sample(Alphanumeric) as char)
.collect::<String>();
let code_challenge =
base64ct::Base64Url::encode_string(sha256::digest(&code_verifier).as_bytes());
let mut auth_url = host.clone();
auth_url
.path_segments_mut()
.map_err(|_| eyre::eyre!("invalid url"))?
.extend(["login", "oauth", "authorize"]);
auth_url.query_pairs_mut().extend_pairs([
("client_id", client_id),
("redirect_uri", "http://127.0.0.1:26218/"),
("response_type", "code"),
("code_challenge_method", "S256"),
("code_challenge", &code_challenge),
("state", &state),
]);
open::that(auth_url.as_str()).unwrap();
let (handle, mut rx) = auth_server();
let res = rx.recv().await.unwrap();
handle.abort();
let code = match res {
Ok(Some((code, returned_state))) => {
if returned_state == state {
code
} else {
eyre::bail!("returned with invalid state");
}
}
Ok(None) => {
println!("Login canceled");
return Ok(());
}
Err(e) => {
eyre::bail!("Failed to authenticate: {e}");
}
};
let api = forgejo_api::Forgejo::new(forgejo_api::Auth::None, host.clone())?;
let request = forgejo_api::structs::OAuthTokenRequest::Public {
client_id,
code_verifier: &code_verifier,
code: &code,
redirect_uri: url::Url::parse("http://127.0.0.1:26218/").unwrap(),
};
let response = api.oauth_get_access_token(request).await?;
let api = forgejo_api::Forgejo::new(
forgejo_api::Auth::OAuth2(&response.access_token),
host.clone(),
)?;
let current_user = api.user_get_current().await?;
let name = current_user
.login
.ok_or_eyre("user does not have login name")?;
// A minute less, in case any weirdness happens at the exact moment it
// expires. Better to refresh slightly too soon than slightly too late.
let expires_in = std::time::Duration::from_secs(response.expires_in.saturating_sub(60) as u64);
let expires_at = time::OffsetDateTime::now_utc() + expires_in;
let login_info = crate::keys::LoginInfo::OAuth {
name,
token: response.access_token,
refresh_token: response.refresh_token,
expires_at,
};
keys.hosts
.insert(host.host_str().unwrap().to_string(), login_info);
Ok(())
}
use tokio::{sync::mpsc::Receiver, task::JoinHandle};
fn auth_server() -> (
JoinHandle<eyre::Result<()>>,
Receiver<Result<Option<(String, String)>, String>>,
) {
let addr: std::net::SocketAddr = ([127, 0, 0, 1], 26218).into();
let (tx, rx) = tokio::sync::mpsc::channel(1);
let tx = std::sync::Arc::new(tx);
let handle = tokio::spawn(async move {
let listener = tokio::net::TcpListener::bind(addr).await?;
let server =
hyper_util::server::conn::auto::Builder::new(hyper_util::rt::TokioExecutor::new());
let svc = hyper::service::service_fn(|req: hyper::Request<hyper::body::Incoming>| {
let tx = std::sync::Arc::clone(&tx);
async move {
let mut code = None;
let mut state = None;
let mut error_description = None;
if let Some(query) = req.uri().query() {
for item in query.split("&") {
let (key, value) = item.split_once("=").unwrap_or((item, ""));
match key {
"code" => code = Some(value),
"state" => state = Some(value),
"error_description" => error_description = Some(value),
_ => eprintln!("unknown key {key} {value}"),
}
}
}
let (response, message) = match (code, state, error_description) {
(_, _, Some(error)) => (Err(error.to_owned()), "Failed to authenticate"),
(Some(code), Some(state), None) => (
Ok(Some((code.to_owned(), state.to_owned()))),
"Authenticated! Close this tab and head back to your terminal",
),
_ => (Ok(None), "Canceled"),
};
tx.send(response).await.unwrap();
Ok::<_, hyper::Error>(hyper::Response::new(message.to_owned()))
}
});
loop {
let (connection, _addr) = listener.accept().await.unwrap();
server
.serve_connection(hyper_util::rt::TokioIo::new(connection), svc)
.await
.unwrap();
}
Ok(())
});
(handle, rx)
}

View file

@ -68,7 +68,7 @@ async fn main() -> eyre::Result<()> {
println!("currently signed in to {name}@{host}{}", url.path()); println!("currently signed in to {name}@{host}{}", url.path());
} }
} }
Command::Auth(subcommand) => subcommand.run(&mut keys).await?, Command::Auth(subcommand) => subcommand.run(&mut keys, host_name).await?,
Command::Release(subcommand) => subcommand.run(&mut keys, host_name).await?, Command::Release(subcommand) => subcommand.run(&mut keys, host_name).await?,
} }