Skip to main content

arminius_server_client/handlers/
test_login.rs

1// SPDX-FileCopyrightText: 2025 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Insecure registration and login endpoints for testing with Complement and
6//! friends.
7
8#![allow(
9	clippy::allow_attributes,
10	reason = "This source file contains enums which allow `clippy::missing_docs_in_private_items`. But an exception to this lint will mistakenly report that it's unfulfilled when used with expect, even though it is not."
11)]
12
13mod tests;
14
15use std::fmt::Debug;
16
17use arminius_lib_authentication::test_utils::generate_auth_token;
18use arminius_lib_component::{
19	Component, ServerState,
20	error::{FeatureNotImplemented, InternalServerError, MatrixApiError, ResultExt as _},
21	request_source::RequestSource,
22	ruma_requests::ArminiusResponse,
23};
24use arminius_lib_db::Database;
25use arminius_lib_db_devices::DeviceQueries as _;
26use arminius_lib_db_keys::UserKeysQueries as _;
27use arminius_lib_db_user::{InsertUserError, UserQueries as _};
28use arminius_lib_macros::FromDbError;
29use axum::{
30	Router,
31	extract::State,
32	http::StatusCode,
33	response::IntoResponse,
34	routing::{get, post},
35};
36use ruma::{
37	DeviceId, OwnedDeviceId, OwnedUserId, UserId,
38	api::{
39		client::{
40			account::{self, get_username_availability},
41			session::{
42				self,
43				get_login_types::{
44					self,
45					v3::{LoginType, PasswordLoginType},
46				},
47				login::v3::LoginInfo,
48			},
49			uiaa::{AuthFlow, AuthType, UiaaInfo, UiaaResponse, UserIdentifier},
50		},
51		error::ErrorKind,
52	},
53	user_id::localpart_is_fully_conforming,
54};
55
56use crate::client_request::{AppserviceTokenOptionalClientRequest, UnauthenticatedClientRequest};
57
58#[allow(
59	missing_docs,
60	clippy::missing_docs_in_private_items,
61	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
62)]
63#[derive(Debug, thiserror::Error, MatrixApiError, FromDbError)]
64enum LoginError {
65	#[error(transparent)]
66	FeatureNotImplemented(#[from] FeatureNotImplemented),
67
68	/// Error thrown when the username does not comply with the specs (See
69	/// <https://spec.matrix.org/v1.15/appendices/#user-identifiers>).
70	#[error("Provided username is invalid.")]
71	#[matrix_api_error(status_code = StatusCode::BAD_REQUEST, error_kind = ErrorKind::InvalidUsername)]
72	InvalidUsername,
73
74	/// Error thrown when the username is missing in a request.
75	#[error("Missing required username")]
76	#[matrix_api_error(status_code = StatusCode::BAD_REQUEST, error_kind = ErrorKind::MissingParam)]
77	MissingUsername,
78
79	/// Error thrown when the given device ID is invalid. For now, this can only
80	/// be the case if it is the same as one of the user's cross-signing key's
81	/// ID (See <https://spec.matrix.org/v1.16/client-server-api/#key-and-signature-security>).
82	#[error("Invalid device ID")]
83	#[matrix_api_error(status_code = StatusCode::FORBIDDEN, error_kind = ErrorKind::Forbidden)]
84	InvalidDeviceId,
85
86	/// Error thrown when the credentials to login are invalid.
87	#[error("Invalid credentials")]
88	#[matrix_api_error(status_code = StatusCode::FORBIDDEN, error_kind = ErrorKind::Forbidden)]
89	UnauthorizedLogin,
90
91	/// Error thrown when the desired user ID is already taken.
92	#[error("Desired user ID is already taken.")]
93	#[matrix_api_error(status_code = StatusCode::BAD_REQUEST, error_kind = ErrorKind::UserInUse)]
94	UserInUse,
95
96	/// Error thrown when the desired user ID is already taken.
97	#[error("Missing session in UIA request")]
98	#[matrix_api_error(status_code = StatusCode::UNAUTHORIZED, error_kind = ErrorKind::MissingParam)]
99	MissingSession,
100
101	#[error(transparent)]
102	InternalServerError(#[from] InternalServerError),
103}
104
105/// Tries at most 3 times to generate a new [DeviceId] that does not conflict
106/// with the given `forbidden_ids`.
107fn try_generate_device_id(forbidden_ids: &[String]) -> Option<OwnedDeviceId> {
108	for _ in 0..3 {
109		let device_id = DeviceId::new();
110
111		if forbidden_ids.iter().any(|id| id.as_str() == device_id.as_str()) {
112			continue;
113		}
114
115		return Some(device_id);
116	}
117
118	None
119}
120
121/// Create a session, optionally with a user-provided device ID.
122///
123/// For more information about device IDs, see
124/// <https://spec.matrix.org/v1.14/client-server-api/#post_matrixclientv3login>
125#[tracing::instrument(skip(database))]
126async fn create_session(
127	database: &mut impl Database,
128	user_id: OwnedUserId,
129	device_id: Option<OwnedDeviceId>,
130) -> Result<(OwnedDeviceId, String), LoginError> {
131	let cross_signing_keys_ids = database.fetch_cross_signing_keys_ids_for_user(&user_id).await?;
132
133	let device_id = match device_id {
134		Some(device_id) => {
135			if cross_signing_keys_ids.iter().any(|id| id.as_str() == device_id.as_str()) {
136				return Err(LoginError::InvalidDeviceId);
137			}
138
139			device_id
140		}
141		None => try_generate_device_id(&cross_signing_keys_ids)
142			.unrecoverable_error("Failed generating a new device ID")?,
143	};
144
145	// Should be an upsert, but all this code is targeted for the bin anyway
146	if database.fetch_device_for_user(&user_id, &device_id).await?.is_none() {
147		database.insert_device_for_user(&user_id, &device_id).await?;
148	};
149
150	let access_token = generate_auth_token(&user_id, &device_id)
151		.unrecoverable_error("Generating authentication token")?;
152
153	Ok((device_id, access_token))
154}
155
156/// A response to a registration - either the registration is successful, or we still require
157/// user-interactive authentication.
158enum RegisterResponse {
159	/// Normal response
160	NormalResponse(account::register::v3::Response),
161	/// User-interactive authentication is required
162	UiaaResponse(UiaaResponse),
163}
164
165impl IntoResponse for RegisterResponse {
166	fn into_response(self) -> axum::response::Response {
167		match self {
168			RegisterResponse::NormalResponse(response) => {
169				ArminiusResponse(response).into_response()
170			}
171			RegisterResponse::UiaaResponse(response) => ArminiusResponse(response).into_response(),
172		}
173	}
174}
175
176impl From<account::register::v3::Response> for RegisterResponse {
177	fn from(value: account::register::v3::Response) -> Self {
178		RegisterResponse::NormalResponse(value)
179	}
180}
181
182impl From<UiaaResponse> for RegisterResponse {
183	fn from(value: UiaaResponse) -> Self {
184		RegisterResponse::UiaaResponse(value)
185	}
186}
187
188/// Handler for `POST /_matrix/client/v3/register` (See
189/// <https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3register>).
190///
191/// Endpoint used to register a new matrix user.
192async fn register_handler(
193	mut state: State<ServerState>,
194	RequestSource(source): RequestSource,
195	AppserviceTokenOptionalClientRequest(request): AppserviceTokenOptionalClientRequest<
196		account::register::v3::Request,
197	>,
198) -> Result<RegisterResponse, LoginError> {
199	tracing::info!("Registering user with address {source:?}");
200
201	let user_id = match request.username.clone() {
202		Some(localpart) => {
203			let localpart = localpart.to_lowercase();
204			if localpart_is_fully_conforming(&localpart) != Ok(true) {
205				return Err(LoginError::InvalidUsername);
206			}
207			UserId::parse_with_server_name(localpart, &state.config.server_name)
208				.map_err(|_| LoginError::InvalidUsername)?
209		}
210		None => UserId::new(&state.config.server_name),
211	};
212
213	let mut transaction = state.database.begin_transaction().await?;
214
215	// We check this before UIA to be Complement and spec compliant
216	// It's not a security issue, because you can use the `/register/available` endpoint the same
217	// way anyway.
218	if transaction.fetch_user(&user_id).await?.is_some() {
219		return Err(LoginError::UserInUse);
220	}
221
222	// We implement a dummy version of the user-interactive authentication API when performing
223	// registration. We don't actually implement m.login.dummy. What we have is a dummy
224	// implementation of m.login.dummy. :D
225	//
226	// We just implement this because the `register` function in Complement expects to be turned
227	// away with a 401 first time it attempts to register.
228	match request.auth {
229		None => {
230			let mut info = UiaaInfo::new(vec![AuthFlow::new(vec![AuthType::Dummy])]);
231			info.session =
232				Some("Want to come to my 62nd birthay? It'll only take a minute.".to_owned());
233			let response = UiaaResponse::AuthResponse(info);
234			return Ok(response.into());
235		}
236		// We just do this one check to pass a Complement test.
237		// Arguably, what's the point?
238		Some(ruma::api::client::uiaa::AuthData::Dummy(dummy)) if dummy.session.is_none() => {
239			return Err(LoginError::MissingSession);
240		}
241		_ => {
242			// If any sort of `auth` data is included, we accept it.
243		}
244	}
245
246	transaction.insert_new_user(&user_id, request.username.as_deref()).await.map_err(
247		|error| -> LoginError {
248			match error {
249				InsertUserError::UserIdTaken => LoginError::UserInUse,
250				InsertUserError::DatabaseError(error) => error.into(),
251			}
252		},
253	)?;
254
255	let (device_id, access_token) =
256		create_session(&mut transaction, user_id.clone(), request.device_id)
257			.await
258			.unrecoverable_error("Creating session")?;
259
260	transaction.commit().await?;
261
262	let mut response = account::register::v3::Response::new(user_id);
263	response.device_id = Some(device_id);
264	response.access_token = Some(access_token);
265	Ok(response.into())
266}
267
268/// Handler for `POST /_matrix/client/v3/login` (See
269/// <https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3login>).
270///
271/// Endpoint used to sign-in a Matrix user.
272///
273/// Warning: Does not check whether the password is correct.
274async fn login_handler(
275	mut state: State<ServerState>,
276	RequestSource(source): RequestSource,
277	AppserviceTokenOptionalClientRequest(request): AppserviceTokenOptionalClientRequest<
278		session::login::v3::Request,
279	>,
280) -> Result<ArminiusResponse<session::login::v3::Response>, LoginError> {
281	tracing::info!("Logging in user with address {source:?}");
282	let LoginInfo::Password(login_info) = request.login_info else {
283		return Err(FeatureNotImplemented("Non-password login".to_owned()).into());
284	};
285
286	let Some(localpart) = login_info.identifier else {
287		return Err(LoginError::MissingUsername);
288	};
289
290	let UserIdentifier::Matrix(localpart) = localpart else {
291		return Err(FeatureNotImplemented("You can only sign in with usernames".to_owned()).into());
292	};
293
294	let user_id = UserId::parse_with_server_name(localpart.user, &state.config.server_name)
295		.map_err(|_| LoginError::InvalidUsername)?;
296
297	let (device_id, access_token) =
298		create_session(&mut state.database, user_id.clone(), request.device_id)
299			.await
300			.map_err(|_| LoginError::UnauthorizedLogin)?;
301
302	Ok(session::login::v3::Response::new(user_id, access_token, device_id).into())
303}
304
305/// Handler for `GET /_matrix/client/v3/login` (See
306/// <https://spec.matrix.org/latest/client-server-api/#get_matrixclientv3login>).
307///
308/// Endpoint used to list the permitted login types.
309async fn get_login_types_handler() -> ArminiusResponse<get_login_types::v3::Response> {
310	get_login_types::v3::Response::new(vec![LoginType::Password(PasswordLoginType::new())]).into()
311}
312
313/// Error type for `get_username_availability_handler`
314#[derive(Debug, thiserror::Error, MatrixApiError, FromDbError)]
315enum GetUsernameAvailabilityError {
316	/// Unrecoverable error.
317	/// See <https://bundesmessenger.gitlab.io/backend/neo/arminius/arminius/03-development/errors.html#unrecoverable-errors>
318	#[error(transparent)]
319	InternalServerError(#[from] InternalServerError),
320	/// Could not parse user ID we're checking for availability
321	#[error("Username is already taken")]
322	#[matrix_api_error(status_code = StatusCode::BAD_REQUEST, error_kind = ErrorKind::UserInUse)]
323	UserInUse,
324	/// Could not parse user ID we're checking for availability
325	#[error("Could not parse user ID: {0}")]
326	#[matrix_api_error(status_code = StatusCode::BAD_REQUEST, error_kind = ErrorKind::InvalidUsername)]
327	IdParseError(#[from] ruma::IdParseError),
328}
329
330/// Handler for `GET /_matrix/client/v3/register/available` (See
331/// <https://spec.matrix.org/latest/client-server-api/#get_matrixclientv3registeravailable>).
332///
333/// Endpoint which lets you check whether a username is available
334async fn get_username_availability_handler(
335	mut state: State<ServerState>,
336	UnauthenticatedClientRequest(request): UnauthenticatedClientRequest<
337		get_username_availability::v3::Request,
338	>,
339) -> Result<ArminiusResponse<get_username_availability::v3::Response>, GetUsernameAvailabilityError>
340{
341	let username = UserId::parse_with_server_name(request.username, &state.config.server_name)?;
342	username.validate_strict()?;
343	if state.database.fetch_user(&username).await?.is_some() {
344		return Err(GetUsernameAvailabilityError::UserInUse);
345	}
346	Ok(get_username_availability::v3::Response::new(true).into())
347}
348
349/// Component for insecure login endpoint
350#[derive(Debug)]
351pub struct TestLoginComponent;
352
353impl Component for TestLoginComponent {
354	fn router() -> Router<ServerState> {
355		Router::new()
356			.route("/_matrix/client/v3/register", post(register_handler))
357			.route("/_matrix/client/v3/register/available", get(get_username_availability_handler))
358			.route("/_matrix/client/v3/login", get(get_login_types_handler).post(login_handler))
359	}
360}