Skip to main content

tuwunel_api/client/register/
register.rs

1use 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/// # `POST /_matrix/client/v3/register`
21///
22/// Register an account on this homeserver.
23///
24/// You can use [`GET
25/// /_matrix/client/v3/register/available`](fn.get_register_available_route.
26/// html) to check if the user id is valid and available.
27///
28/// - Only works if registration is enabled
29/// - If type is guest: ignores all parameters except
30///   initial_device_display_name
31/// - If sender is not appservice: Requires UIAA (but we only use a dummy stage)
32/// - If type is not guest and no username is given: Always fails after UIAA
33///   check
34/// - Creates a new account and populates it with default account data
35/// - If `inhibit_login` is false: Creates a device and returns device id and
36///   access_token
37#[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	// MSC4190: an appservice managing its own devices must register with
50	// inhibit_login set; it cannot mint a login session via /register.
51	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	// Generate new token for the device
105	let (access_token, expires_in) = services
106		.users
107		.generate_access_token(body.refresh_token);
108
109	// Generate a new refresh_token if requested by client
110	let refresh_token = expires_in.is_some().then(generate_refresh_token);
111
112	// Create device for this account
113	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	// don't force the username lowercase if it's from matrix-appservice-irc
205	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					// unless the username is from the broken matrix appservice IRC bridge, or
216					// we are in emergency mode, we should follow synapse's behaviour on
217					// not allowing things like spaces and UTF-8 characters in usernames
218					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	// A dummy stage still forces the client through UIA when nothing else does.
298	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			// Recover the validated email creds the session retained, so the bind works
331			// regardless of which stage completed the flow.
332			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
391/// A stray `id_server` on the credentials is ignored and `id_access_token` is
392/// never required.
393async 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(&notice).await;
470	}
471
472	Ok(())
473}