1#![allow(clippy::doc_markdown)]
7
8use std::collections::HashMap;
19
20use serde::{Deserialize, Serialize};
21
22pub type NamedSecuritySchemes = HashMap<String, SecurityScheme>;
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
32#[serde(rename_all = "camelCase")]
33pub struct StringList {
34 pub list: Vec<String>,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
42#[serde(rename_all = "camelCase")]
43pub struct SecurityRequirement {
44 pub schemes: HashMap<String, StringList>,
46}
47
48#[non_exhaustive]
52#[derive(Debug, Clone, Serialize, Deserialize)]
53#[serde(tag = "type")]
54pub enum SecurityScheme {
55 #[serde(rename = "apiKey")]
57 ApiKey(ApiKeySecurityScheme),
58
59 #[serde(rename = "http")]
61 Http(HttpAuthSecurityScheme),
62
63 #[serde(rename = "oauth2")]
67 OAuth2(Box<OAuth2SecurityScheme>),
68
69 #[serde(rename = "openIdConnect")]
71 OpenIdConnect(OpenIdConnectSecurityScheme),
72
73 #[serde(rename = "mutualTLS")]
75 MutualTls(MutualTlsSecurityScheme),
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub struct ApiKeySecurityScheme {
84 #[serde(rename = "in")]
88 pub location: ApiKeyLocation,
89
90 pub name: String,
92
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub description: Option<String>,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
100#[serde(rename_all = "lowercase")]
101pub enum ApiKeyLocation {
102 Header,
104 Query,
106 Cookie,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
114#[serde(rename_all = "camelCase")]
115pub struct HttpAuthSecurityScheme {
116 pub scheme: String,
118
119 #[serde(skip_serializing_if = "Option::is_none")]
121 pub bearer_format: Option<String>,
122
123 #[serde(skip_serializing_if = "Option::is_none")]
125 pub description: Option<String>,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
132#[serde(rename_all = "camelCase")]
133pub struct OAuth2SecurityScheme {
134 pub flows: OAuthFlows,
136
137 #[serde(skip_serializing_if = "Option::is_none")]
139 pub oauth2_metadata_url: Option<String>,
140
141 #[serde(skip_serializing_if = "Option::is_none")]
143 pub description: Option<String>,
144}
145
146#[derive(Debug, Clone, Default, Serialize, Deserialize)]
150#[serde(rename_all = "camelCase")]
151pub struct OAuthFlows {
152 #[serde(skip_serializing_if = "Option::is_none")]
154 pub authorization_code: Option<AuthorizationCodeFlow>,
155
156 #[serde(skip_serializing_if = "Option::is_none")]
158 pub client_credentials: Option<ClientCredentialsFlow>,
159
160 #[serde(skip_serializing_if = "Option::is_none")]
162 pub device_code: Option<DeviceCodeFlow>,
163
164 #[serde(skip_serializing_if = "Option::is_none")]
166 pub implicit: Option<ImplicitFlow>,
167
168 #[serde(skip_serializing_if = "Option::is_none")]
170 pub password: Option<PasswordOAuthFlow>,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
175#[serde(rename_all = "camelCase")]
176pub struct AuthorizationCodeFlow {
177 pub authorization_url: String,
179
180 pub token_url: String,
182
183 #[serde(skip_serializing_if = "Option::is_none")]
185 pub refresh_url: Option<String>,
186
187 pub scopes: HashMap<String, String>,
189
190 #[serde(skip_serializing_if = "Option::is_none")]
192 pub pkce_required: Option<bool>,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
197#[serde(rename_all = "camelCase")]
198pub struct ClientCredentialsFlow {
199 pub token_url: String,
201
202 #[serde(skip_serializing_if = "Option::is_none")]
204 pub refresh_url: Option<String>,
205
206 pub scopes: HashMap<String, String>,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
212#[serde(rename_all = "camelCase")]
213pub struct DeviceCodeFlow {
214 pub device_authorization_url: String,
216
217 pub token_url: String,
219
220 #[serde(skip_serializing_if = "Option::is_none")]
222 pub refresh_url: Option<String>,
223
224 pub scopes: HashMap<String, String>,
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize)]
230#[serde(rename_all = "camelCase")]
231pub struct ImplicitFlow {
232 pub authorization_url: String,
234
235 #[serde(skip_serializing_if = "Option::is_none")]
237 pub refresh_url: Option<String>,
238
239 pub scopes: HashMap<String, String>,
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize)]
245#[serde(rename_all = "camelCase")]
246pub struct PasswordOAuthFlow {
247 pub token_url: String,
249
250 #[serde(skip_serializing_if = "Option::is_none")]
252 pub refresh_url: Option<String>,
253
254 pub scopes: HashMap<String, String>,
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize)]
262#[serde(rename_all = "camelCase")]
263pub struct OpenIdConnectSecurityScheme {
264 pub open_id_connect_url: String,
266
267 #[serde(skip_serializing_if = "Option::is_none")]
269 pub description: Option<String>,
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize)]
276#[serde(rename_all = "camelCase")]
277pub struct MutualTlsSecurityScheme {
278 #[serde(skip_serializing_if = "Option::is_none")]
280 pub description: Option<String>,
281}
282
283#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn api_key_scheme_roundtrip() {
291 let scheme = SecurityScheme::ApiKey(ApiKeySecurityScheme {
292 location: ApiKeyLocation::Header,
293 name: "X-API-Key".into(),
294 description: None,
295 });
296 let json = serde_json::to_string(&scheme).expect("serialize");
297 assert!(
298 json.contains("\"type\":\"apiKey\""),
299 "tag must be present: {json}"
300 );
301 assert!(
302 json.contains("\"in\":\"header\""),
303 "location must use 'in': {json}"
304 );
305
306 let back: SecurityScheme = serde_json::from_str(&json).expect("deserialize");
307 assert!(matches!(back, SecurityScheme::ApiKey(_)));
308 }
309
310 #[test]
311 fn http_bearer_scheme_roundtrip() {
312 let scheme = SecurityScheme::Http(HttpAuthSecurityScheme {
313 scheme: "bearer".into(),
314 bearer_format: Some("JWT".into()),
315 description: None,
316 });
317 let json = serde_json::to_string(&scheme).expect("serialize");
318 assert!(json.contains("\"type\":\"http\""));
319 let back: SecurityScheme = serde_json::from_str(&json).expect("deserialize");
320 if let SecurityScheme::Http(h) = back {
321 assert_eq!(h.bearer_format.as_deref(), Some("JWT"));
322 } else {
323 panic!("wrong variant");
324 }
325 }
326
327 #[test]
328 fn oauth2_scheme_roundtrip() {
329 let scheme = SecurityScheme::OAuth2(Box::new(OAuth2SecurityScheme {
330 flows: OAuthFlows {
331 authorization_code: None,
332 client_credentials: Some(ClientCredentialsFlow {
333 token_url: "https://auth.example.com/token".into(),
334 refresh_url: None,
335 scopes: HashMap::from([("read".into(), "Read access".into())]),
336 }),
337 device_code: None,
338 implicit: None,
339 password: None,
340 },
341 oauth2_metadata_url: None,
342 description: None,
343 }));
344 let json = serde_json::to_string(&scheme).expect("serialize");
345 assert!(json.contains("\"type\":\"oauth2\""));
346 let back: SecurityScheme = serde_json::from_str(&json).expect("deserialize");
347 assert!(matches!(back, SecurityScheme::OAuth2(_)));
348 }
349
350 #[test]
351 fn mutual_tls_scheme_roundtrip() {
352 let scheme = SecurityScheme::MutualTls(MutualTlsSecurityScheme { description: None });
353 let json = serde_json::to_string(&scheme).expect("serialize");
354 assert!(json.contains("\"type\":\"mutualTLS\""));
355 let back: SecurityScheme = serde_json::from_str(&json).expect("deserialize");
356 assert!(matches!(back, SecurityScheme::MutualTls(_)));
357 }
358
359 #[test]
360 fn api_key_location_serialization() {
361 assert_eq!(
362 serde_json::to_string(&ApiKeyLocation::Header).expect("ser"),
363 "\"header\""
364 );
365 assert_eq!(
366 serde_json::to_string(&ApiKeyLocation::Query).expect("ser"),
367 "\"query\""
368 );
369 assert_eq!(
370 serde_json::to_string(&ApiKeyLocation::Cookie).expect("ser"),
371 "\"cookie\""
372 );
373 }
374
375 #[test]
376 fn wire_format_security_requirement() {
377 let req = SecurityRequirement {
379 schemes: HashMap::from([(
380 "oauth2".into(),
381 StringList {
382 list: vec!["read".into(), "write".into()],
383 },
384 )]),
385 };
386 let json = serde_json::to_string(&req).unwrap();
387 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
388 assert_eq!(
389 parsed["schemes"]["oauth2"]["list"],
390 serde_json::json!(["read", "write"])
391 );
392
393 let back: SecurityRequirement = serde_json::from_str(&json).unwrap();
395 assert_eq!(back.schemes["oauth2"].list, vec!["read", "write"]);
396 }
397
398 #[test]
399 fn wire_format_password_oauth_flow() {
400 let flows = OAuthFlows {
401 authorization_code: None,
402 client_credentials: None,
403 device_code: None,
404 implicit: None,
405 password: Some(PasswordOAuthFlow {
406 token_url: "https://auth.example.com/token".into(),
407 refresh_url: None,
408 scopes: HashMap::from([("read".into(), "Read access".into())]),
409 }),
410 };
411 let json = serde_json::to_string(&flows).unwrap();
412 assert!(
413 json.contains("\"password\""),
414 "password flow must be present: {json}"
415 );
416
417 let back: OAuthFlows = serde_json::from_str(&json).unwrap();
418 assert!(back.password.is_some());
419 assert_eq!(
420 back.password.unwrap().token_url,
421 "https://auth.example.com/token"
422 );
423 }
424}