1use indexmap::IndexMap;
47use utoipa::openapi::security::{
48 ApiKey as UtoipaApiKey, ApiKeyValue, AuthorizationCode, ClientCredentials, Flow, Http,
49 HttpAuthScheme, Implicit, OAuth2 as UtoipaOAuth2, OpenIdConnect as UtoipaOpenIdConnect,
50 Password, Scopes, SecurityScheme as UtoipaSecurityScheme,
51};
52
53#[derive(Debug, Clone, PartialEq)]
84pub enum SecurityScheme {
85 Bearer {
90 format: Option<String>,
92 description: Option<String>,
94 },
95
96 Basic {
100 description: Option<String>,
102 },
103
104 ApiKey {
108 name: String,
110 location: ApiKeyLocation,
112 description: Option<String>,
114 },
115
116 OAuth2 {
121 flows: Box<OAuth2Flows>,
123 description: Option<String>,
125 },
126
127 OpenIdConnect {
132 open_id_connect_url: String,
134 description: Option<String>,
136 },
137}
138
139impl SecurityScheme {
140 pub fn bearer() -> Self {
150 Self::Bearer {
151 format: None,
152 description: None,
153 }
154 }
155
156 pub fn bearer_with_format(format: impl Into<String>) -> Self {
170 Self::Bearer {
171 format: Some(format.into()),
172 description: None,
173 }
174 }
175
176 pub fn basic() -> Self {
186 Self::Basic { description: None }
187 }
188
189 pub fn api_key(name: impl Into<String>, location: ApiKeyLocation) -> Self {
204 Self::ApiKey {
205 name: name.into(),
206 location,
207 description: None,
208 }
209 }
210
211 pub fn openid_connect(url: impl Into<String>) -> Self {
225 Self::OpenIdConnect {
226 open_id_connect_url: url.into(),
227 description: None,
228 }
229 }
230
231 pub fn with_description(mut self, description: impl Into<String>) -> Self {
242 match &mut self {
243 SecurityScheme::Bearer {
244 description: desc, ..
245 } => *desc = Some(description.into()),
246 SecurityScheme::Basic { description: desc } => *desc = Some(description.into()),
247 SecurityScheme::ApiKey {
248 description: desc, ..
249 } => *desc = Some(description.into()),
250 SecurityScheme::OAuth2 {
251 description: desc, ..
252 } => *desc = Some(description.into()),
253 SecurityScheme::OpenIdConnect {
254 description: desc, ..
255 } => *desc = Some(description.into()),
256 }
257 self
258 }
259
260 pub(crate) fn to_utoipa(&self) -> UtoipaSecurityScheme {
262 match self {
263 SecurityScheme::Bearer {
264 format,
265 description,
266 } => {
267 let mut http = Http::new(HttpAuthScheme::Bearer);
268 if let Some(fmt) = format {
269 http.bearer_format = Some(fmt.clone());
270 }
271 if let Some(desc) = description {
272 http.description = Some(desc.clone());
273 }
274 UtoipaSecurityScheme::Http(http)
275 }
276 SecurityScheme::Basic { description } => {
277 let mut http = Http::new(HttpAuthScheme::Basic);
278 if let Some(desc) = description {
279 http.description = Some(desc.clone());
280 }
281 UtoipaSecurityScheme::Http(http)
282 }
283 SecurityScheme::ApiKey {
284 name,
285 location,
286 description,
287 } => {
288 let api_key_value = if let Some(desc) = description {
289 ApiKeyValue::with_description(name, desc)
290 } else {
291 ApiKeyValue::new(name)
292 };
293 let api_key = match location {
294 ApiKeyLocation::Header => UtoipaApiKey::Header(api_key_value),
295 ApiKeyLocation::Query => UtoipaApiKey::Query(api_key_value),
296 ApiKeyLocation::Cookie => UtoipaApiKey::Cookie(api_key_value),
297 };
298 UtoipaSecurityScheme::ApiKey(api_key)
299 }
300 SecurityScheme::OAuth2 { flows, description } => {
301 let mut oauth2 = flows.to_utoipa();
302 if let Some(desc) = description {
303 oauth2.description = Some(desc.clone());
304 }
305 UtoipaSecurityScheme::OAuth2(oauth2)
306 }
307 SecurityScheme::OpenIdConnect {
308 open_id_connect_url,
309 description,
310 } => {
311 let mut oidc = UtoipaOpenIdConnect::new(open_id_connect_url);
312 if let Some(desc) = description {
313 oidc.description = Some(desc.clone());
314 }
315 UtoipaSecurityScheme::OpenIdConnect(oidc)
316 }
317 }
318 }
319}
320
321#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
323pub enum ApiKeyLocation {
324 Header,
326 Query,
328 Cookie,
330}
331
332#[derive(Debug, Clone, PartialEq, Default)]
336pub struct OAuth2Flows {
337 pub authorization_code: Option<OAuth2Flow>,
339 pub client_credentials: Option<OAuth2Flow>,
341 pub implicit: Option<OAuth2ImplicitFlow>,
343 pub password: Option<OAuth2Flow>,
345}
346
347impl OAuth2Flows {
348 pub fn authorization_code(
350 authorization_url: impl Into<String>,
351 token_url: impl Into<String>,
352 scopes: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
353 ) -> Self {
354 Self {
355 authorization_code: Some(OAuth2Flow {
356 authorization_url: Some(authorization_url.into()),
357 token_url: token_url.into(),
358 refresh_url: None,
359 scopes: scopes
360 .into_iter()
361 .map(|(k, v)| (k.into(), v.into()))
362 .collect(),
363 }),
364 ..Default::default()
365 }
366 }
367
368 pub fn client_credentials(
370 token_url: impl Into<String>,
371 scopes: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
372 ) -> Self {
373 Self {
374 client_credentials: Some(OAuth2Flow {
375 authorization_url: None,
376 token_url: token_url.into(),
377 refresh_url: None,
378 scopes: scopes
379 .into_iter()
380 .map(|(k, v)| (k.into(), v.into()))
381 .collect(),
382 }),
383 ..Default::default()
384 }
385 }
386
387 fn to_utoipa(&self) -> UtoipaOAuth2 {
388 let mut flows: Vec<Flow> = Vec::new();
389
390 if let Some(flow) = &self.authorization_code {
391 let scopes = Scopes::from_iter(flow.scopes.clone());
392 let auth_code = if let Some(ref refresh) = flow.refresh_url {
393 AuthorizationCode::with_refresh_url(
394 flow.authorization_url.as_deref().unwrap_or_default(),
395 &flow.token_url,
396 scopes,
397 refresh,
398 )
399 } else {
400 AuthorizationCode::new(
401 flow.authorization_url.as_deref().unwrap_or_default(),
402 &flow.token_url,
403 scopes,
404 )
405 };
406 flows.push(Flow::AuthorizationCode(auth_code));
407 }
408
409 if let Some(flow) = &self.client_credentials {
410 let scopes = Scopes::from_iter(flow.scopes.clone());
411 let client_creds = if let Some(ref refresh) = flow.refresh_url {
412 ClientCredentials::with_refresh_url(&flow.token_url, scopes, refresh)
413 } else {
414 ClientCredentials::new(&flow.token_url, scopes)
415 };
416 flows.push(Flow::ClientCredentials(client_creds));
417 }
418
419 if let Some(flow) = &self.implicit {
420 let scopes = Scopes::from_iter(flow.scopes.clone());
421 let implicit = if let Some(ref refresh) = flow.refresh_url {
422 Implicit::with_refresh_url(&flow.authorization_url, scopes, refresh)
423 } else {
424 Implicit::new(&flow.authorization_url, scopes)
425 };
426 flows.push(Flow::Implicit(implicit));
427 }
428
429 if let Some(flow) = &self.password {
430 let scopes = Scopes::from_iter(flow.scopes.clone());
431 let password = if let Some(ref refresh) = flow.refresh_url {
432 Password::with_refresh_url(&flow.token_url, scopes, refresh)
433 } else {
434 Password::new(&flow.token_url, scopes)
435 };
436 flows.push(Flow::Password(password));
437 }
438
439 UtoipaOAuth2::new(flows)
440 }
441}
442
443#[derive(Debug, Clone, PartialEq)]
445pub struct OAuth2Flow {
446 pub authorization_url: Option<String>,
448 pub token_url: String,
450 pub refresh_url: Option<String>,
452 pub scopes: IndexMap<String, String>,
454}
455
456#[derive(Debug, Clone, PartialEq)]
458pub struct OAuth2ImplicitFlow {
459 pub authorization_url: String,
461 pub refresh_url: Option<String>,
463 pub scopes: IndexMap<String, String>,
465}
466
467#[derive(Debug, Clone, PartialEq, Eq)]
484pub struct SecurityRequirement {
485 pub name: String,
487 pub scopes: Vec<String>,
489}
490
491impl SecurityRequirement {
492 pub fn new(name: impl Into<String>) -> Self {
506 Self {
507 name: name.into(),
508 scopes: Vec::new(),
509 }
510 }
511
512 pub fn with_scopes(
527 name: impl Into<String>,
528 scopes: impl IntoIterator<Item = impl Into<String>>,
529 ) -> Self {
530 Self {
531 name: name.into(),
532 scopes: scopes.into_iter().map(Into::into).collect(),
533 }
534 }
535
536 pub(crate) fn to_utoipa(&self) -> utoipa::openapi::security::SecurityRequirement {
538 utoipa::openapi::security::SecurityRequirement::new(
539 &self.name,
540 self.scopes.iter().map(String::as_str),
541 )
542 }
543}
544
545#[cfg(test)]
546mod tests {
547 use super::*;
548
549 #[test]
550 fn test_bearer_scheme_creation() {
551 let scheme = SecurityScheme::bearer();
552 assert!(matches!(
553 scheme,
554 SecurityScheme::Bearer {
555 format: None,
556 description: None
557 }
558 ));
559 }
560
561 #[test]
562 fn test_bearer_with_format() {
563 let scheme = SecurityScheme::bearer_with_format("JWT");
564 assert!(matches!(
565 scheme,
566 SecurityScheme::Bearer {
567 format: Some(ref f),
568 description: None
569 } if f == "JWT"
570 ));
571 }
572
573 #[test]
574 fn test_basic_scheme_creation() {
575 let scheme = SecurityScheme::basic();
576 assert!(matches!(
577 scheme,
578 SecurityScheme::Basic { description: None }
579 ));
580 }
581
582 #[test]
583 fn test_api_key_scheme_creation() {
584 let scheme = SecurityScheme::api_key("X-API-Key", ApiKeyLocation::Header);
585 assert!(matches!(
586 scheme,
587 SecurityScheme::ApiKey {
588 ref name,
589 location: ApiKeyLocation::Header,
590 description: None
591 } if name == "X-API-Key"
592 ));
593 }
594
595 #[test]
596 fn test_with_description() {
597 let scheme = SecurityScheme::bearer().with_description("JWT Bearer token");
598 assert!(matches!(
599 scheme,
600 SecurityScheme::Bearer {
601 format: None,
602 description: Some(ref d)
603 } if d == "JWT Bearer token"
604 ));
605 }
606
607 #[test]
608 fn test_security_requirement_new() {
609 let req = SecurityRequirement::new("bearerAuth");
610 assert_eq!(req.name, "bearerAuth");
611 assert!(req.scopes.is_empty());
612 }
613
614 #[test]
615 fn test_security_requirement_with_scopes() {
616 let req = SecurityRequirement::with_scopes("oauth2", ["read:users", "write:users"]);
617 assert_eq!(req.name, "oauth2");
618 assert_eq!(req.scopes, vec!["read:users", "write:users"]);
619 }
620
621 #[test]
622 fn test_bearer_to_utoipa() {
623 let scheme = SecurityScheme::bearer_with_format("JWT").with_description("JWT token");
624 let utoipa_scheme = scheme.to_utoipa();
625
626 assert!(matches!(utoipa_scheme, UtoipaSecurityScheme::Http(_)));
627 }
628
629 #[test]
630 fn test_basic_to_utoipa() {
631 let scheme = SecurityScheme::basic();
632 let utoipa_scheme = scheme.to_utoipa();
633
634 assert!(matches!(utoipa_scheme, UtoipaSecurityScheme::Http(_)));
635 }
636
637 #[test]
638 fn test_api_key_to_utoipa() {
639 let scheme = SecurityScheme::api_key("X-API-Key", ApiKeyLocation::Header);
640 let utoipa_scheme = scheme.to_utoipa();
641
642 assert!(matches!(utoipa_scheme, UtoipaSecurityScheme::ApiKey(_)));
643 }
644
645 #[test]
646 fn test_openid_connect_to_utoipa() {
647 let scheme = SecurityScheme::openid_connect("https://auth.example.com/.well-known/openid");
648 let utoipa_scheme = scheme.to_utoipa();
649
650 assert!(matches!(
651 utoipa_scheme,
652 UtoipaSecurityScheme::OpenIdConnect(_)
653 ));
654 }
655
656 #[test]
657 fn test_oauth2_authorization_code_flows() {
658 let flows = OAuth2Flows::authorization_code(
659 "https://auth.example.com/authorize",
660 "https://auth.example.com/token",
661 [("read:users", "Read user data")],
662 );
663
664 assert!(flows.authorization_code.is_some());
665 assert!(flows.client_credentials.is_none());
666 }
667
668 #[test]
669 fn test_oauth2_client_credentials_flows() {
670 let flows = OAuth2Flows::client_credentials(
671 "https://auth.example.com/token",
672 [("api:access", "API access")],
673 );
674
675 assert!(flows.client_credentials.is_some());
676 assert!(flows.authorization_code.is_none());
677 }
678
679 #[test]
680 fn test_security_requirement_to_utoipa() {
681 let req = SecurityRequirement::with_scopes("oauth2", ["read:users"]);
682 let utoipa_req = req.to_utoipa();
683
684 assert!(format!("{utoipa_req:?}").contains("oauth2"));
686 }
687}