mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 22:52:22 +00:00
oidc: Remove all dependencies on other rustical crates
This commit is contained in:
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -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",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 }
|
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
10
crates/oidc/src/user_store.rs
Normal file
10
crates/oidc/src/user_store.rs
Normal 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>;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user