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        let upstream_oauth_link = repo
225            .upstream_oauth_link()
226            .lookup(link_id)
227            .await?
228            .context("Could not load the upstream OAuth link")
229            .map_err(InternalError::from_anyhow)?;
230
231        if upstream_oauth_link.user_id.is_some() {
232            // This means the link was already associated to a user. This could
233            // in theory happen if the same user registers concurrently, but
234            // this is not going to happen often enough to have a dedicated page
235            return Err(InternalError::from_anyhow(anyhow::anyhow!(
236                "The upstream identity was already linked to a user. Try logging in again"
237            )));
238        }
239
240        Some((upstream_oauth_authorization_session, upstream_oauth_link))
241    } else {
242        None
243    };
244
245    // Check that the display name is set
246    if registration.display_name.is_none() {
247        return Ok((
248            cookie_jar,
249            url_builder.redirect(&mas_router::RegisterDisplayName::new(registration.id)),
250        )
251            .into_response());
252    }
253
254    // Everything is good, let's complete the registration
255    let registration = repo
256        .user_registration()
257        .complete(&clock, registration)
258        .await?;
259
260    // If we used a registration token, we need to mark it as used
261    if let Some(registration_token) = registration_token {
262        repo.user_registration_token()
263            .use_token(&clock, registration_token)
264            .await?;
265    }
266
267    // Consume the registration session
268    let cookie_jar = registrations
269        .consume_session(&registration)?
270        .save(cookie_jar, &clock);
271
272    // Now we can start the user creation
273    let user = repo
274        .user()
275        .add(&mut rng, &clock, registration.username)
276        .await?;
277    // Also create a browser session which will log the user in
278    let user_session = repo
279        .browser_session()
280        .add(&mut rng, &clock, &user, user_agent)
281        .await?;
282
283    if let Some(email_authentication) = email_authentication {
284        repo.user_email()
285            .add(&mut rng, &clock, &user, email_authentication.email)
286            .await?;
287    }
288
289    if let Some(password) = registration.password {
290        let user_password = repo
291            .user_password()
292            .add(
293                &mut rng,
294                &clock,
295                &user,
296                password.version,
297                password.hashed_password,
298                None,
299            )
300            .await?;
301
302        repo.browser_session()
303            .authenticate_with_password(&mut rng, &clock, &user_session, &user_password)
304            .await?;
305
306        PASSWORD_REGISTER_COUNTER.add(1, &[]);
307    }
308
309    if let Some((upstream_session, upstream_link)) = upstream_oauth {
310        repo.upstream_oauth_link()
311            .associate_to_user(&upstream_link, &user)
312            .await?;
313
314        repo.browser_session()
315            .authenticate_with_upstream(&mut rng, &clock, &user_session, &upstream_session)
316            .await?;
317    }
318
319    if let Some(terms_url) = registration.terms_url {
320        repo.user_terms()
321            .accept_terms(&mut rng, &clock, &user, terms_url)
322            .await?;
323    }
324
325    let mut job = ProvisionUserJob::new(&user);
326    if let Some(display_name) = registration.display_name {
327        job = job.set_display_name(display_name);
328    }
329    repo.queue_job().schedule_job(&mut rng, &clock, job).await?;
330
331    repo.save().await?;
332
333    activity_tracker
334        .record_browser_session(&clock, &user_session)
335        .await;
336
337    let post_auth_action: Option<PostAuthAction> = registration
338        .post_auth_action
339        .map(serde_json::from_value)
340        .transpose()?;
341
342    // Login the user with the session we just created
343    let cookie_jar = cookie_jar.set_session(&user_session);
344
345    return Ok((
346        cookie_jar,
347        OptionalPostAuthAction::from(post_auth_action).go_next(&url_builder),
348    )
349        .into_response());
350}