Skip to main content

tuwunel_service/oauth/
sessions.rs

1mod adopt;
2pub mod association;
3
4use std::{
5	iter::once,
6	sync::{Arc, Mutex},
7	time::SystemTime,
8};
9
10use futures::{FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt};
11use ruma::{OwnedUserId, UserId};
12use serde::{Deserialize, Serialize};
13use tuwunel_core::{
14	Err, Result, at, implement,
15	utils::{
16		MutexMap,
17		stream::{IterStream, ReadyExt, TryExpect},
18	},
19};
20use tuwunel_database::{Cbor, Database, Deserialized, Ignore, Map};
21use url::Url;
22
23pub use self::adopt::Counts;
24use super::{Provider, Providers, UserInfo, unique_id as session_unique_id};
25use crate::SelfServices;
26
27pub struct Sessions {
28	services: SelfServices,
29	association_pending: Mutex<association::Pending>,
30
31	/// Serializes probes and writes for each unique identity.
32	///
33	/// Transaction batches cannot conditionally claim an index key. Each
34	/// identity therefore has an independent critical section.
35	write_locks: MutexMap<String, ()>,
36
37	providers: Arc<Providers>,
38	db: Data,
39}
40
41struct Data {
42	oauthid_session: Arc<Map>,
43	oauthuniqid_oauthid: Arc<Map>,
44	userid_oauthid: Arc<Map>,
45	database: Arc<Database>,
46}
47
48/// Persistent state for one upstream OAuth authorization.
49///
50/// The record carries provider, redirect, PKCE, nonce, and token data across
51/// the authorization flow. Once linked, it also associates the provider
52/// identity with a Matrix user.
53#[derive(Clone, Debug, Default, Deserialize, Serialize)]
54pub struct Session {
55	/// Identity Provider ID (the `client_id` in the configuration) associated
56	/// with this session.
57	pub idp_id: Option<String>,
58
59	/// Session ID used as the index key for this session itself.
60	pub sess_id: Option<SessionId>,
61
62	/// Token type (bearer, mac, etc).
63	pub token_type: Option<String>,
64
65	/// Access token to the provider.
66	pub access_token: Option<String>,
67
68	/// OIDC ID token returned by the provider.
69	pub id_token: Option<String>,
70
71	/// Duration in seconds the access_token is valid for.
72	pub expires_in: Option<u64>,
73
74	/// Point in time that the access_token expires.
75	pub expires_at: Option<SystemTime>,
76
77	/// Token used to refresh the access_token.
78	pub refresh_token: Option<String>,
79
80	/// Duration in seconds the refresh_token is valid for
81	pub refresh_token_expires_in: Option<u64>,
82
83	/// Point in time that the refresh_token expires.
84	pub refresh_token_expires_at: Option<SystemTime>,
85
86	/// Access scope actually granted (if supported).
87	pub scope: Option<String>,
88
89	/// Redirect URL
90	pub redirect_url: Option<Url>,
91
92	/// Challenge preimage
93	pub code_verifier: Option<String>,
94
95	/// Random string passed exclusively in the grant session cookie.
96	pub cookie_nonce: Option<String>,
97
98	/// Random single-use string passed in the provider redirect.
99	pub query_nonce: Option<String>,
100
101	/// Point in time the authorization grant session expires.
102	pub authorize_expires_at: Option<SystemTime>,
103
104	/// Associated User Id registration.
105	pub user_id: Option<OwnedUserId>,
106
107	/// Last userinfo response persisted here.
108	pub user_info: Option<UserInfo>,
109}
110
111/// Session Identifier type.
112pub type SessionId = String;
113
114/// Number of characters generated for our code_verifier. The code_verifier is a
115/// random string which must be between 43 and 128 characters.
116pub const CODE_VERIFIER_LENGTH: usize = 64;
117
118/// Number of characters we will generate for the Session ID.
119pub const SESSION_ID_LENGTH: usize = 32;
120
121#[implement(Sessions)]
122pub(super) fn build(args: &crate::Args<'_>, providers: Arc<Providers>) -> Self {
123	Self {
124		services: args.services.clone(),
125		association_pending: Default::default(),
126		write_locks: MutexMap::new(),
127		providers,
128		db: Data {
129			oauthid_session: args.db["oauthid_session"].clone(),
130			oauthuniqid_oauthid: args.db["oauthuniqid_oauthid"].clone(),
131			userid_oauthid: args.db["userid_oauthid"].clone(),
132			database: args.db.clone(),
133		},
134	}
135}
136
137/// Delete database state for the session.
138///
139/// The canonical session and every association index that still refers to it
140/// commit together.
141#[implement(Sessions)]
142#[tracing::instrument(level = "debug", skip(self))]
143pub async fn delete(&self, sess_id: &str) {
144	let (session, unique_id, _write_guard) = loop {
145		let Ok(snapshot) = self.get(sess_id).await else {
146			return;
147		};
148
149		let provider = async {
150			let idp_id = snapshot.idp_id.as_deref()?;
151
152			self.providers.get(idp_id).map(Result::ok).await
153		}
154		.await;
155
156		let unique_id = provider
157			.as_ref()
158			.and_then(|provider| session_unique_id((provider, &snapshot)).ok());
159
160		let write_guard = match unique_id.as_deref() {
161			| Some(unique_id) => Some(self.write_locks.lock(unique_id).await),
162			| None => None,
163		};
164
165		let Ok(session) = self.get(sess_id).await else {
166			return;
167		};
168
169		if session.idp_id.as_deref() != snapshot.idp_id.as_deref() {
170			continue;
171		}
172
173		let current_unique_id = provider
174			.as_ref()
175			.and_then(|provider| session_unique_id((provider, &session)).ok());
176
177		if current_unique_id == unique_id {
178			break (session, unique_id, write_guard);
179		}
180	};
181
182	// Preserve a unique identity association updated to a newer session.
183	let unique_id = async {
184		let unique_id = unique_id.as_deref()?;
185		let assoc_id = self
186			.get_sess_id_by_unique_id(unique_id)
187			.map(Result::ok)
188			.await?;
189
190		(assoc_id == sess_id).then_some(unique_id)
191	}
192	.await;
193
194	let user_sessions = async {
195		let user_id = session.user_id.as_deref()?;
196		let sess_ids: Vec<_> = self
197			.get_sess_id_by_user(user_id)
198			.ready_filter_map(Result::ok)
199			.ready_filter(|assoc_id| assoc_id != sess_id)
200			.collect()
201			.await;
202
203		Some((user_id, sess_ids))
204	}
205	.await;
206
207	let mut txn = self.db.database.txn();
208
209	if let Some((user_id, sess_ids)) = user_sessions {
210		if !sess_ids.is_empty() {
211			txn.raw_put(&self.db.userid_oauthid, user_id, sess_ids);
212		} else {
213			txn.del_raw(&self.db.userid_oauthid, user_id);
214		}
215	}
216
217	if let Some(unique_id) = unique_id {
218		txn.del_raw(&self.db.oauthuniqid_oauthid, unique_id);
219	}
220
221	txn.del_raw(&self.db.oauthid_session, sess_id);
222	txn.execute();
223}
224
225/// Create or overwrite database state for the session.
226///
227/// The canonical session and its available identity and user indexes commit
228/// together.
229#[implement(Sessions)]
230#[tracing::instrument(level = "info", skip(self))]
231pub async fn put(&self, session: &Session) {
232	let unique_id = async {
233		let idp_id = session.idp_id.as_ref()?;
234		let provider = self.providers.get(idp_id).map(Result::ok).await?;
235
236		session_unique_id((&provider, session)).ok()
237	}
238	.await;
239
240	let _write_guard = match unique_id.as_deref() {
241		| Some(unique_id) => Some(self.write_locks.lock(unique_id).await),
242		| None => None,
243	};
244
245	self.put_locked(session, unique_id.as_deref())
246		.await;
247}
248
249/// Build and commit a session while exclusively claiming its identity key.
250///
251/// The callback's identity lookup and user selection remain ordered with bulk
252/// adoption until the canonical session and indexes have committed.
253#[implement(Sessions)]
254#[tracing::instrument(level = "debug", skip_all)]
255pub async fn commit_identity_session<T, F, Fut>(
256	&self,
257	unique_id: &str,
258	build: F,
259) -> Result<(Session, T, Option<SessionId>)>
260where
261	T: Send,
262	F: FnOnce(Option<OwnedUserId>) -> Fut + Send,
263	Fut: Future<Output = Result<(Session, T)>> + Send,
264{
265	let write_guard = self.write_locks.lock(unique_id).await;
266	let existing = match self.get_by_unique_id(unique_id).await {
267		| Ok(session) => Some(session),
268		| Err(e) if e.is_not_found() => None,
269		| Err(e) => return Err(e),
270	};
271
272	let old_sess_id = existing
273		.as_ref()
274		.and_then(|session| session.sess_id.clone());
275
276	let old_user_id = existing.and_then(|session| session.user_id);
277	let (session, value) = build(old_user_id).await?;
278
279	self.put_locked(&session, Some(unique_id)).await;
280	drop(write_guard);
281
282	Ok((session, value, old_sess_id))
283}
284
285#[implement(Sessions)]
286async fn put_locked(&self, session: &Session, unique_id: Option<&str>) {
287	let sess_id = session
288		.sess_id
289		.as_deref()
290		.expect("Missing session.sess_id required for sessions.put()");
291
292	let user_sessions = async {
293		let user_id = session.user_id.as_deref()?;
294		let sess_ids = self
295			.get_sess_id_by_user(user_id)
296			.ready_filter_map(Result::ok)
297			.chain(once(sess_id.to_owned()).stream())
298			.collect::<Vec<_>>()
299			.map(|mut ids| {
300				ids.sort_unstable();
301				ids.dedup();
302				ids
303			})
304			.await;
305
306		Some((user_id, sess_ids))
307	}
308	.await;
309
310	let mut txn = self.db.database.txn();
311
312	txn.raw_put(&self.db.oauthid_session, sess_id, Cbor(session));
313
314	if let Some(unique_id) = unique_id {
315		txn.insert_raw(&self.db.oauthuniqid_oauthid, unique_id, sess_id);
316	}
317
318	if let Some((user_id, sess_ids)) = user_sessions {
319		txn.raw_put(&self.db.userid_oauthid, user_id, sess_ids);
320	}
321
322	txn.execute();
323}
324
325/// Fetch database state for a session from its associated `(iss,sub)`, in case
326/// `sess_id` is not known.
327#[implement(Sessions)]
328#[tracing::instrument(level = "debug", skip(self), ret(level = "debug"))]
329pub async fn get_by_unique_id(&self, unique_id: &str) -> Result<Session> {
330	self.get_sess_id_by_unique_id(unique_id)
331		.and_then(async |sess_id| self.get(&sess_id).await)
332		.await
333}
334
335/// Fetch database state for one or more sessions from its associated `user_id`,
336/// in case `sess_id` is not known.
337#[implement(Sessions)]
338#[tracing::instrument(level = "debug", skip(self))]
339pub fn get_by_user(&self, user_id: &UserId) -> impl Stream<Item = Result<Session>> + Send {
340	self.get_sess_id_by_user(user_id)
341		.and_then(async |sess_id| self.get(&sess_id).await)
342}
343
344/// Fetch database state for a session from its `sess_id`.
345#[implement(Sessions)]
346#[tracing::instrument(level = "debug", skip(self), ret(level = "debug"))]
347pub async fn get(&self, sess_id: &str) -> Result<Session> {
348	self.db
349		.oauthid_session
350		.get(sess_id)
351		.await
352		.deserialized::<Cbor<_>>()
353		.map(at!(0))
354}
355
356/// Resolve the `sess_id` associations with a `user_id`.
357#[implement(Sessions)]
358#[tracing::instrument(level = "debug", skip(self))]
359pub fn get_sess_id_by_user(&self, user_id: &UserId) -> impl Stream<Item = Result<String>> + Send {
360	self.db
361		.userid_oauthid
362		.get(user_id)
363		.map(Deserialized::deserialized)
364		.map_ok(Vec::into_iter)
365		.map_ok(IterStream::try_stream)
366		.try_flatten_stream()
367}
368
369/// Resolve the `sess_id` from an associated provider issuer and subject hash.
370#[implement(Sessions)]
371#[tracing::instrument(level = "debug", skip(self), ret(level = "debug"))]
372pub async fn get_sess_id_by_unique_id(&self, unique_id: &str) -> Result<String> {
373	self.db
374		.oauthuniqid_oauthid
375		.get(unique_id)
376		.await
377		.deserialized()
378}
379
380#[implement(Sessions)]
381pub fn users(&self) -> impl Stream<Item = OwnedUserId> + Send {
382	self.db
383		.userid_oauthid
384		.keys()
385		.expect_ok()
386		.map(UserId::to_owned)
387}
388
389#[implement(Sessions)]
390pub fn stream(&self) -> impl Stream<Item = Session> + Send {
391	self.db
392		.oauthid_session
393		.stream()
394		.expect_ok()
395		.map(|(_, session): (Ignore, Cbor<_>)| session.0)
396}
397
398#[implement(Sessions)]
399pub async fn provider(&self, session: &Session) -> Result<Provider> {
400	let Some(idp_id) = session.idp_id.as_deref() else {
401		return Err!(Request(NotFound("No provider for this session")));
402	};
403
404	self.providers.get(idp_id).await
405}