Skip to main content

mas_handlers/
rate_limit.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::{convert::Infallible, future::ready, net::IpAddr, sync::Arc, time::Duration};
9
10use axum::extract::FromRequestParts;
11use governor::{RateLimiter, clock::QuantaClock, state::keyed::DashMapStateStore};
12use mas_config::RateLimitingConfig;
13use mas_data_model::{User, UserEmailAuthentication};
14use ulid::Ulid;
15
16use crate::ClientIp;
17
18#[derive(Debug, Clone, thiserror::Error)]
19pub enum AccountRecoveryLimitedError {
20    #[error("Too many account recovery requests for requester {0}")]
21    Requester(RequesterFingerprint),
22
23    #[error("Too many account recovery requests for e-mail {0}")]
24    Email(String),
25}
26
27#[derive(Debug, Clone, Copy, thiserror::Error)]
28pub enum PasswordCheckLimitedError {
29    #[error("Too many password checks for requester {0}")]
30    Requester(RequesterFingerprint),
31
32    #[error("Too many password checks for user {0}")]
33    User(Ulid),
34}
35
36#[derive(Debug, Clone, thiserror::Error)]
37pub enum RegistrationLimitedError {
38    #[error("Too many account registration requests for requester {0}")]
39    Requester(RequesterFingerprint),
40}
41
42#[derive(Debug, Clone, thiserror::Error)]
43pub enum UsernameAvailabilityLimitedError {
44    #[error("Too many username availability checks for requester {0}")]
45    Requester(RequesterFingerprint),
46}
47
48#[derive(Debug, Clone, thiserror::Error)]
49pub enum EmailAuthenticationLimitedError {
50    #[error("Too many email authentication requests for requester {0}")]
51    Requester(RequesterFingerprint),
52
53    #[error("Too many email authentication requests for authentication session {0}")]
54    Authentication(Ulid),
55
56    #[error("Too many email authentication requests for email {0}")]
57    Email(String),
58}
59
60#[derive(Debug, Clone, thiserror::Error)]
61pub enum DeviceCodeLinkLimitedError {
62    #[error("Too many device code link attempts for requester {0}")]
63    Requester(RequesterFingerprint),
64}
65
66/// Key used to rate limit requests per requester
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
68pub struct RequesterFingerprint {
69    ip: Option<IpAddr>,
70}
71
72impl std::fmt::Display for RequesterFingerprint {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        if let Some(ip) = self.ip {
75            write!(f, "{ip}")
76        } else {
77            write!(f, "(NO CLIENT IP)")
78        }
79    }
80}
81
82impl RequesterFingerprint {
83    /// An anonymous key with no IP address set. This should not be used in
84    /// production, and we should warn users if we can't find their client IPs.
85    pub const EMPTY: Self = Self { ip: None };
86
87    /// Create a new anonymous key with the given IP address
88    #[must_use]
89    pub const fn new(ip: IpAddr) -> Self {
90        Self { ip: Some(ip) }
91    }
92}
93
94impl<S: Send + Sync> FromRequestParts<S> for RequesterFingerprint {
95    type Rejection = Infallible;
96
97    fn from_request_parts(
98        parts: &mut axum::http::request::Parts,
99        _state: &S,
100    ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
101        let ip = parts.extensions.get::<ClientIp>().and_then(|ip| ip.0);
102
103        let fingerprint = if let Some(ip) = ip {
104            Self::new(ip)
105        } else {
106            // If we can't infer the IP address, we'll just use an empty fingerprint and
107            // warn about it
108            tracing::warn!(
109                "Could not infer client IP address for an operation which rate-limits based on IP addresses"
110            );
111            Self::EMPTY
112        };
113
114        ready(Ok(fingerprint))
115    }
116}
117
118/// Rate limiters for the different operations
119#[derive(Debug, Clone)]
120pub struct Limiter {
121    inner: Arc<LimiterInner>,
122}
123
124type KeyedRateLimiter<K> = RateLimiter<K, DashMapStateStore<K>, QuantaClock>;
125
126#[derive(Debug)]
127struct LimiterInner {
128    account_recovery_per_requester: KeyedRateLimiter<RequesterFingerprint>,
129    account_recovery_per_email: KeyedRateLimiter<String>,
130    password_check_for_requester: KeyedRateLimiter<RequesterFingerprint>,
131    password_check_for_user: KeyedRateLimiter<Ulid>,
132    registration_per_requester: KeyedRateLimiter<RequesterFingerprint>,
133    email_authentication_per_requester: KeyedRateLimiter<RequesterFingerprint>,
134    email_authentication_per_email: KeyedRateLimiter<String>,
135    email_authentication_emails_per_session: KeyedRateLimiter<Ulid>,
136    email_authentication_attempt_per_session: KeyedRateLimiter<Ulid>,
137    device_code_link_per_requester: KeyedRateLimiter<RequesterFingerprint>,
138    username_availability_per_requester: KeyedRateLimiter<RequesterFingerprint>,
139}
140
141impl LimiterInner {
142    fn new(config: &RateLimitingConfig) -> Option<Self> {
143        Some(Self {
144            account_recovery_per_requester: RateLimiter::keyed(
145                config.account_recovery.per_ip.to_quota()?,
146            ),
147            account_recovery_per_email: RateLimiter::keyed(
148                config.account_recovery.per_address.to_quota()?,
149            ),
150            password_check_for_requester: RateLimiter::keyed(config.login.per_ip.to_quota()?),
151            password_check_for_user: RateLimiter::keyed(config.login.per_account.to_quota()?),
152            registration_per_requester: RateLimiter::keyed(config.registration.to_quota()?),
153            email_authentication_per_email: RateLimiter::keyed(
154                config.email_authentication.per_address.to_quota()?,
155            ),
156            email_authentication_per_requester: RateLimiter::keyed(
157                config.email_authentication.per_ip.to_quota()?,
158            ),
159            email_authentication_emails_per_session: RateLimiter::keyed(
160                config.email_authentication.emails_per_session.to_quota()?,
161            ),
162            email_authentication_attempt_per_session: RateLimiter::keyed(
163                config.email_authentication.attempt_per_session.to_quota()?,
164            ),
165            device_code_link_per_requester: RateLimiter::keyed(config.device_code_link.to_quota()?),
166            username_availability_per_requester: RateLimiter::keyed(
167                config.username_availability.to_quota()?,
168            ),
169        })
170    }
171}
172
173impl Limiter {
174    /// Creates a new `Limiter` based on a `RateLimitingConfig`.
175    ///
176    /// If the config is not valid, returns `None`.
177    /// (This should not happen if the config was validated, though.)
178    #[must_use]
179    pub fn new(config: &RateLimitingConfig) -> Option<Self> {
180        Some(Self {
181            inner: Arc::new(LimiterInner::new(config)?),
182        })
183    }
184
185    /// Start the rate limiter housekeeping task
186    ///
187    /// This task will periodically remove old entries from the rate limiters,
188    /// to make sure we don't build up a huge number of entries in memory.
189    pub fn start(&self) {
190        // Spawn a task that will periodically clean the rate limiters
191        let this = self.clone();
192        tokio::spawn(async move {
193            // Run the task every minute
194            let mut interval = tokio::time::interval(Duration::from_mins(1));
195            interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
196
197            loop {
198                // Call the retain_recent method on each rate limiter
199                this.inner.account_recovery_per_email.retain_recent();
200                this.inner.account_recovery_per_requester.retain_recent();
201                this.inner.password_check_for_requester.retain_recent();
202                this.inner.password_check_for_user.retain_recent();
203                this.inner.registration_per_requester.retain_recent();
204                this.inner.email_authentication_per_email.retain_recent();
205                this.inner
206                    .email_authentication_per_requester
207                    .retain_recent();
208                this.inner
209                    .email_authentication_emails_per_session
210                    .retain_recent();
211                this.inner
212                    .email_authentication_attempt_per_session
213                    .retain_recent();
214                this.inner.device_code_link_per_requester.retain_recent();
215                this.inner
216                    .username_availability_per_requester
217                    .retain_recent();
218
219                interval.tick().await;
220            }
221        });
222    }
223
224    /// Check if an account recovery can be performed
225    ///
226    /// # Errors
227    ///
228    /// Returns an error if the operation is rate limited.
229    pub fn check_account_recovery(
230        &self,
231        requester: RequesterFingerprint,
232        email_address: &str,
233    ) -> Result<(), AccountRecoveryLimitedError> {
234        self.inner
235            .account_recovery_per_requester
236            .check_key(&requester)
237            .map_err(|_| AccountRecoveryLimitedError::Requester(requester))?;
238
239        // Convert to lowercase to prevent bypassing the limit by enumerating different
240        // case variations.
241        // A case-folding transformation may be more proper.
242        let canonical_email = email_address.to_lowercase();
243        self.inner
244            .account_recovery_per_email
245            .check_key(&canonical_email)
246            .map_err(|_| AccountRecoveryLimitedError::Email(canonical_email))?;
247
248        Ok(())
249    }
250
251    /// Check if a password check can be performed
252    ///
253    /// # Errors
254    ///
255    /// Returns an error if the operation is rate limited
256    pub fn check_password(
257        &self,
258        key: RequesterFingerprint,
259        user: &User,
260    ) -> Result<(), PasswordCheckLimitedError> {
261        self.inner
262            .password_check_for_requester
263            .check_key(&key)
264            .map_err(|_| PasswordCheckLimitedError::Requester(key))?;
265
266        self.inner
267            .password_check_for_user
268            .check_key(&user.id)
269            .map_err(|_| PasswordCheckLimitedError::User(user.id))?;
270
271        Ok(())
272    }
273
274    /// Check if an account registration can be performed
275    ///
276    /// # Errors
277    ///
278    /// Returns an error if the operation is rate limited.
279    pub fn check_registration(
280        &self,
281        requester: RequesterFingerprint,
282    ) -> Result<(), RegistrationLimitedError> {
283        self.inner
284            .registration_per_requester
285            .check_key(&requester)
286            .map_err(|_| RegistrationLimitedError::Requester(requester))?;
287
288        Ok(())
289    }
290
291    /// Check if a username availability check can be performed
292    ///
293    /// # Errors
294    ///
295    /// Returns an error if the operation is rate limited.
296    pub fn check_username_availability(
297        &self,
298        requester: RequesterFingerprint,
299    ) -> Result<(), UsernameAvailabilityLimitedError> {
300        self.inner
301            .username_availability_per_requester
302            .check_key(&requester)
303            .map_err(|_| UsernameAvailabilityLimitedError::Requester(requester))?;
304
305        Ok(())
306    }
307
308    /// Check if an email can be sent to the address for an email
309    /// authentication session
310    ///
311    /// # Errors
312    ///
313    /// Returns an error if the operation is rate limited.
314    pub fn check_email_authentication_email(
315        &self,
316        requester: RequesterFingerprint,
317        email: &str,
318    ) -> Result<(), EmailAuthenticationLimitedError> {
319        self.inner
320            .email_authentication_per_requester
321            .check_key(&requester)
322            .map_err(|_| EmailAuthenticationLimitedError::Requester(requester))?;
323
324        // Convert to lowercase to prevent bypassing the limit by enumerating different
325        // case variations.
326        // A case-folding transformation may be more proper.
327        let canonical_email = email.to_lowercase();
328        self.inner
329            .email_authentication_per_email
330            .check_key(&canonical_email)
331            .map_err(|_| EmailAuthenticationLimitedError::Email(email.to_owned()))?;
332        Ok(())
333    }
334
335    /// Check if an attempt can be done on an email authentication session
336    ///
337    /// # Errors
338    ///
339    /// Returns an error if the operation is rate limited.
340    pub fn check_email_authentication_attempt(
341        &self,
342        authentication: &UserEmailAuthentication,
343    ) -> Result<(), EmailAuthenticationLimitedError> {
344        self.inner
345            .email_authentication_attempt_per_session
346            .check_key(&authentication.id)
347            .map_err(|_| EmailAuthenticationLimitedError::Authentication(authentication.id))
348    }
349
350    /// Check if a new authentication code can be sent for an email
351    /// authentication session
352    ///
353    /// # Errors
354    ///
355    /// Returns an error if the operation is rate limited.
356    pub fn check_email_authentication_send_code(
357        &self,
358        requester: RequesterFingerprint,
359        authentication: &UserEmailAuthentication,
360    ) -> Result<(), EmailAuthenticationLimitedError> {
361        self.check_email_authentication_email(requester, &authentication.email)?;
362        self.inner
363            .email_authentication_emails_per_session
364            .check_key(&authentication.id)
365            .map_err(|_| EmailAuthenticationLimitedError::Authentication(authentication.id))
366    }
367
368    /// Check if a user code can be submitted on the device link page
369    ///
370    /// This protects against brute-forcing the user code of a device code
371    /// grant, as described in RFC 8628 section 5.1.
372    ///
373    /// # Errors
374    ///
375    /// Returns an error if the operation is rate limited.
376    pub fn check_device_code_link(
377        &self,
378        requester: RequesterFingerprint,
379    ) -> Result<(), DeviceCodeLinkLimitedError> {
380        self.inner
381            .device_code_link_per_requester
382            .check_key(&requester)
383            .map_err(|_| DeviceCodeLinkLimitedError::Requester(requester))
384    }
385}
386
387#[cfg(test)]
388mod tests {
389    use mas_data_model::{Clock, UlidExt as _, User, clock::MockClock};
390    use rand::SeedableRng;
391
392    use super::*;
393
394    #[test]
395    fn test_password_check_limiter() {
396        let now = MockClock::default().now();
397        let mut rng = rand_chacha::ChaChaRng::seed_from_u64(42);
398
399        let limiter = Limiter::new(&RateLimitingConfig::default()).unwrap();
400
401        // Let's create a lot of requesters to test account-level rate limiting
402        let requesters: [_; 768] = (0..=255)
403            .flat_map(|a| (0..3).map(move |b| RequesterFingerprint::new([a, a, b, b].into())))
404            .collect::<Vec<_>>()
405            .try_into()
406            .unwrap();
407
408        let alice = User {
409            id: Ulid::from_datetime_with_rng(now, &mut rng),
410            username: "alice".to_owned(),
411            sub: "123-456".to_owned(),
412            created_at: now,
413            locked_at: None,
414            deactivated_at: None,
415            can_request_admin: false,
416            is_guest: true,
417        };
418
419        let bob = User {
420            id: Ulid::from_datetime_with_rng(now, &mut rng),
421            username: "bob".to_owned(),
422            sub: "123-456".to_owned(),
423            created_at: now,
424            locked_at: None,
425            deactivated_at: None,
426            can_request_admin: false,
427            is_guest: true,
428        };
429
430        // Three times the same IP address should be allowed
431        assert!(limiter.check_password(requesters[0], &alice).is_ok());
432        assert!(limiter.check_password(requesters[0], &alice).is_ok());
433        assert!(limiter.check_password(requesters[0], &alice).is_ok());
434
435        // But the fourth time should be rejected
436        assert!(limiter.check_password(requesters[0], &alice).is_err());
437        // Using another user should also be rejected
438        assert!(limiter.check_password(requesters[0], &bob).is_err());
439
440        // Using a different IP address should be allowed, the account isn't locked yet
441        assert!(limiter.check_password(requesters[1], &alice).is_ok());
442
443        // At this point, we consumed 4 cells out of 1800 on alice, let's distribute the
444        // requests with other IPs so that we get rate-limited on the account-level
445        for requester in requesters.iter().skip(2).take(598) {
446            assert!(limiter.check_password(*requester, &alice).is_ok());
447            assert!(limiter.check_password(*requester, &alice).is_ok());
448            assert!(limiter.check_password(*requester, &alice).is_ok());
449            assert!(limiter.check_password(*requester, &alice).is_err());
450        }
451
452        // We now have consumed 4+598*3 = 1798 cells on the account, so we should be
453        // rejected soon
454        assert!(limiter.check_password(requesters[600], &alice).is_ok());
455        assert!(limiter.check_password(requesters[601], &alice).is_ok());
456        assert!(limiter.check_password(requesters[602], &alice).is_err());
457
458        // The other account isn't rate-limited
459        assert!(limiter.check_password(requesters[603], &bob).is_ok());
460    }
461
462    #[test]
463    fn test_device_code_link_limiter() {
464        let limiter = Limiter::new(&RateLimitingConfig::default()).unwrap();
465
466        let alice = RequesterFingerprint::new([1, 2, 3, 4].into());
467        let bob = RequesterFingerprint::new([4, 3, 2, 1].into());
468
469        // The default burst allowance is 10 attempts per requester
470        for _ in 0..10 {
471            assert!(limiter.check_device_code_link(alice).is_ok());
472        }
473        assert!(limiter.check_device_code_link(alice).is_err());
474
475        // Another requester is unaffected
476        assert!(limiter.check_device_code_link(bob).is_ok());
477    }
478}