Skip to main content

tuwunel_api_client/session/
mod.rs

1mod appservice;
2#[cfg(feature = "jwt_login")]
3pub(crate) mod jwt;
4mod ldap;
5mod logout;
6mod password;
7mod refresh;
8mod sso;
9mod token;
10
11use axum::extract::State;
12use ruma::api::client::session::{
13	get_login_types::{
14		self,
15		v3::{
16			ApplicationServiceLoginType, IdentityProvider, JwtLoginType, LoginType,
17			PasswordLoginType, SsoLoginType, TokenLoginType,
18		},
19	},
20	login::{
21		self,
22		v3::{DiscoveryInfo, HomeserverInfo, LoginInfo},
23	},
24};
25use tuwunel_core::{Err, Result, err, info, utils::stream::ReadyExt};
26use tuwunel_service::users::device::generate_refresh_token;
27
28use self::{
29	ldap::ldap_login,
30	password::{handle_login as password_handle_login, password_login},
31};
32pub(crate) use self::{
33	logout::{logout_all_route, logout_route},
34	refresh::refresh_token_route,
35	sso::{
36		sso_callback_route, sso_complete_js_route, sso_css_route, sso_fallback_route,
37		sso_login_route, sso_login_with_provider_route,
38	},
39	token::login_token_route,
40};
41use super::TOKEN_LENGTH;
42use crate::{ClientIp, Ruma};
43
44/// # `GET /_matrix/client/v3/login`
45///
46/// Get the supported login types of this server. One of these should be used as
47/// the `type` field when logging in.
48#[tracing::instrument(skip_all, fields(%client), name = "login")]
49pub(crate) async fn get_login_types_route(
50	State(services): State<crate::State>,
51	ClientIp(client): ClientIp,
52	_body: Ruma<get_login_types::v3::Request>,
53) -> Result<get_login_types::v3::Response> {
54	let get_login_token = services
55		.config
56		.authentication
57		.login
58		.via_existing_session;
59
60	let list_idps = services.config.lists_identity_providers();
61
62	let identity_providers: Option<Vec<_>> = list_idps.then(|| {
63		services
64			.config
65			.identity_provider
66			.values()
67			.map(|config| IdentityProvider {
68				id: config.id().to_owned(),
69				brand: Some(config.brand.as_str().into()),
70				icon: config.icon.clone(),
71				name: config.display_name().to_owned(),
72			})
73			.collect()
74	});
75
76	let show_sso = identity_providers
77		.as_ref()
78		.is_none_or(|providers| !providers.is_empty());
79
80	let appservice = Some(LoginType::ApplicationService(ApplicationServiceLoginType::default()));
81
82	let token = Some(LoginType::Token(TokenLoginType { get_login_token }));
83
84	let password = services
85		.config
86		.authentication
87		.login
88		.with_password
89		.then(|| LoginType::Password(PasswordLoginType::default()));
90
91	let sso = show_sso.then(|| {
92		let oauth_aware_preferred = services
93			.config
94			.authentication
95			.login
96			.oidc_aware_preferred;
97
98		LoginType::Sso(SsoLoginType {
99			identity_providers: identity_providers.unwrap_or_default(),
100			oauth_aware_preferred,
101		})
102	});
103
104	let has_jwt = cfg!(feature = "jwt_login") && services.config.jwt.enable;
105	let jwt = has_jwt.then(|| LoginType::Jwt(JwtLoginType::default()));
106
107	let flows = [appservice, token, password, sso, jwt]
108		.into_iter()
109		.flatten()
110		.collect();
111
112	Ok(get_login_types::v3::Response { flows })
113}
114
115/// # `POST /_matrix/client/v3/login`
116///
117/// Authenticates the user and returns an access token it can use in subsequent
118/// requests.
119///
120/// - The user needs to authenticate using their password (or if enabled using a
121///   JSON Web Token)
122/// - If `device_id` is known: issues an additional access token for that device
123/// - If `device_id` is unknown: creates a new device
124/// - Returns access token that is associated with the user and device
125///
126/// Note: You can use [`GET
127/// /_matrix/client/r0/login`](fn.get_supported_versions_route.html) to see
128/// supported login types.
129#[tracing::instrument(
130	name = "login",
131	level = "debug",
132	skip_all,
133	fields(client = %client_ip),
134)]
135pub(crate) async fn login_route(
136	State(services): State<crate::State>,
137	ClientIp(client_ip): ClientIp,
138	body: Ruma<login::v3::Request>,
139) -> Result<login::v3::Response> {
140	let user_id = match &body.login_info {
141		| LoginInfo::Token(info) => token::handle_login(&services, &body, info).await?,
142		| LoginInfo::Password(info) if services.config.authentication.login.with_password => {
143			let client = body
144				.rate_limit_ip
145				.map_err(|message| err!("{message}"))?;
146
147			password_handle_login(&services, &body, info, client).await?
148		},
149		#[cfg(feature = "jwt_login")]
150		| LoginInfo::Jwt(info) if services.config.jwt.enable =>
151			jwt::handle_login(&services, &body, info).await?,
152		| LoginInfo::ApplicationService(info) =>
153			appservice::handle_login(&services, &body, info)?,
154		| _ => {
155			return Err!(Request(Unknown(debug_warn!("Invalid or unsupported login type"))));
156		},
157	};
158
159	// Appservice users are often passwordless, which reads as deactivated.
160	if !matches!(body.login_info, LoginInfo::ApplicationService(_)) {
161		services.users.deactivated_check(&user_id).await?;
162	}
163
164	services.users.locked_check(&user_id).await?;
165
166	// Only another JWT extends an expiring session, so the issuer decides how long it lasts.
167	let expiring =
168		matches!(body.login_info, LoginInfo::Jwt(_)) && services.config.jwt.expire_access_token;
169
170	let refreshable = body.body.refresh_token && !expiring;
171	let (access_token, expires_in) = services
172		.users
173		.generate_access_token(expiring || refreshable);
174
175	let refresh_token = refreshable.then(generate_refresh_token);
176
177	let device_id = if let Some(device_id) = &body.device_id
178		&& services
179			.users
180			.all_device_ids(&user_id)
181			.ready_any(|existing_device_id| existing_device_id == device_id)
182			.await
183	{
184		services
185			.users
186			.set_access_token(
187				&user_id,
188				device_id,
189				&access_token,
190				expires_in,
191				refresh_token.as_deref(),
192			)
193			.await?;
194
195		device_id.clone()
196	} else {
197		services
198			.users
199			.create_device(
200				&user_id,
201				body.device_id.as_deref(),
202				(Some(&access_token), expires_in),
203				refresh_token.as_deref(),
204				body.initial_device_display_name.as_deref(),
205				Some(client_ip),
206			)
207			.await?
208	};
209
210	info!("{user_id} logged in");
211
212	let home_server = services.server.name.clone().into();
213
214	// Send client well-known information when configured, so the client can reconfigure itself.
215	let well_known: Option<DiscoveryInfo> = services
216		.config
217		.well_known
218		.client
219		.as_ref()
220		.map(ToString::to_string)
221		.map(HomeserverInfo::new)
222		.map(DiscoveryInfo::new);
223
224	#[expect(deprecated)]
225	Ok(login::v3::Response {
226		user_id,
227		access_token,
228		device_id,
229		home_server,
230		well_known,
231		expires_in,
232		refresh_token,
233	})
234}