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		..Default::default()
97	};
98
99	match body
100		.json_body
101		.as_ref()
102		.and_then(CanonicalJsonValue::as_object)
103		.and_then(|body| body.get("auth"))
104		.cloned()
105		.map(CanonicalJsonValue::into)
106		.map(serde_json::from_value)
107		.transpose()?
108	{
109		| Some(AuthData::Jwt(Jwt { ref token, .. })) => {
110			let sender_user = jwt::validate_user(services, token)?;
111			if !services.users.exists(&sender_user).await {
112				return Err!(Request(NotFound("User {sender_user} is not registered.")));
113			}
114
115			// Success!
116			Ok(sender_user)
117		},
118		| Some(ref auth) => {
119			let sender_user = body
120				.sender_user
121				.as_deref()
122				.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
123
124			let sender_device = body.sender_device()?;
125			let (worked, uiaainfo) = services
126				.uiaa
127				.try_auth(sender_user, sender_device, auth, &uiaainfo)
128				.await?;
129
130			if !worked {
131				return Err(Error::Uiaa(uiaainfo));
132			}
133
134			// Success!
135			Ok(sender_user.to_owned())
136		},
137		| _ => match body.json_body {
138			| Some(ref json) => {
139				let sender_user = body
140					.sender_user
141					.as_deref()
142					.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
143
144				let sender_device = body.sender_device()?;
145				uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
146
147				// Bind the exact IdP determined above into the UIAA session so
148				// the SSO fallback page can route re-authentication to the
149				// correct provider without any further heuristic lookups.
150				if let Some(ref idp) = bound_idp {
151					uiaainfo.params = to_raw_value(&json!({
152						"m.login.sso": {
153							"identity_providers": [{"id": idp}]
154						}
155					}))
156					.ok();
157				}
158
159				services
160					.uiaa
161					.create(sender_user, sender_device, &uiaainfo, json);
162
163				Err(Error::Uiaa(uiaainfo))
164			},
165			| _ => Err!(Request(NotJson("JSON body is not valid"))),
166		},
167	}
168}