headless_lms_server/domain/oauth/
revoke_query.rs

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