Skip to main content

tuwunel_api/oidc/
revoke.rs

1use axum::{
2	body::Body,
3	extract::{Form, State},
4	response::{IntoResponse, Response},
5};
6use http::{
7	HeaderValue, StatusCode,
8	header::{CACHE_CONTROL, PRAGMA},
9};
10use serde::Deserialize;
11use tuwunel_service::Services;
12
13use super::oauth_error;
14
15/// MSC4254 / RFC7009 token revocation request body
16/// (`application/x-www-form-urlencoded`).
17///
18/// `client_id` is accepted but not validated: per MSC4254 the server SHOULD
19/// revoke even when `client_id` is missing or does not match, since
20/// secret-scanning tools rely on this to neutralise leaked tokens.
21#[derive(Debug, Deserialize)]
22pub(crate) struct RevokeRequest {
23	token: Option<String>,
24
25	#[serde(default)]
26	token_type_hint: Option<String>,
27
28	#[serde(default, rename = "client_id")]
29	_client_id: Option<String>,
30}
31
32/// `POST /_tuwunel/oidc/revoke`
33///
34/// MSC4254: OAuth 2.0 Token Revocation per RFC7009. Revokes both the access
35/// and refresh tokens associated with the supplied token.
36#[tracing::instrument(level = "debug", skip_all)]
37pub(crate) async fn revoke_route(
38	State(services): State<crate::State>,
39	Form(body): Form<RevokeRequest>,
40) -> impl IntoResponse {
41	let response = revoke(&services, body)
42		.await
43		.unwrap_or_else(|err| err);
44
45	with_cache_headers(response)
46}
47
48async fn revoke(services: &Services, body: RevokeRequest) -> Result<Response, Response> {
49	let token = body
50		.token
51		.filter(|t| !t.is_empty())
52		.ok_or_else(|| {
53			oauth_error(StatusCode::BAD_REQUEST, "invalid_request", "token parameter is required")
54		})?;
55
56	if let Some(hint) = body.token_type_hint.as_deref()
57		&& !matches!(hint, "access_token" | "refresh_token")
58	{
59		return Err(oauth_error(
60			StatusCode::BAD_REQUEST,
61			"unsupported_token_type",
62			"token_type_hint must be access_token or refresh_token",
63		));
64	}
65
66	// RFC7009 ยง2.2: invalid or unknown tokens still produce a 200 OK.
67	// remove_device drops both the access and refresh tokens (and the device).
68	if let Ok((user_id, device_id, _)) = services.users.find_from_token(&token).await {
69		services
70			.users
71			.remove_device(&user_id, &device_id)
72			.await;
73	}
74
75	Ok(Response::builder()
76		.status(StatusCode::OK)
77		.body(Body::empty())
78		.expect("empty 200 OK builds"))
79}
80
81fn with_cache_headers(mut response: Response) -> Response {
82	let headers = response.headers_mut();
83	headers.insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
84	headers.insert(PRAGMA, HeaderValue::from_static("no-cache"));
85	response
86}