From 83bd87c6f8350659da21440fe69c746320c919a0 Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Mon, 4 Sep 2023 11:25:03 +0200 Subject: [PATCH] Add OpenID routes --- virtweb_backend/Cargo.lock | 30 ++++++ virtweb_backend/Cargo.toml | 6 +- virtweb_backend/src/constants.rs | 8 +- .../src/controllers/auth_controller.rs | 94 ++++++++++++++++++- virtweb_backend/src/controllers/mod.rs | 59 ++++++++++++ virtweb_backend/src/main.rs | 21 ++++- 6 files changed, 212 insertions(+), 6 deletions(-) diff --git a/virtweb_backend/Cargo.lock b/virtweb_backend/Cargo.lock index ca53923..235a70a 100644 --- a/virtweb_backend/Cargo.lock +++ b/virtweb_backend/Cargo.lock @@ -409,6 +409,25 @@ version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53" +[[package]] +name = "bincode" +version = "2.0.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f11ea1a0346b94ef188834a65c068a03aec181c94896d481d7a0a40d85b0ce95" +dependencies = [ + "bincode_derive", + "serde", +] + +[[package]] +name = "bincode_derive" +version = "2.0.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e30759b3b99a1b802a7a3aa21c85c3ded5c28e1c83170d82d70f08bbf7f3e4c" +dependencies = [ + "virtue", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -1056,8 +1075,11 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "608aa1b7148a6eeab631c6267deca33407ff851ab50eea115e52c13a9bb184ee" dependencies = [ + "aes-gcm", "base64 0.21.3", + "bincode", "log", + "rand", "reqwest", "serde", "serde_json", @@ -1846,6 +1868,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "virtue" +version = "0.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcc60c0624df774c82a0ef104151231d37da4962957d691c011c852b2473314" + [[package]] name = "virtweb_backend" version = "0.1.0" @@ -1854,6 +1882,7 @@ dependencies = [ "actix-remote-ip", "actix-session", "actix-web", + "anyhow", "clap", "env_logger", "futures-util", @@ -1861,6 +1890,7 @@ dependencies = [ "light-openid", "log", "serde", + "serde_json", ] [[package]] diff --git a/virtweb_backend/Cargo.toml b/virtweb_backend/Cargo.toml index fc0d71d..461bb96 100644 --- a/virtweb_backend/Cargo.toml +++ b/virtweb_backend/Cargo.toml @@ -9,11 +9,13 @@ edition = "2021" log = "0.4.19" env_logger = "0.10.0" clap = { version = "4.3.19", features = ["derive", "env"] } -light-openid = "1.0.1" +light-openid = { version = "1.0.1", features=["crypto-wrapper"] } lazy_static = "1.4.0" actix-web = "4" actix-remote-ip = "0.1.0" actix-session = { version = "0.7.2", features = ["cookie-session"] } actix-identity = "0.5.2" serde = { version = "1.0.175", features = ["derive"] } -futures-util = "0.3.28" \ No newline at end of file +serde_json = "1.0.105" +futures-util = "0.3.28" +anyhow = "1.0.75" \ No newline at end of file diff --git a/virtweb_backend/src/constants.rs b/virtweb_backend/src/constants.rs index f3c3549..cdf2400 100644 --- a/virtweb_backend/src/constants.rs +++ b/virtweb_backend/src/constants.rs @@ -8,4 +8,10 @@ pub const MAX_INACTIVITY_DURATION: u64 = 60 * 30; pub const MAX_SESSION_DURATION: u64 = 3600 * 6; /// The routes that can be accessed without authentication -pub const ROUTES_WITHOUT_AUTH: [&str; 3] = ["/", "/api/server/static_config", "/api/auth/local"]; +pub const ROUTES_WITHOUT_AUTH: [&str; 5] = [ + "/", + "/api/server/static_config", + "/api/auth/local", + "/api/auth/start_oidc", + "/api/auth/finish_oidc", +]; diff --git a/virtweb_backend/src/controllers/auth_controller.rs b/virtweb_backend/src/controllers/auth_controller.rs index 5dce683..165ff15 100644 --- a/virtweb_backend/src/controllers/auth_controller.rs +++ b/virtweb_backend/src/controllers/auth_controller.rs @@ -1,7 +1,12 @@ +use actix_remote_ip::RemoteIP; +use actix_web::web::Data; +use actix_web::{web, HttpResponse, Responder}; +use light_openid::basic_state_manager::BasicStateManager; + use crate::app_config::AppConfig; +use crate::controllers::HttpResult; use crate::extractors::auth_extractor::AuthExtractor; use crate::extractors::local_auth_extractor::LocalAuthEnabled; -use actix_web::{web, HttpResponse, Responder}; #[derive(serde::Deserialize)] pub struct LocalAuthReq { @@ -30,6 +35,87 @@ pub async fn local_auth( HttpResponse::Accepted().json("Welcome") } +#[derive(serde::Serialize)] +struct StartOIDCResponse { + url: String, +} + +/// Start OIDC authentication +pub async fn start_oidc(sm: Data, ip: RemoteIP) -> HttpResult { + let prov = match AppConfig::get().openid_provider() { + None => { + return Ok(HttpResponse::UnprocessableEntity().json("OpenID is disabled!")); + } + Some(conf) => conf, + }; + + let conf = + light_openid::primitives::OpenIDConfig::load_from_url(prov.configuration_url).await?; + + Ok(HttpResponse::Ok().json(StartOIDCResponse { + url: conf.gen_authorization_url( + prov.client_id, + &sm.gen_state(ip.0)?, + &AppConfig::get().oidc_redirect_url(), + ), + })) +} + +#[derive(serde::Deserialize)] +pub struct FinishOpenIDLoginQuery { + code: String, + state: String, +} + +/// Finish OIDC authentication +pub async fn finish_oidc( + sm: Data, + remote_ip: RemoteIP, + req: web::Json, + auth: AuthExtractor, +) -> HttpResult { + if let Err(e) = sm.validate_state(remote_ip.0, &req.state) { + log::error!("Failed to validate OIDC CB state! {e}"); + return Ok(HttpResponse::BadRequest().json("Invalid state!")); + } + + let prov = match AppConfig::get().openid_provider() { + None => { + return Ok(HttpResponse::UnprocessableEntity().json("OpenID is disabled!")); + } + Some(conf) => conf, + }; + + let conf = + light_openid::primitives::OpenIDConfig::load_from_url(prov.configuration_url).await?; + + let (token, _) = conf + .request_token( + prov.client_id, + prov.client_secret, + &req.code, + &AppConfig::get().oidc_redirect_url(), + ) + .await?; + let (user_info, _) = conf.request_user_info(&token).await?; + + 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!")); + } + }; + + auth.authenticate(mail); + + Ok(HttpResponse::Ok().finish()) +} + #[derive(serde::Serialize)] struct CurrentUser { id: String, @@ -41,3 +127,9 @@ pub async fn current_user(auth: AuthExtractor) -> impl Responder { id: auth.id().unwrap(), }) } + +/// Sign out +pub async fn sign_out(auth: AuthExtractor) -> impl Responder { + auth.sign_out(); + HttpResponse::Ok().finish() +} diff --git a/virtweb_backend/src/controllers/mod.rs b/virtweb_backend/src/controllers/mod.rs index ad755c3..644ad74 100644 --- a/virtweb_backend/src/controllers/mod.rs +++ b/virtweb_backend/src/controllers/mod.rs @@ -1,2 +1,61 @@ +use actix_web::body::BoxBody; +use actix_web::HttpResponse; +use std::error::Error; +use std::fmt::{Display, Formatter}; +use std::io::ErrorKind; + pub mod auth_controller; pub mod server_controller; + +/// Custom error to ease controller writing +#[derive(Debug)] +pub struct HttpErr { + err: anyhow::Error, +} + +impl Display for HttpErr { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.err, f) + } +} + +impl actix_web::error::ResponseError for HttpErr { + fn error_response(&self) -> HttpResponse { + log::error!("Error while processing request! {}", self); + HttpResponse::InternalServerError().body("Failed to execute request!") + } +} + +impl From for HttpErr { + fn from(err: anyhow::Error) -> HttpErr { + HttpErr { err } + } +} + +impl From for HttpErr { + fn from(value: serde_json::Error) -> Self { + HttpErr { err: value.into() } + } +} + +impl From> for HttpErr { + fn from(value: Box) -> Self { + HttpErr { + err: std::io::Error::new(ErrorKind::Other, value.to_string()).into(), + } + } +} + +impl From for HttpErr { + fn from(value: std::io::Error) -> Self { + HttpErr { err: value.into() } + } +} + +impl From for HttpErr { + fn from(value: std::num::ParseIntError) -> Self { + HttpErr { err: value.into() } + } +} + +pub type HttpResult = Result; diff --git a/virtweb_backend/src/main.rs b/virtweb_backend/src/main.rs index d623027..61c4e36 100644 --- a/virtweb_backend/src/main.rs +++ b/virtweb_backend/src/main.rs @@ -5,7 +5,9 @@ use actix_session::storage::CookieSessionStore; use actix_session::SessionMiddleware; use actix_web::cookie::{Key, SameSite}; use actix_web::middleware::Logger; +use actix_web::web::Data; use actix_web::{web, App, HttpServer}; +use light_openid::basic_state_manager::BasicStateManager; use std::time::Duration; use virtweb_backend::app_config::AppConfig; use virtweb_backend::constants::{ @@ -20,7 +22,9 @@ async fn main() -> std::io::Result<()> { log::info!("Start to listen on {}", AppConfig::get().listen_address); - HttpServer::new(|| { + let state_manager = Data::new(BasicStateManager::new()); + + HttpServer::new(move || { let session_mw = SessionMiddleware::builder( CookieSessionStore::default(), Key::from(AppConfig::get().secret().as_bytes()), @@ -41,7 +45,8 @@ async fn main() -> std::io::Result<()> { .wrap(AuthChecker) .wrap(identity_middleware) .wrap(session_mw) - .app_data(web::Data::new(RemoteIPConfig { + .app_data(state_manager.clone()) + .app_data(Data::new(RemoteIPConfig { proxy: AppConfig::get().proxy_ip.clone(), })) // Server controller @@ -55,10 +60,22 @@ async fn main() -> std::io::Result<()> { "/api/auth/local", web::post().to(auth_controller::local_auth), ) + .route( + "/api/auth/start_oidc", + web::get().to(auth_controller::start_oidc), + ) + .route( + "/api/auth/finish_oidc", + web::post().to(auth_controller::finish_oidc), + ) .route( "/api/auth/user", web::get().to(auth_controller::current_user), ) + .route( + "/api/auth/sign_out", + web::get().to(auth_controller::sign_out), + ) }) .bind(&AppConfig::get().listen_address)? .run()