Skip to main content

tuwunel_service/oauth/
server.rs

1mod auth;
2mod client;
3mod device;
4mod jwk;
5mod signing_key;
6mod token;
7
8use std::sync::Arc;
9
10use serde_json::Value as JsonValue;
11use tuwunel_core::{Err, Result, debug_info, debug_warn, err, implement, utils::MutexMap, warn};
12use tuwunel_database::Map;
13
14pub use self::{
15	auth::{AUTH_REQUEST_LIFETIME, AuthCodeSession, AuthRequest},
16	client::{ClientRegistration, DcrRequest},
17	device::{
18		ApprovedDeviceGrant, DEVICE_GRANT_INTERVAL_SECS, DEVICE_GRANT_LIFETIME, DeviceGrant,
19		DeviceGrantPoll, DeviceGrantStatus, format_user_code,
20	},
21	token::IdTokenClaims,
22};
23use self::{
24	jwk::init_jwk,
25	signing_key::{SigningKey, init_signing_key},
26};
27use crate::services::OnceServices;
28
29pub struct Server {
30	services: Arc<OnceServices>,
31	db: Data,
32	jwk: JsonValue,
33	key: SigningKey,
34
35	/// Serializes the read-check-consume of a device grant by its `device_code`
36	/// so concurrent polls of one approved grant cannot each mint a device.
37	device_locks: MutexMap<String, ()>,
38
39	/// Serializes selection, claim and completion of one authorization request
40	/// by its id, holding one entry per request with a holder in flight.
41	auth_request_locks: MutexMap<String, ()>,
42}
43
44struct Data {
45	oidc_signingkey: Arc<Map>,
46	oidcclientid_registration: Arc<Map>,
47	oidccode_authsession: Arc<Map>,
48	oidcdevicecode_devicegrant: Arc<Map>,
49	oidcusercode_devicecode: Arc<Map>,
50	oidcreqid_authrequest: Arc<Map>,
51}
52
53impl Server {
54	pub(super) fn build(args: &crate::Args<'_>) -> Result<Option<Self>> {
55		if !Self::can_build(args) {
56			return Ok(None);
57		}
58
59		let db = Data {
60			oidc_signingkey: args.db["oidc_signingkey"].clone(),
61			oidcclientid_registration: args.db["oidcclientid_registration"].clone(),
62			oidccode_authsession: args.db["oidccode_authsession"].clone(),
63			oidcdevicecode_devicegrant: args.db["oidcdevicecode_devicegrant"].clone(),
64			oidcusercode_devicecode: args.db["oidcusercode_devicecode"].clone(),
65			oidcreqid_authrequest: args.db["oidcreqid_authrequest"].clone(),
66		};
67
68		let key = init_signing_key(&db)?;
69		debug_info!(
70			key = ?key.key_id,
71			"Initializing OIDC server for next-gen auth (MSC2965)"
72		);
73
74		Ok(Some(Self {
75			services: args.services.clone(),
76			db,
77			jwk: init_jwk(&key.key_der, &key.key_id)?,
78			key,
79			device_locks: MutexMap::new(),
80			auth_request_locks: MutexMap::new(),
81		}))
82	}
83}
84
85#[implement(Server)]
86fn can_build(args: &crate::Args<'_>) -> bool {
87	let has_idp = !args.server.config.identity_provider.is_empty();
88	let has_cwk = args.server.config.well_known.client.is_some();
89	let native = args.server.config.oidc_native_auth;
90
91	if (has_idp || native) && !has_cwk {
92		warn!("OIDC server (Next-gen auth) requires `well_known.client` to be configured.");
93
94		return false;
95	}
96
97	if !has_idp && !native {
98		debug_warn!(
99			"OIDC server (Next-gen auth) requires at least one `identity_provider`, or \
100			 `oidc_native_auth` to be enabled."
101		);
102
103		return false;
104	}
105
106	true
107}
108
109#[implement(Server)]
110pub fn issuer_url(&self) -> Result<String> {
111	self.services
112		.config
113		.well_known
114		.client
115		.as_ref()
116		.map(|url| {
117			let s = url.to_string();
118
119			if s.ends_with('/') { s } else { format!("{s}/") }
120		})
121		.ok_or_else(|| {
122			err!(Config("well_known.client", "well_known.client must be set for OIDC server"))
123		})
124}
125
126/// MSC2967 device-scope prefixes, stable spelling first.
127const DEVICE_SCOPE_PREFIXES: [&str; 2] =
128	["urn:matrix:client:device:", "urn:matrix:org.matrix.msc2967.client:device:"];
129
130/// MSC2967 API-scope prefixes, stable spelling first.
131const API_SCOPE_PREFIXES: [&str; 2] =
132	["urn:matrix:client:api:", "urn:matrix:org.matrix.msc2967.client:api:"];
133
134/// Restricts a requested OAuth scope to supported tokens per RFC 6749 ยง3.3.
135///
136/// Recognized tokens retain their request order, with an MSC2967 device ID
137/// returned separately when present. Unknown tokens are dropped unless `strict`
138/// is set. Multiple device scopes, empty device IDs, and IDs outside the RFC
139/// 6749 scope-token character set return an error.
140pub fn narrow_scope(requested: &str, strict: bool) -> Result<(String, Option<String>)> {
141	let mut granted = String::new();
142	let mut device_id: Option<&str> = None;
143
144	for token in requested.split_whitespace() {
145		let keep = if let Some(id) = DEVICE_SCOPE_PREFIXES
146			.iter()
147			.find_map(|prefix| token.strip_prefix(prefix))
148		{
149			if device_id.is_some() {
150				return Err!(Request(InvalidParam("more than one device scope requested")));
151			}
152			if id.is_empty() || !id.bytes().all(is_scope_char) {
153				return Err!(Request(InvalidParam("device id contains an invalid character")));
154			}
155
156			device_id = Some(id);
157			true
158		} else {
159			token == "openid"
160				|| API_SCOPE_PREFIXES
161					.iter()
162					.any(|prefix| token.starts_with(prefix))
163		};
164
165		if keep {
166			if !granted.is_empty() {
167				granted.push(' ');
168			}
169
170			granted.push_str(token);
171		} else if strict {
172			return Err!(Request(InvalidParam("unsupported scope requested")));
173		}
174	}
175
176	Ok((granted, device_id.map(ToOwned::to_owned)))
177}
178
179/// RFC 6749 appendix A NQCHAR: printable ASCII except space, double quote
180/// and backslash. MSC4108 clients use unpadded base64 device ids.
181#[inline]
182fn is_scope_char(b: u8) -> bool { b.is_ascii_graphic() && !matches!(b, b'"' | b'\\') }
183
184#[cfg(test)]
185mod tests {
186	use super::narrow_scope;
187
188	#[test]
189	fn narrow_scope_keeps_known_drops_unknown() {
190		let requested =
191			"openid urn:matrix:client:api:* urn:matrix:client:device:ABCDEFGHIJ custom:x";
192
193		let (granted, device) = narrow_scope(requested, false).expect("narrows");
194
195		assert_eq!(granted, "openid urn:matrix:client:api:* urn:matrix:client:device:ABCDEFGHIJ");
196		assert_eq!(device.as_deref(), Some("ABCDEFGHIJ"));
197	}
198
199	#[test]
200	fn narrow_scope_strict_rejects_unknown() {
201		narrow_scope("openid custom:x", true).unwrap_err();
202		narrow_scope("openid custom:x", false).unwrap();
203	}
204
205	#[test]
206	fn narrow_scope_accepts_unstable_device_spelling() {
207		let scope = "urn:matrix:org.matrix.msc2967.client:device:DEV0123456";
208		let (_granted, device) = narrow_scope(scope, false).expect("narrows");
209
210		assert_eq!(device.as_deref(), Some("DEV0123456"));
211	}
212
213	#[test]
214	fn narrow_scope_rejects_two_device_scopes() {
215		let two = "urn:matrix:client:device:AAAAAAAAAA urn:matrix:client:device:BBBBBBBBBB";
216
217		narrow_scope(two, false).unwrap_err();
218	}
219
220	#[test]
221	fn narrow_scope_accepts_base64_device_id() {
222		let scope = "urn:matrix:client:device:wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/V+afGmU5+0";
223		let (_granted, device) = narrow_scope(scope, false).expect("narrows");
224
225		assert_eq!(device.as_deref(), Some("wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/V+afGmU5+0"));
226	}
227
228	#[test]
229	fn narrow_scope_rejects_invalid_device_id() {
230		narrow_scope("urn:matrix:client:device:bad\"id", false).unwrap_err();
231		narrow_scope("urn:matrix:client:device:bad\\id", false).unwrap_err();
232	}
233}