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, 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/// # `POST /_matrix/client/v3/register`
25///
26/// Register an account on this homeserver.
27///
28/// You can use [`GET
29/// /_matrix/client/v3/register/available`](fn.get_register_available_route.
30/// html) to check if the user id is valid and available.
31///
32/// - Only works if registration is enabled
33/// - If type is guest: ignores all parameters except
34///   initial_device_display_name
35/// - If sender is not appservice: Requires UIAA (but we only use a dummy stage)
36/// - If type is not guest and no username is given: Always fails after UIAA
37///   check
38/// - Creates a new account and populates it with default account data
39/// - If `inhibit_login` is false: Creates a device and returns device id and
40///   access_token
41#[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	// MSC4190: an appservice managing its own devices must register with
54	// inhibit_login set; it cannot mint a login session via /register.
55	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	// Generate new token for the device
109	let (access_token, expires_in) = services
110		.users
111		.generate_access_token(body.refresh_token);
112
113	// Generate a new refresh_token if requested by client
114	let refresh_token = expires_in.is_some().then(generate_refresh_token);
115
116	// Create device for this account
117	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	// don't force the username lowercase if it's from matrix-appservice-irc
209	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					// unless the username is from the broken matrix appservice IRC bridge, or
220					// we are in emergency mode, we should follow synapse's behaviour on
221					// not allowing things like spaces and UTF-8 characters in usernames
222					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	// A dummy stage still forces the client through UIA when nothing else does.
308	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
427/// Bind the association spent before account creation.
428///
429/// Binding remains best effort after `full_register`; ownership of the proof
430/// cannot be replayed even when the binding write fails.
431async 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(&notice).await;
497
498	Ok(())
499}