Skip to main content

tuwunel_api/client/session/
password.rs

1use futures::{FutureExt, TryFutureExt};
2use ruma::{
3	OwnedUserId, UserId,
4	api::client::{
5		session::login::v3::{Password, Request},
6		uiaa,
7	},
8};
9use tuwunel_core::{Err, Result, debug_error, err, utils::hash};
10use tuwunel_service::Services;
11
12use super::ldap_login;
13use crate::Ruma;
14
15pub(super) async fn handle_login(
16	services: &Services,
17	body: &Ruma<Request>,
18	info: &Password,
19) -> Result<OwnedUserId> {
20	#[expect(deprecated)]
21	let Password { identifier, password, user, .. } = info;
22
23	let user_id = if let Some(uiaa::UserIdentifier::Matrix(uiaa::MatrixUserIdentifier {
24		user: user_id,
25		..
26	})) = identifier
27	{
28		UserId::parse_with_server_name(user_id, &services.config.server_name)
29	} else if let Some(user) = user {
30		UserId::parse_with_server_name(user, &services.config.server_name)
31	} else {
32		return Err!(Request(Unknown(debug_warn!(
33			?body.login_info,
34			"Valid identifier or username was not provided (invalid or unsupported login type?)"
35		))));
36	}
37	.map_err(|e| err!(Request(InvalidUsername(warn!("Username is invalid: {e}")))))?;
38
39	let lowercased_user_id = UserId::parse_with_server_name(
40		user_id.localpart().to_lowercase(),
41		&services.config.server_name,
42	)?;
43
44	let user_is_remote = !services.globals.user_is_local(&user_id)
45		|| !services
46			.globals
47			.user_is_local(&lowercased_user_id);
48
49	if user_is_remote {
50		return Err!(Request(Unknown("User ID does not belong to this homeserver")));
51	}
52
53	if cfg!(feature = "ldap") && services.config.ldap.enable {
54		ldap_login(services, &user_id, &lowercased_user_id, password)
55			.boxed()
56			.await
57	} else {
58		password_login(services, &user_id, &lowercased_user_id, password).await
59	}
60}
61
62/// Authenticates the given user by its ID and its password.
63///
64/// Returns the user ID if successful, and an error otherwise.
65#[tracing::instrument(skip_all, fields(%user_id), name = "password")]
66pub(super) async fn password_login(
67	services: &Services,
68	user_id: &UserId,
69	lowercased_user_id: &UserId,
70	password: &str,
71) -> Result<OwnedUserId> {
72	// Restrict login to accounts only of type 'password', including untyped
73	// legacy accounts which are equivalent to 'password'.
74	if services
75		.users
76		.origin(user_id)
77		.await
78		.is_ok_and(|origin| origin != "password")
79	{
80		return Err!(Request(Forbidden("Account does not permit password login.")));
81	}
82
83	let (hash, user_id) = services
84		.users
85		.password_hash(user_id)
86		.map_ok(|hash| (hash, user_id))
87		.or_else(|_| {
88			services
89				.users
90				.password_hash(lowercased_user_id)
91				.map_ok(|hash| (hash, lowercased_user_id))
92		})
93		.map_err(|_| err!(Request(Forbidden("Wrong username or password."))))
94		.await?;
95
96	if hash.is_empty() {
97		return Err!(Request(UserDeactivated("The user has been deactivated")));
98	}
99
100	hash::verify_password(password, &hash)
101		.inspect_err(|e| debug_error!("{e}"))
102		.map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?;
103
104	Ok(user_id.to_owned())
105}