diff --git a/moneymgr_backend/Cargo.lock b/moneymgr_backend/Cargo.lock index 8c21828..7182f5c 100644 --- a/moneymgr_backend/Cargo.lock +++ b/moneymgr_backend/Cargo.lock @@ -54,7 +54,7 @@ dependencies = [ "flate2", "foldhash", "futures-core", - "h2", + "h2 0.3.26", "http 0.2.12", "httparse", "httpdate", @@ -414,6 +414,12 @@ dependencies = [ "syn", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "attohttpc" version = "0.28.5" @@ -1218,6 +1224,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.3.1", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -1335,6 +1360,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2 0.4.8", "http 1.3.1", "http-body", "httparse", @@ -1345,6 +1371,23 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +dependencies = [ + "futures-util", + "http 1.3.1", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.6.0" @@ -1629,6 +1672,20 @@ version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +[[package]] +name = "light-openid" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a15777d080e807d5b6b3c0b5a293f7d4680d74a4c66b0cdf9db0441ea9f548" +dependencies = [ + "base64 0.22.1", + "log", + "reqwest", + "serde", + "serde_json", + "urlencoding", +] + [[package]] name = "linux-raw-sys" version = "0.9.3" @@ -1770,9 +1827,12 @@ dependencies = [ "env_logger", "futures-util", "lazy_static", + "light-openid", "log", + "rand 0.9.0", "rust-s3", "serde", + "serde_json", "thiserror", "tokio", ] @@ -2212,12 +2272,15 @@ checksum = "989e327e510263980e231de548a33e63d34962d29ae61b467389a1a09627a254" dependencies = [ "base64 0.22.1", "bytes", + "encoding_rs", "futures-core", "futures-util", + "h2 0.4.8", "http 1.3.1", "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-tls", "hyper-util", "ipnet", @@ -2233,6 +2296,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "sync_wrapper", + "system-configuration", "tokio", "tokio-native-tls", "tokio-util", @@ -2246,6 +2310,20 @@ dependencies = [ "windows-registry", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.15", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rust-ini" version = "0.21.1" @@ -2319,6 +2397,19 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustls" +version = "0.23.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + [[package]] name = "rustls-pemfile" version = "2.2.0" @@ -2334,6 +2425,17 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +[[package]] +name = "rustls-webpki" +version = "0.103.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa4eeac2588ffff23e9d7a7e9b3f971c5fb5b7ebc9452745e0c232c64f83b2f" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.20" @@ -2597,6 +2699,27 @@ dependencies = [ "syn", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.19.0" @@ -2718,6 +2841,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.17" @@ -2875,6 +3008,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.4" @@ -2886,6 +3025,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf16_iter" version = "1.0.5" @@ -3310,6 +3455,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + [[package]] name = "zerovec" version = "0.10.4" diff --git a/moneymgr_backend/Cargo.toml b/moneymgr_backend/Cargo.toml index 69b8451..50bf368 100644 --- a/moneymgr_backend/Cargo.toml +++ b/moneymgr_backend/Cargo.toml @@ -20,4 +20,7 @@ serde = { version = "1.0.219", features = ["derive"] } rust-s3 = "0.36.0-beta.2" thiserror = "1.0.69" tokio = "1.44.1" -futures-util = "0.3.31" \ No newline at end of file +futures-util = "0.3.31" +serde_json = "1.0.140" +light-openid = "1.0.4" +rand = "0.9.0" \ No newline at end of file diff --git a/moneymgr_backend/src/constants.rs b/moneymgr_backend/src/constants.rs index 70b786d..9fa093e 100644 --- a/moneymgr_backend/src/constants.rs +++ b/moneymgr_backend/src/constants.rs @@ -1 +1,5 @@ -// TODO +/// Session-specific constants +pub mod sessions { + /// OpenID auth session state key + pub const OIDC_STATE_KEY: &str = "oidc-state"; +} diff --git a/moneymgr_backend/src/controllers/auth_controller.rs b/moneymgr_backend/src/controllers/auth_controller.rs new file mode 100644 index 0000000..f1047f1 --- /dev/null +++ b/moneymgr_backend/src/controllers/auth_controller.rs @@ -0,0 +1,42 @@ +use crate::app_config::AppConfig; +use crate::controllers::HttpResult; +use crate::extractors::money_session::MoneySession; +use actix_web::HttpResponse; +use light_openid::primitives::OpenIDConfig; + +#[derive(serde::Serialize)] +struct StartOIDCResponse { + url: String, +} + +/// Start OIDC authentication +pub async fn start_oidc(session: MoneySession) -> HttpResult { + let prov = AppConfig::get().openid_provider(); + + let conf = match OpenIDConfig::load_from_url(prov.configuration_url).await { + Ok(c) => c, + Err(e) => { + log::error!("Failed to fetch OpenID provider configuration! {e}"); + return Ok(HttpResponse::InternalServerError() + .json("Failed to fetch OpenID provider configuration!")); + } + }; + + let state = match session.gen_oidc_state() { + Ok(s) => s, + Err(e) => { + log::error!("Failed to generate auth state! {e}"); + return Ok(HttpResponse::InternalServerError().json("Failed to generate auth state!")); + } + }; + + Ok(HttpResponse::Ok().json(StartOIDCResponse { + url: conf.gen_authorization_url( + prov.client_id, + &state, + &AppConfig::get().oidc_redirect_url(), + ), + })) +} + +// TODO : take from previous projects diff --git a/moneymgr_backend/src/controllers/mod.rs b/moneymgr_backend/src/controllers/mod.rs index 98a0949..dbcb9f1 100644 --- a/moneymgr_backend/src/controllers/mod.rs +++ b/moneymgr_backend/src/controllers/mod.rs @@ -1 +1,42 @@ +use actix_web::http::StatusCode; +use actix_web::{HttpResponse, ResponseError}; +use std::error::Error; + +pub mod auth_controller; pub mod server_controller; + +#[derive(thiserror::Error, Debug)] +pub enum HttpFailure { + #[error("this resource requires higher privileges")] + Forbidden, + #[error("this resource was not found")] + NotFound, + #[error("Actix web error")] + ActixError(#[from] actix_web::Error), + #[error("an unhandled session insert error occurred")] + SessionInsertError(#[from] actix_session::SessionInsertError), + #[error("an unhandled session error occurred")] + SessionError(#[from] actix_session::SessionGetError), + #[error("an unspecified open id error occurred: {0}")] + OpenID(Box), + #[error("an unspecified internal error occurred: {0}")] + InternalError(#[from] anyhow::Error), + #[error("a serde_json error occurred: {0}")] + SerdeJsonError(#[from] serde_json::error::Error), +} + +impl ResponseError for HttpFailure { + fn status_code(&self) -> StatusCode { + match &self { + Self::Forbidden => StatusCode::FORBIDDEN, + Self::NotFound => StatusCode::NOT_FOUND, + _ => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code()).body(self.to_string()) + } +} + +pub type HttpResult = Result; diff --git a/moneymgr_backend/src/extractors/mod.rs b/moneymgr_backend/src/extractors/mod.rs new file mode 100644 index 0000000..987b291 --- /dev/null +++ b/moneymgr_backend/src/extractors/mod.rs @@ -0,0 +1 @@ +pub mod money_session; diff --git a/moneymgr_backend/src/extractors/money_session.rs b/moneymgr_backend/src/extractors/money_session.rs new file mode 100644 index 0000000..f347e40 --- /dev/null +++ b/moneymgr_backend/src/extractors/money_session.rs @@ -0,0 +1,35 @@ +use crate::constants; +use crate::utils::rand_utils::rand_string; +use actix_session::Session; +use actix_web::dev::Payload; +use actix_web::{Error, FromRequest, HttpRequest}; +use futures_util::future::{Ready, ready}; + +/// Money session +/// +/// Basic wrapper around actix-session extractor +pub struct MoneySession(Session); + +impl MoneySession { + /// Generate OpenID state for this session + pub fn gen_oidc_state(&self) -> anyhow::Result { + let random_string = rand_string(50); + self.0 + .insert(constants::sessions::OIDC_STATE_KEY, random_string.clone())?; + Ok(random_string) + } +} + +impl FromRequest for MoneySession { + type Error = Error; + type Future = Ready>; + + #[inline] + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + ready( + Session::from_request(req, &mut Payload::None) + .into_inner() + .map(MoneySession), + ) + } +} diff --git a/moneymgr_backend/src/lib.rs b/moneymgr_backend/src/lib.rs index 340b1a7..bd9f046 100644 --- a/moneymgr_backend/src/lib.rs +++ b/moneymgr_backend/src/lib.rs @@ -2,6 +2,7 @@ pub mod app_config; pub mod connections; pub mod constants; pub mod controllers; +pub mod extractors; pub mod models; pub mod routines; pub mod schema; diff --git a/moneymgr_backend/src/main.rs b/moneymgr_backend/src/main.rs index b802208..dec765c 100644 --- a/moneymgr_backend/src/main.rs +++ b/moneymgr_backend/src/main.rs @@ -9,7 +9,7 @@ use actix_web::middleware::Logger; use actix_web::{App, HttpServer, web}; use moneymgr_backend::app_config::AppConfig; use moneymgr_backend::connections::{db_connection, s3_connection}; -use moneymgr_backend::controllers::server_controller; +use moneymgr_backend::controllers::{auth_controller, server_controller}; use moneymgr_backend::routines; use moneymgr_backend::services::users_service; @@ -72,6 +72,11 @@ async fn main() -> std::io::Result<()> { .app_data(TempFileConfig::default().directory(&AppConfig::get().temp_dir)) // Server controller .route("/robots.txt", web::get().to(server_controller::robots_txt)) + // Auth controller + .route( + "/api/auth/start_oidc", + web::get().to(auth_controller::start_oidc), + ) }) .bind(AppConfig::get().listen_address.as_str())? .run() diff --git a/moneymgr_backend/src/utils/mod.rs b/moneymgr_backend/src/utils/mod.rs index f8e456e..223d6b8 100644 --- a/moneymgr_backend/src/utils/mod.rs +++ b/moneymgr_backend/src/utils/mod.rs @@ -1 +1,2 @@ +pub mod rand_utils; pub mod time_utils; diff --git a/moneymgr_backend/src/utils/rand_utils.rs b/moneymgr_backend/src/utils/rand_utils.rs new file mode 100644 index 0000000..aa40750 --- /dev/null +++ b/moneymgr_backend/src/utils/rand_utils.rs @@ -0,0 +1,6 @@ +use rand::distr::{Alphanumeric, SampleString}; + +/// Generate a random string of a given length +pub fn rand_string(len: usize) -> String { + Alphanumeric.sample_string(&mut rand::rng(), len) +}