arminius_server_client/handlers/
test_login.rs1#![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("Provided username is invalid.")]
71 #[matrix_api_error(status_code = StatusCode::BAD_REQUEST, error_kind = ErrorKind::InvalidUsername)]
72 InvalidUsername,
73
74 #[error("Missing required username")]
76 #[matrix_api_error(status_code = StatusCode::BAD_REQUEST, error_kind = ErrorKind::MissingParam)]
77 MissingUsername,
78
79 #[error("Invalid device ID")]
83 #[matrix_api_error(status_code = StatusCode::FORBIDDEN, error_kind = ErrorKind::Forbidden)]
84 InvalidDeviceId,
85
86 #[error("Invalid credentials")]
88 #[matrix_api_error(status_code = StatusCode::FORBIDDEN, error_kind = ErrorKind::Forbidden)]
89 UnauthorizedLogin,
90
91 #[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("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
105fn 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#[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 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
156enum RegisterResponse {
159 NormalResponse(account::register::v3::Response),
161 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
188async 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 if transaction.fetch_user(&user_id).await?.is_some() {
219 return Err(LoginError::UserInUse);
220 }
221
222 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 Some(ruma::api::client::uiaa::AuthData::Dummy(dummy)) if dummy.session.is_none() => {
239 return Err(LoginError::MissingSession);
240 }
241 _ => {
242 }
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
268async 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
305async 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#[derive(Debug, thiserror::Error, MatrixApiError, FromDbError)]
315enum GetUsernameAvailabilityError {
316 #[error(transparent)]
319 InternalServerError(#[from] InternalServerError),
320 #[error("Username is already taken")]
322 #[matrix_api_error(status_code = StatusCode::BAD_REQUEST, error_kind = ErrorKind::UserInUse)]
323 UserInUse,
324 #[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
330async 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#[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}