frontend: Move oidc configuration to dedicated section

This commit is contained in:
Lennart
2025-04-20 20:42:24 +02:00
parent cd0ebc574a
commit 678d2291e0
10 changed files with 98 additions and 98 deletions

View File

@@ -38,8 +38,6 @@ pub struct FrontendConfig {
pub secret_key: [u8; 64], pub secret_key: [u8; 64],
#[serde(default = "default_true")] #[serde(default = "default_true")]
pub enabled: bool, pub enabled: bool,
#[serde(default)]
pub oidc: Option<OidcConfig>,
#[serde(default = "default_true")] #[serde(default = "default_true")]
pub allow_password_login: bool, pub allow_password_login: bool,
} }

View File

@@ -34,7 +34,7 @@ pub mod nextcloud_login;
mod oidc; mod oidc;
mod routes; mod routes;
pub use config::FrontendConfig; pub use config::{FrontendConfig, OidcConfig};
pub fn generate_app_token() -> String { pub fn generate_app_token() -> String {
rand::thread_rng() rand::thread_rng()
@@ -193,63 +193,68 @@ pub fn configure_frontend<AP: AuthenticationProvider, CS: CalendarStore, AS: Add
cal_store: Arc<CS>, cal_store: Arc<CS>,
addr_store: Arc<AS>, addr_store: Arc<AS>,
frontend_config: FrontendConfig, frontend_config: FrontendConfig,
oidc_config: Option<OidcConfig>,
) { ) {
cfg.service( let mut scope = web::scope("")
web::scope("") .wrap(ErrorHandlers::new().handler(StatusCode::UNAUTHORIZED, unauthorized_handler))
.wrap(ErrorHandlers::new().handler(StatusCode::UNAUTHORIZED, unauthorized_handler)) .wrap(AuthenticationMiddleware::new(auth_provider.clone()))
.wrap(AuthenticationMiddleware::new(auth_provider.clone())) .wrap(session_middleware(frontend_config.secret_key))
.wrap(session_middleware(frontend_config.secret_key)) .app_data(Data::from(auth_provider))
.app_data(Data::from(auth_provider)) .app_data(Data::from(cal_store.clone()))
.app_data(Data::from(cal_store.clone())) .app_data(Data::from(addr_store.clone()))
.app_data(Data::from(addr_store.clone())) .app_data(Data::new(frontend_config.clone()))
.app_data(Data::new(frontend_config.clone())) .app_data(Data::new(oidc_config.clone()))
.service(EmbedService::<Assets>::new("/assets".to_owned())) .service(EmbedService::<Assets>::new("/assets".to_owned()))
.service(web::resource("").route(web::method(Method::GET).to(route_root))) .service(web::resource("").route(web::method(Method::GET).to(route_root)))
.service( .service(
web::resource("/user") web::resource("/user")
.route(web::method(Method::GET).to(route_user)) .route(web::method(Method::GET).to(route_user))
.name("frontend_user"), .name("frontend_user"),
) )
.service( .service(
web::resource("/user/{user}") web::resource("/user/{user}")
.route(web::method(Method::GET).to(route_user_named::<CS, AS>)) .route(web::method(Method::GET).to(route_user_named::<CS, AS>))
.name("frontend_user_named"), .name("frontend_user_named"),
) )
.service( .service(
web::resource("/user/{user}/app_token") web::resource("/user/{user}/app_token")
.route(web::method(Method::POST).to(route_post_app_token::<AP>)), .route(web::method(Method::POST).to(route_post_app_token::<AP>)),
) )
.service( .service(
web::resource("/user/{user}/app_token/{id}/delete") web::resource("/user/{user}/app_token/{id}/delete")
.route(web::method(Method::POST).to(route_delete_app_token::<AP>)), .route(web::method(Method::POST).to(route_delete_app_token::<AP>)),
) )
.service( .service(
web::resource("/user/{user}/calendar/{calendar}") web::resource("/user/{user}/calendar/{calendar}")
.route(web::method(Method::GET).to(route_calendar::<CS>)), .route(web::method(Method::GET).to(route_calendar::<CS>)),
) )
.service( .service(
web::resource("/user/{user}/calendar/{calendar}/restore") web::resource("/user/{user}/calendar/{calendar}/restore")
.route(web::method(Method::POST).to(route_calendar_restore::<CS>)), .route(web::method(Method::POST).to(route_calendar_restore::<CS>)),
) )
.service( .service(
web::resource("/user/{user}/addressbook/{addressbook}") web::resource("/user/{user}/addressbook/{addressbook}")
.route(web::method(Method::GET).to(route_addressbook::<AS>)), .route(web::method(Method::GET).to(route_addressbook::<AS>)),
) )
.service( .service(
web::resource("/user/{user}/addressbook/{addressbook}/restore") web::resource("/user/{user}/addressbook/{addressbook}/restore")
.route(web::method(Method::POST).to(route_addressbook_restore::<AS>)), .route(web::method(Method::POST).to(route_addressbook_restore::<AS>)),
) )
.service( .service(
web::resource("/login") web::resource("/login")
.name("frontend_login") .name("frontend_login")
.route(web::method(Method::GET).to(route_get_login)) .route(web::method(Method::GET).to(route_get_login))
.route(web::method(Method::POST).to(route_post_login::<AP>)), .route(web::method(Method::POST).to(route_post_login::<AP>)),
) )
.service( .service(
web::resource("/logout") web::resource("/logout")
.name("frontend_logout") .name("frontend_logout")
.route(web::method(Method::POST).to(route_post_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( .service(
web::resource("/login/oidc") web::resource("/login/oidc")
.name("frontend_login_oidc") .name("frontend_login_oidc")
@@ -259,6 +264,8 @@ pub fn configure_frontend<AP: AuthenticationProvider, CS: CalendarStore, AS: Add
web::resource("/login/oidc/callback") web::resource("/login/oidc/callback")
.name("frontend_oidc_callback") .name("frontend_oidc_callback")
.route(web::method(Method::GET).to(route_get_oidc_callback::<AP>)), .route(web::method(Method::GET).to(route_get_oidc_callback::<AP>)),
), );
); }
cfg.service(scope);
} }

View File

@@ -12,9 +12,6 @@ pub enum OidcError {
#[error("Cannot generate redirect url, something's not configured correctly")] #[error("Cannot generate redirect url, something's not configured correctly")]
ActixUrlGenerationError(#[from] UrlGenerationError), ActixUrlGenerationError(#[from] UrlGenerationError),
#[error("RustiCal is not configured correctly for OIDC")]
IncorrectConfiguration,
#[error(transparent)] #[error(transparent)]
OidcConfigurationError(#[from] ConfigurationError), OidcConfigurationError(#[from] ConfigurationError),

View File

@@ -1,7 +1,4 @@
use crate::{ use crate::config::{OidcConfig, UserIdClaim};
FrontendConfig,
config::{OidcConfig, UserIdClaim},
};
use actix_session::Session; use actix_session::Session;
use actix_web::{ use actix_web::{
HttpRequest, HttpResponse, Responder, HttpRequest, HttpResponse, Responder,
@@ -92,17 +89,12 @@ pub struct GetOidcForm {
pub async fn route_post_oidc( pub async fn route_post_oidc(
req: HttpRequest, req: HttpRequest,
Form(GetOidcForm { redirect_uri }): Form<GetOidcForm>, Form(GetOidcForm { redirect_uri }): Form<GetOidcForm>,
config: Data<FrontendConfig>, oidc_config: Data<OidcConfig>,
session: Session, session: Session,
) -> Result<impl Responder, OidcError> { ) -> Result<impl Responder, OidcError> {
let oidc_config = config
.oidc
.clone()
.ok_or(OidcError::IncorrectConfiguration)?;
let http_client = get_http_client(); let http_client = get_http_client();
let oidc_client = get_oidc_client( let oidc_client = get_oidc_client(
oidc_config.clone(), oidc_config.as_ref().clone(),
&http_client, &http_client,
RedirectUrl::new(req.url_for_static("frontend_oidc_callback")?.to_string())?, RedirectUrl::new(req.url_for_static("frontend_oidc_callback")?.to_string())?,
) )
@@ -137,21 +129,16 @@ pub async fn route_post_oidc(
pub struct AuthCallbackQuery { pub struct AuthCallbackQuery {
code: AuthorizationCode, code: AuthorizationCode,
iss: IssuerUrl, iss: IssuerUrl,
// scope: String,
// state: String,
} }
/// Handle callback from IdP page
pub async fn route_get_oidc_callback<AP: AuthenticationProvider>( pub async fn route_get_oidc_callback<AP: AuthenticationProvider>(
req: HttpRequest, req: HttpRequest,
config: Data<FrontendConfig>, oidc_config: Data<OidcConfig>,
session: Session, session: Session,
auth_provider: Data<AP>, auth_provider: Data<AP>,
Query(AuthCallbackQuery { code, iss }): Query<AuthCallbackQuery>, Query(AuthCallbackQuery { code, iss }): Query<AuthCallbackQuery>,
) -> Result<impl Responder, OidcError> { ) -> Result<impl Responder, OidcError> {
let oidc_config = config
.oidc
.clone()
.ok_or(OidcError::IncorrectConfiguration)?;
assert_eq!(iss, oidc_config.issuer); assert_eq!(iss, oidc_config.issuer);
let oidc_state = session let oidc_state = session
.remove_as::<OidcState>(SESSION_KEY_OIDC_STATE) .remove_as::<OidcState>(SESSION_KEY_OIDC_STATE)
@@ -160,7 +147,7 @@ pub async fn route_get_oidc_callback<AP: AuthenticationProvider>(
let http_client = get_http_client(); let http_client = get_http_client();
let oidc_client = get_oidc_client( let oidc_client = get_oidc_client(
oidc_config.clone(), oidc_config.get_ref().clone(),
&http_client, &http_client,
RedirectUrl::new(req.url_for_static("frontend_oidc_callback")?.to_string())?, RedirectUrl::new(req.url_for_static("frontend_oidc_callback")?.to_string())?,
) )
@@ -186,11 +173,11 @@ pub async fn route_get_oidc_callback<AP: AuthenticationProvider>(
.await .await
.map_err(|_| OidcError::Other("Error fetching user info"))?; .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 if !user_info_claims
.additional_claims() .additional_claims()
.groups .groups
.contains(&require_group) .contains(require_group)
{ {
return Ok(HttpResponse::build(StatusCode::UNAUTHORIZED) return Ok(HttpResponse::build(StatusCode::UNAUTHORIZED)
.body("User is not in an authorized group to use RustiCal")); .body("User is not in an authorized group to use RustiCal"));

View File

@@ -1,4 +1,4 @@
use crate::{FrontendConfig, oidc::OidcProviderData}; use crate::{FrontendConfig, OidcConfig, oidc::OidcProviderData};
use actix_session::Session; use actix_session::Session;
use actix_web::{ use actix_web::{
HttpRequest, HttpResponse, Responder, HttpRequest, HttpResponse, Responder,
@@ -24,22 +24,27 @@ pub struct GetLoginQuery {
redirect_uri: Option<String>, redirect_uri: Option<String>,
} }
#[instrument(skip(req, config))] #[instrument(skip(req, config, oidc_config))]
pub async fn route_get_login( pub async fn route_get_login(
Query(GetLoginQuery { redirect_uri }): Query<GetLoginQuery>, Query(GetLoginQuery { redirect_uri }): Query<GetLoginQuery>,
req: HttpRequest, req: HttpRequest,
config: Data<FrontendConfig>, config: Data<FrontendConfig>,
oidc_config: Data<Option<OidcConfig>>,
) -> impl Responder { ) -> impl Responder {
LoginPage { let oidc_data = oidc_config
redirect_uri, .as_ref()
allow_password_login: config.allow_password_login, .as_ref()
oidc_data: config.oidc.as_ref().map(|oidc| OidcProviderData { .map(|oidc_config| OidcProviderData {
name: &oidc.name, name: &oidc_config.name,
redirect_url: req redirect_url: req
.url_for_static("frontend_login_oidc") .url_for_static("frontend_login_oidc")
.unwrap() .unwrap()
.to_string(), .to_string(),
}), });
LoginPage {
redirect_uri,
allow_password_login: config.allow_password_login,
oidc_data,
} }
.respond_to(&req) .respond_to(&req)
} }

View File

@@ -5,7 +5,7 @@ You can set up RustiCal with an OpenID Connect identity provider
## Example: Authelia ## Example: Authelia
```toml title="RustiCal configuration" ```toml title="RustiCal configuration"
[frontend.oidc] [oidc]
name = "Authelia" name = "Authelia"
issuer = "https://auth.example.com" issuer = "https://auth.example.com"
client_id = "rustical" client_id = "rustical"

View File

@@ -5,7 +5,7 @@ use actix_web::{App, web};
use rustical_caldav::caldav_service; use rustical_caldav::caldav_service;
use rustical_carddav::carddav_service; use rustical_carddav::carddav_service;
use rustical_frontend::nextcloud_login::{NextcloudFlows, configure_nextcloud_login}; 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::auth::AuthenticationProvider;
use rustical_store::{AddressbookStore, CalendarStore, SubscriptionStore}; use rustical_store::{AddressbookStore, CalendarStore, SubscriptionStore};
use std::sync::Arc; use std::sync::Arc;
@@ -13,12 +13,14 @@ use tracing_actix_web::TracingLogger;
use crate::config::NextcloudLoginConfig; use crate::config::NextcloudLoginConfig;
#[allow(clippy::too_many_arguments)]
pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>( pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
addr_store: Arc<AS>, addr_store: Arc<AS>,
cal_store: Arc<CS>, cal_store: Arc<CS>,
subscription_store: Arc<S>, subscription_store: Arc<S>,
auth_provider: Arc<impl AuthenticationProvider>, auth_provider: Arc<impl AuthenticationProvider>,
frontend_config: FrontendConfig, frontend_config: FrontendConfig,
oidc_config: Option<OidcConfig>,
nextcloud_login_config: NextcloudLoginConfig, nextcloud_login_config: NextcloudLoginConfig,
nextcloud_flows_state: Arc<NextcloudFlows>, nextcloud_flows_state: Arc<NextcloudFlows>,
) -> App< ) -> App<
@@ -70,6 +72,7 @@ pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
cal_store.clone(), cal_store.clone(),
addr_store.clone(), addr_store.clone(),
frontend_config, frontend_config,
oidc_config,
) )
})) }))
.service(web::redirect("/", "/frontend").see_other()); .service(web::redirect("/", "/frontend").see_other());

View File

@@ -37,9 +37,9 @@ pub fn cmd_gen_config(_args: GenConfigArgs) -> anyhow::Result<()> {
frontend: FrontendConfig { frontend: FrontendConfig {
secret_key: generate_frontend_secret(), secret_key: generate_frontend_secret(),
enabled: true, enabled: true,
oidc: None,
allow_password_login: true, allow_password_login: true,
}, },
oidc: None,
dav_push: DavPushConfig::default(), dav_push: DavPushConfig::default(),
nextcloud_login: Default::default(), nextcloud_login: Default::default(),
}; };

View File

@@ -1,4 +1,4 @@
use rustical_frontend::FrontendConfig; use rustical_frontend::{FrontendConfig, OidcConfig};
use rustical_store::auth::TomlUserStoreConfig; use rustical_store::auth::TomlUserStoreConfig;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -84,6 +84,8 @@ pub struct Config {
pub http: HttpConfig, pub http: HttpConfig,
pub frontend: FrontendConfig, pub frontend: FrontendConfig,
#[serde(default)] #[serde(default)]
pub oidc: Option<OidcConfig>,
#[serde(default)]
pub tracing: TracingConfig, pub tracing: TracingConfig,
#[serde(default)] #[serde(default)]
pub dav_push: DavPushConfig, pub dav_push: DavPushConfig,

View File

@@ -108,6 +108,7 @@ async fn main() -> Result<()> {
subscription_store.clone(), subscription_store.clone(),
user_store.clone(), user_store.clone(),
config.frontend.clone(), config.frontend.clone(),
config.oidc.clone(),
config.nextcloud_login.clone(), config.nextcloud_login.clone(),
nextcloud_flows.clone(), nextcloud_flows.clone(),
) )
@@ -221,9 +222,9 @@ mod tests {
FrontendConfig { FrontendConfig {
enabled: false, enabled: false,
secret_key: generate_frontend_secret(), secret_key: generate_frontend_secret(),
oidc: None,
allow_password_login: false, allow_password_login: false,
}, },
None,
NextcloudLoginConfig { enabled: false }, NextcloudLoginConfig { enabled: false },
Arc::new(NextcloudFlows::default()), Arc::new(NextcloudFlows::default()),
); );