]> git.ipfire.org Git - thirdparty/samba.git/commitdiff
Add pam_auth to himmelblau daemon
authorDavid Mulder <dmulder@samba.org>
Tue, 30 Jul 2024 17:27:35 +0000 (11:27 -0600)
committerDavid Mulder <dmulder@samba.org>
Wed, 23 Oct 2024 14:21:33 +0000 (14:21 +0000)
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
himmelblaud/src/himmelblaud.rs
himmelblaud/src/himmelblaud/himmelblaud_pam_auth.rs [new file with mode: 0644]

index b3ea0d45090a3372439a6c356e41d9d63e71145e..96d5cc376283d1dd8467eeea60e19ea7bdc3e03e 100644 (file)
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 use crate::cache::{GroupCache, PrivateCache, UserCache};
+use crate::himmelblaud::himmelblaud_pam_auth::AuthSession;
 use bytes::{BufMut, BytesMut};
-use dbg::{DBG_DEBUG, DBG_ERR};
+use dbg::{DBG_DEBUG, DBG_ERR, DBG_WARNING};
 use futures::{SinkExt, StreamExt};
 use himmelblau::graph::Graph;
 use himmelblau::BrokerClientApplication;
 use idmap::Idmap;
 use kanidm_hsm_crypto::{BoxedDynTpm, MachineKey};
 use param::LoadParm;
-use sock::{Request, Response};
+use sock::{PamAuthResponse, Request, Response};
 use std::error::Error;
 use std::io;
 use std::io::{Error as IoError, ErrorKind};
@@ -138,9 +139,74 @@ pub(crate) async fn handle_client(
     };
 
     let mut reqs = Framed::new(stream, ClientCodec::new());
