Skip to main content

authx_core/
config.rs

1//! Unified typed configuration for authx-rs.
2//!
3//! `AuthxConfig` consolidates all configuration knobs used by the CLI, dashboard,
4//! plugins, and runtime. It supports construction via builder pattern, direct
5//! instantiation with defaults, or loading from environment variables.
6
7use std::time::Duration;
8
9/// Central configuration for authx-rs services.
10///
11/// All fields carry sensible defaults. Use [`AuthxConfig::from_env`] to override
12/// any field via environment variables (prefix `AUTHX_`), or construct directly.
13#[derive(Debug, Clone)]
14pub struct AuthxConfig {
15    // ── Server ──────────────────────────────────────────────────
16    /// Bind address (e.g. `0.0.0.0:3000`).
17    pub bind: String,
18    /// Database URL. `None` ⇒ in-memory store.
19    pub database_url: Option<String>,
20    /// Enable HTTPS-only cookies.
21    pub secure_cookies: bool,
22
23    // ── Session ─────────────────────────────────────────────────
24    /// Session TTL in seconds (default: 30 days).
25    pub session_ttl_secs: i64,
26
27    // ── CSRF ────────────────────────────────────────────────────
28    /// Trusted origins for CSRF validation (comma-separated in env).
29    pub trusted_origins: Vec<String>,
30
31    // ── Rate limiting ───────────────────────────────────────────
32    /// Max requests per rate-limit window on auth routes.
33    pub rate_limit_max: u32,
34    /// Rate-limit window duration.
35    pub rate_limit_window: Duration,
36
37    // ── Account lockout ─────────────────────────────────────────
38    /// Number of failures before lockout.
39    pub lockout_max_failures: u32,
40    /// Lockout window duration.
41    pub lockout_window: Duration,
42
43    // ── Encryption ──────────────────────────────────────────────
44    /// 32-byte hex-encoded encryption key for OAuth/federation tokens.
45    /// `None` ⇒ random key generated at startup (tokens won't survive restart).
46    pub encryption_key_hex: Option<String>,
47
48    // ── OIDC Provider ───────────────────────────────────────────
49    /// Issuer URL for the built-in OIDC provider.
50    pub oidc_issuer: Option<String>,
51    /// Access token TTL in seconds.
52    pub oidc_access_token_ttl_secs: i64,
53    /// ID token TTL in seconds.
54    pub oidc_id_token_ttl_secs: i64,
55    /// Refresh token TTL in seconds.
56    pub oidc_refresh_token_ttl_secs: i64,
57    /// Authorization code TTL in seconds.
58    pub oidc_auth_code_ttl_secs: i64,
59    /// Device code TTL in seconds.
60    pub oidc_device_code_ttl_secs: i64,
61    /// Device code poll interval in seconds.
62    pub oidc_device_code_interval_secs: u32,
63    /// Verification URI for device flow.
64    pub oidc_verification_uri: Option<String>,
65
66    // ── WebAuthn / Passkeys ────────────────────────────────────
67    /// Relying party ID (RP ID), usually the effective domain.
68    pub webauthn_rp_id: String,
69    /// Allowed origin for WebAuthn ceremonies.
70    pub webauthn_rp_origin: String,
71    /// Challenge TTL in seconds for begin/finish ceremony pairing.
72    pub webauthn_challenge_ttl_secs: u64,
73}
74
75impl Default for AuthxConfig {
76    fn default() -> Self {
77        Self {
78            bind: "0.0.0.0:3000".into(),
79            database_url: None,
80            secure_cookies: false,
81
82            session_ttl_secs: 60 * 60 * 24 * 30, // 30 days
83
84            trusted_origins: vec!["http://localhost:3000".into()],
85
86            rate_limit_max: 30,
87            rate_limit_window: Duration::from_secs(60),
88
89            lockout_max_failures: 5,
90            lockout_window: Duration::from_secs(15 * 60),
91
92            encryption_key_hex: None,
93
94            oidc_issuer: None,
95            oidc_access_token_ttl_secs: 3600,
96            oidc_id_token_ttl_secs: 3600,
97            oidc_refresh_token_ttl_secs: 60 * 60 * 24 * 30,
98            oidc_auth_code_ttl_secs: 600,
99            oidc_device_code_ttl_secs: 600,
100            oidc_device_code_interval_secs: 5,
101            oidc_verification_uri: None,
102
103            webauthn_rp_id: "localhost".into(),
104            webauthn_rp_origin: "http://localhost:3000".into(),
105            webauthn_challenge_ttl_secs: 600,
106        }
107    }
108}
109
110impl AuthxConfig {
111    /// Load configuration from environment variables with `AUTHX_` prefix.
112    ///
113    /// Every field falls back to [`Default`] when its env var is absent.
114    ///
115    /// | Field                 | Env var                            |
116    /// |-----------------------|------------------------------------|
117    /// | `bind`                | `AUTHX_BIND`                       |
118    /// | `database_url`        | `DATABASE_URL`                     |
119    /// | `secure_cookies`      | `AUTHX_SECURE_COOKIES`             |
120    /// | `session_ttl_secs`    | `AUTHX_SESSION_TTL`                |
121    /// | `trusted_origins`     | `AUTHX_TRUSTED_ORIGINS` (comma)    |
122    /// | `rate_limit_max`      | `AUTHX_RATE_LIMIT`                 |
123    /// | `rate_limit_window`   | `AUTHX_RATE_LIMIT_WINDOW_SECS`     |
124    /// | `lockout_max_failures`| `AUTHX_LOCKOUT_FAILURES`           |
125    /// | `lockout_window`      | `AUTHX_LOCKOUT_MINUTES`            |
126    /// | `encryption_key_hex`  | `AUTHX_ENCRYPTION_KEY`             |
127    /// | `oidc_issuer`         | `AUTHX_OIDC_ISSUER`                |
128    /// | `oidc_*_ttl_secs`     | `AUTHX_OIDC_ACCESS_TOKEN_TTL` etc. |
129    /// | `webauthn_rp_id`      | `AUTHX_WEBAUTHN_RP_ID`             |
130    /// | `webauthn_rp_origin`  | `AUTHX_WEBAUTHN_RP_ORIGIN`         |
131    /// | `webauthn_challenge_ttl_secs` | `AUTHX_WEBAUTHN_CHALLENGE_TTL` |
132    pub fn from_env() -> Self {
133        let defaults = Self::default();
134
135        Self {
136            bind: env_or("AUTHX_BIND", defaults.bind),
137            database_url: std::env::var("DATABASE_URL").ok().or(defaults.database_url),
138            secure_cookies: env_parse("AUTHX_SECURE_COOKIES", defaults.secure_cookies),
139            session_ttl_secs: env_parse("AUTHX_SESSION_TTL", defaults.session_ttl_secs),
140            trusted_origins: std::env::var("AUTHX_TRUSTED_ORIGINS")
141                .map(|s| s.split(',').map(|o| o.trim().to_owned()).collect())
142                .unwrap_or(defaults.trusted_origins),
143            rate_limit_max: env_parse("AUTHX_RATE_LIMIT", defaults.rate_limit_max),
144            rate_limit_window: Duration::from_secs(env_parse(
145                "AUTHX_RATE_LIMIT_WINDOW_SECS",
146                defaults.rate_limit_window.as_secs(),
147            )),
148            lockout_max_failures: env_parse(
149                "AUTHX_LOCKOUT_FAILURES",
150                defaults.lockout_max_failures,
151            ),
152            lockout_window: Duration::from_secs(
153                env_parse(
154                    "AUTHX_LOCKOUT_MINUTES",
155                    defaults.lockout_window.as_secs() / 60,
156                ) * 60,
157            ),
158            encryption_key_hex: std::env::var("AUTHX_ENCRYPTION_KEY")
159                .ok()
160                .or(defaults.encryption_key_hex),
161            oidc_issuer: std::env::var("AUTHX_OIDC_ISSUER")
162                .ok()
163                .or(defaults.oidc_issuer),
164            oidc_access_token_ttl_secs: env_parse(
165                "AUTHX_OIDC_ACCESS_TOKEN_TTL",
166                defaults.oidc_access_token_ttl_secs,
167            ),
168            oidc_id_token_ttl_secs: env_parse(
169                "AUTHX_OIDC_ID_TOKEN_TTL",
170                defaults.oidc_id_token_ttl_secs,
171            ),
172            oidc_refresh_token_ttl_secs: env_parse(
173                "AUTHX_OIDC_REFRESH_TOKEN_TTL",
174                defaults.oidc_refresh_token_ttl_secs,
175            ),
176            oidc_auth_code_ttl_secs: env_parse(
177                "AUTHX_OIDC_AUTH_CODE_TTL",
178                defaults.oidc_auth_code_ttl_secs,
179            ),
180            oidc_device_code_ttl_secs: env_parse(
181                "AUTHX_OIDC_DEVICE_CODE_TTL",
182                defaults.oidc_device_code_ttl_secs,
183            ),
184            oidc_device_code_interval_secs: env_parse(
185                "AUTHX_OIDC_DEVICE_INTERVAL",
186                defaults.oidc_device_code_interval_secs,
187            ),
188            oidc_verification_uri: std::env::var("AUTHX_OIDC_VERIFICATION_URI")
189                .ok()
190                .or(defaults.oidc_verification_uri),
191            webauthn_rp_id: env_or("AUTHX_WEBAUTHN_RP_ID", defaults.webauthn_rp_id),
192            webauthn_rp_origin: env_or("AUTHX_WEBAUTHN_RP_ORIGIN", defaults.webauthn_rp_origin),
193            webauthn_challenge_ttl_secs: env_parse(
194                "AUTHX_WEBAUTHN_CHALLENGE_TTL",
195                defaults.webauthn_challenge_ttl_secs,
196            ),
197        }
198    }
199
200    /// Parse the 32-byte encryption key from hex, or generate a random one.
201    pub fn encryption_key(&self) -> [u8; 32] {
202        if let Some(hex_str) = &self.encryption_key_hex {
203            let bytes = hex::decode(hex_str).expect("AUTHX_ENCRYPTION_KEY must be valid hex");
204            let mut key = [0u8; 32];
205            assert!(
206                bytes.len() == 32,
207                "AUTHX_ENCRYPTION_KEY must be exactly 32 bytes (64 hex chars)"
208            );
209            key.copy_from_slice(&bytes);
210            key
211        } else {
212            rand::random()
213        }
214    }
215}
216
217fn env_or(key: &str, default: String) -> String {
218    std::env::var(key).unwrap_or(default)
219}
220
221fn env_parse<T: std::str::FromStr>(key: &str, default: T) -> T {
222    std::env::var(key)
223        .ok()
224        .and_then(|v| v.parse().ok())
225        .unwrap_or(default)
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn defaults_are_sane() {
234        let cfg = AuthxConfig::default();
235        assert_eq!(cfg.bind, "0.0.0.0:3000");
236        assert_eq!(cfg.session_ttl_secs, 60 * 60 * 24 * 30);
237        assert!(!cfg.secure_cookies);
238        assert_eq!(cfg.lockout_max_failures, 5);
239        assert_eq!(cfg.rate_limit_max, 30);
240        assert_eq!(cfg.oidc_access_token_ttl_secs, 3600);
241        assert_eq!(cfg.webauthn_challenge_ttl_secs, 600);
242    }
243
244    #[test]
245    fn encryption_key_random_when_unset() {
246        let cfg = AuthxConfig::default();
247        let k1 = cfg.encryption_key();
248        let k2 = cfg.encryption_key();
249        // Two random keys should differ (probabilistically)
250        assert_ne!(k1, k2);
251    }
252}