Skip to main content

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