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#[non_exhaustive]
153#[derive(Debug, Clone, Serialize, Deserialize)]
154#[serde(rename_all = "camelCase")]
155pub enum OAuthFlows {
156 AuthorizationCode(AuthorizationCodeFlow),
158
159 ClientCredentials(ClientCredentialsFlow),
161
162 DeviceCode(DeviceCodeFlow),
164
165 Implicit(ImplicitFlow),
167
168 Password(PasswordOAuthFlow),
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
174#[serde(rename_all = "camelCase")]
175pub struct AuthorizationCodeFlow {
176 pub authorization_url: String,
178
179 pub token_url: String,
181
182 #[serde(skip_serializing_if = "Option::is_none")]
184 pub refresh_url: Option<String>,
185
186 pub scopes: HashMap<String, String>,
188
189 #[serde(skip_serializing_if = "Option::is_none")]
191 pub pkce_required: Option<bool>,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
196#[serde(rename_all = "camelCase")]
197pub struct ClientCredentialsFlow {
198 pub token_url: String,
200
201 #[serde(skip_serializing_if = "Option::is_none")]
203 pub refresh_url: Option<String>,
204
205 pub scopes: HashMap<String, String>,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
211#[serde(rename_all = "camelCase")]
212pub struct DeviceCodeFlow {
213 pub device_authorization_url: String,
215
216 pub token_url: String,
218
219 #[serde(skip_serializing_if = "Option::is_none")]
221 pub refresh_url: Option<String>,
222
223 pub scopes: HashMap<String, String>,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
229#[serde(rename_all = "camelCase")]
230pub struct ImplicitFlow {
231 pub authorization_url: String,
233
234 #[serde(skip_serializing_if = "Option::is_none")]
236 pub refresh_url: Option<String>,
237
238 pub scopes: HashMap<String, String>,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
244#[serde(rename_all = "camelCase")]
245pub struct PasswordOAuthFlow {
246 pub token_url: String,
248
249 #[serde(skip_serializing_if = "Option::is_none")]
251 pub refresh_url: Option<String>,
252
253 pub scopes: HashMap<String, String>,
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize)]
261#[serde(rename_all = "camelCase")]
262pub struct OpenIdConnectSecurityScheme {
263 pub open_id_connect_url: String,
265
266 #[serde(skip_serializing_if = "Option::is_none")]
268 pub description: Option<String>,
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
275#[serde(rename_all = "camelCase")]
276pub struct MutualTlsSecurityScheme {
277 #[serde(skip_serializing_if = "Option::is_none")]
279 pub description: Option<String>,
280}
281
282#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn api_key_scheme_roundtrip() {
290 let scheme = SecurityScheme::ApiKey(ApiKeySecurityScheme {
291 location: ApiKeyLocation::Header,
292 name: "X-API-Key".into(),
293 description: None,
294 });
295 let json = serde_json::to_string(&scheme).expect("serialize");
296 assert!(
297 json.contains("\"type\":\"apiKey\""),
298 "tag must be present: {json}"
299 );
300 assert!(
301 json.contains("\"in\":\"header\""),
302 "location must use 'in': {json}"
303 );
304
305 let back: SecurityScheme = serde_json::from_str(&json).expect("deserialize");
306 match &back {
307 SecurityScheme::ApiKey(s) => {
308 assert_eq!(s.location, ApiKeyLocation::Header);
309 assert_eq!(s.name, "X-API-Key");
310 }
311 _ => panic!("expected ApiKey variant"),
312 }
313 }
314
315 #[test]
316 fn http_bearer_scheme_roundtrip() {
317 let scheme = SecurityScheme::Http(HttpAuthSecurityScheme {
318 scheme: "bearer".into(),
319 bearer_format: Some("JWT".into()),
320 description: None,
321 });
322 let json = serde_json::to_string(&scheme).expect("serialize");
323 assert!(json.contains("\"type\":\"http\""));
324 let back: SecurityScheme = serde_json::from_str(&json).expect("deserialize");
325 if let SecurityScheme::Http(h) = back {
326 assert_eq!(h.bearer_format.as_deref(), Some("JWT"));
327 } else {
328 panic!("wrong variant");
329 }
330 }
331
332 #[test]
333 fn oauth2_scheme_roundtrip() {
334 let scheme = SecurityScheme::OAuth2(Box::new(OAuth2SecurityScheme {
335 flows: OAuthFlows::ClientCredentials(ClientCredentialsFlow {
336 token_url: "https://auth.example.com/token".into(),
337 refresh_url: None,
338 scopes: HashMap::from([("read".into(), "Read access".into())]),
339 }),
340 oauth2_metadata_url: None,
341 description: None,
342 }));
343 let json = serde_json::to_string(&scheme).expect("serialize");
344 assert!(json.contains("\"type\":\"oauth2\""));
345 let back: SecurityScheme = serde_json::from_str(&json).expect("deserialize");
346 match &back {
347 SecurityScheme::OAuth2(o) => match &o.flows {
348 OAuthFlows::ClientCredentials(cc) => {
349 assert_eq!(cc.token_url, "https://auth.example.com/token");
350 assert_eq!(
351 cc.scopes.get("read").map(String::as_str),
352 Some("Read access")
353 );
354 }
355 _ => panic!("expected ClientCredentials flow"),
356 },
357 _ => panic!("expected OAuth2 variant"),
358 }
359 }
360
361 #[test]
362 fn mutual_tls_scheme_roundtrip() {
363 let scheme = SecurityScheme::MutualTls(MutualTlsSecurityScheme { description: None });
364 let json = serde_json::to_string(&scheme).expect("serialize");
365 assert!(json.contains("\"type\":\"mutualTLS\""));
366 let back: SecurityScheme = serde_json::from_str(&json).expect("deserialize");
367 match &back {
368 SecurityScheme::MutualTls(m) => {
369 assert!(m.description.is_none());
370 }
371 _ => panic!("expected MutualTls variant"),
372 }
373 }
374
375 #[test]
376 fn api_key_location_serialization() {
377 assert_eq!(
378 serde_json::to_string(&ApiKeyLocation::Header).expect("ser"),
379 "\"header\""
380 );
381 assert_eq!(
382 serde_json::to_string(&ApiKeyLocation::Query).expect("ser"),
383 "\"query\""
384 );
385 assert_eq!(
386 serde_json::to_string(&ApiKeyLocation::Cookie).expect("ser"),
387 "\"cookie\""
388 );
389 }
390
391 #[test]
392 fn wire_format_security_requirement() {
393 let req = SecurityRequirement {
395 schemes: HashMap::from([(
396 "oauth2".into(),
397 StringList {
398 list: vec!["read".into(), "write".into()],
399 },
400 )]),
401 };
402 let json = serde_json::to_string(&req).unwrap();
403 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
404 assert_eq!(
405 parsed["schemes"]["oauth2"]["list"],
406 serde_json::json!(["read", "write"])
407 );
408
409 let back: SecurityRequirement = serde_json::from_str(&json).unwrap();
411 assert_eq!(back.schemes["oauth2"].list, vec!["read", "write"]);
412 }
413
414 #[test]
415 fn wire_format_password_oauth_flow() {
416 let flows = OAuthFlows::Password(PasswordOAuthFlow {
417 token_url: "https://auth.example.com/token".into(),
418 refresh_url: None,
419 scopes: HashMap::from([("read".into(), "Read access".into())]),
420 });
421 let json = serde_json::to_string(&flows).unwrap();
422 assert!(
423 json.contains("\"password\""),
424 "password flow must be present: {json}"
425 );
426
427 let back: OAuthFlows = serde_json::from_str(&json).unwrap();
428 match back {
429 OAuthFlows::Password(p) => {
430 assert_eq!(p.token_url, "https://auth.example.com/token");
431 }
432 _ => panic!("expected Password flow"),
433 }
434 }
435}