Skip to main content

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}