1use std::{fmt::Write, net::IpAddr};
2
3use axum::extract::State;
4use ruma::{
5 DeviceId, MilliSecondsSinceUnixEpoch, OwnedUserId, UserId,
6 api::client::{
7 account::register::{self, LoginType, RegistrationKind},
8 uiaa::{AuthFlow, AuthType, UiaaInfo},
9 },
10 thirdparty::Medium,
11};
12use serde_json::{json, value::to_raw_value};
13use tuwunel_core::{Err, Error, Result, debug_info, debug_warn, info, utils, warn};
14use tuwunel_service::{
15 threepid::Association,
16 users::{Register, device::generate_refresh_token},
17};
18
19use super::{SESSION_ID_LENGTH, is_matrix_appservice_irc};
20use crate::{ClientIp, Ruma};
21
22const RANDOM_USER_ID_LENGTH: usize = 10;
23
24#[expect(clippy::doc_markdown)]
42#[tracing::instrument(skip_all, fields(%client), name = "register")]
43pub(crate) async fn register_route(
44 State(services): State<crate::State>,
45 ClientIp(client): ClientIp,
46 body: Ruma<register::v3::Request>,
47) -> Result<register::v3::Response> {
48 let is_guest = body.kind == RegistrationKind::Guest;
49 let emergency_mode_enabled = services.config.emergency_password.is_some();
50
51 gate_registration_allowed(services, &body, is_guest)?;
52
53 if !body.inhibit_login
56 && body
57 .appservice_info
58 .as_ref()
59 .is_some_and(|appservice| appservice.registration.device_management)
60 {
61 return Err!(Request(AppserviceLoginUnsupported(
62 "Appservice has MSC4190 device management enabled; inhibit_login must be true."
63 )));
64 }
65
66 let user_id =
67 resolve_registration_user_id(services, &body, is_guest, emergency_mode_enabled).await?;
68
69 check_appservice_namespace(services, &body, &user_id, emergency_mode_enabled).await?;
70
71 let email_association = enforce_uiaa(services, &body, is_guest).await?;
72
73 let password = if is_guest { None } else { body.password.as_deref() };
74
75 services
76 .users
77 .full_register(Register {
78 user_id: Some(&user_id),
79 password,
80 is_appservice: body.appservice_info.is_some(),
81 is_guest,
82 grant_first_user_admin: true,
83 ..Default::default()
84 })
85 .await?;
86
87 bind_registration_email(services, &user_id, email_association.as_ref()).await;
88
89 record_accepted_terms(services, &user_id, &body, is_guest).await?;
90
91 if (!is_guest && body.inhibit_login)
92 || body
93 .appservice_info
94 .as_ref()
95 .is_some_and(|appservice| appservice.registration.device_management)
96 {
97 return Ok(register::v3::Response {
98 user_id,
99 device_id: None,
100 access_token: None,
101 refresh_token: None,
102 expires_in: None,
103 });
104 }
105
106 let device_id = if is_guest { None } else { body.device_id.as_deref() };
107
108 let (access_token, expires_in) = services
110 .users
111 .generate_access_token(body.refresh_token);
112
113 let refresh_token = expires_in.is_some().then(generate_refresh_token);
115
116 let device_id = services
118 .users
119 .create_device(
120 &user_id,
121 device_id,
122 (Some(&access_token), expires_in),
123 refresh_token.as_deref(),
124 body.initial_device_display_name.as_deref(),
125 Some(client),
126 )
127 .await?;
128
129 debug_info!(%user_id, %device_id, "User account was created");
130
131 if body.appservice_info.is_none() && (!is_guest || services.config.log_guest_registrations) {
132 announce_new_user(services, &user_id, &body, is_guest, &client).await?;
133 }
134
135 Ok(register::v3::Response {
136 user_id,
137 device_id: Some(device_id),
138 access_token: Some(access_token),
139 refresh_token,
140 expires_in,
141 })
142}
143
144fn gate_registration_allowed(
145 services: crate::State,
146 body: &Ruma<register::v3::Request>,
147 is_guest: bool,
148) -> Result {
149 let user = body.username.as_deref().unwrap_or("");
150 let device_name = body
151 .initial_device_display_name
152 .as_deref()
153 .unwrap_or("");
154
155 if !services.config.allow_registration && body.appservice_info.is_none() {
156 info!(
157 %is_guest,
158 %user,
159 %device_name,
160 "Rejecting registration attempt as registration is disabled"
161 );
162
163 return Err!(Request(Forbidden("Registration has been disabled.")));
164 }
165
166 if is_guest && !services.config.allow_guest_registration {
167 debug_warn!(
168 %device_name,
169 "Guest registration disabled, rejecting guest registration attempt"
170 );
171
172 return Err!(Request(GuestAccessForbidden("Guest registration is disabled.")));
173 }
174
175 Ok(())
176}
177
178async fn resolve_registration_user_id(
179 services: crate::State,
180 body: &Ruma<register::v3::Request>,
181 is_guest: bool,
182 emergency_mode_enabled: bool,
183) -> Result<OwnedUserId> {
184 let (Some(username), false) = (body.username.as_ref(), is_guest) else {
185 loop {
186 let proposed_user_id = UserId::parse_with_server_name(
187 utils::random_string(RANDOM_USER_ID_LENGTH).to_lowercase(),
188 services.globals.server_name(),
189 )?;
190
191 if !services.users.exists(&proposed_user_id).await {
192 return Ok(proposed_user_id);
193 }
194 }
195 };
196
197 let is_irc = is_matrix_appservice_irc(body.appservice_info.as_ref());
198
199 if services
200 .config
201 .forbidden_usernames
202 .is_match(username)
203 && !emergency_mode_enabled
204 {
205 return Err!(Request(Forbidden("Username is forbidden")));
206 }
207
208 let body_username = if is_irc {
210 username.clone()
211 } else {
212 username.to_lowercase()
213 };
214
215 let proposed_user_id =
216 match UserId::parse_with_server_name(&body_username, services.globals.server_name()) {
217 | Ok(user_id) => {
218 if let Err(e) = user_id.validate_strict() {
219 if !is_irc && !emergency_mode_enabled {
223 return Err!(Request(InvalidUsername(debug_warn!(
224 "Username {body_username} contains disallowed characters or spaces: \
225 {e}"
226 ))));
227 }
228 }
229
230 user_id
231 },
232 | Err(e) => {
233 return Err!(Request(InvalidUsername(debug_warn!(
234 "Username {body_username} is not valid: {e}"
235 ))));
236 },
237 };
238
239 if services.users.exists(&proposed_user_id).await {
240 return Err!(Request(UserInUse("User ID is not available.")));
241 }
242
243 services
244 .users
245 .check_creation(&proposed_user_id)
246 .await?;
247
248 Ok(proposed_user_id)
249}
250
251async fn check_appservice_namespace(
252 services: crate::State,
253 body: &Ruma<register::v3::Request>,
254 user_id: &UserId,
255 emergency_mode_enabled: bool,
256) -> Result {
257 if body.body.login_type == Some(LoginType::ApplicationService) {
258 match body.appservice_info {
259 | Some(ref info) =>
260 if !info.is_user_match(user_id) && !emergency_mode_enabled {
261 return Err!(Request(Exclusive(
262 "Username is not in an appservice namespace."
263 )));
264 },
265 | _ => {
266 return Err!(Request(MissingToken("Missing appservice token.")));
267 },
268 }
269 } else if services
270 .appservice
271 .is_exclusive_user_id(user_id)
272 .await
273 && !emergency_mode_enabled
274 {
275 return Err!(Request(Exclusive("Username is reserved by an appservice.")));
276 }
277
278 Ok(())
279}
280
281async fn enforce_uiaa(
282 services: crate::State,
283 body: &Ruma<register::v3::Request>,
284 is_guest: bool,
285) -> Result<Option<Association>> {
286 if body.appservice_info.is_some() || is_guest {
287 return Ok(None);
288 }
289
290 let token_required = services.registration_tokens.is_enabled().await;
291 let terms = services.config.login_terms_params();
292
293 let smtp = &services.config.smtp;
294 let email_required = smtp.connection_uri.is_some()
295 && (smtp.require_email_for_registration
296 || (token_required && smtp.require_email_for_token_registration));
297
298 let stages: Vec<AuthType> = [
299 token_required.then_some(AuthType::RegistrationToken),
300 email_required.then_some(AuthType::EmailIdentity),
301 terms.is_some().then_some(AuthType::Terms),
302 ]
303 .into_iter()
304 .flatten()
305 .collect();
306
307 let stages = if stages.is_empty() {
309 vec![AuthType::Dummy]
310 } else {
311 stages
312 };
313
314 let params = terms
315 .as_ref()
316 .map(|terms| to_raw_value(&json!({ "m.login.terms": terms })))
317 .transpose()?;
318
319 let mut uiaainfo = UiaaInfo {
320 flows: vec![AuthFlow { stages }],
321 completed: Vec::new(),
322 params,
323 session: None,
324 auth_error: None,
325 };
326
327 let server_user = UserId::parse_with_server_name("", services.globals.server_name())?;
328 let server_device: &DeviceId = "".into();
329
330 match &body.auth {
331 | Some(auth) => {
332 let (worked, uiaainfo) = match email_required {
333 | true =>
334 services
335 .uiaa
336 .try_auth_registration(&server_user, server_device, auth, &uiaainfo)
337 .await?,
338 | false =>
339 services
340 .uiaa
341 .try_auth(&server_user, server_device, auth, &uiaainfo)
342 .await?,
343 };
344
345 if !worked {
346 return Err(Error::Uiaa(uiaainfo));
347 }
348
349 let session = uiaainfo.session.expect("session is always set");
350 let claim = (server_user, server_device.to_owned(), session.into());
351
352 let association = match email_required {
353 | false => None,
354 | true => {
355 let association = match services.threepid.redeem_claim(&claim).await {
356 | Ok(association) => association,
357 | Err(error)
358 if error.is_not_found() || matches!(&error, Error::Request(..)) =>
359 {
360 return Err!(Request(Forbidden("Invalid email identity proof.")));
361 },
362 | Err(error) => return Err(error),
363 };
364
365 if association.medium != Medium::Email {
366 return Err!(Request(Forbidden("Invalid email identity proof.")));
367 }
368
369 Some(association)
370 },
371 };
372
373 services
374 .uiaa
375 .update_uiaa_session(&claim.0, &claim.1, &claim.2, None);
376
377 Ok(association)
378 },
379 | _ => match body.json_body {
380 | None => Err!(Request(NotJson("JSON body is not valid"))),
381 | Some(ref json) => {
382 uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
383 services
384 .uiaa
385 .create(&server_user, server_device, &uiaainfo, json);
386
387 Err(Error::Uiaa(uiaainfo))
388 },
389 },
390 }
391}
392
393async fn record_accepted_terms(
394 services: crate::State,
395 user_id: &UserId,
396 body: &Ruma<register::v3::Request>,
397 is_guest: bool,
398) -> Result {
399 if is_guest || body.appservice_info.is_some() {
400 return Ok(());
401 }
402
403 let accepted: Vec<String> = services
404 .config
405 .registration_terms
406 .values()
407 .flat_map(|policy| policy.translations.values())
408 .map(|translation| translation.url.to_string())
409 .collect();
410
411 if accepted.is_empty() {
412 return Ok(());
413 }
414
415 let event_type = "m.accepted_terms";
416 let event = json!({
417 "type": event_type,
418 "content": { "accepted": accepted },
419 });
420
421 services
422 .account_data
423 .update(None, user_id, event_type.into(), &event)
424 .await
425}
426
427async fn bind_registration_email(
432 services: crate::State,
433 user_id: &UserId,
434 association: Option<&Association>,
435) {
436 if !services.sendmail.is_enabled() {
437 return;
438 }
439
440 let Some(association) = association else {
441 return;
442 };
443
444 if let Err(e) = try_bind_registration_email(services, user_id, association).await {
445 warn!(%user_id, "Skipping registration email binding: {e}");
446 }
447}
448
449async fn try_bind_registration_email(
450 services: crate::State,
451 user_id: &UserId,
452 association: &Association,
453) -> Result {
454 if services
455 .threepid
456 .user_id_for_email(&association.address)
457 .await?
458 .is_some_and(|bound| bound != user_id)
459 {
460 warn!(%user_id, "Skipping registration email binding: address bound to another user");
461
462 return Ok(());
463 }
464
465 let now = MilliSecondsSinceUnixEpoch::now();
466
467 services
468 .threepid
469 .put_binding(user_id, &association.address, Medium::Email, now, now)
470 .await;
471
472 Ok(())
473}
474
475async fn announce_new_user(
476 services: crate::State,
477 user_id: &UserId,
478 body: &Ruma<register::v3::Request>,
479 is_guest: bool,
480 client: &IpAddr,
481) -> Result {
482 let mut notice = String::from(if is_guest { "New guest user" } else { "New user" });
483
484 write!(notice, " \"{user_id}\" registered on this server from IP {client}")?;
485
486 if let Some(device_name) = body.initial_device_display_name.as_deref() {
487 write!(notice, " with device name {device_name}")?;
488 }
489
490 if is_guest {
491 debug_info!("{notice}");
492 } else {
493 info!("{notice}");
494 }
495
496 services.admin.notify(¬ice).await;
497
498 Ok(())
499}