Skip to main content

tuwunel_api/router/auth/
uiaa.rs

1use ruma::{
2	CanonicalJsonValue, OwnedUserId,
3	api::{
4		IncomingRequest,
5		client::uiaa::{AuthData, AuthFlow, AuthType, Jwt, UiaaInfo},
6	},
7};
8use serde_json::{json, value::to_raw_value};
9use tuwunel_core::{
10	Err, Error, Result, err, is_equal_to, utils,
11	utils::{
12		OptionExt,
13		future::{OptionFutureExt, TryExtExt},
14	},
15};
16use tuwunel_service::{Services, uiaa::SESSION_ID_LENGTH};
17
18use crate::{Ruma, client::jwt};
19
20pub(crate) async fn auth_uiaa<T>(services: &Services, body: &Ruma<T>) -> Result<OwnedUserId>
21where
22	T: IncomingRequest + Send + Sync,
23{
24	let sender_user = body.sender_user.as_deref();
25
26	let password_flow = [AuthType::Password];
27	let user_origin = sender_user
28		.map_async(|sender_user| services.users.origin(sender_user).ok())
29		.unwrap_or(None)
30		.await;
31
32	let ldap_origin = cfg!(feature = "ldap")
33		&& services.config.ldap.enable
34		&& user_origin.as_deref() == Some("ldap");
35
36	let has_password = ldap_origin
37		|| sender_user
38			.map_async(|sender_user| {
39				services
40					.users
41					.has_password(sender_user)
42					.unwrap_or(false)
43			})
44			.unwrap_or(false)
45			.await;
46
47	// Determine the exact IdP to bind to the UIAA session.
48	//
49	// The correct binding comes from the device that made this request, not
50	// from a heuristic scan of all user sessions.  Rules:
51	//
52	//  1. Preferred: the device is tagged with an idp_id from when it was created
53	//     via the OIDC token endpoint → use that idp_id directly. This is exact and
54	//     correct even on multi-provider servers.
55	//  2. Fallback: the device has no idp tag (pre-dates the idp_id field or was
56	//     created through a legacy path) but origin=="sso" and only one provider is
57	//     configured → routing is still unambiguous.
58	//  3. Otherwise: cannot determine provider → do NOT advertise m.login.sso.
59	let sso_flow = [AuthType::Sso];
60	let bound_idp: Option<String> = sender_user
61		.map_async(async |sender_user| {
62			body.sender_device
63				.as_deref()
64				.map_async(async |device_id| {
65					services
66						.users
67						.get_oidc_device_idp(sender_user, device_id)
68						.await
69						.filter(|s| !s.is_empty())
70				})
71				.await
72				.flatten()
73				.or_else(|| {
74					let use_sso = user_origin
75						.as_deref()
76						.is_some_and(is_equal_to!("sso"))
77						&& services.config.identity_provider.len() == 1;
78
79					use_sso
80						.then(|| services.oauth.providers.get_default_id())
81						.flatten()
82				})
83		})
84		.await
85		.flatten();
86
87	let has_sso = bound_idp.is_some();
88
89	let jwt_flow = [AuthType::Jwt];
90	let has_jwt = services.config.jwt.enable;
91
92	let mut uiaainfo = UiaaInfo {
93		flows: has_password
94			.then_some(password_flow)
95			.into_iter()
96			.chain(has_sso.then_some(sso_flow))
97			.chain(has_jwt.then_some(jwt_flow))
98			.map(Vec::from)
99			.map(AuthFlow::new)
100			.collect(),
101
102		params: to_raw_value(&json!({})).ok(),
103		..Default::default()
104	};
105
106	match body
107		.json_body
108		.as_ref()
109		.and_then(CanonicalJsonValue::as_object)
110		.and_then(|body| body.get("auth"))
111		.cloned()
112		.map(CanonicalJsonValue::into)
113		.map(serde_json::from_value)
114		.transpose()?
115	{
116		| Some(AuthData::Jwt(Jwt { ref token, .. })) => {
117			let sender_user = jwt::validate_user(services, token)?;
118			if !services.users.exists(&sender_user).await {
119				return Err!(Request(NotFound("User {sender_user} is not registered.")));
120			}
121
122			services
123				.users
124				.deactivated_check(&sender_user)
125				.await?;
126
127			// Success!
128			Ok(sender_user)
129		},
130		| Some(ref auth) => {
131			let sender_user = body
132				.sender_user
133				.as_deref()
134				.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
135
136			let sender_device = body.sender_device()?;
137			let (worked, uiaainfo) = services
138				.uiaa
139				.try_auth(sender_user, sender_device, auth, &uiaainfo)
140				.await?;
141
142			if !worked {
143				return Err(Error::Uiaa(uiaainfo));
144			}
145
146			// Success!
147			Ok(sender_user.to_owned())
148		},
149		| _ => match body.json_body {
150			| Some(ref json) => {
151				let sender_user = body
152					.sender_user
153					.as_deref()
154					.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
155
156				let sender_device = body.sender_device()?;
157				uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
158
159				// Bind the exact IdP determined above into the UIAA session so
160				// the SSO fallback page can route re-authentication to the
161				// correct provider without any further heuristic lookups.
162				if let Some(ref idp) = bound_idp {
163					uiaainfo.params = to_raw_value(&json!({
164						"m.login.sso": {
165							"identity_providers": [{"id": idp}]
166						}
167					}))
168					.ok();
169				}
170
171				services
172					.uiaa
173					.create(sender_user, sender_device, &uiaainfo, json);
174
175				Err(Error::Uiaa(uiaainfo))
176			},
177			| _ => Err!(Request(NotJson("JSON body is not valid"))),
178		},
179	}
180}