Skip to main content

tuwunel_api/router/auth/
dispatch.rs

1use std::any::TypeId;
2
3use ruma::{
4	CanonicalJsonValue, OwnedDeviceId, OwnedUserId,
5	api::{
6		auth_scheme::{
7			AccessToken, AccessTokenOptional, AppserviceToken, AppserviceTokenOptional,
8			AuthScheme, NoAccessToken, NoAuthentication,
9		},
10		client::voip::get_turn_server_info,
11		error::{ErrorKind, UnknownTokenErrorData},
12		federation::{authentication::ServerSignatures, openid::get_openid_userinfo},
13	},
14};
15use tuwunel_core::{Err, Error, Result, utils::result::LogDebugErr};
16use tuwunel_service::Services;
17
18use super::{Auth, Request, Token, appservice::auth_appservice, server::auth_server};
19
20/// Tag identifying an [`AuthScheme`] for tuwunel's purposes.
21///
22/// Ruma's `AuthScheme` is a trait, so endpoint-specific bypasses cannot be
23/// expressed as enum match arms anymore. This tag is the value-side handle
24/// used to route through `auth()` and to identify the unauthenticated case
25/// inside `check_auth_still_required`.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub(in crate::router) enum Scheme {
28	None,
29	AccessToken,
30	AccessTokenOptional,
31	AppserviceToken,
32	AppserviceTokenOptional,
33	ServerSignatures,
34}
35
36/// Trait routing a concrete [`AuthScheme`] through the per-scheme dispatch.
37///
38/// `dispatch` is intentionally non-generic over the request type; the
39/// caller passes `TypeId::of::<T>()` so each impl emits a single body
40/// rather than monomorphizing per request.
41pub(in crate::router) trait AuthDispatch: AuthScheme {
42	const SCHEME: Scheme;
43
44	fn dispatch(
45		services: &Services,
46		request: &mut Request,
47		json_body: Option<&CanonicalJsonValue>,
48		token: Token,
49		route: TypeId,
50	) -> impl Future<Output = Result<Auth>> + Send;
51}
52
53impl AuthDispatch for NoAccessToken {
54	const SCHEME: Scheme = Scheme::None;
55
56	async fn dispatch(
57		services: &Services,
58		request: &mut Request,
59		json_body: Option<&CanonicalJsonValue>,
60		token: Token,
61		route: TypeId,
62	) -> Result<Auth> {
63		<NoAuthentication as AuthDispatch>::dispatch(services, request, json_body, token, route)
64			.await
65	}
66}
67
68impl AuthDispatch for NoAuthentication {
69	const SCHEME: Scheme = Scheme::None;
70
71	async fn dispatch(
72		services: &Services,
73		request: &mut Request,
74		_json_body: Option<&CanonicalJsonValue>,
75		token: Token,
76		route: TypeId,
77	) -> Result<Auth> {
78		match token {
79			| Token::Invalid
80				if request.query.access_token.is_some()
81					&& route == TypeId::of::<get_openid_userinfo::v1::Request>() =>
82			{
83				// OpenID federation endpoint uses a query param with the same name; drop
84				// once query params for user auth are removed from the spec. Required to
85				// make the integration manager work.
86				Ok(Auth::default())
87			},
88
89			| Token::Invalid => unknown_token(),
90			| Token::Expired((user_id, device_id)) =>
91				expired_token(services, user_id, device_id).await,
92
93			| Token::User(user) => Ok(Auth {
94				sender_user: Some(user.0),
95				sender_device: Some(user.1),
96				_expires_at: user.2,
97				..Auth::default()
98			}),
99
100			| Token::Appservice(info) => Ok(Auth {
101				appservice_info: Some(*info),
102				..Auth::default()
103			}),
104
105			| Token::None => Ok(Auth::default()),
106		}
107	}
108}
109
110impl AuthDispatch for AccessToken {
111	const SCHEME: Scheme = Scheme::AccessToken;
112
113	async fn dispatch(
114		services: &Services,
115		request: &mut Request,
116		_json_body: Option<&CanonicalJsonValue>,
117		token: Token,
118		route: TypeId,
119	) -> Result<Auth> {
120		match token {
121			| Token::Invalid => unknown_token(),
122			| Token::Expired((user_id, device_id)) =>
123				expired_token(services, user_id, device_id).await,
124			| Token::Appservice(info) => Ok(auth_appservice(services, request, info).await?),
125			| Token::User(user) => Ok(Auth {
126				sender_user: Some(user.0),
127				sender_device: Some(user.1),
128				_expires_at: user.2,
129				..Auth::default()
130			}),
131			| Token::None
132				if route == TypeId::of::<get_turn_server_info::v3::Request>()
133					&& services.server.config.turn_allow_guests =>
134				Ok(Auth::default()),
135
136			| Token::None => Err!(Request(MissingToken("Missing access token."))),
137		}
138	}
139}
140
141impl AuthDispatch for AccessTokenOptional {
142	const SCHEME: Scheme = Scheme::AccessTokenOptional;
143
144	async fn dispatch(
145		services: &Services,
146		_request: &mut Request,
147		_json_body: Option<&CanonicalJsonValue>,
148		token: Token,
149		_route: TypeId,
150	) -> Result<Auth> {
151		match token {
152			| Token::Invalid => unknown_token(),
153			| Token::Expired((user_id, device_id)) =>
154				expired_token(services, user_id, device_id).await,
155			| Token::User(user) => Ok(Auth {
156				sender_user: Some(user.0),
157				sender_device: Some(user.1),
158				_expires_at: user.2,
159				..Auth::default()
160			}),
161			| Token::Appservice(info) => Ok(Auth {
162				appservice_info: Some(*info),
163				..Auth::default()
164			}),
165			| Token::None => Ok(Auth::default()),
166		}
167	}
168}
169
170impl AuthDispatch for AppserviceToken {
171	const SCHEME: Scheme = Scheme::AppserviceToken;
172
173	async fn dispatch(
174		services: &Services,
175		_request: &mut Request,
176		_json_body: Option<&CanonicalJsonValue>,
177		token: Token,
178		_route: TypeId,
179	) -> Result<Auth> {
180		match token {
181			| Token::Invalid => unknown_token(),
182			| Token::Expired((user_id, device_id)) =>
183				expired_token(services, user_id, device_id).await,
184			| Token::User(_) =>
185				Err!(Request(Unauthorized("Appservice tokens must be used on this endpoint."))),
186			| Token::Appservice(info) => Ok(Auth {
187				appservice_info: Some(*info),
188				..Auth::default()
189			}),
190			| Token::None => Err!(Request(MissingToken("Missing access token."))),
191		}
192	}
193}
194
195impl AuthDispatch for AppserviceTokenOptional {
196	const SCHEME: Scheme = Scheme::AppserviceTokenOptional;
197
198	async fn dispatch(
199		services: &Services,
200		_request: &mut Request,
201		_json_body: Option<&CanonicalJsonValue>,
202		token: Token,
203		_route: TypeId,
204	) -> Result<Auth> {
205		match token {
206			| Token::Invalid => unknown_token(),
207			| Token::Expired((user_id, device_id)) =>
208				expired_token(services, user_id, device_id).await,
209			| Token::User(user) => Ok(Auth {
210				sender_user: Some(user.0),
211				sender_device: Some(user.1),
212				_expires_at: user.2,
213				..Auth::default()
214			}),
215			| Token::Appservice(info) => Ok(Auth {
216				appservice_info: Some(*info),
217				..Auth::default()
218			}),
219			| Token::None => Ok(Auth::default()),
220		}
221	}
222}
223
224impl AuthDispatch for ServerSignatures {
225	const SCHEME: Scheme = Scheme::ServerSignatures;
226
227	async fn dispatch(
228		services: &Services,
229		request: &mut Request,
230		json_body: Option<&CanonicalJsonValue>,
231		token: Token,
232		_route: TypeId,
233	) -> Result<Auth> {
234		match token {
235			| Token::Invalid => unknown_token(),
236			| Token::Expired((user_id, device_id)) =>
237				expired_token(services, user_id, device_id).await,
238			| Token::Appservice(_) | Token::User(_) =>
239				Err!(Request(Unauthorized("Server signatures must be used on this endpoint."))),
240			| Token::None => Ok(auth_server(services, request, json_body).await?),
241		}
242	}
243}
244
245fn unknown_token() -> Result<Auth> {
246	Err(Error::BadRequest(
247		ErrorKind::UnknownToken(UnknownTokenErrorData::new()),
248		"Unknown access token.",
249	))
250}
251
252async fn expired_token(
253	services: &Services,
254	user_id: OwnedUserId,
255	device_id: OwnedDeviceId,
256) -> Result<Auth> {
257	services
258		.users
259		.remove_access_token(&user_id, &device_id)
260		.await
261		.log_debug_err()
262		.ok();
263
264	Err(Error::BadRequest(
265		ErrorKind::UnknownToken(UnknownTokenErrorData { soft_logout: true }),
266		"Expired access token.",
267	))
268}