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 =
158        if let Some(email_authentication_id) = registration.email_authentication_id {
159            let email_authentication = repo
160                .user_email()
161                .lookup_authentication(email_authentication_id)
162                .await?
163                .context("Could not load the email authentication")
164                .map_err(InternalError::from_anyhow)?;
165
166            // Check that the email authentication has been completed
167            if email_authentication.completed_at.is_none() {
168                return Ok((
169                    cookie_jar,
170                    url_builder.redirect(&mas_router::RegisterVerifyEmail::new(id)),
171                )
172                    .into_response());
173            }
174
175            // Check that the email address isn't already used
176            // It is important to do that here, as we we're not checking during the
177            // registration, because we don't want to disclose whether an email is
178            // already being used or not before we verified it
179            if repo
180                .user_email()
181                .count(UserEmailFilter::new().for_email(&email_authentication.email))
182                .await?
183                > 0
184            {
185                let action = registration
186                    .post_auth_action
187                    .map(serde_json::from_value)
188                    .transpose()?;
189
190                let ctx = RegisterStepsEmailInUseContext::new(email_authentication.email, action)
191                    .with_language(lang);
192
193                return Ok((
194                    cookie_jar,
195                    Html(templates.render_register_steps_email_in_use(&ctx)?),
196                )
197                    .into_response());
198            }
199
200            Some(email_authentication)
201        } else {
202            None
203        };
204
205    // If this registration was created from an upstream OAuth session, check
206    // it is still valid and wasn't linked to a user in the meantime
207    let upstream_oauth = if let Some(upstream_oauth_authorization_session_id) =
208        registration.upstream_oauth_authorization_session_id
209    {
210        let upstream_oauth_authorization_session = repo
211            .upstream_oauth_session()
212            .lookup(upstream_oauth_authorization_session_id)
213            .await?
214            .context("Could not load the upstream OAuth authorization session")
215            .map_err(InternalError::from_anyhow)?;
216
217        let link_id = upstream_oauth_authorization_session
218            .link_id()
219            // This should not happen, the session is associated with the user
220            // registration once the link was already created
221            .context("Authorization session has no upstream link associated with it")
222            .map_err(InternalError::from_anyhow)?;
223
224        if upstream_oauth_authorization_session.is_consumed() {
225            // This means an authorization session was used to create multiple
226            // user registrations. This can happen if the user goes back in
227            // their navigation history and basically registers twice. We also
228            // used to consume the session earlier in the flow, so it's also
229            // possible that it happens during the rollout of that version. This
230            // is not going to happen often enough to have a dedicated page
231            return Err(InternalError::from_anyhow(anyhow::anyhow!(
232                "The upstream authorization session was already used. Try registering again"
233            )));
234        }
235
236        let upstream_oauth_link = repo
237            .upstream_oauth_link()
238            .lookup(link_id)
239            .await?
240            .context("Could not load the upstream OAuth link")
241            .map_err(InternalError::from_anyhow)?;
242
243        if upstream_oauth_link.user_id.is_some() {
244            // This means the link was already associated to a user. This could
245            // in theory happen if the same user registers concurrently, but
246            // this is not going to happen often enough to have a dedicated page
247            return Err(InternalError::from_anyhow(anyhow::anyhow!(
248                "The upstream identity was already linked to a user. Try logging in again"
249            )));
250        }
251
252        Some((upstream_oauth_authorization_session, upstream_oauth_link))
253    } else {
254        None
255    };
256
257    // Check that the display name is set
258    if registration.display_name.is_none() {
259        return Ok((
260            cookie_jar,
261            url_builder.redirect(&mas_router::RegisterDisplayName::new(registration.id)),
262        )
263            .into_response());
264    }
265
266    // Everything is good, let's complete the registration
267    let registration = repo
268        .user_registration()
269        .complete(&clock, registration)
270        .await?;
271
272    // If we used a registration token, we need to mark it as used
273    if let Some(registration_token) = registration_token {
274        repo.user_registration_token()
275            .use_token(&clock, registration_token)
276            .await?;
277    }
278
279    // Consume the registration session
280    let cookie_jar = registrations
281        .consume_session(&registration)?
282        .save(cookie_jar, &clock);
283
284    // Now we can start the user creation
285    let user = repo
286        .user()
287        .add(&mut rng, &clock, registration.username)
288        .await?;
289    // Also create a browser session which will log the user in
290    let user_session = repo
291        .browser_session()
292        .add(&mut rng, &clock, &user, user_agent)
293        .await?;
294
295    if let Some(email_authentication) = email_authentication {
296        repo.user_email()
297            .add(&mut rng, &clock, &user, email_authentication.email)
298            .await?;
299    }
300
301    if let Some(password) = registration.password {
302        let user_password = repo
303            .user_password()
304            .add(
305                &mut rng,
306                &clock,
307                &user,
308                password.version,
309                password.hashed_password,
310                None,
311            )
312            .await?;
313
314        repo.browser_session()
315            .authenticate_with_password(&mut rng, &clock, &user_session, &user_password)
316            .await?;
317
318        PASSWORD_REGISTER_COUNTER.add(1, &[]);
319    }
320
321    if let Some((upstream_session, upstream_link)) = upstream_oauth {
322        let upstream_session = repo
323            .upstream_oauth_session()
324            .consume(&clock, upstream_session, &user_session)
325            .await?;
326
327        repo.upstream_oauth_link()
328            .associate_to_user(&upstream_link, &user)
329            .await?;
330
331        repo.browser_session()
332            .authenticate_with_upstream(&mut rng, &clock, &user_session, &upstream_session)
333            .await?;
334    }
335
336    if let Some(terms_url) = registration.terms_url {
337        repo.user_terms()
338            .accept_terms(&mut rng, &clock, &user, terms_url)
339            .await?;
340    }
341
342    let mut job = ProvisionUserJob::new(&user);
343    if let Some(display_name) = registration.display_name {
344        job = job.set_display_name(display_name);
345    }
346    repo.queue_job().schedule_job(&mut rng, &clock, job).await?;
347
348    repo.save().await?;
349
350    activity_tracker
351        .record_browser_session(&clock, &user_session)
352        .await;
353
354    let post_auth_action: Option<PostAuthAction> = registration
355        .post_auth_action
356        .map(serde_json::from_value)
357        .transpose()?;
358
359    // Login the user with the session we just created
360    let cookie_jar = cookie_jar.set_session(&user_session);
361
362    return Ok((
363        cookie_jar,
364        OptionalPostAuthAction::from(post_auth_action).go_next(&url_builder),
365    )
366        .into_response());
367}