Skip to content

Commit

Permalink
add login / logout commands
Browse files Browse the repository at this point in the history
  • Loading branch information
arlyon committed Jun 11, 2024
1 parent 3c5b262 commit 4478b09
Show file tree
Hide file tree
Showing 8 changed files with 884 additions and 44 deletions.
606 changes: 563 additions & 43 deletions Cargo.lock

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,12 @@ cache-provider = "buildjet"
[workspace.metadata.dist.github-custom-runners]
aarch64-unknown-linux-gnu = "buildjet-8vcpu-ubuntu-2204-arm"
aarch64-unknown-linux-musl = "buildjet-8vcpu-ubuntu-2204-arm"

[workspace.metadata.dist.dependencies.homebrew]
flatbuffers = '*'

[workspace.metadata.dist.dependencies.apt]
flatbuffers = '*'

[workspace.metadata.dist.dependencies.chocolatey]
flatc = '*'
3 changes: 3 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Version 0.3.0

- Integrated registry auth into the command line
30 changes: 30 additions & 0 deletions crates/auth/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[package]
name = "litehouse-auth"
version = "0.1.0"
repository.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
description.workspace = true
homepage.workspace = true

[dependencies]
bytes = "1.6.0"
http-body-util = "0.1.2"
hyper = { version = "1.3.1", features = ["http1", "http2", "server"] }
hyper-util = { version = "0.1.5", features = ["tokio"] }
oauth = "0.0.1"
oauth2 = { version = "4.4.2", features = ["pkce-plain"] }
open = "5.1.4"
reqwest = { version = "0.12.4", features = ["json"] }
serde = { version = "1.0.203", features = ["derive"] }
serde_qs = "0.13.0"
serde_urlencoded = "0.7.1"
tokio = { version = "1.38.0", features = ["macros", "signal"] }
tokio-util = { version = "0.7.11", features = ["codec"] }
tower-service = "0.3.2"
tracing = "0.1.40"
url = "2.5.1"

[dev-dependencies]
tokio = { version = "1.38.0", features = ["macros"] }
186 changes: 186 additions & 0 deletions crates/auth/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
use std::convert::Infallible;

use bytes::Bytes;
use http_body_util::Empty;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::StatusCode;
use hyper_util::rt::TokioIo;
use oauth2::basic::{BasicClient, BasicTokenType};
use oauth2::reqwest::async_http_client;
pub use oauth2::{AuthUrl, ClientId, TokenUrl};
use oauth2::{
AuthorizationCode, CsrfToken, EmptyExtraTokenFields, PkceCodeChallenge, RedirectUrl, Scope,
StandardTokenResponse, TokenResponse as _,
};
use serde::Deserialize;

pub type TokenResponse = StandardTokenResponse<EmptyExtraTokenFields, BasicTokenType>;

#[derive(Deserialize)]
struct QsData {
code: String,
state: String,
}

#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub struct AuthUser {
pub email: String,
pub family_name: Option<String>,
pub given_name: Option<String>,
pub name: Option<String>,
pub picture: Option<String>,
pub username: Option<String>,
pub user_id: String,
}

/// Get a token for use with the litehouse API.
///
/// If the access token has more than 30 minutes left, it will be returned as-is.
/// If the refresh token is still valid, it will be used to get a new access token.
/// Otherwise, a new access token will be requested.
pub async fn get_token(
client_id: ClientId,
auth_url: AuthUrl,
token_url: TokenUrl,
) -> Result<TokenResponse, ()> {
let redirect_url =
RedirectUrl::new("http://localhost:9789/oauth2/callback".to_string()).unwrap();

let client = BasicClient::new(client_id, None, auth_url, Some(token_url))
// Set the URL the user will be redirected to after the authorization process.
.set_redirect_uri(redirect_url);

// Generate a PKCE challenge.
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();

// Generate the full authorization URL.
let (auth_url, csrf_token) = client
.authorize_url(CsrfToken::new_random)
// Set the desired scopes.
.add_scope(Scope::new("profile".to_string()))
.add_scope(Scope::new("email".to_string()))
// Set the PKCE code challenge.
.set_pkce_challenge(pkce_challenge)
.url();

let socket = tokio::net::TcpListener::bind(("0.0.0.0", 9789))
.await
.unwrap();
let (token_tx, mut token_rx) = tokio::sync::watch::channel(None);

if open::that(auth_url.to_string()).is_err() {
println!(
"Please open the following url in your browser: {}",
auth_url
);
}

let token = loop {
tokio::select! {
_ = tokio::signal::ctrl_c() => {
return Err(());
},
conn = socket.accept() => {
let (conn, _addr) = conn.unwrap();
let io = TokioIo::new(conn);
let token_tx = token_tx.clone();

tokio::task::spawn(async move {
http1::Builder::new()
.serve_connection(
io,
service_fn(|req| {
let token_tx = token_tx.clone();
async move {
let mut resp = hyper::Response::new(Empty::<Bytes>::new());
let Some(qs) = req.uri().query() else {
*resp.status_mut() = StatusCode::BAD_REQUEST;
return Ok::<_, Infallible>(resp);
};

let Ok(data) = serde_qs::from_str::<QsData>(qs) else {
*resp.status_mut() = StatusCode::BAD_REQUEST;
return Ok::<_, Infallible>(resp);
};

token_tx.send(Some(data.code)).unwrap();

Ok::<_, Infallible>(resp)
}
}),
)
.await
.unwrap();
});
}
token = token_rx.wait_for(|v| v.is_some()) => {
let token = token.unwrap();
break token.as_ref().unwrap().to_owned();
}
}
};

let token_result = client
.exchange_code(AuthorizationCode::new(token))
// Set the PKCE code verifier.
.set_pkce_verifier(pkce_verifier)
.request_async(async_http_client)
.await
.unwrap();

Ok(token_result)
}

