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