1use 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#[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 pub const EMPTY: Self = Self { ip: None };
86
87 #[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 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#[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 #[must_use]
179 pub fn new(config: &RateLimitingConfig) -> Option<Self> {
180 Some(Self {
181 inner: Arc::new(LimiterInner::new(config)?),
182 })
183 }
184
185 pub fn start(&self) {
190 let this = self.clone();
192 tokio::spawn(async move {
193 let mut interval = tokio::time::interval(Duration::from_mins(1));
195 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
196
197 loop {
198 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 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 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 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 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 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 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 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 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 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 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 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 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 assert!(limiter.check_password(requesters[0], &alice).is_err());
437 assert!(limiter.check_password(requesters[0], &bob).is_err());
439
440 assert!(limiter.check_password(requesters[1], &alice).is_ok());
442
443 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 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 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 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 assert!(limiter.check_device_code_link(bob).is_ok());
477 }
478}