Finish OIDC login

This commit is contained in:
Pierre HUBERT 2025-03-18 19:37:46 +01:00
parent dbe1ec22e0
commit 1a022bd33e
6 changed files with 149 additions and 12 deletions

View File

@ -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"

View File

@ -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"

View File

@ -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";
}

View File

@ -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())
}

View File

@ -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 {

View File

@ -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()