Skip to main content

headless_lms_server/domain/oauth/
introspect_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 introspection endpoint (RFC 7662).
9///
10/// The introspection endpoint allows resource servers to query the authorization server
11/// about the active state and metadata of an access token.
12#[derive(Debug, Deserialize)]
13pub struct IntrospectQuery {
14    pub client_id: Option<String>,
15    pub client_secret: Option<SecretString>,
16    /// The token to be introspected (required).
17    pub token: Option<SecretString>,
18
19    /// Hint about the type of the token being introspected (optional).
20    /// Valid values: "access_token" or "refresh_token".
21    /// Currently only "access_token" is supported.
22    pub token_type_hint: Option<String>,
23    // OAuth 2.0 requires unknown params be ignored
24    #[serde(flatten)]
25    pub _extra: HashMap<String, String>,
26}
27
28#[derive(Debug)]
29pub struct IntrospectParams {
30    pub client_id: String,
31    pub client_secret: Option<SecretString>,
32    pub token: SecretString,
33    pub token_type_hint: Option<String>,
34}
35
36impl OAuthValidate for IntrospectQuery {
37    type Output = IntrospectParams;
38
39    fn validate(&self) -> Result<Self::Output, ControllerError> {
40        let client_id = self.client_id.as_deref().unwrap_or_default();
41
42        if client_id.is_empty() {
43            return Err(ControllerError::new(
44                ControllerErrorType::OAuthError(Box::new(OAuthErrorData {
45                    error: OAuthErrorCode::InvalidClient.as_str().into(),
46                    error_description: "client_id is required".into(),
47                    redirect_uri: None,
48                    state: None,
49                    nonce: None,
50                })),
51                "Missing client_id",
52                None::<anyhow::Error>,
53            ));
54        }
55
56        let token = self
57            .token
58            .as_ref()
59            .map(|t| t.expose_secret())
60            .unwrap_or_default();
61        if token.is_empty() {
62            return Err(ControllerError::new(
63                ControllerErrorType::OAuthError(Box::new(OAuthErrorData {
64                    error: OAuthErrorCode::InvalidRequest.as_str().into(),
65                    error_description: "token is required".into(),
66                    redirect_uri: None,
67                    state: None,
68                    nonce: None,
69                })),
70                "Missing token",
71                None::<anyhow::Error>,
72            ));
73        }
74
75        // RFC 7662 ยง2.1: "The resource server MAY ignore the hint."
76        // Normalize token_type_hint: only recognize "access_token" and "refresh_token",
77        // treat any other value as None (ignore unknown hints)
78        let token_type_hint = self.token_type_hint.as_deref().and_then(|h| {
79            match h {
80                "access_token" | "refresh_token" => Some(h.to_string()),
81                _ => None, // Unknown hints are ignored per RFC 7662
82            }
83        });
84
85        Ok(IntrospectParams {
86            client_id: client_id.to_string(),
87            client_secret: self.client_secret.clone(),
88            // Non-empty presence verified above.
89            token: SecretString::new(token.into()),
90            token_type_hint,
91        })
92    }
93}