From 0c940b595e9b7248c9261181710fc72103200f11 Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Sun, 20 Apr 2025 22:12:52 +0200 Subject: [PATCH] oidc: Remove all dependencies on other rustical crates --- Cargo.lock | 3 +- crates/frontend/Cargo.toml | 13 +++--- crates/frontend/src/lib.rs | 44 ++++++++++++++++--- crates/oidc/Cargo.toml | 11 +++-- crates/oidc/src/error.rs | 3 -- crates/oidc/src/lib.rs | 82 +++++++++++++++++++---------------- crates/oidc/src/user_store.rs | 10 +++++ 7 files changed, 107 insertions(+), 59 deletions(-) create mode 100644 crates/oidc/src/user_store.rs diff --git a/Cargo.lock b/Cargo.lock index 4605f96..314bde3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3153,6 +3153,7 @@ dependencies = [ "actix-web", "askama", "askama_web", + "async-trait", "chrono", "chrono-humanize", "futures-core", @@ -3176,9 +3177,9 @@ version = "0.1.0" dependencies = [ "actix-session", "actix-web", + "async-trait", "openidconnect", "reqwest", - "rustical_store", "serde", "thiserror 2.0.12", ] diff --git a/crates/frontend/Cargo.toml b/crates/frontend/Cargo.toml index 2dcf1d3..c86b188 100644 --- a/crates/frontend/Cargo.toml +++ b/crates/frontend/Cargo.toml @@ -8,13 +8,14 @@ publish = false [dependencies] askama.workspace = true +async-trait.workspace = true askama_web.workspace = true -actix-session = { workspace = true } -serde = { workspace = true } -thiserror = { workspace = true } -tokio = { workspace = true } -actix-web = { workspace = true } -rustical_store = { workspace = true } +actix-session.workspace = true +serde.workspace = true +thiserror.workspace = true +tokio.workspace = true +actix-web.workspace = true +rustical_store.workspace = true rust-embed.workspace = true futures-core.workspace = true hex.workspace = true diff --git a/crates/frontend/src/lib.rs b/crates/frontend/src/lib.rs index 4fe556f..5f98252 100644 --- a/crates/frontend/src/lib.rs +++ b/crates/frontend/src/lib.rs @@ -14,13 +14,14 @@ use actix_web::{ use askama::Template; use askama_web::WebTemplate; use assets::{Assets, EmbedService}; +use async_trait::async_trait; use rand::{Rng, distributions::Alphanumeric}; use routes::{ addressbook::{route_addressbook, route_addressbook_restore}, calendar::{route_calendar, route_calendar_restore}, login::{route_get_login, route_post_login, route_post_logout}, }; -use rustical_oidc::{OidcConfig, configure_oidc}; +use rustical_oidc::{OidcConfig, OidcServiceConfig, UserStore, configure_oidc}; use rustical_store::{ Addressbook, AddressbookStore, Calendar, CalendarStore, auth::{AuthenticationMiddleware, AuthenticationProvider, User}, @@ -201,7 +202,7 @@ pub fn configure_frontend(cfg, oidc_config, ROUTE_NAME_HOME)), - ); + scope = scope.service(web::scope("/login/oidc").configure(|cfg| { + configure_oidc( + cfg, + oidc_config, + OidcServiceConfig { + default_redirect_route_name: ROUTE_NAME_HOME, + session_key_user_id: "user", + }, + Arc::new(OidcUserStore(auth_provider.clone())), + ) + })); } cfg.service(scope); } + +struct OidcUserStore(Arc); + +#[async_trait(?Send)] +impl UserStore for OidcUserStore { + type Error = rustical_store::Error; + + async fn user_exists(&self, id: &str) -> Result { + Ok(self.0.get_principal(id).await?.is_some()) + } + + async fn insert_user(&self, id: &str) -> Result<(), Self::Error> { + self.0 + .insert_principal(User { + id: id.to_owned(), + displayname: None, + principal_type: Default::default(), + password: None, + app_tokens: vec![], + memberships: vec![], + }) + .await + } +} diff --git a/crates/oidc/Cargo.toml b/crates/oidc/Cargo.toml index d5773f0..4da29f8 100644 --- a/crates/oidc/Cargo.toml +++ b/crates/oidc/Cargo.toml @@ -7,10 +7,9 @@ repository.workspace = true [dependencies] openidconnect.workspace = true -serde = { workspace = true } +serde.workspace = true reqwest.workspace = true -# TODO: Remove this dependency -rustical_store = { workspace = true } -actix-web = { workspace = true } -actix-session = { workspace = true } -thiserror = { workspace = true } +actix-web.workspace = true +actix-session.workspace = true +thiserror.workspace = true +async-trait.workspace = true diff --git a/crates/oidc/src/error.rs b/crates/oidc/src/error.rs index 9dce580..605b025 100644 --- a/crates/oidc/src/error.rs +++ b/crates/oidc/src/error.rs @@ -21,9 +21,6 @@ pub enum OidcError { #[error(transparent)] SessionInsertError(#[from] SessionInsertError), - #[error(transparent)] - StoreError(#[from] rustical_store::Error), - #[error("{0}")] Other(&'static str), } diff --git a/crates/oidc/src/lib.rs b/crates/oidc/src/lib.rs index dce73cf..bf61d64 100644 --- a/crates/oidc/src/lib.rs +++ b/crates/oidc/src/lib.rs @@ -1,6 +1,6 @@ use actix_session::Session; use actix_web::{ - HttpRequest, HttpResponse, Responder, + HttpRequest, HttpResponse, Responder, ResponseError, http::StatusCode, web::{self, Data, Form, Query, Redirect, ServiceConfig}, }; @@ -13,18 +13,23 @@ use openidconnect::{ RedirectUrl, TokenResponse, UserInfoClaims, core::{CoreClient, CoreGenderClaim, CoreProviderMetadata, CoreResponseType}, }; -use rustical_store::auth::{AuthenticationProvider, User, user::PrincipalType::Individual}; use serde::{Deserialize, Serialize}; +use std::sync::Arc; +pub use user_store::UserStore; mod config; mod error; +mod user_store; pub const ROUTE_NAME_OIDC_LOGIN: &str = "oidc_login"; const ROUTE_NAME_OIDC_CALLBACK: &str = "oidc_callback"; const SESSION_KEY_OIDC_STATE: &str = "oidc_state"; -#[derive(Debug)] -pub struct DefaultRedirectRouteName(pub &'static str); +#[derive(Debug, Clone)] +pub struct OidcServiceConfig { + pub default_redirect_route_name: &'static str, + pub session_key_user_id: &'static str, +} #[derive(Debug, Deserialize, Serialize)] struct OidcState { @@ -93,7 +98,7 @@ pub async fn route_post_oidc( Form(GetOidcForm { redirect_uri }): Form, oidc_config: Data, session: Session, -) -> Result { +) -> Result { let http_client = get_http_client(); let oidc_client = get_oidc_client( oidc_config.as_ref().clone(), @@ -124,7 +129,10 @@ pub async fn route_post_oidc( }, )?; - Ok(Redirect::to(auth_url.to_string()).see_other()) + Ok(Redirect::to(auth_url.to_string()) + .see_other() + .respond_to(&req) + .map_into_boxed_body()) } #[derive(Debug, Clone, Deserialize)] @@ -135,14 +143,14 @@ pub struct AuthCallbackQuery { } /// Handle callback from IdP page -pub async fn route_get_oidc_callback( +pub async fn route_get_oidc_callback( req: HttpRequest, oidc_config: Data, session: Session, - auth_provider: Data, + user_store: Data, Query(AuthCallbackQuery { code, iss, state }): Query, - default_redirect_name: Data, -) -> Result { + service_config: Data, +) -> Result { assert_eq!(iss, oidc_config.issuer); let oidc_state = session .remove_as::(SESSION_KEY_OIDC_STATE) @@ -198,23 +206,25 @@ pub async fn route_get_oidc_callback( .to_string(), }; - let mut user = auth_provider.get_principal(&user_id).await?; - if user.is_none() { - let new_user = User { - id: user_id, - displayname: None, - app_tokens: vec![], - password: None, - principal_type: Individual, - memberships: vec![], - }; - - auth_provider.insert_principal(new_user.clone()).await?; - user = Some(new_user); + match user_store.user_exists(&user_id).await { + Ok(false) => { + // User does not exist + if !oidc_config.allow_sign_up { + return Ok(HttpResponse::Unauthorized().body("User sign up disabled")); + } + // Create new user + if let Err(err) = user_store.insert_user(&user_id).await { + return Ok(err.error_response()); + } + } + Ok(true) => {} + Err(err) => { + return Ok(err.error_response()); + } } let default_redirect = req - .url_for_static(default_redirect_name.as_ref().0)? + .url_for_static(service_config.default_redirect_route_name)? .to_string(); let redirect_uri = oidc_state.redirect_uri.unwrap_or(default_redirect.clone()); let redirect_uri = req @@ -225,25 +235,23 @@ pub async fn route_get_oidc_callback( .unwrap_or(default_redirect); // Complete login flow - if let Some(user) = user { - session.insert("user", user.id.clone())?; + session.insert(service_config.session_key_user_id, user_id.clone())?; - Ok(Redirect::to(redirect_uri) - .temporary() - .respond_to(&req) - .map_into_boxed_body()) - } else { - Ok(HttpResponse::build(StatusCode::UNAUTHORIZED).body("User does not exist")) - } + Ok(Redirect::to(redirect_uri) + .temporary() + .respond_to(&req) + .map_into_boxed_body()) } -pub fn configure_oidc( +pub fn configure_oidc( cfg: &mut ServiceConfig, oidc_config: OidcConfig, - default_redirect_name: &'static str, + service_config: OidcServiceConfig, + user_store: Arc, ) { cfg.app_data(Data::new(oidc_config)) - .app_data(Data::new(DefaultRedirectRouteName(default_redirect_name))) + .app_data(Data::new(service_config)) + .app_data(Data::from(user_store)) .service( web::resource("") .name(ROUTE_NAME_OIDC_LOGIN) @@ -252,6 +260,6 @@ pub fn configure_oidc( .service( web::resource("/callback") .name(ROUTE_NAME_OIDC_CALLBACK) - .get(route_get_oidc_callback::), + .get(route_get_oidc_callback::), ); } diff --git a/crates/oidc/src/user_store.rs b/crates/oidc/src/user_store.rs new file mode 100644 index 0000000..8a380e5 --- /dev/null +++ b/crates/oidc/src/user_store.rs @@ -0,0 +1,10 @@ +use actix_web::ResponseError; +use async_trait::async_trait; + +#[async_trait(?Send)] +pub trait UserStore: 'static { + type Error: ResponseError; + + async fn user_exists(&self, id: &str) -> Result; + async fn insert_user(&self, id: &str) -> Result<(), Self::Error>; +}