diff --git a/Cargo.lock b/Cargo.lock index d816021..d227cef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2233,6 +2233,18 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "noop" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum 0.6.16", + "clap 4.4.7", + "nefarious-login", + "tokio", + "tracing-subscriber", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -4089,6 +4101,9 @@ name = "uuid" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" +dependencies = [ + "getrandom", +] [[package]] name = "valuable" diff --git a/Cargo.toml b/Cargo.toml index c6bdf52..f5c2deb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,10 +27,10 @@ async-sqlx-session = { version = "0.4.0", features = ["pg"] } serde = { version = "1", features = ["derive"] } serde_json = { version = "1" } -uuid = { version = "1.5", features = [] } +uuid = {version = "1.5.0", features = ["v4"]} sqlx = { version = "0.7", features = [ -"runtime-tokio", - "postgres", + "runtime-tokio", + "postgres", "migrate", ] } diff --git a/crates/nefarious-login/src/auth.rs b/crates/nefarious-login/src/auth.rs index fbf7793..9e8c1c0 100644 --- a/crates/nefarious-login/src/auth.rs +++ b/crates/nefarious-login/src/auth.rs @@ -26,12 +26,17 @@ pub struct AuthService(Arc); impl AuthService { pub async fn new(config: &AuthClap) -> anyhow::Result { match config.engine { - AuthEngine::Noop => Ok(Self::new_noop()), + AuthEngine::Noop => { + let session = SessionService::new(config).await?; + + Ok(Self::new_noop(session, &config.config)) + } AuthEngine::Zitadel => { let session = SessionService::new(config).await?; let oauth: OAuth = ZitadelConfig::try_from(config.zitadel.clone())?.into(); let introspection: IntrospectionService = IntrospectionService::new_zitadel(config).await?; + Ok(Self::new_zitadel( oauth, introspection, @@ -56,8 +61,11 @@ impl AuthService { })) } - pub fn new_noop() -> Self { - Self(Arc::new(NoopAuthService {})) + pub fn new_noop(session: SessionService, config: &ConfigClap) -> Self { + Self(Arc::new(NoopAuthService { + session, + config: config.clone(), + })) } } @@ -111,26 +119,62 @@ impl Auth for ZitadelAuthService { } } -pub struct NoopAuthService {} +pub struct NoopAuthService { + session: SessionService, + config: ConfigClap, +} #[async_trait] impl Auth for NoopAuthService { async fn login(&self) -> anyhow::Result { - todo!() + let url = Url::parse(&format!( + "{}/auth/authorized?code=noop&state=noop", + self.config + .return_url + .rsplit_once('/') + .map(|(a, b)| a) + .unwrap() + )) + .unwrap(); + Ok(url) } async fn login_authorized( &self, _code: &str, _state: &str, ) -> anyhow::Result<(HeaderMap, Url)> { - todo!() + let cookie_value = self + .session + .insert_user( + "user", + IdToken { + sub: uuid::Uuid::new_v4().to_string(), + email: format!("{}@kjuulh.io", uuid::Uuid::new_v4()), + name: uuid::Uuid::new_v4().to_string(), + }, + ) + .await?; + + let cookie = format!("{}={}; SameSite=Lax; Path=/", COOKIE_NAME, cookie_value); + + let mut headers = HeaderMap::new(); + headers.insert(SET_COOKIE, cookie.parse().unwrap()); + + Ok(( + headers, + Url::parse(&self.config.return_url) + .context("failed to parse login_authorized zitadel return url")?, + )) } async fn login_token(&self, _user: &str, _password: &str) -> anyhow::Result { todo!() } - async fn get_user_from_session(&self, _cookie: &str) -> anyhow::Result { - todo!() + async fn get_user_from_session(&self, cookie: &str) -> anyhow::Result { + match self.session.get_user(cookie).await? { + Some(u) => Ok(u), + None => Err(anyhow::anyhow!("failed to find user")), + } } } diff --git a/crates/nefarious-login/src/login/auth_clap.rs b/crates/nefarious-login/src/login/auth_clap.rs index 4f2d6e1..5dfa4cd 100644 --- a/crates/nefarious-login/src/login/auth_clap.rs +++ b/crates/nefarious-login/src/login/auth_clap.rs @@ -15,7 +15,7 @@ pub struct AuthConfigFile { zitadel: Option, } -#[derive(clap::Args, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(clap::Args, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)] #[group(requires_all = ["client_id", "client_secret", "redirect_url", "authority_url"])] pub struct ZitadelClap { #[arg(env = "ZITADEL_CLIENT_ID", long = "zitadel-client-id")] diff --git a/crates/nefarious-login/src/session.rs b/crates/nefarious-login/src/session.rs index 1667bc1..8db0793 100644 --- a/crates/nefarious-login/src/session.rs +++ b/crates/nefarious-login/src/session.rs @@ -19,7 +19,7 @@ pub struct SessionClap { pub postgresql: PostgresqlSessionClap, } -#[derive(clap::Args, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(clap::Args, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct PostgresqlSessionClap { #[arg(env = "SESSION_POSTGRES_CONN", long = "session-postgres-conn")] pub conn: Option, @@ -36,7 +36,7 @@ pub struct SessionService(Arc); impl SessionService { pub async fn new(config: &AuthClap) -> anyhow::Result { match config.session_backend { - SessionBackend::InMemory => Ok(Self(Arc::new(InMemorySessionService {}))), + SessionBackend::InMemory => Ok(Self(Arc::new(InMemorySessionService::default()))), SessionBackend::Postgresql => { let postgres_session = PostgresSessionStore::new( config @@ -119,15 +119,30 @@ impl Session for PostgresSessionService { } } -pub struct InMemorySessionService {} +#[derive(Default)] +pub struct InMemorySessionService { + store: std::sync::Mutex>, +} #[async_trait] impl Session for InMemorySessionService { - async fn insert_user(&self, _id: &str, _id_token: IdToken) -> anyhow::Result { - todo!() + async fn insert_user(&self, _id: &str, id_token: IdToken) -> anyhow::Result { + let user = User { + id: id_token.sub, + email: id_token.email, + name: id_token.name, + }; + + let id = uuid::Uuid::new_v4(); + + self.store.lock().unwrap().insert(id.to_string(), user); + + Ok(id.to_string()) } - async fn get_user(&self, _cookie: &str) -> anyhow::Result> { - todo!() + async fn get_user(&self, cookie: &str) -> anyhow::Result> { + let user = self.store.lock().unwrap().get(cookie).cloned(); + + Ok(user) } } diff --git a/examples/noop/Cargo.toml b/examples/noop/Cargo.toml new file mode 100644 index 0000000..7a6de43 --- /dev/null +++ b/examples/noop/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "noop" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +nefarious-login.workspace = true + +tokio.workspace = true +anyhow.workspace = true +axum.workspace = true +clap.workspace = true + +tracing-subscriber.workspace = true diff --git a/examples/noop/src/main.rs b/examples/noop/src/main.rs new file mode 100644 index 0000000..b64e808 --- /dev/null +++ b/examples/noop/src/main.rs @@ -0,0 +1,84 @@ +use std::{net::SocketAddr, str::FromStr}; + +use axum::{ + extract::{FromRef, State}, + response::IntoResponse, + routing::get, + Router, +}; +use nefarious_login::{ + auth::AuthService, + axum::{AuthController, UserFromSession}, + login::{ + auth_clap::{AuthEngine, ZitadelClap}, + config::ConfigClap, + AuthClap, + }, + session::{PostgresqlSessionClap, SessionBackend}, +}; +use tracing_subscriber::EnvFilter; + +#[derive(Clone)] +struct AppState { + auth: AuthService, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .init(); + + let auth = AuthClap { + engine: AuthEngine::Noop, + session_backend: SessionBackend::InMemory, + zitadel: ZitadelClap { + ..Default::default() + }, + session: nefarious_login::session::SessionClap { + postgresql: PostgresqlSessionClap { + ..Default::default() + }, + }, + config: ConfigClap { + return_url: "http://localhost:3000/authed".into(), + }, + }; + + let auth_service = AuthService::new(&auth).await?; + + let state = AppState { + auth: auth_service.clone(), + }; + + let app = Router::new() + .route("/unauthed", get(unauthed)) + .route("/authed", get(authed)) + .with_state(state) + .nest("/auth", AuthController::new_router(auth_service).await?); + + let addr = SocketAddr::from_str(&format!("{}:{}", "127.0.0.1", "3000"))?; + let listener = tokio::net::TcpListener::bind(&addr).await?; + + axum::serve(listener, app).await?; + + Ok(()) +} + +impl FromRef for AuthService { + fn from_ref(input: &AppState) -> Self { + input.auth.clone() + } +} + +async fn unauthed() -> String { + "Hello Unauthorized User".into() +} + +#[axum::debug_handler()] +async fn authed( + user: UserFromSession, + State(_auth_service): State, +) -> impl IntoResponse { + format!("Hello authorized user: {:?}", user.user.id) +}