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