tuwunel_api/oidc/
revoke.rs1use 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#[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#[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 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}