Skip to main content

mas_config/sections/
rate_limiting.rs

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