From 47af5d3a3967734fa28b2384c88a5397d15d496f Mon Sep 17 00:00:00 2001 From: Ginger Date: Wed, 6 May 2026 11:03:44 -0400 Subject: [PATCH] refactor: Change template context to allow using a CSP nonce --- src/web/mod.rs | 11 ++-- src/web/pages/account/cross_signing_reset.rs | 9 ++-- src/web/pages/account/deactivate.rs | 12 +++-- src/web/pages/account/device.rs | 12 +++-- src/web/pages/account/email.rs | 44 ++++++++------- src/web/pages/account/login.rs | 7 +-- src/web/pages/account/mod.rs | 12 +++-- src/web/pages/account/password/change.rs | 12 +++-- src/web/pages/account/password/reset.rs | 22 ++++---- src/web/pages/components/form.rs | 18 +++++-- src/web/pages/index.rs | 11 ++-- src/web/pages/mod.rs | 54 +++++++++++++++---- src/web/pages/oauth/grant.rs | 7 +-- .../pages/templates/_components/form.html.j2 | 2 +- .../templates/cross_signing_reset.html.j2 | 2 +- src/web/pages/threepid.rs | 7 +-- 16 files changed, 154 insertions(+), 88 deletions(-) diff --git a/src/web/mod.rs b/src/web/mod.rs index b4cdbf6ae..64225d0d7 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -4,11 +4,12 @@ use askama::Template; use axum::{ Router, extract::rejection::{FormRejection, PathRejection, QueryRejection}, - http::{HeaderValue, StatusCode, header}, + http::StatusCode, + middleware::from_fn_with_state, response::{Html, IntoResponse, Redirect, Response}, }; use conduwuit_service::{Services, state}; -use tower_http::{catch_panic::CatchPanicLayer, set_header::SetResponseHeaderLayer}; +use tower_http::catch_panic::CatchPanicLayer; use tower_sec_fetch::SecFetchLayer; use tower_sessions::{ExpiredDeletion, SessionManagerLayer, cookie::SameSite}; @@ -95,6 +96,7 @@ impl IntoResponse for WebError { context: TemplateContext { // Statically set false to prevent error pages from being indexed. allow_indexing: false, + csp_nonce: String::new(), }, }; @@ -150,10 +152,7 @@ pub fn build(services: &Services) -> Router { WebError::Panic(details).into_response() })) - .layer(SetResponseHeaderLayer::if_not_present( - header::CONTENT_SECURITY_POLICY, - HeaderValue::from_static("default-src 'self'; img-src 'self' https: data:;"), - )) + .layer(from_fn_with_state(services.config.clone(), template_context_middleware)) .layer(SecFetchLayer::new(|policy| { policy.allow_safe_methods().reject_missing_metadata(); })) diff --git a/src/web/pages/account/cross_signing_reset.rs b/src/web/pages/account/cross_signing_reset.rs index b6391b0e8..412dfc768 100644 --- a/src/web/pages/account/cross_signing_reset.rs +++ b/src/web/pages/account/cross_signing_reset.rs @@ -1,9 +1,9 @@ -use axum::{Router, extract::State, routing::on}; +use axum::{Extension, Router, extract::State, routing::on}; use conduwuit_service::oauth::OAuthTicket; use crate::{ extract::PostForm, - pages::{GET_POST, Result, components::UserCard}, + pages::{GET_POST, Result, TemplateContext, components::UserCard}, response, session::{LoginTarget, User}, template, @@ -28,6 +28,7 @@ enum CrossSigningResetBody { async fn route_cross_signing_reset( State(services): State, + Extension(context): Extension, user: User, PostForm(form): PostForm<()>, ) -> Result { @@ -39,8 +40,8 @@ async fn route_cross_signing_reset( .oauth .issue_ticket(user_id.localpart().to_owned(), OAuthTicket::CrossSigningReset); - response!(CrossSigningReset::new(&services, user_card, CrossSigningResetBody::Success)) + response!(CrossSigningReset::new(context, user_card, CrossSigningResetBody::Success)) } else { - response!(CrossSigningReset::new(&services, user_card, CrossSigningResetBody::Form)) + response!(CrossSigningReset::new(context, user_card, CrossSigningResetBody::Form)) } } diff --git a/src/web/pages/account/deactivate.rs b/src/web/pages/account/deactivate.rs index 905f08154..01e6279f7 100644 --- a/src/web/pages/account/deactivate.rs +++ b/src/web/pages/account/deactivate.rs @@ -1,4 +1,4 @@ -use axum::{Router, extract::State, routing::on}; +use axum::{Extension, Router, extract::State, routing::on}; use conduwuit_api::client::full_user_deactivate; use futures::StreamExt; use ruma::{OwnedRoomId, OwnedUserId, UserId}; @@ -9,7 +9,7 @@ use crate::{ extract::PostForm, form, pages::{ - GET_POST, Result, + GET_POST, Result, TemplateContext, components::{UserCard, form::Form}, }, response, @@ -28,6 +28,7 @@ template! { } #[derive(Debug)] +#[allow(clippy::large_enum_variant)] enum DeactivateBody { Form { user_id: OwnedUserId, @@ -57,6 +58,7 @@ form! { async fn route_deactivate( State(services): State, + Extension(context): Extension, user: User, session: Session, PostForm(form): PostForm, @@ -70,7 +72,7 @@ async fn route_deactivate( DeactivateBody::Form { user_id, user_card, - form: DeactivateForm::with_errors(err), + form: DeactivateForm::with_errors(context.clone(), err), } } else { let all_joined_rooms: Vec = services @@ -90,12 +92,12 @@ async fn route_deactivate( DeactivateBody::Form { user_id, user_card, - form: DeactivateForm::build(), + form: DeactivateForm::build(context.clone()), } } }; - response!(Deactivate::new(&services, body)) + response!(Deactivate::new(context, body)) } async fn validate_deactivate_form( diff --git a/src/web/pages/account/device.rs b/src/web/pages/account/device.rs index 49f271786..05d602664 100644 --- a/src/web/pages/account/device.rs +++ b/src/web/pages/account/device.rs @@ -1,5 +1,5 @@ use axum::{ - Router, + Extension, Router, extract::{Path, State}, routing::{get, on}, }; @@ -12,7 +12,7 @@ use crate::{ WebError, extract::{Expect, PostForm}, pages::{ - GET_POST, Result, + GET_POST, Result, TemplateContext, components::{ClientScopes, DeviceCard, DeviceCardStyle}, }, response, @@ -35,6 +35,7 @@ template! { async fn get_device_info( State(services): State, + Extension(context): Extension, user: User, Expect(Path(query)): Expect>, ) -> Result { @@ -65,7 +66,7 @@ async fn get_device_info( let device_card = DeviceCard::for_device(&services, &user_id, device, DeviceCardStyle::Detailed).await; - response!(DeviceInfo::new(&services, device_card, client_metadata)) + response!(DeviceInfo::new(context, device_card, client_metadata)) } template! { @@ -90,6 +91,7 @@ pub(crate) struct DevicePath { async fn route_remove_device( State(services): State, + Extension(context): Extension, user: User, Expect(Path(query)): Expect>, PostForm(form): PostForm<()>, @@ -110,13 +112,13 @@ async fn route_remove_device( .remove_device(&user_id, &device.device_id) .await; - response!(RemoveDevice::new(&services, RemoveDeviceBody::Success)) + response!(RemoveDevice::new(context, RemoveDeviceBody::Success)) } else { let device_card = DeviceCard::for_device(&services, &user_id, device, DeviceCardStyle::Minimal).await; let last_device = services.users.all_devices_metadata(&user_id).count().await <= 1; - response!(RemoveDevice::new(&services, RemoveDeviceBody::Form { + response!(RemoveDevice::new(context, RemoveDeviceBody::Form { device_card: Box::new(device_card), last_device })) diff --git a/src/web/pages/account/email.rs b/src/web/pages/account/email.rs index 7dbbc634e..b68564e70 100644 --- a/src/web/pages/account/email.rs +++ b/src/web/pages/account/email.rs @@ -1,5 +1,5 @@ use axum::{ - Router, + Extension, Router, extract::{Query, State}, routing::{get, on, post}, }; @@ -14,7 +14,7 @@ use crate::{ extract::{Expect, PostForm}, form, pages::{ - GET_POST, Result, + GET_POST, Result, TemplateContext, account::ThreepidQuery, components::{UserCard, form::Form}, }, @@ -75,25 +75,24 @@ enum ChangeEmailBody { async fn route_change_email_request( State(services): State, + Extension(context): Extension, user: User, PostForm(form): PostForm, ) -> Result { let user_id = user.expect_recent(LoginTarget::ChangeEmail)?; - let template = ChangeEmailRequest::new( - &services, - UserCard::for_local_user(&services, user_id.clone()).await, - services - .threepid - .get_email_for_localpart(user_id.localpart()) - .await - .map(|address| address.to_string()), - ChangeEmailRequestForm::build(), - services.threepid.email_requirement().may_remove(), - ); - let Some(form) = form else { - return response!(template); + return response!(ChangeEmailRequest::new( + context.clone(), + UserCard::for_local_user(&services, user_id.clone()).await, + services + .threepid + .get_email_for_localpart(user_id.localpart()) + .await + .map(|address| address.to_string()), + ChangeEmailRequestForm::build(context), + services.threepid.email_requirement().may_remove(), + )); }; let client_secret = ClientSecret::new(); @@ -130,7 +129,7 @@ async fn route_change_email_request( }; response!(ChangeEmail::new( - &services, + context, UserCard::for_local_user(&services, user_id).await, ChangeEmailBody::ValidationPending { session_id, @@ -148,6 +147,7 @@ struct ChangeEmailQuery { async fn get_change_email( State(services): State, + Extension(context): Extension, Expect(Query(ChangeEmailQuery { threepid: ThreepidQuery { client_secret, session_id }, })): Expect>, @@ -166,7 +166,7 @@ async fn get_change_email( .await else { return response!(ChangeEmail::new( - &services, + context, user_card, ChangeEmailBody::ValidationPending { session_id, @@ -186,10 +186,14 @@ async fn get_change_email( return response!(BadRequest(err.message())); } - response!(ChangeEmail::new(&services, user_card, ChangeEmailBody::Success)) + response!(ChangeEmail::new(context, user_card, ChangeEmailBody::Success)) } -async fn post_delete_email(State(services): State, user: User) -> Result { +async fn post_delete_email( + State(services): State, + Extension(context): Extension, + user: User, +) -> Result { let user_id = user.expect(LoginTarget::ChangeEmail)?; let user_card = UserCard::for_local_user(&services, user_id.clone()).await; @@ -202,5 +206,5 @@ async fn post_delete_email(State(services): State, user: User) -> .disassociate_localpart_email(user_id.localpart()) .await; - response!(DeleteEmail::new(&services, user_card)) + response!(DeleteEmail::new(context, user_card)) } diff --git a/src/web/pages/account/login.rs b/src/web/pages/account/login.rs index 368c08345..d3e50bbd5 100644 --- a/src/web/pages/account/login.rs +++ b/src/web/pages/account/login.rs @@ -1,7 +1,7 @@ use std::time::SystemTime; use axum::{ - Router, + Extension, Router, extract::{Query, RawQuery, State}, response::{IntoResponse, Redirect}, routing::{get, on}, @@ -17,7 +17,7 @@ use tower_sessions::Session; use crate::{ ROUTE_PREFIX, WebError, extract::{Expect, PostForm}, - pages::{GET_POST, Result, components::UserCard}, + pages::{GET_POST, Result, TemplateContext, components::UserCard}, response, session::{LoginQuery, LoginTarget, User, UserSession}, template, @@ -55,6 +55,7 @@ struct LoginForm { async fn route_login( State(services): State, + Extension(context): Extension, Expect(Query(LoginQuery { next, reauthenticate })): Expect>, session_store: Session, user: User, @@ -78,7 +79,7 @@ async fn route_login( }, }; - let mut template = Login::new(&services, body, next != LoginTarget::Account, None); + let mut template = Login::new(context, body, next != LoginTarget::Account, None); if let Some(form) = form { let login_result = match (user_id, form.identifier) { diff --git a/src/web/pages/account/mod.rs b/src/web/pages/account/mod.rs index 781893a1d..10dccb6d1 100644 --- a/src/web/pages/account/mod.rs +++ b/src/web/pages/account/mod.rs @@ -1,5 +1,5 @@ use axum::{ - Router, + Extension, Router, extract::{Query, State}, response::Redirect, routing::get, @@ -17,7 +17,7 @@ use crate::{ WebError, extract::Expect, pages::{ - Result, + Result, TemplateContext, components::{DeviceCard, DeviceCardStyle, UserCard}, }, response, @@ -62,7 +62,11 @@ template! { } } -async fn get_account(State(services): State, user: User) -> Result { +async fn get_account( + State(services): State, + Extension(context): Extension, + user: User, +) -> Result { let user_id = user.expect(LoginTarget::Account)?; let email_requirement = services.threepid.email_requirement(); @@ -105,7 +109,7 @@ async fn get_account(State(services): State, user: User) -> Result .collect() .await; - response!(Account::new(&services, user_card, email_requirement, email, device_cards)) + response!(Account::new(context, user_card, email_requirement, email, device_cards)) } #[derive(Deserialize)] diff --git a/src/web/pages/account/password/change.rs b/src/web/pages/account/password/change.rs index 30b9d2725..f484b72c5 100644 --- a/src/web/pages/account/password/change.rs +++ b/src/web/pages/account/password/change.rs @@ -1,4 +1,4 @@ -use axum::{Router, extract::State, routing::on}; +use axum::{Extension, Router, extract::State, routing::on}; use conduwuit_service::users::HashedPassword; use ruma::UserId; use validator::{Validate, ValidationError, ValidationErrors}; @@ -7,7 +7,7 @@ use crate::{ extract::PostForm, form, pages::{ - GET_POST, Result, + GET_POST, Result, TemplateContext, components::{UserCard, form::Form}, }, response, @@ -61,6 +61,7 @@ form! { async fn route_change_password( State(services): State, + Extension(context): Extension, user: User, PostForm(form): PostForm, ) -> Result { @@ -70,13 +71,14 @@ async fn route_change_password( let body = if let Some(form) = form { match change_password(&services, &user_id, form).await { | Ok(()) => ChangePasswordBody::Success, - | Err(errors) => ChangePasswordBody::Form(ChangePasswordForm::with_errors(errors)), + | Err(errors) => + ChangePasswordBody::Form(ChangePasswordForm::with_errors(context.clone(), errors)), } } else { - ChangePasswordBody::Form(ChangePasswordForm::build()) + ChangePasswordBody::Form(ChangePasswordForm::build(context.clone())) }; - response!(ChangePassword::new(&services, user_card, body)) + response!(ChangePassword::new(context, user_card, body)) } async fn change_password( diff --git a/src/web/pages/account/password/reset.rs b/src/web/pages/account/password/reset.rs index 18f9bc0fa..0cd6b5ffe 100644 --- a/src/web/pages/account/password/reset.rs +++ b/src/web/pages/account/password/reset.rs @@ -1,5 +1,5 @@ use axum::{ - Router, + Extension, Router, extract::{Query, State}, routing::on, }; @@ -17,7 +17,7 @@ use crate::{ extract::{Expect, PostForm}, form, pages::{ - GET_POST, Result, + GET_POST, Result, TemplateContext, account::ThreepidQuery, components::{UserCard, form::Form}, }, @@ -57,12 +57,13 @@ form! { async fn route_reset_password_request( State(services): State, + Extension(context): Extension, PostForm(form): PostForm, ) -> Result { // Check if SMTP is configured if services.mailer.mailer().is_none() { return response!(ResetPasswordRequest::new( - &services, + context, ResetPasswordRequestBody::Unavailable )); } @@ -70,8 +71,8 @@ async fn route_reset_password_request( let Some(form) = form else { // For GET requests return the reset request form return response!(ResetPasswordRequest::new( - &services, - ResetPasswordRequestBody::Form(ResetPasswordRequestForm::build()) + context.clone(), + ResetPasswordRequestBody::Form(ResetPasswordRequestForm::build(context)) )); }; @@ -117,7 +118,7 @@ async fn route_reset_password_request( ValidationSessions::generate_session_id() }); - response!(ResetPassword::new(&services, ResetPasswordBody::ValidationPending { + response!(ResetPassword::new(context, ResetPasswordBody::ValidationPending { client_secret, session_id, validation_error: false @@ -174,6 +175,7 @@ struct ResetPasswordQuery { async fn route_reset_password( State(services): State, + Extension(context): Extension, Expect(Query(query)): Expect>, PostForm(form): PostForm, ) -> Result { @@ -203,7 +205,7 @@ async fn route_reset_password( if let Err(err) = form.validate() { ResetPasswordBody::ValidationSuccess { user_card, - form: ResetPasswordForm::with_errors(err), + form: ResetPasswordForm::with_errors(context.clone(), err), } } else { match HashedPassword::new(&form.new_password) { @@ -225,7 +227,7 @@ async fn route_reset_password( ResetPasswordBody::ValidationSuccess { user_card, - form: ResetPasswordForm::with_errors(errors), + form: ResetPasswordForm::with_errors(context.clone(), errors), } }, } @@ -233,7 +235,7 @@ async fn route_reset_password( } else { ResetPasswordBody::ValidationSuccess { user_card, - form: ResetPasswordForm::build(), + form: ResetPasswordForm::build(context.clone()), } } }, @@ -244,5 +246,5 @@ async fn route_reset_password( }, }; - response!(ResetPassword::new(&services, body)) + response!(ResetPassword::new(context, body)) } diff --git a/src/web/pages/components/form.rs b/src/web/pages/components/form.rs index 88f915d63..55b07bda3 100644 --- a/src/web/pages/components/form.rs +++ b/src/web/pages/components/form.rs @@ -1,10 +1,13 @@ use askama::{Template, filters::HtmlSafe}; use validator::ValidationErrors; +use crate::pages::TemplateContext; + /// A reusable form component with field validation. #[derive(Debug, Template)] #[template(path = "_components/form.html.j2")] pub(crate) struct Form<'a> { + context: TemplateContext, inputs: Vec>, submit_label: &'a str, slowdown: bool, @@ -12,8 +15,14 @@ pub(crate) struct Form<'a> { } impl<'a> Form<'a> { - pub(crate) fn new(inputs: Vec>, submit_label: &'a str, slowdown: bool) -> Self { + pub(crate) fn new( + context: TemplateContext, + inputs: Vec>, + submit_label: &'a str, + slowdown: bool, + ) -> Self { Self { + context, inputs, submit_label, slowdown, @@ -100,8 +109,9 @@ macro_rules! form { impl $struct_name { /// Generate a [`Form`] which matches the shape of this struct. #[allow(clippy::needless_update)] - fn build() -> $crate::pages::components::form::Form<'static> { + fn build(context: TemplateContext) -> $crate::pages::components::form::Form<'static> { $crate::pages::components::form::Form::new( + context, vec![ $( $crate::pages::components::form::FormInput { @@ -119,8 +129,8 @@ macro_rules! form { /// Generate a [`Form`] with validation errors. #[allow(unused)] - fn with_errors(errors: validator::ValidationErrors) -> $crate::pages::components::form::Form<'static> { - let mut form = Self::build(); + fn with_errors(context: TemplateContext, errors: validator::ValidationErrors) -> $crate::pages::components::form::Form<'static> { + let mut form = Self::build(context); form.validation_errors = Some(errors); form } diff --git a/src/web/pages/index.rs b/src/web/pages/index.rs index 17595c3db..7a7808cf2 100644 --- a/src/web/pages/index.rs +++ b/src/web/pages/index.rs @@ -1,6 +1,6 @@ -use axum::{Router, extract::State, response::IntoResponse, routing::get}; +use axum::{Extension, Router, extract::State, response::IntoResponse, routing::get}; -use crate::{WebError, template}; +use crate::{WebError, pages::TemplateContext, template}; pub(crate) fn build() -> Router { Router::new() @@ -8,7 +8,10 @@ pub(crate) fn build() -> Router { .route(&format!("{}/", crate::ROUTE_PREFIX), get(index)) } -async fn index(State(services): State) -> Result { +async fn index( + State(services): State, + Extension(context): Extension, +) -> Result { template! { struct Index<'a> use "index.html.j2" { server_name: &'a str, @@ -17,7 +20,7 @@ async fn index(State(services): State) -> Result = std::result::Result; const GET_POST: MethodFilter = MethodFilter::GET.or(MethodFilter::POST); -#[derive(Debug)] +#[derive(Debug, Clone)] pub(crate) struct TemplateContext { pub allow_indexing: bool, + pub csp_nonce: String, } -impl From<&crate::State> for TemplateContext { - fn from(state: &crate::State) -> Self { - Self { - allow_indexing: state.config.allow_web_indexing, - } - } +const CSP_NONCE_LENGTH: usize = 32; + +pub(super) async fn template_context_middleware( + State(config): State>, + mut request: Request, + next: Next, +) -> Response { + let csp_nonce = utils::random_string(CSP_NONCE_LENGTH); + let context = TemplateContext { + allow_indexing: config.allow_web_indexing, + csp_nonce: csp_nonce.clone(), + }; + + assert!( + request.extensions_mut().insert(context).is_none(), + "template context should only be inserted once" + ); + + let mut response = next.run(request).await; + + response.headers_mut().insert( + header::CONTENT_SECURITY_POLICY, + HeaderValue::from_str(&format!( + "default-src 'none'; style-src 'self'; img-src 'self' 'https' data:; script-src \ + 'nonce-{csp_nonce}';" + )) + .expect("should be able to build CSP header"), + ); + + response } #[macro_export] @@ -43,9 +77,9 @@ macro_rules! template { impl$(<$lifetime>)? $name$(<$lifetime>)? { #[allow(clippy::too_many_arguments)] - fn new(state: &$crate::State, $($field_name: $field_type,)*) -> Self { + fn new(context: $crate::pages::TemplateContext, $($field_name: $field_type,)*) -> Self { Self { - context: state.into(), + context, $($field_name,)* } } diff --git a/src/web/pages/oauth/grant.rs b/src/web/pages/oauth/grant.rs index 070bcd118..15f0e6b91 100644 --- a/src/web/pages/oauth/grant.rs +++ b/src/web/pages/oauth/grant.rs @@ -1,5 +1,5 @@ use axum::{ - Router, + Extension, Router, extract::{Query, State}, response::Redirect, routing::on, @@ -12,7 +12,7 @@ use crate::{ WebError, extract::{Expect, PostForm}, pages::{ - GET_POST, Result, + GET_POST, Result, TemplateContext, components::{Avatar, AvatarType, ClientScopes}, }, response, @@ -40,6 +40,7 @@ template! { async fn route_authorization_code( State(services): State, + Extension(context): Extension, user: User, Expect(Query(query)): Expect>, PostForm(form): PostForm<()>, @@ -86,7 +87,7 @@ async fn route_authorization_code( let user_avatar = Avatar::for_local_user(&services, &user_id).await; response!(Grant::new( - &services, + context, serde_urlencoded::to_string(LoginQuery { next: Some(LoginTarget::AuthorizationCode(query)), reauthenticate: false, diff --git a/src/web/pages/templates/_components/form.html.j2 b/src/web/pages/templates/_components/form.html.j2 index 7c50fcb47..646632c0d 100644 --- a/src/web/pages/templates/_components/form.html.j2 +++ b/src/web/pages/templates/_components/form.html.j2 @@ -45,6 +45,6 @@ {% if slowdown %} - + {% endif %} diff --git a/src/web/pages/templates/cross_signing_reset.html.j2 b/src/web/pages/templates/cross_signing_reset.html.j2 index 3e1e5f829..bcf8a613d 100644 --- a/src/web/pages/templates/cross_signing_reset.html.j2 +++ b/src/web/pages/templates/cross_signing_reset.html.j2 @@ -30,7 +30,7 @@ Reset your digital identity
- + {% when CrossSigningResetBody::Success %}

The identity reset has been approved for the next ten minutes. diff --git a/src/web/pages/threepid.rs b/src/web/pages/threepid.rs index b660f11a7..5aefcf6f0 100644 --- a/src/web/pages/threepid.rs +++ b/src/web/pages/threepid.rs @@ -1,5 +1,5 @@ use axum::{ - Router, + Extension, Router, extract::{Query, State, rejection::QueryRejection}, response::IntoResponse, routing::get, @@ -7,7 +7,7 @@ use axum::{ use ruma::OwnedSessionId; use serde::Deserialize; -use crate::{WebError, template}; +use crate::{WebError, pages::TemplateContext, template}; template! { struct ThreepidValidation use "threepid_validation.html.j2" {} @@ -25,6 +25,7 @@ struct ThreepidValidationQuery { async fn threepid_validation( State(services): State, + Extension(context): Extension, query: Result, QueryRejection>, ) -> Result { let Query(query) = query?; @@ -35,5 +36,5 @@ async fn threepid_validation( .await .map_err(|message| WebError::BadRequest(message.into_owned()))?; - Ok(ThreepidValidation::new(&services)) + Ok(ThreepidValidation::new(context)) }