Skip to main content

tuwunel_api_oidc/
token.rs

1use std::time::Duration;
2
3use axum::{
4	Json,
5	body::Body,
6	extract::{Form, State},
7	response::IntoResponse,
8};
9use http::{
10	Response, StatusCode,
11	header::{CACHE_CONTROL, PRAGMA},
12};
13use ruma::{OwnedDeviceId, UserId};
14use serde::Deserialize;
15use serde_json::json;
16use tuwunel_core::{Err, Error, Result, err, info, utils::time::now, warn};
17use tuwunel_service::{
18	Services,
19	oauth::server::{DeviceGrantPoll, IdTokenClaims, Server, narrow_scope},
20	users::device::generate_refresh_token,
21};
22
23use super::{oauth_error, require_account_usable};
24use crate::ClientIp;
25
26#[derive(Debug, Deserialize)]
27pub(crate) struct TokenRequest {
28	grant_type: String,
29	code: Option<String>,
30	redirect_uri: Option<String>,
31	client_id: Option<String>,
32	code_verifier: Option<String>,
33	refresh_token: Option<String>,
34	device_code: Option<String>,
35	#[serde(rename = "scope")]
36	_scope: Option<String>,
37}
38
39/// An authenticated grant ready to mint tokens.
40struct ApprovedGrant<'a> {
41	client_id: &'a str,
42	scope: &'a str,
43	user_id: &'a UserId,
44	nonce: Option<String>,
45	idp_id: Option<String>,
46}
47
48pub(crate) async fn token_route(
49	State(services): State<crate::State>,
50	ClientIp(client): ClientIp,
51	Form(body): Form<TokenRequest>,
52) -> impl IntoResponse {
53	// RFC 6749 §5.1 and §5.2 require Cache-Control: no-store and Pragma: no-cache
54	// on all token endpoint responses (success and error).
55	let inner = if services.oauth.check_rate_limit(client).is_err() {
56		oauth_error(StatusCode::TOO_MANY_REQUESTS, "slow_down", "Too many token requests")
57	} else {
58		match body.grant_type.as_str() {
59			| "authorization_code" => token_authorization_code(&services, &body)
60				.await
61				.unwrap_or_else(token_error_response),
62
63			| "refresh_token" => token_refresh(&services, &body)
64				.await
65				.unwrap_or_else(token_error_response),
66
67			| "urn:ietf:params:oauth:grant-type:device_code" =>
68				token_device_code(&services, &body)
69					.await
70					.unwrap_or_else(token_error_response),
71
72			| _ => oauth_error(
73				StatusCode::BAD_REQUEST,
74				"unsupported_grant_type",
75				"Unsupported grant_type",
76			),
77		}
78	};
79	let mut response = inner.into_response();
80	let headers = response.headers_mut();
81	headers.insert(CACHE_CONTROL, http::HeaderValue::from_static("no-store"));
82	headers.insert(PRAGMA, http::HeaderValue::from_static("no-cache"));
83	response
84}
85
86async fn token_authorization_code(
87	services: &Services,
88	body: &TokenRequest,
89) -> Result<Response<Body>> {
90	let code = body
91		.code
92		.as_deref()
93		.ok_or_else(|| err!(Request(InvalidParam("code is required"))))?;
94
95	let redirect_uri = body
96		.redirect_uri
97		.as_deref()
98		.ok_or_else(|| err!(Request(InvalidParam("redirect_uri is required"))))?;
99
100	let client_id = body
101		.client_id
102		.as_deref()
103		.ok_or_else(|| err!(Request(InvalidParam("client_id is required"))))?;
104
105	let require_pkce = services
106		.server
107		.config
108		.authentication
109		.oidc
110		.require_pkce;
111
112	let session = services
113		.oauth
114		.get_server()?
115		.exchange_auth_code(
116			code,
117			client_id,
118			redirect_uri,
119			body.code_verifier.as_deref(),
120			require_pkce,
121		)
122		.await?;
123
124	issue_tokens(services, ApprovedGrant {
125		client_id: &session.client_id,
126		scope: &session.scope,
127		user_id: &session.user_id,
128		nonce: session.nonce,
129		idp_id: session.idp_id,
130	})
131	.await
132}
133
134/// RFC 8628 §3.4: poll the device-code grant; pending, denied and expired map
135/// to the §3.5 error codes.
136async fn token_device_code(services: &Services, body: &TokenRequest) -> Result<Response<Body>> {
137	let device_code = body
138		.device_code
139		.as_deref()
140		.ok_or_else(|| err!(Request(InvalidParam("device_code is required"))))?;
141
142	let client_id = body
143		.client_id
144		.as_deref()
145		.ok_or_else(|| err!(Request(InvalidParam("client_id is required"))))?;
146
147	match services
148		.oauth
149		.get_server()?
150		.poll_device_grant(device_code, client_id)
151		.await?
152	{
153		| DeviceGrantPoll::Pending => Ok(oauth_error(
154			StatusCode::BAD_REQUEST,
155			"authorization_pending",
156			"The user has not yet completed authorization",
157		)),
158
159		| DeviceGrantPoll::Denied => Ok(oauth_error(
160			StatusCode::BAD_REQUEST,
161			"access_denied",
162			"The authorization request was denied",
163		)),
164
165		| DeviceGrantPoll::Expired => Ok(oauth_error(
166			StatusCode::BAD_REQUEST,
167			"expired_token",
168			"The device code has expired",
169		)),
170
171		| DeviceGrantPoll::Approved(grant) =>
172			issue_tokens(services, ApprovedGrant {
173				client_id: &grant.client_id,
174				scope: &grant.scope,
175				user_id: &grant.user_id,
176				nonce: None,
177				idp_id: grant.idp_id,
178			})
179			.await,
180	}
181}
182
183/// Mint the access token, refresh token and device for an authenticated grant,
184/// honoring the MSC2967 device scope and emitting the OAuth token response.
185async fn issue_tokens(services: &Services, grant: ApprovedGrant<'_>) -> Result<Response<Body>> {
186	let ApprovedGrant { client_id, scope, user_id, nonce, idp_id } = grant;
187
188	require_account_usable(services, user_id).await?;
189
190	let strict_scope = services
191		.server
192		.config
193		.authentication
194		.oidc
195		.strict_scope;
196
197	let (granted_scope, requested_device_id) = narrow_scope(scope, strict_scope)?;
198
199	let requested_device: Option<OwnedDeviceId> = requested_device_id
200		.as_deref()
201		.map(OwnedDeviceId::from);
202
203	if requested_device.is_none()
204		&& services
205			.server
206			.config
207			.authentication
208			.oidc
209			.require_device_scope
210	{
211		return Err!(Request(InvalidParam(
212			"a device scope (urn:matrix:client:device:<id>) is required"
213		)));
214	}
215
216	let (access_token, expires_in) = services.users.generate_access_token(true);
217	let refresh_token = generate_refresh_token();
218	let client_name = services
219		.oauth
220		.get_server()?
221		.get_client(client_id)
222		.await
223		.ok()
224		.and_then(|c| c.client_name);
225
226	let device_display_name = client_name.as_deref().unwrap_or("OIDC Client");
227
228	let iss = services.oauth.get_server()?.issuer_url()?;
229	let id_token = granted_scope
230		.contains("openid")
231		.then(|| {
232			let now = now().as_secs();
233			let claims = IdTokenClaims {
234				iss,
235				sub: user_id.to_string(),
236				aud: client_id.to_owned(),
237				exp: now.saturating_add(3600),
238				iat: now,
239				nonce,
240				at_hash: Some(Server::at_hash(&access_token)),
241			};
242
243			services
244				.oauth
245				.get_server()?
246				.sign_id_token(&claims)
247		})
248		.transpose()?;
249
250	let device_id = services
251		.users
252		.create_device(
253			user_id,
254			requested_device.as_deref(),
255			(Some(&access_token), expires_in),
256			Some(&refresh_token),
257			Some(device_display_name),
258			None,
259		)
260		.await?;
261
262	// Tag the device with the IdP that authenticated it; a native (local
263	// account) grant carries no provider, so the device stays untagged.
264	if let Some(idp_id) = idp_id.filter(|idp| !idp.is_empty()) {
265		services
266			.users
267			.mark_oidc_device(user_id, &device_id, &idp_id);
268	}
269
270	info!("{user_id} logged in via OIDC on {device_id} ({device_display_name})");
271
272	// MSC2967: echo a server-chosen device id back in the scope when the client
273	// omitted one.
274	let scope = if requested_device.is_some() {
275		granted_scope
276	} else {
277		warn!(%user_id, %device_id, "OIDC client omitted the device scope; generated a device id");
278
279		let sep = if granted_scope.is_empty() { "" } else { " " };
280		format!("{granted_scope}{sep}urn:matrix:client:device:{device_id}")
281	};
282
283	let mut response = json!({
284		"access_token": access_token,
285		"refresh_token": refresh_token,
286		"scope": scope,
287		"token_type": "Bearer",
288	});
289
290	if let Some(id_token) = id_token {
291		response["id_token"] = json!(id_token);
292	}
293
294	if let Some(expires_in) = expires_in {
295		response["expires_in"] = json!(expires_in.as_secs());
296	}
297
298	Ok(Json(response).into_response())
299}
300
301async fn token_refresh(services: &Services, body: &TokenRequest) -> Result<Response<Body>> {
302	let presented = body
303		.refresh_token
304		.as_deref()
305		.ok_or_else(|| err!(Request(InvalidParam("refresh_token is required"))))?;
306
307	let token = services
308		.users
309		.classify_refresh_token(presented)
310		.await;
311
312	let (user_id, _) = token
313		.owner()
314		.ok_or_else(|| err!(Request(Forbidden("Invalid refresh token"))))?;
315
316	let (access_token, expires_in) = services.users.generate_access_token(true);
317	let refresh_token = services
318		.users
319		.rotate_refresh_token(
320			&token,
321			presented,
322			(&access_token, expires_in),
323			services.users.deactivated_check(user_id),
324		)
325		.await?;
326
327	token_refresh_response(&access_token, &refresh_token, expires_in)
328}
329
330fn token_refresh_response(
331	access_token: &str,
332	refresh_token: &str,
333	expires_in: Option<Duration>,
334) -> Result<Response<Body>> {
335	let mut response = json!({
336		"access_token": access_token,
337		"refresh_token": refresh_token,
338		"token_type": "Bearer",
339	});
340
341	if let Some(expires_in) = expires_in {
342		response["expires_in"] = json!(expires_in.as_secs());
343	}
344
345	Ok(Json(response).into_response())
346}
347
348/// RFC 6749 §5.2: map error to correct HTTP status and OAuth2 error code.
349/// Client-side errors (invalid grant, bad params) → 400 invalid_grant.
350/// Server-side errors → 500 server_error with sanitized message.
351#[expect(clippy::needless_pass_by_value)]
352fn token_error_response(e: Error) -> Response<Body> {
353	if !e.status_code().is_client_error() {
354		return oauth_error(
355			StatusCode::INTERNAL_SERVER_ERROR,
356			"server_error",
357			"An internal error occurred",
358		);
359	}
360
361	oauth_error(StatusCode::BAD_REQUEST, "invalid_grant", &e.sanitized_message())
362}