diff --git a/Cargo.toml b/Cargo.toml index 94dfa79..cc1cc67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ tracing-subscriber = "^0.3.17" tracing = "^0.1.37" himmelblau_unix_common = { path = "src/common" } kanidm_unix_common = { path = "src/glue" } -libhimmelblau = { version = "0.4.2" } +libhimmelblau = { version = "0.4.4" } clap = { version = "^4.5", features = ["derive", "env"] } clap_complete = "^4.4.1" reqwest = { version = "^0.12.2", features = ["json"] } diff --git a/README.md b/README.md index d2af51f..0f2fb37 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ sudo zypper ref && sudo zypper in himmelblau nss-himmelblau pam-himmelblau The following packages are required on openSUSE to build and test this package. - sudo zypper in make cargo git gcc sqlite3-devel libopenssl-3-devel pam-devel libcap-devel libtalloc-devel libtevent-devel libldb-devel libdhash-devel krb5-devel pcre2-devel libclang13 autoconf make automake gettext-tools clang dbus-1-devel utf8proc-devel + sudo zypper in make cargo git gcc sqlite3-devel libopenssl-3-devel pam-devel libcap-devel libtalloc-devel libtevent-devel libldb-devel libdhash-devel krb5-devel pcre2-devel libclang13 autoconf make automake gettext-tools clang dbus-1-devel utf8proc-devel gobject-introspection-devel cairo-devel gdk-pixbuf-devel libsoup-devel pango-devel atk-devel gtk3-devel webkit2gtk3-devel Or on Debian based systems: diff --git a/images/rpm/Dockerfile.fedora41 b/images/rpm/Dockerfile.fedora41 index b0647bc..ddc0e80 100644 --- a/images/rpm/Dockerfile.fedora41 +++ b/images/rpm/Dockerfile.fedora41 @@ -22,7 +22,14 @@ RUN dnf -y update && \ gettext \ sqlite-devel \ utf8proc-devel \ - cargo && \ + cargo \ + gobject-introspection-devel \ + cairo-devel \ + libsoup-devel \ + pango-devel \ + atk-devel \ + gtk3-devel \ + webkit2gtk3-devel && \ dnf clean all # Set environment for Rust @@ -37,4 +44,4 @@ WORKDIR /himmelblau RUN cargo install cargo-generate-rpm # Build the project and create the RPM package -CMD cargo clean && cargo build --release && strip -s target/release/*.so && strip -s target/release/aad-tool && strip -s target/release/himmelblaud && strip -s target/release/himmelblaud_tasks && strip -s target/release/broker && cargo generate-rpm -p src/daemon && cargo generate-rpm -p src/nss && cargo generate-rpm -p src/pam && cargo generate-rpm -p src/sshd-config && cargo generate-rpm -p src/sso +CMD cargo clean && cargo build --release --features interactive && strip -s target/release/*.so && strip -s target/release/aad-tool && strip -s target/release/himmelblaud && strip -s target/release/himmelblaud_tasks && strip -s target/release/broker && cargo generate-rpm -p src/daemon && cargo generate-rpm -p src/nss && cargo generate-rpm -p src/pam && cargo generate-rpm -p src/sshd-config && cargo generate-rpm -p src/sso diff --git a/images/rpm/Dockerfile.rawhide b/images/rpm/Dockerfile.rawhide index 493f0f4..cf4d4d7 100644 --- a/images/rpm/Dockerfile.rawhide +++ b/images/rpm/Dockerfile.rawhide @@ -22,7 +22,14 @@ RUN dnf -y update && \ gettext \ sqlite-devel \ utf8proc-devel \ - cargo && \ + cargo \ + gobject-introspection-devel \ + cairo-devel \ + libsoup-devel \ + pango-devel \ + atk-devel \ + gtk3-devel \ + webkit2gtk3-devel && \ dnf clean all # Set environment for Rust @@ -37,4 +44,4 @@ WORKDIR /himmelblau RUN cargo install cargo-generate-rpm # Build the project and create the RPM package -CMD cargo clean && cargo build --release && strip -s target/release/*.so && strip -s target/release/aad-tool && strip -s target/release/himmelblaud && strip -s target/release/himmelblaud_tasks && strip -s target/release/broker && cargo generate-rpm -p src/daemon && cargo generate-rpm -p src/nss && cargo generate-rpm -p src/pam && cargo generate-rpm -p src/sshd-config && cargo generate-rpm -p src/sso +CMD cargo clean && cargo build --release --features interactive && strip -s target/release/*.so && strip -s target/release/aad-tool && strip -s target/release/himmelblaud && strip -s target/release/himmelblaud_tasks && strip -s target/release/broker && cargo generate-rpm -p src/daemon && cargo generate-rpm -p src/nss && cargo generate-rpm -p src/pam && cargo generate-rpm -p src/sshd-config && cargo generate-rpm -p src/sso diff --git a/images/rpm/Dockerfile.rocky9 b/images/rpm/Dockerfile.rocky9 index e48d98d..2617e9a 100644 --- a/images/rpm/Dockerfile.rocky9 +++ b/images/rpm/Dockerfile.rocky9 @@ -28,6 +28,14 @@ RUN yum update -y && yum install -y \ gettext \ sqlite-devel \ utf8proc-devel \ + gobject-introspection-devel \ + cairo-devel \ + gdk-pixbuf-devel \ + libsoup-devel \ + pango-devel \ + atk-devel \ + gtk3-devel \ + webkit2gtk3-devel \ && yum clean all # Install Rust (latest stable) @@ -45,4 +53,4 @@ WORKDIR /himmelblau RUN cargo install cargo-generate-rpm # Build the project and create the .deb package -CMD cargo clean && cargo build --release && strip -s target/release/*.so && strip -s target/release/aad-tool && strip -s target/release/himmelblaud && strip -s target/release/himmelblaud_tasks && strip -s target/release/broker && cargo generate-rpm -p src/daemon && cargo generate-rpm -p src/nss && cargo generate-rpm -p src/pam && cargo generate-rpm -p src/sshd-config && cargo generate-rpm -p src/sso +CMD cargo clean && cargo build --release --features interactive && strip -s target/release/*.so && strip -s target/release/aad-tool && strip -s target/release/himmelblaud && strip -s target/release/himmelblaud_tasks && strip -s target/release/broker && cargo generate-rpm -p src/daemon && cargo generate-rpm -p src/nss && cargo generate-rpm -p src/pam && cargo generate-rpm -p src/sshd-config && cargo generate-rpm -p src/sso diff --git a/images/rpm/Dockerfile.tumbleweed b/images/rpm/Dockerfile.tumbleweed index 719a60d..c98c12b 100644 --- a/images/rpm/Dockerfile.tumbleweed +++ b/images/rpm/Dockerfile.tumbleweed @@ -24,6 +24,14 @@ RUN zypper --non-interactive refresh && zypper --non-interactive update && \ sqlite3-devel \ utf8proc-devel \ cargo \ + gobject-introspection-devel \ + cairo-devel \ + gdk-pixbuf-devel \ + libsoup-devel \ + pango-devel \ + atk-devel \ + gtk3-devel \ + webkit2gtk3-devel \ && zypper clean --all # Set environment for Rust @@ -38,4 +46,4 @@ WORKDIR /himmelblau RUN cargo install cargo-generate-rpm # Build the project and create the RPM package -CMD cargo clean && cargo build --release && strip -s target/release/*.so && strip -s target/release/aad-tool && strip -s target/release/himmelblaud && strip -s target/release/himmelblaud_tasks && strip -s target/release/broker && cargo generate-rpm -p src/daemon && cargo generate-rpm -p src/nss && cargo generate-rpm -p src/pam && cargo generate-rpm -p src/sshd-config && cargo generate-rpm -p src/sso +CMD cargo clean && cargo build --release --features interactive && strip -s target/release/*.so && strip -s target/release/aad-tool && strip -s target/release/himmelblaud && strip -s target/release/himmelblaud_tasks && strip -s target/release/broker && cargo generate-rpm -p src/daemon && cargo generate-rpm -p src/nss && cargo generate-rpm -p src/pam && cargo generate-rpm -p src/sshd-config && cargo generate-rpm -p src/sso diff --git a/images/ubuntu/Dockerfile.22.04 b/images/ubuntu/Dockerfile.22.04 index 3949078..e92734c 100644 --- a/images/ubuntu/Dockerfile.22.04 +++ b/images/ubuntu/Dockerfile.22.04 @@ -32,6 +32,15 @@ RUN apt-get update && apt-get install -y \ cargo \ libsqlite3-dev \ libutf8proc-dev \ + libgirepository1.0-dev \ + libcairo2-dev \ + libgdk-pixbuf2.0-dev \ + libsoup-3.0-dev \ + libpango1.0-dev \ + libatk1.0-dev \ + libgtk-3-dev \ + libwebkit2gtk-4.1-dev \ + libjavascriptcoregtk-4.1-dev \ && rm -rf /var/lib/apt/lists/* # Install Rust (latest stable) diff --git a/images/ubuntu/Dockerfile.24.04 b/images/ubuntu/Dockerfile.24.04 index 2d217a4..460247a 100644 --- a/images/ubuntu/Dockerfile.24.04 +++ b/images/ubuntu/Dockerfile.24.04 @@ -32,6 +32,14 @@ RUN apt-get update && apt-get install -y \ cargo \ libsqlite3-dev \ libutf8proc-dev \ + libgirepository1.0-dev \ + libcairo2-dev \ + libgdk-pixbuf2.0-dev \ + libsoup-3.0-dev \ + libpango1.0-dev \ + libatk1.0-dev \ + libgtk-3-dev \ + libwebkit2gtk-4.1-dev \ && rm -rf /var/lib/apt/lists/* # Install Rust (latest stable) diff --git a/src/common/src/idprovider/himmelblau.rs b/src/common/src/idprovider/himmelblau.rs index b3acfd5..46653ee 100644 --- a/src/common/src/idprovider/himmelblau.rs +++ b/src/common/src/idprovider/himmelblau.rs @@ -264,6 +264,38 @@ impl IdProvider for HimmelblauMultiProvider { } } + async fn change_auth_token( + &self, + account_id: &str, + token: &UnixUserToken, + new_tok: &str, + keystore: &mut D, + tpm: &mut tpm::BoxedDynTpm, + machine_key: &tpm::MachineKey, + ) -> Result { + match split_username(account_id) { + Some((_sam, domain)) => { + let providers = self.providers.read().await; + match providers.get(domain) { + Some(provider) => { + provider + .change_auth_token( + account_id, + token, + new_tok, + keystore, + tpm, + machine_key, + ) + .await + } + None => Err(IdpError::NotFound), + } + } + None => Err(IdpError::NotFound), + } + } + async fn unix_user_get( &self, id: &Id, @@ -575,6 +607,54 @@ impl IdProvider for HimmelblauProvider { }) } + async fn change_auth_token( + &self, + account_id: &str, + token: &UnixUserToken, + new_tok: &str, + keystore: &mut D, + tpm: &mut tpm::BoxedDynTpm, + machine_key: &tpm::MachineKey, + ) -> Result { + let hello_tag = self.fetch_hello_key_tag(account_id); + + // Ensure the user is setting the token for the account it has authenticated to + if account_id.to_string().to_lowercase() + != token + .spn() + .map_err(|e| { + error!("Failed checking the spn on the user token: {:?}", e); + IdpError::BadRequest + })? + .to_lowercase() + { + error!("A hello key may only be set by the authenticated user!"); + return Err(IdpError::BadRequest); + } + + // Set the hello pin + let hello_key = match self + .client + .write() + .await + .provision_hello_for_business_key(token, tpm, machine_key, new_tok) + .await + { + Ok(hello_key) => hello_key, + Err(e) => { + error!("Failed to provision hello key: {:?}", e); + return Ok(false); + } + }; + keystore + .insert_tagged_hsm_key(&hello_tag, &hello_key) + .map_err(|e| { + error!("Failed to provision hello key: {:?}", e); + IdpError::Tpm + })?; + Ok(true) + } + async fn unix_user_get( &self, id: &Id, diff --git a/src/common/src/idprovider/interface.rs b/src/common/src/idprovider/interface.rs index 7cb56a7..2f7e9b4 100644 --- a/src/common/src/idprovider/interface.rs +++ b/src/common/src/idprovider/interface.rs @@ -196,6 +196,16 @@ pub trait IdProvider { _machine_key: &tpm::MachineKey, ) -> Result; + async fn change_auth_token( + &self, + _account_id: &str, + _token: &UnixUserToken, + _new_tok: &str, + _keystore: &mut D, + _tpm: &mut tpm::BoxedDynTpm, + _machine_key: &tpm::MachineKey, + ) -> Result; + async fn unix_user_online_auth_init( &self, _account_id: &str, diff --git a/src/common/src/resolver.rs b/src/common/src/resolver.rs index cb3309a..a3db179 100644 --- a/src/common/src/resolver.rs +++ b/src/common/src/resolver.rs @@ -706,6 +706,35 @@ where } } + pub async fn change_auth_token( + &self, + account_id: &str, + token: &UnixUserToken, + new_tok: &str, + ) -> Result { + let mut hsm_lock = self.hsm.lock().await; + let mut dbtxn = self.db.write().await; + + let res = self + .client + .change_auth_token( + account_id, + token, + new_tok, + &mut dbtxn, + hsm_lock.deref_mut(), + &self.machine_key, + ) + .await; + + drop(hsm_lock); + dbtxn.commit().map_err(|_| ())?; + + res.map_err(|e| { + debug!("change_auth_token error -> {:?}", e); + }) + } + pub async fn get_usertoken(&self, account_id: Id) -> Result, ()> { debug!("get_usertoken"); // get the item from the cache diff --git a/src/common/src/unix_proto.rs b/src/common/src/unix_proto.rs index 1146b8f..a04ea3e 100644 --- a/src/common/src/unix_proto.rs +++ b/src/common/src/unix_proto.rs @@ -74,6 +74,7 @@ pub enum ClientRequest { PamAuthenticateStep(PamAuthRequest), PamAccountAllowed(String), PamAccountBeginSession(String), + PamChangeAuthToken(String, String, String, String), InvalidateCache, ClearCache, Status, @@ -95,6 +96,9 @@ impl ClientRequest { format!("PamAccountAllowed({})", id) } ClientRequest::PamAccountBeginSession(_) => "PamAccountBeginSession".to_string(), + ClientRequest::PamChangeAuthToken(id, _, _, _) => { + format!("PamChangeAuthToken({}, ...)", id) + } ClientRequest::InvalidateCache => "InvalidateCache".to_string(), ClientRequest::ClearCache => "ClearCache".to_string(), ClientRequest::Status => "Status".to_string(), diff --git a/src/daemon/Cargo.toml b/src/daemon/Cargo.toml index 6671275..ccfc745 100644 --- a/src/daemon/Cargo.toml +++ b/src/daemon/Cargo.toml @@ -42,6 +42,7 @@ kanidm_lib_file_permissions.workspace = true identity_dbus_broker.workspace = true base64.workspace = true async-trait = "0.1.83" +libhimmelblau.workspace = true [package.metadata.deb] name = "himmelblau" diff --git a/src/daemon/src/daemon.rs b/src/daemon/src/daemon.rs index ee026a4..d0f9f4c 100644 --- a/src/daemon/src/daemon.rs +++ b/src/daemon/src/daemon.rs @@ -33,6 +33,7 @@ use std::time::Duration; use bytes::{BufMut, BytesMut}; use clap::{Arg, ArgAction, Command}; use futures::{SinkExt, StreamExt}; +use himmelblau::{ClientInfo, IdToken, UserToken as UnixUserToken}; use himmelblau_unix_common::config::HimmelblauConfig; use himmelblau_unix_common::constants::DEFAULT_CONFIG_PATH; use himmelblau_unix_common::db::{Cache, CacheTxn, Db}; @@ -549,6 +550,25 @@ async fn handle_client( _ => ClientResponse::Error, } } + ClientRequest::PamChangeAuthToken(account_id, access_token, refresh_token, new_pin) => { + debug!("sm_chauthtok req"); + let token = UnixUserToken { + token_type: "Bearer".to_string(), + scope: None, + expires_in: 0, + ext_expires_in: 0, + access_token: Some(access_token), + refresh_token, + id_token: IdToken::default(), + client_info: ClientInfo::default(), + prt: None, + }; + cachelayer + .change_auth_token(&account_id, &token, &new_pin) + .await + .map(|_| ClientResponse::Ok) + .unwrap_or(ClientResponse::Error) + } ClientRequest::InvalidateCache => { debug!("invalidate cache"); cachelayer diff --git a/src/pam/Cargo.toml b/src/pam/Cargo.toml index be05c77..cc1bb70 100644 --- a/src/pam/Cargo.toml +++ b/src/pam/Cargo.toml @@ -15,12 +15,17 @@ repository.workspace = true name = "pam_himmelblau" crate-type = [ "cdylib" ] +[features] +interactive = ["libhimmelblau/interactive"] + [dependencies] libc = { workspace = true } kanidm_unix_common = { workspace = true } tracing-subscriber = { workspace = true } tracing = { workspace = true } himmelblau_unix_common.workspace = true +tokio.workspace = true +libhimmelblau.workspace = true [build-dependencies] pkg-config.workspace = true @@ -34,6 +39,7 @@ assets = [ ["../../platform/debian/apparmor.unix-chkpwd.local", "etc/apparmor.d/local/unix-chkpwd", "644"], ] maintainer-scripts = "../../platform/debian/scripts" +features = ["interactive"] [package.metadata.generate-rpm] name = "pam-himmelblau" diff --git a/src/pam/src/pam/constants.rs b/src/pam/src/pam/constants.rs index c9e62fb..988ddf6 100644 --- a/src/pam/src/pam/constants.rs +++ b/src/pam/src/pam/constants.rs @@ -39,6 +39,9 @@ pub const _PAM_DELETE_CRED: PamFlag = 0x0004; pub const _PAM_REINITIALIZE_CRED: PamFlag = 0x0008; pub const _PAM_REFRESH_CRED: PamFlag = 0x0010; pub const _PAM_CHANGE_EXPIRED_AUTHTOK: PamFlag = 0x0020; +// see /usr/include/security/pam_modules.h +pub const PAM_PRELIM_CHECK: PamFlag = 0x4000; +pub const PAM_UPDATE_AUTHTOK: PamFlag = 0x2000; // The Linux-PAM item types // see /usr/include/security/_pam_types.h diff --git a/src/pam/src/pam/items.rs b/src/pam/src/pam/items.rs index edd8503..4c75980 100644 --- a/src/pam/src/pam/items.rs +++ b/src/pam/src/pam/items.rs @@ -24,8 +24,7 @@ SOFTWARE. */ use crate::pam::constants::{ - PamItemType, PAM_AUTHTOK, PAM_OLDAUTHTOK, PAM_RHOST, PAM_RUSER, PAM_SERVICE, PAM_TTY, PAM_USER, - PAM_USER_PROMPT, + PamItemType, PAM_AUTHTOK, PAM_RHOST, PAM_RUSER, PAM_SERVICE, PAM_TTY, PAM_USER, PAM_USER_PROMPT, }; pub use crate::pam::conv::PamConv; use crate::pam::module::PamItem; @@ -92,12 +91,3 @@ impl PamItem for PamAuthTok { PAM_AUTHTOK } } - -#[allow(dead_code)] -pub struct PamOldAuthTok {} - -impl PamItem for PamOldAuthTok { - fn item_type() -> PamItemType { - PAM_OLDAUTHTOK - } -} diff --git a/src/pam/src/pam/mod.rs b/src/pam/src/pam/mod.rs index c9e377c..dc1b613 100755 --- a/src/pam/src/pam/mod.rs +++ b/src/pam/src/pam/mod.rs @@ -60,13 +60,19 @@ use std::collections::BTreeSet; use std::convert::TryFrom; use std::ffi::CStr; -use himmelblau_unix_common::config::HimmelblauConfig; +use himmelblau::error::MsalError; +use himmelblau::PublicClientApplication; +use himmelblau_unix_common::config::{split_username, HimmelblauConfig}; +use himmelblau_unix_common::constants::BROKER_APP_ID; use kanidm_unix_common::client_sync::DaemonClientBlocking; use kanidm_unix_common::constants::DEFAULT_CONFIG_PATH; use kanidm_unix_common::unix_config::KanidmUnixdConfig; use kanidm_unix_common::unix_proto::{ ClientRequest, ClientResponse, PamAuthRequest, PamAuthResponse, }; +#[cfg(feature = "interactive")] +use std::env; +use std::thread::sleep; use crate::pam::constants::*; use crate::pam::conv::PamConv; @@ -82,6 +88,8 @@ use tracing_subscriber::prelude::*; use std::thread; use std::time::Duration; +use tokio::runtime::Runtime; + pub fn get_cfg() -> Result { HimmelblauConfig::new(Some(DEFAULT_CONFIG_PATH)).map_err(|_| PamResultCode::PAM_SERVICE_ERR) } @@ -524,7 +532,17 @@ impl PamHooks for PamKanidm { } // while true, continue calling PamAuthenticateStep until we get a decision. } - fn sm_chauthtok(_pamh: &PamHandle, args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode { + fn sm_chauthtok(pamh: &PamHandle, args: Vec<&CStr>, flags: PamFlag) -> PamResultCode { + if flags & PAM_PRELIM_CHECK != 0 { + return PamResultCode::PAM_SUCCESS; + } + + if flags & PAM_UPDATE_AUTHTOK == 0 { + // If this isn't a PAM_PRELIM_CHECK, and not a PAM_UPDATE_AUTHTOK, + // what is it? + return PamResultCode::PAM_SERVICE_ERR; + } + let opts = match Options::try_from(&args) { Ok(o) => o, Err(_) => return PamResultCode::PAM_SERVICE_ERR, @@ -534,7 +552,295 @@ impl PamHooks for PamKanidm { debug!(?args, ?opts, "sm_chauthtok"); - PamResultCode::PAM_IGNORE + let account_id = match pamh.get_user(None) { + Ok(aid) => aid, + Err(e) => { + error!(err = ?e, "get_user"); + return PamResultCode::PAM_SERVICE_ERR; + } + }; + + let cfg = match get_cfg() { + Ok(cfg) => cfg, + Err(e) => return e, + }; + let account_id = cfg.map_cn_name(&account_id); + + let mut daemon_client = match DaemonClientBlocking::new(cfg.get_socket_path().as_str()) { + Ok(dc) => dc, + Err(e) => { + error!(err = ?e, "Error DaemonClientBlocking::new()"); + return PamResultCode::PAM_SERVICE_ERR; + } + }; + + let (_, domain) = match split_username(&account_id) { + Some(resp) => resp, + None => { + error!("split_username"); + return PamResultCode::PAM_AUTH_ERR; + } + }; + let tenant_id = match cfg.get_tenant_id(domain) { + Some(tenant_id) => tenant_id, + None => "common".to_string(), + }; + let authority = format!("https://{}/{}", cfg.get_authority_host(domain), tenant_id); + let app = match PublicClientApplication::new(BROKER_APP_ID, Some(&authority)) { + Ok(app) => app, + Err(e) => { + error!(err = ?e, "PublicClientApplication"); + return PamResultCode::PAM_AUTH_ERR; + } + }; + + let conv = match pamh.get_item::() { + Ok(conv) => conv, + Err(err) => { + error!(?err, "pam_conv"); + return err; + } + }; + match conv.send( + PAM_TEXT_INFO, + "This command changes your local Hello PIN, NOT your Entra Id password.", + ) { + Ok(_) => {} + Err(err) => { + if opts.debug { + println!("Message prompt failed"); + } + return err; + } + } + + let mut pin; + loop { + pin = match conv.send(PAM_PROMPT_ECHO_OFF, "New PIN: ") { + Ok(password) => match password { + Some(cred) => { + if cred.len() < cfg.get_hello_pin_min_length() { + match conv.send( + PAM_TEXT_INFO, + &format!( + "Chosen pin is too short! {} chars required.", + cfg.get_hello_pin_min_length() + ), + ) { + Ok(_) => {} + Err(err) => { + if opts.debug { + println!("Message prompt failed"); + } + return err; + } + } + continue; + } + cred + } + None => { + debug!("no pin"); + return PamResultCode::PAM_CRED_INSUFFICIENT; + } + }, + Err(err) => { + debug!("unable to get pin"); + return err; + } + }; + + let confirm = match conv.send(PAM_PROMPT_ECHO_OFF, "Confirm PIN: ") { + Ok(password) => match password { + Some(cred) => cred, + None => { + debug!("no confirmation pin"); + return PamResultCode::PAM_CRED_INSUFFICIENT; + } + }, + Err(err) => { + debug!("unable to get confirmation pin"); + return err; + } + }; + + if pin == confirm { + break; + } else { + match conv.send(PAM_TEXT_INFO, "Inputs did not match. Try again.") { + Ok(_) => {} + Err(err) => { + if opts.debug { + println!("Message prompt failed"); + } + return err; + } + } + } + } + + let rt = match Runtime::new() { + Ok(rt) => rt, + Err(e) => { + error!("{:?}", e); + return PamResultCode::PAM_AUTH_ERR; + } + }; + let interactive; + #[cfg(feature = "interactive")] + { + interactive = !cfg.get_enable_experimental_mfa() + || env::var("INTERACTIVE") + .map(|value| value.to_lowercase() == "true") + .unwrap_or(false); + } + #[cfg(not(feature = "interactive"))] + { + interactive = false; + } + let token = if !interactive { + #[cfg(feature = "interactive")] + match conv.send( + PAM_TEXT_INFO, + "If necessary, you can authenticate via a browser by setting the environment variable INTERACTIVE=true" + ) { + Ok(_) => {} + Err(err) => { + if opts.debug { + println!("Message prompt failed"); + } + return err; + } + } + + let password = match conv.send(PAM_PROMPT_ECHO_OFF, "Entra Id Password: ") { + Ok(password) => match password { + Some(cred) => cred, + None => { + debug!("no password"); + return PamResultCode::PAM_CRED_INSUFFICIENT; + } + }, + Err(err) => { + debug!("unable to get password"); + return err; + } + }; + + let mut mfa_req = match rt.block_on(async { + app.initiate_acquire_token_by_mfa_flow(&account_id, &password, vec![], None, vec![]) + .await + }) { + Ok(mfa) => mfa, + Err(e) => { + error!("{:?}", e); + return PamResultCode::PAM_AUTH_ERR; + } + }; + + match mfa_req.mfa_method.as_str() { + "PhoneAppOTP" | "OneWaySMS" | "ConsolidatedTelephony" => { + let input = match conv.send(PAM_PROMPT_ECHO_OFF, &mfa_req.msg) { + Ok(password) => match password { + Some(cred) => cred, + None => { + debug!("no password"); + return PamResultCode::PAM_CRED_INSUFFICIENT; + } + }, + Err(err) => { + debug!("unable to get password"); + return err; + } + }; + match rt.block_on(async { + app.acquire_token_by_mfa_flow(&account_id, Some(&input), None, &mut mfa_req) + .await + }) { + Ok(token) => token, + Err(e) => { + error!("MFA FAIL: {:?}", e); + return PamResultCode::PAM_AUTH_ERR; + } + } + } + _ => { + match conv.send(PAM_TEXT_INFO, &mfa_req.msg) { + Ok(_) => {} + Err(err) => { + if opts.debug { + println!("Message prompt failed"); + } + return err; + } + } + let mut poll_attempt = 1; + let polling_interval = mfa_req.polling_interval.unwrap_or(5000); + loop { + match rt.block_on(async { + app.acquire_token_by_mfa_flow( + &account_id, + None, + Some(poll_attempt), + &mut mfa_req, + ) + .await + }) { + Ok(token) => break token, + Err(e) => match e { + MsalError::MFAPollContinue => { + poll_attempt += 1; + sleep(Duration::from_millis(polling_interval.into())); + continue; + } + e => { + error!("MFA FAIL: {:?}", e); + return PamResultCode::PAM_AUTH_ERR; + } + }, + } + } + } + } + } else { + #[cfg(feature = "interactive")] + match rt.block_on(async { app.acquire_token_interactive(&account_id, None).await }) { + Ok(token) => token, + Err(e) => { + error!(err = ?e, "acquire_token"); + return PamResultCode::PAM_AUTH_ERR; + } + } + #[cfg(not(feature = "interactive"))] + { + error!("Himmelblau was built without interactive support"); + return PamResultCode::PAM_AUTH_ERR; + } + }; + + let req = ClientRequest::PamChangeAuthToken( + account_id, + match token.access_token.clone() { + Some(access_token) => access_token, + None => { + error!("Failed fetching access token for pin change"); + return PamResultCode::PAM_AUTH_ERR; + } + }, + token.refresh_token.clone(), + pin, + ); + + match daemon_client.call_and_wait(&req, cfg.get_unix_sock_timeout()) { + Ok(ClientResponse::Ok) => { + debug!("PamResultCode::PAM_SUCCESS"); + PamResultCode::PAM_SUCCESS + } + other => { + debug!(err = ?other, "PamResultCode::PAM_AUTH_ERR"); + PamResultCode::PAM_AUTH_ERR + } + } } fn sm_close_session(_pamh: &PamHandle, args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode {