From 1a022bd33e63feaa2c7d82f790931f84d235a111 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 18 Mar 2025 19:37:46 +0100 Subject: [PATCH] Finish OIDC login --- moneymgr_backend/Cargo.lock | 30 ++++++-- moneymgr_backend/Cargo.toml | 2 +- moneymgr_backend/src/constants.rs | 4 + .../src/controllers/auth_controller.rs | 74 +++++++++++++++++-- .../src/extractors/money_session.rs | 47 +++++++++++- moneymgr_backend/src/main.rs | 4 + 6 files changed, 149 insertions(+), 12 deletions(-) diff --git a/moneymgr_backend/Cargo.lock b/moneymgr_backend/Cargo.lock index 7182f5c..22efdde 100644 --- a/moneymgr_backend/Cargo.lock +++ b/moneymgr_backend/Cargo.lock @@ -452,7 +452,7 @@ dependencies = [ "quick-xml 0.32.0", "rust-ini", "serde", - "thiserror", + "thiserror 1.0.69", "time", "url", ] @@ -463,7 +463,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ae4ae7c45238b60af0a3b27ef2fcc7bd5b8fdcd8a6d679919558b40d3eff7a" dependencies = [ - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -1833,7 +1833,7 @@ dependencies = [ "rust-s3", "serde", "serde_json", - "thiserror", + "thiserror 2.0.12", "tokio", ] @@ -2362,7 +2362,7 @@ dependencies = [ "serde_derive", "serde_json", "sha2", - "thiserror", + "thiserror 1.0.69", "time", "tokio", "tokio-stream", @@ -2739,7 +2739,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", ] [[package]] @@ -2753,6 +2762,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "time" version = "0.3.39" diff --git a/moneymgr_backend/Cargo.toml b/moneymgr_backend/Cargo.toml index 50bf368..8b4cd2c 100644 --- a/moneymgr_backend/Cargo.toml +++ b/moneymgr_backend/Cargo.toml @@ -18,7 +18,7 @@ lazy_static = "1.5.0" anyhow = "1.0.97" serde = { version = "1.0.219", features = ["derive"] } rust-s3 = "0.36.0-beta.2" -thiserror = "1.0.69" +thiserror = "2.0.12" tokio = "1.44.1" futures-util = "0.3.31" serde_json = "1.0.140" diff --git a/moneymgr_backend/src/constants.rs b/moneymgr_backend/src/constants.rs index 9fa093e..efb31fb 100644 --- a/moneymgr_backend/src/constants.rs +++ b/moneymgr_backend/src/constants.rs @@ -2,4 +2,8 @@ pub mod sessions { /// OpenID auth session state key pub const OIDC_STATE_KEY: &str = "oidc-state"; + /// OpenID auth remote IP address + pub const OIDC_REMOTE_IP: &str = "oidc-remote-ip"; + /// Authenticated ID + pub const USER_ID: &str = "uid"; } diff --git a/moneymgr_backend/src/controllers/auth_controller.rs b/moneymgr_backend/src/controllers/auth_controller.rs index f1047f1..6003b7f 100644 --- a/moneymgr_backend/src/controllers/auth_controller.rs +++ b/moneymgr_backend/src/controllers/auth_controller.rs @@ -1,7 +1,9 @@ use crate::app_config::AppConfig; -use crate::controllers::HttpResult; +use crate::controllers::{HttpFailure, HttpResult}; use crate::extractors::money_session::MoneySession; -use actix_web::HttpResponse; +use crate::services::users_service; +use actix_remote_ip::RemoteIP; +use actix_web::{HttpResponse, web}; use light_openid::primitives::OpenIDConfig; #[derive(serde::Serialize)] @@ -10,7 +12,7 @@ struct StartOIDCResponse { } /// Start OIDC authentication -pub async fn start_oidc(session: MoneySession) -> HttpResult { +pub async fn start_oidc(session: MoneySession, remote_ip: RemoteIP) -> HttpResult { let prov = AppConfig::get().openid_provider(); let conf = match OpenIDConfig::load_from_url(prov.configuration_url).await { @@ -22,7 +24,7 @@ pub async fn start_oidc(session: MoneySession) -> HttpResult { } }; - let state = match session.gen_oidc_state() { + let state = match session.gen_oidc_state(remote_ip.0) { Ok(s) => s, Err(e) => { log::error!("Failed to generate auth state! {e}"); @@ -39,4 +41,66 @@ pub async fn start_oidc(session: MoneySession) -> HttpResult { })) } -// TODO : take from previous projects +#[derive(serde::Deserialize)] +pub struct FinishOpenIDLoginQuery { + code: String, + state: String, +} + +/// Finish OIDC authentication +pub async fn finish_oidc( + session: MoneySession, + remote_ip: RemoteIP, + req: web::Json, +) -> HttpResult { + if let Err(e) = session.validate_state(&req.state, remote_ip.0) { + log::error!("Failed to validate OIDC CB state! {e}"); + return Ok(HttpResponse::BadRequest().json("Invalid state!")); + } + + let prov = AppConfig::get().openid_provider(); + + let conf = OpenIDConfig::load_from_url(prov.configuration_url) + .await + .map_err(HttpFailure::OpenID)?; + + let (token, _) = conf + .request_token( + prov.client_id, + prov.client_secret, + &req.code, + &AppConfig::get().oidc_redirect_url(), + ) + .await + .map_err(HttpFailure::OpenID)?; + let (user_info, _) = conf + .request_user_info(&token) + .await + .map_err(HttpFailure::OpenID)?; + + if user_info.email_verified != Some(true) { + log::error!("Email is not verified!"); + return Ok(HttpResponse::Unauthorized().json("Email unverified by IDP!")); + } + + let mail = match user_info.email { + Some(m) => m, + None => { + return Ok(HttpResponse::Unauthorized().json("Email not provided by the IDP!")); + } + }; + + let user_name = user_info.name.unwrap_or_else(|| { + format!( + "{} {}", + user_info.given_name.as_deref().unwrap_or(""), + user_info.family_name.as_deref().unwrap_or("") + ) + }); + + let user = users_service::create_or_update_user(&mail, &user_name).await?; + + session.set_user(&user)?; + + Ok(HttpResponse::Ok().finish()) +} diff --git a/moneymgr_backend/src/extractors/money_session.rs b/moneymgr_backend/src/extractors/money_session.rs index f347e40..4116c34 100644 --- a/moneymgr_backend/src/extractors/money_session.rs +++ b/moneymgr_backend/src/extractors/money_session.rs @@ -1,9 +1,24 @@ use crate::constants; +use crate::models::users::User; 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}; +use std::net::IpAddr; + +/// Money session errors +#[derive(thiserror::Error, Debug)] +enum MoneySessionError { + #[error("Missing state!")] + OIDCMissingState, + #[error("Missing IP address!")] + OIDCMissingIP, + #[error("Invalid state!")] + OIDCInvalidState, + #[error("Invalid IP address!")] + OIDCInvalidIP, +} /// Money session /// @@ -12,12 +27,42 @@ pub struct MoneySession(Session); impl MoneySession { /// Generate OpenID state for this session - pub fn gen_oidc_state(&self) -> anyhow::Result { + pub fn gen_oidc_state(&self, ip: IpAddr) -> anyhow::Result { let random_string = rand_string(50); self.0 .insert(constants::sessions::OIDC_STATE_KEY, random_string.clone())?; + self.0.insert(constants::sessions::OIDC_REMOTE_IP, ip)?; Ok(random_string) } + + /// Validate OpenID state + pub fn validate_state(&self, state: &str, ip: IpAddr) -> anyhow::Result<()> { + let session_state: String = self + .0 + .get(constants::sessions::OIDC_STATE_KEY)? + .ok_or(MoneySessionError::OIDCMissingState)?; + + let session_ip: IpAddr = self + .0 + .get(constants::sessions::OIDC_REMOTE_IP)? + .ok_or(MoneySessionError::OIDCMissingIP)?; + + if session_state != state { + return Err(anyhow::anyhow!(MoneySessionError::OIDCInvalidState)); + } + + if session_ip != ip { + return Err(anyhow::anyhow!(MoneySessionError::OIDCInvalidIP)); + } + + Ok(()) + } + + /// Set current user + pub fn set_user(&self, user: &User) -> anyhow::Result<()> { + self.0.insert(constants::sessions::USER_ID, user.id())?; + Ok(()) + } } impl FromRequest for MoneySession { diff --git a/moneymgr_backend/src/main.rs b/moneymgr_backend/src/main.rs index 3678bbe..cdcf561 100644 --- a/moneymgr_backend/src/main.rs +++ b/moneymgr_backend/src/main.rs @@ -81,6 +81,10 @@ async fn main() -> std::io::Result<()> { "/api/auth/start_oidc", web::get().to(auth_controller::start_oidc), ) + .route( + "/api/auth/finish_oidc", + web::post().to(auth_controller::finish_oidc), + ) }) .bind(AppConfig::get().listen_address.as_str())? .run()