Rust · 6816 bytes Raw Blame History
1 //! PAM authentication handling
2 //!
3 //! Implements a state machine for PAM-based authentication with
4 //! proper conversation handling for the greeter.
5
6 use anyhow::{anyhow, Result};
7 use pam::Client;
8
9 /// Service name for PAM configuration
10 const PAM_SERVICE: &str = "gardm";
11
12 /// Authentication state machine
13 #[derive(Debug)]
14 pub enum AuthState {
15 /// No active authentication
16 Idle,
17 /// Waiting for password after CreateSession
18 AwaitingPassword { username: String },
19 /// Authentication succeeded - stores password for session PAM
20 Authenticated { username: String, password: String },
21 }
22
23 impl Default for AuthState {
24 fn default() -> Self {
25 Self::Idle
26 }
27 }
28
29 /// Authentication session manager
30 pub struct AuthSession {
31 state: AuthState,
32 }
33
34 /// Response from authentication operations
35 #[derive(Debug)]
36 pub enum AuthResponse {
37 /// Request password from user
38 Prompt { prompt: String, echo: bool },
39 /// Authentication succeeded
40 Success,
41 /// Authentication failed
42 Error { message: String },
43 /// Informational message
44 Info { message: String },
45 }
46
47 impl AuthSession {
48 pub fn new() -> Self {
49 Self {
50 state: AuthState::Idle,
51 }
52 }
53
54 /// Get current state for inspection
55 pub fn state(&self) -> &AuthState {
56 &self.state
57 }
58
59 /// Start a new authentication session for a user
60 pub fn create_session(&mut self, username: &str) -> AuthResponse {
61 tracing::info!(username, "Creating auth session");
62
63 self.state = AuthState::AwaitingPassword {
64 username: username.to_string(),
65 };
66
67 AuthResponse::Prompt {
68 prompt: "Password:".to_string(),
69 echo: false,
70 }
71 }
72
73 /// Attempt authentication with provided password
74 ///
75 /// This runs PAM authentication in a blocking thread since PAM is synchronous.
76 /// The password is stored temporarily for use by the session spawner, which
77 /// will re-authenticate with PAM in the child process to properly register
78 /// the session with logind.
79 pub async fn authenticate(&mut self, password: &str) -> AuthResponse {
80 let username = match &self.state {
81 AuthState::AwaitingPassword { username } => username.clone(),
82 _ => {
83 return AuthResponse::Error {
84 message: "No authentication in progress".to_string(),
85 }
86 }
87 };
88
89 // Run PAM in blocking thread - this just verifies credentials
90 let password_str = password.to_string();
91 let username_for_pam = username.clone();
92 tracing::debug!("Spawning PAM authentication task");
93 let result = tokio::task::spawn_blocking(move || {
94 pam_verify_only(&username_for_pam, &password_str)
95 })
96 .await;
97 tracing::debug!(?result, "PAM task completed");
98
99 match result {
100 Ok(Ok(())) => {
101 tracing::info!(%username, "Authentication succeeded");
102 // Store password for session spawner to use with PAM open_session
103 self.state = AuthState::Authenticated {
104 username,
105 password: password.to_string(),
106 };
107 AuthResponse::Success
108 }
109 Ok(Err(e)) => {
110 tracing::warn!(%username, error = %e, "Authentication failed");
111 self.state = AuthState::Idle;
112 AuthResponse::Error {
113 message: format!("Authentication failed: {}", e),
114 }
115 }
116 Err(e) => {
117 tracing::error!(error = %e, "PAM task panicked");
118 self.state = AuthState::Idle;
119 AuthResponse::Error {
120 message: "Internal authentication error".to_string(),
121 }
122 }
123 }
124 }
125
126 /// Cancel the current authentication session
127 pub fn cancel(&mut self) {
128 if let AuthState::AwaitingPassword { username } | AuthState::Authenticated { username, .. } =
129 &self.state
130 {
131 tracing::info!(username, "Session cancelled");
132 }
133 self.state = AuthState::Idle;
134 }
135
136 /// Check if user is authenticated and ready to start session
137 pub fn is_authenticated(&self) -> Option<&str> {
138 match &self.state {
139 AuthState::Authenticated { username, .. } => Some(username),
140 _ => None,
141 }
142 }
143
144 /// Consume the authenticated state and return (username, password)
145 /// The password is needed for the session spawner to re-authenticate with PAM
146 pub fn take_authenticated(&mut self) -> Option<(String, String)> {
147 if matches!(self.state, AuthState::Authenticated { .. }) {
148 let old = std::mem::replace(&mut self.state, AuthState::Idle);
149 if let AuthState::Authenticated { username, password } = old {
150 return Some((username, password));
151 }
152 }
153 None
154 }
155 }
156
157 /// Verify PAM credentials only (no open_session)
158 /// This is used for quick credential verification. The actual session opening
159 /// happens in the spawned child process.
160 fn pam_verify_only(username: &str, password: &str) -> Result<()> {
161 tracing::debug!(%username, password_len = password.len(), "Verifying PAM credentials");
162
163 // Create client with PasswordConv (non-interactive, uses provided password)
164 let mut client = Client::with_password(PAM_SERVICE)
165 .map_err(|e| anyhow!("Failed to create PAM client: {:?}", e))?;
166
167 // Set the credentials
168 client
169 .conversation_mut()
170 .set_credentials(username, password);
171
172 tracing::debug!("PAM client created, calling authenticate");
173
174 // Authenticate only - don't open session here
175 // open_session will be called in the spawned child process
176 client
177 .authenticate()
178 .map_err(|e| anyhow!("PAM authentication failed: {:?}", e))?;
179
180 tracing::debug!("PAM credential verification complete");
181 Ok(())
182 }
183
184 /// Service name for PAM - exported for use by session module
185 pub const PAM_SERVICE_NAME: &str = PAM_SERVICE;
186
187 #[cfg(test)]
188 mod tests {
189 use super::*;
190
191 #[test]
192 fn test_auth_state_machine() {
193 let mut session = AuthSession::new();
194
195 // Initial state is idle
196 assert!(matches!(session.state(), AuthState::Idle));
197
198 // Create session transitions to awaiting password
199 let response = session.create_session("testuser");
200 assert!(matches!(response, AuthResponse::Prompt { .. }));
201 assert!(matches!(
202 session.state(),
203 AuthState::AwaitingPassword { .. }
204 ));
205
206 // Cancel returns to idle
207 session.cancel();
208 assert!(matches!(session.state(), AuthState::Idle));
209 }
210 }
211