oidc: Remove all dependencies on other rustical crates

This commit is contained in:
Lennart
2025-04-20 22:12:52 +02:00
parent be9712241c
commit 0c940b595e
7 changed files with 107 additions and 59 deletions

3
Cargo.lock generated
View File

@@ -3153,6 +3153,7 @@ dependencies = [
"actix-web", "actix-web",
"askama", "askama",
"askama_web", "askama_web",
"async-trait",
"chrono", "chrono",
"chrono-humanize", "chrono-humanize",
"futures-core", "futures-core",
@@ -3176,9 +3177,9 @@ version = "0.1.0"
dependencies = [ dependencies = [
"actix-session", "actix-session",
"actix-web", "actix-web",
"async-trait",
"openidconnect", "openidconnect",
"reqwest", "reqwest",
"rustical_store",
"serde", "serde",
"thiserror 2.0.12", "thiserror 2.0.12",
] ]

View File

@@ -8,13 +8,14 @@ publish = false
[dependencies] [dependencies]
askama.workspace = true askama.workspace = true
async-trait.workspace = true
askama_web.workspace = true askama_web.workspace = true
actix-session = { workspace = true } actix-session.workspace = true
serde = { workspace = true } serde.workspace = true
thiserror = { workspace = true } thiserror.workspace = true
tokio = { workspace = true } tokio.workspace = true
actix-web = { workspace = true } actix-web.workspace = true
rustical_store = { workspace = true } rustical_store.workspace = true
rust-embed.workspace = true rust-embed.workspace = true
futures-core.workspace = true futures-core.workspace = true
hex.workspace = true hex.workspace = true

View File

@@ -14,13 +14,14 @@ use actix_web::{
use askama::Template; use askama::Template;
use askama_web::WebTemplate; use askama_web::WebTemplate;
use assets::{Assets, EmbedService}; use assets::{Assets, EmbedService};
use async_trait::async_trait;
use rand::{Rng, distributions::Alphanumeric}; use rand::{Rng, distributions::Alphanumeric};
use routes::{ use routes::{
addressbook::{route_addressbook, route_addressbook_restore}, addressbook::{route_addressbook, route_addressbook_restore},
calendar::{route_calendar, route_calendar_restore}, calendar::{route_calendar, route_calendar_restore},
login::{route_get_login, route_post_login, route_post_logout}, 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::{ use rustical_store::{
Addressbook, AddressbookStore, Calendar, CalendarStore, Addressbook, AddressbookStore, Calendar, CalendarStore,
auth::{AuthenticationMiddleware, AuthenticationProvider, User}, auth::{AuthenticationMiddleware, AuthenticationProvider, User},
@@ -201,7 +202,7 @@ pub fn configure_frontend<AP: AuthenticationProvider, CS: CalendarStore, AS: Add
.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.clone()))
.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()))
@@ -252,11 +253,42 @@ pub fn configure_frontend<AP: AuthenticationProvider, CS: CalendarStore, AS: Add
); );
if let Some(oidc_config) = oidc_config { if let Some(oidc_config) = oidc_config {
scope = scope.service( scope = scope.service(web::scope("/login/oidc").configure(|cfg| {
web::scope("/login/oidc") configure_oidc(
.configure(|cfg| configure_oidc::<AP>(cfg, oidc_config, ROUTE_NAME_HOME)), 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); cfg.service(scope);
} }
struct OidcUserStore<AP: AuthenticationProvider>(Arc<AP>);
#[async_trait(?Send)]
impl<AP: AuthenticationProvider> UserStore for OidcUserStore<AP> {
type Error = rustical_store::Error;
async fn user_exists(&self, id: &str) -> Result<bool, Self::Error> {
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
}
}

View File

@@ -7,10 +7,9 @@ repository.workspace = true
[dependencies] [dependencies]
openidconnect.workspace = true openidconnect.workspace = true
serde = { workspace = true } serde.workspace = true
reqwest.workspace = true reqwest.workspace = true
# TODO: Remove this dependency actix-web.workspace = true
rustical_store = { workspace = true } actix-session.workspace = true
actix-web = { workspace = true } thiserror.workspace = true
actix-session = { workspace = true } async-trait.workspace = true
thiserror = { workspace = true }

View File

@@ -21,9 +21,6 @@ pub enum OidcError {
#[error(transparent)] #[error(transparent)]
SessionInsertError(#[from] SessionInsertError), SessionInsertError(#[from] SessionInsertError),
#[error(transparent)]
StoreError(#[from] rustical_store::Error),
#[error("{0}")] #[error("{0}")]
Other(&'static str), Other(&'static str),
} }

View File

@@ -1,6 +1,6 @@
use actix_session::Session; use actix_session::Session;
use actix_web::{ use actix_web::{
HttpRequest, HttpResponse, Responder, HttpRequest, HttpResponse, Responder, ResponseError,
http::StatusCode, http::StatusCode,
web::{self, Data, Form, Query, Redirect, ServiceConfig}, web::{self, Data, Form, Query, Redirect, ServiceConfig},
}; };
@@ -13,18 +13,23 @@ use openidconnect::{
RedirectUrl, TokenResponse, UserInfoClaims, RedirectUrl, TokenResponse, UserInfoClaims,
core::{CoreClient, CoreGenderClaim, CoreProviderMetadata, CoreResponseType}, core::{CoreClient, CoreGenderClaim, CoreProviderMetadata, CoreResponseType},
}; };
use rustical_store::auth::{AuthenticationProvider, User, user::PrincipalType::Individual};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::sync::Arc;
pub use user_store::UserStore;
mod config; mod config;
mod error; mod error;
mod user_store;
pub const ROUTE_NAME_OIDC_LOGIN: &str = "oidc_login"; pub const ROUTE_NAME_OIDC_LOGIN: &str = "oidc_login";
const ROUTE_NAME_OIDC_CALLBACK: &str = "oidc_callback"; const ROUTE_NAME_OIDC_CALLBACK: &str = "oidc_callback";
const SESSION_KEY_OIDC_STATE: &str = "oidc_state"; const SESSION_KEY_OIDC_STATE: &str = "oidc_state";
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct DefaultRedirectRouteName(pub &'static str); pub struct OidcServiceConfig {
pub default_redirect_route_name: &'static str,
pub session_key_user_id: &'static str,
}
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
struct OidcState { struct OidcState {
@@ -93,7 +98,7 @@ pub async fn route_post_oidc(
Form(GetOidcForm { redirect_uri }): Form<GetOidcForm>, Form(GetOidcForm { redirect_uri }): Form<GetOidcForm>,
oidc_config: Data<OidcConfig>, oidc_config: Data<OidcConfig>,
session: Session, session: Session,
) -> Result<impl Responder, OidcError> { ) -> Result<HttpResponse, OidcError> {
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.as_ref().clone(), 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)] #[derive(Debug, Clone, Deserialize)]
@@ -135,14 +143,14 @@ pub struct AuthCallbackQuery {
} }
/// Handle callback from IdP page /// Handle callback from IdP page
pub async fn route_get_oidc_callback<AP: AuthenticationProvider>( pub async fn route_get_oidc_callback<US: UserStore>(
req: HttpRequest, req: HttpRequest,
oidc_config: Data<OidcConfig>, oidc_config: Data<OidcConfig>,
session: Session, session: Session,
auth_provider: Data<AP>, user_store: Data<US>,
Query(AuthCallbackQuery { code, iss, state }): Query<AuthCallbackQuery>, Query(AuthCallbackQuery { code, iss, state }): Query<AuthCallbackQuery>,
default_redirect_name: Data<DefaultRedirectRouteName>, service_config: Data<OidcServiceConfig>,
) -> Result<impl Responder, OidcError> { ) -> Result<HttpResponse, OidcError> {
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)
@@ -198,23 +206,25 @@ pub async fn route_get_oidc_callback<AP: AuthenticationProvider>(
.to_string(), .to_string(),
}; };
let mut user = auth_provider.get_principal(&user_id).await?; match user_store.user_exists(&user_id).await {
if user.is_none() { Ok(false) => {
let new_user = User { // User does not exist
id: user_id, if !oidc_config.allow_sign_up {
displayname: None, return Ok(HttpResponse::Unauthorized().body("User sign up disabled"));
app_tokens: vec![], }
password: None, // Create new user
principal_type: Individual, if let Err(err) = user_store.insert_user(&user_id).await {
memberships: vec![], return Ok(err.error_response());
}; }
}
auth_provider.insert_principal(new_user.clone()).await?; Ok(true) => {}
user = Some(new_user); Err(err) => {
return Ok(err.error_response());
}
} }
let default_redirect = req let default_redirect = req
.url_for_static(default_redirect_name.as_ref().0)? .url_for_static(service_config.default_redirect_route_name)?
.to_string(); .to_string();
let redirect_uri = oidc_state.redirect_uri.unwrap_or(default_redirect.clone()); let redirect_uri = oidc_state.redirect_uri.unwrap_or(default_redirect.clone());
let redirect_uri = req let redirect_uri = req
@@ -225,25 +235,23 @@ pub async fn route_get_oidc_callback<AP: AuthenticationProvider>(
.unwrap_or(default_redirect); .unwrap_or(default_redirect);
// Complete login flow // Complete login flow
if let Some(user) = user { session.insert(service_config.session_key_user_id, user_id.clone())?;
session.insert("user", user.id.clone())?;
Ok(Redirect::to(redirect_uri) Ok(Redirect::to(redirect_uri)
.temporary() .temporary()
.respond_to(&req) .respond_to(&req)
.map_into_boxed_body()) .map_into_boxed_body())
} else {
Ok(HttpResponse::build(StatusCode::UNAUTHORIZED).body("User does not exist"))
}
} }
pub fn configure_oidc<AP: AuthenticationProvider>( pub fn configure_oidc<US: UserStore>(
cfg: &mut ServiceConfig, cfg: &mut ServiceConfig,
oidc_config: OidcConfig, oidc_config: OidcConfig,
default_redirect_name: &'static str, service_config: OidcServiceConfig,
user_store: Arc<US>,
) { ) {
cfg.app_data(Data::new(oidc_config)) 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( .service(
web::resource("") web::resource("")
.name(ROUTE_NAME_OIDC_LOGIN) .name(ROUTE_NAME_OIDC_LOGIN)
@@ -252,6 +260,6 @@ pub fn configure_oidc<AP: AuthenticationProvider>(
.service( .service(
web::resource("/callback") web::resource("/callback")
.name(ROUTE_NAME_OIDC_CALLBACK) .name(ROUTE_NAME_OIDC_CALLBACK)
.get(route_get_oidc_callback::<AP>), .get(route_get_oidc_callback::<US>),
); );
} }

View File

@@ -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<bool, Self::Error>;
async fn insert_user(&self, id: &str) -> Result<(), Self::Error>;
}