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)]
108#[serde(rename_all = "lowercase")]
109pub enum ApiKeyLocation {
110 Header,
112 Query,
114 Cookie,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
122#[serde(rename_all = "camelCase")]
123pub struct HttpAuthSecurityScheme {
124 pub scheme: String,
126
127 #[serde(skip_serializing_if = "Option::is_none")]
129 pub bearer_format: Option<String>,
130
131 #[serde(skip_serializing_if = "Option::is_none")]
133 pub description: Option<String>,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
140#[serde(rename_all = "camelCase")]
141pub struct OAuth2SecurityScheme {
142 pub flows: OAuthFlows,
144
145 #[serde(skip_serializing_if = "Option::is_none")]
147 pub oauth2_metadata_url: Option<String>,
148
149 #[serde(skip_serializing_if = "Option::is_none")]
151 pub description: Option<String>,
152}
153
154#[non_exhaustive]
159#[derive(Debug, Clone, Serialize, Deserialize)]
160#[serde(rename_all = "camelCase")]
161pub enum OAuthFlows {
162 AuthorizationCode(AuthorizationCodeFlow),
164
165 ClientCredentials(ClientCredentialsFlow),
167
168 DeviceCode(DeviceCodeFlow),
170
171 Implicit(ImplicitFlow),
173
174 Password(PasswordOAuthFlow),
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
180#[serde(rename_all = "camelCase")]
181pub struct AuthorizationCodeFlow {
182 pub authorization_url: String,
184
185 pub token_url: String,
187
188 #[serde(skip_serializing_if = "Option::is_none")]
190 pub refresh_url: Option<String>,
191
192 pub scopes: HashMap<String, String>,
194
195 #[serde(skip_serializing_if = "Option::is_none")]
197 pub pkce_required: Option<bool>,
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
202#[serde(rename_all = "camelCase")]
203pub struct ClientCredentialsFlow {
204 pub token_url: String,
206
207 #[serde(skip_serializing_if = "Option::is_none")]
209 pub refresh_url: Option<String>,
210
211 pub scopes: HashMap<String, String>,
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize)]
217#[serde(rename_all = "camelCase")]
218pub struct DeviceCodeFlow {
219 pub device_authorization_url: String,
221
222 pub token_url: String,
224
225 #[serde(skip_serializing_if = "Option::is_none")]
227 pub refresh_url: Option<String>,
228
229 pub scopes: HashMap<String, String>,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
235#[serde(rename_all = "camelCase")]
236pub struct ImplicitFlow {
237 pub authorization_url: String,
239
240 #[serde(skip_serializing_if = "Option::is_none")]
242 pub refresh_url: Option<String>,
243
244 pub scopes: HashMap<String, String>,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
250#[serde(rename_all = "camelCase")]
251pub struct PasswordOAuthFlow {
252 pub token_url: String,
254
255 #[serde(skip_serializing_if = "Option::is_none")]
257 pub refresh_url: Option<String>,
258
259 pub scopes: HashMap<String, String>,
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
267#[serde(rename_all = "camelCase")]
268pub struct OpenIdConnectSecurityScheme {
269 pub open_id_connect_url: String,
271
272 #[serde(skip_serializing_if = "Option::is_none")]
274 pub description: Option<String>,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize)]
281#[serde(rename_all = "camelCase")]
282pub struct MutualTlsSecurityScheme {
283 #[serde(skip_serializing_if = "Option::is_none")]
285 pub description: Option<String>,
286}
287
288#[cfg(test)]
291mod tests {
292 use super::*;
293
294 #[test]
295 fn api_key_scheme_roundtrip() {
296 let scheme = SecurityScheme::ApiKey(ApiKeySecurityScheme {
297 location: ApiKeyLocation::Header,
298 name: "X-API-Key".into(),
299 description: None,
300 });
301 let json = serde_json::to_string(&scheme).expect("serialize");
302 assert!(
303 json.contains("\"type\":\"apiKey\""),
304 "tag must be present: {json}"
305 );
306 assert!(
307 json.contains("\"in\":\"header\""),
308 "location must use 'in': {json}"
309 );
310
311 let back: SecurityScheme = serde_json::from_str(&json).expect("deserialize");
312 match &back {
313 SecurityScheme::ApiKey(s) => {
314 assert_eq!(s.location, ApiKeyLocation::Header);
315 assert_eq!(s.name, "X-API-Key");
316 }
317 _ => panic!("expected ApiKey variant"),
318 }
319 }
320
321 #[test]
322 fn http_bearer_scheme_roundtrip() {
323 let scheme = SecurityScheme::Http(HttpAuthSecurityScheme {
324 scheme: "bearer".into(),
325 bearer_format: Some("JWT".into()),
326 description: None,
327 });
328 let json = serde_json::to_string(&scheme).expect("serialize");
329 assert!(json.contains("\"type\":\"http\""));
330 let back: SecurityScheme = serde_json::from_str(&json).expect("deserialize");
331 if let SecurityScheme::Http(h) = back {
332 assert_eq!(h.bearer_format.as_deref(), Some("JWT"));
333 } else {
334 panic!("wrong variant");
335 }
336 }
337
338 #[test]
339 fn oauth2_scheme_roundtrip() {
340 let scheme = SecurityScheme::OAuth2(Box::new(OAuth2SecurityScheme {
341 flows: OAuthFlows::ClientCredentials(ClientCredentialsFlow {
342 token_url: "https://auth.example.com/token".into(),
343 refresh_url: None,
344 scopes: HashMap::from([("read".into(), "Read access".into())]),
345 }),
346 oauth2_metadata_url: None,
347 description: None,
348 }));
349 let json = serde_json::to_string(&scheme).expect("serialize");
350 assert!(json.contains("\"type\":\"oauth2\""));
351 let back: SecurityScheme = serde_json::from_str(&json).expect("deserialize");
352 match &back {
353 SecurityScheme::OAuth2(o) => match &o.flows {
354 OAuthFlows::ClientCredentials(cc) => {
355 assert_eq!(cc.token_url, "https://auth.example.com/token");
356 assert_eq!(
357 cc.scopes.get("read").map(String::as_str),
358 Some("Read access")
359 );
360 }
361 _ => panic!("expected ClientCredentials flow"),
362 },
363 _ => panic!("expected OAuth2 variant"),
364 }
365 }
366
367 #[test]
368 fn mutual_tls_scheme_roundtrip() {
369 let scheme = SecurityScheme::MutualTls(MutualTlsSecurityScheme { description: None });
370 let json = serde_json::to_string(&scheme).expect("serialize");
371 assert!(json.contains("\"type\":\"mutualTLS\""));
372 let back: SecurityScheme = serde_json::from_str(&json).expect("deserialize");
373 match &back {
374 SecurityScheme::MutualTls(m) => {
375 assert!(m.description.is_none());
376 }
377 _ => panic!("expected MutualTls variant"),
378 }
379 }
380
381 #[test]
382 fn api_key_location_serialization() {
383 assert_eq!(
384 serde_json::to_string(&ApiKeyLocation::Header).expect("ser"),
385 "\"header\""
386 );
387 assert_eq!(
388 serde_json::to_string(&ApiKeyLocation::Query).expect("ser"),
389 "\"query\""
390 );
391 assert_eq!(
392 serde_json::to_string(&ApiKeyLocation::Cookie).expect("ser"),
393 "\"cookie\""
394 );
395 }
396
397 #[test]
398 fn wire_format_security_requirement() {
399 let req = SecurityRequirement {
401 schemes: HashMap::from([(
402 "oauth2".into(),
403 StringList {
404 list: vec!["read".into(), "write".into()],
405 },
406 )]),
407 };
408 let json = serde_json::to_string(&req).unwrap();
409 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
410 assert_eq!(
411 parsed["schemes"]["oauth2"]["list"],
412 serde_json::json!(["read", "write"])
413 );
414
415 let back: SecurityRequirement = serde_json::from_str(&json).unwrap();
417 assert_eq!(back.schemes["oauth2"].list, vec!["read", "write"]);
418 }
419
420 #[test]
421 fn wire_format_password_oauth_flow() {
422 let flows = OAuthFlows::Password(PasswordOAuthFlow {
423 token_url: "https://auth.example.com/token".into(),
424 refresh_url: None,
425 scopes: HashMap::from([("read".into(), "Read access".into())]),
426 });
427 let json = serde_json::to_string(&flows).unwrap();
428 assert!(
429 json.contains("\"password\""),
430 "password flow must be present: {json}"
431 );
432
433 let back: OAuthFlows = serde_json::from_str(&json).unwrap();
434 match back {
435 OAuthFlows::Password(p) => {
436 assert_eq!(p.token_url, "https://auth.example.com/token");
437 }
438 _ => panic!("expected Password flow"),
439 }
440 }
441}