diff --git a/geneit_backend/Cargo.lock b/geneit_backend/Cargo.lock index 3cdfa97..e68b7d6 100644 --- a/geneit_backend/Cargo.lock +++ b/geneit_backend/Cargo.lock @@ -379,6 +379,12 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + [[package]] name = "byteorder" version = "1.4.3" @@ -709,6 +715,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", +] + [[package]] name = "futures-core" version = "0.3.28" @@ -774,6 +789,7 @@ dependencies = [ "futures-util", "lazy_static", "lettre", + "light-openid", "log", "mailchecker", "rand", @@ -871,6 +887,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + [[package]] name = "httparse" version = "1.8.0" @@ -889,6 +916,43 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "hyper" +version = "0.14.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "idna" version = "0.3.0" @@ -938,6 +1002,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "ipnet" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" + [[package]] name = "is-terminal" version = "0.4.7" @@ -965,6 +1035,15 @@ dependencies = [ "libc", ] +[[package]] +name = "js-sys" +version = "0.3.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -1006,6 +1085,20 @@ version = "0.2.144" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" +[[package]] +name = "light-openid" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608aa1b7148a6eeab631c6267deca33407ff851ab50eea115e52c13a9bb184ee" +dependencies = [ + "base64", + "log", + "reqwest", + "serde", + "serde_json", + "urlencoding", +] + [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -1387,6 +1480,43 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +[[package]] +name = "reqwest" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "rustc_version" version = "0.4.0" @@ -1669,6 +1799,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.8" @@ -1683,6 +1823,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + [[package]] name = "tracing" version = "0.1.37" @@ -1704,6 +1850,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + [[package]] name = "typenum" version = "1.16.0" @@ -1742,6 +1894,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9" + [[package]] name = "utf8parse" version = "0.2.1" @@ -1760,12 +1918,98 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.16", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d1985d03709c53167ce907ff394f5316aa22cb4e12761295c5dc57dacb6297e" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.16", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" + +[[package]] +name = "web-sys" +version = "0.3.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bdd9ef4e984da1187bf8110c5cf5b845fbc87a23602cdf912386a76fcd3a7c2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1944,6 +2188,15 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + [[package]] name = "zeroize" version = "1.6.0" diff --git a/geneit_backend/Cargo.toml b/geneit_backend/Cargo.toml index 0a39195..951ded6 100644 --- a/geneit_backend/Cargo.toml +++ b/geneit_backend/Cargo.toml @@ -21,4 +21,5 @@ mailchecker = "5.0.9" redis = "0.23.0" lettre = "0.10.4" rand = "0.8.5" -bcrypt = "0.14.0" \ No newline at end of file +bcrypt = "0.14.0" +light-openid = "1.0.1" \ No newline at end of file diff --git a/geneit_backend/src/app_config.rs b/geneit_backend/src/app_config.rs index b8d2748..cc8e8f7 100644 --- a/geneit_backend/src/app_config.rs +++ b/geneit_backend/src/app_config.rs @@ -112,9 +112,9 @@ pub struct AppConfig { #[arg(long, env, default_value = "bar")] pub oidc_client_secret: String, - /// OpenID login callback URL + /// OpenID login redirect URL #[arg(long, env, default_value = "http://localhost:3000/oidc_cb")] - pub oidc_callback_url: String, + pub oidc_redirect_url: String, } lazy_static::lazy_static! { @@ -155,29 +155,29 @@ impl AppConfig { } /// Get OpenID providers configuration - pub fn openid_providers(&self) -> Vec { + pub fn openid_providers(&self) -> Vec> { if self.disable_oidc { return vec![]; } - return vec![OIDCProvider { - id: "first_prov".to_string(), - client_id: self.oidc_client_id.to_string(), - client_secret: self.oidc_client_secret.to_string(), - configuration_url: self.oidc_configuration_url.to_string(), - name: self.oidc_provider_name.to_string(), - }]; + vec![OIDCProvider { + id: "first_prov", + client_id: self.oidc_client_id.as_str(), + client_secret: self.oidc_client_secret.as_str(), + configuration_url: self.oidc_configuration_url.as_str(), + name: self.oidc_provider_name.as_str(), + }] } } #[derive(Debug, Clone, serde::Serialize)] -pub struct OIDCProvider { - pub id: String, +pub struct OIDCProvider<'a> { + pub id: &'a str, #[serde(skip_serializing)] - pub client_id: String, + pub client_id: &'a str, #[serde(skip_serializing)] - pub client_secret: String, + pub client_secret: &'a str, #[serde(skip_serializing)] - pub configuration_url: String, - pub name: String, + pub configuration_url: &'a str, + pub name: &'a str, } diff --git a/geneit_backend/src/constants.rs b/geneit_backend/src/constants.rs index e155111..726a638 100644 --- a/geneit_backend/src/constants.rs +++ b/geneit_backend/src/constants.rs @@ -36,3 +36,6 @@ impl Default for StaticConstraints { /// Password reset token duration pub const PASSWORD_RESET_TOKEN_DURATION: Duration = Duration::from_secs(3600 * 24); + +/// OpenID state duration +pub const OPEN_ID_STATE_DURATION: Duration = Duration::from_secs(3600); diff --git a/geneit_backend/src/controllers/auth_controller.rs b/geneit_backend/src/controllers/auth_controller.rs index 64e3342..cb4e418 100644 --- a/geneit_backend/src/controllers/auth_controller.rs +++ b/geneit_backend/src/controllers/auth_controller.rs @@ -2,7 +2,7 @@ use crate::constants::StaticConstraints; use crate::controllers::HttpResult; use crate::models::{User, UserID}; use crate::services::rate_limiter_service::RatedAction; -use crate::services::{login_token_service, rate_limiter_service, users_service}; +use crate::services::{login_token_service, openid_service, rate_limiter_service, users_service}; use actix_remote_ip::RemoteIP; use actix_web::{web, HttpResponse}; @@ -227,3 +227,20 @@ async fn finish_login(user: &User) -> HttpResult { token: login_token_service::gen_new_token(user).await?, })) } + +#[derive(serde::Deserialize)] +pub struct StartOpenIDLoginQuery { + provider: String, +} + +#[derive(serde::Serialize)] +pub struct StartOpenIDLoginResponse { + url: String, +} + +/// Start OpenID login +pub async fn start_openid_login(ip: RemoteIP, req: web::Json) -> HttpResult { + let url = openid_service::start_login(&req.provider, ip.0).await?; + + Ok(HttpResponse::Ok().json(StartOpenIDLoginResponse { url })) +} diff --git a/geneit_backend/src/controllers/server_controller.rs b/geneit_backend/src/controllers/server_controller.rs index 41c3113..02f5a8a 100644 --- a/geneit_backend/src/controllers/server_controller.rs +++ b/geneit_backend/src/controllers/server_controller.rs @@ -8,13 +8,13 @@ pub async fn home() -> impl Responder { } #[derive(Debug, Clone, serde::Serialize)] -struct ServerConfig { +struct ServerConfig<'a> { constraints: StaticConstraints, mail: &'static str, - oidc_providers: Vec, + oidc_providers: Vec>, } -impl Default for ServerConfig { +impl Default for ServerConfig<'_> { fn default() -> Self { Self { mail: AppConfig::get().mail_sender.as_str(), diff --git a/geneit_backend/src/main.rs b/geneit_backend/src/main.rs index 3dbaea8..134b8a4 100644 --- a/geneit_backend/src/main.rs +++ b/geneit_backend/src/main.rs @@ -43,6 +43,10 @@ async fn main() -> std::io::Result<()> { "/auth/password_login", web::post().to(auth_controller::password_login), ) + .route( + "/auth/start_openid_login", + web::post().to(auth_controller::start_openid_login), + ) // User controller .route("/user/info", web::get().to(user_controller::auth_info)) }) diff --git a/geneit_backend/src/services/mod.rs b/geneit_backend/src/services/mod.rs index c908511..122af28 100644 --- a/geneit_backend/src/services/mod.rs +++ b/geneit_backend/src/services/mod.rs @@ -2,5 +2,6 @@ pub mod login_token_service; pub mod mail_service; +pub mod openid_service; pub mod rate_limiter_service; pub mod users_service; diff --git a/geneit_backend/src/services/openid_service.rs b/geneit_backend/src/services/openid_service.rs new file mode 100644 index 0000000..df92921 --- /dev/null +++ b/geneit_backend/src/services/openid_service.rs @@ -0,0 +1,85 @@ +//! # OpenID service + +use crate::app_config::{AppConfig, OIDCProvider}; +use crate::connections::redis_connection; +use crate::constants::OPEN_ID_STATE_DURATION; +use crate::utils::string_utils; +use crate::utils::time_utils::time; +use light_openid::primitives::OpenIDConfig; +use std::cell::RefCell; +use std::collections::HashMap; +use std::io::ErrorKind; +use std::net::IpAddr; + +thread_local! { + static CONFIG_CACHES: RefCell> = RefCell::new(Default::default()); + +} + +struct OpenIDClient<'a> { + prov: OIDCProvider<'a>, + conf: OpenIDConfig, +} + +#[derive(serde::Serialize, serde::Deserialize)] +struct OpenIDState { + #[serde(rename = "i")] + ip: IpAddr, + #[serde(rename = "e")] + expire: u64, + #[serde(rename = "p")] + prov_id: String, +} + +impl OpenIDState { + pub fn new(ip: IpAddr, client: &OpenIDClient) -> (String, Self) { + ( + string_utils::rand_str(30), + Self { + ip, + expire: time() + OPEN_ID_STATE_DURATION.as_secs(), + prov_id: client.prov.id.to_string(), + }, + ) + } +} + +fn redis_key(state: &str) -> String { + format!("oidc-state-{state}") +} + +async fn load_provider_info(prov_id: &str) -> anyhow::Result { + let prov = AppConfig::get() + .openid_providers() + .into_iter() + .find(|p| p.id.eq(prov_id)) + .ok_or_else(|| std::io::Error::new(ErrorKind::Other, "Provider not found!"))?; + + if let Some(conf) = CONFIG_CACHES.with(|i| i.borrow().get(prov_id).cloned()) { + return Ok(OpenIDClient { prov, conf }); + } + + let conf = OpenIDConfig::load_from_url(prov.configuration_url) + .await + .map_err(|e| std::io::Error::new(ErrorKind::Other, e.to_string()))?; + + CONFIG_CACHES.with(|i| { + i.borrow_mut() + .insert(prov.configuration_url.to_string(), conf.clone()) + }); + + Ok(OpenIDClient { prov, conf }) +} + +/// Get the URL where a user should be redirected for login +pub async fn start_login(prov_id: &str, ip: IpAddr) -> anyhow::Result { + let prov = load_provider_info(prov_id).await?; + let (state_key, state) = OpenIDState::new(ip, &prov); + redis_connection::set_value(&redis_key(&state_key), &state, OPEN_ID_STATE_DURATION).await?; + + Ok(prov.conf.gen_authorization_url( + prov.prov.client_id, + &state_key, + &AppConfig::get().oidc_redirect_url, + )) +}