mas_config/sections/
rate_limiting.rs

1// Copyright 2024, 2025 New Vector Ltd.
2// Copyright 2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE in the repository root for full details.
6
7use std::{num::NonZeroU32, time::Duration};
8
9use governor::Quota;
10use schemars::JsonSchema;
11use serde::{Deserialize, Serialize, de::Error as _};
12
13use crate::ConfigurationSection;
14
15/// Configuration related to sending emails
16#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
17pub struct RateLimitingConfig {
18    /// Account Recovery-specific rate limits
19    #[serde(default)]
20    pub account_recovery: AccountRecoveryRateLimitingConfig,
21
22    /// Login-specific rate limits
23    #[serde(default)]
24    pub login: LoginRateLimitingConfig,
25
26    /// Controls how many registrations attempts are permitted
27    /// based on source address.
28    #[serde(default = "default_registration")]
29    pub registration: RateLimiterConfiguration,
30
31    /// Email authentication-specific rate limits
32    #[serde(default)]
33    pub email_authentication: EmailauthenticationRateLimitingConfig,
34}
35
36#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
37pub struct LoginRateLimitingConfig {
38    /// Controls how many login attempts are permitted
39    /// based on source IP address.
40    /// This can protect against brute force login attempts.
41    ///
42    /// Note: this limit also applies to password checks when a user attempts to
43    /// change their own password.
44    #[serde(default = "default_login_per_ip")]
45    pub per_ip: RateLimiterConfiguration,
46
47    /// Controls how many login attempts are permitted
48    /// based on the account that is being attempted to be logged into.
49    /// This can protect against a distributed brute force attack
50    /// but should be set high enough to prevent someone's account being
51    /// casually locked out.
52    ///
53    /// Note: this limit also applies to password checks when a user attempts to
54    /// change their own password.
55    #[serde(default = "default_login_per_account")]
56    pub per_account: RateLimiterConfiguration,
57}
58
59#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
60pub struct AccountRecoveryRateLimitingConfig {
61    /// Controls how many account recovery attempts are permitted
62    /// based on source IP address.
63    /// This can protect against causing e-mail spam to many targets.
64    ///
65    /// Note: this limit also applies to re-sends.
66    #[serde(default = "default_account_recovery_per_ip")]
67    pub per_ip: RateLimiterConfiguration,
68
69    /// Controls how many account recovery attempts are permitted
70    /// based on the e-mail address entered into the recovery form.
71    /// This can protect against causing e-mail spam to one target.
72    ///
73    /// Note: this limit also applies to re-sends.
74    #[serde(default = "default_account_recovery_per_address")]
75    pub per_address: RateLimiterConfiguration,
76}
77
78#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
79pub struct EmailauthenticationRateLimitingConfig {
80    /// Controls how many email authentication attempts are permitted
81    /// based on the source IP address.
82    /// This can protect against causing e-mail spam to many targets.
83    #[serde(default = "default_email_authentication_per_ip")]
84    pub per_ip: RateLimiterConfiguration,
85
86    /// Controls how many email authentication attempts are permitted
87    /// based on the e-mail address entered into the authentication form.
88    /// This can protect against causing e-mail spam to one target.
89    ///
90    /// Note: this limit also applies to re-sends.
91    #[serde(default = "default_email_authentication_per_address")]
92    pub per_address: RateLimiterConfiguration,
93
94    /// Controls how many authentication emails are permitted to be sent per
95    /// authentication session. This ensures not too many authentication codes
96    /// are created for the same authentication session.
97    #[serde(default = "default_email_authentication_emails_per_session")]
98    pub emails_per_session: RateLimiterConfiguration,
99
100    /// Controls how many code authentication attempts are permitted per
101    /// authentication session. This can protect against brute-forcing the
102    /// code.
103    #[serde(default = "default_email_authentication_attempt_per_session")]
104    pub attempt_per_session: RateLimiterConfiguration,
105}
106
107#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
108pub struct RateLimiterConfiguration {
109    /// A one-off burst of actions that the user can perform
110    /// in one go without waiting.
111    pub burst: NonZeroU32,
112    /// How quickly the allowance replenishes, in number of actions per second.
113    /// Can be fractional to replenish slower.
114    pub per_second: f64,
115}
116
117impl ConfigurationSection for RateLimitingConfig {
118    const PATH: Option<&'static str> = Some("rate_limiting");
119
120    fn validate(&self, figment: &figment::Figment) -> Result<(), figment::Error> {
121        let metadata = figment.find_metadata(Self::PATH.unwrap());
122
123        let error_on_field = |mut error: figment::error::Error, field: &'static str| {
124            error.metadata = metadata.cloned();
125            error.profile = Some(figment::Profile::Default);
126            error.path = vec![Self::PATH.unwrap().to_owned(), field.to_owned()];
127            error
128        };
129
130        let error_on_nested_field =
131            |mut error: figment::error::Error, container: &'static str, field: &'static str| {
132                error.metadata = metadata.cloned();
133                error.profile = Some(figment::Profile::Default);
134                error.path = vec![
135                    Self::PATH.unwrap().to_owned(),
136                    container.to_owned(),
137                    field.to_owned(),
138                ];
139                error
140            };
141
142        // Check one limiter's configuration for errors
143        let error_on_limiter =
144            |limiter: &RateLimiterConfiguration| -> Option<figment::error::Error> {
145                let recip = limiter.per_second.recip();
146                // period must be at least 1 nanosecond according to the governor library
147                if recip < 1.0e-9 || !recip.is_finite() {
148                    return Some(figment::error::Error::custom(
149                        "`per_second` must be a number that is more than zero and less than 1_000_000_000 (1e9)",
150                    ));
151                }
152
153                None
154            };
155
156        if let Some(error) = error_on_limiter(&self.account_recovery.per_ip) {
157            return Err(error_on_nested_field(error, "account_recovery", "per_ip"));
158        }
159        if let Some(error) = error_on_limiter(&self.account_recovery.per_address) {
160            return Err(error_on_nested_field(
161                error,
162                "account_recovery",
163                "per_address",
164            ));
165        }
166
167        if let Some(error) = error_on_limiter(&self.registration) {
168            return Err(error_on_field(error, "registration"));
169        }
170
171        if let Some(error) = error_on_limiter(&self.login.per_ip) {
172            return Err(error_on_nested_field(error, "login", "per_ip"));
173        }
174        if let Some(error) = error_on_limiter(&self.login.per_account) {
175            return Err(error_on_nested_field(error, "login", "per_account"));
176        }
177
178        Ok(())
179    }
180}
181
182impl RateLimitingConfig {
183    pub(crate) fn is_default(config: &RateLimitingConfig) -> bool {
184        config == &RateLimitingConfig::default()
185    }
186}
187
188impl RateLimiterConfiguration {
189    pub fn to_quota(self) -> Option<Quota> {
190        let reciprocal = self.per_second.recip();
191        if !reciprocal.is_finite() {
192            return None;
193        }
194        Some(Quota::with_period(Duration::from_secs_f64(reciprocal))?.allow_burst(self.burst))
195    }
196}
197
198fn default_login_per_ip() -> RateLimiterConfiguration {
199    RateLimiterConfiguration {
200        burst: NonZeroU32::new(3).unwrap(),
201        per_second: 3.0 / 60.0,
202    }
203}
204
205fn default_login_per_account() -> RateLimiterConfiguration {
206    RateLimiterConfiguration {
207        burst: NonZeroU32::new(1800).unwrap(),
208        per_second: 1800.0 / 3600.0,
209    }
210}
211
212fn default_registration() -> RateLimiterConfiguration {
213    RateLimiterConfiguration {
214        burst: NonZeroU32::new(3).unwrap(),
215        per_second: 3.0 / 3600.0,
216    }
217}
218
219fn default_account_recovery_per_ip() -> RateLimiterConfiguration {
220    RateLimiterConfiguration {
221        burst: NonZeroU32::new(3).unwrap(),
222        per_second: 3.0 / 3600.0,
223    }
224}
225
226fn default_account_recovery_per_address() -> RateLimiterConfiguration {
227    RateLimiterConfiguration {
228        burst: NonZeroU32::new(3).unwrap(),
229        per_second: 1.0 / 3600.0,
230    }
231}
232
233fn default_email_authentication_per_ip() -> RateLimiterConfiguration {
234    RateLimiterConfiguration {
235        burst: NonZeroU32::new(5).unwrap(),
236        per_second: 1.0 / 60.0,
237    }
238}
239
240fn default_email_authentication_per_address() -> RateLimiterConfiguration {
241    RateLimiterConfiguration {
242        burst: NonZeroU32::new(3).unwrap(),
243        per_second: 1.0 / 3600.0,
244    }
245}
246
247fn default_email_authentication_emails_per_session() -> RateLimiterConfiguration {
248    RateLimiterConfiguration {
249        burst: NonZeroU32::new(2).unwrap(),
250        per_second: 1.0 / 300.0,
251    }
252}
253
254fn default_email_authentication_attempt_per_session() -> RateLimiterConfiguration {
255    RateLimiterConfiguration {
256        burst: NonZeroU32::new(10).unwrap(),
257        per_second: 1.0 / 60.0,
258    }
259}
260
261impl Default for RateLimitingConfig {
262    fn default() -> Self {
263        RateLimitingConfig {
264            login: LoginRateLimitingConfig::default(),
265            registration: default_registration(),
266            account_recovery: AccountRecoveryRateLimitingConfig::default(),
267            email_authentication: EmailauthenticationRateLimitingConfig::default(),
268        }
269    }
270}
271
272impl Default for LoginRateLimitingConfig {
273    fn default() -> Self {
274        LoginRateLimitingConfig {
275            per_ip: default_login_per_ip(),
276            per_account: default_login_per_account(),
277        }
278    }
279}
280
281impl Default for AccountRecoveryRateLimitingConfig {
282    fn default() -> Self {
283        AccountRecoveryRateLimitingConfig {
284            per_ip: default_account_recovery_per_ip(),
285            per_address: default_account_recovery_per_address(),
286        }
287    }
288}
289
290impl Default for EmailauthenticationRateLimitingConfig {
291    fn default() -> Self {
292        EmailauthenticationRateLimitingConfig {
293            per_ip: default_email_authentication_per_ip(),
294            per_address: default_email_authentication_per_address(),
295            emails_per_session: default_email_authentication_emails_per_session(),
296            attempt_per_session: default_email_authentication_attempt_per_session(),
297        }
298    }
299}