Rate-limit email authentications
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
@@ -18,13 +18,19 @@ pub struct RateLimitingConfig {
|
||||
/// Account Recovery-specific rate limits
|
||||
#[serde(default)]
|
||||
pub account_recovery: AccountRecoveryRateLimitingConfig,
|
||||
|
||||
/// Login-specific rate limits
|
||||
#[serde(default)]
|
||||
pub login: LoginRateLimitingConfig,
|
||||
|
||||
/// Controls how many registrations attempts are permitted
|
||||
/// based on source address.
|
||||
#[serde(default = "default_registration")]
|
||||
pub registration: RateLimiterConfiguration,
|
||||
|
||||
/// Email authentication-specific rate limits
|
||||
#[serde(default)]
|
||||
pub email_authentication: EmailauthenticationRateLimitingConfig,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
@@ -37,6 +43,7 @@ pub struct LoginRateLimitingConfig {
|
||||
/// change their own password.
|
||||
#[serde(default = "default_login_per_ip")]
|
||||
pub per_ip: RateLimiterConfiguration,
|
||||
|
||||
/// Controls how many login attempts are permitted
|
||||
/// based on the account that is being attempted to be logged into.
|
||||
/// This can protect against a distributed brute force attack
|
||||
@@ -58,6 +65,7 @@ pub struct AccountRecoveryRateLimitingConfig {
|
||||
/// Note: this limit also applies to re-sends.
|
||||
#[serde(default = "default_account_recovery_per_ip")]
|
||||
pub per_ip: RateLimiterConfiguration,
|
||||
|
||||
/// Controls how many account recovery attempts are permitted
|
||||
/// based on the e-mail address entered into the recovery form.
|
||||
/// This can protect against causing e-mail spam to one target.
|
||||
@@ -67,6 +75,35 @@ pub struct AccountRecoveryRateLimitingConfig {
|
||||
pub per_address: RateLimiterConfiguration,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct EmailauthenticationRateLimitingConfig {
|
||||
/// Controls how many email authentication attempts are permitted
|
||||
/// based on the source IP address.
|
||||
/// This can protect against causing e-mail spam to many targets.
|
||||
#[serde(default = "default_email_authentication_per_ip")]
|
||||
pub per_ip: RateLimiterConfiguration,
|
||||
|
||||
/// Controls how many email authentication attempts are permitted
|
||||
/// based on the e-mail address entered into the authentication form.
|
||||
/// This can protect against causing e-mail spam to one target.
|
||||
///
|
||||
/// Note: this limit also applies to re-sends.
|
||||
#[serde(default = "default_email_authentication_per_address")]
|
||||
pub per_address: RateLimiterConfiguration,
|
||||
|
||||
/// Controls how many authentication emails are permitted to be sent per
|
||||
/// authentication session. This ensures not too many authentication codes
|
||||
/// are created for the same authentication session.
|
||||
#[serde(default = "default_email_authentication_emails_per_session")]
|
||||
pub emails_per_session: RateLimiterConfiguration,
|
||||
|
||||
/// Controls how many code authentication attempts are permitted per
|
||||
/// authentication session. This can protect against brute-forcing the
|
||||
/// code.
|
||||
#[serde(default = "default_email_authentication_attempt_per_session")]
|
||||
pub attempt_per_session: RateLimiterConfiguration,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct RateLimiterConfiguration {
|
||||
/// A one-off burst of actions that the user can perform
|
||||
@@ -193,12 +230,41 @@ fn default_account_recovery_per_address() -> RateLimiterConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
fn default_email_authentication_per_ip() -> RateLimiterConfiguration {
|
||||
RateLimiterConfiguration {
|
||||
burst: NonZeroU32::new(5).unwrap(),
|
||||
per_second: 1.0 / 60.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn default_email_authentication_per_address() -> RateLimiterConfiguration {
|
||||
RateLimiterConfiguration {
|
||||
burst: NonZeroU32::new(3).unwrap(),
|
||||
per_second: 1.0 / 3600.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn default_email_authentication_emails_per_session() -> RateLimiterConfiguration {
|
||||
RateLimiterConfiguration {
|
||||
burst: NonZeroU32::new(2).unwrap(),
|
||||
per_second: 1.0 / 300.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn default_email_authentication_attempt_per_session() -> RateLimiterConfiguration {
|
||||
RateLimiterConfiguration {
|
||||
burst: NonZeroU32::new(10).unwrap(),
|
||||
per_second: 1.0 / 60.0,
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RateLimitingConfig {
|
||||
fn default() -> Self {
|
||||
RateLimitingConfig {
|
||||
login: LoginRateLimitingConfig::default(),
|
||||
registration: default_registration(),
|
||||
account_recovery: AccountRecoveryRateLimitingConfig::default(),
|
||||
email_authentication: EmailauthenticationRateLimitingConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -220,3 +286,14 @@ impl Default for AccountRecoveryRateLimitingConfig {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EmailauthenticationRateLimitingConfig {
|
||||
fn default() -> Self {
|
||||
EmailauthenticationRateLimitingConfig {
|
||||
per_ip: default_email_authentication_per_ip(),
|
||||
per_address: default_email_authentication_per_address(),
|
||||
emails_per_session: default_email_authentication_emails_per_session(),
|
||||
attempt_per_session: default_email_authentication_attempt_per_session(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,6 +238,8 @@ enum StartEmailAuthenticationStatus {
|
||||
Started,
|
||||
/// The email address is invalid
|
||||
InvalidEmailAddress,
|
||||
/// Too many attempts to start an email authentication
|
||||
RateLimited,
|
||||
/// The email address isn't allowed by the policy
|
||||
Denied,
|
||||
/// The email address is already in use
|
||||
@@ -249,6 +251,7 @@ enum StartEmailAuthenticationStatus {
|
||||
enum StartEmailAuthenticationPayload {
|
||||
Started(UserEmailAuthentication),
|
||||
InvalidEmailAddress,
|
||||
RateLimited,
|
||||
Denied {
|
||||
violations: Vec<mas_policy::Violation>,
|
||||
},
|
||||
@@ -262,6 +265,7 @@ impl StartEmailAuthenticationPayload {
|
||||
match self {
|
||||
Self::Started(_) => StartEmailAuthenticationStatus::Started,
|
||||
Self::InvalidEmailAddress => StartEmailAuthenticationStatus::InvalidEmailAddress,
|
||||
Self::RateLimited => StartEmailAuthenticationStatus::RateLimited,
|
||||
Self::Denied { .. } => StartEmailAuthenticationStatus::Denied,
|
||||
Self::InUse => StartEmailAuthenticationStatus::InUse,
|
||||
}
|
||||
@@ -271,7 +275,9 @@ impl StartEmailAuthenticationPayload {
|
||||
async fn authentication(&self) -> Option<&UserEmailAuthentication> {
|
||||
match self {
|
||||
Self::Started(authentication) => Some(authentication),
|
||||
Self::InvalidEmailAddress | Self::Denied { .. } | Self::InUse => None,
|
||||
Self::InvalidEmailAddress | Self::RateLimited | Self::Denied { .. } | Self::InUse => {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,6 +308,7 @@ enum CompleteEmailAuthenticationPayload {
|
||||
Completed,
|
||||
InvalidCode,
|
||||
CodeExpired,
|
||||
RateLimited,
|
||||
}
|
||||
|
||||
/// The status of the `completeEmailAuthentication` mutation
|
||||
@@ -313,6 +320,8 @@ enum CompleteEmailAuthenticationStatus {
|
||||
InvalidCode,
|
||||
/// The authentication code has expired
|
||||
CodeExpired,
|
||||
/// Too many attempts to complete an email authentication
|
||||
RateLimited,
|
||||
}
|
||||
|
||||
#[Object(use_type_description)]
|
||||
@@ -323,6 +332,7 @@ impl CompleteEmailAuthenticationPayload {
|
||||
Self::Completed => CompleteEmailAuthenticationStatus::Completed,
|
||||
Self::InvalidCode => CompleteEmailAuthenticationStatus::InvalidCode,
|
||||
Self::CodeExpired => CompleteEmailAuthenticationStatus::CodeExpired,
|
||||
Self::RateLimited => CompleteEmailAuthenticationStatus::RateLimited,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -345,6 +355,8 @@ enum ResendEmailAuthenticationCodePayload {
|
||||
Resent,
|
||||
/// The email authentication session is already completed
|
||||
Completed,
|
||||
/// Too many attempts to resend an email authentication code
|
||||
RateLimited,
|
||||
}
|
||||
|
||||
/// The status of the `resendEmailAuthenticationCode` mutation
|
||||
@@ -354,6 +366,8 @@ enum ResendEmailAuthenticationCodeStatus {
|
||||
Resent,
|
||||
/// The email authentication session is already completed
|
||||
Completed,
|
||||
/// Too many attempts to resend an email authentication code
|
||||
RateLimited,
|
||||
}
|
||||
|
||||
#[Object(use_type_description)]
|
||||
@@ -363,6 +377,7 @@ impl ResendEmailAuthenticationCodePayload {
|
||||
match self {
|
||||
Self::Resent => ResendEmailAuthenticationCodeStatus::Resent,
|
||||
Self::Completed => ResendEmailAuthenticationCodeStatus::Completed,
|
||||
Self::RateLimited => ResendEmailAuthenticationCodeStatus::RateLimited,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -536,6 +551,7 @@ impl UserEmailMutations {
|
||||
let mut rng = state.rng();
|
||||
let clock = state.clock();
|
||||
let requester = ctx.requester();
|
||||
let limiter = state.limiter();
|
||||
|
||||
// Only allow calling this if the requester is a browser session
|
||||
let Some(browser_session) = requester.browser_session() else {
|
||||
@@ -563,7 +579,12 @@ impl UserEmailMutations {
|
||||
return Ok(StartEmailAuthenticationPayload::InvalidEmailAddress);
|
||||
}
|
||||
|
||||
// TODO: check rate limting
|
||||
if let Err(e) =
|
||||
limiter.check_email_authentication_email(ctx.requester_fingerprint(), &input.email)
|
||||
{
|
||||
tracing::warn!(error = &e as &dyn std::error::Error);
|
||||
return Ok(StartEmailAuthenticationPayload::RateLimited);
|
||||
}
|
||||
|
||||
let mut repo = state.repository().await?;
|
||||
|
||||
@@ -615,6 +636,7 @@ impl UserEmailMutations {
|
||||
let state = ctx.state();
|
||||
let mut rng = state.rng();
|
||||
let clock = state.clock();
|
||||
let limiter = state.limiter();
|
||||
|
||||
let id = NodeType::UserEmailAuthentication.extract_ulid(&input.id)?;
|
||||
let Some(browser_session) = ctx.requester().browser_session() else {
|
||||
@@ -647,6 +669,13 @@ impl UserEmailMutations {
|
||||
return Ok(ResendEmailAuthenticationCodePayload::Completed);
|
||||
}
|
||||
|
||||
if let Err(e) = limiter
|
||||
.check_email_authentication_send_code(ctx.requester_fingerprint(), &authentication)
|
||||
{
|
||||
tracing::warn!(error = &e as &dyn std::error::Error);
|
||||
return Ok(ResendEmailAuthenticationCodePayload::RateLimited);
|
||||
}
|
||||
|
||||
repo.queue_job()
|
||||
.schedule_job(
|
||||
&mut rng,
|
||||
@@ -669,6 +698,7 @@ impl UserEmailMutations {
|
||||
let state = ctx.state();
|
||||
let mut rng = state.rng();
|
||||
let clock = state.clock();
|
||||
let limiter = state.limiter();
|
||||
|
||||
let id = NodeType::UserEmailAuthentication.extract_ulid(&input.id)?;
|
||||
|
||||
@@ -695,6 +725,11 @@ impl UserEmailMutations {
|
||||
return Ok(CompleteEmailAuthenticationPayload::InvalidCode);
|
||||
}
|
||||
|
||||
if let Err(e) = limiter.check_email_authentication_attempt(&authentication) {
|
||||
tracing::warn!(error = &e as &dyn std::error::Error);
|
||||
return Ok(CompleteEmailAuthenticationPayload::RateLimited);
|
||||
}
|
||||
|
||||
let Some(code) = repo
|
||||
.user_email()
|
||||
.find_authentication_code(&authentication, &input.code)
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::{net::IpAddr, sync::Arc, time::Duration};
|
||||
|
||||
use governor::{clock::QuantaClock, state::keyed::DashMapStateStore, RateLimiter};
|
||||
use mas_config::RateLimitingConfig;
|
||||
use mas_data_model::User;
|
||||
use mas_data_model::{User, UserEmailAuthentication};
|
||||
use ulid::Ulid;
|
||||
|
||||
#[derive(Debug, Clone, thiserror::Error)]
|
||||
@@ -35,6 +35,18 @@ pub enum RegistrationLimitedError {
|
||||
Requester(RequesterFingerprint),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, thiserror::Error)]
|
||||
pub enum EmailAuthenticationLimitedError {
|
||||
#[error("Too many email authentication requests for requester {0}")]
|
||||
Requester(RequesterFingerprint),
|
||||
|
||||
#[error("Too many email authentication requests for authentication session {0}")]
|
||||
Authentication(Ulid),
|
||||
|
||||
#[error("Too many email authentication requests for email {0}")]
|
||||
Email(String),
|
||||
}
|
||||
|
||||
/// Key used to rate limit requests per requester
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct RequesterFingerprint {
|
||||
@@ -78,6 +90,10 @@ struct LimiterInner {
|
||||
password_check_for_requester: KeyedRateLimiter<RequesterFingerprint>,
|
||||
password_check_for_user: KeyedRateLimiter<Ulid>,
|
||||
registration_per_requester: KeyedRateLimiter<RequesterFingerprint>,
|
||||
email_authentication_per_requester: KeyedRateLimiter<RequesterFingerprint>,
|
||||
email_authentication_per_email: KeyedRateLimiter<String>,
|
||||
email_authentication_emails_per_session: KeyedRateLimiter<Ulid>,
|
||||
email_authentication_attempt_per_session: KeyedRateLimiter<Ulid>,
|
||||
}
|
||||
|
||||
impl LimiterInner {
|
||||
@@ -92,6 +108,18 @@ impl LimiterInner {
|
||||
password_check_for_requester: RateLimiter::keyed(config.login.per_ip.to_quota()?),
|
||||
password_check_for_user: RateLimiter::keyed(config.login.per_account.to_quota()?),
|
||||
registration_per_requester: RateLimiter::keyed(config.registration.to_quota()?),
|
||||
email_authentication_per_email: RateLimiter::keyed(
|
||||
config.email_authentication.per_address.to_quota()?,
|
||||
),
|
||||
email_authentication_per_requester: RateLimiter::keyed(
|
||||
config.email_authentication.per_ip.to_quota()?,
|
||||
),
|
||||
email_authentication_emails_per_session: RateLimiter::keyed(
|
||||
config.email_authentication.emails_per_session.to_quota()?,
|
||||
),
|
||||
email_authentication_attempt_per_session: RateLimiter::keyed(
|
||||
config.email_authentication.attempt_per_session.to_quota()?,
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -127,6 +155,16 @@ impl Limiter {
|
||||
this.inner.password_check_for_requester.retain_recent();
|
||||
this.inner.password_check_for_user.retain_recent();
|
||||
this.inner.registration_per_requester.retain_recent();
|
||||
this.inner.email_authentication_per_email.retain_recent();
|
||||
this.inner
|
||||
.email_authentication_per_requester
|
||||
.retain_recent();
|
||||
this.inner
|
||||
.email_authentication_emails_per_session
|
||||
.retain_recent();
|
||||
this.inner
|
||||
.email_authentication_attempt_per_session
|
||||
.retain_recent();
|
||||
|
||||
interval.tick().await;
|
||||
}
|
||||
@@ -199,6 +237,66 @@ impl Limiter {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if an email can be sent to the address for an email
|
||||
/// authentication session
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the operation is rate limited.
|
||||
pub fn check_email_authentication_email(
|
||||
&self,
|
||||
requester: RequesterFingerprint,
|
||||
email: &str,
|
||||
) -> Result<(), EmailAuthenticationLimitedError> {
|
||||
self.inner
|
||||
.email_authentication_per_requester
|
||||
.check_key(&requester)
|
||||
.map_err(|_| EmailAuthenticationLimitedError::Requester(requester))?;
|
||||
|
||||
// Convert to lowercase to prevent bypassing the limit by enumerating different
|
||||
// case variations.
|
||||
// A case-folding transformation may be more proper.
|
||||
let canonical_email = email.to_lowercase();
|
||||
self.inner
|
||||
.email_authentication_per_email
|
||||
.check_key(&canonical_email)
|
||||
.map_err(|_| EmailAuthenticationLimitedError::Email(email.to_owned()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if an attempt can be done on an email authentication session
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the operation is rate limited.
|
||||
pub fn check_email_authentication_attempt(
|
||||
&self,
|
||||
authentication: &UserEmailAuthentication,
|
||||
) -> Result<(), EmailAuthenticationLimitedError> {
|
||||
self.inner
|
||||
.email_authentication_attempt_per_session
|
||||
.check_key(&authentication.id)
|
||||
.map_err(|_| EmailAuthenticationLimitedError::Authentication(authentication.id))
|
||||
}
|
||||
|
||||
/// Check if a new authentication code can be sent for an email
|
||||
/// authentication session
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the operation is rate limited.
|
||||
pub fn check_email_authentication_send_code(
|
||||
&self,
|
||||
requester: RequesterFingerprint,
|
||||
authentication: &UserEmailAuthentication,
|
||||
) -> Result<(), EmailAuthenticationLimitedError> {
|
||||
self.check_email_authentication_email(requester, &authentication.email)?;
|
||||
self.inner
|
||||
.email_authentication_emails_per_session
|
||||
.check_key(&authentication.id)
|
||||
.map_err(|_| EmailAuthenticationLimitedError::Authentication(authentication.id))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -286,6 +286,11 @@ pub(crate) async fn post(
|
||||
tracing::warn!(error = &e as &dyn std::error::Error);
|
||||
state.add_error_on_form(FormError::RateLimitExceeded);
|
||||
}
|
||||
|
||||
if let Err(e) = limiter.check_email_authentication_email(requester, &form.email) {
|
||||
tracing::warn!(error = &e as &dyn std::error::Error);
|
||||
state.add_error_on_form(FormError::RateLimitExceeded);
|
||||
}
|
||||
}
|
||||
|
||||
state
|
||||
|
||||
@@ -22,7 +22,7 @@ use mas_templates::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{views::shared::OptionalPostAuthAction, PreferredLanguage};
|
||||
use crate::{views::shared::OptionalPostAuthAction, Limiter, PreferredLanguage};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct CodeForm {
|
||||
@@ -111,6 +111,7 @@ pub(crate) async fn post(
|
||||
mut rng: BoxRng,
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
State(templates): State<Templates>,
|
||||
State(limiter): State<Limiter>,
|
||||
mut repo: BoxRepository,
|
||||
cookie_jar: CookieJar,
|
||||
State(url_builder): State<UrlBuilder>,
|
||||
@@ -157,6 +158,22 @@ pub(crate) async fn post(
|
||||
)));
|
||||
}
|
||||
|
||||
if let Err(e) = limiter.check_email_authentication_attempt(&email_authentication) {
|
||||
tracing::warn!(error = &e as &dyn std::error::Error);
|
||||
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
|
||||
let ctx = RegisterStepsVerifyEmailContext::new(email_authentication)
|
||||
.with_form_state(
|
||||
form.to_form_state()
|
||||
.with_error_on_form(mas_templates::FormError::RateLimitExceeded),
|
||||
)
|
||||
.with_csrf(csrf_token.form_value())
|
||||
.with_language(locale);
|
||||
|
||||
let content = templates.render_register_steps_verify_email(&ctx)?;
|
||||
|
||||
return Ok((cookie_jar, Html(content)).into_response());
|
||||
}
|
||||
|
||||
let Some(code) = repo
|
||||
.user_email()
|
||||
.find_authentication_code(&email_authentication, &form.code)
|
||||
|
||||
@@ -1715,6 +1715,32 @@
|
||||
"$ref": "#/definitions/RateLimiterConfiguration"
|
||||
}
|
||||
]
|
||||
},
|
||||
"email_authentication": {
|
||||
"description": "Email authentication-specific rate limits",
|
||||
"default": {
|
||||
"per_ip": {
|
||||
"burst": 5,
|
||||
"per_second": 0.016666666666666666
|
||||
},
|
||||
"per_address": {
|
||||
"burst": 3,
|
||||
"per_second": 0.0002777777777777778
|
||||
},
|
||||
"emails_per_session": {
|
||||
"burst": 2,
|
||||
"per_second": 0.0033333333333333335
|
||||
},
|
||||
"attempt_per_session": {
|
||||
"burst": 10,
|
||||
"per_second": 0.016666666666666666
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/EmailauthenticationRateLimitingConfig"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1796,6 +1822,59 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"EmailauthenticationRateLimitingConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"per_ip": {
|
||||
"description": "Controls how many email authentication attempts are permitted based on the source IP address. This can protect against causing e-mail spam to many targets.",
|
||||
"default": {
|
||||
"burst": 5,
|
||||
"per_second": 0.016666666666666666
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/RateLimiterConfiguration"
|
||||
}
|
||||
]
|
||||
},
|
||||
"per_address": {
|
||||
"description": "Controls how many email authentication attempts are permitted based on the e-mail address entered into the authentication form. This can protect against causing e-mail spam to one target.\n\nNote: this limit also applies to re-sends.",
|
||||
"default": {
|
||||
"burst": 3,
|
||||
"per_second": 0.0002777777777777778
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/RateLimiterConfiguration"
|
||||
}
|
||||
]
|
||||
},
|
||||
"emails_per_session": {
|
||||
"description": "Controls how many authentication emails are permitted to be sent per authentication session. This ensures not too many authentication codes are created for the same authentication session.",
|
||||
"default": {
|
||||
"burst": 2,
|
||||
"per_second": 0.0033333333333333335
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/RateLimiterConfiguration"
|
||||
}
|
||||
]
|
||||
},
|
||||
"attempt_per_session": {
|
||||
"description": "Controls how many code authentication attempts are permitted per authentication session. This can protect against brute-forcing the code.",
|
||||
"default": {
|
||||
"burst": 10,
|
||||
"per_second": 0.016666666666666666
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/RateLimiterConfiguration"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"UpstreamOAuth2Config": {
|
||||
"description": "Upstream OAuth 2.0 providers configuration",
|
||||
"type": "object",
|
||||
|
||||
@@ -524,6 +524,10 @@ enum CompleteEmailAuthenticationStatus {
|
||||
The authentication code has expired
|
||||
"""
|
||||
CODE_EXPIRED
|
||||
"""
|
||||
Too many attempts to complete an email authentication
|
||||
"""
|
||||
RATE_LIMITED
|
||||
}
|
||||
|
||||
"""
|
||||
@@ -1265,6 +1269,10 @@ enum ResendEmailAuthenticationCodeStatus {
|
||||
The email authentication session is already completed
|
||||
"""
|
||||
COMPLETED
|
||||
"""
|
||||
Too many attempts to resend an email authentication code
|
||||
"""
|
||||
RATE_LIMITED
|
||||
}
|
||||
|
||||
"""
|
||||
@@ -1634,6 +1642,10 @@ enum StartEmailAuthenticationStatus {
|
||||
"""
|
||||
INVALID_EMAIL_ADDRESS
|
||||
"""
|
||||
Too many attempts to start an email authentication
|
||||
"""
|
||||
RATE_LIMITED
|
||||
"""
|
||||
The email address isn't allowed by the policy
|
||||
"""
|
||||
DENIED
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
@@ -61,7 +61,7 @@ const AddEmailForm: React.FC<{
|
||||
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const email = formData.get("input") as string;
|
||||
addEmail.mutate({ email, language: i18n.languages[0] });
|
||||
await addEmail.mutateAsync({ email, language: i18n.languages[0] });
|
||||
};
|
||||
|
||||
const status = addEmail.data?.startEmailAuthentication.status ?? null;
|
||||
@@ -93,6 +93,10 @@ const AddEmailForm: React.FC<{
|
||||
</ErrorMessage>
|
||||
)}
|
||||
|
||||
{status === "RATE_LIMITED" && (
|
||||
<ErrorMessage>{t("frontend.errors.rate_limit_exceeded")}</ErrorMessage>
|
||||
)}
|
||||
|
||||
{status === "DENIED" && (
|
||||
<>
|
||||
<ErrorMessage>
|
||||
|
||||
@@ -348,7 +348,9 @@ export type CompleteEmailAuthenticationStatus =
|
||||
/** The authentication was completed */
|
||||
| 'COMPLETED'
|
||||
/** The authentication code is invalid */
|
||||
| 'INVALID_CODE';
|
||||
| 'INVALID_CODE'
|
||||
/** Too many attempts to complete an email authentication */
|
||||
| 'RATE_LIMITED';
|
||||
|
||||
/** The input of the `createOauth2Session` mutation. */
|
||||
export type CreateOAuth2SessionInput = {
|
||||
@@ -972,6 +974,8 @@ export type ResendEmailAuthenticationCodePayload = {
|
||||
export type ResendEmailAuthenticationCodeStatus =
|
||||
/** The email authentication session is already completed */
|
||||
| 'COMPLETED'
|
||||
/** Too many attempts to resend an email authentication code */
|
||||
| 'RATE_LIMITED'
|
||||
/** The email was resent */
|
||||
| 'RESENT';
|
||||
|
||||
@@ -1205,6 +1209,8 @@ export type StartEmailAuthenticationStatus =
|
||||
| 'INVALID_EMAIL_ADDRESS'
|
||||
/** The email address is already in use */
|
||||
| 'IN_USE'
|
||||
/** Too many attempts to start an email authentication */
|
||||
| 'RATE_LIMITED'
|
||||
/** The email address was started */
|
||||
| 'STARTED';
|
||||
|
||||
|
||||
@@ -99,6 +99,8 @@ function EmailVerify(): React.ReactElement {
|
||||
"RESENT";
|
||||
const invalidCode =
|
||||
verifyEmail.data?.completeEmailAuthentication.status === "INVALID_CODE";
|
||||
const rateLimited =
|
||||
verifyEmail.data?.completeEmailAuthentication.status === "RATE_LIMITED";
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
@@ -133,9 +135,16 @@ function EmailVerify(): React.ReactElement {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{rateLimited && (
|
||||
<Alert
|
||||
type="critical"
|
||||
title={t("frontend.errors.rate_limit_exceeded")}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Form.Field
|
||||
name="code"
|
||||
serverInvalid={invalidCode}
|
||||
serverInvalid={invalidCode || rateLimited}
|
||||
className="self-center mb-4"
|
||||
>
|
||||
<Form.Label>{t("frontend.verify_email.code_field_label")}</Form.Label>
|
||||
|
||||
Reference in New Issue
Block a user