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>, #[serde(flatten)]
16 pub grant: Option<TokenGrant>,
17 pub resource: Option<ResourceIndicator>,
18 #[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>, 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 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 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 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(), 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 redirect_uri: Option<String>,
116 code_verifier: Option<SecretString>,
118 },
119 RefreshToken {
120 refresh_token: SecretString,
121 #[serde(default)]
123 scope: Option<String>,
124 },
125 #[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 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 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 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 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}