diff --git a/crates/frontend/src/config.rs b/crates/frontend/src/config.rs index 7268de6..bbc6155 100644 --- a/crates/frontend/src/config.rs +++ b/crates/frontend/src/config.rs @@ -38,8 +38,6 @@ pub struct FrontendConfig { pub secret_key: [u8; 64], #[serde(default = "default_true")] pub enabled: bool, - #[serde(default)] - pub oidc: Option, #[serde(default = "default_true")] pub allow_password_login: bool, } diff --git a/crates/frontend/src/lib.rs b/crates/frontend/src/lib.rs index 8462604..f736429 100644 --- a/crates/frontend/src/lib.rs +++ b/crates/frontend/src/lib.rs @@ -34,7 +34,7 @@ pub mod nextcloud_login; mod oidc; mod routes; -pub use config::FrontendConfig; +pub use config::{FrontendConfig, OidcConfig}; pub fn generate_app_token() -> String { rand::thread_rng() @@ -193,63 +193,68 @@ pub fn configure_frontend, addr_store: Arc, frontend_config: FrontendConfig, + oidc_config: Option, ) { - cfg.service( - web::scope("") - .wrap(ErrorHandlers::new().handler(StatusCode::UNAUTHORIZED, unauthorized_handler)) - .wrap(AuthenticationMiddleware::new(auth_provider.clone())) - .wrap(session_middleware(frontend_config.secret_key)) - .app_data(Data::from(auth_provider)) - .app_data(Data::from(cal_store.clone())) - .app_data(Data::from(addr_store.clone())) - .app_data(Data::new(frontend_config.clone())) - .service(EmbedService::::new("/assets".to_owned())) - .service(web::resource("").route(web::method(Method::GET).to(route_root))) - .service( - web::resource("/user") - .route(web::method(Method::GET).to(route_user)) - .name("frontend_user"), - ) - .service( - web::resource("/user/{user}") - .route(web::method(Method::GET).to(route_user_named::)) - .name("frontend_user_named"), - ) - .service( - web::resource("/user/{user}/app_token") - .route(web::method(Method::POST).to(route_post_app_token::)), - ) - .service( - web::resource("/user/{user}/app_token/{id}/delete") - .route(web::method(Method::POST).to(route_delete_app_token::)), - ) - .service( - web::resource("/user/{user}/calendar/{calendar}") - .route(web::method(Method::GET).to(route_calendar::)), - ) - .service( - web::resource("/user/{user}/calendar/{calendar}/restore") - .route(web::method(Method::POST).to(route_calendar_restore::)), - ) - .service( - web::resource("/user/{user}/addressbook/{addressbook}") - .route(web::method(Method::GET).to(route_addressbook::)), - ) - .service( - web::resource("/user/{user}/addressbook/{addressbook}/restore") - .route(web::method(Method::POST).to(route_addressbook_restore::)), - ) - .service( - web::resource("/login") - .name("frontend_login") - .route(web::method(Method::GET).to(route_get_login)) - .route(web::method(Method::POST).to(route_post_login::)), - ) - .service( - web::resource("/logout") - .name("frontend_logout") - .route(web::method(Method::POST).to(route_post_logout)), - ) + let mut scope = web::scope("") + .wrap(ErrorHandlers::new().handler(StatusCode::UNAUTHORIZED, unauthorized_handler)) + .wrap(AuthenticationMiddleware::new(auth_provider.clone())) + .wrap(session_middleware(frontend_config.secret_key)) + .app_data(Data::from(auth_provider)) + .app_data(Data::from(cal_store.clone())) + .app_data(Data::from(addr_store.clone())) + .app_data(Data::new(frontend_config.clone())) + .app_data(Data::new(oidc_config.clone())) + .service(EmbedService::::new("/assets".to_owned())) + .service(web::resource("").route(web::method(Method::GET).to(route_root))) + .service( + web::resource("/user") + .route(web::method(Method::GET).to(route_user)) + .name("frontend_user"), + ) + .service( + web::resource("/user/{user}") + .route(web::method(Method::GET).to(route_user_named::)) + .name("frontend_user_named"), + ) + .service( + web::resource("/user/{user}/app_token") + .route(web::method(Method::POST).to(route_post_app_token::)), + ) + .service( + web::resource("/user/{user}/app_token/{id}/delete") + .route(web::method(Method::POST).to(route_delete_app_token::)), + ) + .service( + web::resource("/user/{user}/calendar/{calendar}") + .route(web::method(Method::GET).to(route_calendar::)), + ) + .service( + web::resource("/user/{user}/calendar/{calendar}/restore") + .route(web::method(Method::POST).to(route_calendar_restore::)), + ) + .service( + web::resource("/user/{user}/addressbook/{addressbook}") + .route(web::method(Method::GET).to(route_addressbook::)), + ) + .service( + web::resource("/user/{user}/addressbook/{addressbook}/restore") + .route(web::method(Method::POST).to(route_addressbook_restore::)), + ) + .service( + web::resource("/login") + .name("frontend_login") + .route(web::method(Method::GET).to(route_get_login)) + .route(web::method(Method::POST).to(route_post_login::)), + ) + .service( + web::resource("/logout") + .name("frontend_logout") + .route(web::method(Method::POST).to(route_post_logout)), + ); + + if let Some(oidc_config) = oidc_config { + scope = scope + .app_data(Data::new(oidc_config)) .service( web::resource("/login/oidc") .name("frontend_login_oidc") @@ -259,6 +264,8 @@ pub fn configure_frontend)), - ), - ); + ); + } + + cfg.service(scope); } diff --git a/crates/frontend/src/oidc/error.rs b/crates/frontend/src/oidc/error.rs index 926f933..9dce580 100644 --- a/crates/frontend/src/oidc/error.rs +++ b/crates/frontend/src/oidc/error.rs @@ -12,9 +12,6 @@ pub enum OidcError { #[error("Cannot generate redirect url, something's not configured correctly")] ActixUrlGenerationError(#[from] UrlGenerationError), - #[error("RustiCal is not configured correctly for OIDC")] - IncorrectConfiguration, - #[error(transparent)] OidcConfigurationError(#[from] ConfigurationError), diff --git a/crates/frontend/src/oidc/mod.rs b/crates/frontend/src/oidc/mod.rs index 40fe3d2..e2f4e27 100644 --- a/crates/frontend/src/oidc/mod.rs +++ b/crates/frontend/src/oidc/mod.rs @@ -1,7 +1,4 @@ -use crate::{ - FrontendConfig, - config::{OidcConfig, UserIdClaim}, -}; +use crate::config::{OidcConfig, UserIdClaim}; use actix_session::Session; use actix_web::{ HttpRequest, HttpResponse, Responder, @@ -92,17 +89,12 @@ pub struct GetOidcForm { pub async fn route_post_oidc( req: HttpRequest, Form(GetOidcForm { redirect_uri }): Form, - config: Data, + oidc_config: Data, session: Session, ) -> Result { - let oidc_config = config - .oidc - .clone() - .ok_or(OidcError::IncorrectConfiguration)?; - let http_client = get_http_client(); let oidc_client = get_oidc_client( - oidc_config.clone(), + oidc_config.as_ref().clone(), &http_client, RedirectUrl::new(req.url_for_static("frontend_oidc_callback")?.to_string())?, ) @@ -137,21 +129,16 @@ pub async fn route_post_oidc( pub struct AuthCallbackQuery { code: AuthorizationCode, iss: IssuerUrl, - // scope: String, - // state: String, } +/// Handle callback from IdP page pub async fn route_get_oidc_callback( req: HttpRequest, - config: Data, + oidc_config: Data, session: Session, auth_provider: Data, Query(AuthCallbackQuery { code, iss }): Query, ) -> Result { - let oidc_config = config - .oidc - .clone() - .ok_or(OidcError::IncorrectConfiguration)?; assert_eq!(iss, oidc_config.issuer); let oidc_state = session .remove_as::(SESSION_KEY_OIDC_STATE) @@ -160,7 +147,7 @@ pub async fn route_get_oidc_callback( let http_client = get_http_client(); let oidc_client = get_oidc_client( - oidc_config.clone(), + oidc_config.get_ref().clone(), &http_client, RedirectUrl::new(req.url_for_static("frontend_oidc_callback")?.to_string())?, ) @@ -186,11 +173,11 @@ pub async fn route_get_oidc_callback( .await .map_err(|_| OidcError::Other("Error fetching user info"))?; - if let Some(require_group) = oidc_config.require_group { + if let Some(require_group) = &oidc_config.require_group { if !user_info_claims .additional_claims() .groups - .contains(&require_group) + .contains(require_group) { return Ok(HttpResponse::build(StatusCode::UNAUTHORIZED) .body("User is not in an authorized group to use RustiCal")); diff --git a/crates/frontend/src/routes/login.rs b/crates/frontend/src/routes/login.rs index 31f6bee..462f639 100644 --- a/crates/frontend/src/routes/login.rs +++ b/crates/frontend/src/routes/login.rs @@ -1,4 +1,4 @@ -use crate::{FrontendConfig, oidc::OidcProviderData}; +use crate::{FrontendConfig, OidcConfig, oidc::OidcProviderData}; use actix_session::Session; use actix_web::{ HttpRequest, HttpResponse, Responder, @@ -24,22 +24,27 @@ pub struct GetLoginQuery { redirect_uri: Option, } -#[instrument(skip(req, config))] +#[instrument(skip(req, config, oidc_config))] pub async fn route_get_login( Query(GetLoginQuery { redirect_uri }): Query, req: HttpRequest, config: Data, + oidc_config: Data>, ) -> impl Responder { - LoginPage { - redirect_uri, - allow_password_login: config.allow_password_login, - oidc_data: config.oidc.as_ref().map(|oidc| OidcProviderData { - name: &oidc.name, + let oidc_data = oidc_config + .as_ref() + .as_ref() + .map(|oidc_config| OidcProviderData { + name: &oidc_config.name, redirect_url: req .url_for_static("frontend_login_oidc") .unwrap() .to_string(), - }), + }); + LoginPage { + redirect_uri, + allow_password_login: config.allow_password_login, + oidc_data, } .respond_to(&req) } diff --git a/docs/setup/oidc.md b/docs/setup/oidc.md index d3d5153..8f76fc2 100644 --- a/docs/setup/oidc.md +++ b/docs/setup/oidc.md @@ -5,7 +5,7 @@ You can set up RustiCal with an OpenID Connect identity provider ## Example: Authelia ```toml title="RustiCal configuration" -[frontend.oidc] +[oidc] name = "Authelia" issuer = "https://auth.example.com" client_id = "rustical" diff --git a/src/app.rs b/src/app.rs index fe8e87e..abc16ef 100644 --- a/src/app.rs +++ b/src/app.rs @@ -5,7 +5,7 @@ use actix_web::{App, web}; use rustical_caldav::caldav_service; use rustical_carddav::carddav_service; use rustical_frontend::nextcloud_login::{NextcloudFlows, configure_nextcloud_login}; -use rustical_frontend::{FrontendConfig, configure_frontend}; +use rustical_frontend::{FrontendConfig, OidcConfig, configure_frontend}; use rustical_store::auth::AuthenticationProvider; use rustical_store::{AddressbookStore, CalendarStore, SubscriptionStore}; use std::sync::Arc; @@ -13,12 +13,14 @@ use tracing_actix_web::TracingLogger; use crate::config::NextcloudLoginConfig; +#[allow(clippy::too_many_arguments)] pub fn make_app( addr_store: Arc, cal_store: Arc, subscription_store: Arc, auth_provider: Arc, frontend_config: FrontendConfig, + oidc_config: Option, nextcloud_login_config: NextcloudLoginConfig, nextcloud_flows_state: Arc, ) -> App< @@ -70,6 +72,7 @@ pub fn make_app( cal_store.clone(), addr_store.clone(), frontend_config, + oidc_config, ) })) .service(web::redirect("/", "/frontend").see_other()); diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 043b083..9b703af 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -37,9 +37,9 @@ pub fn cmd_gen_config(_args: GenConfigArgs) -> anyhow::Result<()> { frontend: FrontendConfig { secret_key: generate_frontend_secret(), enabled: true, - oidc: None, allow_password_login: true, }, + oidc: None, dav_push: DavPushConfig::default(), nextcloud_login: Default::default(), }; diff --git a/src/config.rs b/src/config.rs index a71444f..3a1c306 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,4 @@ -use rustical_frontend::FrontendConfig; +use rustical_frontend::{FrontendConfig, OidcConfig}; use rustical_store::auth::TomlUserStoreConfig; use serde::{Deserialize, Serialize}; @@ -84,6 +84,8 @@ pub struct Config { pub http: HttpConfig, pub frontend: FrontendConfig, #[serde(default)] + pub oidc: Option, + #[serde(default)] pub tracing: TracingConfig, #[serde(default)] pub dav_push: DavPushConfig, diff --git a/src/main.rs b/src/main.rs index 2b0b4b1..bd0bd61 100644 --- a/src/main.rs +++ b/src/main.rs @@ -108,6 +108,7 @@ async fn main() -> Result<()> { subscription_store.clone(), user_store.clone(), config.frontend.clone(), + config.oidc.clone(), config.nextcloud_login.clone(), nextcloud_flows.clone(), ) @@ -221,9 +222,9 @@ mod tests { FrontendConfig { enabled: false, secret_key: generate_frontend_secret(), - oidc: None, allow_password_login: false, }, + None, NextcloudLoginConfig { enabled: false }, Arc::new(NextcloudFlows::default()), );