Finish OIDC login
This commit is contained in:
		
							
								
								
									
										30
									
								
								moneymgr_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										30
									
								
								moneymgr_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							@@ -452,7 +452,7 @@ dependencies = [
 | 
			
		||||
 "quick-xml 0.32.0",
 | 
			
		||||
 "rust-ini",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "thiserror",
 | 
			
		||||
 "thiserror 1.0.69",
 | 
			
		||||
 "time",
 | 
			
		||||
 "url",
 | 
			
		||||
]
 | 
			
		||||
@@ -463,7 +463,7 @@ version = "0.26.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "73ae4ae7c45238b60af0a3b27ef2fcc7bd5b8fdcd8a6d679919558b40d3eff7a"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "thiserror",
 | 
			
		||||
 "thiserror 1.0.69",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
@@ -1833,7 +1833,7 @@ dependencies = [
 | 
			
		||||
 "rust-s3",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "thiserror",
 | 
			
		||||
 "thiserror 2.0.12",
 | 
			
		||||
 "tokio",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
@@ -2362,7 +2362,7 @@ dependencies = [
 | 
			
		||||
 "serde_derive",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "sha2",
 | 
			
		||||
 "thiserror",
 | 
			
		||||
 "thiserror 1.0.69",
 | 
			
		||||
 "time",
 | 
			
		||||
 "tokio",
 | 
			
		||||
 "tokio-stream",
 | 
			
		||||
@@ -2739,7 +2739,16 @@ version = "1.0.69"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "thiserror-impl",
 | 
			
		||||
 "thiserror-impl 1.0.69",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "thiserror"
 | 
			
		||||
version = "2.0.12"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "thiserror-impl 2.0.12",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
@@ -2753,6 +2762,17 @@ dependencies = [
 | 
			
		||||
 "syn",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "thiserror-impl"
 | 
			
		||||
version = "2.0.12"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "syn",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "time"
 | 
			
		||||
version = "0.3.39"
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,7 @@ lazy_static = "1.5.0"
 | 
			
		||||
anyhow = "1.0.97"
 | 
			
		||||
serde = { version = "1.0.219", features = ["derive"] }
 | 
			
		||||
rust-s3 = "0.36.0-beta.2"
 | 
			
		||||
thiserror = "1.0.69"
 | 
			
		||||
thiserror = "2.0.12"
 | 
			
		||||
tokio = "1.44.1"
 | 
			
		||||
futures-util = "0.3.31"
 | 
			
		||||
serde_json = "1.0.140"
 | 
			
		||||
 
 | 
			
		||||
@@ -2,4 +2,8 @@
 | 
			
		||||
pub mod sessions {
 | 
			
		||||
    /// OpenID auth session state key
 | 
			
		||||
    pub const OIDC_STATE_KEY: &str = "oidc-state";
 | 
			
		||||
    /// OpenID auth remote IP address
 | 
			
		||||
    pub const OIDC_REMOTE_IP: &str = "oidc-remote-ip";
 | 
			
		||||
    /// Authenticated ID
 | 
			
		||||
    pub const USER_ID: &str = "uid";
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,9 @@
 | 
			
		||||
use crate::app_config::AppConfig;
 | 
			
		||||
use crate::controllers::HttpResult;
 | 
			
		||||
use crate::controllers::{HttpFailure, HttpResult};
 | 
			
		||||
use crate::extractors::money_session::MoneySession;
 | 
			
		||||
use actix_web::HttpResponse;
 | 
			
		||||
use crate::services::users_service;
 | 
			
		||||
use actix_remote_ip::RemoteIP;
 | 
			
		||||
use actix_web::{HttpResponse, web};
 | 
			
		||||
use light_openid::primitives::OpenIDConfig;
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize)]
 | 
			
		||||
@@ -10,7 +12,7 @@ struct StartOIDCResponse {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Start OIDC authentication
 | 
			
		||||
pub async fn start_oidc(session: MoneySession) -> HttpResult {
 | 
			
		||||
pub async fn start_oidc(session: MoneySession, remote_ip: RemoteIP) -> HttpResult {
 | 
			
		||||
    let prov = AppConfig::get().openid_provider();
 | 
			
		||||
 | 
			
		||||
    let conf = match OpenIDConfig::load_from_url(prov.configuration_url).await {
 | 
			
		||||
@@ -22,7 +24,7 @@ pub async fn start_oidc(session: MoneySession) -> HttpResult {
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let state = match session.gen_oidc_state() {
 | 
			
		||||
    let state = match session.gen_oidc_state(remote_ip.0) {
 | 
			
		||||
        Ok(s) => s,
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            log::error!("Failed to generate auth state! {e}");
 | 
			
		||||
@@ -39,4 +41,66 @@ pub async fn start_oidc(session: MoneySession) -> HttpResult {
 | 
			
		||||
    }))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO : take from previous projects
 | 
			
		||||
#[derive(serde::Deserialize)]
 | 
			
		||||
pub struct FinishOpenIDLoginQuery {
 | 
			
		||||
    code: String,
 | 
			
		||||
    state: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Finish OIDC authentication
 | 
			
		||||
pub async fn finish_oidc(
 | 
			
		||||
    session: MoneySession,
 | 
			
		||||
    remote_ip: RemoteIP,
 | 
			
		||||
    req: web::Json<FinishOpenIDLoginQuery>,
 | 
			
		||||
) -> HttpResult {
 | 
			
		||||
    if let Err(e) = session.validate_state(&req.state, remote_ip.0) {
 | 
			
		||||
        log::error!("Failed to validate OIDC CB state! {e}");
 | 
			
		||||
        return Ok(HttpResponse::BadRequest().json("Invalid state!"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let prov = AppConfig::get().openid_provider();
 | 
			
		||||
 | 
			
		||||
    let conf = OpenIDConfig::load_from_url(prov.configuration_url)
 | 
			
		||||
        .await
 | 
			
		||||
        .map_err(HttpFailure::OpenID)?;
 | 
			
		||||
 | 
			
		||||
    let (token, _) = conf
 | 
			
		||||
        .request_token(
 | 
			
		||||
            prov.client_id,
 | 
			
		||||
            prov.client_secret,
 | 
			
		||||
            &req.code,
 | 
			
		||||
            &AppConfig::get().oidc_redirect_url(),
 | 
			
		||||
        )
 | 
			
		||||
        .await
 | 
			
		||||
        .map_err(HttpFailure::OpenID)?;
 | 
			
		||||
    let (user_info, _) = conf
 | 
			
		||||
        .request_user_info(&token)
 | 
			
		||||
        .await
 | 
			
		||||
        .map_err(HttpFailure::OpenID)?;
 | 
			
		||||
 | 
			
		||||
    if user_info.email_verified != Some(true) {
 | 
			
		||||
        log::error!("Email is not verified!");
 | 
			
		||||
        return Ok(HttpResponse::Unauthorized().json("Email unverified by IDP!"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let mail = match user_info.email {
 | 
			
		||||
        Some(m) => m,
 | 
			
		||||
        None => {
 | 
			
		||||
            return Ok(HttpResponse::Unauthorized().json("Email not provided by the IDP!"));
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let user_name = user_info.name.unwrap_or_else(|| {
 | 
			
		||||
        format!(
 | 
			
		||||
            "{} {}",
 | 
			
		||||
            user_info.given_name.as_deref().unwrap_or(""),
 | 
			
		||||
            user_info.family_name.as_deref().unwrap_or("")
 | 
			
		||||
        )
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    let user = users_service::create_or_update_user(&mail, &user_name).await?;
 | 
			
		||||
 | 
			
		||||
    session.set_user(&user)?;
 | 
			
		||||
 | 
			
		||||
    Ok(HttpResponse::Ok().finish())
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,24 @@
 | 
			
		||||
use crate::constants;
 | 
			
		||||
use crate::models::users::User;
 | 
			
		||||
use crate::utils::rand_utils::rand_string;
 | 
			
		||||
use actix_session::Session;
 | 
			
		||||
use actix_web::dev::Payload;
 | 
			
		||||
use actix_web::{Error, FromRequest, HttpRequest};
 | 
			
		||||
use futures_util::future::{Ready, ready};
 | 
			
		||||
use std::net::IpAddr;
 | 
			
		||||
 | 
			
		||||
/// Money session errors
 | 
			
		||||
#[derive(thiserror::Error, Debug)]
 | 
			
		||||
enum MoneySessionError {
 | 
			
		||||
    #[error("Missing state!")]
 | 
			
		||||
    OIDCMissingState,
 | 
			
		||||
    #[error("Missing IP address!")]
 | 
			
		||||
    OIDCMissingIP,
 | 
			
		||||
    #[error("Invalid state!")]
 | 
			
		||||
    OIDCInvalidState,
 | 
			
		||||
    #[error("Invalid IP address!")]
 | 
			
		||||
    OIDCInvalidIP,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Money session
 | 
			
		||||
///
 | 
			
		||||
@@ -12,12 +27,42 @@ pub struct MoneySession(Session);
 | 
			
		||||
 | 
			
		||||
impl MoneySession {
 | 
			
		||||
    /// Generate OpenID state for this session
 | 
			
		||||
    pub fn gen_oidc_state(&self) -> anyhow::Result<String> {
 | 
			
		||||
    pub fn gen_oidc_state(&self, ip: IpAddr) -> anyhow::Result<String> {
 | 
			
		||||
        let random_string = rand_string(50);
 | 
			
		||||
        self.0
 | 
			
		||||
            .insert(constants::sessions::OIDC_STATE_KEY, random_string.clone())?;
 | 
			
		||||
        self.0.insert(constants::sessions::OIDC_REMOTE_IP, ip)?;
 | 
			
		||||
        Ok(random_string)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Validate OpenID state
 | 
			
		||||
    pub fn validate_state(&self, state: &str, ip: IpAddr) -> anyhow::Result<()> {
 | 
			
		||||
        let session_state: String = self
 | 
			
		||||
            .0
 | 
			
		||||
            .get(constants::sessions::OIDC_STATE_KEY)?
 | 
			
		||||
            .ok_or(MoneySessionError::OIDCMissingState)?;
 | 
			
		||||
 | 
			
		||||
        let session_ip: IpAddr = self
 | 
			
		||||
            .0
 | 
			
		||||
            .get(constants::sessions::OIDC_REMOTE_IP)?
 | 
			
		||||
            .ok_or(MoneySessionError::OIDCMissingIP)?;
 | 
			
		||||
 | 
			
		||||
        if session_state != state {
 | 
			
		||||
            return Err(anyhow::anyhow!(MoneySessionError::OIDCInvalidState));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if session_ip != ip {
 | 
			
		||||
            return Err(anyhow::anyhow!(MoneySessionError::OIDCInvalidIP));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Set current user
 | 
			
		||||
    pub fn set_user(&self, user: &User) -> anyhow::Result<()> {
 | 
			
		||||
        self.0.insert(constants::sessions::USER_ID, user.id())?;
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl FromRequest for MoneySession {
 | 
			
		||||
 
 | 
			
		||||
@@ -81,6 +81,10 @@ async fn main() -> std::io::Result<()> {
 | 
			
		||||
                "/api/auth/start_oidc",
 | 
			
		||||
                web::get().to(auth_controller::start_oidc),
 | 
			
		||||
            )
 | 
			
		||||
            .route(
 | 
			
		||||
                "/api/auth/finish_oidc",
 | 
			
		||||
                web::post().to(auth_controller::finish_oidc),
 | 
			
		||||
            )
 | 
			
		||||
    })
 | 
			
		||||
    .bind(AppConfig::get().listen_address.as_str())?
 | 
			
		||||
    .run()
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user