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//! | System (Unix) | [`backends::system`] | Pure Rust `/etc/shadow` auth |
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//! ## Example
42//!
43//! ```rust,no_run
44//! use rusmes_auth::file::FileAuthBackend;
45//! use rusmes_auth::AuthBackend;
46//! use rusmes_proto::Username;
47//!
48//! # async fn example() -> anyhow::Result<()> {
49//! let backend = FileAuthBackend::new("/etc/rusmes/passwd").await?;
50//! let user = Username::new("alice".to_string())?;
51//! let ok = backend.authenticate(&user, "s3cr3t").await?;
52//! println!("authenticated: {ok}");
53//! # Ok(())
54//! # }
55//! ```
56
57use async_trait::async_trait;
58use rusmes_proto::Username;
59
60pub mod backends;
61pub mod file;
62pub mod sasl;
63pub mod security;
64
65#[async_trait]
66pub trait AuthBackend: Send + Sync {
67 /// Authenticate a user with username and password
68 async fn authenticate(&self, username: &Username, password: &str) -> anyhow::Result<bool>;
69
70 /// Verify if a username maps to a valid identity
71 async fn verify_identity(&self, username: &Username) -> anyhow::Result<bool>;
72
73 /// List all users (for admin CLI)
74 async fn list_users(&self) -> anyhow::Result<Vec<Username>>;
75
76 /// Create a new user with the given password
77 async fn create_user(&self, username: &Username, password: &str) -> anyhow::Result<()>;
78
79 /// Delete a user
80 async fn delete_user(&self, username: &Username) -> anyhow::Result<()>;
81
82 /// Change a user's password
83 async fn change_password(&self, username: &Username, new_password: &str) -> anyhow::Result<()>;
84
85 // ========================================================================
86 // SCRAM-SHA-256 Support (Optional)
87 // ========================================================================
88 // IMPORTANT: SCRAM-SHA-256 requires different credential storage than
89 // bcrypt. The following methods provide SCRAM-specific credential access.
90 // Default implementations return errors to maintain backward compatibility.
91
92 /// Get SCRAM-SHA-256 parameters (salt, iteration count) for a user
93 ///
94 /// Returns (salt, iterations) if SCRAM credentials are stored.
95 /// Default implementation returns an error indicating SCRAM is not supported.
96 async fn get_scram_params(&self, _username: &str) -> anyhow::Result<(Vec<u8>, u32)> {
97 Err(anyhow::anyhow!(
98 "SCRAM-SHA-256 credential storage not implemented in this AuthBackend"
99 ))
100 }
101
102 /// Get SCRAM-SHA-256 StoredKey for a user
103 ///
104 /// StoredKey = SHA256(ClientKey) where ClientKey = HMAC(SaltedPassword, "Client Key")
105 /// Default implementation returns an error indicating SCRAM is not supported.
106 async fn get_scram_stored_key(&self, _username: &str) -> anyhow::Result<Vec<u8>> {
107 Err(anyhow::anyhow!(
108 "SCRAM-SHA-256 credential storage not implemented in this AuthBackend"
109 ))
110 }
111
112 /// Get SCRAM-SHA-256 ServerKey for a user
113 ///
114 /// ServerKey = HMAC(SaltedPassword, "Server Key")
115 /// Default implementation returns an error indicating SCRAM is not supported.
116 async fn get_scram_server_key(&self, _username: &str) -> anyhow::Result<Vec<u8>> {
117 Err(anyhow::anyhow!(
118 "SCRAM-SHA-256 credential storage not implemented in this AuthBackend"
119 ))
120 }
121
122 /// Store SCRAM-SHA-256 credentials for a user
123 ///
124 /// This should store: salt, iterations, StoredKey, and ServerKey
125 /// Default implementation returns an error indicating SCRAM is not supported.
126 async fn store_scram_credentials(
127 &self,
128 _username: &Username,
129 _salt: Vec<u8>,
130 _iterations: u32,
131 _stored_key: Vec<u8>,
132 _server_key: Vec<u8>,
133 ) -> anyhow::Result<()> {
134 Err(anyhow::anyhow!(
135 "SCRAM-SHA-256 credential storage not implemented in this AuthBackend"
136 ))
137 }
138
139 // ========================================================================
140 // APOP (MD5 Digest) Support (Optional)
141 // ========================================================================
142 // APOP requires access to plaintext password to compute MD5 digest.
143 // This is incompatible with bcrypt and most secure password storage.
144 // Backends can optionally support APOP by storing plaintext passwords
145 // separately (not recommended for production).
146
147 /// Get plaintext password for APOP authentication
148 ///
149 /// Returns the plaintext password if available.
150 /// Default implementation returns an error indicating APOP is not supported.
151 ///
152 /// WARNING: This method exposes plaintext passwords and should only be used
153 /// for APOP authentication. Consider disabling APOP in production environments.
154 async fn get_apop_secret(&self, _username: &Username) -> anyhow::Result<String> {
155 Err(anyhow::anyhow!(
156 "APOP authentication not supported by this AuthBackend"
157 ))
158 }
159}