Skip to main content

tuwunel_api_client/session/
password.rs

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