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 <dmulder@samba.org>
This commit is contained in:
David Mulder 2023-09-12 15:33:46 -06:00 committed by GitHub
parent 383592d921
commit 8401c3e1c8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 168 additions and 98 deletions

View file

@ -20,16 +20,15 @@ impl PasswdHooks for KanidmPasswd {
}; };
let req = ClientRequest::NssAccounts; let req = ClientRequest::NssAccounts;
let mut daemon_client = let mut daemon_client = match DaemonClientBlocking::new(cfg.sock_path.as_str()) {
match DaemonClientBlocking::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout) { Ok(dc) => dc,
Ok(dc) => dc, Err(_) => {
Err(_) => { return Response::Unavail;
return Response::Unavail; }
} };
};
daemon_client daemon_client
.call_and_wait(&req) .call_and_wait(&req, cfg.unix_sock_timeout)
.map(|r| match r { .map(|r| match r {
ClientResponse::NssAccounts(l) => l.into_iter().map(passwd_from_nssuser).collect(), ClientResponse::NssAccounts(l) => l.into_iter().map(passwd_from_nssuser).collect(),
_ => Vec::new(), _ => Vec::new(),
@ -48,16 +47,15 @@ impl PasswdHooks for KanidmPasswd {
}; };
let req = ClientRequest::NssAccountByUid(uid); let req = ClientRequest::NssAccountByUid(uid);
let mut daemon_client = let mut daemon_client = match DaemonClientBlocking::new(cfg.sock_path.as_str()) {
match DaemonClientBlocking::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout) { Ok(dc) => dc,
Ok(dc) => dc, Err(_) => {
Err(_) => { return Response::Unavail;
return Response::Unavail; }
} };
};
daemon_client daemon_client
.call_and_wait(&req) .call_and_wait(&req, cfg.unix_sock_timeout)
.map(|r| match r { .map(|r| match r {
ClientResponse::NssAccount(opt) => opt ClientResponse::NssAccount(opt) => opt
.map(passwd_from_nssuser) .map(passwd_from_nssuser)
@ -77,16 +75,15 @@ impl PasswdHooks for KanidmPasswd {
} }
}; };
let req = ClientRequest::NssAccountByName(name); let req = ClientRequest::NssAccountByName(name);
let mut daemon_client = let mut daemon_client = match DaemonClientBlocking::new(cfg.sock_path.as_str()) {
match DaemonClientBlocking::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout) { Ok(dc) => dc,
Ok(dc) => dc, Err(_) => {
Err(_) => { return Response::Unavail;
return Response::Unavail; }
} };
};
daemon_client daemon_client
.call_and_wait(&req) .call_and_wait(&req, cfg.unix_sock_timeout)
.map(|r| match r { .map(|r| match r {
ClientResponse::NssAccount(opt) => opt ClientResponse::NssAccount(opt) => opt
.map(passwd_from_nssuser) .map(passwd_from_nssuser)
@ -111,16 +108,15 @@ impl GroupHooks for KanidmGroup {
} }
}; };
let req = ClientRequest::NssGroups; let req = ClientRequest::NssGroups;
let mut daemon_client = let mut daemon_client = match DaemonClientBlocking::new(cfg.sock_path.as_str()) {
match DaemonClientBlocking::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout) { Ok(dc) => dc,
Ok(dc) => dc, Err(_) => {
Err(_) => { return Response::Unavail;
return Response::Unavail; }
} };
};
daemon_client daemon_client
.call_and_wait(&req) .call_and_wait(&req, cfg.unix_sock_timeout)
.map(|r| match r { .map(|r| match r {
ClientResponse::NssGroups(l) => l.into_iter().map(group_from_nssgroup).collect(), ClientResponse::NssGroups(l) => l.into_iter().map(group_from_nssgroup).collect(),
_ => Vec::new(), _ => Vec::new(),
@ -138,16 +134,15 @@ impl GroupHooks for KanidmGroup {
} }
}; };
let req = ClientRequest::NssGroupByGid(gid); let req = ClientRequest::NssGroupByGid(gid);
let mut daemon_client = let mut daemon_client = match DaemonClientBlocking::new(cfg.sock_path.as_str()) {
match DaemonClientBlocking::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout) { Ok(dc) => dc,
Ok(dc) => dc, Err(_) => {
Err(_) => { return Response::Unavail;
return Response::Unavail; }
} };
};
daemon_client daemon_client
.call_and_wait(&req) .call_and_wait(&req, cfg.unix_sock_timeout)
.map(|r| match r { .map(|r| match r {
ClientResponse::NssGroup(opt) => opt ClientResponse::NssGroup(opt) => opt
.map(group_from_nssgroup) .map(group_from_nssgroup)
@ -167,16 +162,15 @@ impl GroupHooks for KanidmGroup {
} }
}; };
let req = ClientRequest::NssGroupByName(name); let req = ClientRequest::NssGroupByName(name);
let mut daemon_client = let mut daemon_client = match DaemonClientBlocking::new(cfg.sock_path.as_str()) {
match DaemonClientBlocking::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout) { Ok(dc) => dc,
Ok(dc) => dc, Err(_) => {
Err(_) => { return Response::Unavail;
return Response::Unavail; }
} };
};
daemon_client daemon_client
.call_and_wait(&req) .call_and_wait(&req, cfg.unix_sock_timeout)
.map(|r| match r { .map(|r| match r {
ClientResponse::NssGroup(opt) => opt ClientResponse::NssGroup(opt) => opt
.map(group_from_nssgroup) .map(group_from_nssgroup)

View file

@ -135,16 +135,15 @@ impl PamHooks for PamKanidm {
let req = ClientRequest::PamAccountAllowed(account_id); let req = ClientRequest::PamAccountAllowed(account_id);
// PamResultCode::PAM_IGNORE // PamResultCode::PAM_IGNORE
let mut daemon_client = let mut daemon_client = match DaemonClientBlocking::new(cfg.sock_path.as_str()) {
match DaemonClientBlocking::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout) { Ok(dc) => dc,
Ok(dc) => dc, Err(e) => {
Err(e) => { error!(err = ?e, "Error DaemonClientBlocking::new()");
error!(err = ?e, "Error DaemonClientBlocking::new()"); return PamResultCode::PAM_SERVICE_ERR;
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 { Ok(r) => match r {
ClientResponse::PamStatus(Some(true)) => { ClientResponse::PamStatus(Some(true)) => {
debug!("PamResultCode::PAM_SUCCESS"); debug!("PamResultCode::PAM_SUCCESS");
@ -203,14 +202,14 @@ impl PamHooks for PamKanidm {
Err(e) => return e, Err(e) => return e,
}; };
let mut daemon_client = let mut timeout = cfg.unix_sock_timeout;
match DaemonClientBlocking::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout) { let mut daemon_client = match DaemonClientBlocking::new(cfg.sock_path.as_str()) {
Ok(dc) => dc, Ok(dc) => dc,
Err(e) => { Err(e) => {
error!(err = ?e, "Error DaemonClientBlocking::new()"); error!(err = ?e, "Error DaemonClientBlocking::new()");
return PamResultCode::PAM_SERVICE_ERR; return PamResultCode::PAM_SERVICE_ERR;
} }
}; };
// Later we may need to move this to a function and call it as a oneshot for auth methods // 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 // 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); let mut req = ClientRequest::PamAuthenticateInit(account_id);
loop { loop {
match daemon_client.call_and_wait(&req) { match daemon_client.call_and_wait(&req, timeout) {
Ok(r) => match r { Ok(r) => match r {
ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Success) => { ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Success) => {
return PamResultCode::PAM_SUCCESS; return PamResultCode::PAM_SUCCESS;
@ -282,9 +281,34 @@ impl PamHooks for PamKanidm {
}; };
// Now setup the request for the next loop. // Now setup the request for the next loop.
timeout = cfg.unix_sock_timeout;
req = ClientRequest::PamAuthenticateStep(PamAuthRequest::Password { cred }); req = ClientRequest::PamAuthenticateStep(PamAuthRequest::Password { cred });
continue; 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. // unexpected response.
error!(err = ?r, "PAM_IGNORE, unexpected resolver response"); error!(err = ?r, "PAM_IGNORE, unexpected resolver response");
@ -349,16 +373,15 @@ impl PamHooks for PamKanidm {
}; };
let req = ClientRequest::PamAccountBeginSession(account_id); let req = ClientRequest::PamAccountBeginSession(account_id);
let mut daemon_client = let mut daemon_client = match DaemonClientBlocking::new(cfg.sock_path.as_str()) {
match DaemonClientBlocking::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout) { Ok(dc) => dc,
Ok(dc) => dc, Err(e) => {
Err(e) => { error!(err = ?e, "Error DaemonClientBlocking::new()");
error!(err = ?e, "Error DaemonClientBlocking::new()"); return PamResultCode::PAM_SERVICE_ERR;
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) => { Ok(ClientResponse::Ok) => {
// println!("PAM_SUCCESS"); // println!("PAM_SUCCESS");
PamResultCode::PAM_SUCCESS PamResultCode::PAM_SUCCESS

View file

@ -6,34 +6,50 @@ use std::time::{Duration, SystemTime};
use crate::unix_proto::{ClientRequest, ClientResponse}; use crate::unix_proto::{ClientRequest, ClientResponse};
pub struct DaemonClientBlocking { pub struct DaemonClientBlocking {
timeout: Duration,
stream: UnixStream, stream: UnixStream,
} }
impl DaemonClientBlocking { impl DaemonClientBlocking {
pub fn new(path: &str, timeout: u64) -> Result<DaemonClientBlocking, Box<dyn Error>> { pub fn new(path: &str) -> Result<DaemonClientBlocking, Box<dyn Error>> {
let timeout = Duration::from_secs(timeout); debug!(%path);
debug!(%path, ?timeout);
let stream = UnixStream::connect(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| { .map_err(|e| {
error!("stream setup error -> {:?}", e); error!("stream setup error -> {:?}", e);
e e
}) })
.map_err(Box::new)?; .map_err(Box::new)?;
Ok(DaemonClientBlocking { timeout, stream }) Ok(DaemonClientBlocking { stream })
} }
pub fn call_and_wait(&mut self, req: &ClientRequest) -> Result<ClientResponse, Box<dyn Error>> { pub fn call_and_wait(
&mut self,
req: &ClientRequest,
timeout: u64,
) -> Result<ClientResponse, Box<dyn Error>> {
let timeout = Duration::from_secs(timeout);
let data = serde_json::to_vec(&req).map_err(|e| { let data = serde_json::to_vec(&req).map_err(|e| {
error!("socket encoding error -> {:?}", e); error!("socket encoding error -> {:?}", e);
Box::new(IoError::new(ErrorKind::Other, "JSON encode error")) 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 self.stream
.write_all(data.as_slice()) .write_all(data.as_slice())
.and_then(|_| self.stream.flush()) .and_then(|_| self.stream.flush())
@ -52,7 +68,7 @@ impl DaemonClientBlocking {
loop { loop {
let mut buffer = [0; 1024]; let mut buffer = [0; 1024];
let durr = SystemTime::now().duration_since(start).map_err(Box::new)?; let durr = SystemTime::now().duration_since(start).map_err(Box::new)?;
if durr > self.timeout { if durr > timeout {
error!("Socket timeout"); error!("Socket timeout");
// timed out, not enough activity. // timed out, not enough activity.
break; break;

View file

@ -1,4 +1,4 @@
use crate::unix_proto::{PamAuthRequest, PamAuthResponse}; use crate::unix_proto::{DeviceAuthorizationResponse, PamAuthRequest, PamAuthResponse};
use async_trait::async_trait; use async_trait::async_trait;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
@ -55,10 +55,12 @@ pub struct UserToken {
#[derive(Debug)] #[derive(Debug)]
pub enum AuthCredHandler { pub enum AuthCredHandler {
Password, Password,
DeviceAuthorizationGrant,
} }
pub enum AuthRequest { pub enum AuthRequest {
Password, Password,
DeviceAuthorizationGrant { data: DeviceAuthorizationResponse },
} }
#[allow(clippy::from_over_into)] #[allow(clippy::from_over_into)]
@ -66,6 +68,9 @@ impl Into<PamAuthResponse> for AuthRequest {
fn into(self) -> PamAuthResponse { fn into(self) -> PamAuthResponse {
match self { match self {
AuthRequest::Password => PamAuthResponse::Password, AuthRequest::Password => PamAuthResponse::Password,
AuthRequest::DeviceAuthorizationGrant { data } => {
PamAuthResponse::DeviceAuthorizationGrant { data }
}
} }
} }
} }

View file

@ -223,13 +223,18 @@ impl IdProvider for KanidmProvider {
Err(IdpError::BadRequest) Err(IdpError::BadRequest)
} }
} }
} // For future when we have different auth combos/types. }
/* (
_ => { AuthCredHandler::DeviceAuthorizationGrant,
error!("invalid authentication request state"); PamAuthRequest::DeviceAuthorizationGrant { .. },
Err(IdpError::BadRequest) ) => {
} error!("DeviceAuthorizationGrant not implemented!");
*/ Err(IdpError::BadRequest)
}
_ => {
error!("invalid authentication request state");
Err(IdpError::BadRequest)
}
} }
} }

View file

@ -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! // Can continue!
auth_session auth_session
} }
(auth_session, PamAuthResponse::DeviceAuthorizationGrant { .. }) => {
// Can continue!
auth_session
}
(_, PamAuthResponse::Unknown) => return Ok(None), (_, PamAuthResponse::Unknown) => return Ok(None),
(_, PamAuthResponse::Denied) => return Ok(Some(false)), (_, PamAuthResponse::Denied) => return Ok(Some(false)),
(_, PamAuthResponse::Success) => { (_, PamAuthResponse::Success) => {

View file

@ -16,12 +16,27 @@ pub struct NssGroup {
pub members: Vec<String>, pub members: Vec<String>,
} }
/* 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<String>,
pub expires_in: u32,
pub interval: Option<u32>,
/* The message is not part of RFC8628, but an add-on from MS. Listed
* optional here to support all implementations. */
pub message: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub enum PamAuthResponse { pub enum PamAuthResponse {
Unknown, Unknown,
Success, Success,
Denied, Denied,
Password, Password,
DeviceAuthorizationGrant { data: DeviceAuthorizationResponse },
/* /*
MFACode { MFACode {
}, },
@ -32,11 +47,11 @@ pub enum PamAuthResponse {
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub enum PamAuthRequest { pub enum PamAuthRequest {
Password { cred: String }, Password { cred: String },
/* DeviceAuthorizationGrant { data: DeviceAuthorizationResponse }, /*
MFACode { MFACode {
cred: Option<PamCred> cred: Option<PamCred>
} }
*/ */
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]