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}