Skip to content

Commit

Permalink
Merge pull request #332 from himmelblau-idm/dmulder/passwordless
Browse files Browse the repository at this point in the history
Implement NGC Passwordless authentication
  • Loading branch information
dmulder authored Dec 19, 2024
2 parents 3b19d0b + b2972df commit 9353fa2
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 45 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.6" }
libhimmelblau = { version = "0.5.0" }
clap = { version = "^4.5", features = ["derive", "env"] }
clap_complete = "^4.4.1"
reqwest = { version = "^0.12.2", features = ["json"] }
Expand Down
68 changes: 51 additions & 17 deletions src/common/src/idprovider/himmelblau.rs
Original file line number Diff line number Diff line change
Expand Up @@ -698,17 +698,17 @@ impl IdProvider for HimmelblauProvider {
// Otherwise, see if we should fake it
None => {
// Check if the user exists
let exists = self
let auth_init = self
.client
.write()
.await
.check_user_exists(&account_id)
.check_user_exists(&account_id, &[])
.await
.map_err(|e| {
error!("Failed checking if the user exists: {:?}", e);
IdpError::BadRequest
})?;
if exists {
if auth_init.exists() {
// Generate a UserToken, with invalid uuid. We can
// only fetch this from an authenticated token.
let config = self.config.read().await;
Expand Down Expand Up @@ -819,7 +819,47 @@ impl IdProvider for HimmelblauProvider {
let hello_enabled = self.config.read().await.get_enable_hello();
if !self.is_domain_joined(keystore).await || hello_key.is_none() || !hello_enabled {
if self.config.read().await.get_enable_experimental_mfa() {
Ok((AuthRequest::Password, AuthCredHandler::None))
let auth_options = vec![AuthOption::Fido, AuthOption::Passwordless];
let auth_init = self
.client
.write()
.await
.check_user_exists(account_id, &auth_options)
.await
.map_err(|e| {
error!("{:?}", e);
IdpError::BadRequest
})?;
if !auth_init.passwordless() {
Ok((AuthRequest::Password, AuthCredHandler::None))
} else {
let flow = self
.client
.write()
.await
.initiate_acquire_token_by_mfa_flow_for_device_enrollment(
account_id,
None,
&auth_options,
Some(auth_init),
)
.await
.map_err(|e| {
error!("{:?}", e);
IdpError::BadRequest
})?;
let msg = flow.msg.clone();
let polling_interval = flow.polling_interval.unwrap_or(5000);
Ok((
AuthRequest::MFAPoll {
msg,
// Kanidm pam expects a polling_interval in
// seconds, not milliseconds.
polling_interval: polling_interval / 1000,
},
AuthCredHandler::MFA { flow },
))
}
} else {
let resp = self
.client
Expand All @@ -836,10 +876,7 @@ impl IdProvider for HimmelblauProvider {
flow.resource = Some("https://enrollment.manage.microsoft.com".to_string());
}
let msg = flow.msg.clone();
let polling_interval = flow.polling_interval.ok_or_else(|| {
error!("Invalid response from the server");
IdpError::BadRequest
})?;
let polling_interval = flow.polling_interval.unwrap_or(5000);
Ok((
AuthRequest::MFAPoll {
msg,
Expand Down Expand Up @@ -1034,7 +1071,10 @@ impl IdProvider for HimmelblauProvider {
.write()
.await
.initiate_acquire_token_by_mfa_flow_for_device_enrollment(
account_id, &cred, opts,
account_id,
Some(&cred),
&opts,
None,
)
.await;
// We need to wait to handle the response until after we've released
Expand Down Expand Up @@ -1124,10 +1164,7 @@ impl IdProvider for HimmelblauProvider {
}
_ => {
let msg = resp.msg.clone();
let polling_interval = resp.polling_interval.ok_or_else(|| {
error!("Invalid response from the server");
IdpError::BadRequest
})?;
let polling_interval = resp.polling_interval.unwrap_or(5000);
*cred_handler = AuthCredHandler::MFA { flow: resp };
return Ok((
AuthResult::Next(AuthRequest::MFAPoll {
Expand Down Expand Up @@ -1192,10 +1229,7 @@ impl IdProvider for HimmelblauProvider {
}
}
(AuthCredHandler::MFA { ref mut flow }, PamAuthRequest::MFAPoll { poll_attempt }) => {
let max_poll_attempts = flow.max_poll_attempts.ok_or_else(|| {
error!("Invalid response from the server");
IdpError::BadRequest
})?;
let max_poll_attempts = flow.max_poll_attempts.unwrap_or(180);
if poll_attempt > max_poll_attempts {
error!("MFA polling timed out");
return Err(IdpError::BadRequest);
Expand Down
83 changes: 56 additions & 27 deletions src/pam/src/pam/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ use std::convert::TryFrom;
use std::ffi::CStr;

use himmelblau::error::MsalError;
use himmelblau::PublicClientApplication;
use himmelblau::{AuthOption, PublicClientApplication};
use himmelblau_unix_common::config::{split_username, HimmelblauConfig};
use himmelblau_unix_common::constants::BROKER_APP_ID;
use kanidm_unix_common::client_sync::DaemonClientBlocking;
Expand Down Expand Up @@ -735,23 +735,29 @@ impl PamHooks for PamKanidm {
msg,
polling_interval,
}) => {
let lconv = conv.lock().unwrap();
match lconv.send(PAM_TEXT_INFO, &msg) {
Ok(_) => {}
Err(err) => {
if opts.debug {
println!("Message prompt failed");
// This conversation is intentionally nested within a block
// to ensure the lconv lock is dropped before calling the
// nested `match_sm_auth_client_response`, otherwise we
// deadlock here.
{
let lconv = conv.lock().unwrap();
match lconv.send(PAM_TEXT_INFO, &msg) {
Ok(_) => {}
Err(err) => {
if opts.debug {
println!("Message prompt failed");
}
return err;
}
return err;
}
}

// Necessary because of OpenSSH bug
// https://bugzilla.mindrot.org/show_bug.cgi?id=2876 -
// PAM_TEXT_INFO and PAM_ERROR_MSG conversation not
// honoured during PAM authentication
if opts.mfa_poll_prompt {
let _ = lconv.send(PAM_PROMPT_ECHO_OFF, "Press enter to continue");
// Necessary because of OpenSSH bug
// https://bugzilla.mindrot.org/show_bug.cgi?id=2876 -
// PAM_TEXT_INFO and PAM_ERROR_MSG conversation not
// honoured during PAM authentication
if opts.mfa_poll_prompt {
let _ = lconv.send(PAM_PROMPT_ECHO_OFF, "Press enter to continue");
}
}

let mut poll_attempt = 0;
Expand Down Expand Up @@ -963,23 +969,46 @@ impl PamHooks for PamKanidm {
}
}

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;
let auth_options = vec![AuthOption::Fido, AuthOption::Passwordless];
let auth_init = match rt.block_on(async {
app.check_user_exists(&account_id, None, &auth_options)
.await
}) {
Ok(auth_init) => auth_init,
Err(e) => {
error!("{:?}", e);
return PamResultCode::PAM_AUTH_ERR;
}
};

let password = if !auth_init.passwordless() {
match conv.send(PAM_PROMPT_ECHO_OFF, "Entra Id Password: ") {
Ok(password) => match password {
Some(cred) => Some(cred),
None => {
debug!("no password");
return PamResultCode::PAM_CRED_INSUFFICIENT;
}
},
Err(err) => {
debug!("unable to get password");
return err;
}
},
Err(err) => {
debug!("unable to get password");
return err;
}
} else {
None
};

let mut mfa_req = match rt.block_on(async {
app.initiate_acquire_token_by_mfa_flow(&account_id, &password, vec![], None, vec![])
.await
app.initiate_acquire_token_by_mfa_flow(
&account_id,
password.as_deref(),
vec![],
None,
&auth_options,
Some(auth_init),
)
.await
}) {
Ok(mfa) => mfa,
Err(e) => {
Expand Down

0 comments on commit 9353fa2

Please sign in to comment.