Skip to main content

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