From 8401c3e1c8545b1ff29e8816840e65e78cd85c1b Mon Sep 17 00:00:00 2001 From: David Mulder Date: Tue, 12 Sep 2023 15:33:46 -0600 Subject: [PATCH] Implement DeviceAuthorizationGrant for MFA (#2079) Himmelblau will use the DeviceAuthorizationGrant (defined in RFC8628) to perform MFA. This commit adds the bits to Kanidm to make that possible, using the new pam state machine code. Signed-off-by: David Mulder --- .../nss_kanidm/src/implementation.rs | 90 +++++++++---------- unix_integration/pam_kanidm/src/pam/mod.rs | 77 ++++++++++------ unix_integration/src/client_sync.rs | 36 +++++--- unix_integration/src/idprovider/interface.rs | 7 +- unix_integration/src/idprovider/kanidm.rs | 19 ++-- unix_integration/src/resolver.rs | 12 +++ unix_integration/src/unix_proto.rs | 25 ++++-- 7 files changed, 168 insertions(+), 98 deletions(-) diff --git a/unix_integration/nss_kanidm/src/implementation.rs b/unix_integration/nss_kanidm/src/implementation.rs index b385c05ef..5774a8bf6 100644 --- a/unix_integration/nss_kanidm/src/implementation.rs +++ b/unix_integration/nss_kanidm/src/implementation.rs @@ -20,16 +20,15 @@ impl PasswdHooks for KanidmPasswd { }; let req = ClientRequest::NssAccounts; - let mut daemon_client = - match DaemonClientBlocking::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout) { - Ok(dc) => dc, - Err(_) => { - return Response::Unavail; - } - }; + let mut daemon_client = match DaemonClientBlocking::new(cfg.sock_path.as_str()) { + Ok(dc) => dc, + Err(_) => { + return Response::Unavail; + } + }; daemon_client - .call_and_wait(&req) + .call_and_wait(&req, cfg.unix_sock_timeout) .map(|r| match r { ClientResponse::NssAccounts(l) => l.into_iter().map(passwd_from_nssuser).collect(), _ => Vec::new(), @@ -48,16 +47,15 @@ impl PasswdHooks for KanidmPasswd { }; let req = ClientRequest::NssAccountByUid(uid); - let mut daemon_client = - match DaemonClientBlocking::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout) { - Ok(dc) => dc, - Err(_) => { - return Response::Unavail; - } - }; + let mut daemon_client = match DaemonClientBlocking::new(cfg.sock_path.as_str()) { + Ok(dc) => dc, + Err(_) => { + return Response::Unavail; + } + }; daemon_client - .call_and_wait(&req) + .call_and_wait(&req, cfg.unix_sock_timeout) .map(|r| match r { ClientResponse::NssAccount(opt) => opt .map(passwd_from_nssuser) @@ -77,16 +75,15 @@ impl PasswdHooks for KanidmPasswd { } }; let req = ClientRequest::NssAccountByName(name); - let mut daemon_client = - match DaemonClientBlocking::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout) { - Ok(dc) => dc, - Err(_) => { - return Response::Unavail; - } - }; + let mut daemon_client = match DaemonClientBlocking::new(cfg.sock_path.as_str()) { + Ok(dc) => dc, + Err(_) => { + return Response::Unavail; + } + }; daemon_client - .call_and_wait(&req) + .call_and_wait(&req, cfg.unix_sock_timeout) .map(|r| match r { ClientResponse::NssAccount(opt) => opt .map(passwd_from_nssuser) @@ -111,16 +108,15 @@ impl GroupHooks for KanidmGroup { } }; let req = ClientRequest::NssGroups; - let mut daemon_client = - match DaemonClientBlocking::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout) { - Ok(dc) => dc, - Err(_) => { - return Response::Unavail; - } - }; + let mut daemon_client = match DaemonClientBlocking::new(cfg.sock_path.as_str()) { + Ok(dc) => dc, + Err(_) => { + return Response::Unavail; + } + }; daemon_client - .call_and_wait(&req) + .call_and_wait(&req, cfg.unix_sock_timeout) .map(|r| match r { ClientResponse::NssGroups(l) => l.into_iter().map(group_from_nssgroup).collect(), _ => Vec::new(), @@ -138,16 +134,15 @@ impl GroupHooks for KanidmGroup { } }; let req = ClientRequest::NssGroupByGid(gid); - let mut daemon_client = - match DaemonClientBlocking::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout) { - Ok(dc) => dc, - Err(_) => { - return Response::Unavail; - } - }; + let mut daemon_client = match DaemonClientBlocking::new(cfg.sock_path.as_str()) { + Ok(dc) => dc, + Err(_) => { + return Response::Unavail; + } + }; daemon_client - .call_and_wait(&req) + .call_and_wait(&req, cfg.unix_sock_timeout) .map(|r| match r { ClientResponse::NssGroup(opt) => opt .map(group_from_nssgroup) @@ -167,16 +162,15 @@ impl GroupHooks for KanidmGroup { } }; let req = ClientRequest::NssGroupByName(name); - let mut daemon_client = - match DaemonClientBlocking::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout) { - Ok(dc) => dc, - Err(_) => { - return Response::Unavail; - } - }; + let mut daemon_client = match DaemonClientBlocking::new(cfg.sock_path.as_str()) { + Ok(dc) => dc, + Err(_) => { + return Response::Unavail; + } + }; daemon_client - .call_and_wait(&req) + .call_and_wait(&req, cfg.unix_sock_timeout) .map(|r| match r { ClientResponse::NssGroup(opt) => opt .map(group_from_nssgroup) diff --git a/unix_integration/pam_kanidm/src/pam/mod.rs b/unix_integration/pam_kanidm/src/pam/mod.rs index 8c72097bf..1a186e0b3 100755 --- a/unix_integration/pam_kanidm/src/pam/mod.rs +++ b/unix_integration/pam_kanidm/src/pam/mod.rs @@ -135,16 +135,15 @@ impl PamHooks for PamKanidm { let req = ClientRequest::PamAccountAllowed(account_id); // PamResultCode::PAM_IGNORE - let mut daemon_client = - match DaemonClientBlocking::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout) { - Ok(dc) => dc, - Err(e) => { - error!(err = ?e, "Error DaemonClientBlocking::new()"); - return PamResultCode::PAM_SERVICE_ERR; - } - }; + let mut daemon_client = match DaemonClientBlocking::new(cfg.sock_path.as_str()) { + Ok(dc) => dc, + Err(e) => { + error!(err = ?e, "Error DaemonClientBlocking::new()"); + return PamResultCode::PAM_SERVICE_ERR; + } + }; - match daemon_client.call_and_wait(&req) { + match daemon_client.call_and_wait(&req, cfg.unix_sock_timeout) { Ok(r) => match r { ClientResponse::PamStatus(Some(true)) => { debug!("PamResultCode::PAM_SUCCESS"); @@ -203,14 +202,14 @@ impl PamHooks for PamKanidm { Err(e) => return e, }; - let mut daemon_client = - match DaemonClientBlocking::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout) { - Ok(dc) => dc, - Err(e) => { - error!(err = ?e, "Error DaemonClientBlocking::new()"); - return PamResultCode::PAM_SERVICE_ERR; - } - }; + let mut timeout = cfg.unix_sock_timeout; + let mut daemon_client = match DaemonClientBlocking::new(cfg.sock_path.as_str()) { + Ok(dc) => dc, + Err(e) => { + error!(err = ?e, "Error DaemonClientBlocking::new()"); + return PamResultCode::PAM_SERVICE_ERR; + } + }; // Later we may need to move this to a function and call it as a oneshot for auth methods // that don't require any authtoks at all. For example, imagine a user authed and they @@ -242,7 +241,7 @@ impl PamHooks for PamKanidm { let mut req = ClientRequest::PamAuthenticateInit(account_id); loop { - match daemon_client.call_and_wait(&req) { + match daemon_client.call_and_wait(&req, timeout) { Ok(r) => match r { ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Success) => { return PamResultCode::PAM_SUCCESS; @@ -282,9 +281,34 @@ impl PamHooks for PamKanidm { }; // Now setup the request for the next loop. + timeout = cfg.unix_sock_timeout; req = ClientRequest::PamAuthenticateStep(PamAuthRequest::Password { cred }); continue; } + ClientResponse::PamAuthenticateStepResponse( + PamAuthResponse::DeviceAuthorizationGrant { data }, + ) => { + let msg = match &data.message { + Some(msg) => msg.clone(), + None => format!("Using a browser on another device, visit:\n{}\nAnd enter the code:\n{}", + data.verification_uri, data.user_code) + }; + match conv.send(PAM_TEXT_INFO, &msg) { + Ok(_) => {} + Err(err) => { + if opts.debug { + println!("Message prompt failed"); + } + return err; + } + } + + timeout = u64::from(data.expires_in); + req = ClientRequest::PamAuthenticateStep( + PamAuthRequest::DeviceAuthorizationGrant { data }, + ); + continue; + } _ => { // unexpected response. error!(err = ?r, "PAM_IGNORE, unexpected resolver response"); @@ -349,16 +373,15 @@ impl PamHooks for PamKanidm { }; let req = ClientRequest::PamAccountBeginSession(account_id); - let mut daemon_client = - match DaemonClientBlocking::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout) { - Ok(dc) => dc, - Err(e) => { - error!(err = ?e, "Error DaemonClientBlocking::new()"); - return PamResultCode::PAM_SERVICE_ERR; - } - }; + let mut daemon_client = match DaemonClientBlocking::new(cfg.sock_path.as_str()) { + Ok(dc) => dc, + Err(e) => { + error!(err = ?e, "Error DaemonClientBlocking::new()"); + return PamResultCode::PAM_SERVICE_ERR; + } + }; - match daemon_client.call_and_wait(&req) { + match daemon_client.call_and_wait(&req, cfg.unix_sock_timeout) { Ok(ClientResponse::Ok) => { // println!("PAM_SUCCESS"); PamResultCode::PAM_SUCCESS diff --git a/unix_integration/src/client_sync.rs b/unix_integration/src/client_sync.rs index fb0c38be4..99805f845 100644 --- a/unix_integration/src/client_sync.rs +++ b/unix_integration/src/client_sync.rs @@ -6,34 +6,50 @@ use std::time::{Duration, SystemTime}; use crate::unix_proto::{ClientRequest, ClientResponse}; pub struct DaemonClientBlocking { - timeout: Duration, stream: UnixStream, } impl DaemonClientBlocking { - pub fn new(path: &str, timeout: u64) -> Result> { - let timeout = Duration::from_secs(timeout); - - debug!(%path, ?timeout); + pub fn new(path: &str) -> Result> { + debug!(%path); let stream = UnixStream::connect(path) - .and_then(|socket| socket.set_read_timeout(Some(timeout)).map(|_| socket)) - .and_then(|socket| socket.set_write_timeout(Some(timeout)).map(|_| socket)) .map_err(|e| { error!("stream setup error -> {:?}", e); e }) .map_err(Box::new)?; - Ok(DaemonClientBlocking { timeout, stream }) + Ok(DaemonClientBlocking { stream }) } - pub fn call_and_wait(&mut self, req: &ClientRequest) -> Result> { + pub fn call_and_wait( + &mut self, + req: &ClientRequest, + timeout: u64, + ) -> Result> { + let timeout = Duration::from_secs(timeout); + let data = serde_json::to_vec(&req).map_err(|e| { error!("socket encoding error -> {:?}", e); Box::new(IoError::new(ErrorKind::Other, "JSON encode error")) })?; + match self.stream.set_read_timeout(Some(timeout)) { + Ok(()) => {} + Err(e) => { + error!("stream setup error -> {:?}", e); + return Err(Box::new(e)); + } + }; + match self.stream.set_write_timeout(Some(timeout)) { + Ok(()) => {} + Err(e) => { + error!("stream setup error -> {:?}", e); + return Err(Box::new(e)); + } + }; + self.stream .write_all(data.as_slice()) .and_then(|_| self.stream.flush()) @@ -52,7 +68,7 @@ impl DaemonClientBlocking { loop { let mut buffer = [0; 1024]; let durr = SystemTime::now().duration_since(start).map_err(Box::new)?; - if durr > self.timeout { + if durr > timeout { error!("Socket timeout"); // timed out, not enough activity. break; diff --git a/unix_integration/src/idprovider/interface.rs b/unix_integration/src/idprovider/interface.rs index 732056ca5..eac32b069 100644 --- a/unix_integration/src/idprovider/interface.rs +++ b/unix_integration/src/idprovider/interface.rs @@ -1,4 +1,4 @@ -use crate::unix_proto::{PamAuthRequest, PamAuthResponse}; +use crate::unix_proto::{DeviceAuthorizationResponse, PamAuthRequest, PamAuthResponse}; use async_trait::async_trait; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -55,10 +55,12 @@ pub struct UserToken { #[derive(Debug)] pub enum AuthCredHandler { Password, + DeviceAuthorizationGrant, } pub enum AuthRequest { Password, + DeviceAuthorizationGrant { data: DeviceAuthorizationResponse }, } #[allow(clippy::from_over_into)] @@ -66,6 +68,9 @@ impl Into for AuthRequest { fn into(self) -> PamAuthResponse { match self { AuthRequest::Password => PamAuthResponse::Password, + AuthRequest::DeviceAuthorizationGrant { data } => { + PamAuthResponse::DeviceAuthorizationGrant { data } + } } } } diff --git a/unix_integration/src/idprovider/kanidm.rs b/unix_integration/src/idprovider/kanidm.rs index 0e76c5b63..d1b02de0f 100644 --- a/unix_integration/src/idprovider/kanidm.rs +++ b/unix_integration/src/idprovider/kanidm.rs @@ -223,13 +223,18 @@ impl IdProvider for KanidmProvider { Err(IdpError::BadRequest) } } - } // For future when we have different auth combos/types. - /* - _ => { - error!("invalid authentication request state"); - Err(IdpError::BadRequest) - } - */ + } + ( + AuthCredHandler::DeviceAuthorizationGrant, + PamAuthRequest::DeviceAuthorizationGrant { .. }, + ) => { + error!("DeviceAuthorizationGrant not implemented!"); + Err(IdpError::BadRequest) + } + _ => { + error!("invalid authentication request state"); + Err(IdpError::BadRequest) + } } } diff --git a/unix_integration/src/resolver.rs b/unix_integration/src/resolver.rs index cae64740e..9668a53ac 100644 --- a/unix_integration/src/resolver.rs +++ b/unix_integration/src/resolver.rs @@ -1047,6 +1047,14 @@ where } } } + (AuthCredHandler::Password, _) => { + // AuthCredHandler::Password is only valid with a cred provided + return Err(()); + } + (AuthCredHandler::DeviceAuthorizationGrant, _) => { + // AuthCredHandler::DeviceAuthorizationGrant is invalid for offline auth + return Err(()); + } } /* @@ -1116,6 +1124,10 @@ where // Can continue! auth_session } + (auth_session, PamAuthResponse::DeviceAuthorizationGrant { .. }) => { + // Can continue! + auth_session + } (_, PamAuthResponse::Unknown) => return Ok(None), (_, PamAuthResponse::Denied) => return Ok(Some(false)), (_, PamAuthResponse::Success) => { diff --git a/unix_integration/src/unix_proto.rs b/unix_integration/src/unix_proto.rs index b28e7b524..e175c53c2 100644 --- a/unix_integration/src/unix_proto.rs +++ b/unix_integration/src/unix_proto.rs @@ -16,12 +16,27 @@ pub struct NssGroup { pub members: Vec, } +/* RFC8628: 3.2. Device Authorization Response */ +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct DeviceAuthorizationResponse { + pub device_code: String, + pub user_code: String, + pub verification_uri: String, + pub verification_uri_complete: Option, + pub expires_in: u32, + pub interval: Option, + /* The message is not part of RFC8628, but an add-on from MS. Listed + * optional here to support all implementations. */ + pub message: Option, +} + #[derive(Serialize, Deserialize, Debug)] pub enum PamAuthResponse { Unknown, Success, Denied, Password, + DeviceAuthorizationGrant { data: DeviceAuthorizationResponse }, /* MFACode { }, @@ -32,11 +47,11 @@ pub enum PamAuthResponse { #[derive(Serialize, Deserialize, Debug)] pub enum PamAuthRequest { Password { cred: String }, - /* - MFACode { - cred: Option - } - */ + DeviceAuthorizationGrant { data: DeviceAuthorizationResponse }, /* + MFACode { + cred: Option + } + */ } #[derive(Serialize, Deserialize, Debug)]