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
40struct Data {
41	oidc_signingkey: Arc<Map>,
42	oidcclientid_registration: Arc<Map>,
43	oidccode_authsession: Arc<Map>,
44	oidcdevicecode_devicegrant: Arc<Map>,
45	oidcusercode_devicecode: Arc<Map>,
46	oidcreqid_authrequest: Arc<Map>,
47}
48
49impl Server {
50	pub(super) fn build(args: &crate::Args<'_>) -> Result<Option<Self>> {
51		if !Self::can_build(args) {
52			return Ok(None);
53		}
54
55		let db = Data {
56			oidc_signingkey: args.db["oidc_signingkey"].clone(),
57			oidcclientid_registration: args.db["oidcclientid_registration"].clone(),
58			oidccode_authsession: args.db["oidccode_authsession"].clone(),
59			oidcdevicecode_devicegrant: args.db["oidcdevicecode_devicegrant"].clone(),
60			oidcusercode_devicecode: args.db["oidcusercode_devicecode"].clone(),
61			oidcreqid_authrequest: args.db["oidcreqid_authrequest"].clone(),
62		};
63
64		let key = init_signing_key(&db)?;
65		debug_info!(
66			key = ?key.key_id,
67			"Initializing OIDC server for next-gen auth (MSC2965)"
68		);
69
70		Ok(Some(Self {
71			services: args.services.clone(),
72			db,
73			jwk: init_jwk(&key.key_der, &key.key_id)?,
74			key,
75			device_locks: MutexMap::new(),
76		}))
77	}
78}
79
80#[implement(Server)]
81fn can_build(args: &crate::Args<'_>) -> bool {
82	let has_idp = !args.server.config.identity_provider.is_empty();
83	let has_cwk = args.server.config.well_known.client.is_some();
84	let native = args.server.config.oidc_native_auth;
85
86	if (has_idp || native) && !has_cwk {
87		warn!("OIDC server (Next-gen auth) requires `well_known.client` to be configured.");
88
89		return false;
90	}
91
92	if !has_idp && !native {
93		debug_warn!(
94			"OIDC server (Next-gen auth) requires at least one `identity_provider`, or \
95			 `oidc_native_auth` to be enabled."
96		);
97
98		return false;
99	}
100
101	true
102}
103
104#[implement(Server)]
105pub fn issuer_url(&self) -> Result<String> {
106	self.services
107		.config
108		.well_known
109		.client
110		.as_ref()
111		.map(|url| {
112			let s = url.to_string();
113			if s.ends_with('/') { s } else { s + "/" }
114		})
115		.ok_or_else(|| {
116			err!(Config("well_known.client", "well_known.client must be set for OIDC server"))
117		})
118}
119
120/// MSC2967 device-scope prefixes, stable spelling first.
121const DEVICE_SCOPE_PREFIXES: [&str; 2] =
122	["urn:matrix:client:device:", "urn:matrix:org.matrix.msc2967.client:device:"];
123
124/// MSC2967 API-scope prefixes, stable spelling first.
125const API_SCOPE_PREFIXES: [&str; 2] =
126	["urn:matrix:client:api:", "urn:matrix:org.matrix.msc2967.client:api:"];
127
128/// Narrow a requested scope to the granted scope (RFC 6749 ยง3.3): keep the
129/// tokens this server recognises and return them alongside the MSC2967 device
130/// id, when one was requested. Unrecognised tokens are dropped, or rejected
131/// when `strict` is set. A request carrying more than one device scope, or a
132/// device id outside the RFC 3986 unreserved set, is always rejected.
133pub fn narrow_scope(requested: &str, strict: bool) -> Result<(String, Option<String>)> {
134	let mut granted = String::new();
135	let mut device_id: Option<&str> = None;
136
137	for token in requested.split_whitespace() {
138		let keep = if let Some(id) = DEVICE_SCOPE_PREFIXES
139			.iter()
140			.find_map(|prefix| token.strip_prefix(prefix))
141		{
142			if device_id.is_some() {
143				return Err!(Request(InvalidParam("more than one device scope requested")));
144			}
145			if id.is_empty() || !id.bytes().all(is_unreserved) {
146				return Err!(Request(InvalidParam("device id contains a reserved character")));
147			}
148
149			device_id = Some(id);
150			true
151		} else {
152			token == "openid"
153				|| API_SCOPE_PREFIXES
154					.iter()
155					.any(|prefix| token.starts_with(prefix))
156		};
157
158		if keep {
159			if !granted.is_empty() {
160				granted.push(' ');
161			}
162
163			granted.push_str(token);
164		} else if strict {
165			return Err!(Request(InvalidParam("unsupported scope requested")));
166		}
167	}
168
169	Ok((granted, device_id.map(ToOwned::to_owned)))
170}
171
172#[inline]
173fn is_unreserved(b: u8) -> bool {
174	b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_' | b'~')
175}
176
177#[cfg(test)]
178mod tests {
179	use super::narrow_scope;
180
181	#[test]
182	fn narrow_scope_keeps_known_drops_unknown() {
183		let requested =
184			"openid urn:matrix:client:api:* urn:matrix:client:device:ABCDEFGHIJ custom:x";
185
186		let (granted, device) = narrow_scope(requested, false).expect("narrows");
187
188		assert_eq!(granted, "openid urn:matrix:client:api:* urn:matrix:client:device:ABCDEFGHIJ");
189		assert_eq!(device.as_deref(), Some("ABCDEFGHIJ"));
190	}
191
192	#[test]
193	fn narrow_scope_strict_rejects_unknown() {
194		narrow_scope("openid custom:x", true).unwrap_err();
195		narrow_scope("openid custom:x", false).unwrap();
196	}
197
198	#[test]
199	fn narrow_scope_accepts_unstable_device_spelling() {
200		let scope = "urn:matrix:org.matrix.msc2967.client:device:DEV0123456";
201		let (_granted, device) = narrow_scope(scope, false).expect("narrows");
202
203		assert_eq!(device.as_deref(), Some("DEV0123456"));
204	}
205
206	#[test]
207	fn narrow_scope_rejects_two_device_scopes() {
208		let two = "urn:matrix:client:device:AAAAAAAAAA urn:matrix:client:device:BBBBBBBBBB";
209
210		narrow_scope(two, false).unwrap_err();
211	}
212
213	#[test]
214	fn narrow_scope_rejects_reserved_device_id() {
215		narrow_scope("urn:matrix:client:device:bad/id", false).unwrap_err();
216	}
217}