// do a request to `user_info_url` with the token as a Authorization header
pub async fn get_user(token: &TokenResponse, user_info_url: &str) -> Result<AuthUser, ()> {
// get hyper client and make request
let client = reqwest::Client::new();
let resp = client
.get(user_info_url)
.header(
"Authorization",
format!("Bearer {}", token.access_token().secret()),
)
.send()
.await
.unwrap();

// check response status
if !resp.status().is_success() {
return Err(());
}

// read response body as json
let user: AuthUser = resp.json().await.unwrap();

Ok(user)
}

#[cfg(test)]
mod tests {

use super::*;

#[tokio::test]
#[ignore]
async fn can_get_token() {
let client_id = ClientId::new("Xdxbwyvdo4gce8jw".to_string());
let auth_url =
AuthUrl::new("https://clerk.arlyon.dev/oauth/authorize".to_string()).unwrap();
let token_url = TokenUrl::new("https://clerk.arlyon.dev/oauth/token".to_string()).unwrap();

let result = get_token(client_id, auth_url, token_url).await.unwrap();

println!("{:?}", result.access_token().secret());

let user = get_user(
result,
"https://clerk.arlyon.dev/oauth/userinfo".to_string(),
)
.await
.unwrap();

println!("{:?}", user);
}
}
4 changes: 3 additions & 1 deletion crates/litehouse/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "litehouse"
version = "0.2.0"
version = "0.3.0"
description = "A lightweight home automation server"
edition.workspace = true
license.workspace = true
Expand Down Expand Up @@ -66,6 +66,8 @@ which = "6.0.1"
inquire = "0.7.5"
opendal-fs-cache = { version = "0.1.0", path = "../opendal-fs-cache" }
litehouse-registry = { version = "0.1.0", path = "../registry" }
litehouse-auth = { version = "0.1.0", path = "../auth" }
serde_json = "1.0.117"

[dev-dependencies]
rusty-hook = "0.11.2"
79 changes: 79 additions & 0 deletions crates/litehouse/src/cmd/auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
use std::fs::OpenOptions;

use litehouse_auth::{self, AuthUrl, AuthUser, ClientId, TokenResponse, TokenUrl};

#[derive(clap::Subcommand)]
pub enum AuthCommand {
/// Get the currently logged in user
Whoami {
#[clap(long)]
verbose: bool,
},
/// Log in to litehouse.arlyon.dev
Login,
/// Log out of litehouse.arlyon.dev
Logout,
}

#[derive(serde::Serialize, serde::Deserialize, Debug)]
struct AuthConfig {
tokens: TokenResponse,
user: AuthUser,
}

pub async fn do_auth(auth_command: AuthCommand) {
let project_dirs = litehouse_config::directories().unwrap();
std::fs::create_dir_all(project_dirs.config_dir()).unwrap();
let auth_path = project_dirs.config_dir().join("auth.json");
tracing::debug!("auth path: {auth_path:?}");
let auth_file = OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.open(&auth_path)
.unwrap();

match auth_command {
AuthCommand::Whoami { verbose } => {
// try to parse the auth file. if it fails, we are not logged in
let data: Option<AuthConfig> = serde_json::from_reader(auth_file)
.map(Some)
.unwrap_or_default();

if let Some(data) = data {
println!("logged in as {}", data.user.email);
if verbose {
println!("{:#?}", data);
}
} else {
println!("not logged in");
}
}
AuthCommand::Login => {
let client_id = ClientId::new("Xdxbwyvdo4gce8jw".to_string());
let auth_url =
AuthUrl::new("https://clerk.arlyon.dev/oauth/authorize".to_string()).unwrap();
let token_url =
TokenUrl::new("https://clerk.arlyon.dev/oauth/token".to_string()).unwrap();

let tokens = litehouse_auth::get_token(client_id, auth_url, token_url)
.await
.unwrap();

let user = litehouse_auth::get_user(&tokens, "https://clerk.arlyon.dev/oauth/userinfo")
.await
.unwrap();

let data = AuthConfig { tokens, user };

serde_json::to_writer_pretty(&auth_file, &data).unwrap();

println!("logged in as {}", data.user.email);
}
AuthCommand::Logout => {
drop(auth_file);
std::fs::remove_file(auth_path);
}
}
}
11 changes: 11 additions & 0 deletions crates/litehouse/src/cmd/mod.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
//! The main commands for the Litehouse CLI.

mod auth;
mod generate;
mod packages;
mod run;
mod validate;

use std::path::PathBuf;

use auth::AuthCommand;
use jsonc_parser::CollectOptions;
use jsonschema::{error::ValidationErrorKind, paths::PathChunk};
use litehouse_config::{
Expand Down Expand Up @@ -120,6 +122,11 @@ pub enum Subcommand {
/// Send any feedback! Note that this will be sent to the litehouse team
/// with your git email and name (so that we can get in touch).
Feedback { message: String },
/// Authenticate with litehouse.arlyon.dev to upload plugins
Auth {
#[command(subcommand)]
auth_command: AuthCommand,
},
}

impl Subcommand {
Expand Down Expand Up @@ -530,6 +537,10 @@ impl Subcommand {

Ok(())
}
Subcommand::Auth { auth_command } => {
auth::do_auth(auth_command).await;
Ok(())
}
}
}
}

0 comments on commit 4478b09

Please sign in to comment.