mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
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:
parent
383592d921
commit
8401c3e1c8
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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)]
|
||||
|
|
Loading…
Reference in a new issue