Skip to main content

tuwunel_api/client/
register.rs

1use std::fmt::Write;
2
3use axum::extract::State;
4use ruma::{
5	UserId,
6	api::client::{
7		account::{
8			check_registration_token_validity, get_username_availability,
9			register::{self, LoginType, RegistrationKind},
10		},
11		uiaa::{AuthFlow, AuthType, UiaaInfo},
12	},
13};
14use tuwunel_core::{Err, Error, Result, debug_info, debug_warn, info, utils};
15use tuwunel_service::users::{Register, device::generate_refresh_token};
16
17use super::SESSION_ID_LENGTH;
18use crate::{ClientIp, Ruma};
19
20const RANDOM_USER_ID_LENGTH: usize = 10;
21
22/// # `GET /_matrix/client/v3/register/available`
23///
24/// Checks if a username is valid and available on this server.
25///
26/// Conditions for returning true:
27/// - The user id is not historical
28/// - The server name of the user id matches this server
29/// - No user or appservice on this server already claimed this username
30///
31/// Note: This will not reserve the username, so the username might become
32/// invalid when trying to register
33#[tracing::instrument(skip_all, fields(%client), name = "register_available")]
34pub(crate) async fn get_register_available_route(
35	State(services): State<crate::State>,
36	ClientIp(client): ClientIp,
37	body: Ruma<get_username_availability::v3::Request>,
38) -> Result<get_username_availability::v3::Response> {
39	// workaround for https://github.com/matrix-org/matrix-appservice-irc/issues/1780 due to inactivity of fixing the issue
40	let is_matrix_appservice_irc = body
41		.appservice_info
42		.as_ref()
43		.is_some_and(|appservice| {
44			let id = &appservice.registration.id;
45			id == "irc"
46				|| id.contains("matrix-appservice-irc")
47				|| id.contains("matrix_appservice_irc")
48		});
49
50	if services
51		.config
52		.forbidden_usernames
53		.is_match(&body.username)
54	{
55		return Err!(Request(Forbidden("Username is forbidden")));
56	}
57
58	// don't force the username lowercase if it's from matrix-appservice-irc
59	let body_username = if is_matrix_appservice_irc {
60		body.username.clone()
61	} else {
62		body.username.to_lowercase()
63	};
64
65	// Validate user id
66	let user_id =
67		match UserId::parse_with_server_name(&body_username, services.globals.server_name()) {
68			| Ok(user_id) => {
69				if let Err(e) = user_id.validate_strict() {
70					// unless the username is from the broken matrix appservice IRC bridge, we
71					// should follow synapse's behaviour on not allowing things like spaces
72					// and UTF-8 characters in usernames
73					if !is_matrix_appservice_irc {
74						return Err!(Request(InvalidUsername(debug_warn!(
75							"Username {body_username} contains disallowed characters or spaces: \
76							 {e}"
77						))));
78					}
79				}
80
81				user_id
82			},
83			| Err(e) => {
84				return Err!(Request(InvalidUsername(debug_warn!(
85					"Username {body_username} is not valid: {e}"
86				))));
87			},
88		};
89
90	// Check if username is creative enough
91	if services.users.exists(&user_id).await {
92		return Err!(Request(UserInUse("User ID is not available.")));
93	}
94
95	if let Some(ref info) = body.appservice_info
96		&& !info.is_user_match(&user_id)
97	{
98		return Err!(Request(Exclusive("Username is not in an appservice namespace.")));
99	}
100
101	if services
102		.appservice
103		.is_exclusive_user_id(&user_id)
104		.await
105	{
106		return Err!(Request(Exclusive("Username is reserved by an appservice.")));
107	}
108
109	Ok(get_username_availability::v3::Response { available: true })
110}
111
112/// # `POST /_matrix/client/v3/register`
113///
114/// Register an account on this homeserver.
115///
116/// You can use [`GET
117/// /_matrix/client/v3/register/available`](fn.get_register_available_route.
118/// html) to check if the user id is valid and available.
119///
120/// - Only works if registration is enabled
121/// - If type is guest: ignores all parameters except
122///   initial_device_display_name
123/// - If sender is not appservice: Requires UIAA (but we only use a dummy stage)
124/// - If type is not guest and no username is given: Always fails after UIAA
125///   check
126/// - Creates a new account and populates it with default account data
127/// - If `inhibit_login` is false: Creates a device and returns device id and
128///   access_token
129#[expect(clippy::doc_markdown)]
130#[tracing::instrument(skip_all, fields(%client), name = "register")]
131pub(crate) async fn register_route(
132	State(services): State<crate::State>,
133	ClientIp(client): ClientIp,
134	body: Ruma<register::v3::Request>,
135) -> Result<register::v3::Response> {
136	let is_guest = body.kind == RegistrationKind::Guest;
137	let emergency_mode_enabled = services.config.emergency_password.is_some();
138
139	let user = body.username.as_deref().unwrap_or("");
140	let device_name = body
141		.initial_device_display_name
142		.as_deref()
143		.unwrap_or("");
144
145	if !services.config.allow_registration && body.appservice_info.is_none() {
146		info!(
147			%is_guest,
148			%user,
149			%device_name,
150			"Rejecting registration attempt as registration is disabled"
151		);
152
153		return Err!(Request(Forbidden("Registration has been disabled.")));
154	}
155
156	if is_guest && !services.config.allow_guest_registration {
157		debug_warn!(
158			%device_name,
159			"Guest registration disabled, rejecting guest registration attempt"
160		);
161
162		return Err!(Request(GuestAccessForbidden("Guest registration is disabled.")));
163	}
164
165	let user_id = match (body.username.as_ref(), is_guest) {
166		| (Some(username), false) => {
167			// workaround for https://github.com/matrix-org/matrix-appservice-irc/issues/1780 due to inactivity of fixing the issue
168			let is_matrix_appservice_irc =
169				body.appservice_info
170					.as_ref()
171					.is_some_and(|appservice| {
172						appservice.registration.id == "irc"
173							|| appservice
174								.registration
175								.id
176								.contains("matrix-appservice-irc")
177							|| appservice
178								.registration
179								.id
180								.contains("matrix_appservice_irc")
181					});
182
183			if services
184				.config
185				.forbidden_usernames
186				.is_match(username)
187				&& !emergency_mode_enabled
188			{
189				return Err!(Request(Forbidden("Username is forbidden")));
190			}
191
192			// don't force the username lowercase if it's from matrix-appservice-irc
193			let body_username = if is_matrix_appservice_irc {
194				username.clone()
195			} else {
196				username.to_lowercase()
197			};
198
199			let proposed_user_id = match UserId::parse_with_server_name(
200				&body_username,
201				services.globals.server_name(),
202			) {
203				| Ok(user_id) => {
204					if let Err(e) = user_id.validate_strict() {
205						// unless the username is from the broken matrix appservice IRC bridge, or
206						// we are in emergency mode, we should follow synapse's behaviour on
207						// not allowing things like spaces and UTF-8 characters in usernames
208						if !is_matrix_appservice_irc && !emergency_mode_enabled {
209							return Err!(Request(InvalidUsername(debug_warn!(
210								"Username {body_username} contains disallowed characters or \
211								 spaces: {e}"
212							))));
213						}
214					}
215
216					user_id
217				},
218				| Err(e) => {
219					return Err!(Request(InvalidUsername(debug_warn!(
220						"Username {body_username} is not valid: {e}"
221					))));
222				},
223			};
224
225			if services.users.exists(&proposed_user_id).await {
226				return Err!(Request(UserInUse("User ID is not available.")));
227			}
228
229			proposed_user_id
230		},
231		| _ => loop {
232			let proposed_user_id = UserId::parse_with_server_name(
233				utils::random_string(RANDOM_USER_ID_LENGTH).to_lowercase(),
234				services.globals.server_name(),
235			)?;
236
237			if !services.users.exists(&proposed_user_id).await {
238				break proposed_user_id;
239			}
240		},
241	};
242
243	if body.body.login_type == Some(LoginType::ApplicationService) {
244		match body.appservice_info {
245			| Some(ref info) =>
246				if !info.is_user_match(&user_id) && !emergency_mode_enabled {
247					return Err!(Request(Exclusive(
248						"Username is not in an appservice namespace."
249					)));
250				},
251			| _ => {
252				return Err!(Request(MissingToken("Missing appservice token.")));
253			},
254		}
255	} else if services
256		.appservice
257		.is_exclusive_user_id(&user_id)
258		.await && !emergency_mode_enabled
259	{
260		return Err!(Request(Exclusive("Username is reserved by an appservice.")));
261	}
262
263	// UIAA
264	let mut uiaainfo;
265	let skip_auth = if services.registration_tokens.is_enabled().await && !is_guest {
266		// Registration token required
267		uiaainfo = UiaaInfo {
268			flows: vec![AuthFlow {
269				stages: vec![AuthType::RegistrationToken],
270			}],
271			completed: Vec::new(),
272			params: Default::default(),
273			session: None,
274			auth_error: None,
275		};
276
277		body.appservice_info.is_some()
278	} else {
279		// No registration token necessary, but clients must still go through the flow
280		uiaainfo = UiaaInfo {
281			flows: vec![AuthFlow { stages: vec![AuthType::Dummy] }],
282			completed: Vec::new(),
283			params: Default::default(),
284			session: None,
285			auth_error: None,
286		};
287
288		body.appservice_info.is_some() || is_guest
289	};
290
291	if !skip_auth {
292		match &body.auth {
293			| Some(auth) => {
294				let (worked, uiaainfo) = services
295					.uiaa
296					.try_auth(
297						&UserId::parse_with_server_name("", services.globals.server_name())?,
298						"".into(),
299						auth,
300						&uiaainfo,
301					)
302					.await?;
303				if !worked {
304					return Err(Error::Uiaa(uiaainfo));
305				}
306				// Success!
307			},
308			| _ => match body.json_body {
309				| Some(ref json) => {
310					uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
311					services.uiaa.create(
312						&UserId::parse_with_server_name("", services.globals.server_name())?,
313						"".into(),
314						&uiaainfo,
315						json,
316					);
317					return Err(Error::Uiaa(uiaainfo));
318				},
319				| _ => {
320					return Err!(Request(NotJson("JSON body is not valid")));
321				},
322			},
323		}
324	}
325
326	let password = if is_guest { None } else { body.password.as_deref() };
327
328	services
329		.users
330		.full_register(Register {
331			user_id: Some(&user_id),
332			password,
333			is_appservice: body.appservice_info.is_some(),
334			is_guest,
335			grant_first_user_admin: true,
336			..Default::default()
337		})
338		.await?;
339
340	if (!is_guest && body.inhibit_login)
341		|| body
342			.appservice_info
343			.as_ref()
344			.is_some_and(|appservice| appservice.registration.device_management)
345	{
346		return Ok(register::v3::Response {
347			user_id,
348			device_id: None,
349			access_token: None,
350			refresh_token: None,
351			expires_in: None,
352		});
353	}
354
355	let device_id = if is_guest { None } else { body.device_id.as_deref() };
356
357	// Generate new token for the device
358	let (access_token, expires_in) = services
359		.users
360		.generate_access_token(body.refresh_token);
361
362	// Generate a new refresh_token if requested by client
363	let refresh_token = expires_in.is_some().then(generate_refresh_token);
364
365	// Create device for this account
366	let device_id = services
367		.users
368		.create_device(
369			&user_id,
370			device_id,
371			(Some(&access_token), expires_in),
372			refresh_token.as_deref(),
373			body.initial_device_display_name.as_deref(),
374			Some(client),
375		)
376		.await?;
377
378	debug_info!(%user_id, %device_id, "User account was created");
379
380	if body.appservice_info.is_none() && (!is_guest || services.config.log_guest_registrations) {
381		let mut notice = String::from(if is_guest { "New guest user" } else { "New user" });
382
383		write!(notice, " \"{user_id}\" registered on this server from IP {client}")?;
384
385		if let Some(device_name) = body.initial_device_display_name.as_deref() {
386			write!(notice, " with device name {device_name}")?;
387		}
388
389		if !is_guest {
390			info!("{notice}");
391		} else {
392			debug_info!("{notice}");
393		}
394
395		if services.server.config.admin_room_notices {
396			services.admin.notice(&notice).await;
397		}
398	}
399
400	Ok(register::v3::Response {
401		user_id,
402		device_id: Some(device_id),
403		access_token: Some(access_token),
404		refresh_token,
405		expires_in,
406	})
407}
408
409/// # `GET /_matrix/client/v1/register/m.login.registration_token/validity`
410///
411/// Checks if the provided registration token is valid at the time of checking
412///
413/// Currently does not have any ratelimiting, and this isn't very practical as
414/// there is only one registration token allowed.
415pub(crate) async fn check_registration_token_validity(
416	State(services): State<crate::State>,
417	body: Ruma<check_registration_token_validity::v1::Request>,
418) -> Result<check_registration_token_validity::v1::Response> {
419	if !services.registration_tokens.is_enabled().await {
420		return Err!(Request(Forbidden("Server does not allow token registration")));
421	}
422
423	let valid = services
424		.registration_tokens
425		.is_token_valid(&body.token)
426		.await
427		.is_ok();
428
429	Ok(check_registration_token_validity::v1::Response { valid })
430}