rusmes_auth/lib.rs
1//! # rusmes-auth
2//!
3//! Pluggable authentication backends for the RusMES mail server.
4//!
5//! ## Overview
6//!
7//! `rusmes-auth` provides a unified [`AuthBackend`] trait that abstracts over multiple
8//! authentication strategies. All backends implement the same async interface, allowing
9//! them to be composed, swapped, or wrapped by middleware such as the brute-force
10//! protector found in the [`security`] module.
11//!
12//! ## Backends
13//!
14//! | Backend | Module | Notes |
15//! |---------|--------|-------|
16//! | File (htpasswd-style) | [`mod@file`] | bcrypt hashes, atomic writes |
17//! | LDAP / Active Directory | [`backends::ldap`] | connection pooling, group filtering |
18//! | SQL (SQLite / Postgres / MySQL) | [`backends::sql`] | bcrypt + Argon2 + SCRAM-SHA-256 |
19//! | OAuth2 / OIDC | [`backends::oauth2`] | JWT validation, XOAUTH2 SASL |
20//! | PAM | [`backends::pam`] | **feature-gated** (`pam-auth`), C bindings |
21//!
22//! ## SASL Mechanisms
23//!
24//! The [`sasl`] module implements RFC-compliant SASL mechanisms on top of any `AuthBackend`:
25//!
26//! - `PLAIN` (RFC 4616)
27//! - `LOGIN` (obsolete but widely supported)
28//! - `CRAM-MD5` (RFC 2195)
29//! - `SCRAM-SHA-256` (RFC 5802 / RFC 7677)
30//! - `XOAUTH2` (RFC 7628)
31//!
32//! ## Security
33//!
34//! The [`security`] module provides:
35//!
36//! - Brute-force / account-lockout protection (progressive lockout)
37//! - Per-IP rate limiting
38//! - Password strength validation (entropy, character class, banned list)
39//! - In-memory audit logging
40//!
41//! ## Feature Flags
42//!
43//! | Feature | Default | Description |
44//! |---------|---------|-------------|
45//! | `pam-auth` | **no** | Enables PAM backend (requires C `libpam`). Disabled by default to satisfy the Pure Rust policy and avoid RUSTSEC-2023-0040/0059. |
46//!
47//! ## Example
48//!
49//! ```rust,no_run
50//! use rusmes_auth::file::FileAuthBackend;
51//! use rusmes_auth::AuthBackend;
52//! use rusmes_proto::Username;
53//!
54//! # async fn example() -> anyhow::Result<()> {
55//! let backend = FileAuthBackend::new("/etc/rusmes/passwd").await?;
56//! let user = Username::new("alice".to_string())?;
57//! let ok = backend.authenticate(&user, "s3cr3t").await?;
58//! println!("authenticated: {ok}");
59//! # Ok(())
60//! # }
61//! ```
62
63use async_trait::async_trait;
64use rusmes_proto::Username;
65
66pub mod backends;
67pub mod file;
68pub mod sasl;
69pub mod security;
70
71#[async_trait]
72pub trait AuthBackend: Send + Sync {
73 /// Authenticate a user with username and password
74 async fn authenticate(&self, username: &Username, password: &str) -> anyhow::Result<bool>;
75
76 /// Verify if a username maps to a valid identity
77 async fn verify_identity(&self, username: &Username) -> anyhow::Result<bool>;
78
79 /// List all users (for admin CLI)
80 async fn list_users(&self) -> anyhow::Result<Vec<Username>>;
81
82 /// Create a new user with the given password
83 async fn create_user(&self, username: &Username, password: &str) -> anyhow::Result<()>;
84
85 /// Delete a user
86 async fn delete_user(&self, username: &Username) -> anyhow::Result<()>;
87
88 /// Change a user's password
89 async fn change_password(&self, username: &Username, new_password: &str) -> anyhow::Result<()>;
90
91 // ========================================================================
92 // SCRAM-SHA-256 Support (Optional)
93 // ========================================================================
94 // IMPORTANT: SCRAM-SHA-256 requires different credential storage than
95 // bcrypt. The following methods provide SCRAM-specific credential access.
96 // Default implementations return errors to maintain backward compatibility.
97
98 /// Get SCRAM-SHA-256 parameters (salt, iteration count) for a user
99 ///
100 /// Returns (salt, iterations) if SCRAM credentials are stored.
101 /// Default implementation returns an error indicating SCRAM is not supported.
102 async fn get_scram_params(&self, _username: &str) -> anyhow::Result<(Vec<u8>, u32)> {
103 Err(anyhow::anyhow!(
104 "SCRAM-SHA-256 credential storage not implemented in this AuthBackend"
105 ))
106 }
107
108 /// Get SCRAM-SHA-256 StoredKey for a user
109 ///
110 /// StoredKey = SHA256(ClientKey) where ClientKey = HMAC(SaltedPassword, "Client Key")
111 /// Default implementation returns an error indicating SCRAM is not supported.
112 async fn get_scram_stored_key(&self, _username: &str) -> anyhow::Result<Vec<u8>> {
113 Err(anyhow::anyhow!(
114 "SCRAM-SHA-256 credential storage not implemented in this AuthBackend"
115 ))
116 }
117
118 /// Get SCRAM-SHA-256 ServerKey for a user
119 ///
120 /// ServerKey = HMAC(SaltedPassword, "Server Key")
121 /// Default implementation returns an error indicating SCRAM is not supported.
122 async fn get_scram_server_key(&self, _username: &str) -> anyhow::Result<Vec<u8>> {
123 Err(anyhow::anyhow!(
124 "SCRAM-SHA-256 credential storage not implemented in this AuthBackend"
125 ))
126 }
127
128 /// Store SCRAM-SHA-256 credentials for a user
129 ///
130 /// This should store: salt, iterations, StoredKey, and ServerKey
131 /// Default implementation returns an error indicating SCRAM is not supported.
132 async fn store_scram_credentials(
133 &self,
134 _username: &Username,
135 _salt: Vec<u8>,
136 _iterations: u32,
137 _stored_key: Vec<u8>,
138 _server_key: Vec<u8>,
139 ) -> anyhow::Result<()> {
140 Err(anyhow::anyhow!(
141 "SCRAM-SHA-256 credential storage not implemented in this AuthBackend"
142 ))
143 }
144
145 // ========================================================================
146 // APOP (MD5 Digest) Support (Optional)
147 // ========================================================================
148 // APOP requires access to plaintext password to compute MD5 digest.
149 // This is incompatible with bcrypt and most secure password storage.
150 // Backends can optionally support APOP by storing plaintext passwords
151 // separately (not recommended for production).
152
153 /// Get plaintext password for APOP authentication
154 ///
155 /// Returns the plaintext password if available.
156 /// Default implementation returns an error indicating APOP is not supported.
157 ///
158 /// WARNING: This method exposes plaintext passwords and should only be used
159 /// for APOP authentication. Consider disabling APOP in production environments.
160 async fn get_apop_secret(&self, _username: &Username) -> anyhow::Result<String> {
161 Err(anyhow::anyhow!(
162 "APOP authentication not supported by this AuthBackend"
163 ))
164 }
165}