Finish OIDC login
This commit is contained in:
parent
dbe1ec22e0
commit
1a022bd33e
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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user