mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-14 10:32:19 +00:00
Implement almost all previous features
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
use openidconnect::{ClientId, ClientSecret, IssuerUrl, Scope};
|
||||
use reqwest::Url;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Default)]
|
||||
|
||||
@@ -13,6 +13,9 @@ pub enum OidcError {
|
||||
#[error(transparent)]
|
||||
OidcClaimsVerificationError(#[from] ClaimsVerificationError),
|
||||
|
||||
#[error(transparent)]
|
||||
SessionError(#[from] tower_sessions::session::Error),
|
||||
|
||||
#[error("{0}")]
|
||||
Other(&'static str),
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use axum::{
|
||||
Extension, Form,
|
||||
extract::{Query, Request, State},
|
||||
extract::Query,
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
};
|
||||
use axum_extra::extract::Host;
|
||||
pub use config::OidcConfig;
|
||||
use config::UserIdClaim;
|
||||
use error::OidcError;
|
||||
@@ -12,21 +13,20 @@ use openidconnect::{
|
||||
RedirectUrl, TokenResponse, UserInfoClaims,
|
||||
core::{CoreClient, CoreGenderClaim, CoreProviderMetadata, CoreResponseType},
|
||||
};
|
||||
use reqwest::{StatusCode, Url};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tower_sessions::Session;
|
||||
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, Clone)]
|
||||
pub struct OidcServiceConfig {
|
||||
pub default_redirect_route_name: &'static str,
|
||||
pub default_redirect_path: &'static str,
|
||||
pub session_key_user_id: &'static str,
|
||||
}
|
||||
|
||||
@@ -92,44 +92,48 @@ pub struct GetOidcForm {
|
||||
}
|
||||
|
||||
/// Endpoint that redirects to the authorize endpoint of the OIDC service
|
||||
// pub async fn route_post_oidc(
|
||||
// Form(GetOidcForm { redirect_uri }): Form<GetOidcForm>,
|
||||
// State(oidc_config): State<OidcConfig>,
|
||||
// // session: Session,
|
||||
// req: Request,
|
||||
// ) -> Result<Response, OidcError> {
|
||||
// let http_client = get_http_client();
|
||||
// let oidc_client = get_oidc_client(
|
||||
// oidc_config.clone(),
|
||||
// &http_client,
|
||||
// RedirectUrl::new(req.url_for_static(ROUTE_NAME_OIDC_CALLBACK)?.to_string())?,
|
||||
// )
|
||||
// .await?;
|
||||
//
|
||||
// let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
|
||||
//
|
||||
// let (auth_url, csrf_token, nonce) = oidc_client
|
||||
// .authorize_url(
|
||||
// AuthenticationFlow::<CoreResponseType>::AuthorizationCode,
|
||||
// CsrfToken::new_random,
|
||||
// Nonce::new_random,
|
||||
// )
|
||||
// .add_scopes(oidc_config.scopes.clone())
|
||||
// .set_pkce_challenge(pkce_challenge)
|
||||
// .url();
|
||||
//
|
||||
// // session.insert(
|
||||
// // SESSION_KEY_OIDC_STATE,
|
||||
// // OidcState {
|
||||
// // state: csrf_token,
|
||||
// // nonce,
|
||||
// // pkce_verifier,
|
||||
// // redirect_uri,
|
||||
// // },
|
||||
// // )?;
|
||||
//
|
||||
// Ok(Redirect::to(auth_url.as_str()).into_response())
|
||||
// }
|
||||
pub async fn route_post_oidc(
|
||||
Extension(oidc_config): Extension<OidcConfig>,
|
||||
session: Session,
|
||||
Host(host): Host,
|
||||
Form(GetOidcForm { redirect_uri }): Form<GetOidcForm>,
|
||||
) -> Result<Response, OidcError> {
|
||||
let callback_uri = format!("https://{host}/frontend/login/oidc/callback");
|
||||
|
||||
let http_client = get_http_client();
|
||||
let oidc_client = get_oidc_client(
|
||||
oidc_config.clone(),
|
||||
&http_client,
|
||||
RedirectUrl::new(callback_uri)?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
|
||||
|
||||
let (auth_url, csrf_token, nonce) = oidc_client
|
||||
.authorize_url(
|
||||
AuthenticationFlow::<CoreResponseType>::AuthorizationCode,
|
||||
CsrfToken::new_random,
|
||||
Nonce::new_random,
|
||||
)
|
||||
.add_scopes(oidc_config.scopes.clone())
|
||||
.set_pkce_challenge(pkce_challenge)
|
||||
.url();
|
||||
|
||||
session
|
||||
.insert(
|
||||
SESSION_KEY_OIDC_STATE,
|
||||
OidcState {
|
||||
state: csrf_token,
|
||||
nonce,
|
||||
pkce_verifier,
|
||||
redirect_uri,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Redirect::to(auth_url.as_str()).into_response())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct AuthCallbackQuery {
|
||||
@@ -139,123 +143,111 @@ pub struct AuthCallbackQuery {
|
||||
}
|
||||
|
||||
// Handle callback from IdP page
|
||||
// pub async fn route_get_oidc_callback<US: UserStore>(
|
||||
// Extension(oidc_config): Extension<OidcConfig>,
|
||||
// session: Session,
|
||||
// Extension(user_store): Extension<US>,
|
||||
// Query(AuthCallbackQuery { code, iss, state }): Query<AuthCallbackQuery>,
|
||||
// State(service_config): State<OidcServiceConfig>,
|
||||
// req: Request,
|
||||
// ) -> Result<Response, OidcError> {
|
||||
// assert_eq!(iss, oidc_config.issuer);
|
||||
// let oidc_state = session
|
||||
// .remove_as::<OidcState>(SESSION_KEY_OIDC_STATE)
|
||||
// .ok_or(OidcError::Other("No local OIDC state"))?
|
||||
// .map_err(|_| OidcError::Other("Error parsing OIDC state"))?;
|
||||
//
|
||||
// assert_eq!(oidc_state.state.secret(), &state);
|
||||
//
|
||||
// let http_client = get_http_client();
|
||||
// let oidc_client = get_oidc_client(
|
||||
// oidc_config.clone(),
|
||||
// &http_client,
|
||||
// RedirectUrl::new(req.url_for_static(ROUTE_NAME_OIDC_CALLBACK)?.to_string())?,
|
||||
// )
|
||||
// .await?;
|
||||
//
|
||||
// let token_response = oidc_client
|
||||
// .exchange_code(code)?
|
||||
// .set_pkce_verifier(oidc_state.pkce_verifier)
|
||||
// .request_async(&http_client)
|
||||
// .await
|
||||
// .map_err(|_| OidcError::Other("Error requesting token"))?;
|
||||
// let id_claims = token_response
|
||||
// .id_token()
|
||||
// .ok_or(OidcError::Other("OIDC provider did not return an ID token"))?
|
||||
// .claims(&oidc_client.id_token_verifier(), &oidc_state.nonce)?;
|
||||
//
|
||||
// let user_info_claims: UserInfoClaims<GroupAdditionalClaims, CoreGenderClaim> = oidc_client
|
||||
// .user_info(
|
||||
// token_response.access_token().clone(),
|
||||
// Some(id_claims.subject().clone()),
|
||||
// )?
|
||||
// .request_async(&http_client)
|
||||
// .await
|
||||
// .map_err(|_| OidcError::Other("Error fetching user info"))?;
|
||||
//
|
||||
// if let Some(require_group) = &oidc_config.require_group {
|
||||
// if !user_info_claims
|
||||
// .additional_claims()
|
||||
// .groups
|
||||
// .contains(require_group)
|
||||
// {
|
||||
// return Ok(HttpResponse::build(StatusCode::UNAUTHORIZED)
|
||||
// .body("User is not in an authorized group to use RustiCal"));
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// let user_id = match oidc_config.claim_userid {
|
||||
// UserIdClaim::Sub => user_info_claims.subject().to_string(),
|
||||
// UserIdClaim::PreferredUsername => user_info_claims
|
||||
// .preferred_username()
|
||||
// .ok_or(OidcError::Other("Missing preferred_username claim"))?
|
||||
// .to_string(),
|
||||
// };
|
||||
//
|
||||
// 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(service_config.default_redirect_route_name)?
|
||||
// .to_string();
|
||||
// let redirect_uri = oidc_state.redirect_uri.unwrap_or(default_redirect.clone());
|
||||
// let redirect_uri = req
|
||||
// .full_url()
|
||||
// .join(&redirect_uri)
|
||||
// .ok()
|
||||
// .and_then(|uri| req.full_url().make_relative(&uri))
|
||||
// .unwrap_or(default_redirect);
|
||||
//
|
||||
// // Complete login flow
|
||||
// session.insert(service_config.session_key_user_id, user_id.clone())?;
|
||||
//
|
||||
// Ok(Redirect::to(redirect_uri)
|
||||
// .temporary()
|
||||
// .respond_to(&req)
|
||||
// .map_into_boxed_body())
|
||||
// }
|
||||
pub async fn route_get_oidc_callback<US: UserStore + Clone>(
|
||||
Extension(oidc_config): Extension<OidcConfig>,
|
||||
Extension(user_store): Extension<US>,
|
||||
Extension(service_config): Extension<OidcServiceConfig>,
|
||||
session: Session,
|
||||
Query(AuthCallbackQuery { code, iss, state }): Query<AuthCallbackQuery>,
|
||||
Host(host): Host,
|
||||
) -> Result<Response, OidcError> {
|
||||
let callback_uri = format!("https://{host}/frontend/login/oidc/callback");
|
||||
|
||||
// pub fn configure_oidc<US: UserStore>(
|
||||
// cfg: &mut ServiceConfig,
|
||||
// oidc_config: OidcConfig,
|
||||
// service_config: OidcServiceConfig,
|
||||
// user_store: Arc<US>,
|
||||
// ) {
|
||||
// cfg.app_data(Data::new(oidc_config))
|
||||
// .app_data(Data::new(service_config))
|
||||
// .app_data(Data::from(user_store))
|
||||
// .service(
|
||||
// web::resource("")
|
||||
// .name(ROUTE_NAME_OIDC_LOGIN)
|
||||
// .post(route_post_oidc),
|
||||
// )
|
||||
// .service(
|
||||
// web::resource("/callback")
|
||||
// .name(ROUTE_NAME_OIDC_CALLBACK)
|
||||
// .get(route_get_oidc_callback::<US>),
|
||||
// );
|
||||
// }
|
||||
assert_eq!(iss, oidc_config.issuer);
|
||||
let oidc_state = session
|
||||
.remove::<OidcState>(SESSION_KEY_OIDC_STATE)
|
||||
.await?
|
||||
.ok_or(OidcError::Other("No local OIDC state"))?;
|
||||
|
||||
assert_eq!(oidc_state.state.secret(), &state);
|
||||
|
||||
let http_client = get_http_client();
|
||||
let oidc_client = get_oidc_client(
|
||||
oidc_config.clone(),
|
||||
&http_client,
|
||||
RedirectUrl::new(callback_uri)?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let token_response = oidc_client
|
||||
.exchange_code(code)?
|
||||
.set_pkce_verifier(oidc_state.pkce_verifier)
|
||||
.request_async(&http_client)
|
||||
.await
|
||||
.map_err(|_| OidcError::Other("Error requesting token"))?;
|
||||
let id_claims = token_response
|
||||
.id_token()
|
||||
.ok_or(OidcError::Other("OIDC provider did not return an ID token"))?
|
||||
.claims(&oidc_client.id_token_verifier(), &oidc_state.nonce)?;
|
||||
|
||||
let user_info_claims: UserInfoClaims<GroupAdditionalClaims, CoreGenderClaim> = oidc_client
|
||||
.user_info(
|
||||
token_response.access_token().clone(),
|
||||
Some(id_claims.subject().clone()),
|
||||
)?
|
||||
.request_async(&http_client)
|
||||
.await
|
||||
.map_err(|_| OidcError::Other("Error fetching user info"))?;
|
||||
|
||||
if let Some(require_group) = &oidc_config.require_group {
|
||||
if !user_info_claims
|
||||
.additional_claims()
|
||||
.groups
|
||||
.contains(require_group)
|
||||
{
|
||||
return Ok((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"User is not in an authorized group to use RustiCal",
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
}
|
||||
|
||||
let user_id = match oidc_config.claim_userid {
|
||||
UserIdClaim::Sub => user_info_claims.subject().to_string(),
|
||||
UserIdClaim::PreferredUsername => user_info_claims
|
||||
.preferred_username()
|
||||
.ok_or(OidcError::Other("Missing preferred_username claim"))?
|
||||
.to_string(),
|
||||
};
|
||||
|
||||
match user_store.user_exists(&user_id).await {
|
||||
Ok(false) => {
|
||||
// User does not exist
|
||||
if !oidc_config.allow_sign_up {
|
||||
return Ok((StatusCode::UNAUTHORIZED, "User signup is disabled").into_response());
|
||||
}
|
||||
// Create new user
|
||||
if let Err(err) = user_store.insert_user(&user_id).await {
|
||||
return Ok(err.into_response());
|
||||
}
|
||||
}
|
||||
Ok(true) => {}
|
||||
Err(err) => {
|
||||
return Ok(err.into_response());
|
||||
}
|
||||
}
|
||||
|
||||
let default_redirect = service_config.default_redirect_path.to_owned();
|
||||
let base_url: Url = format!("https://{host}").parse().unwrap();
|
||||
let redirect_uri = if let Some(redirect_uri) = oidc_state.redirect_uri {
|
||||
if let Ok(redirect_url) = base_url.join(&redirect_uri) {
|
||||
if redirect_url.origin() == base_url.origin() {
|
||||
redirect_url.path().to_owned()
|
||||
} else {
|
||||
default_redirect
|
||||
}
|
||||
} else {
|
||||
default_redirect
|
||||
}
|
||||
} else {
|
||||
default_redirect
|
||||
};
|
||||
|
||||
// Complete login flow
|
||||
session
|
||||
.insert(service_config.session_key_user_id, user_id.clone())
|
||||
.await?;
|
||||
|
||||
Ok(Redirect::to(&redirect_uri).into_response())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user