1#![allow(clippy::doc_markdown)]
9
10use std::collections::HashMap;
21
22use serde::{Deserialize, Serialize};
23
24pub type NamedSecuritySchemes = HashMap<String, SecurityScheme>;
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
34#[serde(rename_all = "camelCase")]
35pub struct StringList {
36 pub list: Vec<String>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
44#[serde(rename_all = "camelCase")]
45pub struct SecurityRequirement {
46 pub schemes: HashMap<String, StringList>,
48}
49
50#[non_exhaustive]
54#[derive(Debug, Clone, Serialize, Deserialize)]
55#[serde(tag = "type")]
56pub enum SecurityScheme {
57 #[serde(rename = "apiKey")]
59 ApiKey(ApiKeySecurityScheme),
60
61 #[serde(rename = "http")]
63 Http(HttpAuthSecurityScheme),
64
65 #[serde(rename = "oauth2")]
69 OAuth2(Box<OAuth2SecurityScheme>),
70
71 #[serde(rename = "openIdConnect")]
73 OpenIdConnect(OpenIdConnectSecurityScheme),
74
75 #[serde(rename = "mutualTLS")]
77 MutualTls(MutualTlsSecurityScheme),
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
84#[serde(rename_all = "camelCase")]
85pub struct ApiKeySecurityScheme {
86 #[serde(rename = "in")]
90 pub location: ApiKeyLocation,
91
92 pub name: String,
94
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub description: Option<String>,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
102#[serde(rename_all = "lowercase")]
103pub enum ApiKeyLocation {
104 Header,
106 Query,
108 Cookie,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
116#[serde(rename_all = "camelCase")]
117pub struct HttpAuthSecurityScheme {
118 pub scheme: String,
120
121 #[serde(skip_serializing_if = "Option::is_none")]
123 pub bearer_format: Option<String>,
124
125 #[serde(skip_serializing_if = "Option::is_none")]
127 pub description: Option<String>,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
134#[serde(rename_all = "camelCase")]
135pub struct OAuth2SecurityScheme {
136 pub flows: OAuthFlows,
138
139 #[serde(skip_serializing_if = "Option::is_none")]
141 pub oauth2_metadata_url: Option<String>,
142
143 #[serde(skip_serializing_if = "Option::is_none")]
145 pub description: Option<String>,
146}
147
148#[derive(Debug, Clone, Default, Serialize, Deserialize)]
152#[serde(rename_all = "camelCase")]
153pub struct OAuthFlows {
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub authorization_code: Option<AuthorizationCodeFlow>,
157
158 #[serde(skip_serializing_if = "Option::is_none")]
160 pub client_credentials: Option<ClientCredentialsFlow>,
161
162 #[serde(skip_serializing_if = "Option::is_none")]
164 pub device_code: Option<DeviceCodeFlow>,
165
166 #[serde(skip_serializing_if = "Option::is_none")]
168 pub implicit: Option<ImplicitFlow>,
169
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub password: Option<PasswordOAuthFlow>,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
177#[serde(rename_all = "camelCase")]
178pub struct AuthorizationCodeFlow {
179 pub authorization_url: String,
181
182 pub token_url: String,
184
185 #[serde(skip_serializing_if = "Option::is_none")]
187 pub refresh_url: Option<String>,
188
189 pub scopes: HashMap<String, String>,
191
192 #[serde(skip_serializing_if = "Option::is_none")]
194 pub pkce_required: Option<bool>,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
199#[serde(rename_all = "camelCase")]
200pub struct ClientCredentialsFlow {
201 pub token_url: String,
203
204 #[serde(skip_serializing_if = "Option::is_none")]
206 pub refresh_url: Option<String>,
207
208 pub scopes: HashMap<String, String>,
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
214#[serde(rename_all = "camelCase")]
215pub struct DeviceCodeFlow {
216 pub device_authorization_url: String,
218
219 pub token_url: String,
221
222 #[serde(skip_serializing_if = "Option::is_none")]
224 pub refresh_url: Option<String>,
225
226 pub scopes: HashMap<String, String>,
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize)]
232#[serde(rename_all = "camelCase")]
233pub struct ImplicitFlow {
234 pub authorization_url: String,
236
237 #[serde(skip_serializing_if = "Option::is_none")]
239 pub refresh_url: Option<String>,
240
241 pub scopes: HashMap<String, String>,
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
247#[serde(rename_all = "camelCase")]
248pub struct PasswordOAuthFlow {
249 pub token_url: String,
251
252 #[serde(skip_serializing_if = "Option::is_none")]
254 pub refresh_url: Option<String>,
255
256 pub scopes: HashMap<String, String>,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
264#[serde(rename_all = "camelCase")]
265pub struct OpenIdConnectSecurityScheme {
266 pub open_id_connect_url: String,
268
269 #[serde(skip_serializing_if = "Option::is_none")]
271 pub description: Option<String>,
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize)]
278#[serde(rename_all = "camelCase")]
279pub struct MutualTlsSecurityScheme {
280 #[serde(skip_serializing_if = "Option::is_none")]
282 pub description: Option<String>,
283}
284
285#[cfg(test)]
288mod tests {
289 use super::*;
290
291 #[test]
292 fn api_key_scheme_roundtrip() {
293 let scheme = SecurityScheme::ApiKey(ApiKeySecurityScheme {
294 location: ApiKeyLocation::Header,
295 name: "X-API-Key".into(),
296 description: None,
297 });
298 let json = serde_json::to_string(&scheme).expect("serialize");
299 assert!(
300 json.contains("\"type\":\"apiKey\""),
301 "tag must be present: {json}"
302 );
303 assert!(
304 json.contains("\"in\":\"header\""),
305 "location must use 'in': {json}"
306 );
307
308 let back: SecurityScheme = serde_json::from_str(&json).expect("deserialize");
309 match &back {
310 SecurityScheme::ApiKey(s) => {
311 assert_eq!(s.location, ApiKeyLocation::Header);
312 assert_eq!(s.name, "X-API-Key");
313 }
314 _ => panic!("expected ApiKey variant"),
315 }
316 }
317
318 #[test]
319 fn http_bearer_scheme_roundtrip() {
320 let scheme = SecurityScheme::Http(HttpAuthSecurityScheme {
321 scheme: "bearer".into(),
322 bearer_format: Some("JWT".into()),
323 description: None,
324 });
325 let json = serde_json::to_string(&scheme).expect("serialize");
326 assert!(json.contains("\"type\":\"http\""));
327 let back: SecurityScheme = serde_json::from_str(&json).expect("deserialize");
328 if let SecurityScheme::Http(h) = back {
329 assert_eq!(h.bearer_format.as_deref(), Some("JWT"));
330 } else {
331 panic!("wrong variant");
332 }
333 }
334
335 #[test]
336 fn oauth2_scheme_roundtrip() {
337 let scheme = SecurityScheme::OAuth2(Box::new(OAuth2SecurityScheme {
338 flows: OAuthFlows {
339 authorization_code: None,
340 client_credentials: Some(ClientCredentialsFlow {
341 token_url: "https://auth.example.com/token".into(),
342 refresh_url: None,
343 scopes: HashMap::from([("read".into(), "Read access".into())]),
344 }),
345 device_code: None,
346 implicit: None,
347 password: None,
348 },
349 oauth2_metadata_url: None,
350 description: None,
351 }));
352 let json = serde_json::to_string(&scheme).expect("serialize");
353 assert!(json.contains("\"type\":\"oauth2\""));
354 let back: SecurityScheme = serde_json::from_str(&json).expect("deserialize");
355 match &back {
356 SecurityScheme::OAuth2(o) => {
357 let cc = o
358 .flows
359 .client_credentials
360 .as_ref()
361 .expect("client_credentials");
362 assert_eq!(cc.token_url, "https://auth.example.com/token");
363 assert_eq!(
364 cc.scopes.get("read").map(String::as_str),
365 Some("Read access")
366 );
367 }
368 _ => panic!("expected OAuth2 variant"),
369 }
370 }
371
372 #[test]
373 fn mutual_tls_scheme_roundtrip() {
374 let scheme = SecurityScheme::MutualTls(MutualTlsSecurityScheme { description: None });
375 let json = serde_json::to_string(&scheme).expect("serialize");
376 assert!(json.contains("\"type\":\"mutualTLS\""));
377 let back: SecurityScheme = serde_json::from_str(&json).expect("deserialize");
378 match &back {
379 SecurityScheme::MutualTls(m) => {
380 assert!(m.description.is_none());
381 }
382 _ => panic!("expected MutualTls variant"),
383 }
384 }
385
386 #[test]
387 fn api_key_location_serialization() {
388 assert_eq!(
389 serde_json::to_string(&ApiKeyLocation::Header).expect("ser"),
390 "\"header\""
391 );
392 assert_eq!(
393 serde_json::to_string(&ApiKeyLocation::Query).expect("ser"),
394 "\"query\""
395 );
396 assert_eq!(
397 serde_json::to_string(&ApiKeyLocation::Cookie).expect("ser"),
398 "\"cookie\""
399 );
400 }
401
402 #[test]
403 fn wire_format_security_requirement() {
404 let req = SecurityRequirement {
406 schemes: HashMap::from([(
407 "oauth2".into(),
408 StringList {
409 list: vec!["read".into(), "write".into()],
410 },
411 )]),
412 };
413 let json = serde_json::to_string(&req).unwrap();
414 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
415 assert_eq!(
416 parsed["schemes"]["oauth2"]["list"],
417 serde_json::json!(["read", "write"])
418 );
419
420 let back: SecurityRequirement = serde_json::from_str(&json).unwrap();
422 assert_eq!(back.schemes["oauth2"].list, vec!["read", "write"]);
423 }
424
425 #[test]
426 fn wire_format_password_oauth_flow() {
427 let flows = OAuthFlows {
428 authorization_code: None,
429 client_credentials: None,
430 device_code: None,
431 implicit: None,
432 password: Some(PasswordOAuthFlow {
433 token_url: "https://auth.example.com/token".into(),
434 refresh_url: None,
435 scopes: HashMap::from([("read".into(), "Read access".into())]),
436 }),
437 };
438 let json = serde_json::to_string(&flows).unwrap();
439 assert!(
440 json.contains("\"password\""),
441 "password flow must be present: {json}"
442 );
443
444 let back: OAuthFlows = serde_json::from_str(&json).unwrap();
445 assert!(back.password.is_some());
446 assert_eq!(
447 back.password.unwrap().token_url,
448 "https://auth.example.com/token"
449 );
450 }
451}