Skip to main content

headless_lms_server/domain/oauth/
introspect_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 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(oauth_invalid_client("client_id is required"));
44        }
45
46        let token = self
47            .token
48            .as_ref()
49            .map(|t| t.expose_secret())
50            .unwrap_or_default();
51        if token.is_empty() {
52            return Err(oauth_invalid_request("token is required", None, None));
53        }
54
55        // RFC 7662 ยง2.1: "The resource server MAY ignore the hint."
56        // Normalize token_type_hint: only recognize "access_token" and "refresh_token",
57        // treat any other value as None (ignore unknown hints)
58        let token_type_hint = self.token_type_hint.as_deref().and_then(|h| {
59            match h {
60                "access_token" | "refresh_token" => Some(h.to_string()),
61                _ => None, // Unknown hints are ignored per RFC 7662
62            }
63        });
64
65        Ok(IntrospectParams {
66            client_id: client_id.to_string(),
67            client_secret: self.client_secret.clone(),
68            // Non-empty presence verified above.
69            token: SecretString::new(token.into()),
70            token_type_hint,
71        })
72    }
73}