Skip to main content

headless_lms_server/domain/oauth/
revoke_query.rs

1use super::oauth_validate::OAuthValidate;
2use crate::prelude::*;
3use domain::error::{OAuthErrorCode, OAuthErrorData};
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(ControllerError::new(
42                ControllerErrorType::OAuthError(Box::new(OAuthErrorData {
43                    error: OAuthErrorCode::InvalidClient.as_str().into(),
44                    error_description: "client_id is required".into(),
45                    redirect_uri: None,
46                    state: None,
47                    nonce: None,
48                })),
49                "Missing client_id",
50                None::<anyhow::Error>,
51            ));
52        }
53
54        let token = self
55            .token
56            .as_ref()
57            .map(|t| t.expose_secret())
58            .unwrap_or_default();
59        if token.is_empty() {
60            return Err(ControllerError::new(
61                ControllerErrorType::OAuthError(Box::new(OAuthErrorData {
62                    error: OAuthErrorCode::InvalidRequest.as_str().into(),
63                    error_description: "token is required".into(),
64                    redirect_uri: None,
65                    state: None,
66                    nonce: None,
67                })),
68                "Missing token",
69                None::<anyhow::Error>,
70            ));
71        }
72
73        // RFC 7009 §2.1: "The authorization server MAY ignore the hint."
74        // Normalize token_type_hint: only recognize "access_token" and "refresh_token",
75        // treat any other value as None (ignore unknown hints)
76        let token_type_hint = self.token_type_hint.as_deref().and_then(|h| {
77            match h {
78                "access_token" | "refresh_token" => Some(h.to_string()),
79                _ => None, // Unknown hints are ignored per RFC 7009
80            }
81        });
82
83        Ok(RevokeParams {
84            client_id: client_id.to_string(),
85            client_secret: self.client_secret.clone(),
86            // Non-empty presence verified above.
87            token: SecretString::new(token.into()),
88            token_type_hint,
89        })
90    }
91}