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