diff --git a/Cargo.lock b/Cargo.lock
index f89b04a..cc0837d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -124,7 +124,6 @@ dependencies = [
  "inish",
  "libc",
  "nix",
- "pam-sys",
  "serde",
  "serde_json",
  "thiserror",
@@ -204,15 +203,6 @@ dependencies = [
  "memchr",
 ]
 
-[[package]]
-name = "pam-sys"
-version = "0.5.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd4858311a097f01a0006ef7d0cd50bca81ec430c949d7bf95cbefd202282434"
-dependencies = [
- "libc",
-]
-
 [[package]]
 name = "pin-project-lite"
 version = "0.2.14"
diff --git a/greetd/Cargo.toml b/greetd/Cargo.toml
index a9ac8d1..3e68a98 100644
--- a/greetd/Cargo.toml
+++ b/greetd/Cargo.toml
@@ -12,7 +12,6 @@ debug = []
 
 [dependencies]
 nix = { version = "0.27", features = ["ioctl", "signal", "user", "fs", "mman"] }
-pam-sys = "0.5.6"
 serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
 greetd_ipc = { path = "../greetd_ipc", features = ["tokio-codec"] }
diff --git a/greetd/src/error.rs b/greetd/src/error.rs
index 68aa07c..ca7d4d2 100644
--- a/greetd/src/error.rs
+++ b/greetd/src/error.rs
@@ -33,12 +33,6 @@ impl From<std::io::Error> for Error {
     }
 }
 
