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 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)

View file

@ -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

View file

@ -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<DaemonClientBlocking, Box<dyn Error>> {
let timeout = Duration::from_secs(timeout);
debug!(%path, ?timeout);
pub fn new(path: &str) -> Result<DaemonClientBlocking, Box<dyn Error>> {
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<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| {
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;

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 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<PamAuthResponse> for AuthRequest {
fn into(self) -> PamAuthResponse {
match self {
AuthRequest::Password => PamAuthResponse::Password,
AuthRequest::DeviceAuthorizationGrant { data } => {
PamAuthResponse::DeviceAuthorizationGrant { data }
}
}
}
}

View file

@ -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)
}
}
}

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

View file

@ -16,12 +16,27 @@ pub struct NssGroup {
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)]
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<PamCred>
}
*/
DeviceAuthorizationGrant { data: DeviceAuthorizationResponse }, /*
MFACode {
cred: Option<PamCred>
}
*/
}
#[derive(Serialize, Deserialize, Debug)]