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