You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-08-07 17:03:01 +03:00
Render reCAPTCHA challenge on the registration form
This commit is contained in:
@@ -145,7 +145,8 @@ impl Options {
|
|||||||
&config.matrix,
|
&config.matrix,
|
||||||
&config.experimental,
|
&config.experimental,
|
||||||
&config.passwords,
|
&config.passwords,
|
||||||
);
|
&config.captcha,
|
||||||
|
)?;
|
||||||
|
|
||||||
// Load and compile the templates
|
// Load and compile the templates
|
||||||
let templates =
|
let templates =
|
||||||
|
@@ -15,8 +15,8 @@
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use figment::Figment;
|
use figment::Figment;
|
||||||
use mas_config::{
|
use mas_config::{
|
||||||
BrandingConfig, ConfigurationSection, ExperimentalConfig, MatrixConfig, PasswordsConfig,
|
BrandingConfig, CaptchaConfig, ConfigurationSection, ExperimentalConfig, MatrixConfig,
|
||||||
TemplatesConfig,
|
PasswordsConfig, TemplatesConfig,
|
||||||
};
|
};
|
||||||
use mas_storage::{Clock, SystemClock};
|
use mas_storage::{Clock, SystemClock};
|
||||||
use rand::SeedableRng;
|
use rand::SeedableRng;
|
||||||
@@ -48,6 +48,7 @@ impl Options {
|
|||||||
let matrix_config = MatrixConfig::extract(figment)?;
|
let matrix_config = MatrixConfig::extract(figment)?;
|
||||||
let experimental_config = ExperimentalConfig::extract(figment)?;
|
let experimental_config = ExperimentalConfig::extract(figment)?;
|
||||||
let password_config = PasswordsConfig::extract(figment)?;
|
let password_config = PasswordsConfig::extract(figment)?;
|
||||||
|
let captcha_config = CaptchaConfig::extract(figment)?;
|
||||||
|
|
||||||
let clock = SystemClock::default();
|
let clock = SystemClock::default();
|
||||||
// XXX: we should disallow SeedableRng::from_entropy
|
// XXX: we should disallow SeedableRng::from_entropy
|
||||||
@@ -59,7 +60,8 @@ impl Options {
|
|||||||
&matrix_config,
|
&matrix_config,
|
||||||
&experimental_config,
|
&experimental_config,
|
||||||
&password_config,
|
&password_config,
|
||||||
);
|
&captcha_config,
|
||||||
|
)?;
|
||||||
let templates =
|
let templates =
|
||||||
templates_from_config(&template_config, &site_config, &url_builder).await?;
|
templates_from_config(&template_config, &site_config, &url_builder).await?;
|
||||||
templates.check_render(clock.now(), &mut rng)?;
|
templates.check_render(clock.now(), &mut rng)?;
|
||||||
|
@@ -52,7 +52,8 @@ impl Options {
|
|||||||
&config.matrix,
|
&config.matrix,
|
||||||
&config.experimental,
|
&config.experimental,
|
||||||
&config.passwords,
|
&config.passwords,
|
||||||
);
|
&config.captcha,
|
||||||
|
)?;
|
||||||
|
|
||||||
// Load and compile the templates
|
// Load and compile the templates
|
||||||
let templates =
|
let templates =
|
||||||
|
@@ -16,7 +16,7 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use mas_config::{
|
use mas_config::{
|
||||||
BrandingConfig, DatabaseConfig, EmailConfig, EmailSmtpMode, EmailTransportKind,
|
BrandingConfig, CaptchaConfig, DatabaseConfig, EmailConfig, EmailSmtpMode, EmailTransportKind,
|
||||||
ExperimentalConfig, MatrixConfig, PasswordsConfig, PolicyConfig, TemplatesConfig,
|
ExperimentalConfig, MatrixConfig, PasswordsConfig, PolicyConfig, TemplatesConfig,
|
||||||
};
|
};
|
||||||
use mas_data_model::SiteConfig;
|
use mas_data_model::SiteConfig;
|
||||||
@@ -120,13 +120,39 @@ pub async fn policy_factory_from_config(
|
|||||||
.context("failed to load the policy")
|
.context("failed to load the policy")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn captcha_config_from_config(
|
||||||
|
captcha_config: &CaptchaConfig,
|
||||||
|
) -> Result<Option<mas_data_model::CaptchaConfig>, anyhow::Error> {
|
||||||
|
let Some(service) = captcha_config.service else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let service = match service {
|
||||||
|
mas_config::CaptchaServiceKind::RecaptchaV2 => mas_data_model::CaptchaService::RecaptchaV2,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(mas_data_model::CaptchaConfig {
|
||||||
|
service,
|
||||||
|
site_key: captcha_config
|
||||||
|
.site_key
|
||||||
|
.clone()
|
||||||
|
.context("missing site key")?,
|
||||||
|
secret_key: captcha_config
|
||||||
|
.secret_key
|
||||||
|
.clone()
|
||||||
|
.context("missing secret key")?,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn site_config_from_config(
|
pub fn site_config_from_config(
|
||||||
branding_config: &BrandingConfig,
|
branding_config: &BrandingConfig,
|
||||||
matrix_config: &MatrixConfig,
|
matrix_config: &MatrixConfig,
|
||||||
experimental_config: &ExperimentalConfig,
|
experimental_config: &ExperimentalConfig,
|
||||||
password_config: &PasswordsConfig,
|
password_config: &PasswordsConfig,
|
||||||
) -> SiteConfig {
|
captcha_config: &CaptchaConfig,
|
||||||
SiteConfig {
|
) -> Result<SiteConfig, anyhow::Error> {
|
||||||
|
let captcha = captcha_config_from_config(captcha_config)?;
|
||||||
|
Ok(SiteConfig {
|
||||||
access_token_ttl: experimental_config.access_token_ttl,
|
access_token_ttl: experimental_config.access_token_ttl,
|
||||||
compat_token_ttl: experimental_config.compat_token_ttl,
|
compat_token_ttl: experimental_config.compat_token_ttl,
|
||||||
server_name: matrix_config.homeserver.clone(),
|
server_name: matrix_config.homeserver.clone(),
|
||||||
@@ -140,7 +166,8 @@ pub fn site_config_from_config(
|
|||||||
displayname_change_allowed: experimental_config.displayname_change_allowed,
|
displayname_change_allowed: experimental_config.displayname_change_allowed,
|
||||||
password_change_allowed: password_config.enabled()
|
password_change_allowed: password_config.enabled()
|
||||||
&& experimental_config.password_change_allowed,
|
&& experimental_config.password_change_allowed,
|
||||||
}
|
captcha,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn templates_from_config(
|
pub async fn templates_from_config(
|
||||||
|
80
crates/config/src/sections/captcha.rs
Normal file
80
crates/config/src/sections/captcha.rs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::{de::Error, Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::ConfigurationSection;
|
||||||
|
|
||||||
|
/// Which service should be used for CAPTCHA protection
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize, JsonSchema, Serialize)]
|
||||||
|
pub enum CaptchaServiceKind {
|
||||||
|
/// Use Google's reCAPTCHA v2 API
|
||||||
|
#[serde(rename = "recaptcha_v2")]
|
||||||
|
RecaptchaV2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configuration section to setup CAPTCHA protection on a few operations
|
||||||
|
#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize, Default)]
|
||||||
|
pub struct CaptchaConfig {
|
||||||
|
/// Which service should be used for CAPTCHA protection
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub service: Option<CaptchaServiceKind>,
|
||||||
|
|
||||||
|
/// The site key to use
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub site_key: Option<String>,
|
||||||
|
|
||||||
|
/// The secret key to use
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub secret_key: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CaptchaConfig {
|
||||||
|
/// Returns true if the configuration is the default one
|
||||||
|
pub(crate) fn is_default(&self) -> bool {
|
||||||
|
self.service.is_none() && self.site_key.is_none() && self.secret_key.is_none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConfigurationSection for CaptchaConfig {
|
||||||
|
const PATH: Option<&'static str> = Some("captcha");
|
||||||
|
|
||||||
|
fn validate(&self, figment: &figment::Figment) -> Result<(), figment::Error> {
|
||||||
|
let metadata = figment.find_metadata(Self::PATH.unwrap());
|
||||||
|
|
||||||
|
let error_on_field = |mut error: figment::error::Error, field: &'static str| {
|
||||||
|
error.metadata = metadata.cloned();
|
||||||
|
error.profile = Some(figment::Profile::Default);
|
||||||
|
error.path = vec![Self::PATH.unwrap().to_owned(), field.to_owned()];
|
||||||
|
error
|
||||||
|
};
|
||||||
|
|
||||||
|
let missing_field = |field: &'static str| {
|
||||||
|
error_on_field(figment::error::Error::missing_field(field), field)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(CaptchaServiceKind::RecaptchaV2) = self.service {
|
||||||
|
if self.site_key.is_none() {
|
||||||
|
return Err(missing_field("site_key"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.secret_key.is_none() {
|
||||||
|
return Err(missing_field("secret_key"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@@ -17,6 +17,7 @@ use schemars::JsonSchema;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
mod branding;
|
mod branding;
|
||||||
|
mod captcha;
|
||||||
mod clients;
|
mod clients;
|
||||||
mod database;
|
mod database;
|
||||||
mod email;
|
mod email;
|
||||||
@@ -32,6 +33,7 @@ mod upstream_oauth2;
|
|||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
branding::BrandingConfig,
|
branding::BrandingConfig,
|
||||||
|
captcha::{CaptchaConfig, CaptchaServiceKind},
|
||||||
clients::{ClientAuthMethodConfig, ClientConfig, ClientsConfig},
|
clients::{ClientAuthMethodConfig, ClientConfig, ClientsConfig},
|
||||||
database::DatabaseConfig,
|
database::DatabaseConfig,
|
||||||
email::{EmailConfig, EmailSmtpMode, EmailTransportKind},
|
email::{EmailConfig, EmailSmtpMode, EmailTransportKind},
|
||||||
@@ -107,6 +109,10 @@ pub struct RootConfig {
|
|||||||
#[serde(default, skip_serializing_if = "BrandingConfig::is_default")]
|
#[serde(default, skip_serializing_if = "BrandingConfig::is_default")]
|
||||||
pub branding: BrandingConfig,
|
pub branding: BrandingConfig,
|
||||||
|
|
||||||
|
/// Configuration section to setup CAPTCHA protection on a few operations
|
||||||
|
#[serde(default, skip_serializing_if = "CaptchaConfig::is_default")]
|
||||||
|
pub captcha: CaptchaConfig,
|
||||||
|
|
||||||
/// Experimental configuration options
|
/// Experimental configuration options
|
||||||
#[serde(default, skip_serializing_if = "ExperimentalConfig::is_default")]
|
#[serde(default, skip_serializing_if = "ExperimentalConfig::is_default")]
|
||||||
pub experimental: ExperimentalConfig,
|
pub experimental: ExperimentalConfig,
|
||||||
@@ -126,6 +132,7 @@ impl ConfigurationSection for RootConfig {
|
|||||||
self.policy.validate(figment)?;
|
self.policy.validate(figment)?;
|
||||||
self.upstream_oauth2.validate(figment)?;
|
self.upstream_oauth2.validate(figment)?;
|
||||||
self.branding.validate(figment)?;
|
self.branding.validate(figment)?;
|
||||||
|
self.captcha.validate(figment)?;
|
||||||
self.experimental.validate(figment)?;
|
self.experimental.validate(figment)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -155,6 +162,7 @@ impl RootConfig {
|
|||||||
policy: PolicyConfig::default(),
|
policy: PolicyConfig::default(),
|
||||||
upstream_oauth2: UpstreamOAuth2Config::default(),
|
upstream_oauth2: UpstreamOAuth2Config::default(),
|
||||||
branding: BrandingConfig::default(),
|
branding: BrandingConfig::default(),
|
||||||
|
captcha: CaptchaConfig::default(),
|
||||||
experimental: ExperimentalConfig::default(),
|
experimental: ExperimentalConfig::default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -175,6 +183,7 @@ impl RootConfig {
|
|||||||
policy: PolicyConfig::default(),
|
policy: PolicyConfig::default(),
|
||||||
upstream_oauth2: UpstreamOAuth2Config::default(),
|
upstream_oauth2: UpstreamOAuth2Config::default(),
|
||||||
branding: BrandingConfig::default(),
|
branding: BrandingConfig::default(),
|
||||||
|
captcha: CaptchaConfig::default(),
|
||||||
experimental: ExperimentalConfig::default(),
|
experimental: ExperimentalConfig::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,6 +218,9 @@ pub struct AppConfig {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub branding: BrandingConfig,
|
pub branding: BrandingConfig,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub captcha: CaptchaConfig,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub experimental: ExperimentalConfig,
|
pub experimental: ExperimentalConfig,
|
||||||
}
|
}
|
||||||
@@ -224,6 +236,7 @@ impl ConfigurationSection for AppConfig {
|
|||||||
self.matrix.validate(figment)?;
|
self.matrix.validate(figment)?;
|
||||||
self.policy.validate(figment)?;
|
self.policy.validate(figment)?;
|
||||||
self.branding.validate(figment)?;
|
self.branding.validate(figment)?;
|
||||||
|
self.captcha.validate(figment)?;
|
||||||
self.experimental.validate(figment)?;
|
self.experimental.validate(figment)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@@ -40,7 +40,7 @@ pub use self::{
|
|||||||
AuthorizationCode, AuthorizationGrant, AuthorizationGrantStage, Client, DeviceCodeGrant,
|
AuthorizationCode, AuthorizationGrant, AuthorizationGrantStage, Client, DeviceCodeGrant,
|
||||||
DeviceCodeGrantState, InvalidRedirectUriError, JwksOrJwksUri, Pkce, Session, SessionState,
|
DeviceCodeGrantState, InvalidRedirectUriError, JwksOrJwksUri, Pkce, Session, SessionState,
|
||||||
},
|
},
|
||||||
site_config::SiteConfig,
|
site_config::{CaptchaConfig, CaptchaService, SiteConfig},
|
||||||
tokens::{
|
tokens::{
|
||||||
AccessToken, AccessTokenState, RefreshToken, RefreshTokenState, TokenFormatError, TokenType,
|
AccessToken, AccessTokenState, RefreshToken, RefreshTokenState, TokenFormatError, TokenType,
|
||||||
},
|
},
|
||||||
|
@@ -15,6 +15,25 @@
|
|||||||
use chrono::Duration;
|
use chrono::Duration;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
/// Which Captcha service is being used
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum CaptchaService {
|
||||||
|
RecaptchaV2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Captcha configuration
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CaptchaConfig {
|
||||||
|
/// Which Captcha service is being used
|
||||||
|
pub service: CaptchaService,
|
||||||
|
|
||||||
|
/// The site key used by the instance
|
||||||
|
pub site_key: String,
|
||||||
|
|
||||||
|
/// The secret key used by the instance
|
||||||
|
pub secret_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// Random site configuration we want accessible in various places.
|
/// Random site configuration we want accessible in various places.
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -51,4 +70,7 @@ pub struct SiteConfig {
|
|||||||
|
|
||||||
/// Whether users can change their password.
|
/// Whether users can change their password.
|
||||||
pub password_change_allowed: bool,
|
pub password_change_allowed: bool,
|
||||||
|
|
||||||
|
/// Captcha configuration
|
||||||
|
pub captcha: Option<CaptchaConfig>,
|
||||||
}
|
}
|
||||||
|
@@ -132,6 +132,7 @@ pub fn test_site_config() -> SiteConfig {
|
|||||||
email_change_allowed: true,
|
email_change_allowed: true,
|
||||||
displayname_change_allowed: true,
|
displayname_change_allowed: true,
|
||||||
password_change_allowed: true,
|
password_change_allowed: true,
|
||||||
|
captcha: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -26,7 +26,7 @@ use mas_axum_utils::{
|
|||||||
csrf::{CsrfExt, CsrfToken, ProtectedForm},
|
csrf::{CsrfExt, CsrfToken, ProtectedForm},
|
||||||
FancyError, SessionInfoExt,
|
FancyError, SessionInfoExt,
|
||||||
};
|
};
|
||||||
use mas_data_model::UserAgent;
|
use mas_data_model::{CaptchaConfig, UserAgent};
|
||||||
use mas_i18n::DataLocale;
|
use mas_i18n::DataLocale;
|
||||||
use mas_matrix::BoxHomeserverConnection;
|
use mas_matrix::BoxHomeserverConnection;
|
||||||
use mas_policy::Policy;
|
use mas_policy::Policy;
|
||||||
@@ -96,6 +96,7 @@ pub(crate) async fn get(
|
|||||||
csrf_token,
|
csrf_token,
|
||||||
&mut repo,
|
&mut repo,
|
||||||
&templates,
|
&templates,
|
||||||
|
site_config.captcha.clone(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -216,6 +217,7 @@ pub(crate) async fn post(
|
|||||||
csrf_token,
|
csrf_token,
|
||||||
&mut repo,
|
&mut repo,
|
||||||
&templates,
|
&templates,
|
||||||
|
site_config.captcha.clone(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -278,6 +280,7 @@ async fn render(
|
|||||||
csrf_token: CsrfToken,
|
csrf_token: CsrfToken,
|
||||||
repo: &mut impl RepositoryAccess,
|
repo: &mut impl RepositoryAccess,
|
||||||
templates: &Templates,
|
templates: &Templates,
|
||||||
|
captcha_config: Option<CaptchaConfig>,
|
||||||
) -> Result<String, FancyError> {
|
) -> Result<String, FancyError> {
|
||||||
let next = action.load_context(repo).await?;
|
let next = action.load_context(repo).await?;
|
||||||
let ctx = if let Some(next) = next {
|
let ctx = if let Some(next) = next {
|
||||||
@@ -285,7 +288,10 @@ async fn render(
|
|||||||
} else {
|
} else {
|
||||||
ctx
|
ctx
|
||||||
};
|
};
|
||||||
let ctx = ctx.with_csrf(csrf_token.form_value()).with_language(locale);
|
let ctx = ctx
|
||||||
|
.with_captcha(captcha_config)
|
||||||
|
.with_csrf(csrf_token.form_value())
|
||||||
|
.with_language(locale);
|
||||||
|
|
||||||
let content = templates.render_register(&ctx)?;
|
let content = templates.render_register(&ctx)?;
|
||||||
Ok(content)
|
Ok(content)
|
||||||
|
@@ -15,6 +15,7 @@
|
|||||||
//! Contexts used in templates
|
//! Contexts used in templates
|
||||||
|
|
||||||
mod branding;
|
mod branding;
|
||||||
|
mod captcha;
|
||||||
mod ext;
|
mod ext;
|
||||||
mod features;
|
mod features;
|
||||||
|
|
||||||
@@ -41,7 +42,9 @@ use serde::{ser::SerializeStruct, Deserialize, Serialize};
|
|||||||
use ulid::Ulid;
|
use ulid::Ulid;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
pub use self::{branding::SiteBranding, ext::SiteConfigExt, features::SiteFeatures};
|
pub use self::{
|
||||||
|
branding::SiteBranding, captcha::WithCaptcha, ext::SiteConfigExt, features::SiteFeatures,
|
||||||
|
};
|
||||||
use crate::{FieldError, FormField, FormState};
|
use crate::{FieldError, FormField, FormState};
|
||||||
|
|
||||||
/// Helper trait to construct context wrappers
|
/// Helper trait to construct context wrappers
|
||||||
@@ -95,6 +98,14 @@ pub trait TemplateContext: Serialize {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Attach a CAPTCHA configuration to the template context
|
||||||
|
fn with_captcha(self, captcha: Option<mas_data_model::CaptchaConfig>) -> WithCaptcha<Self>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
WithCaptcha::new(captcha, self)
|
||||||
|
}
|
||||||
|
|
||||||
/// Generate sample values for this context type
|
/// Generate sample values for this context type
|
||||||
///
|
///
|
||||||
/// This is then used to check for template validity in unit tests and in
|
/// This is then used to check for template validity in unit tests and in
|
||||||
|
77
crates/templates/src/context/captcha.rs
Normal file
77
crates/templates/src/context/captcha.rs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use minijinja::{
|
||||||
|
value::{Enumerator, Object},
|
||||||
|
Value,
|
||||||
|
};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::TemplateContext;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct CaptchaConfig(mas_data_model::CaptchaConfig);
|
||||||
|
|
||||||
|
impl Object for CaptchaConfig {
|
||||||
|
fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
|
||||||
|
match key.as_str() {
|
||||||
|
Some("service") => Some(match &self.0.service {
|
||||||
|
mas_data_model::CaptchaService::RecaptchaV2 => "recaptcha_v2".into(),
|
||||||
|
}),
|
||||||
|
Some("site_key") => Some(self.0.site_key.clone().into()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enumerate(self: &Arc<Self>) -> Enumerator {
|
||||||
|
Enumerator::Str(&["service", "site_key"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Context with an optional CAPTCHA configuration in it
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct WithCaptcha<T> {
|
||||||
|
captcha: Option<Value>,
|
||||||
|
|
||||||
|
#[serde(flatten)]
|
||||||
|
inner: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> WithCaptcha<T> {
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(captcha: Option<mas_data_model::CaptchaConfig>, inner: T) -> Self {
|
||||||
|
Self {
|
||||||
|
captcha: captcha.map(|captcha| Value::from_object(CaptchaConfig(captcha))),
|
||||||
|
inner,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: TemplateContext> TemplateContext for WithCaptcha<T> {
|
||||||
|
fn sample(
|
||||||
|
now: chrono::DateTime<chrono::prelude::Utc>,
|
||||||
|
rng: &mut impl rand::prelude::Rng,
|
||||||
|
) -> Vec<Self>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
let inner = T::sample(now, rng);
|
||||||
|
inner
|
||||||
|
.into_iter()
|
||||||
|
.map(|inner| Self::new(None, inner))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
@@ -22,6 +22,7 @@ use std::{collections::HashSet, sync::Arc};
|
|||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
use arc_swap::ArcSwap;
|
use arc_swap::ArcSwap;
|
||||||
use camino::{Utf8Path, Utf8PathBuf};
|
use camino::{Utf8Path, Utf8PathBuf};
|
||||||
|
use context::WithCaptcha;
|
||||||
use mas_i18n::Translator;
|
use mas_i18n::Translator;
|
||||||
use mas_router::UrlBuilder;
|
use mas_router::UrlBuilder;
|
||||||
use mas_spa::ViteManifest;
|
use mas_spa::ViteManifest;
|
||||||
@@ -326,7 +327,7 @@ register_templates! {
|
|||||||
pub fn render_login(WithLanguage<WithCsrf<LoginContext>>) { "pages/login.html" }
|
pub fn render_login(WithLanguage<WithCsrf<LoginContext>>) { "pages/login.html" }
|
||||||
|
|
||||||
/// Render the registration page
|
/// Render the registration page
|
||||||
pub fn render_register(WithLanguage<WithCsrf<RegisterContext>>) { "pages/register.html" }
|
pub fn render_register(WithLanguage<WithCsrf<WithCaptcha<RegisterContext>>>) { "pages/register.html" }
|
||||||
|
|
||||||
/// Render the client consent page
|
/// Render the client consent page
|
||||||
pub fn render_consent(WithLanguage<WithCsrf<WithSession<ConsentContext>>>) { "pages/consent.html" }
|
pub fn render_consent(WithLanguage<WithCsrf<WithSession<ConsentContext>>>) { "pages/consent.html" }
|
||||||
|
@@ -184,6 +184,14 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"captcha": {
|
||||||
|
"description": "Configuration section to setup CAPTCHA protection on a few operations",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/CaptchaConfig"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"experimental": {
|
"experimental": {
|
||||||
"description": "Experimental configuration options",
|
"description": "Experimental configuration options",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
@@ -1949,6 +1957,40 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"CaptchaConfig": {
|
||||||
|
"description": "Configuration section to setup CAPTCHA protection on a few operations",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"service": {
|
||||||
|
"description": "Which service should be used for CAPTCHA protection",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/CaptchaServiceKind"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"site_key": {
|
||||||
|
"description": "The site key to use",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"secret_key": {
|
||||||
|
"description": "The secret key to use",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CaptchaServiceKind": {
|
||||||
|
"description": "Which service should be used for CAPTCHA protection",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"description": "Use Google's reCAPTCHA v2 API",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"recaptcha_v2"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"ExperimentalConfig": {
|
"ExperimentalConfig": {
|
||||||
"description": "Configuration sections for experimental options\n\nDo not change these options unless you know what you are doing.",
|
"description": "Configuration sections for experimental options\n\nDo not change these options unless you know what you are doing.",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
@@ -23,6 +23,7 @@ limitations under the License.
|
|||||||
{% import "components/errors.html" as errors %}
|
{% import "components/errors.html" as errors %}
|
||||||
{% import "components/icon.html" as icon %}
|
{% import "components/icon.html" as icon %}
|
||||||
{% import "components/scope.html" as scope %}
|
{% import "components/scope.html" as scope %}
|
||||||
|
{% import "components/captcha.html" as captcha %}
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ lang }}">
|
<html lang="{{ lang }}">
|
||||||
@@ -31,6 +32,7 @@ limitations under the License.
|
|||||||
<title>{% block title %}{{ _("app.name") }}{% endblock title %}</title>
|
<title>{% block title %}{{ _("app.name") }}{% endblock title %}</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
{{ include_asset('src/templates.css', preload=true) | indent(4) | safe }}
|
{{ include_asset('src/templates.css', preload=true) | indent(4) | safe }}
|
||||||
|
{{ captcha.head() }}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="layout-container">
|
<div class="layout-container">
|
||||||
|
35
templates/components/captcha.html
Normal file
35
templates/components/captcha.html
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{#
|
||||||
|
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
#}
|
||||||
|
|
||||||
|
{% macro form() -%}
|
||||||
|
{%- if captcha|default(False) -%}
|
||||||
|
{%- if captcha.service == "recaptcha_v2" -%}
|
||||||
|
<div class="g-recaptcha" data-sitekey="{{ captcha.site_key }}"></div>
|
||||||
|
{%- else -%}
|
||||||
|
{{ throw(message="Invalid captcha service setup") }}
|
||||||
|
{%- endif %}
|
||||||
|
{%- endif -%}
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro head() -%}
|
||||||
|
{%- if captcha|default(False) -%}
|
||||||
|
{%- if captcha.service == "recaptcha_v2" -%}
|
||||||
|
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
|
||||||
|
{%- else -%}
|
||||||
|
{{ throw(message="Invalid captcha service setup") }}
|
||||||
|
{%- endif %}
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endmacro %}
|
@@ -69,6 +69,8 @@ limitations under the License.
|
|||||||
{% endcall %}
|
{% endcall %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{{ captcha.form() }}
|
||||||
|
|
||||||
{{ button.button(text=_("action.continue")) }}
|
{{ button.button(text=_("action.continue")) }}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
@@ -2,11 +2,11 @@
|
|||||||
"action": {
|
"action": {
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"@cancel": {
|
"@cancel": {
|
||||||
"context": "pages/consent.html:75:11-29, pages/device_consent.html:132:13-31, pages/login.html:100:13-31, pages/policy_violation.html:52:13-31, pages/register.html:77:13-31"
|
"context": "pages/consent.html:75:11-29, pages/device_consent.html:132:13-31, pages/login.html:100:13-31, pages/policy_violation.html:52:13-31, pages/register.html:79:13-31"
|
||||||
},
|
},
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"@continue": {
|
"@continue": {
|
||||||
"context": "pages/account/emails/add.html:45:26-46, pages/account/emails/verify.html:60:26-46, pages/consent.html:63:28-48, pages/device_consent.html:129:13-33, pages/device_link.html:48:26-46, pages/login.html:62:30-50, pages/reauth.html:40:28-48, pages/register.html:72:28-48, pages/sso.html:45:28-48"
|
"context": "pages/account/emails/add.html:45:26-46, pages/account/emails/verify.html:60:26-46, pages/consent.html:63:28-48, pages/device_consent.html:129:13-33, pages/device_link.html:48:26-46, pages/login.html:62:30-50, pages/reauth.html:40:28-48, pages/register.html:74:28-48, pages/sso.html:45:28-48"
|
||||||
},
|
},
|
||||||
"create_account": "Create Account",
|
"create_account": "Create Account",
|
||||||
"@create_account": {
|
"@create_account": {
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
},
|
},
|
||||||
"name": "matrix-authentication-service",
|
"name": "matrix-authentication-service",
|
||||||
"@name": {
|
"@name": {
|
||||||
"context": "app.html:25:14-27, base.html:31:31-44",
|
"context": "app.html:25:14-27, base.html:32:31-44",
|
||||||
"description": "Name of the application"
|
"description": "Name of the application"
|
||||||
},
|
},
|
||||||
"technical_description": "OpenID Connect discovery document: <a class=\"cpd-link\" data-kind=\"primary\" href=\"%(discovery_url)s\">%(discovery_url)s</a>",
|
"technical_description": "OpenID Connect discovery document: <a class=\"cpd-link\" data-kind=\"primary\" href=\"%(discovery_url)s\">%(discovery_url)s</a>",
|
||||||
@@ -349,7 +349,7 @@
|
|||||||
"register": {
|
"register": {
|
||||||
"call_to_login": "Already have an account?",
|
"call_to_login": "Already have an account?",
|
||||||
"@call_to_login": {
|
"@call_to_login": {
|
||||||
"context": "pages/register.html:87:11-42",
|
"context": "pages/register.html:89:11-42",
|
||||||
"description": "Displayed on the registration page to suggest to log in instead"
|
"description": "Displayed on the registration page to suggest to log in instead"
|
||||||
},
|
},
|
||||||
"create_account": {
|
"create_account": {
|
||||||
@@ -364,7 +364,7 @@
|
|||||||
},
|
},
|
||||||
"sign_in_instead": "Sign in instead",
|
"sign_in_instead": "Sign in instead",
|
||||||
"@sign_in_instead": {
|
"@sign_in_instead": {
|
||||||
"context": "pages/register.html:91:31-64"
|
"context": "pages/register.html:93:31-64"
|
||||||
},
|
},
|
||||||
"terms_of_service": "I agree to the <a href=\"%s\" data-kind=\"primary\" class=\"cpd-link\">Terms and Conditions</a>",
|
"terms_of_service": "I agree to the <a href=\"%s\" data-kind=\"primary\" class=\"cpd-link\">Terms and Conditions</a>",
|
||||||
"@terms_of_service": {
|
"@terms_of_service": {
|
||||||
|
Reference in New Issue
Block a user