Skip to main content

tuwunel_api_client/register/
register.rs

1use std::{fmt::Write, net::IpAddr};
2
3use axum::extract::State;
4use ruma::{
5	DeviceId, 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::{
21	ClientIp, Ruma,
22	utils::{bind_new_account_email, check_email_domain},
23};
24
25const RANDOM_USER_ID_LENGTH: usize = 10;
26
27/// # `POST /_matrix/client/v3/register`
28///
29/// Register an account on this homeserver.
30///
31/// You can use [`GET
32/// /_matrix/client/v3/register/available`](fn.get_register_available_route.
33/// html) to check if the user id is valid and available.
34///
35/// - Only works if registration is enabled
36/// - If type is guest: ignores all parameters except
37///   initial_device_display_name
38/// - If sender is not appservice: Requires UIAA (but we only use a dummy stage)
39/// - If type is not guest and no username is given: Always fails after UIAA
40///   check
41/// - Creates a new account and populates it with default account data
42/// - If `inhibit_login` is false: Creates a device and returns device id and
43///   access_token
44#[expect(clippy::doc_markdown)]
45#[tracing::instrument(skip_all, fields(%client), name = "register")]
46pub(crate) async fn register_route(
47	State(services): State<crate::State>,
48	ClientIp(client): ClientIp,
49	body: Ruma<register::v3::Request>,
50) -> Result<register::v3::Response> {
51	let is_guest = body.kind == RegistrationKind::Guest;
52	let emergency_mode_enabled = services
53		.config
54		.authentication
55		.emergency_password
56		.is_some();
57
58	gate_registration_allowed(services, &body, is_guest)?;
59
60	// MSC4190: an appservice managing its own devices must register with
61	// inhibit_login set; it cannot mint a login session via /register.
62	if !body.inhibit_login
63		&& body
64			.appservice_info
65			.as_ref()
66			.is_some_and(|appservice| appservice.registration.device_management)
67	{
68		return Err!(Request(AppserviceLoginUnsupported(
69			"Appservice has MSC4190 device management enabled; inhibit_login must be true."
70		)));
71	}
72
73	let user_id =
74		resolve_registration_user_id(services, &body, is_guest, emergency_mode_enabled).await?;
75
76	check_appservice_namespace(services, &body, &user_id, emergency_mode_enabled).await?;
77
78	let device_id = body
79		.device_id
80		.as_deref()
81		.filter(|_| !is_guest && !body.inhibit_login);
82
83	// Checked before anything is stored, so a refused ID leaves the name free.
84	services
85		.users
86		.check_device_id(&user_id, device_id)
87		.await?;
88
89	let email_association = enforce_uiaa(services, &body, is_guest).await?;
90
91	if let Some(ref association) = email_association {
92		check_email_domain(&services, &association.address)?;
93	}
94
95	let password = if is_guest { None } else { body.password.as_deref() };
96
97	services
98		.users
99		.full_register(Register {
100			user_id: Some(&user_id),
101			password,
102			is_appservice: body.appservice_info.is_some(),
103			is_guest,
104			grant_first_user_admin: true,
105			..Default::default()
106		})
107		.await?;
108
109	bind_registration_email(services, &user_id, email_association.as_ref()).await;
110
111	record_accepted_terms(services, &user_id, &body, is_guest).await?;
112
113	if (!is_guest && body.inhibit_login)
114		|| body
115			.appservice_info
116			.as_ref()
117			.is_some_and(|appservice| appservice.registration.device_management)
118	{
119		return Ok(register::v3::Response {
120			user_id,
121			device_id: None,
122			access_token: None,
123			refresh_token: None,
124			expires_in: None,
125		});
126	}
127
128	// Generate new token for the device
129	let (access_token, expires_in) = services
130		.users
131		.generate_access_token(body.refresh_token);
132
133	// Generate a new refresh_token if requested by client
134	let refresh_token = expires_in.is_some().then(generate_refresh_token);
135
136	// Create device for this account
137	let device_id = services
138		.users
139		.create_device(
140			&user_id,
141			device_id,
142			(Some(&access_token), expires_in),
143			refresh_token.as_deref(),
144			body.initial_device_display_name.as_deref(),
145			Some(client),
146		)
147		.await?;
148
149	debug_info!(%user_id, %device_id, "User account was created");
150
151	if body.appservice_info.is_none()
152		&& (!is_guest || services.config.admin.room.log_guest_registrations)
153	{
154		announce_new_user(services, &user_id, &body, is_guest, &client).await?;
155	}
156
157	Ok(register::v3::Response {
158		user_id,
159		device_id: Some(device_id),
160		access_token: Some(access_token),
161		refresh_token,
162		expires_in,
163	})
164}
165
166fn gate_registration_allowed(
167	services: crate::State,
168	body: &Ruma<register::v3::Request>,
169	is_guest: bool,
170) -> Result {
171	let user = body.username.as_deref().unwrap_or("");
172	let device_name = body
173		.initial_device_display_name
174		.as_deref()
175		.unwrap_or("");
176
177	if !services.config.registration.enable && body.appservice_info.is_none() {
178		info!(
179			%is_guest,
180			%user,
181			%device_name,
182			"Rejecting registration attempt as registration is disabled"
183		);
184
185		return Err!(Request(Forbidden("Registration has been disabled.")));
186	}
187
188	if is_guest && !services.config.registration.guests.enable {
189		debug_warn!(
190			%device_name,
191			"Guest registration disabled, rejecting guest registration attempt"
192		);
193
194		return Err!(Request(GuestAccessForbidden("Guest registration is disabled.")));
195	}
196
197	if !is_guest
198		&& body.appservice_info.is_none()
199		&& !services.config.allows_password_registration()
200	{
201		debug_warn!(
202			%user,
203			%device_name,
204			"Rejecting registration attempt as password login is disabled"
205		);
206
207		return Err!(Request(Forbidden("Password registration has been disabled.")));
208	}
209
210	Ok(())
211}
212
213async fn resolve_registration_user_id(
214	services: crate::State,
215	body: &Ruma<register::v3::Request>,
216	is_guest: bool,
217	emergency_mode_enabled: bool,
218) -> Result<OwnedUserId> {
219	let (Some(username), false) = (body.username.as_ref(), is_guest) else {
220		loop {
221			let proposed_user_id = UserId::parse_with_server_name(
222				utils::random_string(RANDOM_USER_ID_LENGTH).to_lowercase(),
223				services.globals.server_name(),
224			)?;
225
226			if !services.users.exists(&proposed_user_id).await {
227				return Ok(proposed_user_id);
228			}
229		}
230	};
231
232	let is_irc = is_matrix_appservice_irc(body.appservice_info.as_ref());
233
234	if services
235		.config
236		.registration
237		.forbidden_usernames
238		.is_match(username)
239		&& !emergency_mode_enabled
240	{
241		return Err!(Request(Forbidden("Username is forbidden")));
242	}
243
244	// don't force the username lowercase if it's from matrix-appservice-irc
245	let body_username = if is_irc {
246		username.clone()
247	} else {
248		username.to_lowercase()
249	};
250
251	let proposed_user_id =
252		match UserId::parse_with_server_name(&body_username, services.globals.server_name()) {
253			| Ok(user_id) => {
254				if let Err(e) = user_id.validate_strict() {
255					// unless the username is from the broken matrix appservice IRC bridge, or
256					// we are in emergency mode, we should follow synapse's behaviour on
257					// not allowing things like spaces and UTF-8 characters in usernames
258					if !is_irc && !emergency_mode_enabled {
259						return Err!(Request(InvalidUsername(debug_warn!(
260							"Username {body_username} contains disallowed characters or spaces: \
261							 {e}"
262						))));
263					}
264				}
265
266				user_id
267			},
268			| Err(e) => {
269				return Err!(Request(InvalidUsername(debug_warn!(
270					"Username {body_username} is not valid: {e}"
271				))));
272			},
273		};
274
275	if services.users.exists(&proposed_user_id).await {
276		return Err!(Request(UserInUse("User ID is not available.")));
277	}
278
279	services
280		.users
281		.check_creation(&proposed_user_id)
282		.await?;
283
284	Ok(proposed_user_id)
285}
286
287async fn check_appservice_namespace(
288	services: crate::State,
289	body: &Ruma<register::v3::Request>,
290	user_id: &UserId,
291	emergency_mode_enabled: bool,
292) -> Result {
293	if body.body.login_type == Some(LoginType::ApplicationService) {
294		match body.appservice_info {
295			| Some(ref info) =>
296				if !info.is_user_match(user_id) && !emergency_mode_enabled {
297					return Err!(Request(Exclusive(
298						"Username is not in an appservice namespace."
299					)));
300				},
301			| _ => {
302				return Err!(Request(MissingToken("Missing appservice token.")));
303			},
304		}
305	} else if services
306		.appservice
307		.is_exclusive_user_id(user_id)
308		.await
309		&& !emergency_mode_enabled
310	{
311		return Err!(Request(Exclusive("Username is reserved by an appservice.")));
312	}
313
314	Ok(())
315}
316
317async fn enforce_uiaa(
318	services: crate::State,
319	body: &Ruma<register::v3::Request>,
320	is_guest: bool,
321) -> Result<Option<Association>> {
322	if body.appservice_info.is_some() || is_guest {
323		return Ok(None);
324	}
325
326	let token_required = services.registration_tokens.is_enabled().await;
327	let terms = services.config.login_terms_params();
328
329	let smtp = &services.config.smtp;
330	let email_required = smtp.connection_uri.is_some()
331		&& (smtp.require_email_for_registration
332			|| (token_required && smtp.require_email_for_token_registration));
333
334	let stages: Vec<AuthType> = [
335		token_required.then_some(AuthType::RegistrationToken),
336		email_required.then_some(AuthType::EmailIdentity),
337		terms.is_some().then_some(AuthType::Terms),
338	]
339	.into_iter()
340	.flatten()
341	.collect();
342
343	// A dummy stage still forces the client through UIA when nothing else does.
344	let stages = if stages.is_empty() {
345		vec![AuthType::Dummy]
346	} else {
347		stages
348	};
349
350	let params = terms
351		.as_ref()
352		.map(|terms| to_raw_value(&json!({ "m.login.terms": terms })))
353		.transpose()?;
354
355	let mut uiaainfo = UiaaInfo {
356		flows: vec![AuthFlow { stages }],
357		completed: Vec::new(),
358		params,
359		session: None,
360		auth_error: None,
361	};
362
363	let server_user = UserId::parse_with_server_name("", services.globals.server_name())?;
364	let server_device: &DeviceId = "".into();
365
366	match &body.auth {
367		| Some(auth) => {
368			let (worked, uiaainfo) = match email_required {
369				| true =>
370					services
371						.uiaa
372						.try_auth_registration(
373							&server_user,
374							server_device,
375							auth,
376							&uiaainfo,
377							body.rate_limit_ip.ok(),
378						)
379						.await?,
380				| false =>
381					services
382						.uiaa
383						.try_auth(
384							&server_user,
385							server_device,
386							auth,
387							&uiaainfo,
388							body.rate_limit_ip.ok(),
389						)
390						.await?,
391			};
392
393			if !worked {
394				return Err(Error::Uiaa(uiaainfo));
395			}
396
397			let session = uiaainfo.session.expect("session is always set");
398			let claim = (server_user, server_device.to_owned(), session.into());
399
400			let association = match email_required {
401				| false => None,
402				| true => {
403					let association = match services.threepid.redeem_claim(&claim).await {
404						| Ok(association) => association,
405						| Err(error)
406							if error.is_not_found() || matches!(&error, Error::Request(..)) =>
407						{
408							return Err!(Request(Forbidden("Invalid email identity proof.")));
409						},
410						| Err(error) => return Err(error),
411					};
412
413					if association.medium != Medium::Email {
414						return Err!(Request(Forbidden("Invalid email identity proof.")));
415					}
416
417					Some(association)
418				},
419			};
420
421			services
422				.uiaa
423				.update_uiaa_session(&claim.0, &claim.1, &claim.2, None);
424
425			Ok(association)
426		},
427		| _ => match body.json_body {
428			| None => Err!(Request(NotJson("JSON body is not valid"))),
429			| Some(ref json) => {
430				uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
431				services
432					.uiaa
433					.create(&server_user, server_device, &uiaainfo, json);
434
435				Err(Error::Uiaa(uiaainfo))
436			},
437		},
438	}
439}
440
441async fn record_accepted_terms(
442	services: crate::State,
443	user_id: &UserId,
444	body: &Ruma<register::v3::Request>,
445	is_guest: bool,
446) -> Result {
447	if is_guest || body.appservice_info.is_some() {
448		return Ok(());
449	}
450
451	let accepted: Vec<String> = services
452		.config
453		.registration_terms
454		.values()
455		.flat_map(|policy| policy.translations.values())
456		.map(|translation| translation.url.to_string())
457		.collect();
458
459	if accepted.is_empty() {
460		return Ok(());
461	}
462
463	let event_type = "m.accepted_terms";
464	let event = json!({
465		"type": event_type,
466		"content": { "accepted": accepted },
467	});
468
469	services
470		.account_data
471		.update(None, user_id, event_type.into(), &event)
472		.await
473}
474
475/// Bind the association spent before account creation.
476///
477/// Binding remains best effort after `full_register`; ownership of the proof
478/// cannot be replayed even when the binding write fails.
479async fn bind_registration_email(
480	services: crate::State,
481	user_id: &UserId,
482	association: Option<&Association>,
483) {
484	if !services.sendmail.is_enabled() {
485		return;
486	}
487
488	let Some(association) = association else {
489		return;
490	};
491
492	if let Err(error) = bind_new_account_email(&services, user_id, &association.address).await {
493		warn!(%user_id, %error, "Skipping registration email binding");
494	}
495}
496
497async fn announce_new_user(
498	services: crate::State,
499	user_id: &UserId,
500	body: &Ruma<register::v3::Request>,
501	is_guest: bool,
502	client: &IpAddr,
503) -> Result {
504	let mut notice = String::from(if is_guest { "New guest user" } else { "New user" });
505
506	write!(notice, " \"{user_id}\" registered on this server from IP {client}")?;
507
508	if let Some(device_name) = body.initial_device_display_name.as_deref() {
509		write!(notice, " with device name {device_name}")?;
510	}
511
512	if is_guest {
513		debug_info!("{notice}");
514	} else {
515		info!("{notice}");
516	}
517
518	services.admin.notify(&notice).await;
519
520	Ok(())
521}