1use std::time::Duration;
8
9#[derive(Debug, Clone)]
14pub struct AuthxConfig {
15 pub bind: String,
18 pub database_url: Option<String>,
20 pub secure_cookies: bool,
22
23 pub session_ttl_secs: i64,
26
27 pub trusted_origins: Vec<String>,
30
31 pub rate_limit_max: u32,
34 pub rate_limit_window: Duration,
36
37 pub lockout_max_failures: u32,
40 pub lockout_window: Duration,
42
43 pub encryption_key_hex: Option<String>,
47
48 pub oidc_issuer: Option<String>,
51 pub oidc_access_token_ttl_secs: i64,
53 pub oidc_id_token_ttl_secs: i64,
55 pub oidc_refresh_token_ttl_secs: i64,
57 pub oidc_auth_code_ttl_secs: i64,
59 pub oidc_device_code_ttl_secs: i64,
61 pub oidc_device_code_interval_secs: u32,
63 pub oidc_verification_uri: Option<String>,
65
66 pub webauthn_rp_id: String,
69 pub webauthn_rp_origin: String,
71 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, 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 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 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 assert_ne!(k1, k2);
251 }
252}