mas_handlers/views/register/steps/
finish.rs

1// Copyright 2025 New Vector Ltd.
2//
3// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
4// Please see LICENSE files in the repository root for full details.
5
6use std::sync::{Arc, LazyLock};
7
8use anyhow::Context as _;
9use axum::{
10    extract::{Path, State},
11    response::{Html, IntoResponse, Response},
12};
13use axum_extra::TypedHeader;
14use chrono::Duration;
15use mas_axum_utils::{InternalError, SessionInfoExt as _, cookies::CookieJar};
16use mas_data_model::{BoxClock, BoxRng, SiteConfig};
17use mas_matrix::HomeserverConnection;
18use mas_router::{PostAuthAction, UrlBuilder};
19use mas_storage::{
20    BoxRepository,
21    queue::{ProvisionUserJob, QueueJobRepositoryExt as _},
22    user::UserEmailFilter,
23};
24use mas_templates::{RegisterStepsEmailInUseContext, TemplateContext as _, Templates};
25use opentelemetry::metrics::Counter;
26use ulid::Ulid;
27
28use super::super::cookie::UserRegistrationSessions;
29use crate::{
30    BoundActivityTracker, METER, PreferredLanguage, views::shared::OptionalPostAuthAction,
31};
32
33static PASSWORD_REGISTER_COUNTER: LazyLock<Counter<u64>> = LazyLock::new(|| {
34    METER
35        .u64_counter("mas.user.password_registration")
36        .with_description("Number of password registrations")
37        .with_unit("{registration}")
38        .build()
39});
40
41#[tracing::instrument(
42    name = "handlers.views.register.steps.finish.get",
43    fields(user_registration.id = %id),
44    skip_all,
45)]
46pub(crate) async fn get(
47    mut rng: BoxRng,
48    clock: BoxClock,
49    mut repo: BoxRepository,
50    activity_tracker: BoundActivityTracker,
51    user_agent: Option<TypedHeader<headers::UserAgent>>,
52    State(url_builder): State<UrlBuilder>,
53    State(homeserver): State<Arc<dyn HomeserverConnection>>,
54    State(templates): State<Templates>,
55    State(site_config): State<SiteConfig>,
56    PreferredLanguage(lang): PreferredLanguage,
57    cookie_jar: CookieJar,
58    Path(id): Path<Ulid>,
59) -> Result<Response, InternalError> {
60    let user_agent = user_agent.map(|ua| ua.as_str().to_owned());
61    let registration = repo
62        .user_registration()
63        .lookup(id)
64        .await?
65        .context("User registration not found")
66        .map_err(InternalError::from_anyhow)?;
67
68    // If the registration is completed, we can go to the registration destination
69    // XXX: this might not be the right thing to do? Maybe an error page would be
70    // better?
71    if registration.completed_at.is_some() {
72        let post_auth_action: Option<PostAuthAction> = registration
73            .post_auth_action
74            .map(serde_json::from_value)
75            .transpose()?;
76
77        return Ok((
78            cookie_jar,
79            OptionalPostAuthAction::from(post_auth_action).go_next(&url_builder),
80        )
81            .into_response());
82    }
83
84    // Make sure the registration session hasn't expired
85    // XXX: this duration is hard-coded, could be configurable
86    if clock.now() - registration.created_at > Duration::hours(1) {
87        return Err(InternalError::from_anyhow(anyhow::anyhow!(
88            "Registration session has expired"
89        )));
90    }
91
92    // Check that this registration belongs to this browser
93    let registrations = UserRegistrationSessions::load(&cookie_jar);
94    if !registrations.contains(&registration) {
95        // XXX: we should have a better error screen here
96        return Err(InternalError::from_anyhow(anyhow::anyhow!(
97            "Could not find the registration in the browser cookies"
98        )));
99    }
100
101    // Let's perform last minute checks on the registration, especially to avoid
102    // race conditions where multiple users register with the same username or email
103    // address
104
105    if repo.user().exists(&registration.username).await? {
106        // XXX: this could have a better error message, but as this is unlikely to
107        // happen, we're fine with a vague message for now
108        return Err(InternalError::from_anyhow(anyhow::anyhow!(
109            "Username is already taken"
110        )));
111    }
112
113    if !homeserver
114        .is_localpart_available(&registration.username)
115        .await
116        .map_err(InternalError::from_anyhow)?
117    {
118        return Err(InternalError::from_anyhow(anyhow::anyhow!(
119            "Username is not available"
120        )));
121    }
122
123    // Check if the registration token is required and was provided
124    let registration_token = if site_config.registration_token_required {
125        if let Some(registration_token_id) = registration.user_registration_token_id {
126            let registration_token = repo
127                .user_registration_token()
128                .lookup(registration_token_id)
129                .await?
130                .context("Could not load the registration token")
131                .map_err(InternalError::from_anyhow)?;
132
133            if !registration_token.is_valid(clock.now()) {
134                // XXX: the registration token isn't valid anymore, we should
135                // have a better error in this case?
136                return Err(InternalError::from_anyhow(anyhow::anyhow!(
137                    "Registration token used is no longer valid"
138                )));
139            }
140
141            Some(registration_token)
142        } else {
143            // Else redirect to the registration token page
144            return Ok((
145                cookie_jar,
146                url_builder.redirect(&mas_router::RegisterToken::new(registration.id)),
147            )
148                .into_response());
149        }
150    } else {
151        None
152    };
153
154    // If there is an email authentication, we need to check that the email
155    // address was verified. If there is no email authentication attached, we
156    // need to make sure the server doesn't require it
157    let email_authentication = if let Some(email_authentication_id) =
158        registration.email_authentication_id
159    {
160        let email_authentication = repo
161            .user_email()
162            .lookup_authentication(email_authentication_id)
163            .await?
164            .context("Could not load the email authentication")
165            .map_err(InternalError::from_anyhow)?;
166
167        // Check that the email authentication has been completed
168        if email_authentication.completed_at.is_none() {
169            return Ok((
170                cookie_jar,
171                url_builder.redirect(&mas_router::RegisterVerifyEmail::new(id)),
172            )
173                .into_response());
174        }
175
176        // Check that the email address isn't already used
177        // It is important to do that here, as we we're not checking during the
178        // registration, because we don't want to disclose whether an email is
179        // already being used or not before we verified it
180        if repo
181            .user_email()
182            .count(UserEmailFilter::new().for_email(&email_authentication.email))
183            .await?
184            > 0
185        {
186            let action = registration
187                .post_auth_action
188                .map(serde_json::from_value)
189                .transpose()?;
190
191            let ctx = RegisterStepsEmailInUseContext::new(email_authentication.email, action)
192                .with_language(lang);
193
194            return Ok((
195                cookie_jar,
196                Html(templates.render_register_steps_email_in_use(&ctx)?),
197            )
198                .into_response());
199        }
200
201        Some(email_authentication)
202    } else if site_config.password_registration_email_required {
203        // This could only happen in theory during a configuration change
204        return Err(InternalError::from_anyhow(anyhow::anyhow!(
205            "Server requires an email address to complete the registration, but no email authentication was attached to the user registration"
206        )));
207    } else {
208        None
209    };
210
211    // Check that the display name is set
212    if registration.display_name.is_none() {
213        return Ok((
214            cookie_jar,
215            url_builder.redirect(&mas_router::RegisterDisplayName::new(registration.id)),
216        )
217            .into_response());
218    }
219
220    // Everything is good, let's complete the registration
221    let registration = repo
222        .user_registration()
223        .complete(&clock, registration)
224        .await?;
225
226    // If we used a registration token, we need to mark it as used
227    if let Some(registration_token) = registration_token {
228        repo.user_registration_token()
229            .use_token(&clock, registration_token)
230            .await?;
231    }
232
233    // Consume the registration session
234    let cookie_jar = registrations
235        .consume_session(&registration)?
236        .save(cookie_jar, &clock);
237
238    // Now we can start the user creation
239    let user = repo
240        .user()
241        .add(&mut rng, &clock, registration.username)
242        .await?;
243    // Also create a browser session which will log the user in
244    let user_session = repo
245        .browser_session()
246        .add(&mut rng, &clock, &user, user_agent)
247        .await?;
248
249    if let Some(email_authentication) = email_authentication {
250        repo.user_email()
251            .add(&mut rng, &clock, &user, email_authentication.email)
252            .await?;
253    }
254
255    if let Some(password) = registration.password {
256        let user_password = repo
257            .user_password()
258            .add(
259                &mut rng,
260                &clock,
261                &user,
262                password.version,
263                password.hashed_password,
264                None,
265            )
266            .await?;
267
268        repo.browser_session()
269            .authenticate_with_password(&mut rng, &clock, &user_session, &user_password)
270            .await?;
271
272        PASSWORD_REGISTER_COUNTER.add(1, &[]);
273    }
274
275    if let Some(terms_url) = registration.terms_url {
276        repo.user_terms()
277            .accept_terms(&mut rng, &clock, &user, terms_url)
278            .await?;
279    }
280
281    let mut job = ProvisionUserJob::new(&user);
282    if let Some(display_name) = registration.display_name {
283        job = job.set_display_name(display_name);
284    }
285    repo.queue_job().schedule_job(&mut rng, &clock, job).await?;
286
287    repo.save().await?;
288
289    activity_tracker
290        .record_browser_session(&clock, &user_session)
291        .await;
292
293    let post_auth_action: Option<PostAuthAction> = registration
294        .post_auth_action
295        .map(serde_json::from_value)
296        .transpose()?;
297
298    // Login the user with the session we just created
299    let cookie_jar = cookie_jar.set_session(&user_session);
300
301    return Ok((
302        cookie_jar,
303        OptionalPostAuthAction::from(post_auth_action).go_next(&url_builder),
304    )
305        .into_response());
306}