-impl From<crate::pam::PamError> for Error {
-    fn from(error: crate::pam::PamError) -> Self {
-        Error::AuthError(error.to_string())
-    }
-}
-
 impl From<greetd_ipc::codec::Error> for Error {
     fn from(error: greetd_ipc::codec::Error) -> Self {
         match error {
diff --git a/greetd/src/main.rs b/greetd/src/main.rs
index 92a53d4..d63d66e 100644
--- a/greetd/src/main.rs
+++ b/greetd/src/main.rs
@@ -1,7 +1,6 @@
 mod config;
 mod context;
 mod error;
-mod pam;
 mod scrambler;
 mod server;
 mod session;
diff --git a/greetd/src/server.rs b/greetd/src/server.rs
index b215c4a..3fa5a49 100644
--- a/greetd/src/server.rs
+++ b/greetd/src/server.rs
@@ -1,4 +1,4 @@
-use std::{path::Path, rc::Rc};
+use std::rc::Rc;
 
 use nix::unistd::{chown, getpid, Gid, Uid};
 use tokio::{
@@ -198,31 +198,8 @@ impl Drop for Listener {
 }
 
 pub async fn main(config: Config) -> Result<(), Error> {
-    let service = if Path::new(&format!("/etc/pam.d/{}", config.file.general.service)).exists() {
-        &config.file.general.service
-    } else if Path::new(&format!("/usr/lib/pam.d/{}", config.file.general.service)).exists() {
-        &config.file.general.service
-    } else {
-        return Err(format!("PAM '{}' service missing", config.file.general.service).into());
-    };
-
-    let greeter_service = if Path::new(&format!(
-        "/etc/pam.d/{}",
-        config.file.default_session.service
-    ))
-    .exists()
-    {
-        &config.file.default_session.service
-    } else if Path::new(&format!(
-        "/usr/lib/pam.d/{}",
-        config.file.default_session.service
-    ))
-    .exists()
-    {
-        &config.file.default_session.service
-    } else {
-        service
-    };
+    let service = &config.file.general.service;
+    let greeter_service = &config.file.default_session.service;
 
     let u = nix::unistd::User::from_name(&config.file.default_session.user)?.ok_or(format!(
         "configured default session user '{}' not found",
diff --git a/greetd/src/session/mod.rs b/greetd/src/session/mod.rs
index e1a08cb..698425e 100644
--- a/greetd/src/session/mod.rs
+++ b/greetd/src/session/mod.rs
@@ -1,4 +1,3 @@
-pub mod conv;
 pub mod interface;
 mod prctl;
 pub mod worker;
diff --git a/greetd/src/session/worker.rs b/greetd/src/session/worker.rs
index 71c9d10..ede3a6d 100644
--- a/greetd/src/session/worker.rs
+++ b/greetd/src/session/worker.rs
@@ -1,17 +1,43 @@
-use std::{env, ffi::CString, os::unix::net::UnixDatagram};
+use std::{
+    env,
+    ffi::CString,
+    io::Write,
+    os::unix::net::UnixDatagram,
+    process::{Command, Stdio},
+};
 
 use nix::{
     sys::wait::waitpid,
     unistd::{execve, fork, initgroups, setgid, setsid, setuid, ForkResult},
 };
-use pam_sys::{PamFlag, PamItemType};
 use serde::{Deserialize, Serialize};
 
-use super::{
-    conv::SessionConv,
-    prctl::{prctl, PrctlOption},
-};
-use crate::{error::Error, pam::session::PamSession, terminal};
+use super::prctl::{prctl, PrctlOption};
+use crate::{error::Error, scrambler::Scrambler, terminal};
+
+const AUTH_HELPER: &str = "/usr/libexec/mouse-auth";
+
+fn authenticate(username: &str, mut password: String) -> Result<(), Error> {
+    let result = (|| -> std::io::Result<bool> {
+        let mut child = Command::new(AUTH_HELPER)
+            .stdin(Stdio::piped())
+            .stdout(Stdio::null())
+            .stderr(Stdio::null())
+            .spawn()?;
+        if let Some(mut input) = child.stdin.take() {
+            input.write_all(username.as_bytes())?;
+            input.write_all(b"\n")?;
+            input.write_all(password.as_bytes())?;
+            input.write_all(b"\n")?;
+        }
+        Ok(child.wait()?.success())
+    })();
+    password.scramble();
+    match result {
+        Ok(true) => Ok(()),
+        Ok(false) | Err(_) => Err(Error::AuthError("authentication failed".to_string())),
+    }
+}
 
 #[derive(Clone, Debug, Serialize, Deserialize)]
 pub enum AuthMessageType {
@@ -100,10 +126,10 @@ impl SessionChildToParent {
 /// started by Session::start.
 fn worker(sock: &UnixDatagram) -> Result<(), Error> {
     let mut data = [0; 10240];
-    let (service, class, user, authenticate, tty, source_profile, listener_path) =
+    let (class, username, authenticate_user, tty, source_profile, listener_path) =
         match ParentToSessionChild::recv(sock, &mut data)? {
             ParentToSessionChild::InitiateLogin {
-                service,
+                service: _,
                 class,
                 user,
                 authenticate,
@@ -111,38 +137,39 @@ fn worker(sock: &UnixDatagram) -> Result<(), Error> {
                 source_profile,
                 listener_path,
             } => (
-                service,
                 class,
-                user,
+                user.to_owned(),
                 authenticate,
                 tty,
                 source_profile,
-                listener_path,
+                listener_path.to_owned(),
             ),
             ParentToSessionChild::Cancel => return Err("cancelled".into()),
             msg => return Err(format!("expected InitiateLogin or Cancel, got: {:?}", msg).into()),
         };
 
-    let conv = Box::pin(SessionConv::new(sock));
-    let mut pam = PamSession::start(service, user, conv)?;
-
-    if authenticate {
-        pam.authenticate(PamFlag::NONE)?;
+    if authenticate_user {
+        SessionChildToParent::PamMessage {
+            style: AuthMessageType::Secret,
+            msg: "Password: ".to_string(),
+        }
+        .send(sock)?;
+        let auth_result = match ParentToSessionChild::recv(sock, &mut data)? {
+            ParentToSessionChild::PamResponse {
+                resp: Some(password),
+            } => authenticate(&username, password),
+            ParentToSessionChild::PamResponse { resp: None } | ParentToSessionChild::Cancel => {
+                return Err("cancelled".into())
+            }
+            msg => return Err(format!("expected PamResponse or Cancel, got: {:?}", msg).into()),
+        };
+        data.fill(0);
+        auth_result?;
     }
-    pam.acct_mgmt(PamFlag::NONE)?;
-
-    // Not the credentials you think.
-    pam.setcred(PamFlag::ESTABLISH_CRED)?;
 
     // Mark authentication as a success.
     SessionChildToParent::Success.send(sock)?;
 
-    // Add GREETD_SOCK if this is a greeter session - we do this early as we are about to reuse the
-    // buffer, invalidating our borrow.
-    if let SessionClass::Greeter = class {
-        pam.putenv(&format!("GREETD_SOCK={}", &listener_path))?;
-    }
-
     // Fetch our arguments from the parent.
     let (env, cmd) = match ParentToSessionChild::recv(sock, &mut data)? {
         ParentToSessionChild::Args { env, cmd } => (env, cmd),
@@ -159,20 +186,18 @@ fn worker(sock: &UnixDatagram) -> Result<(), Error> {
         msg => return Err(format!("expected Start or Cancel, got: {:?}", msg).into()),
     };
 
-    let pam_username = pam.get_user()?;
-
-    let user = nix::unistd::User::from_name(&pam_username)?.ok_or("unable to get user info")?;
+    let user = nix::unistd::User::from_name(&username)?.ok_or("unable to get user info")?;
 
     // Make this process a session leader.
     setsid().map_err(|e| format!("unable to become session leader: {}", e))?;
 
+    let xdg_vtnr = match &tty {
+        TerminalMode::Terminal { vt, .. } => Some(*vt),
+        TerminalMode::Stdin => None,
+    };
     match tty {
         TerminalMode::Stdin => (),
         TerminalMode::Terminal { path, vt, switch } => {
-            // Tell PAM what TTY we're targetting, which is used by logind.
-            pam.set_item(PamItemType::TTY, &format!("tty{}", vt))?;
-            pam.putenv(&format!("XDG_VTNR={}", vt))?;
-
             // Opening our target terminal.
             let target_term = terminal::Terminal::open(&path)?;
 
@@ -197,12 +222,7 @@ fn worker(sock: &UnixDatagram) -> Result<(), Error> {
         }
     }
 
-    // PAM has to be provided a bunch of environment variables before
-    // open_session. We pass any environment variables from our greeter
-    // through here as well. This allows them to affect PAM (more
-    // specifically, pam_systemd.so), as well as make it easier to gather
-    // and set all environment variables later.
-    let prepared_env = [
+    let mut prepared_env = vec![
         "XDG_SEAT=seat0".to_string(),
         format!("XDG_SESSION_CLASS={}", class.as_str()),
         format!("USER={}", user.name),
@@ -214,15 +234,12 @@ fn worker(sock: &UnixDatagram) -> Result<(), Error> {
             env::var("TERM").unwrap_or_else(|_| "linux".to_string())
         ),
     ];
-    for e in env.iter().chain(prepared_env.iter()) {
-        pam.putenv(e)?;
+    if let SessionClass::Greeter = class {
+        prepared_env.push(format!("GREETD_SOCK={}", listener_path));
+    }
+    if let Some(vt) = xdg_vtnr {
+        prepared_env.push(format!("XDG_VTNR={}", vt));
     }
-
-    // Session time!
-    pam.open_session(PamFlag::NONE)?;
-
-    // We are done with PAM, clear variables that the child will not need.
-    _ = pam.putenv(&"XDG_SESSION_CLASS");
 
     // Prepare some strings in C format that we'll need.
     let cusername = CString::new(user.name)?;
@@ -235,13 +252,14 @@ fn worker(sock: &UnixDatagram) -> Result<(), Error> {
         format!("exec {}", cmd.join(" "))
     };
 
-    // Extract PAM environment for use with execve below.
-    let pamenvlist = pam.getenvlist()?;
-    let envvec = pamenvlist.to_vec();
+    let envvec = env
+        .iter()
+        .chain(prepared_env.iter())
+        .map(|entry| CString::new(entry.as_str()))
+        .collect::<Result<Vec<_>, _>>()?;
 
-    // PAM is weird and gets upset if you exec from the process that opened
-    // the session, registering it automatically as a log-out. Thus, we must
-    // exec in a new child.
+    // Keep a privileged outer worker so it can own and reap the session while
+    // the inner child runs entirely as the authenticated account.
     let child = match unsafe { fork() }.map_err(|e| format!("unable to fork: {}", e))? {
         ForkResult::Parent { child, .. } => child,
         ForkResult::Child => {
@@ -300,15 +318,6 @@ fn worker(sock: &UnixDatagram) -> Result<(), Error> {
         }
     }
 
-    // Close the session. This step requires root privileges to run, as it
-    // will result in various forms of login teardown (including unmounting
-    // home folders, telling logind that the session ended, etc.). This is
-    // why we cannot drop privileges in this process, but must do it in the
-    // inner-most child.
-    pam.close_session(PamFlag::NONE)?;
-    pam.setcred(PamFlag::DELETE_CRED)?;
-    pam.end()?;
-
     Ok(())
 }
 
