Implement DeviceAuthorizationGrant for MFA ()

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
unix_integration

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