Skip to main content

headless_lms_server/domain/oauth/
token_query.rs

1use super::helpers::{oauth_error, oauth_invalid_client, oauth_invalid_request};
2use super::oauth_validate::OAuthValidate;
3use super::resource_indicator::ResourceIndicator;
4use crate::prelude::*;
5use domain::error::OAuthErrorCode;
6use models::library::oauth::GrantTypeName;
7use secrecy::{ExposeSecret, SecretString};
8use serde::Deserialize;
9use std::collections::HashMap;
10
11#[derive(Debug, Deserialize, Clone, Default)]
12pub struct TokenQuery {
13    pub client_id: Option<String>,
14    pub client_secret: Option<SecretString>, // optional: public clients won't send this
15    #[serde(flatten)]
16    pub grant: Option<TokenGrant>,
17    pub resource: Option<ResourceIndicator>,
18    // OAuth 2.0 requires unknown params be ignored at /token (RFC 6749 §3.2)
19    #[serde(flatten)]
20    pub _extra: HashMap<String, String>,
21}
22
23#[derive(Debug, Clone)]
24pub struct TokenParams {
25    pub client_id: String,
26    pub client_secret: Option<SecretString>, // carry through; validation for presence is done per-client later
27    pub grant: TokenGrant,
28    pub resource: Option<ResourceIndicator>,
29}
30
31impl OAuthValidate for TokenQuery {
32    type Output = TokenParams;
33
34    fn validate(&self) -> Result<Self::Output, ControllerError> {
35        let client_id = self.client_id.as_deref().unwrap_or_default();
36
37        if client_id.is_empty() {
38            return Err(oauth_invalid_client("client_id is required"));
39        }
40
41        // Grant-specific required params
42        let grant = match self.grant.clone() {
43            Some(grant @ TokenGrant::AuthorizationCode { .. }) => {
44                if let TokenGrant::AuthorizationCode {
45                    code, redirect_uri, ..
46                } = &grant
47                {
48                    if code.expose_secret().is_empty() {
49                        return Err(oauth_invalid_request(
50                            "code is required for authorization_code grant",
51                            None,
52                            None,
53                        ));
54                    }
55                    // If redirect_uri is provided, it must not be empty
56                    if matches!(redirect_uri.as_deref(), Some("")) {
57                        return Err(oauth_invalid_request(
58                            "redirect_uri must not be empty when provided",
59                            None,
60                            None,
61                        ));
62                    }
63                }
64                // PKCE code_verifier is verified at the token handler (if the code had a challenge)
65                grant
66            }
67            Some(grant @ TokenGrant::RefreshToken { .. }) => {
68                if let TokenGrant::RefreshToken { refresh_token, .. } = &grant
69                    && refresh_token.expose_secret().is_empty()
70                {
71                    return Err(oauth_invalid_request(
72                        "refresh_token is required",
73                        None,
74                        None,
75                    ));
76                }
77                grant
78            }
79            Some(grant @ TokenGrant::DeviceCode { .. }) => {
80                if let TokenGrant::DeviceCode { device_code } = &grant
81                    && device_code.expose_secret().is_empty()
82                {
83                    return Err(oauth_invalid_request("device_code is required", None, None));
84                }
85                grant
86            }
87            Some(TokenGrant::Unknown) => {
88                return Err(oauth_error(
89                    OAuthErrorCode::UnsupportedGrantType,
90                    "unsupported grant type",
91                    None,
92                    None,
93                ));
94            }
95            None => {
96                return Err(oauth_invalid_request("grant_type is required", None, None));
97            }
98        };
99
100        Ok(TokenParams {
101            client_id: client_id.to_string(),
102            client_secret: self.client_secret.clone(), // may be None for public clients
103            grant,
104            resource: self.resource.clone(),
105        })
106    }
107}
108
109#[derive(Debug, Deserialize, Clone)]
110#[serde(tag = "grant_type", rename_all = "snake_case")]
111pub enum TokenGrant {
112    AuthorizationCode {
113        code: SecretString,
114        /// Optional per RFC 6749 §4.1.3 (required if it was present in the authorization request)
115        redirect_uri: Option<String>,
116        /// Optional; enforced later if the code stored a challenge
117        code_verifier: Option<SecretString>,
118    },
119    RefreshToken {
120        refresh_token: SecretString,
121        /// Optional down-scope
122        #[serde(default)]
123        scope: Option<String>,
124    },
125    /// OAuth 2.0 Device Authorization Grant (RFC 8628). The `grant_type` value
126    /// is the exact URN `urn:ietf:params:oauth:grant-type:device_code`.
127    #[serde(rename = "urn:ietf:params:oauth:grant-type:device_code")]
128    DeviceCode { device_code: SecretString },
129    #[serde(other)]
130    Unknown,
131}
132
133impl TokenGrant {
134    pub fn kind(&self) -> GrantTypeName {
135        match self {
136            TokenGrant::AuthorizationCode { .. } => GrantTypeName::AuthorizationCode,
137            TokenGrant::RefreshToken { .. } => GrantTypeName::RefreshToken,
138            TokenGrant::DeviceCode { .. } => GrantTypeName::DeviceCode,
139            TokenGrant::Unknown => {
140                unreachable!(
141                    "Unknown grant type should be caught by validate() before kind() is called"
142                )
143            }
144        }
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use domain::error::{ControllerError, ControllerErrorType, OAuthErrorCode};
152    use serde_json::{Value, json};
153
154    fn assert_oauth_error(
155        result: Result<TokenParams, ControllerError>,
156        expected_error: OAuthErrorCode,
157        expected_description: &str,
158    ) {
159        match result {
160            Err(err) => match err.error_type() {
161                ControllerErrorType::OAuthError(data) => {
162                    assert_eq!(data.error, expected_error.as_str());
163                    assert_eq!(data.error_description, expected_description);
164                }
165                other => panic!("Expected OAuthError, got {:?}", other),
166            },
167            Ok(_) => panic!("Expected Err, got Ok(())"),
168        }
169    }
170
171    #[test]
172    fn token_missing_client_id() {
173        let q = TokenQuery {
174            client_id: None,
175            client_secret: None,
176            grant: None,
177            resource: None,
178            _extra: Default::default(),
179        };
180        let res = q.validate();
181        assert_oauth_error(res, OAuthErrorCode::InvalidClient, "client_id is required");
182    }
183
184    #[test]
185    fn token_public_client_without_secret_is_ok() {
186        let q = TokenQuery {
187            client_id: Some("cid".into()),
188            client_secret: None,
189            grant: Some(TokenGrant::RefreshToken {
190                refresh_token: "rt".into(),
191                scope: None,
192            }),
193            resource: None,
194            _extra: Default::default(),
195        };
196        assert!(q.validate().is_ok());
197    }
198
199    #[test]
200    fn token_missing_grant_type() {
201        let q = TokenQuery {
202            client_id: Some("cid".into()),
203            client_secret: Some("sec".into()),
204            grant: None,
205            resource: None,
206            _extra: Default::default(),
207        };
208        let res = q.validate();
209        assert_oauth_error(
210            res,
211            OAuthErrorCode::InvalidRequest,
212            "grant_type is required",
213        );
214    }
215
216    #[test]
217    fn token_auth_code_missing_code() {
218        let q = TokenQuery {
219            client_id: Some("cid".into()),
220            client_secret: Some("sec".into()),
221            grant: Some(TokenGrant::AuthorizationCode {
222                code: "".into(),
223                redirect_uri: Some("http://localhost".into()),
224                code_verifier: None,
225            }),
226            resource: None,
227            _extra: Default::default(),
228        };
229        let res = q.validate();
230        assert_oauth_error(
231            res,
232            OAuthErrorCode::InvalidRequest,
233            "code is required for authorization_code grant",
234        );
235    }
236
237    #[test]
238    fn token_auth_code_empty_redirect_uri_is_invalid() {
239        let q = TokenQuery {
240            client_id: Some("cid".into()),
241            client_secret: Some("sec".into()),
242            grant: Some(TokenGrant::AuthorizationCode {
243                code: "C".into(),
244                redirect_uri: Some("".into()),
245                code_verifier: None,
246            }),
247            resource: None,
248            _extra: Default::default(),
249        };
250        let res = q.validate();
251        assert_oauth_error(
252            res,
253            OAuthErrorCode::InvalidRequest,
254            "redirect_uri must not be empty when provided",
255        );
256    }
257
258    #[test]
259    fn token_auth_code_minimal_ok_without_redirect_uri_or_pkce() {
260        // Allowed by validator; actual PKCE/redirect checks happen in handler.
261        let q = TokenQuery {
262            client_id: Some("cid".into()),
263            client_secret: Some("sec".into()),
264            grant: Some(TokenGrant::AuthorizationCode {
265                code: "C".into(),
266                redirect_uri: None,
267                code_verifier: None,
268            }),
269            resource: None,
270            _extra: Default::default(),
271        };
272        assert!(q.validate().is_ok());
273    }
274
275    #[test]
276    fn token_auth_code_with_pkce_ok() {
277        let q = TokenQuery {
278            client_id: Some("cid".into()),
279            client_secret: Some("sec".into()),
280            grant: Some(TokenGrant::AuthorizationCode {
281                code: "C".into(),
282                redirect_uri: Some("http://localhost".into()),
283                code_verifier: Some("verifier".into()),
284            }),
285            resource: None,
286            _extra: Default::default(),
287        };
288        assert!(q.validate().is_ok());
289    }
290
291    #[test]
292    fn token_refresh_missing_field() {
293        let q = TokenQuery {
294            client_id: Some("cid".into()),
295            client_secret: Some("sec".into()),
296            grant: Some(TokenGrant::RefreshToken {
297                refresh_token: "".into(),
298                scope: None,
299            }),
300            resource: None,
301            _extra: Default::default(),
302        };
303        let res = q.validate();
304        assert_oauth_error(
305            res,
306            OAuthErrorCode::InvalidRequest,
307            "refresh_token is required",
308        );
309    }
310
311    #[test]
312    fn token_valid_auth_code() {
313        let q = TokenQuery {
314            client_id: Some("cid".into()),
315            client_secret: Some("sec".into()),
316            grant: Some(TokenGrant::AuthorizationCode {
317                code: "abc".into(),
318                redirect_uri: Some("http://localhost".into()),
319                code_verifier: None,
320            }),
321            resource: None,
322            _extra: Default::default(),
323        };
324        assert!(q.validate().is_ok());
325    }
326
327    #[test]
328    fn token_device_code_missing_field() {
329        let q = TokenQuery {
330            client_id: Some("cid".into()),
331            client_secret: None,
332            grant: Some(TokenGrant::DeviceCode {
333                device_code: "".into(),
334            }),
335            resource: None,
336            _extra: Default::default(),
337        };
338        let res = q.validate();
339        assert_oauth_error(
340            res,
341            OAuthErrorCode::InvalidRequest,
342            "device_code is required",
343        );
344    }
345
346    #[test]
347    fn token_valid_device_code() {
348        let q = TokenQuery {
349            client_id: Some("cid".into()),
350            client_secret: None,
351            grant: Some(TokenGrant::DeviceCode {
352                device_code: "dc".into(),
353            }),
354            resource: None,
355            _extra: Default::default(),
356        };
357        assert!(q.validate().is_ok());
358    }
359
360    #[test]
361    fn token_valid_refresh_token() {
362        let q = TokenQuery {
363            client_id: Some("cid".into()),
364            client_secret: Some("sec".into()),
365            grant: Some(TokenGrant::RefreshToken {
366                refresh_token: "r1".into(),
367                scope: None,
368            }),
369            resource: None,
370            _extra: Default::default(),
371        };
372        assert!(q.validate().is_ok());
373    }
374
375    #[test]
376    fn token_unknown_params_are_captured_in_extra() {
377        let v: Value = json!({
378            "client_id": "cid",
379            "client_secret": "sec",
380            "grant_type": "refresh_token",
381            "refresh_token": "rt",
382            "extra_param": "zzz"
383        });
384        let q: TokenQuery = serde_json::from_value(v).unwrap();
385        assert_eq!(q._extra.get("extra_param").map(String::as_str), Some("zzz"));
386        assert!(q.validate().is_ok());
387    }
388
389    #[test]
390    fn token_grant_tagging_deserializes_properly() {
391        // authorization_code branch
392        let ac: TokenQuery = serde_json::from_value(json!({
393            "client_id": "cid",
394            "client_secret": "sec",
395            "grant_type": "authorization_code",
396            "code": "C",
397            "redirect_uri": "http://localhost",
398            "code_verifier": "ver"
399        }))
400        .unwrap();
401        match ac.grant {
402            Some(TokenGrant::AuthorizationCode {
403                code,
404                redirect_uri,
405                code_verifier,
406            }) => {
407                assert_eq!(code.expose_secret(), "C");
408                assert_eq!(redirect_uri.as_deref(), Some("http://localhost"));
409                assert_eq!(
410                    code_verifier.as_ref().map(|v| v.expose_secret()),
411                    Some("ver")
412                );
413            }
414            _ => panic!("expected AuthorizationCode"),
415        }
416
417        // refresh_token branch
418        let rt: TokenQuery = serde_json::from_value(json!({
419            "client_id": "cid",
420            "client_secret": "sec",
421            "grant_type": "refresh_token",
422            "refresh_token": "R",
423            "scope": "read write"
424        }))
425        .unwrap();
426        match rt.grant {
427            Some(TokenGrant::RefreshToken {
428                refresh_token,
429                scope,
430            }) => {
431                assert_eq!(refresh_token.expose_secret(), "R");
432                assert_eq!(scope.as_deref(), Some("read write"));
433            }
434            _ => panic!("expected RefreshToken"),
435        }
436
437        // device_code branch (RFC 8628 URN grant_type)
438        let dc: TokenQuery = serde_json::from_value(json!({
439            "client_id": "cid",
440            "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
441            "device_code": "DEV"
442        }))
443        .unwrap();
444        match dc.grant {
445            Some(TokenGrant::DeviceCode { device_code }) => {
446                assert_eq!(device_code.expose_secret(), "DEV");
447            }
448            _ => panic!("expected DeviceCode"),
449        }
450    }
451}