1use std::{
2 collections::BTreeMap,
3 ops::ControlFlow,
4 sync::{Arc, RwLock},
5};
6
7use futures::{TryStreamExt, pin_mut};
8use ruma::{
9 CanonicalJsonValue, DeviceId, OwnedDeviceId, OwnedUserId, UserId,
10 api::{
11 client::uiaa::{
12 AuthData, AuthType, EmailIdentity, Password, ThirdpartyIdCredentials, UiaaInfo,
13 UserIdentifier,
14 },
15 error::{ErrorKind, StandardErrorBody},
16 },
17};
18use tuwunel_core::{
19 Err, Result, err, error, extract, implement,
20 utils::{self, BoolExt, hash::verify_password, string::EMPTY},
21};
22use tuwunel_database::{Deserialized, Json, Map};
23
24pub struct Service {
25 userdevicesessionid_uiaarequest: RwLock<RequestMap>,
26 db: Data,
27 services: Arc<crate::services::OnceServices>,
28}
29
30struct Data {
31 userdevicesessionid_uiaainfo: Arc<Map>,
32}
33
34type RequestMap = BTreeMap<RequestKey, CanonicalJsonValue>;
35type RequestKey = (OwnedUserId, OwnedDeviceId, String);
36
37pub const SESSION_ID_LENGTH: usize = 32;
38
39#[derive(Clone, Copy)]
40enum EmailIdentityMode {
41 Validate,
42 Claim,
43}
44
45impl crate::Service for Service {
46 fn build(args: &crate::Args<'_>) -> Result<Arc<Self>> {
47 Ok(Arc::new(Self {
48 userdevicesessionid_uiaarequest: RwLock::new(RequestMap::new()),
49 db: Data {
50 userdevicesessionid_uiaainfo: args.db["userdevicesessionid_uiaainfo"].clone(),
51 },
52 services: args.services.clone(),
53 }))
54 }
55
56 fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
57}
58
59#[implement(Service)]
61pub fn create(
62 &self,
63 user_id: &UserId,
64 device_id: &DeviceId,
65 uiaainfo: &UiaaInfo,
66 json_body: &CanonicalJsonValue,
67) {
68 let session = uiaainfo
71 .session
72 .as_ref()
73 .expect("session should be set");
74
75 self.set_uiaa_request(user_id, device_id, session, json_body);
76
77 self.update_uiaa_session(user_id, device_id, session, Some(uiaainfo));
78}
79
80#[implement(Service)]
85pub async fn try_auth(
86 &self,
87 user_id: &UserId,
88 device_id: &DeviceId,
89 auth: &AuthData,
90 uiaainfo: &UiaaInfo,
91) -> Result<(bool, UiaaInfo)> {
92 self.try_auth_inner(user_id, device_id, auth, uiaainfo, EmailIdentityMode::Validate)
93 .await
94}
95
96#[implement(Service)]
101pub async fn try_auth_registration(
102 &self,
103 user_id: &UserId,
104 device_id: &DeviceId,
105 auth: &AuthData,
106 uiaainfo: &UiaaInfo,
107) -> Result<(bool, UiaaInfo)> {
108 self.try_auth_inner(user_id, device_id, auth, uiaainfo, EmailIdentityMode::Claim)
109 .await
110}
111
112#[implement(Service)]
113async fn try_auth_inner(
114 &self,
115 user_id: &UserId,
116 device_id: &DeviceId,
117 auth: &AuthData,
118 uiaainfo: &UiaaInfo,
119 email_identity_mode: EmailIdentityMode,
120) -> Result<(bool, UiaaInfo)> {
121 let mut uiaainfo = if let Some(session) = auth.session() {
122 self.get_uiaa_session(user_id, device_id, session)
123 .await?
124 } else {
125 uiaainfo.clone()
126 };
127
128 if uiaainfo.session.is_none() {
129 uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
130 }
131
132 match auth {
133 | AuthData::Password(password) => {
135 if let ControlFlow::Break(authed) = self
136 .verify_password(user_id, &mut uiaainfo, password)
137 .await?
138 {
139 return Ok((authed, uiaainfo));
140 }
141 },
142 | AuthData::RegistrationToken(t) => {
143 let token = t.token.trim();
144 if self
145 .services
146 .registration_tokens
147 .try_consume(token)
148 .await
149 .is_ok()
150 {
151 uiaainfo
152 .completed
153 .push(AuthType::RegistrationToken);
154 } else {
155 uiaainfo.auth_error = Some(Box::new(StandardErrorBody {
156 kind: ErrorKind::forbidden(),
157 message: "Invalid registration token.".to_owned(),
158 }));
159
160 return Ok((false, uiaainfo));
161 }
162 },
163 | AuthData::FallbackAcknowledgement(_session) => {
164 },
167 | AuthData::OAuth(_) => {
168 if !uiaainfo.completed.contains(&AuthType::OAuth) {
171 if self
172 .services
173 .users
174 .can_replace_cross_signing_keys(user_id)
175 .await
176 {
177 uiaainfo.completed.push(AuthType::OAuth);
178 } else {
179 uiaainfo.auth_error = Some(Box::new(StandardErrorBody {
180 kind: ErrorKind::forbidden(),
181 message: "OAuth cross-signing reset not approved for this session."
182 .to_owned(),
183 }));
184
185 return Ok((false, uiaainfo));
186 }
187 }
188 },
189 | AuthData::Dummy(_) => {
190 uiaainfo.completed.push(AuthType::Dummy);
191 },
192 | AuthData::Terms(_) => {
193 uiaainfo.completed.push(AuthType::Terms);
195 },
196 | AuthData::EmailIdentity(EmailIdentity { thirdparty_id_creds, .. }) => {
197 let validated = self
199 .authenticate_email_identity(
200 user_id,
201 device_id,
202 &uiaainfo,
203 thirdparty_id_creds,
204 email_identity_mode,
205 )
206 .await?;
207
208 if !validated {
209 uiaainfo.auth_error = Some(Box::new(StandardErrorBody {
210 kind: ErrorKind::forbidden(),
211 message: "Email address has not been validated.".to_owned(),
212 }));
213
214 return Ok((false, uiaainfo));
215 }
216
217 uiaainfo.completed.push(AuthType::EmailIdentity);
218 },
219 | auth => error!("AuthData type not supported: {auth:?}"),
220 }
221
222 let mut completed = false;
224 'flows: for flow in &mut uiaainfo.flows {
225 for stage in &flow.stages {
226 if !uiaainfo.completed.contains(stage) {
227 continue 'flows;
228 }
229 }
230 completed = true;
232 }
233
234 let session = uiaainfo
235 .session
236 .as_ref()
237 .expect("session is always set");
238
239 if matches!(email_identity_mode, EmailIdentityMode::Claim)
240 && !matches!(auth, AuthData::EmailIdentity(_))
241 && uiaainfo
242 .completed
243 .contains(&AuthType::EmailIdentity)
244 {
245 let claim = (user_id.to_owned(), device_id.to_owned(), session.as_str().into());
246
247 if !self
248 .services
249 .threepid
250 .refresh_claim(&claim)
251 .await?
252 {
253 uiaainfo
254 .completed
255 .retain(|stage| stage != &AuthType::EmailIdentity);
256
257 uiaainfo.auth_error = Some(Box::new(StandardErrorBody {
258 kind: ErrorKind::forbidden(),
259 message: "Email address has not been validated.".to_owned(),
260 }));
261
262 self.update_uiaa_session(user_id, device_id, session, Some(&uiaainfo));
263
264 return Ok((false, uiaainfo));
265 }
266 }
267
268 if !completed {
269 self.update_uiaa_session(user_id, device_id, session, Some(&uiaainfo));
270
271 return Ok((false, uiaainfo));
272 }
273
274 let retain_session = matches!(email_identity_mode, EmailIdentityMode::Claim)
276 && uiaainfo
277 .completed
278 .contains(&AuthType::EmailIdentity);
279
280 self.update_uiaa_session(user_id, device_id, session, retain_session.then_some(&uiaainfo));
281
282 Ok((true, uiaainfo))
283}
284
285#[implement(Service)]
286async fn authenticate_email_identity(
287 &self,
288 user_id: &UserId,
289 device_id: &DeviceId,
290 uiaainfo: &UiaaInfo,
291 creds: &ThirdpartyIdCredentials,
292 mode: EmailIdentityMode,
293) -> Result<bool> {
294 match mode {
295 | EmailIdentityMode::Validate => Ok(self
296 .services
297 .threepid
298 .session_validated(creds.sid.as_str(), creds.client_secret.as_str())
299 .await),
300 | EmailIdentityMode::Claim => {
301 let session = uiaainfo
302 .session
303 .as_ref()
304 .expect("session is always set");
305
306 let claim = (user_id.to_owned(), device_id.to_owned(), session.as_str().into());
307
308 self.services
309 .threepid
310 .claim_validated(creds.sid.as_str(), creds.client_secret.as_str(), claim)
311 .await
312 },
313 }
314}
315
316#[implement(Service)]
317async fn verify_password(
318 &self,
319 user_id: &UserId,
320 uiaainfo: &mut UiaaInfo,
321 password: &Password,
322) -> Result<ControlFlow<bool>> {
323 let Password { identifier, password, user, .. } = password;
324
325 let username = extract!(identifier, x in Some(UserIdentifier::Matrix(ruma::api::client::uiaa::MatrixUserIdentifier { user: x, .. })))
326 .or_else(|| cfg!(feature = "element_hacks").and(user.as_ref()))
327 .ok_or(err!(Request(Unrecognized("Identifier type not recognized."))))?;
328
329 let user_id_from_username =
330 UserId::parse_with_server_name(username.clone(), self.services.globals.server_name())
331 .map_err(|_| err!(Request(InvalidParam("User ID is invalid."))))?;
332
333 if user_id.localpart() != user_id_from_username.localpart() {
335 return Err!(Request(Forbidden("User ID and access token mismatch.")));
336 }
337
338 let user_id = user_id_from_username;
339 let password_verified = self
341 .services
342 .users
343 .password_hash(&user_id)
344 .await
345 .is_ok_and(|hash| verify_password(password, &hash).is_ok());
346
347 #[cfg(feature = "ldap")]
350 let password_verified = if !password_verified
351 && self.services.server.config.ldap.enable
352 && self
353 .services
354 .users
355 .origin(&user_id)
356 .await
357 .is_ok_and(|origin| origin == "ldap")
358 && let Ok(dns) = self.services.users.search_ldap(&user_id).await
359 && let Some((user_dn, _is_admin)) = dns.first()
360 {
361 self.services
362 .users
363 .auth_ldap(user_dn, password)
364 .await
365 .is_ok()
366 } else {
367 password_verified
368 };
369
370 if !password_verified {
371 uiaainfo.auth_error = Some(Box::new(StandardErrorBody {
372 kind: ErrorKind::forbidden(),
373 message: "Invalid username or password.".to_owned(),
374 }));
375
376 return Ok(ControlFlow::Break(false));
377 }
378
379 uiaainfo.completed.push(AuthType::Password);
380
381 Ok(ControlFlow::Continue(()))
382}
383
384#[implement(Service)]
385fn set_uiaa_request(
386 &self,
387 user_id: &UserId,
388 device_id: &DeviceId,
389 session: &str,
390 request: &CanonicalJsonValue,
391) {
392 let key = (user_id.to_owned(), device_id.to_owned(), session.to_owned());
393
394 self.userdevicesessionid_uiaarequest
395 .write()
396 .expect("locked for writing")
397 .insert(key, request.to_owned());
398}
399
400#[implement(Service)]
401pub fn get_uiaa_request(
402 &self,
403 user_id: &UserId,
404 device_id: Option<&DeviceId>,
405 session: &str,
406) -> Option<CanonicalJsonValue> {
407 let device_id = device_id.unwrap_or_else(|| EMPTY.into());
408 let key = (user_id.to_owned(), device_id.to_owned(), session.to_owned());
409
410 self.userdevicesessionid_uiaarequest
411 .read()
412 .expect("locked for reading")
413 .get(&key)
414 .cloned()
415}
416
417#[implement(Service)]
418pub fn update_uiaa_session(
419 &self,
420 user_id: &UserId,
421 device_id: &DeviceId,
422 session: &str,
423 uiaainfo: Option<&UiaaInfo>,
424) {
425 let key = (user_id, device_id, session);
426
427 if let Some(uiaainfo) = uiaainfo {
428 self.db
429 .userdevicesessionid_uiaainfo
430 .put(key, Json(uiaainfo));
431 } else {
432 self.db.userdevicesessionid_uiaainfo.del(key);
433 }
434}
435
436#[implement(Service)]
437async fn get_uiaa_session(
438 &self,
439 user_id: &UserId,
440 device_id: &DeviceId,
441 session: &str,
442) -> Result<UiaaInfo> {
443 let key = (user_id, device_id, session);
444
445 self.db
446 .userdevicesessionid_uiaainfo
447 .qry(&key)
448 .await
449 .deserialized()
450 .map_err(|_| err!(Request(Forbidden("UIAA session does not exist."))))
451}
452
453#[implement(Service)]
454pub async fn get_uiaa_session_by_session_id(
455 &self,
456 session_id: &str,
457) -> Option<(OwnedUserId, OwnedDeviceId, UiaaInfo)> {
458 let stream = self
460 .db
461 .userdevicesessionid_uiaainfo
462 .keys::<(OwnedUserId, OwnedDeviceId, String)>();
463
464 pin_mut!(stream);
465 while let Ok(Some((user_id, device_id, session))) = stream.try_next().await {
466 if session == session_id {
467 if let Ok(uiaainfo) = self
469 .get_uiaa_session(&user_id, &device_id, session_id)
470 .await
471 {
472 return Some((user_id, device_id, uiaainfo));
473 }
474 }
475 }
476
477 None
478}