openapiv3_1/security.rs
1//! Implements [OpenAPI Security Schema][security] types.
2//!
3//! Refer to [`SecurityScheme`] for usage and more details.
4//!
5//! [security]: https://spec.openapis.org/oas/latest.html#security-scheme-object
6use std::iter;
7
8use indexmap::IndexMap;
9use serde_derive::{Deserialize, Serialize};
10
11use super::extensions::Extensions;
12
13/// OpenAPI [security requirement][security] object.
14///
15/// Security requirement holds list of required [`SecurityScheme`] *names* and possible *scopes* required
16/// to execute the operation.
17///
18/// Applying the security requirement to [`OpenApi`][crate::OpenApi] will make it globally
19/// available to all operations. Only one of the requirements must be
20/// satisfied.
21///
22/// [security]: https://spec.openapis.org/oas/latest.html#security-requirement-object
23#[non_exhaustive]
24#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Eq)]
25#[cfg_attr(feature = "debug", derive(Debug))]
26pub struct SecurityRequirement {
27 #[serde(flatten)]
28 value: IndexMap<String, Vec<String>>,
29}
30
31impl SecurityRequirement {
32 /// Construct a new [`SecurityRequirement`].
33 ///
34 /// Accepts name for the security requirement which must match to the name of available [`SecurityScheme`].
35 /// Second parameter is [`IntoIterator`] of [`Into<String>`] scopes needed by the [`SecurityRequirement`].
36 /// Scopes must match to the ones defined in [`SecurityScheme`].
37 ///
38 /// # Examples
39 ///
40 /// Create new security requirement with scopes.
41 /// ```rust
42 /// # use openapiv3_1::security::SecurityRequirement;
43 /// SecurityRequirement::new("api_oauth2_flow", ["edit:items", "read:items"]);
44 /// ```
45 ///
46 /// You can also create an empty security requirement with `Default::default()`.
47 /// ```rust
48 /// # use openapiv3_1::security::SecurityRequirement;
49 /// SecurityRequirement::default();
50 /// ```
51 ///
52 /// If you have more than one name in the security requirement you can use
53 /// [`SecurityRequirement::add`].
54 pub fn new<N: Into<String>, S: IntoIterator<Item = I>, I: Into<String>>(name: N, scopes: S) -> Self {
55 Self {
56 value: IndexMap::from_iter(iter::once_with(|| {
57 (
58 Into::<String>::into(name),
59 scopes
60 .into_iter()
61 .map(|scope| Into::<String>::into(scope))
62 .collect::<Vec<_>>(),
63 )
64 })),
65 }
66 }
67
68 /// Allows to add multiple names to security requirement.
69 ///
70 /// Accepts name for the security requirement which must match to the name of available [`SecurityScheme`].
71 /// Second parameter is [`IntoIterator`] of [`Into<String>`] scopes needed by the [`SecurityRequirement`].
72 /// Scopes must match to the ones defined in [`SecurityScheme`].
73 pub fn add<N: Into<String>, S: IntoIterator<Item = I>, I: Into<String>>(mut self, name: N, scopes: S) -> Self {
74 self.value.insert(
75 Into::<String>::into(name),
76 scopes.into_iter().map(Into::<String>::into).collect(),
77 );
78
79 self
80 }
81}
82
83/// OpenAPI [security scheme][security] for path operations.
84///
85/// [security]: https://spec.openapis.org/oas/latest.html#security-scheme-object
86///
87/// # Examples
88///
89/// Create implicit oauth2 flow security schema for path operations.
90/// ```rust
91/// # use openapiv3_1::security::{SecurityScheme, OAuth2, Implicit, Flow, Scopes};
92/// SecurityScheme::OAuth2(
93/// OAuth2::with_description([Flow::Implicit(
94/// Implicit::new(
95/// "https://localhost/auth/dialog",
96/// Scopes::from_iter([
97/// ("edit:items", "edit my items"),
98/// ("read:items", "read my items")
99/// ]),
100/// ),
101/// )], "my oauth2 flow")
102/// );
103/// ```
104///
105/// Create JWT header authentication.
106/// ```rust
107/// # use openapiv3_1::security::{SecurityScheme, HttpAuthScheme, Http};
108/// SecurityScheme::Http(
109/// Http::builder().scheme(HttpAuthScheme::Bearer).bearer_format("JWT").build()
110/// );
111/// ```
112#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
113#[serde(tag = "type", rename_all = "camelCase")]
114#[cfg_attr(feature = "debug", derive(Debug))]
115pub enum SecurityScheme {
116 /// Oauth flow authentication.
117 #[serde(rename = "oauth2")]
118 OAuth2(OAuth2),
119 /// Api key authentication sent in *`header`*, *`cookie`* or *`query`*.
120 ApiKey(ApiKey),
121 /// Http authentication such as *`bearer`* or *`basic`*.
122 Http(Http),
123 /// Open id connect url to discover OAuth2 configuration values.
124 OpenIdConnect(OpenIdConnect),
125 /// Authentication is done via client side certificate.
126 ///
127 /// OpenApi 3.1 type
128 #[serde(rename = "mutualTLS")]
129 MutualTls {
130 #[allow(missing_docs)]
131 #[serde(skip_serializing_if = "Option::is_none", default)]
132 description: Option<String>,
133 /// Optional extensions "x-something".
134 #[serde(skip_serializing_if = "Option::is_none", default, flatten)]
135 extensions: Option<Extensions>,
136 },
137}
138
139/// Api key authentication [`SecurityScheme`].
140#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
141#[serde(tag = "in", rename_all = "lowercase")]
142#[cfg_attr(feature = "debug", derive(Debug))]
143pub enum ApiKey {
144 /// Create api key which is placed in HTTP header.
145 Header(ApiKeyValue),
146 /// Create api key which is placed in query parameters.
147 Query(ApiKeyValue),
148 /// Create api key which is placed in cookie value.
149 Cookie(ApiKeyValue),
150}
151
152/// Value object for [`ApiKey`].
153#[non_exhaustive]
154#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, bon::Builder)]
155#[cfg_attr(feature = "debug", derive(Debug))]
156#[builder(on(_, into))]
157pub struct ApiKeyValue {
158 /// Name of the [`ApiKey`] parameter.
159 pub name: String,
160
161 /// Description of the the [`ApiKey`] [`SecurityScheme`]. Supports markdown syntax.
162 #[serde(skip_serializing_if = "Option::is_none", default)]
163 pub description: Option<String>,
164
165 /// Optional extensions "x-something".
166 #[serde(skip_serializing_if = "Option::is_none", default, flatten)]
167 pub extensions: Option<Extensions>,
168}
169
170impl ApiKeyValue {
171 /// Constructs new api key value.
172 ///
173 /// # Examples
174 ///
175 /// Create new api key security schema with name `api_key`.
176 /// ```rust
177 /// # use openapiv3_1::security::ApiKeyValue;
178 /// let api_key = ApiKeyValue::new("api_key");
179 /// ```
180 pub fn new<S: Into<String>>(name: S) -> Self {
181 Self {
182 name: name.into(),
183 description: None,
184 extensions: Default::default(),
185 }
186 }
187
188 /// Construct a new api key with optional description supporting markdown syntax.
189 ///
190 /// # Examples
191 ///
192 /// Create new api key security schema with name `api_key` with description.
193 /// ```rust
194 /// # use openapiv3_1::security::ApiKeyValue;
195 /// let api_key = ApiKeyValue::with_description("api_key", "my api_key token");
196 /// ```
197 pub fn with_description<S: Into<String>>(name: S, description: S) -> Self {
198 Self {
199 name: name.into(),
200 description: Some(description.into()),
201 extensions: Default::default(),
202 }
203 }
204}
205
206/// Http authentication [`SecurityScheme`] builder.
207///
208/// Methods can be chained to configure _bearer_format_ or to add _description_.
209#[non_exhaustive]
210#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, bon::Builder)]
211#[serde(rename_all = "camelCase")]
212#[cfg_attr(feature = "debug", derive(Debug))]
213#[builder(on(_, into))]
214pub struct Http {
215 /// Http authorization scheme in HTTP `Authorization` header value.
216 pub scheme: HttpAuthScheme,
217
218 /// Optional hint to client how the bearer token is formatted. Valid only with [`HttpAuthScheme::Bearer`].
219 #[serde(skip_serializing_if = "Option::is_none", default)]
220 pub bearer_format: Option<String>,
221
222 /// Optional description of [`Http`] [`SecurityScheme`] supporting markdown syntax.
223 #[serde(skip_serializing_if = "Option::is_none", default)]
224 pub description: Option<String>,
225
226 /// Optional extensions "x-something".
227 #[serde(skip_serializing_if = "Option::is_none", default, flatten)]
228 pub extensions: Option<Extensions>,
229}
230
231impl Http {
232 /// Create new http authentication security schema.
233 ///
234 /// Accepts one argument which defines the scheme of the http authentication.
235 ///
236 /// # Examples
237 ///
238 /// Create http security schema with basic authentication.
239 /// ```rust
240 /// # use openapiv3_1::security::{SecurityScheme, Http, HttpAuthScheme};
241 /// SecurityScheme::Http(Http::new(HttpAuthScheme::Basic));
242 /// ```
243 pub fn new(scheme: HttpAuthScheme) -> Self {
244 Self {
245 scheme,
246 bearer_format: None,
247 description: None,
248 extensions: Default::default(),
249 }
250 }
251}
252
253/// Implements types according [RFC7235](https://datatracker.ietf.org/doc/html/rfc7235#section-5.1).
254///
255/// Types are maintained at <https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml>.
256#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
257#[cfg_attr(feature = "debug", derive(Debug))]
258#[serde(rename_all = "lowercase")]
259#[allow(missing_docs)]
260pub enum HttpAuthScheme {
261 Basic,
262 Bearer,
263 Digest,
264 Hoba,
265 Mutual,
266 Negotiate,
267 OAuth,
268 #[serde(rename = "scram-sha-1")]
269 ScramSha1,
270 #[serde(rename = "scram-sha-256")]
271 ScramSha256,
272 Vapid,
273}
274
275impl Default for HttpAuthScheme {
276 fn default() -> Self {
277 Self::Basic
278 }
279}
280
281/// Open id connect [`SecurityScheme`].
282#[non_exhaustive]
283#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
284#[serde(rename_all = "camelCase")]
285#[cfg_attr(feature = "debug", derive(Debug))]
286pub struct OpenIdConnect {
287 /// Url of the [`OpenIdConnect`] to discover OAuth2 connect values.
288 pub open_id_connect_url: String,
289
290 /// Description of [`OpenIdConnect`] [`SecurityScheme`] supporting markdown syntax.
291 #[serde(skip_serializing_if = "Option::is_none")]
292 pub description: Option<String>,
293
294 /// Optional extensions "x-something".
295 #[serde(skip_serializing_if = "Option::is_none", flatten)]
296 pub extensions: Option<Extensions>,
297}
298
299impl OpenIdConnect {
300 /// Construct a new open id connect security schema.
301 ///
302 /// # Examples
303 ///
304 /// ```rust
305 /// # use openapiv3_1::security::OpenIdConnect;
306 /// OpenIdConnect::new("https://localhost/openid");
307 /// ```
308 pub fn new<S: Into<String>>(open_id_connect_url: S) -> Self {
309 Self {
310 open_id_connect_url: open_id_connect_url.into(),
311 description: None,
312 extensions: Default::default(),
313 }
314 }
315
316 /// Construct a new [`OpenIdConnect`] [`SecurityScheme`] with optional description
317 /// supporting markdown syntax.
318 ///
319 /// # Examples
320 ///
321 /// ```rust
322 /// # use openapiv3_1::security::OpenIdConnect;
323 /// OpenIdConnect::with_description("https://localhost/openid", "my pet api open id connect");
324 /// ```
325 pub fn with_description<S: Into<String>>(open_id_connect_url: S, description: S) -> Self {
326 Self {
327 open_id_connect_url: open_id_connect_url.into(),
328 description: Some(description.into()),
329 extensions: Default::default(),
330 }
331 }
332}
333
334/// OAuth2 [`Flow`] configuration for [`SecurityScheme`].
335#[non_exhaustive]
336#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
337#[cfg_attr(feature = "debug", derive(Debug))]
338pub struct OAuth2 {
339 /// Map of supported OAuth2 flows.
340 pub flows: IndexMap<String, Flow>,
341
342 /// Optional description for the [`OAuth2`] [`Flow`] [`SecurityScheme`].
343 #[serde(skip_serializing_if = "Option::is_none")]
344 pub description: Option<String>,
345
346 /// Optional extensions "x-something".
347 #[serde(skip_serializing_if = "Option::is_none", flatten)]
348 pub extensions: Option<Extensions>,
349}
350
351impl OAuth2 {
352 /// Construct a new OAuth2 security schema configuration object.
353 ///
354 /// Oauth flow accepts slice of [`Flow`] configuration objects and can be optionally provided with description.
355 ///
356 /// # Examples
357 ///
358 /// Create new OAuth2 flow with multiple authentication flows.
359 /// ```rust
360 /// # use openapiv3_1::security::{OAuth2, Flow, Password, AuthorizationCode, Scopes};
361 /// OAuth2::new([Flow::Password(
362 /// Password::with_refresh_url(
363 /// "https://localhost/oauth/token",
364 /// Scopes::from_iter([
365 /// ("edit:items", "edit my items"),
366 /// ("read:items", "read my items")
367 /// ]),
368 /// "https://localhost/refresh/token"
369 /// )),
370 /// Flow::AuthorizationCode(
371 /// AuthorizationCode::new(
372 /// "https://localhost/authorization/token",
373 /// "https://localhost/token/url",
374 /// Scopes::from_iter([
375 /// ("edit:items", "edit my items"),
376 /// ("read:items", "read my items")
377 /// ])),
378 /// ),
379 /// ]);
380 /// ```
381 pub fn new<I: IntoIterator<Item = Flow>>(flows: I) -> Self {
382 Self {
383 flows: IndexMap::from_iter(
384 flows
385 .into_iter()
386 .map(|auth_flow| (String::from(auth_flow.get_type_as_str()), auth_flow)),
387 ),
388 extensions: None,
389 description: None,
390 }
391 }
392
393 /// Construct a new OAuth2 flow with optional description supporting markdown syntax.
394 ///
395 /// # Examples
396 ///
397 /// Create new OAuth2 flow with multiple authentication flows with description.
398 /// ```rust
399 /// # use openapiv3_1::security::{OAuth2, Flow, Password, AuthorizationCode, Scopes};
400 /// OAuth2::with_description([Flow::Password(
401 /// Password::with_refresh_url(
402 /// "https://localhost/oauth/token",
403 /// Scopes::from_iter([
404 /// ("edit:items", "edit my items"),
405 /// ("read:items", "read my items")
406 /// ]),
407 /// "https://localhost/refresh/token"
408 /// )),
409 /// Flow::AuthorizationCode(
410 /// AuthorizationCode::new(
411 /// "https://localhost/authorization/token",
412 /// "https://localhost/token/url",
413 /// Scopes::from_iter([
414 /// ("edit:items", "edit my items"),
415 /// ("read:items", "read my items")
416 /// ])
417 /// ),
418 /// ),
419 /// ], "my oauth2 flow");
420 /// ```
421 pub fn with_description<I: IntoIterator<Item = Flow>, S: Into<String>>(flows: I, description: S) -> Self {
422 Self {
423 flows: IndexMap::from_iter(
424 flows
425 .into_iter()
426 .map(|auth_flow| (String::from(auth_flow.get_type_as_str()), auth_flow)),
427 ),
428 extensions: None,
429 description: Some(description.into()),
430 }
431 }
432}
433
434/// [`OAuth2`] flow configuration object.
435///
436/// See more details at <https://spec.openapis.org/oas/latest.html#oauth-flows-object>.
437#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
438#[serde(untagged)]
439#[cfg_attr(feature = "debug", derive(Debug))]
440pub enum Flow {
441 /// Define implicit [`Flow`] type. See [`Implicit::new`] for usage details.
442 ///
443 /// Soon to be deprecated by <https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics>.
444 Implicit(Implicit),
445 /// Define password [`Flow`] type. See [`Password::new`] for usage details.
446 Password(Password),
447 /// Define client credentials [`Flow`] type. See [`ClientCredentials::new`] for usage details.
448 ClientCredentials(ClientCredentials),
449 /// Define authorization code [`Flow`] type. See [`AuthorizationCode::new`] for usage details.
450 AuthorizationCode(AuthorizationCode),
451}
452
453impl Flow {
454 fn get_type_as_str(&self) -> &str {
455 match self {
456 Self::Implicit(_) => "implicit",
457 Self::Password(_) => "password",
458 Self::ClientCredentials(_) => "clientCredentials",
459 Self::AuthorizationCode(_) => "authorizationCode",
460 }
461 }
462}
463
464/// Implicit [`Flow`] configuration for [`OAuth2`].
465#[non_exhaustive]
466#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
467#[serde(rename_all = "camelCase")]
468#[cfg_attr(feature = "debug", derive(Debug))]
469pub struct Implicit {
470 /// Authorization token url for the flow.
471 pub authorization_url: String,
472
473 /// Optional refresh token url for the flow.
474 #[serde(skip_serializing_if = "Option::is_none")]
475 pub refresh_url: Option<String>,
476
477 /// Scopes required by the flow.
478 #[serde(flatten)]
479 pub scopes: Scopes,
480
481 /// Optional extensions "x-something".
482 #[serde(skip_serializing_if = "Option::is_none", flatten)]
483 pub extensions: Option<Extensions>,
484}
485
486impl Implicit {
487 /// Construct a new implicit oauth2 flow.
488 ///
489 /// Accepts two arguments: one which is authorization url and second map of scopes. Scopes can
490 /// also be an empty map.
491 ///
492 /// # Examples
493 ///
494 /// Create new implicit flow with scopes.
495 /// ```rust
496 /// # use openapiv3_1::security::{Implicit, Scopes};
497 /// Implicit::new(
498 /// "https://localhost/auth/dialog",
499 /// Scopes::from_iter([
500 /// ("edit:items", "edit my items"),
501 /// ("read:items", "read my items")
502 /// ]),
503 /// );
504 /// ```
505 ///
506 /// Create new implicit flow without any scopes.
507 /// ```rust
508 /// # use openapiv3_1::security::{Implicit, Scopes};
509 /// Implicit::new(
510 /// "https://localhost/auth/dialog",
511 /// Scopes::new(),
512 /// );
513 /// ```
514 pub fn new<S: Into<String>>(authorization_url: S, scopes: Scopes) -> Self {
515 Self {
516 authorization_url: authorization_url.into(),
517 refresh_url: None,
518 scopes,
519 extensions: Default::default(),
520 }
521 }
522
523 /// Construct a new implicit oauth2 flow with refresh url for getting refresh tokens.
524 ///
525 /// This is essentially same as [`Implicit::new`] but allows defining `refresh_url` for the [`Implicit`]
526 /// oauth2 flow.
527 ///
528 /// # Examples
529 ///
530 /// Create a new implicit oauth2 flow with refresh token.
531 /// ```rust
532 /// # use openapiv3_1::security::{Implicit, Scopes};
533 /// Implicit::with_refresh_url(
534 /// "https://localhost/auth/dialog",
535 /// Scopes::new(),
536 /// "https://localhost/refresh-token"
537 /// );
538 /// ```
539 pub fn with_refresh_url<S: Into<String>>(authorization_url: S, scopes: Scopes, refresh_url: S) -> Self {
540 Self {
541 authorization_url: authorization_url.into(),
542 refresh_url: Some(refresh_url.into()),
543 scopes,
544 extensions: Default::default(),
545 }
546 }
547}
548
549/// Authorization code [`Flow`] configuration for [`OAuth2`].
550#[non_exhaustive]
551#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
552#[serde(rename_all = "camelCase")]
553#[cfg_attr(feature = "debug", derive(Debug))]
554pub struct AuthorizationCode {
555 /// Url for authorization token.
556 pub authorization_url: String,
557 /// Token url for the flow.
558 pub token_url: String,
559
560 /// Optional refresh token url for the flow.
561 #[serde(skip_serializing_if = "Option::is_none")]
562 pub refresh_url: Option<String>,
563
564 /// Scopes required by the flow.
565 #[serde(flatten)]
566 pub scopes: Scopes,
567
568 /// Optional extensions "x-something".
569 #[serde(skip_serializing_if = "Option::is_none", flatten)]
570 pub extensions: Option<Extensions>,
571}
572
573impl AuthorizationCode {
574 /// Construct a new authorization code oauth flow.
575 ///
576 /// Accepts three arguments: one which is authorization url, two a token url and
577 /// three a map of scopes for oauth flow.
578 ///
579 /// # Examples
580 ///
581 /// Create new authorization code flow with scopes.
582 /// ```rust
583 /// # use openapiv3_1::security::{AuthorizationCode, Scopes};
584 /// AuthorizationCode::new(
585 /// "https://localhost/auth/dialog",
586 /// "https://localhost/token",
587 /// Scopes::from_iter([
588 /// ("edit:items", "edit my items"),
589 /// ("read:items", "read my items")
590 /// ]),
591 /// );
592 /// ```
593 ///
594 /// Create new authorization code flow without any scopes.
595 /// ```rust
596 /// # use openapiv3_1::security::{AuthorizationCode, Scopes};
597 /// AuthorizationCode::new(
598 /// "https://localhost/auth/dialog",
599 /// "https://localhost/token",
600 /// Scopes::new(),
601 /// );
602 /// ```
603 pub fn new<A: Into<String>, T: Into<String>>(authorization_url: A, token_url: T, scopes: Scopes) -> Self {
604 Self {
605 authorization_url: authorization_url.into(),
606 token_url: token_url.into(),
607 refresh_url: None,
608 scopes,
609 extensions: Default::default(),
610 }
611 }
612
613 /// Construct a new [`AuthorizationCode`] OAuth2 flow with additional refresh token url.
614 ///
615 /// This is essentially same as [`AuthorizationCode::new`] but allows defining extra parameter `refresh_url`
616 /// for fetching refresh token.
617 ///
618 /// # Examples
619 ///
620 /// Create [`AuthorizationCode`] OAuth2 flow with refresh url.
621 /// ```rust
622 /// # use openapiv3_1::security::{AuthorizationCode, Scopes};
623 /// AuthorizationCode::with_refresh_url(
624 /// "https://localhost/auth/dialog",
625 /// "https://localhost/token",
626 /// Scopes::new(),
627 /// "https://localhost/refresh-token"
628 /// );
629 /// ```
630 pub fn with_refresh_url<S: Into<String>>(authorization_url: S, token_url: S, scopes: Scopes, refresh_url: S) -> Self {
631 Self {
632 authorization_url: authorization_url.into(),
633 token_url: token_url.into(),
634 refresh_url: Some(refresh_url.into()),
635 scopes,
636 extensions: Default::default(),
637 }
638 }
639}
640
641/// Password [`Flow`] configuration for [`OAuth2`].
642#[non_exhaustive]
643#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
644#[serde(rename_all = "camelCase")]
645#[cfg_attr(feature = "debug", derive(Debug))]
646pub struct Password {
647 /// Token url for this OAuth2 flow. OAuth2 standard requires TLS.
648 pub token_url: String,
649
650 /// Optional refresh token url.
651 #[serde(skip_serializing_if = "Option::is_none")]
652 pub refresh_url: Option<String>,
653
654 /// Scopes required by the flow.
655 #[serde(flatten)]
656 pub scopes: Scopes,
657
658 /// Optional extensions "x-something".
659 #[serde(skip_serializing_if = "Option::is_none", flatten)]
660 pub extensions: Option<Extensions>,
661}
662
663impl Password {
664 /// Construct a new password oauth flow.
665 ///
666 /// Accepts two arguments: one which is a token url and
667 /// two a map of scopes for oauth flow.
668 ///
669 /// # Examples
670 ///
671 /// Create new password flow with scopes.
672 /// ```rust
673 /// # use openapiv3_1::security::{Password, Scopes};
674 /// Password::new(
675 /// "https://localhost/token",
676 /// Scopes::from_iter([
677 /// ("edit:items", "edit my items"),
678 /// ("read:items", "read my items")
679 /// ]),
680 /// );
681 /// ```
682 ///
683 /// Create new password flow without any scopes.
684 /// ```rust
685 /// # use openapiv3_1::security::{Password, Scopes};
686 /// Password::new(
687 /// "https://localhost/token",
688 /// Scopes::new(),
689 /// );
690 /// ```
691 pub fn new<S: Into<String>>(token_url: S, scopes: Scopes) -> Self {
692 Self {
693 token_url: token_url.into(),
694 refresh_url: None,
695 scopes,
696 extensions: Default::default(),
697 }
698 }
699
700 /// Construct a new password oauth flow with additional refresh url.
701 ///
702 /// This is essentially same as [`Password::new`] but allows defining third parameter for `refresh_url`
703 /// for fetching refresh tokens.
704 ///
705 /// # Examples
706 ///
707 /// Create new password flow with refresh url.
708 /// ```rust
709 /// # use openapiv3_1::security::{Password, Scopes};
710 /// Password::with_refresh_url(
711 /// "https://localhost/token",
712 /// Scopes::from_iter([
713 /// ("edit:items", "edit my items"),
714 /// ("read:items", "read my items")
715 /// ]),
716 /// "https://localhost/refres-token"
717 /// );
718 /// ```
719 pub fn with_refresh_url<S: Into<String>>(token_url: S, scopes: Scopes, refresh_url: S) -> Self {
720 Self {
721 token_url: token_url.into(),
722 refresh_url: Some(refresh_url.into()),
723 scopes,
724 extensions: Default::default(),
725 }
726 }
727}
728
729/// Client credentials [`Flow`] configuration for [`OAuth2`].
730#[non_exhaustive]
731#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
732#[serde(rename_all = "camelCase")]
733#[cfg_attr(feature = "debug", derive(Debug))]
734pub struct ClientCredentials {
735 /// Token url used for [`ClientCredentials`] flow. OAuth2 standard requires TLS.
736 pub token_url: String,
737
738 /// Optional refresh token url.
739 #[serde(skip_serializing_if = "Option::is_none")]
740 pub refresh_url: Option<String>,
741
742 /// Scopes required by the flow.
743 #[serde(flatten)]
744 pub scopes: Scopes,
745
746 /// Optional extensions "x-something".
747 #[serde(skip_serializing_if = "Option::is_none", flatten)]
748 pub extensions: Option<Extensions>,
749}
750
751impl ClientCredentials {
752 /// Construct a new client credentials oauth flow.
753 ///
754 /// Accepts two arguments: one which is a token url and
755 /// two a map of scopes for oauth flow.
756 ///
757 /// # Examples
758 ///
759 /// Create new client credentials flow with scopes.
760 /// ```rust
761 /// # use openapiv3_1::security::{ClientCredentials, Scopes};
762 /// ClientCredentials::new(
763 /// "https://localhost/token",
764 /// Scopes::from_iter([
765 /// ("edit:items", "edit my items"),
766 /// ("read:items", "read my items")
767 /// ]),
768 /// );
769 /// ```
770 ///
771 /// Create new client credentials flow without any scopes.
772 /// ```rust
773 /// # use openapiv3_1::security::{ClientCredentials, Scopes};
774 /// ClientCredentials::new(
775 /// "https://localhost/token",
776 /// Scopes::new(),
777 /// );
778 /// ```
779 pub fn new<S: Into<String>>(token_url: S, scopes: Scopes) -> Self {
780 Self {
781 token_url: token_url.into(),
782 refresh_url: None,
783 scopes,
784 extensions: Default::default(),
785 }
786 }
787
788 /// Construct a new client credentials oauth flow with additional refresh url.
789 ///
790 /// This is essentially same as [`ClientCredentials::new`] but allows defining third parameter for
791 /// `refresh_url`.
792 ///
793 /// # Examples
794 ///
795 /// Create new client credentials for with refresh url.
796 /// ```rust
797 /// # use openapiv3_1::security::{ClientCredentials, Scopes};
798 /// ClientCredentials::with_refresh_url(
799 /// "https://localhost/token",
800 /// Scopes::from_iter([
801 /// ("edit:items", "edit my items"),
802 /// ("read:items", "read my items")
803 /// ]),
804 /// "https://localhost/refresh-url"
805 /// );
806 /// ```
807 pub fn with_refresh_url<S: Into<String>>(token_url: S, scopes: Scopes, refresh_url: S) -> Self {
808 Self {
809 token_url: token_url.into(),
810 refresh_url: Some(refresh_url.into()),
811 scopes,
812 extensions: Default::default(),
813 }
814 }
815}
816
817/// [`OAuth2`] flow scopes object defines required permissions for oauth flow.
818///
819/// Scopes must be given to oauth2 flow but depending on need one of few initialization methods
820/// could be used.
821///
822/// * Create empty map of scopes you can use [`Scopes::new`].
823/// * Create map with only one scope you can use [`Scopes::one`].
824/// * Create multiple scopes from iterator with [`Scopes::from_iter`].
825///
826/// # Examples
827///
828/// Create empty map of scopes.
829/// ```rust
830/// # use openapiv3_1::security::Scopes;
831/// let scopes = Scopes::new();
832/// ```
833///
834/// Create [`Scopes`] holding one scope.
835/// ```rust
836/// # use openapiv3_1::security::Scopes;
837/// let scopes = Scopes::one("edit:item", "edit pets");
838/// ```
839///
840/// Create map of scopes from iterator.
841/// ```rust
842/// # use openapiv3_1::security::Scopes;
843/// let scopes = Scopes::from_iter([
844/// ("edit:items", "edit my items"),
845/// ("read:items", "read my items")
846/// ]);
847/// ```
848#[derive(Default, Serialize, Deserialize, Clone, PartialEq, Eq)]
849#[cfg_attr(feature = "debug", derive(Debug))]
850pub struct Scopes {
851 scopes: IndexMap<String, String>,
852}
853
854impl Scopes {
855 /// Construct new [`Scopes`] with empty map of scopes. This is useful if oauth flow does not need
856 /// any permission scopes.
857 ///
858 /// # Examples
859 ///
860 /// Create empty map of scopes.
861 /// ```rust
862 /// # use openapiv3_1::security::Scopes;
863 /// let scopes = Scopes::new();
864 /// ```
865 pub fn new() -> Self {
866 Self { ..Default::default() }
867 }
868
869 /// Construct new [`Scopes`] with holding one scope.
870 ///
871 /// * `scope` Is be the permission required.
872 /// * `description` Short description about the permission.
873 ///
874 /// # Examples
875 ///
876 /// Create map of scopes with one scope item.
877 /// ```rust
878 /// # use openapiv3_1::security::Scopes;
879 /// let scopes = Scopes::one("edit:item", "edit items");
880 /// ```
881 pub fn one<S: Into<String>>(scope: S, description: S) -> Self {
882 Self {
883 scopes: IndexMap::from_iter(iter::once_with(|| (scope.into(), description.into()))),
884 }
885 }
886}
887
888impl<I> FromIterator<(I, I)> for Scopes
889where
890 I: Into<String>,
891{
892 fn from_iter<T: IntoIterator<Item = (I, I)>>(iter: T) -> Self {
893 Self {
894 scopes: iter.into_iter().map(|(key, value)| (key.into(), value.into())).collect(),
895 }
896 }
897}
898
899#[cfg(test)]
900#[cfg_attr(coverage_nightly, coverage(off))]
901mod tests {
902 use super::*;
903
904 macro_rules! test_fn {
905 ($name:ident : $schema:expr; $expected:literal) => {
906 #[test]
907 fn $name() {
908 let value = serde_json::to_value($schema).unwrap();
909 let expected_value: serde_json::Value = serde_json::from_str($expected).unwrap();
910
911 assert_eq!(
912 value,
913 expected_value,
914 "testing serializing \"{}\": \nactual:\n{}\nexpected:\n{}",
915 stringify!($name),
916 value,
917 expected_value
918 );
919
920 println!("{}", &serde_json::to_string_pretty(&$schema).unwrap());
921 }
922 };
923 }
924
925 test_fn! {
926 security_scheme_correct_http_bearer_json:
927 SecurityScheme::Http(
928 Http::builder().scheme(HttpAuthScheme::Bearer).bearer_format("JWT").build()
929 );
930 r###"{
931 "type": "http",
932 "scheme": "bearer",
933 "bearerFormat": "JWT"
934}"###
935 }
936
937 test_fn! {
938 security_scheme_correct_basic_auth:
939 SecurityScheme::Http(Http::new(HttpAuthScheme::Basic));
940 r###"{
941 "type": "http",
942 "scheme": "basic"
943}"###
944 }
945
946 test_fn! {
947 security_scheme_correct_digest_auth:
948 SecurityScheme::Http(Http::new(HttpAuthScheme::Digest));
949 r###"{
950 "type": "http",
951 "scheme": "digest"
952}"###
953 }
954
955 test_fn! {
956 security_scheme_correct_hoba_auth:
957 SecurityScheme::Http(Http::new(HttpAuthScheme::Hoba));
958 r###"{
959 "type": "http",
960 "scheme": "hoba"
961}"###
962 }
963
964 test_fn! {
965 security_scheme_correct_mutual_auth:
966 SecurityScheme::Http(Http::new(HttpAuthScheme::Mutual));
967 r###"{
968 "type": "http",
969 "scheme": "mutual"
970}"###
971 }
972
973 test_fn! {
974 security_scheme_correct_negotiate_auth:
975 SecurityScheme::Http(Http::new(HttpAuthScheme::Negotiate));
976 r###"{
977 "type": "http",
978 "scheme": "negotiate"
979}"###
980 }
981
982 test_fn! {
983 security_scheme_correct_oauth_auth:
984 SecurityScheme::Http(Http::new(HttpAuthScheme::OAuth));
985 r###"{
986 "type": "http",
987 "scheme": "oauth"
988}"###
989 }
990
991 test_fn! {
992 security_scheme_correct_scram_sha1_auth:
993 SecurityScheme::Http(Http::new(HttpAuthScheme::ScramSha1));
994 r###"{
995 "type": "http",
996 "scheme": "scram-sha-1"
997}"###
998 }
999
1000 test_fn! {
1001 security_scheme_correct_scram_sha256_auth:
1002 SecurityScheme::Http(Http::new(HttpAuthScheme::ScramSha256));
1003 r###"{
1004 "type": "http",
1005 "scheme": "scram-sha-256"
1006}"###
1007 }
1008
1009 test_fn! {
1010 security_scheme_correct_api_key_cookie_auth:
1011 SecurityScheme::ApiKey(ApiKey::Cookie(ApiKeyValue::new(String::from("api_key"))));
1012 r###"{
1013 "type": "apiKey",
1014 "name": "api_key",
1015 "in": "cookie"
1016}"###
1017 }
1018
1019 test_fn! {
1020 security_scheme_correct_api_key_header_auth:
1021 SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("api_key")));
1022 r###"{
1023 "type": "apiKey",
1024 "name": "api_key",
1025 "in": "header"
1026}"###
1027 }
1028
1029 test_fn! {
1030 security_scheme_correct_api_key_query_auth:
1031 SecurityScheme::ApiKey(ApiKey::Query(ApiKeyValue::new(String::from("api_key"))));
1032 r###"{
1033 "type": "apiKey",
1034 "name": "api_key",
1035 "in": "query"
1036}"###
1037 }
1038
1039 test_fn! {
1040 security_scheme_correct_open_id_connect_auth:
1041 SecurityScheme::OpenIdConnect(OpenIdConnect::new("https://localhost/openid"));
1042 r###"{
1043 "type": "openIdConnect",
1044 "openIdConnectUrl": "https://localhost/openid"
1045}"###
1046 }
1047
1048 test_fn! {
1049 security_scheme_correct_oauth2_implicit:
1050 SecurityScheme::OAuth2(
1051 OAuth2::with_description([Flow::Implicit(
1052 Implicit::new(
1053 "https://localhost/auth/dialog",
1054 Scopes::from_iter([
1055 ("edit:items", "edit my items"),
1056 ("read:items", "read my items")
1057 ]),
1058 ),
1059 )], "my oauth2 flow")
1060 );
1061 r###"{
1062 "type": "oauth2",
1063 "flows": {
1064 "implicit": {
1065 "authorizationUrl": "https://localhost/auth/dialog",
1066 "scopes": {
1067 "edit:items": "edit my items",
1068 "read:items": "read my items"
1069 }
1070 }
1071 },
1072 "description": "my oauth2 flow"
1073}"###
1074 }
1075
1076 test_fn! {
1077 security_scheme_correct_oauth2_password:
1078 SecurityScheme::OAuth2(
1079 OAuth2::with_description([Flow::Password(
1080 Password::with_refresh_url(
1081 "https://localhost/oauth/token",
1082 Scopes::from_iter([
1083 ("edit:items", "edit my items"),
1084 ("read:items", "read my items")
1085 ]),
1086 "https://localhost/refresh/token"
1087 ),
1088 )], "my oauth2 flow")
1089 );
1090 r###"{
1091 "type": "oauth2",
1092 "flows": {
1093 "password": {
1094 "tokenUrl": "https://localhost/oauth/token",
1095 "refreshUrl": "https://localhost/refresh/token",
1096 "scopes": {
1097 "edit:items": "edit my items",
1098 "read:items": "read my items"
1099 }
1100 }
1101 },
1102 "description": "my oauth2 flow"
1103}"###
1104 }
1105
1106 test_fn! {
1107 security_scheme_correct_oauth2_client_credentials:
1108 SecurityScheme::OAuth2(
1109 OAuth2::new([Flow::ClientCredentials(
1110 ClientCredentials::with_refresh_url(
1111 "https://localhost/oauth/token",
1112 Scopes::from_iter([
1113 ("edit:items", "edit my items"),
1114 ("read:items", "read my items")
1115 ]),
1116 "https://localhost/refresh/token"
1117 ),
1118 )])
1119 );
1120 r###"{
1121 "type": "oauth2",
1122 "flows": {
1123 "clientCredentials": {
1124 "tokenUrl": "https://localhost/oauth/token",
1125 "refreshUrl": "https://localhost/refresh/token",
1126 "scopes": {
1127 "edit:items": "edit my items",
1128 "read:items": "read my items"
1129 }
1130 }
1131 }
1132}"###
1133 }
1134
1135 test_fn! {
1136 security_scheme_correct_oauth2_authorization_code:
1137 SecurityScheme::OAuth2(
1138 OAuth2::new([Flow::AuthorizationCode(
1139 AuthorizationCode::with_refresh_url(
1140 "https://localhost/authorization/token",
1141 "https://localhost/token/url",
1142 Scopes::from_iter([
1143 ("edit:items", "edit my items"),
1144 ("read:items", "read my items")
1145 ]),
1146 "https://localhost/refresh/token"
1147 ),
1148 )])
1149 );
1150 r###"{
1151 "type": "oauth2",
1152 "flows": {
1153 "authorizationCode": {
1154 "authorizationUrl": "https://localhost/authorization/token",
1155 "tokenUrl": "https://localhost/token/url",
1156 "refreshUrl": "https://localhost/refresh/token",
1157 "scopes": {
1158 "edit:items": "edit my items",
1159 "read:items": "read my items"
1160 }
1161 }
1162 }
1163}"###
1164 }
1165
1166 test_fn! {
1167 security_scheme_correct_oauth2_authorization_code_no_scopes:
1168 SecurityScheme::OAuth2(
1169 OAuth2::new([Flow::AuthorizationCode(
1170 AuthorizationCode::with_refresh_url(
1171 "https://localhost/authorization/token",
1172 "https://localhost/token/url",
1173 Scopes::new(),
1174 "https://localhost/refresh/token"
1175 ),
1176 )])
1177 );
1178 r###"{
1179 "type": "oauth2",
1180 "flows": {
1181 "authorizationCode": {
1182 "authorizationUrl": "https://localhost/authorization/token",
1183 "tokenUrl": "https://localhost/token/url",
1184 "refreshUrl": "https://localhost/refresh/token",
1185 "scopes": {}
1186 }
1187 }
1188}"###
1189 }
1190
1191 test_fn! {
1192 security_scheme_correct_mutual_tls:
1193 SecurityScheme::MutualTls {
1194 description: Some(String::from("authorization is performed with client side certificate")),
1195 extensions: None,
1196 };
1197 r###"{
1198 "type": "mutualTLS",
1199 "description": "authorization is performed with client side certificate"
1200}"###
1201 }
1202}