+    let mut pam_auth_session_state = None;
 
     while let Some(Ok(req)) = reqs.next().await {
+        let mut resolver = resolver.lock().await;
         let resp = match req {
+            Request::PamAuthenticateInit(account_id) => {
+                DBG_DEBUG!("pam authenticate init");
+
+                match &pam_auth_session_state {
+                    Some(_) => {
+                        DBG_WARNING!(
+                            "Attempt to init \
+                                                    auth session while current \
+                                                    session is active"
+                        );
+                        pam_auth_session_state = None;
+                        Response::Error
+                    }
+                    None => {
+                        let (auth_session, resp) =
+                            resolver.pam_auth_init(&account_id)?;
+                        pam_auth_session_state = Some(auth_session);
+                        resp
+                    }
+                }
+            }
+            Request::PamAuthenticateStep(pam_next_req) => {
+                DBG_DEBUG!("pam authenticate step");
+                match &mut pam_auth_session_state {
+                    Some(AuthSession::InProgress {
+                        account_id,
+                        cred_handler,
+                    }) => {
+                        let resp = resolver
+                            .pam_auth_step(
+                                account_id,
+                                cred_handler,
+                                pam_next_req,
+                            )
+                            .await?;
+                        match resp {
+                            Response::PamAuthStepResponse(
+                                PamAuthResponse::Success,
+                            ) => {
+                                pam_auth_session_state =
+                                    Some(AuthSession::Success);
+                            }
+                            Response::PamAuthStepResponse(
+                                PamAuthResponse::Denied,
+                            ) => {
+                                pam_auth_session_state =
+                                    Some(AuthSession::Denied);
+                            }
+                            _ => {}
+                        }
+                        resp
+                    }
+                    _ => {
+                        DBG_WARNING!(
+                            "Attempt to \
+                                                    continue auth session \
+                                                    while current session is \
+                                                    inactive"
+                        );
+                        Response::Error
+                    }
+                }
+            }
             _ => todo!(),
         };
         reqs.send(resp).await?;
@@ -151,3 +217,5 @@ pub(crate) async fn handle_client(
     DBG_DEBUG!("Disconnecting client ...");
     Ok(())
 }
+
+mod himmelblaud_pam_auth;
diff --git a/himmelblaud/src/himmelblaud/himmelblaud_pam_auth.rs b/himmelblaud/src/himmelblaud/himmelblaud_pam_auth.rs
new file mode 100644 (file)
index 0000000..f09832e
--- /dev/null
@@ -0,0 +1,775 @@
+/*
+   Unix SMB/CIFS implementation.
+
+   Himmelblau daemon implementation for PAM_AUTH
+
+   Copyright (C) David Mulder 2024
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+use crate::cache::{GroupEntry, UserEntry};
+use crate::himmelblaud::Resolver;
+use dbg::{DBG_DEBUG, DBG_ERR, DBG_INFO, DBG_WARNING};
+use himmelblau::error::{
+    MsalError, AUTH_PENDING, DEVICE_AUTH_FAIL, REQUIRES_MFA,
+};
+use himmelblau::{
+    DeviceAuthorizationResponse, EnrollAttrs, MFAAuthContinue,
+    UserToken as UnixUserToken,
+};
+use ntstatus_gen::*;
+use serde::{Deserialize, Serialize};
+use sock::{PamAuthRequest, PamAuthResponse, Response};
+use std::env;
+use std::thread::sleep;
+use std::time::Duration;
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct GroupToken {
+    pub name: String,
+    pub spn: String,
+    pub object_id: String,
+    pub gidnumber: u32,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct UserToken {
+    pub name: String,
+    pub spn: String,
+    pub object_id: String,
+    pub gidnumber: u32,
+    pub displayname: String,
+    pub shell: Option<String>,
+    pub groups: Vec<GroupToken>,
+}
+
+pub(crate) enum AuthCredHandler {
+    MFA { flow: MFAAuthContinue },
+    DeviceAuthorizationGrant { flow: DeviceAuthorizationResponse },
+    SetupPin { token: UnixUserToken },
+    None,
+}
+
+pub(crate) enum AuthSession {
+    InProgress {
+        account_id: String,
+        cred_handler: AuthCredHandler,
+    },
+    Success,
+    Denied,
+}
+
+impl Resolver {
+    pub(crate) fn pam_auth_init(
+        &self,
+        account_id: &str,
+    ) -> Result<(AuthSession, Response), Box<NTSTATUS>> {
+        let auth_session = AuthSession::InProgress {
+            account_id: account_id.to_string(),
+            cred_handler: AuthCredHandler::None,
+        };
+        // TODO: Check for Hello Key in the Cache
+
+        // Send a password request to the client
+        Ok((
+            auth_session,
+            Response::PamAuthStepResponse(PamAuthResponse::Password),
+        ))
+    }
+
+    pub(crate) async fn pam_auth_step(
+        &mut self,
+        account_id: &str,
+        cred_handler: &mut AuthCredHandler,
+        pam_next_req: PamAuthRequest,
+    ) -> Result<Response, Box<NTSTATUS>> {
+        macro_rules! enroll_and_obtain_enrolled_token {
+            ($token:ident) => {{
+                if !self.is_domain_joined().await {
+                    DBG_DEBUG!("Device is not enrolled. Enrolling now.");
+                    self.join_domain(&$token)
+                        .await
+                        .map_err(|e| {
+                            DBG_ERR!("Failed to join domain: {:?}", e);
+                            Box::new(NT_STATUS_LOGON_FAILURE)
+                        })?;
+                }
+                let mut tpm = self.hsm.lock().await;
+                let mtoken2 = self
+                    .client
+                    .lock()
+                    .await
+                    .acquire_token_by_refresh_token(
+                        &$token.refresh_token,
+                        vec!["User.Read"],
+                        Some("https://graph.microsoft.com".to_string()),
+                        &mut tpm,
+                        &self.machine_key,
+                    )
+                    .await;
+                match mtoken2 {
+                    Ok(token) => token,
+                    Err(e) => {
+                        DBG_ERR!("{:?}", e);
+                        match e {
+                            MsalError::AcquireTokenFailed(err_resp) => {
+                                if err_resp.error_codes.contains(&DEVICE_AUTH_FAIL) {
+                                    /* A device authentication failure may happen
+                                     * if Azure hasn't finished replicating the new
+                                     * device object. Wait 5 seconds and try again. */
+                                    DBG_INFO!("Azure hasn't finished replicating the device...");
+                                    DBG_INFO!("Retrying in 5 seconds");
+                                    sleep(Duration::from_secs(5));
+                                    self.client
+                                        .lock()
+                                        .await
+                                        .acquire_token_by_refresh_token(
+                                            &$token.refresh_token,
+                                            vec!["User.Read"],
+                                            Some("https://graph.microsoft.com".to_string()),
+                                            &mut tpm,
+                                            &self.machine_key,
+                                        )
+                                        .await
+                                        .map_err(|e| {
+                                            DBG_ERR!("{:?}", e);
+                                            Box::new(NT_STATUS_NOT_FOUND)
+                                        })?
+                                } else {
+                                    return Err(Box::new(NT_STATUS_NOT_FOUND));
+                                }
+                            }
+                            _ => return Err(Box::new(NT_STATUS_NOT_FOUND)),
+                        }
+                    }
+                }
+            }};
+        }
+        macro_rules! auth_and_validate_hello_key {
+            ($hello_key:ident, $cred:ident) => {{
+                let token = {
+                    let mut tpm = self.hsm.lock().await;
+                    self
+                        .client
+                        .lock()
+                        .await
+                        .acquire_token_by_hello_for_business_key(
+                            account_id,
+                            &$hello_key,
+                            vec!["User.Read"],
+                            Some("https://graph.microsoft.com".to_string()),
+                            &mut tpm,
+                            &self.machine_key,
+                            &$cred,
+                        )
+                        .await
+                        .map_err(|e| {
+                            DBG_ERR!(
+                                "Failed to authenticate with hello key: {:?}",
+                                e
+                            );
+                            Box::new(NT_STATUS_LOGON_FAILURE)
+                        })?
+                    };
+
+                self.token_validate(account_id, &token).await
+            }};
+        }
+        match (&mut *cred_handler, pam_next_req) {
+            (
+                AuthCredHandler::SetupPin { token },
+                PamAuthRequest::SetupPin { pin },
+            ) => {
+                let hello_key = {
+                    let mut tpm = self.hsm.lock().await;
+                    match self
+                        .client
+                        .lock()
+                        .await
+                        .provision_hello_for_business_key(
+                            &token,
+                            &mut tpm,
+                            &self.machine_key,
+                            &pin,
+                        )
+                        .await
+                    {
+                        Ok(hello_key) => hello_key,
+                        Err(e) => {
+                            return Ok(Response::PamAuthStepResponse(
+                                PamAuthResponse::SetupPin {
+                                    msg: format!(
+                                        "Failed to provision hello key: {:?}\n{}",
+                                        e,
+                                        "Create a PIN to use in place of passwords."
+                                    ),
+                                }
+                            ));
+                        }
+                    }
+                };
+                self.pcache
+                    .loadable_hello_key_store(account_id, hello_key.clone())
+                    .map_err(|e| {
+                        DBG_ERR!("Failed to provision hello key: {:?}", e);
+                        Box::new(NT_STATUS_LOGON_FAILURE)
+                    })?;
+
+                auth_and_validate_hello_key!(hello_key, pin)
+            }
+            (_, PamAuthRequest::Pin { pin }) => {
+                let hello_key = self
+                    .pcache
+                    .loadable_hello_key_fetch(account_id)
+                    .ok_or_else(|| {
+                        DBG_ERR!("Authentication failed. Hello key missing.");
+                        Box::new(NT_STATUS_LOGON_FAILURE)
+                    })?;
+
+                auth_and_validate_hello_key!(hello_key, pin)
+            }
+            (_, PamAuthRequest::Password { cred }) => {
+                // Always attempt to force MFA when enrolling the device, otherwise
+                // the device object will not have the MFA claim. If we are already
+                // enrolled but creating a new Hello Pin, we follow the same process,
+                // since only an enrollment token can be exchanged for a PRT (which
+                // will be needed to enroll the Hello Pin).
+                let mresp = self
+                    .client
+                    .lock()
+                    .await
+                    .initiate_acquire_token_by_mfa_flow_for_device_enrollment(
+                        account_id, &cred,
+                    )
+                    .await;
+                // We need to wait to handle the response until after we've released
+                // the lock on the client, otherwise we will deadlock.
+                let resp = match mresp {
+                    Ok(resp) => resp,
+                    Err(e) => {
+                        // If SFA is disabled, we need to skip the SFA fallback.
+                        let sfa_enabled = self
+                            .lp
+                            .himmelblaud_sfa_fallback()
+                            .map_err(|e| {
+                                DBG_ERR!("{:?}", e);
+                                Box::new(NT_STATUS_LOGON_FAILURE)
+                            })?;
+                        macro_rules! init_dag {
+                            ($msg:expr) => {{
+                                DBG_WARNING!(
+                                    "SFA auth failed, falling back to DAG: {}",
+                                    $msg
+                                );
+                                // We've exhausted alternatives, and must perform a DAG
+                                let resp = self
+                                    .client
+                                    .lock()
+                                    .await
+                                    .initiate_device_flow_for_device_enrollment()
+                                    .await
+                                    .map_err(|e| {
+                                        DBG_ERR!("{:?}", e);
+                                        Box::new(NT_STATUS_LOGON_FAILURE)
+                                    })?;
+                                let msg = match &resp.message {
+                                    Some(msg) => msg.to_string(),
+                                    None => format!("Using a browser on another \
+                                        device, visit:\n{}\nAnd enter the code:\n{}",
+                                        resp.verification_uri, resp.user_code),
+                                };
+                                let polling_interval = match resp.interval {
+                                    Some(polling_interval) => polling_interval,
+                                    None => 5,
+                                };
+                                *cred_handler = AuthCredHandler::DeviceAuthorizationGrant {
+                                    flow: resp,
+                                };
+                                return Ok(Response::PamAuthStepResponse(
+                                    PamAuthResponse::MFAPoll {
+                                        msg,
+                                        polling_interval,
+                                    }
+                                ));
+                            }}
+                        }
+                        let token = if sfa_enabled {
+                            DBG_WARNING!(
+                                "MFA auth failed, falling back to SFA: {:?}",
+                                e
+                            );
+                            // Again, we need to wait to handle the response until after
+                            // we've released the write lock on the client, otherwise we
+                            // will deadlock.
+                            match self
+                                .client
+                                .lock()
+                                .await
+                                .acquire_token_by_username_password_for_device_enrollment(
+                                    account_id, &cred,
+                                )
+                                .await
+                            {
+                                Ok(token) => token,
+                                Err(e) => {
+                                    DBG_ERR!("{:?}", e);
+                                    match e {
+                                        MsalError::AcquireTokenFailed(
+                                            err_resp,
+                                        ) => {
+                                            if err_resp
+                                                .error_codes
+                                                .contains(&REQUIRES_MFA)
+                                            {
+                                                init_dag!(
+                                                    err_resp.error_description
+                                                );
+                                            }
+                                            return Err(Box::new(
+                                                NT_STATUS_LOGON_FAILURE,
+                                            ));
+                                        }
+                                        _ => {
+                                            return Err(Box::new(
+                                                NT_STATUS_LOGON_FAILURE,
+                                            ))
+                                        }
+                                    }
+                                }
+                            }
+                        } else {
+                            init_dag!(
+                                "SFA fallback is disabled by configuration"
+                            )
+                        };
+                        let token2 = enroll_and_obtain_enrolled_token!(token);
+                        return self.token_validate(account_id, &token2).await;
+                    }
+                };
+                match resp.mfa_method.as_str() {
+                    "PhoneAppOTP" | "OneWaySMS" | "ConsolidatedTelephony" => {
+                        let msg = resp.msg.clone();
+                        *cred_handler = AuthCredHandler::MFA { flow: resp };
+                        return Ok(Response::PamAuthStepResponse(
+                            PamAuthResponse::MFACode { msg },
+                        ));
+                    }
+                    _ => {
+                        let msg = resp.msg.clone();
+                        let polling_interval =
+                            resp.polling_interval.ok_or_else(|| {
+                                DBG_ERR!("Invalid response from the server");
+                                Box::new(NT_STATUS_LOGON_FAILURE)
+                            })?;
+                        *cred_handler = AuthCredHandler::MFA { flow: resp };
+                        return Ok(Response::PamAuthStepResponse(
+                            PamAuthResponse::MFAPoll {
+                                msg,
+                                // pam expects a polling_interval in
+                                // seconds, not milliseconds.
+                                polling_interval: polling_interval / 1000,
+                            },
+                        ));
+                    }
+                }
+            }
+            (
+                AuthCredHandler::DeviceAuthorizationGrant { flow },
+                PamAuthRequest::MFAPoll { .. },
+            ) => {
+                let token = match self
+                    .client
+                    .lock()
+                    .await
+                    .acquire_token_by_device_flow(flow.clone())
+                    .await
+                {
+                    Err(MsalError::AcquireTokenFailed(ref resp)) => {
+                        if resp.error_codes.contains(&AUTH_PENDING) {
+                            DBG_DEBUG!(
+                                "Polling for acquire_token_by_device_flow"
+                            );
+                            return Ok(Response::PamAuthStepResponse(
+                                PamAuthResponse::MFAPollWait,
+                            ));
+                        } else {
+                            DBG_ERR!("{}", resp.error_description);
+                            return Err(Box::new(NT_STATUS_LOGON_FAILURE));
+                        }
+                    }
+                    Err(e) => {
+                        DBG_ERR!("{:?}", e);
+                        return Err(Box::new(NT_STATUS_LOGON_FAILURE));
+                    }
+                    Ok(token) => token,
+                };
+                let token2 = enroll_and_obtain_enrolled_token!(token);
+                match self.token_validate(account_id, &token2).await {
+                    Ok(Response::PamAuthStepResponse(
+                        PamAuthResponse::Success,
+                    )) => {
+                        let mfa = token2.amr_mfa().map_err(|e| {
+                            DBG_ERR!("{:?}", e);
+                            Box::new(NT_STATUS_NOT_FOUND)
+                        })?;
+                        // If the DAG didn't obtain an MFA amr, and SFA fallback
+                        // is disabled, we need to reject the authentication
+                        // attempt here.
+                        let sfa_enabled = self
+                            .lp
+                            .himmelblaud_sfa_fallback()
+                            .map_err(|e| {
+                                DBG_ERR!("{:?}", e);
+                                Box::new(NT_STATUS_LOGON_FAILURE)
+                            })?;
+                        if !mfa && !sfa_enabled {
+                            DBG_INFO!(
+                                "A DAG produced an SFA token, yet SFA \
+                                fallback is disabled by configuration"
+                            );
+                            return Ok(Response::PamAuthStepResponse(
+                                PamAuthResponse::Denied,
+                            ));
+                        }
+                        // STOP! If the DAG doesn't hold an MFA amr, then we
+                        // need to bail out here and refuse Hello enrollment
+                        // (we can't enroll in Hello with an SFA token).
+                        // Also skip Hello enrollment if it is disabled by config
+                        let hello_enabled = self
+                            .lp
+                            .himmelblaud_hello_enabled()
+                            .map_err(|e| {
+                                DBG_ERR!("{:?}", e);
+                                Box::new(NT_STATUS_LOGON_FAILURE)
+                            })?;
+                        if !mfa || !hello_enabled {
+                            if !mfa {
+                                DBG_INFO!(
+                                    "Skipping Hello enrollment because \
+                                    the token doesn't contain an MFA amr"
+                                );
+                            } else if !hello_enabled {
+                                DBG_INFO!(
+                                    "Skipping Hello enrollment \
+                                    because it is disabled"
+                                );
+                            }
+                            return Ok(Response::PamAuthStepResponse(
+                                PamAuthResponse::Success,
+                            ));
+                        }
+
+                        // Setup Windows Hello
+                        *cred_handler = AuthCredHandler::SetupPin { token };
+                        return Ok(Response::PamAuthStepResponse(
+                            PamAuthResponse::SetupPin {
+                                msg: format!(
+                                    "Set up a PIN\n {}{}",
+                                    "A Hello PIN is a fast, secure way to sign",
+                                    "in to your device, apps, and services."
+                                ),
+                            },
+                        ));
+                    }
+                    Ok(auth_result) => Ok(auth_result),
+                    Err(e) => Err(e),
+                }
+            }
+            (
+                AuthCredHandler::MFA { ref mut flow },
+                PamAuthRequest::MFACode { cred },
+            ) => {
+                let token = self
+                    .client
+                    .lock()
+                    .await
+                    .acquire_token_by_mfa_flow(
+                        account_id,
+                        Some(&cred),
+                        None,
+                        flow,
+                    )
+                    .await
+                    .map_err(|e| {
+                        DBG_ERR!("{:?}", e);
+                        Box::new(NT_STATUS_NOT_FOUND)
+                    })?;
+                let token2 = enroll_and_obtain_enrolled_token!(token);
+                match self.token_validate(account_id, &token2).await {
+                    Ok(Response::PamAuthStepResponse(
+                        PamAuthResponse::Success,
+                    )) => {
+                        // Skip Hello enrollment if it is disabled by config
+                        let hello_enabled = self
+                            .lp
+                            .himmelblaud_hello_enabled()
+                            .map_err(|e| {
+                                DBG_ERR!("{:?}", e);
+                                Box::new(NT_STATUS_LOGON_FAILURE)
+                            })?;
+                        if !hello_enabled {
+                            DBG_INFO!("Skipping Hello enrollment because it is disabled");
+                            return Ok(Response::PamAuthStepResponse(
+                                PamAuthResponse::Success,
+                            ));
+                        }
+
+                        // Setup Windows Hello
+                        *cred_handler = AuthCredHandler::SetupPin { token };
+                        return Ok(Response::PamAuthStepResponse(
+                            PamAuthResponse::SetupPin {
+                                msg: format!(
+                                    "Set up a PIN\n {}{}",
+                                    "A Hello PIN is a fast, secure way to sign",
+                                    "in to your device, apps, and services."
+                                ),
+                            },
+                        ));
+                    }
+                    Ok(auth_result) => Ok(auth_result),
+                    Err(e) => Err(e),
+                }
+            }
+            (
+                AuthCredHandler::MFA { flow },
+                PamAuthRequest::MFAPoll { poll_attempt },
+            ) => {
+                let max_poll_attempts =
+                    flow.max_poll_attempts.ok_or_else(|| {
+                        DBG_ERR!("Invalid response from the server");
+                        Box::new(NT_STATUS_LOGON_FAILURE)
+                    })?;
+                if poll_attempt > max_poll_attempts {
+                    DBG_ERR!("MFA polling timed out");
+                    return Err(Box::new(NT_STATUS_LOGON_FAILURE));
+                }
+                let token = match self
+                    .client
+                    .lock()
+                    .await
+                    .acquire_token_by_mfa_flow(
+                        account_id,
+                        None,
+                        Some(poll_attempt),
+                        flow,
+                    )
+                    .await
+                {
+                    Ok(token) => token,
+                    Err(e) => match e {
+                        MsalError::MFAPollContinue => {
+                            return Ok(Response::PamAuthStepResponse(
+                                PamAuthResponse::MFAPollWait,
+                            ));
+                        }
+                        e => {
+                            DBG_ERR!("{:?}", e);
+                            return Err(Box::new(NT_STATUS_NOT_FOUND));
+                        }
+                    },
+                };
+                let token2 = enroll_and_obtain_enrolled_token!(token);
+                match self.token_validate(account_id, &token2).await {
+                    Ok(Response::PamAuthStepResponse(
+                        PamAuthResponse::Success,
+                    )) => {
+                        // Skip Hello enrollment if it is disabled by config
+                        let hello_enabled = self
+                            .lp
+                            .himmelblaud_hello_enabled()
+                            .map_err(|e| {
+                                DBG_ERR!("{:?}", e);
+                                Box::new(NT_STATUS_LOGON_FAILURE)
+                            })?;
+                        if !hello_enabled {
+                            DBG_INFO!("Skipping Hello enrollment because it is disabled");
+                            return Ok(Response::PamAuthStepResponse(
+                                PamAuthResponse::Success,
+                            ));
+                        }
+
+                        // Setup Windows Hello
+                        *cred_handler = AuthCredHandler::SetupPin { token };
+                        return Ok(Response::PamAuthStepResponse(
+                            PamAuthResponse::SetupPin {
+                                msg: format!(
+                                    "Set up a PIN\n {}{}",
+                                    "A Hello PIN is a fast, secure way to sign",
+                                    "in to your device, apps, and services."
+                                ),
+                            },
+                        ));
+                    }
+                    Ok(auth_result) => Ok(auth_result),
+                    Err(e) => Err(e),
+                }
+            }
+            _ => {
+                DBG_ERR!(
+                    "Unexpected AuthCredHandler and PamAuthRequest pairing"
+                );
+                Err(Box::new(NT_STATUS_NOT_IMPLEMENTED))
+            }
+        }
+    }
+
+    async fn token_validate(
+        &mut self,
+        account_id: &str,
+        token: &UnixUserToken,
+    ) -> Result<Response, Box<NTSTATUS>> {
+        match &token.access_token {
+            Some(access_token) => {
+                /* MFA can respond with different user than requested.
+                 * Azure resource names are case insensitive.
+                 */
+                let spn = token.spn().map_err(|e| {
+                    DBG_ERR!("Failed fetching user spn: {:?}", e);
+                    Box::new(NT_STATUS_LOGON_FAILURE)
+                })?;
+                if account_id.to_string().to_lowercase()
+                    != spn.to_string().to_lowercase()
+                {
+                    DBG_ERR!(
+                        "Authenticated user {} does not match requested user {}",
+                        spn, account_id
+                    );
+                    return Ok(Response::PamAuthStepResponse(
+                        PamAuthResponse::Denied,
+                    ));
+                }
+                DBG_INFO!(
+                    "Authentication successful for user '{}'",
+                    account_id
+                );
+
+                // Store the user in the cache
+                let user_entry: UserEntry = token.try_into().map_err(|e| {
+                    DBG_ERR!("{:?}", e);
+                    Box::new(NT_STATUS_LOGON_FAILURE)
+                })?;
+                self.user_cache.store(user_entry).map_err(|e| {
+                    DBG_ERR!("{:?}", e);
+                    Box::new(NT_STATUS_LOGON_FAILURE)
+                })?;
+
+                // Get the users groups, and store those groups in the cache
+                let groups: Vec<GroupEntry> = self
+                    .graph
+                    .request_user_groups(access_token)
+                    .await
+                    .map_err(|e| {
+                        DBG_ERR!("{:?}", e);
+                        Box::new(NT_STATUS_LOGON_FAILURE)
+                    })?
+                    .into_iter()
+                    .map(|g| GroupEntry::into_with_member(g, account_id))
+                    .collect();
+                self.group_cache.merge_groups(account_id, groups).map_err(
+                    |e| {
+                        DBG_ERR!("{:?}", e);
+                        Box::new(NT_STATUS_LOGON_FAILURE)
+                    },
+                )?;
+
+                Ok(Response::PamAuthStepResponse(PamAuthResponse::Success))
+            }
+            None => {
+                DBG_INFO!("Authentication failed for user '{}'", account_id);
+                Err(Box::new(NT_STATUS_NOT_FOUND))
+            }
+        }
+    }
+
+    async fn join_domain(
+        &mut self,
+        token: &UnixUserToken,
+    ) -> Result<(), Box<NTSTATUS>> {
+        /* If not already joined, join the domain now. */
+        let os = env::var("TARGET_OS").map_err(|e| {
+            DBG_ERR!("{:?}", e);
+            Box::new(NT_STATUS_LOGON_FAILURE)
+        })?;
+        let attrs =
+            EnrollAttrs::new(self.realm.clone(), None, Some(os), None, None)
+                .map_err(|e| {
+                    DBG_ERR!("{:?}", e);
+                    Box::new(NT_STATUS_LOGON_FAILURE)
+                })?;
+        let mut tpm = self.hsm.lock().await;
+        match self
+            .client
+            .lock()
+            .await
+            .enroll_device(token, attrs, &mut tpm, &self.machine_key)
+            .await
+        {
+            Ok((
+                new_loadable_transport_key,
+                new_loadable_cert_key,
+                device_id,
+            )) => {
+                DBG_INFO!("Joined domain {} ({})", self.realm, device_id);
+                // Store the new_loadable_cert_key in the keystore
+                self.pcache
+                    .loadable_cert_key_store(&self.realm, new_loadable_cert_key)
+                    .map_err(|e| {
+                        DBG_ERR!("{:?}", e);
+                        Box::new(NT_STATUS_LOGON_FAILURE)
+                    })?;
+                // Store the new_loadable_transport_key
+                self.pcache
+                    .loadable_transport_key_store(
+                        &self.realm,
+                        new_loadable_transport_key,
+                    )
+                    .map_err(|e| {
+                        DBG_ERR!("{:?}", e);
+                        Box::new(NT_STATUS_LOGON_FAILURE)
+                    })?;
+                // Store the device_id
+                self.pcache
+                    .device_id_store(&self.realm, &device_id)
+                    .map_err(|e| {
+                        DBG_ERR!("{:?}", e);
+                        Box::new(NT_STATUS_LOGON_FAILURE)
+                    })?;
+                Ok(())
+            }
+            Err(e) => {
+                DBG_ERR!("{:?}", e);
+                Err(Box::new(NT_STATUS_LOGON_FAILURE))
+            }
+        }
+    }
+
+    async fn is_domain_joined(&mut self) -> bool {
+        /* If we have access to tpm keys, and the domain device_id is
+         * configured, we'll assume we are domain joined. */
+        let device_id = self.pcache.device_id(&self.realm);
+        if device_id.is_none() {
+            return false;
+        }
+        let transport_key =
+            self.pcache.loadable_transport_key_fetch(&self.realm);
+        if transport_key.is_none() {
+            return false;
+        }
+        let cert_key = self.pcache.loadable_cert_key_fetch(&self.realm);
+        if cert_key.is_none() {
+            return false;
+        }
+        true
+    }
+}