Skip to main content

headless_lms_server/domain/oauth/
revoke_query.rs

1use super::helpers::{oauth_invalid_client, oauth_invalid_request};
2use super::oauth_validate::OAuthValidate;
3use crate::prelude::*;
4use secrecy::{ExposeSecret, SecretString};
5use serde::Deserialize;
6use std::collections::HashMap;
7
8/// Query parameters for the OAuth 2.0 token revocation endpoint (RFC 7009).
9///
10/// The revocation endpoint allows clients to revoke access tokens or refresh tokens.
11#[derive(Debug, Deserialize)]
12pub struct RevokeQuery {
13    pub client_id: Option<String>,
14    pub client_secret: Option<SecretString>,
15    /// The token to be revoked (required).
16    pub token: Option<SecretString>,
17
18    /// Hint about the type of the token being revoked (optional).
19    /// Valid values: "access_token" or "refresh_token".
20    pub token_type_hint: Option<String>,
21    // OAuth 2.0 requires unknown params be ignored at /revoke (RFC 7009 §2.1)
22    #[serde(flatten)]
23    pub _extra: HashMap<String, String>,
24}
25
26#[derive(Debug)]
27pub struct RevokeParams {
28    pub client_id: String,
29    pub client_secret: Option<SecretString>,
30    pub token: SecretString,
31    pub token_type_hint: Option<String>,
32}
33
34impl OAuthValidate for RevokeQuery {
35    type Output = RevokeParams;
36
37    fn validate(&self) -> Result<Self::Output, ControllerError> {
38        let client_id = self.client_id.as_deref().unwrap_or_default();
39
40        if client_id.is_empty() {
41            return Err(oauth_invalid_client("client_id is required"));
42        }
43
44        let token = self
45            .token
46            .as_ref()
47            .map(|t| t.expose_secret())
48            .unwrap_or_default();
49        if token.is_empty() {
50            return Err(oauth_invalid_request("token is required", None, None));
51        }
52
53        // RFC 7009 §2.1: "The authorization server MAY ignore the hint."
54        // Normalize token_type_hint: only recognize "access_token" and "refresh_token",
55        // treat any other value as None (ignore unknown hints)
56        let token_type_hint = self.token_type_hint.as_deref().and_then(|h| {
57            match h {
58                "access_token" | "refresh_token" => Some(h.to_string()),
59                _ => None, // Unknown hints are ignored per RFC 7009
60            }
61        });
62
63        Ok(RevokeParams {
64            client_id: client_id.to_string(),
65            client_secret: self.client_secret.clone(),
66            // Non-empty presence verified above.
67            token: SecretString::new(token.into()),
68            token_type_hint,
69        })
70    }
71}